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.SWITCH,
]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER]
PROGRAM_PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.COVER,
@ -308,7 +308,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
},
}
NODE_AUX_FILTERS: dict[str, Platform] = {
PROP_ON_LEVEL: Platform.SENSOR,
PROP_ON_LEVEL: Platform.NUMBER,
PROP_RAMP_RATE: Platform.SENSOR,
}

View File

@ -35,6 +35,7 @@ from .const import (
ISY_GROUP_PLATFORM,
KEY_ACTIONS,
KEY_STATUS,
NODE_AUX_FILTERS,
NODE_FILTERS,
NODE_PLATFORMS,
PROGRAM_PLATFORMS,
@ -331,7 +332,10 @@ def _categorize_nodes(
if getattr(node, "is_dimmable", False):
aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties)
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))
platform = NODE_AUX_FILTERS[control]
isy_data.aux_properties[platform].append((node, control))
if node.protocol == PROTO_GROUP:
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.config_entries import ConfigEntry
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_platform import AddEntitiesCallback
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.restore_state import RestoreEntity
from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE
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"
@ -125,6 +130,18 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
async def async_set_on_level(self, value: int) -> None:
"""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)
async def async_set_ramp_rate(self, value: int) -> None:

View File

@ -1,22 +1,49 @@
"""Support for ISY number entities."""
from __future__ import annotations
from dataclasses import replace
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.nodes import Node
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.const import CONF_VARIABLES, Platform
from homeassistant.const import CONF_VARIABLES, PERCENTAGE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
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
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(
@ -27,7 +54,7 @@ async def async_setup_entry(
"""Set up ISY/IoX number entities from config entry."""
isy_data = hass.data[DOMAIN][config_entry.entry_id]
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)
for node in isy_data.variables[Platform.NUMBER]:
@ -43,15 +70,10 @@ async def async_setup_entry(
native_min_value=-min_max,
native_max_value=min_max,
)
description_init = NumberEntityDescription(
description_init = replace(
description,
key=f"{node.address}_init",
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,
)
@ -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)
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):
"""Representation of an ISY variable as a number entity device."""

View File

@ -7,7 +7,6 @@ from pyisy.constants import (
COMMAND_FRIENDLY_NAME,
ISY_VALUE_UNKNOWN,
PROP_BATTERY_LEVEL,
PROP_BUSY,
PROP_COMMS_ERROR,
PROP_ENERGY_MODE,
PROP_HEAT_COOL_STATE,
@ -28,7 +27,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
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_platform import AddEntitiesCallback
@ -53,7 +52,6 @@ AUX_DISABLED_BY_DEFAULT_EXACT = {
PROP_RAMP_RATE,
PROP_STATUS,
}
SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS}
# Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details.
# Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit
@ -260,6 +258,22 @@ class ISYAuxSensorEntity(ISYSensorEntity):
"""Return the 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):
"""Representation of an ISY variable as a sensor device."""

View File

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