Add Insteon ramp rate select entities to ISY994 (#85895)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
shbatm 2023-01-14 21:37:07 -06:00 committed by GitHub
parent a653ea30bd
commit a81045653d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 190 additions and 44 deletions

View File

@ -612,6 +612,7 @@ omit =
homeassistant/components/isy994/lock.py homeassistant/components/isy994/lock.py
homeassistant/components/isy994/models.py homeassistant/components/isy994/models.py
homeassistant/components/isy994/number.py homeassistant/components/isy994/number.py
homeassistant/components/isy994/select.py
homeassistant/components/isy994/sensor.py homeassistant/components/isy994/sensor.py
homeassistant/components/isy994/services.py homeassistant/components/isy994/services.py
homeassistant/components/isy994/switch.py homeassistant/components/isy994/switch.py

View File

@ -87,7 +87,7 @@ NODE_PLATFORMS = [
Platform.SENSOR, Platform.SENSOR,
Platform.SWITCH, Platform.SWITCH,
] ]
NODE_AUX_PROP_PLATFORMS = [Platform.SENSOR, Platform.NUMBER] NODE_AUX_PROP_PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.NUMBER]
PROGRAM_PLATFORMS = [ PROGRAM_PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.COVER, Platform.COVER,
@ -309,7 +309,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.NUMBER, PROP_ON_LEVEL: Platform.NUMBER,
PROP_RAMP_RATE: Platform.SENSOR, PROP_RAMP_RATE: Platform.SELECT,
} }
UOM_FRIENDLY_NAME = { UOM_FRIENDLY_NAME = {

View File

@ -18,7 +18,7 @@ from pyisy.variables import Variable
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from .const import DOMAIN from .const import DOMAIN
@ -189,3 +189,42 @@ class ISYProgramEntity(ISYEntity):
if self._node.last_update != EMPTY_TIME: if self._node.last_update != EMPTY_TIME:
attr["status_last_update"] = self._node.last_update attr["status_last_update"] = self._node.last_update
return attr return attr
class ISYAuxControlEntity(Entity):
"""Representation of a ISY/IoX Aux Control base entity."""
_attr_should_poll = False
def __init__(
self,
node: Node,
control: str,
unique_id: str,
description: EntityDescription,
device_info: DeviceInfo | None,
) -> None:
"""Initialize the ISY Aux Control Number entity."""
self._node = node
self._control = control
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.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."""
# Only watch for our control changing or the node being enabled/disabled
if event.control != self._control:
return
self.async_write_ha_state()

View File

@ -146,4 +146,16 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity):
async def async_set_ramp_rate(self, value: int) -> None: async def async_set_ramp_rate(self, value: int) -> None:
"""Set the Ramp Rate for a device.""" """Set the Ramp Rate 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="select.select_option",
alternate_target=entity_registry.async_get_entity_id(
Platform.NUMBER,
DOMAIN,
f"{self._node.isy.uuid}_{self._node.address}_RR",
),
breaks_in_ha_version="2023.5.0",
)
await self._node.set_ramp_rate(value) await self._node.set_ramp_rate(value)

View File

@ -4,9 +4,8 @@ from __future__ import annotations
from dataclasses import replace 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.constants import 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 ( from homeassistant.components.number import (
@ -30,6 +29,7 @@ from .const import (
DOMAIN, DOMAIN,
UOM_8_BIT_RANGE, UOM_8_BIT_RANGE,
) )
from .entity import ISYAuxControlEntity
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
@ -58,12 +58,11 @@ async def async_setup_entry(
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]:
step = 10 ** (-1 * node.prec) step = 10 ** (-1 * int(node.prec))
min_max = ISY_MAX_SIZE / (10**node.prec) min_max = ISY_MAX_SIZE / (10 ** int(node.prec))
description = NumberEntityDescription( description = NumberEntityDescription(
key=node.address, key=node.address,
name=node.name, name=node.name,
icon="mdi:counter",
entity_registry_enabled_default=var_id in node.name, entity_registry_enabled_default=var_id in node.name,
native_unit_of_measurement=None, native_unit_of_measurement=None,
native_step=step, native_step=step,
@ -108,43 +107,10 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
class ISYAuxControlNumberEntity(NumberEntity): class ISYAuxControlNumberEntity(ISYAuxControlEntity, NumberEntity):
"""Representation of a ISY/IoX Aux Control Number entity.""" """Representation of a ISY/IoX Aux Control Number entity."""
_attr_mode = NumberMode.SLIDER _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 @property
def native_value(self) -> float | int | None: def native_value(self) -> float | int | None:

View File

@ -0,0 +1,127 @@
"""Support for ISY select entities."""
from __future__ import annotations
from typing import cast
from pyisy.constants import (
COMMAND_FRIENDLY_NAME,
INSTEON_RAMP_RATES,
ISY_VALUE_UNKNOWN,
PROP_RAMP_RATE,
UOM_TO_STATES,
)
from pyisy.helpers import NodeProperty
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import _LOGGER, DOMAIN, UOM_INDEX
from .entity import ISYAuxControlEntity
from .models import IsyData
def time_string(i: int) -> str:
"""Return a formatted ramp rate time string."""
if i >= 60:
return f"{(float(i)/60):.1f} {UnitOfTime.MINUTES}"
return f"{i} {UnitOfTime.SECONDS}"
RAMP_RATE_OPTIONS = [time_string(rate) for rate in INSTEON_RAMP_RATES.values()]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up ISY/IoX select entities from config entry."""
isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id]
isy = isy_data.root
device_info = isy_data.devices
entities: list[ISYAuxControlIndexSelectEntity | ISYRampRateSelectEntity] = []
for node, control in isy_data.aux_properties[Platform.SELECT]:
name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title()
if node.address != node.primary_node:
name = f"{node.name} {name}"
node_prop: NodeProperty = node.aux_properties[control]
options = []
if control == PROP_RAMP_RATE:
options = RAMP_RATE_OPTIONS
if node_prop.uom == UOM_INDEX:
if options_dict := UOM_TO_STATES.get(node_prop.uom):
options = list(options_dict.values())
description = SelectEntityDescription(
key=f"{node.address}_{control}",
name=name,
entity_category=EntityCategory.CONFIG,
options=options,
)
entity_detail = {
"node": node,
"control": control,
"unique_id": f"{isy.uuid}_{node.address}_{control}",
"description": description,
"device_info": device_info.get(node.primary_node),
}
if control == PROP_RAMP_RATE:
entities.append(ISYRampRateSelectEntity(**entity_detail))
continue
if node.uom == UOM_INDEX and options:
entities.append(ISYAuxControlIndexSelectEntity(**entity_detail))
continue
# Future: support Node Server custom index UOMs
_LOGGER.debug(
"ISY missing node index unit definitions for %s: %s", node.name, name
)
async_add_entities(entities)
class ISYRampRateSelectEntity(ISYAuxControlEntity, SelectEntity):
"""Representation of a ISY/IoX Aux Control Ramp Rate Select entity."""
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
node_prop: NodeProperty = self._node.aux_properties[self._control]
if node_prop.value == ISY_VALUE_UNKNOWN:
return None
return RAMP_RATE_OPTIONS[int(node_prop.value)]
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self._node.set_ramp_rate(RAMP_RATE_OPTIONS.index(option))
class ISYAuxControlIndexSelectEntity(ISYAuxControlEntity, SelectEntity):
"""Representation of a ISY/IoX Aux Control Index Select entity."""
@property
def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state."""
node_prop: NodeProperty = self._node.aux_properties[self._control]
if node_prop.value == ISY_VALUE_UNKNOWN:
return None
if options_dict := UOM_TO_STATES.get(node_prop.uom):
return cast(str, options_dict.get(node_prop.value, node_prop.value))
return cast(str, node_prop.formatted)
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
node_prop: NodeProperty = self._node.aux_properties[self._control]
await self._node.send_cmd(
self._control, val=self.options.index(option), uom=node_prop.uom
)

View File

@ -46,6 +46,7 @@ from .helpers import convert_isy_value_to_hass
# Disable general purpose and redundant sensors by default # Disable general purpose and redundant sensors by default
AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"]
AUX_DISABLED_BY_DEFAULT_EXACT = { AUX_DISABLED_BY_DEFAULT_EXACT = {
PROP_COMMS_ERROR,
PROP_ENERGY_MODE, PROP_ENERGY_MODE,
PROP_HEAT_COOL_STATE, PROP_HEAT_COOL_STATE,
PROP_ON_LEVEL, PROP_ON_LEVEL,

View File

@ -152,8 +152,8 @@ set_on_level:
min: 0 min: 0
max: 255 max: 255
set_ramp_rate: set_ramp_rate:
name: Set ramp rate name: Set ramp rate (Deprecated)
description: Send a ISY set_ramp_rate command to a Node. description: "Send a ISY set_ramp_rate command to a Node. Deprecated: Use On Level Number entity instead."
target: target:
entity: entity:
integration: isy994 integration: isy994