mirror of https://github.com/home-assistant/core
304 lines
10 KiB
Python
304 lines
10 KiB
Python
"""Support for ISY number entities."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from typing import Any
|
|
|
|
from pyisy.constants import (
|
|
ATTR_ACTION,
|
|
CMD_BACKLIGHT,
|
|
DEV_BL_ADDR,
|
|
DEV_CMD_MEMORY_WRITE,
|
|
DEV_MEMORY,
|
|
ISY_VALUE_UNKNOWN,
|
|
PROP_ON_LEVEL,
|
|
TAG_ADDRESS,
|
|
UOM_PERCENTAGE,
|
|
)
|
|
from pyisy.helpers import EventListener, NodeProperty
|
|
from pyisy.nodes import Node, NodeChangedEvent
|
|
from pyisy.variables import Variable
|
|
|
|
from homeassistant.components.number import (
|
|
NumberEntity,
|
|
NumberEntityDescription,
|
|
NumberMode,
|
|
RestoreNumber,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_VARIABLES,
|
|
PERCENTAGE,
|
|
STATE_UNAVAILABLE,
|
|
STATE_UNKNOWN,
|
|
EntityCategory,
|
|
Platform,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
from homeassistant.helpers.device_registry import DeviceInfo
|
|
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,
|
|
UOM_8_BIT_RANGE,
|
|
)
|
|
from .entity import ISYAuxControlEntity
|
|
from .helpers import convert_isy_value_to_hass
|
|
from .models import IsyData
|
|
|
|
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,
|
|
),
|
|
CMD_BACKLIGHT: NumberEntityDescription(
|
|
key=CMD_BACKLIGHT,
|
|
native_unit_of_measurement=PERCENTAGE,
|
|
entity_category=EntityCategory.CONFIG,
|
|
native_min_value=0.0,
|
|
native_max_value=100.0,
|
|
native_step=1.0,
|
|
),
|
|
}
|
|
BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up ISY/IoX number entities from config entry."""
|
|
isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id]
|
|
device_info = isy_data.devices
|
|
entities: list[
|
|
ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity
|
|
] = []
|
|
var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
|
|
|
|
for node in isy_data.variables[Platform.NUMBER]:
|
|
step = 10 ** (-1 * int(node.prec))
|
|
min_max = ISY_MAX_SIZE / (10 ** int(node.prec))
|
|
description = NumberEntityDescription(
|
|
key=node.address,
|
|
name=node.name,
|
|
entity_registry_enabled_default=var_id in node.name,
|
|
native_unit_of_measurement=None,
|
|
native_step=step,
|
|
native_min_value=-min_max,
|
|
native_max_value=min_max,
|
|
)
|
|
description_init = replace(
|
|
description,
|
|
key=f"{node.address}_init",
|
|
name=f"{node.name} Initial Value",
|
|
entity_category=EntityCategory.CONFIG,
|
|
)
|
|
|
|
entities.append(
|
|
ISYVariableNumberEntity(
|
|
node,
|
|
unique_id=isy_data.uid_base(node),
|
|
description=description,
|
|
device_info=device_info[CONF_VARIABLES],
|
|
)
|
|
)
|
|
entities.append(
|
|
ISYVariableNumberEntity(
|
|
node=node,
|
|
unique_id=f"{isy_data.uid_base(node)}_init",
|
|
description=description_init,
|
|
device_info=device_info[CONF_VARIABLES],
|
|
init_entity=True,
|
|
)
|
|
)
|
|
|
|
for node, control in isy_data.aux_properties[Platform.NUMBER]:
|
|
entity_init_info = {
|
|
"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),
|
|
}
|
|
if control == CMD_BACKLIGHT:
|
|
entities.append(ISYBacklightNumberEntity(**entity_init_info))
|
|
continue
|
|
entities.append(ISYAuxControlNumberEntity(**entity_init_info))
|
|
async_add_entities(entities)
|
|
|
|
|
|
class ISYAuxControlNumberEntity(ISYAuxControlEntity, NumberEntity):
|
|
"""Representation of a ISY/IoX Aux Control Number entity."""
|
|
|
|
_attr_mode = NumberMode.SLIDER
|
|
|
|
@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
|
|
|
|
if not await self._node.send_cmd(self._control, val=value, uom=node_prop.uom):
|
|
raise HomeAssistantError(
|
|
f"Could not set {self.name} to {value} for {self._node.address}"
|
|
)
|
|
|
|
|
|
class ISYVariableNumberEntity(NumberEntity):
|
|
"""Representation of an ISY variable as a number entity device."""
|
|
|
|
_attr_has_entity_name = False
|
|
_attr_should_poll = False
|
|
_init_entity: bool
|
|
_node: Variable
|
|
entity_description: NumberEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
node: Variable,
|
|
unique_id: str,
|
|
description: NumberEntityDescription,
|
|
device_info: DeviceInfo,
|
|
init_entity: bool = False,
|
|
) -> None:
|
|
"""Initialize the ISY variable number."""
|
|
self._node = node
|
|
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
|
|
self._attr_device_info = device_info
|
|
|
|
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."""
|
|
if not await self._node.set_value(value, init=self._init_entity):
|
|
raise HomeAssistantError(
|
|
f"Could not set {self.name} to {value} for {self._node.address}"
|
|
)
|
|
|
|
|
|
class ISYBacklightNumberEntity(ISYAuxControlEntity, RestoreNumber):
|
|
"""Representation of a ISY/IoX Backlight Number entity."""
|
|
|
|
_assumed_state = True # Backlight values aren't read from device
|
|
|
|
def __init__(
|
|
self,
|
|
node: Node,
|
|
control: str,
|
|
unique_id: str,
|
|
description: NumberEntityDescription,
|
|
device_info: DeviceInfo | None,
|
|
) -> None:
|
|
"""Initialize the ISY Backlight number entity."""
|
|
super().__init__(node, control, unique_id, description, device_info)
|
|
self._memory_change_handler: EventListener | None = None
|
|
self._attr_native_value = 0
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
"""Load the last known state when added to hass."""
|
|
await super().async_added_to_hass()
|
|
if (last_state := await self.async_get_last_state()) and (
|
|
last_number_data := await self.async_get_last_number_data()
|
|
):
|
|
if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
|
self._attr_native_value = last_number_data.native_value
|
|
|
|
# Listen to memory writing events to update state if changed in ISY
|
|
self._memory_change_handler = self._node.isy.nodes.status_events.subscribe(
|
|
self.async_on_memory_write,
|
|
event_filter={
|
|
TAG_ADDRESS: self._node.address,
|
|
ATTR_ACTION: DEV_MEMORY,
|
|
},
|
|
key=self.unique_id,
|
|
)
|
|
|
|
@callback
|
|
def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None:
|
|
"""Handle a memory write event from the ISY Node."""
|
|
if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()):
|
|
return # This was not a backlight event
|
|
value = ranged_value_to_percentage((0, 127), event.event_info["value"])
|
|
if value == self._attr_native_value:
|
|
return # Change was from this entity, don't update twice
|
|
self._attr_native_value = value
|
|
self.async_write_ha_state()
|
|
|
|
async def async_set_native_value(self, value: float) -> None:
|
|
"""Update the current value."""
|
|
|
|
if not await self._node.send_cmd(
|
|
CMD_BACKLIGHT, val=int(value), uom=UOM_PERCENTAGE
|
|
):
|
|
raise HomeAssistantError(
|
|
f"Could not set backlight to {value}% for {self._node.address}"
|
|
)
|
|
self._attr_native_value = value
|
|
self.async_write_ha_state()
|