Add On Level number entities to ISY994 Insteon Devices (#85798)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
shbatm 2023-01-13 20:01:41 -06:00 committed by GitHub
parent 5f67e79ad9
commit 1fcd25130f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 156 additions and 20 deletions

View File

@ -87,7 +87,7 @@ NODE_PLATFORMS = [
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
] ]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR] NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER]
PROGRAM_PLATFORMS = [ PROGRAM_PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.COVER, Platform.COVER,
@ -308,7 +308,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
}, },
} }
NODE_AUX_FILTERS: dict[str, Platform] = { NODE_AUX_FILTERS: dict[str, Platform] = {
PROP_ON_LEVEL: Platform.SENSOR, PROP_ON_LEVEL: Platform.NUMBER,
PROP_RAMP_RATE: Platform.SENSOR, PROP_RAMP_RATE: Platform.SENSOR,
} }

View File

@ -35,6 +35,7 @@ from .const import (
ISY_GROUP_PLATFORM, ISY_GROUP_PLATFORM,
KEY_ACTIONS, KEY_ACTIONS,
KEY_STATUS, KEY_STATUS,
NODE_AUX_FILTERS,
NODE_FILTERS, NODE_FILTERS,
NODE_PLATFORMS, NODE_PLATFORMS,
PROGRAM_PLATFORMS, PROGRAM_PLATFORMS,
@ -331,7 +332,10 @@ def _categorize_nodes(
if getattr(node, "is_dimmable", False): if getattr(node, "is_dimmable", False):
aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties) aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
for control in aux_controls: for control in aux_controls:
# Deprecated all aux properties as sensors. Update in 2023.5.0 to remove extras.
isy_data.aux_properties[Platform.SENSOR].append((node, control)) isy_data.aux_properties[Platform.SENSOR].append((node, control))
platform = NODE_AUX_FILTERS[control]
isy_data.aux_properties[platform].append((node, control))
if node.protocol == PROTO_GROUP: if node.protocol == PROTO_GROUP:
isy_data.nodes[ISY_GROUP_PLATFORM].append(node) isy_data.nodes[ISY_GROUP_PLATFORM].append(node)

View File

@ -10,14 +10,19 @@ from pyisy.nodes import Node
from homeassistant.components.light import ColorMode, LightEntity from homeassistant.components.light import ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
from .entity import ISYNodeEntity from .entity import ISYNodeEntity
from .services import async_setup_light_services from .services import (
SERVICE_SET_ON_LEVEL,
async_log_deprecated_service_call,
async_setup_light_services,
)
ATTR_LAST_BRIGHTNESS = "last_brightness" ATTR_LAST_BRIGHTNESS = "last_brightness"
@ -125,6 +130,18 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
async def async_set_on_level(self, value: int) -> None: async def async_set_on_level(self, value: int) -> None:
"""Set the ON Level for a device.""" """Set the ON Level for a device."""
entity_registry = er.async_get(self.hass)
async_log_deprecated_service_call(
self.hass,
call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL),
alternate_service="number.set_value",
alternate_target=entity_registry.async_get_entity_id(
Platform.NUMBER,
DOMAIN,
f"{self._node.isy.uuid}_{self._node.address}_OL",
),
breaks_in_ha_version="2023.5.0",
)
await self._node.set_on_level(value) await self._node.set_on_level(value)
async def async_set_ramp_rate(self, value: int) -> None: async def async_set_ramp_rate(self, value: int) -> None:

View File

@ -1,22 +1,49 @@
"""Support for ISY number entities.""" """Support for ISY number entities."""
from __future__ import annotations from __future__ import annotations
from dataclasses import replace
from typing import Any from typing import Any
from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, PROP_ON_LEVEL
from pyisy.helpers import EventListener, NodeProperty from pyisy.helpers import EventListener, NodeProperty
from pyisy.nodes import Node
from pyisy.variables import Variable from pyisy.variables import Variable
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
NumberMode,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_VARIABLES, Platform from homeassistant.const import CONF_VARIABLES, PERCENTAGE, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.percentage import (
percentage_to_ranged_value,
ranged_value_to_percentage,
)
from .const import CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN from .const import (
CONF_VAR_SENSOR_STRING,
DEFAULT_VAR_SENSOR_STRING,
DOMAIN,
UOM_8_BIT_RANGE,
)
from .helpers import convert_isy_value_to_hass from .helpers import convert_isy_value_to_hass
ISY_MAX_SIZE = (2**32) / 2 ISY_MAX_SIZE = (2**32) / 2
ON_RANGE = (1, 255) # Off is not included
CONTROL_DESC = {
PROP_ON_LEVEL: NumberEntityDescription(
key=PROP_ON_LEVEL,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.CONFIG,
native_min_value=1.0,
native_max_value=100.0,
native_step=1.0,
)
}
async def async_setup_entry( async def async_setup_entry(
@ -27,7 +54,7 @@ async def async_setup_entry(
"""Set up ISY/IoX number entities from config entry.""" """Set up ISY/IoX number entities from config entry."""
isy_data = hass.data[DOMAIN][config_entry.entry_id] isy_data = hass.data[DOMAIN][config_entry.entry_id]
device_info = isy_data.devices device_info = isy_data.devices
entities: list[ISYVariableNumberEntity] = [] entities: list[ISYVariableNumberEntity | ISYAuxControlNumberEntity] = []
var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
for node in isy_data.variables[Platform.NUMBER]: for node in isy_data.variables[Platform.NUMBER]:
@ -43,15 +70,10 @@ async def async_setup_entry(
native_min_value=-min_max, native_min_value=-min_max,
native_max_value=min_max, native_max_value=min_max,
) )
description_init = NumberEntityDescription( description_init = replace(
description,
key=f"{node.address}_init", key=f"{node.address}_init",
name=f"{node.name} Initial Value", name=f"{node.name} Initial Value",
icon="mdi:counter",
entity_registry_enabled_default=False,
native_unit_of_measurement=None,
native_step=step,
native_min_value=-min_max,
native_max_value=min_max,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
) )
@ -73,9 +95,88 @@ async def async_setup_entry(
) )
) )
for node, control in isy_data.aux_properties[Platform.NUMBER]:
entities.append(
ISYAuxControlNumberEntity(
node=node,
control=control,
unique_id=f"{isy_data.uid_base(node)}_{control}",
description=CONTROL_DESC[control],
device_info=device_info.get(node.primary_node),
)
)
async_add_entities(entities) async_add_entities(entities)
class ISYAuxControlNumberEntity(NumberEntity):
"""Representation of a ISY/IoX Aux Control Number entity."""
_attr_mode = NumberMode.SLIDER
_attr_should_poll = False
def __init__(
self,
node: Node,
control: str,
unique_id: str,
description: NumberEntityDescription,
device_info: DeviceInfo | None,
) -> None:
"""Initialize the ISY Aux Control Number entity."""
self._node = node
name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title()
if node.address != node.primary_node:
name = f"{node.name} {name}"
self._attr_name = name
self._control = control
self.entity_description = description
self._attr_has_entity_name = node.address == node.primary_node
self._attr_unique_id = unique_id
self._attr_device_info = device_info
self._change_handler: EventListener | None = None
async def async_added_to_hass(self) -> None:
"""Subscribe to the node control change events."""
self._change_handler = self._node.control_events.subscribe(self.async_on_update)
@callback
def async_on_update(self, event: NodeProperty) -> None:
"""Handle a control event from the ISY Node."""
if event.control != self._control:
return
self.async_write_ha_state()
@property
def native_value(self) -> float | int | None:
"""Return the state of the variable."""
node_prop: NodeProperty = self._node.aux_properties[self._control]
if node_prop.value == ISY_VALUE_UNKNOWN:
return None
if (
self.entity_description.native_unit_of_measurement == PERCENTAGE
and node_prop.uom == UOM_8_BIT_RANGE # Insteon 0-255
):
return ranged_value_to_percentage(ON_RANGE, node_prop.value)
return int(node_prop.value)
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
node_prop: NodeProperty = self._node.aux_properties[self._control]
if self.entity_description.native_unit_of_measurement == PERCENTAGE:
value = (
percentage_to_ranged_value(ON_RANGE, round(value))
if node_prop.uom == UOM_8_BIT_RANGE
else value
)
if self._control == PROP_ON_LEVEL:
await self._node.set_on_level(value)
return
await self._node.send_cmd(self._control, val=value, uom=node_prop.uom)
class ISYVariableNumberEntity(NumberEntity): class ISYVariableNumberEntity(NumberEntity):
"""Representation of an ISY variable as a number entity device.""" """Representation of an ISY variable as a number entity device."""

View File

@ -7,7 +7,6 @@ from pyisy.constants import (
COMMAND_FRIENDLY_NAME, COMMAND_FRIENDLY_NAME,
ISY_VALUE_UNKNOWN, ISY_VALUE_UNKNOWN,
PROP_BATTERY_LEVEL, PROP_BATTERY_LEVEL,
PROP_BUSY,
PROP_COMMS_ERROR, PROP_COMMS_ERROR,
PROP_ENERGY_MODE, PROP_ENERGY_MODE,
PROP_HEAT_COOL_STATE, PROP_HEAT_COOL_STATE,
@ -28,7 +27,7 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform, UnitOfTemperature from homeassistant.const import Platform, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -53,7 +52,6 @@ AUX_DISABLED_BY_DEFAULT_EXACT = {
PROP_RAMP_RATE, PROP_RAMP_RATE,
PROP_STATUS, PROP_STATUS,
} }
SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS}
# Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details. # Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details.
# Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit # Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit
@ -260,6 +258,22 @@ class ISYAuxSensorEntity(ISYSensorEntity):
"""Return the target value.""" """Return the target value."""
return None if self.target is None else self.target.value return None if self.target is None else self.target.value
async def async_added_to_hass(self) -> None:
"""Subscribe to the node control change events.
Overloads the default ISYNodeEntity updater to only update when
this control is changed on the device and prevent duplicate firing
of `isy994_control` events.
"""
self._change_handler = self._node.control_events.subscribe(self.async_on_update)
@callback
def async_on_update(self, event: NodeProperty) -> None:
"""Handle a control event from the ISY Node."""
if event.control != self._control:
return
self.async_write_ha_state()
class ISYSensorVariableEntity(ISYEntity, SensorEntity): class ISYSensorVariableEntity(ISYEntity, SensorEntity):
"""Representation of an ISY variable as a sensor device.""" """Representation of an ISY variable as a sensor device."""

View File

@ -136,8 +136,8 @@ rename_node:
selector: selector:
text: text:
set_on_level: set_on_level:
name: Set On Level name: Set On Level (Deprecated)
description: Send a ISY set_on_level command to a Node. description: "Send a ISY set_on_level command to a Node. Deprecated: Use On Level Number entity instead."
target: target:
entity: entity:
integration: isy994 integration: isy994