Skip to content

Commit 143daa5

Browse files
committed
Added remove node logic
1 parent d6544af commit 143daa5

File tree

4 files changed

+115
-41
lines changed

4 files changed

+115
-41
lines changed

custom_components/dmx/__init__.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
from custom_components.dmx.bridge.artnet_controller import ArtNetController, DiscoveredNode
2424
from custom_components.dmx.client import PortAddress, ArtPollReply
25-
from custom_components.dmx.client.artnet_server import ArtNetServer
25+
from custom_components.dmx.client.artnet_server import ArtNetServer, Node
2626
from custom_components.dmx.const import DOMAIN, HASS_DATA_ENTITIES, ARTNET_CONTROLLER, CONF_DATA, UNDO_UPDATE_LISTENER
2727
from custom_components.dmx.fixture.fixture import Fixture
2828
from custom_components.dmx.fixture.parser import parse
@@ -309,15 +309,20 @@ def _get_node_handler():
309309
DynamicNodeHandler = _get_node_handler()
310310
node_handler = DynamicNodeHandler(hass, entry, controller)
311311

312-
def new_code_callback(artpoll_reply: ArtPollReply):
312+
def node_new_callback(artpoll_reply: ArtPollReply):
313313
hass.async_create_task(node_handler.handle_new_node(artpoll_reply))
314314

315-
controller.new_node_callback = new_code_callback
315+
controller.node_new_callback = node_new_callback
316316

317-
def existing_node_callback(artpoll_reply: ArtPollReply):
317+
def node_update_callback(artpoll_reply: ArtPollReply):
318318
hass.async_create_task(node_handler.update_node(artpoll_reply))
319319

320-
controller.art_poll_reply_callback = existing_node_callback
320+
controller.node_update_callback = node_update_callback
321+
322+
def node_lost_callback(node: Node):
323+
hass.async_create_task(node_handler.disable_node(node))
324+
325+
controller.node_lost_callback = node_lost_callback
321326

322327
for universe_dict in artnet_yaml[CONF_UNIVERSES]:
323328
(universe_str, universe_yaml), = universe_dict.items()

custom_components/dmx/client/artnet_server.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
class Node:
3535
addr: bytes = [0x00] * 4,
3636
bind_index: int = 0,
37+
mac_address: bytes = [0x00] * 6,
3738

3839
last_seen: datetime.datetime = datetime.datetime.now(),
3940
net_switch: int = 0,
@@ -84,8 +85,11 @@ def __init__(self, hass: HomeAssistant, state_update_callback=None,
8485

8586
self.__hass = hass
8687
self.__state_update_callback = state_update_callback
87-
self.new_node_callback = None
88-
self.art_poll_reply_callback = None
88+
89+
self.node_new_callback = None
90+
self.node_update_callback = None
91+
self.node_lost_callback = None
92+
8993
self.firmware_version = firmware_version
9094
self.oem = oem
9195
self.esta = esta
@@ -161,9 +165,6 @@ def add_node_by_port_address(self, port_address: PortAddress, node: Node):
161165
else:
162166
self.nodes_by_port_address[port_address] = {node}
163167

164-
def remove_node_by_ip(self, addr: bytes, bind_index: int = 1):
165-
del self.nodes_by_ip[addr, bind_index]
166-
167168
def remove_node_by_port_address(self, port_address: PortAddress, node: Node):
168169
nodes = self.nodes_by_port_address[port_address]
169170
if not nodes:
@@ -177,6 +178,7 @@ def remove_node_by_port_address(self, port_address: PortAddress, node: Node):
177178
ip_str = inet_ntoa(node.addr)
178179
if ip_str in self.node_change_subscribers:
179180
self.node_change_subscribers.remove(ip_str)
181+
self.node_lost_callback(node)
180182

181183
def update_subscribers(self):
182184
for subscriber in self.node_change_subscribers:
@@ -491,25 +493,26 @@ def handle_poll_reply(self, addr, reply):
491493
# Maintain data structures
492494
bind_index = reply.bind_index
493495
node = self.get_node_by_ip(source_ip, bind_index)
496+
mac_address = reply.mac_address
494497

495498
current_time = datetime.datetime.now()
496499
if not node:
497-
node = Node(source_ip, bind_index, current_time)
500+
node = Node(source_ip, bind_index, mac_address, current_time)
498501
self.add_node_by_ip(node, source_ip, bind_index)
499502
log.info(f"Discovered new node at {inet_ntoa(source_ip)}@{bind_index} with "
500503
f"{reply.net_switch}:{reply.sub_switch}:[{','.join([str(p.sw_out) for p in reply.ports if p.output])}]"
501504
)
502505

503-
if self.new_node_callback:
504-
self.new_node_callback(reply)
506+
if self.node_new_callback:
507+
self.node_new_callback(reply)
505508

506509
else:
507510
node.last_seen = current_time
508511
log.debug(f"Existing node checking in {inet_ntoa(source_ip)}@{bind_index} with "
509512
f"{reply.net_switch}:{reply.sub_switch}:[{','.join([str(p.sw_out) for p in reply.ports])}]"
510513
)
511-
if self.art_poll_reply_callback:
512-
self.art_poll_reply_callback(reply)
514+
if self.node_update_callback:
515+
self.node_update_callback(reply)
513516

514517
old_addresses = node.get_addresses()
515518
node.net_switch = reply.net_switch

custom_components/dmx/entity/node.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@
88
from homeassistant.components.sensor import SensorEntity, SensorDeviceClass
99
from homeassistant.const import EntityCategory
1010
from homeassistant.helpers.entity import DeviceInfo
11-
from homeassistant.helpers.entity_platform import EntityPlatform
1211

1312
from custom_components.dmx import ArtPollReply
1413
from custom_components.dmx.client import StyleCode, IndicatorState, BootProcess, FailsafeState, PortAddressProgrammingAuthority
1514

1615
_LOGGER = logging.getLogger(__name__)
1716

17+
def bind_index_str(artpoll_reply: ArtPollReply):
18+
if artpoll_reply.bind_index == 0:
19+
return ""
20+
return f" {artpoll_reply.bind_index}"
21+
1822

1923
class ArtNetEntity:
2024
"""Representation of an ArtNet entity."""
@@ -47,14 +51,14 @@ class ArtNetOnlineBinarySensor(ArtNetEntity, BinarySensorEntity):
4751

4852
def __init__(self, art_poll_reply, device_info: DeviceInfo):
4953
"""Initialize the binary sensor."""
50-
super().__init__(art_poll_reply, f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Online", "online", device_info)
54+
super().__init__(art_poll_reply, f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Online", "online", device_info)
5155
self._attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
56+
self.connected = True
5257

5358
@property
5459
def is_on(self) -> bool:
5560
"""Return true if the binary sensor is on."""
56-
# TODO
57-
return True
61+
return self.connected
5862

5963
@property
6064
def extra_state_attributes(self) -> Dict[str, Any]:
@@ -77,7 +81,7 @@ class ArtNetIndicatorStateSensor(ArtNetEntity, SensorEntity):
7781

7882
def __init__(self, art_poll_reply, device_info: DeviceInfo):
7983
"""Initialize the sensor."""
80-
super().__init__(art_poll_reply, f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Indicator", "indicator", device_info)
84+
super().__init__(art_poll_reply, f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Indicator", "indicator", device_info)
8185
self._attr_device_class = SensorDeviceClass.ENUM
8286
self._attr_icon = "mdi:lightbulb"
8387
self._attr_options = [member.name for member in IndicatorState]
@@ -98,7 +102,7 @@ class ArtNetBootProcessSensor(ArtNetEntity, SensorEntity):
98102

99103
def __init__(self, art_poll_reply, device_info: DeviceInfo):
100104
"""Initialize the sensor."""
101-
super().__init__(art_poll_reply, f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Boot Process", "boot_process", device_info)
105+
super().__init__(art_poll_reply, f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Boot Process", "boot_process", device_info)
102106
self._attr_device_class = SensorDeviceClass.ENUM
103107
self._attr_options = [member.name for member in BootProcess]
104108
self._attr_entity_category = EntityCategory.DIAGNOSTIC
@@ -118,7 +122,7 @@ class ArtNetRDMBinarySensor(ArtNetEntity, BinarySensorEntity):
118122

119123
def __init__(self, art_poll_reply, device_info: DeviceInfo):
120124
"""Initialize the binary sensor."""
121-
super().__init__(art_poll_reply, f"{art_poll_reply.short_name} {art_poll_reply.bind_index} RDM Support", "rdm_support", device_info)
125+
super().__init__(art_poll_reply, f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} RDM Support", "rdm_support", device_info)
122126
self._attr_entity_category = EntityCategory.DIAGNOSTIC
123127

124128
@property
@@ -143,7 +147,7 @@ class ArtNetDHCPBinarySensor(ArtNetEntity, BinarySensorEntity):
143147

144148
def __init__(self, art_poll_reply, device_info: DeviceInfo):
145149
"""Initialize the binary sensor."""
146-
super().__init__(art_poll_reply, f"{art_poll_reply.short_name} {art_poll_reply.bind_index} DHCP", "dhcp", device_info)
150+
super().__init__(art_poll_reply, f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} DHCP", "dhcp", device_info)
147151
self._attr_icon = "mdi:network"
148152
self._attr_entity_category = EntityCategory.DIAGNOSTIC
149153

@@ -173,7 +177,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
173177
self.port = art_poll_reply.ports[port_index]
174178
super().__init__(
175179
art_poll_reply,
176-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} Input Active",
180+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} Input Active",
177181
f"port_{port_index}_input_active",
178182
device_info
179183
)
@@ -220,7 +224,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
220224
self.port = art_poll_reply.ports[port_index]
221225
super().__init__(
222226
art_poll_reply,
223-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} Output Active",
227+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} Output Active",
224228
f"port_{port_index}_output_active",
225229
device_info
226230
)
@@ -267,7 +271,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
267271
self.port = art_poll_reply.ports[port_index]
268272
super().__init__(
269273
art_poll_reply,
270-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} Merge Mode",
274+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} Merge Mode",
271275
f"port_{port_index}_merge_mode",
272276
device_info
273277
)
@@ -308,7 +312,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
308312
self.port = art_poll_reply.ports[port_index]
309313
super().__init__(
310314
art_poll_reply,
311-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} sACN Mode",
315+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} sACN Mode",
312316
f"port_{port_index}_sacn_mode",
313317
device_info
314318
)
@@ -343,7 +347,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
343347
self.port = art_poll_reply.ports[port_index]
344348
super().__init__(
345349
art_poll_reply,
346-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} RDM",
350+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} RDM",
347351
f"port_{port_index}_rdm",
348352
device_info
349353
)
@@ -378,7 +382,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index):
378382
self.port = art_poll_reply.ports[port_index]
379383
super().__init__(
380384
art_poll_reply,
381-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} Output Mode",
385+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} Output Mode",
382386
f"port_{port_index}_output_mode",
383387
device_info
384388
)
@@ -413,7 +417,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo, port_index, is_input
413417
direction = "Input" if is_input else "Output"
414418
super().__init__(
415419
art_poll_reply,
416-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port {port_index + 1} {direction} Universe",
420+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port {port_index + 1} {direction} Universe",
417421
f"port_{port_index}_{direction.lower()}_universe",
418422
device_info
419423
)
@@ -443,7 +447,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo):
443447
"""Initialize the sensor."""
444448
super().__init__(
445449
art_poll_reply,
446-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Failsafe State",
450+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Failsafe State",
447451
"failsafe_state",
448452
device_info
449453
)
@@ -468,7 +472,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo):
468472
"""Initialize the sensor."""
469473
super().__init__(
470474
art_poll_reply,
471-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} ACN Priority",
475+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} ACN Priority",
472476
"acn_priority",
473477
device_info
474478
)
@@ -491,7 +495,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo):
491495
"""Initialize the sensor."""
492496
super().__init__(
493497
art_poll_reply,
494-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Node Report",
498+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Node Report",
495499
"node_report",
496500
device_info
497501
)
@@ -514,7 +518,7 @@ def __init__(self, art_poll_reply, device_info: DeviceInfo):
514518
"""Initialize the sensor."""
515519
super().__init__(
516520
art_poll_reply,
517-
f"{art_poll_reply.short_name} {art_poll_reply.bind_index} Port Programming Authority",
521+
f"{art_poll_reply.short_name}{bind_index_str(art_poll_reply)} Port Programming Authority",
518522
"port_programming_authority",
519523
device_info
520524
)

custom_components/dmx/entity/node_handler.py

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
from homeassistant.core import HomeAssistant
66
from homeassistant.helpers.entity import DeviceInfo
77
from homeassistant.helpers.entity_platform import async_get_platforms
8+
from homeassistant.helpers import entity_registry as er
9+
from homeassistant.helpers.entity_registry import RegistryEntryDisabler
810

9-
from custom_components.dmx import ArtPollReply, DOMAIN, PortAddress, DmxUniverse
11+
from custom_components.dmx import ArtPollReply, DOMAIN, Node
1012
from custom_components.dmx.entity.node import ArtNetOnlineBinarySensor, ArtNetIndicatorStateSensor, ArtNetBootProcessSensor, ArtNetRDMBinarySensor, ArtNetDHCPBinarySensor, \
1113
ArtNetFailsafeStateSensor, ArtNetACNPrioritySensor, ArtNetNodeReportSensor, ArtNetPortAddressProgrammingAuthoritySensor, ArtNetPortInputBinarySensor, \
1214
ArtNetPortUniverseSensor, ArtNetPortOutputBinarySensor, ArtNetPortMergeModeSelect, ArtNetPortSACNBinarySensor, ArtNetPortRDMBinarySensor, ArtNetPortOutputModeSensor
1315

16+
NODE_ENTITIES = "node_entities"
17+
1418
log = logging.getLogger(__name__)
1519

1620

@@ -29,6 +33,8 @@ async def handle_new_node(self, artpoll_reply: ArtPollReply) -> None:
2933
unique_id = f"{artpoll_reply.mac_address}{artpoll_reply.bind_index}"
3034

3135
if unique_id in self.discovered_nodes:
36+
# Previously disabled, but found again
37+
await self.reenable_node(artpoll_reply)
3238
return
3339

3440
log.info(f"Discovered new ArtNet node: {artpoll_reply.short_name}")
@@ -87,8 +93,8 @@ async def update_node(self, artpoll_reply: ArtPollReply) -> None:
8793
self.discovered_nodes[unique_id] = artpoll_reply
8894

8995
# Find all entities for this node and update their art_poll_reply reference
90-
if "entities" in self.hass.data[DOMAIN][self.entry.entry_id]:
91-
entities = self.hass.data[DOMAIN][self.entry.entry_id]["entities"]
96+
if NODE_ENTITIES in self.hass.data[DOMAIN][self.entry.entry_id]:
97+
entities = self.hass.data[DOMAIN][self.entry.entry_id][NODE_ENTITIES]
9298

9399
# Find entities belonging to this node and update them
94100
mac_string = ":".join(f"{b:02x}" for b in artpoll_reply.mac_address)
@@ -105,14 +111,70 @@ async def update_node(self, artpoll_reply: ArtPollReply) -> None:
105111
if hasattr(entity, "async_schedule_update_ha_state"):
106112
entity.async_schedule_update_ha_state()
107113

108-
log.debug(f"Updated ArtNet node: {artpoll_reply.long_name})")
114+
log.debug(f"Updated ArtNet node: {artpoll_reply.long_name}")
115+
116+
async def disable_node(self, node: Node) -> None:
117+
unique_id = f"{node.mac_address}{node.bind_index}"
118+
119+
if unique_id not in self.discovered_nodes:
120+
return
121+
122+
entity_reg = er.async_get(self.hass)
123+
124+
if NODE_ENTITIES in self.hass.data[DOMAIN][self.entry.entry_id]:
125+
entities = self.hass.data[DOMAIN][self.entry.entry_id][NODE_ENTITIES]
126+
127+
# Find entities belonging to this node and update them
128+
mac_string = ":".join(f"{b:02x}" for b in node.mac_address)
129+
for entity in entities:
130+
# Check if this entity belongs to the node being updated
131+
if hasattr(entity, "_mac_address") and entity._mac_address == mac_string and \
132+
hasattr(entity, "art_poll_reply") and \
133+
entity.art_poll_reply.bind_index == node.bind_index:
134+
135+
if isinstance(entity, ArtNetOnlineBinarySensor):
136+
entity.connected = False
137+
138+
if hasattr(entity, "async_schedule_update_ha_state"):
139+
entity.async_schedule_update_ha_state()
140+
continue
141+
142+
entity_reg.async_update_entity(entity.registry_entry, disabled_by=RegistryEntryDisabler.INTEGRATION)
143+
144+
async def reenable_node(self, artpoll_reply: ArtPollReply) -> None:
145+
unique_id = f"{artpoll_reply.mac_address}{artpoll_reply.bind_index}"
146+
147+
if unique_id not in self.discovered_nodes:
148+
return
149+
150+
entity_reg = er.async_get(self.hass)
151+
152+
if NODE_ENTITIES in self.hass.data[DOMAIN][self.entry.entry_id]:
153+
entities = self.hass.data[DOMAIN][self.entry.entry_id][NODE_ENTITIES]
154+
155+
# Find entities belonging to this node and update them
156+
mac_string = ":".join(f"{b:02x}" for b in artpoll_reply.mac_address)
157+
for entity in entities:
158+
# Check if this entity belongs to the node being updated
159+
if hasattr(entity, "_mac_address") and entity._mac_address == mac_string and \
160+
hasattr(entity, "art_poll_reply") and \
161+
entity.art_poll_reply.bind_index == artpoll_reply.bind_index:
162+
163+
if isinstance(entity, ArtNetOnlineBinarySensor):
164+
entity.connected = True
165+
166+
if hasattr(entity, "async_schedule_update_ha_state"):
167+
entity.async_schedule_update_ha_state()
168+
continue
169+
170+
entity_reg.async_update_entity(entity.registry_entry, enabled_by=RegistryEntryDisabler.INTEGRATION)
109171

110172
async def _add_entities(self, entities) -> None:
111173
"""Add entities to Home Assistant."""
112-
if "entities" not in self.hass.data[DOMAIN][self.entry.entry_id]:
113-
self.hass.data[DOMAIN][self.entry.entry_id]["entities"] = []
174+
if NODE_ENTITIES not in self.hass.data[DOMAIN][self.entry.entry_id]:
175+
self.hass.data[DOMAIN][self.entry.entry_id][NODE_ENTITIES] = []
114176

115-
self.hass.data[DOMAIN][self.entry.entry_id]["entities"].extend(entities)
177+
self.hass.data[DOMAIN][self.entry.entry_id][NODE_ENTITIES].extend(entities)
116178

117179
for platform in async_get_platforms(self.hass, DOMAIN):
118180
platform_entities = [e for e in entities if e.platform_type == platform.domain]

0 commit comments

Comments
 (0)