1 """Sorting helpers for ISY device classifications."""
3 from __future__
import annotations
5 from typing
import cast
7 from pyisy.constants
import (
24 from pyisy.nodes
import Group, Node, Nodes
25 from pyisy.programs
import Programs
32 DEFAULT_PROGRAM_STRING,
48 SUBNODE_EZIO2X4_SENSORS,
49 SUBNODE_FANLINC_LIGHT,
51 TYPE_CATEGORY_SENSOR_ACTUATORS,
56 from .models
import IsyData
58 BINARY_SENSOR_UOMS = [
"2",
"78"]
59 BINARY_SENSOR_ISY_STATES = [
"on",
"off"]
64 SKIP_AUX_PROPS = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS, *ROOT_AUX_CONTROLS}
68 isy_data: IsyData, node: Group | Node, single_platform: Platform |
None =
None
70 """Check if the node matches the node_def_id for any platforms.
72 This is only present on the 5.0 ISY firmware, and is the most reliable
73 way to determine a device's type.
75 if not hasattr(node,
"node_def_id")
or node.node_def_id
is None:
79 node_def_id = node.node_def_id
81 platforms = NODE_PLATFORMS
if not single_platform
else [single_platform]
82 for platform
in platforms:
83 if node_def_id
in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]:
84 isy_data.nodes[platform].append(node)
91 isy_data: IsyData, node: Group | Node, single_platform: Platform |
None =
None
93 """Check if the node matches the Insteon type for any platforms.
95 This is for (presumably) every version of the ISY firmware, but only
96 works for Insteon device. "Node Server" (v5+) and Z-Wave and others will
99 if node.protocol != PROTO_INSTEON:
101 if not hasattr(node,
"type")
or node.type
is None:
105 device_type = node.type
106 platforms = NODE_PLATFORMS
if not single_platform
else [single_platform]
107 for platform
in platforms:
109 device_type.startswith(t)
110 for t
in set(NODE_FILTERS[platform][FILTER_INSTEON_TYPE])
115 subnode_id =
int(node.address.split(
" ")[-1], 16)
118 if platform == Platform.FAN
and subnode_id == SUBNODE_FANLINC_LIGHT:
119 isy_data.nodes[Platform.LIGHT].append(node)
123 if platform == Platform.CLIMATE
and subnode_id
in (
124 SUBNODE_CLIMATE_COOL,
125 SUBNODE_CLIMATE_HEAT,
127 isy_data.nodes[Platform.BINARY_SENSOR].append(node)
132 platform == Platform.BINARY_SENSOR
133 and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS)
134 and subnode_id == SUBNODE_IOLINC_RELAY
136 isy_data.nodes[Platform.SWITCH].append(node)
141 platform == Platform.SWITCH
142 and device_type.startswith(TYPE_EZIO2X4)
143 and subnode_id
in SUBNODE_EZIO2X4_SENSORS
145 isy_data.nodes[Platform.BINARY_SENSOR].append(node)
148 isy_data.nodes[platform].append(node)
155 isy_data: IsyData, node: Group | Node, single_platform: Platform |
None =
None
157 """Check if the node matches the ISY Z-Wave Category for any platforms.
159 This is for (presumably) every version of the ISY firmware, but only
160 works for Z-Wave Devices with the devtype.cat property.
162 if node.protocol != PROTO_ZWAVE:
165 if not hasattr(node,
"zwave_props")
or node.zwave_props
is None:
169 device_type = node.zwave_props.category
170 platforms = NODE_PLATFORMS
if not single_platform
else [single_platform]
171 for platform
in platforms:
173 device_type.startswith(t)
174 for t
in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT])
176 isy_data.nodes[platform].append(node)
185 single_platform: Platform |
None =
None,
186 uom_list: list[str] |
None =
None,
188 """Check if a node's uom matches any of the platforms uom filter.
190 This is used for versions of the ISY firmware that report uoms as a single
191 ID. We can often infer what type of device it is by that ID.
193 if not hasattr(node,
"uom")
or node.uom
in (
None,
""):
199 if isinstance(node.uom, list):
200 node_uom = node.uom[0]
202 if uom_list
and single_platform:
203 if node_uom
in uom_list:
204 isy_data.nodes[single_platform].append(node)
208 platforms = NODE_PLATFORMS
if not single_platform
else [single_platform]
209 for platform
in platforms:
210 if node_uom
in NODE_FILTERS[platform][FILTER_UOM]:
211 isy_data.nodes[platform].append(node)
220 single_platform: Platform |
None =
None,
221 states_list: list[str] |
None =
None,
223 """Check if a list of uoms matches two possible filters.
225 This is for versions of the ISY firmware that report uoms as a list of all
226 possible "human readable" states. This filter passes if all of the possible
227 states fit inside the given filter.
229 if not hasattr(node,
"uom")
or node.uom
in (
None,
""):
234 if not isinstance(node.uom, list):
237 node_uom = set(map(str.lower, node.uom))
239 if states_list
and single_platform:
240 if node_uom == set(states_list):
241 isy_data.nodes[single_platform].append(node)
245 platforms = NODE_PLATFORMS
if not single_platform
else [single_platform]
246 for platform
in platforms:
247 if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]):
248 isy_data.nodes[platform].append(node)
255 """Determine if the given sensor node should be a binary_sensor."""
268 single_platform=Platform.BINARY_SENSOR,
269 uom_list=BINARY_SENSOR_UOMS,
275 single_platform=Platform.BINARY_SENSOR,
276 states_list=BINARY_SENSOR_ISY_STATES,
284 """Check if a node supports setting a backlight and add entity."""
285 if not getattr(node,
"is_backlight_supported",
False):
287 if BACKLIGHT_SUPPORT[node.node_def_id] == UOM_INDEX:
288 isy_data.aux_properties[Platform.SELECT].append((node, CMD_BACKLIGHT))
290 isy_data.aux_properties[Platform.NUMBER].append((node, CMD_BACKLIGHT))
294 """Generate the device info for a root node device."""
297 identifiers={(DOMAIN, f
"{isy.uuid}_{node.address}")},
298 manufacturer=node.protocol.title(),
300 via_device=(DOMAIN, isy.uuid),
301 configuration_url=isy.conn.url,
302 suggested_area=node.folder,
306 model: str =
str(node.address).rpartition(
" ")[0]
or node.address
307 if node.node_def_id
is not None:
308 model += f
": {node.node_def_id}"
311 if node.type
is not None:
312 model += f
" ({node.type})"
316 node.protocol == PROTO_ZWAVE
318 and node.zwave_props.mfr_id !=
"0"
320 device_info[ATTR_MANUFACTURER] = (
321 f
"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}"
324 f
"Type:{int(node.zwave_props.prod_type_id):#0{6}x} "
325 f
"Product:{int(node.zwave_props.product_id):#0{6}x}"
327 device_info[ATTR_MODEL] = model
333 isy_data: IsyData, nodes: Nodes, ignore_identifier: str, sensor_identifier: str
335 """Sort the nodes to their proper platforms."""
336 for path, node
in nodes:
337 ignored = ignore_identifier
in path
or ignore_identifier
in node.name
342 if hasattr(node,
"parent_node")
and node.parent_node
is None:
345 isy_data.root_nodes[Platform.BUTTON].append(node)
347 isy_data.aux_properties[Platform.SENSOR].append((node, PROP_COMMS_ERROR))
349 if getattr(node,
"is_dimmable",
False):
350 aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
351 for control
in aux_controls:
352 platform = NODE_AUX_FILTERS[control]
353 isy_data.aux_properties[platform].append((node, control))
354 if hasattr(node, TAG_ENABLED):
355 isy_data.aux_properties[Platform.SWITCH].append((node, TAG_ENABLED))
358 if node.protocol == PROTO_GROUP:
359 isy_data.nodes[ISY_GROUP_PLATFORM].append(node)
362 if node.protocol == PROTO_INSTEON:
363 for control
in node.aux_properties:
364 if control
in SKIP_AUX_PROPS:
366 isy_data.aux_properties[Platform.SENSOR].append((node, control))
368 if sensor_identifier
in path
or sensor_identifier
in node.name:
373 isy_data.nodes[Platform.SENSOR].append(node)
391 isy_data.nodes[Platform.SENSOR].append(node)
395 """Categorize the ISY programs."""
396 for platform
in PROGRAM_PLATFORMS:
397 folder = programs.get_by_name(f
"{DEFAULT_PROGRAM_STRING}{platform}")
401 for dtype, _, node_id
in folder.children:
402 if dtype != TAG_FOLDER:
404 entity_folder = folder[node_id]
407 status = entity_folder.get_by_name(KEY_STATUS)
408 if not status
or status.protocol != PROTO_PROGRAM:
410 "Program %s entity '%s' not loaded, invalid/missing status program",
416 if platform != Platform.BINARY_SENSOR:
417 actions = entity_folder.get_by_name(KEY_ACTIONS)
418 if not actions
or actions.protocol != PROTO_PROGRAM:
421 "Program %s entity '%s' not loaded, invalid/missing actions"
429 entity = (entity_folder.name, status, actions)
430 isy_data.programs[platform].append(entity)
436 precision: int | str,
437 fallback_precision: int |
None =
None,
438 ) -> float | int |
None:
439 """Fix ISY Reported Values.
441 ISY provides float values as an integer and precision component.
442 Correct by shifting the decimal place left by the value of precision.
443 (e.g. value=2345, prec="2" == 23.45)
445 Insteon Thermostats report temperature in 0.5-deg precision as an int
446 by sending a value of 2 times the Temp. Correct by dividing by 2 here.
448 if value
is None or value == ISY_VALUE_UNKNOWN:
450 if uom
in (UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES):
451 return round(
float(value) / 2.0, 1)
452 if precision
not in (
"0", 0):
453 return cast(float, round(
float(value) / 10 **
int(precision),
int(precision)))
454 if fallback_precision:
455 return round(
float(value), fallback_precision)
None _categorize_programs(IsyData isy_data, Programs programs)
bool _check_for_uom_id(IsyData isy_data, Group|Node node, Platform|None single_platform=None, list[str]|None uom_list=None)
bool _check_for_insteon_type(IsyData isy_data, Group|Node node, Platform|None single_platform=None)
bool _check_for_states_in_uom(IsyData isy_data, Group|Node node, Platform|None single_platform=None, list[str]|None states_list=None)
bool _is_sensor_a_binary_sensor(IsyData isy_data, Group|Node node)
float|int|None convert_isy_value_to_hass(float|None value, str|None uom, int|str precision, int|None fallback_precision=None)
bool _check_for_zwave_cat(IsyData isy_data, Group|Node node, Platform|None single_platform=None)
bool _check_for_node_def(IsyData isy_data, Group|Node node, Platform|None single_platform=None)
None _categorize_nodes(IsyData isy_data, Nodes nodes, str ignore_identifier, str sensor_identifier)
DeviceInfo _generate_device_info(Node node)
None _add_backlight_if_supported(IsyData isy_data, Node node)