Skip to content

Commit 1fcd251

Browse files
shbatmbdraco
andauthored
Add On Level number entities to ISY994 Insteon Devices (home-assistant#85798)
Co-authored-by: J. Nick Koston <nick@koston.org>
1 parent 5f67e79 commit 1fcd251

File tree

6 files changed

+157
-21
lines changed

6 files changed

+157
-21
lines changed

homeassistant/components/isy994/const.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
Platform.SENSOR,
8888
Platform.SWITCH,
8989
]
90-
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR]
90+
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER]
9191
PROGRAM_PLATFORMS = [
9292
Platform.BINARY_SENSOR,
9393
Platform.COVER,
@@ -308,7 +308,7 @@
308308
},
309309
}
310310
NODE_AUX_FILTERS: dict[str, Platform] = {
311-
PROP_ON_LEVEL: Platform.SENSOR,
311+
PROP_ON_LEVEL: Platform.NUMBER,
312312
PROP_RAMP_RATE: Platform.SENSOR,
313313
}
314314

homeassistant/components/isy994/helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
ISY_GROUP_PLATFORM,
3636
KEY_ACTIONS,
3737
KEY_STATUS,
38+
NODE_AUX_FILTERS,
3839
NODE_FILTERS,
3940
NODE_PLATFORMS,
4041
PROGRAM_PLATFORMS,
@@ -331,7 +332,10 @@ def _categorize_nodes(
331332
if getattr(node, "is_dimmable", False):
332333
aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
333334
for control in aux_controls:
335+
# Deprecated all aux properties as sensors. Update in 2023.5.0 to remove extras.
334336
isy_data.aux_properties[Platform.SENSOR].append((node, control))
337+
platform = NODE_AUX_FILTERS[control]
338+
isy_data.aux_properties[platform].append((node, control))
335339

336340
if node.protocol == PROTO_GROUP:
337341
isy_data.nodes[ISY_GROUP_PLATFORM].append(node)

homeassistant/components/isy994/light.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,19 @@
1010
from homeassistant.components.light import ColorMode, LightEntity
1111
from homeassistant.config_entries import ConfigEntry
1212
from homeassistant.const import Platform
13-
from homeassistant.core import HomeAssistant, callback
13+
from homeassistant.core import HomeAssistant, ServiceCall, callback
1414
from homeassistant.helpers.entity import DeviceInfo
1515
from homeassistant.helpers.entity_platform import AddEntitiesCallback
16+
import homeassistant.helpers.entity_registry as er
1617
from homeassistant.helpers.restore_state import RestoreEntity
1718

1819
from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
1920
from .entity import ISYNodeEntity
20-
from .services import async_setup_light_services
21+
from .services import (
22+
SERVICE_SET_ON_LEVEL,
23+
async_log_deprecated_service_call,
24+
async_setup_light_services,
25+
)
2126

2227
ATTR_LAST_BRIGHTNESS = "last_brightness"
2328

@@ -125,6 +130,18 @@ async def async_added_to_hass(self) -> None:
125130

126131
async def async_set_on_level(self, value: int) -> None:
127132
"""Set the ON Level for a device."""
133+
entity_registry = er.async_get(self.hass)
134+
async_log_deprecated_service_call(
135+
self.hass,
136+
call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL),
137+
alternate_service="number.set_value",
138+
alternate_target=entity_registry.async_get_entity_id(
139+
Platform.NUMBER,
140+
DOMAIN,
141+
f"{self._node.isy.uuid}_{self._node.address}_OL",
142+
),
143+
breaks_in_ha_version="2023.5.0",
144+
)
128145
await self._node.set_on_level(value)
129146

130147
async def async_set_ramp_rate(self, value: int) -> None:

homeassistant/components/isy994/number.py

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,49 @@
11
"""Support for ISY number entities."""
22
from __future__ import annotations
33

4+
from dataclasses import replace
45
from typing import Any
56

7+
from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, PROP_ON_LEVEL
68
from pyisy.helpers import EventListener, NodeProperty
9+
from pyisy.nodes import Node
710
from pyisy.variables import Variable
811

9-
from homeassistant.components.number import NumberEntity, NumberEntityDescription
12+
from homeassistant.components.number import (
13+
NumberEntity,
14+
NumberEntityDescription,
15+
NumberMode,
16+
)
1017
from homeassistant.config_entries import ConfigEntry
11-
from homeassistant.const import CONF_VARIABLES, Platform
18+
from homeassistant.const import CONF_VARIABLES, PERCENTAGE, Platform
1219
from homeassistant.core import HomeAssistant, callback
1320
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
1421
from homeassistant.helpers.entity_platform import AddEntitiesCallback
15-
16-
from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN
22+
from homeassistant.util.percentage import (
23+
percentage_to_ranged_value,
24+
ranged_value_to_percentage,
25+
)
26+
27+
from .const import (
28+
CONF_VAR_SENSOR_STRING,
29+
DEFAULT_VAR_SENSOR_STRING,
30+
DOMAIN,
31+
UOM_8_BIT_RANGE,
32+
)
1733
from .helpers import convert_isy_value_to_hass
1834

1935
ISY_MAX_SIZE = (2**32) / 2
36+
ON_RANGE = (1, 255) # Off is not included
37+
CONTROL_DESC = {
38+
PROP_ON_LEVEL: NumberEntityDescription(
39+
key=PROP_ON_LEVEL,
40+
native_unit_of_measurement=PERCENTAGE,
41+
entity_category=EntityCategory.CONFIG,
42+
native_min_value=1.0,
43+
native_max_value=100.0,
44+
native_step=1.0,
45+
)
46+
}
2047

2148

2249
async def async_setup_entry(
@@ -27,7 +54,7 @@ async def async_setup_entry(
2754
"""Set up ISY/IoX number entities from config entry."""
2855
isy_data = hass.data[DOMAIN][config_entry.entry_id]
2956
device_info = isy_data.devices
30-
entities: list[ISYVariableNumberEntity] = []
57+
entities: list[ISYVariableNumberEntity | ISYAuxControlNumberEntity] = []
3158
var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
3259

3360
for node in isy_data.variables[Platform.NUMBER]:
@@ -43,15 +70,10 @@ async def async_setup_entry(
4370
native_min_value=-min_max,
4471
native_max_value=min_max,
4572
)
46-
description_init = NumberEntityDescription(
73+
description_init = replace(
74+
description,
4775
key=f"{node.address}_init",
4876
name=f"{node.name} Initial Value",
49-
icon="mdi:counter",
50-
entity_registry_enabled_default=False,
51-
native_unit_of_measurement=None,
52-
native_step=step,
53-
native_min_value=-min_max,
54-
native_max_value=min_max,
5577
entity_category=EntityCategory.CONFIG,
5678
)
5779

@@ -73,9 +95,88 @@ async def async_setup_entry(
7395
)
7496
)
7597

98+
for node, control in isy_data.aux_properties[Platform.NUMBER]:
99+
entities.append(
100+
ISYAuxControlNumberEntity(
101+
node=node,
102+
control=control,
103+
unique_id=f"{isy_data.uid_base(node)}_{control}",
104+
description=CONTROL_DESC[control],
105+
device_info=device_info.get(node.primary_node),
106+
)
107+
)
76108
async_add_entities(entities)
77109

78110

111+
class ISYAuxControlNumberEntity(NumberEntity):
112+
"""Representation of a ISY/IoX Aux Control Number entity."""
113+
114+
_attr_mode = NumberMode.SLIDER
115+
_attr_should_poll = False
116+
117+
def __init__(
118+
self,
119+
node: Node,
120+
control: str,
121+
unique_id: str,
122+
description: NumberEntityDescription,
123+
device_info: DeviceInfo | None,
124+
) -> None:
125+
"""Initialize the ISY Aux Control Number entity."""
126+
self._node = node
127+
name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title()
128+
if node.address != node.primary_node:
129+
name = f"{node.name} {name}"
130+
self._attr_name = name
131+
self._control = control
132+
self.entity_description = description
133+
self._attr_has_entity_name = node.address == node.primary_node
134+
self._attr_unique_id = unique_id
135+
self._attr_device_info = device_info
136+
self._change_handler: EventListener | None = None
137+
138+
async def async_added_to_hass(self) -> None:
139+
"""Subscribe to the node control change events."""
140+
self._change_handler = self._node.control_events.subscribe(self.async_on_update)
141+
142+
@callback
143+
def async_on_update(self, event: NodeProperty) -> None:
144+
"""Handle a control event from the ISY Node."""
145+
if event.control != self._control:
146+
return
147+
self.async_write_ha_state()
148+
149+
@property
150+
def native_value(self) -> float | int | None:
151+
"""Return the state of the variable."""
152+
node_prop: NodeProperty = self._node.aux_properties[self._control]
153+
if node_prop.value == ISY_VALUE_UNKNOWN:
154+
return None
155+
156+
if (
157+
self.entity_description.native_unit_of_measurement == PERCENTAGE
158+
and node_prop.uom == UOM_8_BIT_RANGE # Insteon 0-255
159+
):
160+
return ranged_value_to_percentage(ON_RANGE, node_prop.value)
161+
return int(node_prop.value)
162+
163+
async def async_set_native_value(self, value: float) -> None:
164+
"""Update the current value."""
165+
node_prop: NodeProperty = self._node.aux_properties[self._control]
166+
167+
if self.entity_description.native_unit_of_measurement == PERCENTAGE:
168+
value = (
169+
percentage_to_ranged_value(ON_RANGE, round(value))
170+
if node_prop.uom == UOM_8_BIT_RANGE
171+
else value
172+
)
173+
if self._control == PROP_ON_LEVEL:
174+
await self._node.set_on_level(value)
175+
return
176+
177+
await self._node.send_cmd(self._control, val=value, uom=node_prop.uom)
178+
179+
79180
class ISYVariableNumberEntity(NumberEntity):
80181
"""Representation of an ISY variable as a number entity device."""
81182

homeassistant/components/isy994/sensor.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
COMMAND_FRIENDLY_NAME,
88
ISY_VALUE_UNKNOWN,
99
PROP_BATTERY_LEVEL,
10-
PROP_BUSY,
1110
PROP_COMMS_ERROR,
1211
PROP_ENERGY_MODE,
1312
PROP_HEAT_COOL_STATE,
@@ -28,7 +27,7 @@
2827
)
2928
from homeassistant.config_entries import ConfigEntry
3029
from homeassistant.const import Platform, UnitOfTemperature
31-
from homeassistant.core import HomeAssistant
30+
from homeassistant.core import HomeAssistant, callback
3231
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
3332
from homeassistant.helpers.entity_platform import AddEntitiesCallback
3433

@@ -53,7 +52,6 @@
5352
PROP_RAMP_RATE,
5453
PROP_STATUS,
5554
}
56-
SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS}
5755

5856
# Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details.
5957
# Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit
@@ -260,6 +258,22 @@ def target_value(self) -> Any:
260258
"""Return the target value."""
261259
return None if self.target is None else self.target.value
262260

261+
async def async_added_to_hass(self) -> None:
262+
"""Subscribe to the node control change events.
263+
264+
Overloads the default ISYNodeEntity updater to only update when
265+
this control is changed on the device and prevent duplicate firing
266+
of `isy994_control` events.
267+
"""
268+
self._change_handler = self._node.control_events.subscribe(self.async_on_update)
269+
270+
@callback
271+
def async_on_update(self, event: NodeProperty) -> None:
272+
"""Handle a control event from the ISY Node."""
273+
if event.control != self._control:
274+
return
275+
self.async_write_ha_state()
276+
263277

264278
class ISYSensorVariableEntity(ISYEntity, SensorEntity):
265279
"""Representation of an ISY variable as a sensor device."""

homeassistant/components/isy994/services.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ rename_node:
136136
selector:
137137
text:
138138
set_on_level:
139-
name: Set On Level
140-
description: Send a ISY set_on_level command to a Node.
139+
name: Set On Level (Deprecated)
140+
description: "Send a ISY set_on_level command to a Node. Deprecated: Use On Level Number entity instead."
141141
target:
142142
entity:
143143
integration: isy994

0 commit comments

Comments
 (0)