1 """Support for ISY binary sensors."""
3 from __future__
import annotations
5 from datetime
import datetime, timedelta
8 from pyisy.constants
import (
15 from pyisy.helpers
import NodeProperty
16 from pyisy.nodes
import Group, Node
19 BinarySensorDeviceClass,
32 BINARY_SENSOR_DEVICE_TYPES_ISY,
33 BINARY_SENSOR_DEVICE_TYPES_ZWAVE,
40 SUBNODE_MOTION_DISABLED,
43 TYPE_CATEGORY_CLIMATE,
46 from .entity
import ISYNodeEntity, ISYProgramEntity
47 from .models
import IsyData
49 DEVICE_PARENT_REQUIRED = [
50 BinarySensorDeviceClass.OPENING,
51 BinarySensorDeviceClass.MOISTURE,
52 BinarySensorDeviceClass.MOTION,
57 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
59 """Set up the ISY binary sensor platform."""
61 ISYInsteonBinarySensorEntity
62 | ISYBinarySensorEntity
63 | ISYBinarySensorHeartbeat
64 | ISYBinarySensorProgramEntity
66 entities_by_address: dict[
68 ISYInsteonBinarySensorEntity
69 | ISYBinarySensorEntity
70 | ISYBinarySensorHeartbeat
71 | ISYBinarySensorProgramEntity,
74 tuple[Node, BinarySensorDeviceClass |
None, str |
None, DeviceInfo |
None]
77 ISYInsteonBinarySensorEntity
78 | ISYBinarySensorEntity
79 | ISYBinarySensorHeartbeat
80 | ISYBinarySensorProgramEntity
83 isy_data: IsyData = hass.data[DOMAIN][entry.entry_id]
84 devices: dict[str, DeviceInfo] = isy_data.devices
85 for node
in isy_data.nodes[Platform.BINARY_SENSOR]:
86 assert isinstance(node, Node)
87 device_info = devices.get(node.primary_node)
89 if node.protocol == PROTO_INSTEON:
90 if node.parent_node
is not None:
93 child_nodes.append((node, device_class, device_type, device_info))
96 node, device_class, device_info=device_info
100 entities.append(entity)
101 entities_by_address[node.address] = entity
104 for node, device_class, device_type, device_info
in child_nodes:
105 subnode_id =
int(node.address.split(
" ")[-1], 16)
107 if device_type
is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE):
108 if subnode_id == SUBNODE_CLIMATE_COOL:
115 node, BinarySensorDeviceClass.COLD,
False, device_info=device_info
117 entities.append(entity)
118 elif subnode_id == SUBNODE_CLIMATE_HEAT:
121 node, BinarySensorDeviceClass.HEAT,
False, device_info=device_info
123 entities.append(entity)
126 if device_class
in DEVICE_PARENT_REQUIRED:
127 parent_entity = entities_by_address.get(node.parent_node.address)
128 if not parent_entity:
131 "Node %s has a parent node %s, but no device "
132 "was created for the parent. Skipping"
140 BinarySensorDeviceClass.OPENING,
141 BinarySensorDeviceClass.MOISTURE,
145 if subnode_id == SUBNODE_NEGATIVE:
146 assert isinstance(parent_entity, ISYInsteonBinarySensorEntity)
147 parent_entity.add_negative_node(node)
148 elif subnode_id == SUBNODE_HEARTBEAT:
149 assert isinstance(parent_entity, ISYInsteonBinarySensorEntity)
153 node, parent_entity, device_info=device_info
155 parent_entity.add_heartbeat_device(entity)
156 entities.append(entity)
159 device_class == BinarySensorDeviceClass.MOTION
160 and device_type
is not None
161 and any(device_type.startswith(t)
for t
in TYPE_INSTEON_MOTION)
168 assert isinstance(parent_entity, ISYInsteonBinarySensorEntity)
169 initial_state =
None if parent_entity.state
is None else False
170 if subnode_id == SUBNODE_DUSK_DAWN:
173 node, BinarySensorDeviceClass.LIGHT, device_info=device_info
175 entities.append(entity)
177 if subnode_id == SUBNODE_LOW_BATTERY:
181 BinarySensorDeviceClass.BATTERY,
183 device_info=device_info,
185 entities.append(entity)
187 if subnode_id
in SUBNODE_TAMPER:
192 BinarySensorDeviceClass.PROBLEM,
194 device_info=device_info,
196 entities.append(entity)
198 if subnode_id
in SUBNODE_MOTION_DISABLED:
201 entities.append(entity)
207 node, force_device_class=device_class, device_info=device_info
209 entities.append(entity)
211 for name, status, _
in isy_data.programs[Platform.BINARY_SENSOR]:
219 ) -> tuple[BinarySensorDeviceClass |
None, str |
None]:
221 device_type = node.type
222 except AttributeError:
227 if node.protocol == PROTO_ZWAVE:
228 device_type = f
"Z{node.zwave_props.category}"
229 for device_class, values
in BINARY_SENSOR_DEVICE_TYPES_ZWAVE.items():
230 if node.zwave_props.category
in values:
231 return device_class, device_type
232 return (
None, device_type)
235 for device_class, values
in BINARY_SENSOR_DEVICE_TYPES_ISY.items():
236 if any(device_type.startswith(t)
for t
in values):
237 return device_class, device_type
238 return (
None, device_type)
242 """Representation of a basic ISY binary sensor device."""
247 force_device_class: BinarySensorDeviceClass |
None =
None,
248 unknown_state: bool |
None =
None,
249 device_info: DeviceInfo |
None =
None,
251 """Initialize the ISY binary sensor device."""
252 super().
__init__(node, device_info=device_info)
258 """Get whether the ISY binary sensor device is on."""
259 if self.
_node_node.status == ISY_VALUE_UNKNOWN:
265 """Representation of an ISY Insteon binary sensor device.
267 Often times, a single device is represented by multiple nodes in the ISY,
268 allowing for different nuances in how those devices report their on and
269 off events. This class turns those multiple nodes into a single Home
270 Assistant entity and handles both ways that ISY binary sensors can work.
276 force_device_class: BinarySensorDeviceClass |
None =
None,
277 unknown_state: bool |
None =
None,
278 device_info: DeviceInfo |
None =
None,
280 """Initialize the ISY binary sensor device."""
281 super().
__init__(node, force_device_class, device_info=device_info)
284 if self.
_node_node.status == ISY_VALUE_UNKNOWN:
292 """Subscribe to the node and subnode event emitters."""
303 """Register a heartbeat device for this sensor.
305 The heartbeat node beats on its own, but we can gain a little
306 reliability by considering any node activity for this sensor
307 to be a heartbeat as well.
312 """Send a heartbeat to our heartbeat device, if we have one."""
317 """Add a negative node to this binary sensor device.
319 The negative node is a node that can receive the 'off' events
320 for the sensor, depending on device configuration and type.
338 """Handle an "On" control event from the "negative" node."""
339 if event.control == CMD_ON:
341 "Sensor %s turning Off via the Negative node sending a DON command",
350 """Handle On and Off control event coming from the primary node.
352 Depending on device configuration, sometimes only On events
353 will come to this node, with the negative node representing Off
356 if event.control == CMD_ON:
358 "Sensor %s turning On via the Primary node sending a DON command",
364 if event.control == CMD_OFF:
366 "Sensor %s turning Off via the Primary node sending a DOF command",
375 """Primary node status updates.
377 We MOSTLY ignore these updates, as we listen directly to the Control
378 events on all nodes for this device. However, there is one edge case:
379 If a leak sensor is unknown, due to a recent reboot of the ISY, the
380 status will get updated to dry upon the first heartbeat. This status
381 update is the only way that a leak sensor's status changes without
382 an accompanying Control event, so we need to watch for it.
392 """Get whether the ISY binary sensor device is on.
394 Insteon leak sensors set their primary node to On when the state is
395 DRY, not WET, so we invert the binary state if the user indicates
396 that it is a moisture sensor.
398 Dusk/Dawn sensors set their node to On when DUSK, not light detected,
399 so this is inverted as well.
406 BinarySensorDeviceClass.LIGHT,
407 BinarySensorDeviceClass.MOISTURE,
415 """Representation of the battery state of an ISY sensor."""
417 _attr_device_class = BinarySensorDeviceClass.BATTERY
422 parent_device: ISYInsteonBinarySensorEntity
423 | ISYBinarySensorEntity
424 | ISYBinarySensorHeartbeat
425 | ISYBinarySensorProgramEntity,
426 device_info: DeviceInfo |
None =
None,
428 """Initialize the ISY binary sensor device.
430 Computed state is set to UNKNOWN unless the ISY provided a valid
431 state. See notes above regarding ISY Sensor status on ISY restart.
432 If a valid state is provided (either on or off), the computed state in
433 HA is restored to the previous value or defaulted to OFF (Normal).
434 If the heartbeat is not received in 25 hours then the computed state is
435 set to ON (Low Battery).
437 super().
__init__(node, device_info=device_info)
441 if self.
statestate
is None:
445 """Subscribe to the node and subnode event emitters."""
455 if last_state.state == STATE_ON:
459 """Update the heartbeat timestamp when any ON/OFF event is sent.
461 The ISY uses both DON and DOF commands (alternating) for a heartbeat.
463 if event.control
in (CMD_ON, CMD_OFF):
468 """Mark the device as online, and restart the 25 hour timer.
470 This gets called when the heartbeat node beats, but also when the
471 parent sensor sends any events, as we can trust that to mean the device
472 is online. This mitigates the risk of false positives due to a single
473 missed heartbeat event.
480 """Restart the 25 hour timer."""
486 def timer_elapsed(now: datetime) ->
None:
487 """Heartbeat missed; set state to ON to indicate dead battery."""
498 """Ignore node status updates.
500 We listen directly to the Control events for this device.
505 """Get whether the ISY binary sensor device is on.
507 Note: This method will return false if the current state is UNKNOWN
508 which occurs after a restart until the first heartbeat or control
509 parent control event is received.
515 """Get the state attributes for the device."""
516 attr = super().extra_state_attributes
517 attr[
"parent_entity_id"] = self.
_parent_device_parent_device.entity_id
522 """Representation of an ISY binary sensor program.
524 This does not need all of the subnode logic in the device version of binary
530 """Get whether the ISY binary sensor device is on."""
None __init__(self, Node node, BinarySensorDeviceClass|None force_device_class=None, bool|None unknown_state=None, DeviceInfo|None device_info=None)
None _restart_timer(self)
None async_on_update(self, object event)
None __init__(self, Node node, ISYInsteonBinarySensorEntity|ISYBinarySensorEntity|ISYBinarySensorHeartbeat|ISYBinarySensorProgramEntity parent_device, DeviceInfo|None device_info=None)
None async_added_to_hass(self)
None _heartbeat_node_control_handler(self, NodeProperty event)
dict[str, Any] extra_state_attributes(self)
None async_heartbeat(self)
None add_heartbeat_device(self, ISYBinarySensorHeartbeat|None entity)
None async_on_update(self, NodeProperty event)
None __init__(self, Node node, BinarySensorDeviceClass|None force_device_class=None, bool|None unknown_state=None, DeviceInfo|None device_info=None)
None async_added_to_hass(self)
None _async_heartbeat(self)
None add_negative_node(self, Node child)
None _async_positive_node_control_handler(self, NodeProperty event)
None _async_negative_node_control_handler(self, NodeProperty event)
str|None device_class(self)
None async_write_ha_state(self)
str|UndefinedType|None name(self)
State|None async_get_last_state(self)
tuple[BinarySensorDeviceClass|None, str|None] _detect_device_type_and_class(Group|Node node)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)