From d3249432c908e08b6981c8d0300811922c2ded72 Mon Sep 17 00:00:00 2001 From: shbatm Date: Tue, 10 Jan 2023 16:29:11 -0600 Subject: [PATCH] Add ISY994 variables as number entities (#85511) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/isy994/__init__.py | 10 +- homeassistant/components/isy994/const.py | 9 + homeassistant/components/isy994/helpers.py | 3 +- homeassistant/components/isy994/number.py | 162 ++++++++++++++++++ homeassistant/components/isy994/sensor.py | 5 +- homeassistant/components/isy994/services.py | 12 ++ homeassistant/components/isy994/services.yaml | 4 +- 8 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/isy994/number.py diff --git a/.coveragerc b/.coveragerc index 93f7434634b..f9ca672e8eb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -607,6 +607,7 @@ omit = homeassistant/components/isy994/helpers.py homeassistant/components/isy994/light.py homeassistant/components/isy994/lock.py + homeassistant/components/isy994/number.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index be2948c7aa2..a8b3d4e239e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -141,7 +142,9 @@ async def async_setup_entry( for platform in PROGRAM_PLATFORMS: hass_isy_data[ISY994_PROGRAMS][platform] = [] - hass_isy_data[ISY994_VARIABLES] = [] + hass_isy_data[ISY994_VARIABLES] = {} + hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] = [] + hass_isy_data[ISY994_VARIABLES][Platform.SENSOR] = [] isy_config = entry.data isy_options = entry.options @@ -212,7 +215,12 @@ async def async_setup_entry( _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) + # Categorize variables call to be removed with variable sensors in 2023.5.0 _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + # Gather ISY Variables to be added. Identifier used to enable by default. + numbers = hass_isy_data[ISY994_VARIABLES][Platform.NUMBER] + for vtype, vname, vid in isy.variables.children: + numbers.append((isy.variables[vtype][vid], variable_identifier in vname)) if isy.configuration[ISY_CONF_NETWORKING]: for resource in isy.networking.nobjs: hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 402086ddec1..3df11f078ea 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -82,6 +82,7 @@ PLATFORMS = [ Platform.FAN, Platform.LIGHT, Platform.LOCK, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] @@ -307,6 +308,14 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], FILTER_ZWAVE_CAT: ["140"], }, + Platform.NUMBER: { + # No devices automatically sorted as numbers at this time. + FILTER_UOM: [], + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [], + FILTER_INSTEON_TYPE: [], + FILTER_ZWAVE_CAT: [], + }, } UOM_FRIENDLY_NAME = { diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 54d2890c84c..cc602a49777 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -376,8 +376,9 @@ def _categorize_variables( except KeyError as err: _LOGGER.error("Error adding ISY Variables: %s", err) return + variable_entities = hass_isy_data[ISY994_VARIABLES] for vtype, vname, vid in var_to_add: - hass_isy_data[ISY994_VARIABLES].append((vname, variables[vtype][vid])) + variable_entities[Platform.SENSOR].append((vname, variables[vtype][vid])) async def migrate_old_unique_ids( diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py new file mode 100644 index 00000000000..064b6c6e60a --- /dev/null +++ b/homeassistant/components/isy994/number.py @@ -0,0 +1,162 @@ +"""Support for ISY number entities.""" +from __future__ import annotations + +from typing import Any + +from pyisy import ISY +from pyisy.helpers import EventListener, NodeProperty +from pyisy.variables import Variable + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import _async_isy_to_configuration_url +from .const import ( + DOMAIN as ISY994_DOMAIN, + ISY994_ISY, + ISY994_VARIABLES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_UUID, + MANUFACTURER, +) +from .helpers import convert_isy_value_to_hass + +ISY_MAX_SIZE = (2**32) / 2 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX number entities from config entry.""" + hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY994_ISY] + uuid = isy.configuration[ISY_CONF_UUID] + entities: list[ISYVariableNumberEntity] = [] + + for node, enable_by_default in hass_isy_data[ISY994_VARIABLES][Platform.NUMBER]: + step = 10 ** (-1 * node.prec) + min_max = ISY_MAX_SIZE / (10**node.prec) + description = NumberEntityDescription( + key=node.address, + name=node.name, + icon="mdi:counter", + entity_registry_enabled_default=enable_by_default, + native_unit_of_measurement=None, + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, + ) + description_init = NumberEntityDescription( + 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, + ) + + entities.append( + ISYVariableNumberEntity( + node, + unique_id=f"{uuid}_{node.address}", + description=description, + ) + ) + entities.append( + ISYVariableNumberEntity( + node=node, + unique_id=f"{uuid}_{node.address}_init", + description=description_init, + init_entity=True, + ) + ) + + async_add_entities(entities) + + +class ISYVariableNumberEntity(NumberEntity): + """Representation of an ISY variable as a number entity device.""" + + _attr_has_entity_name = True + _attr_should_poll = False + _init_entity: bool + _node: Variable + entity_description: NumberEntityDescription + + def __init__( + self, + node: Variable, + unique_id: str, + description: NumberEntityDescription, + init_entity: bool = False, + ) -> None: + """Initialize the ISY variable number.""" + self._node = node + self._name = description.name + self.entity_description = description + self._change_handler: EventListener | None = None + + # Two entities are created for each variable, one for current value and one for initial. + # Initial value entities are disabled by default + self._init_entity = init_entity + + self._attr_unique_id = unique_id + + url = _async_isy_to_configuration_url(node.isy) + config = node.isy.configuration + self._attr_device_info = DeviceInfo( + identifiers={ + ( + ISY994_DOMAIN, + f"{config[ISY_CONF_UUID]}_variables", + ) + }, + manufacturer=MANUFACTURER, + name=f"{config[ISY_CONF_NAME]} Variables", + model=config[ISY_CONF_MODEL], + sw_version=config[ISY_CONF_FIRMWARE], + configuration_url=url, + via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle the update event from the ISY Node.""" + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + return convert_isy_value_to_hass( + self._node.init if self._init_entity else self._node.status, + "", + self._node.prec, + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Get the state attributes for the device.""" + return { + "last_edited": self._node.last_edited, + } + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._node.set_value(value, init=self._init_entity) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 727600edea2..e3e812d1b26 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -132,7 +132,7 @@ async def async_setup_entry( # Any node in SENSOR_AUX can potentially have communication errors entities.append(ISYAuxSensorEntity(node, PROP_COMMS_ERROR, False)) - for vname, vobj in hass_isy_data[ISY994_VARIABLES]: + for vname, vobj in hass_isy_data[ISY994_VARIABLES][Platform.SENSOR]: entities.append(ISYSensorVariableEntity(vname, vobj)) await migrate_old_unique_ids(hass, Platform.SENSOR, entities) @@ -269,6 +269,9 @@ class ISYAuxSensorEntity(ISYSensorEntity): class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY variable as a sensor device.""" + # Depreceted sensors, will be removed in 2023.5.0 + _attr_entity_registry_enabled_default = False + def __init__(self, vname: str, vobj: object) -> None: """Initialize the ISY binary sensor program.""" super().__init__(vobj) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index ff7fdb965c7..bd49478905f 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -307,6 +307,18 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 variable = isy.variables.vobjs[vtype].get(address) if variable is not None: await variable.set_value(value, init) + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{isy.configuration[ISY_CONF_UUID]}_{address}{'_init' if init else ''}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error("Could not set variable value; not found or enabled on the ISY") diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 8d1aa8c58ef..c9daa828970 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -184,8 +184,8 @@ system_query: selector: text: set_variable: - name: Set variable - description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. + name: Set variable (Deprecated) + description: "Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. Deprecated: Use number entities instead." fields: address: name: Address