Home Assistant Unofficial Reference 2024.12.1
select.py
Go to the documentation of this file.
1 """Support for ISY select entities."""
2 
3 from __future__ import annotations
4 
5 from typing import cast
6 
7 from pyisy.constants import (
8  ATTR_ACTION,
9  BACKLIGHT_INDEX,
10  CMD_BACKLIGHT,
11  COMMAND_FRIENDLY_NAME,
12  DEV_BL_ADDR,
13  DEV_CMD_MEMORY_WRITE,
14  DEV_MEMORY,
15  INSTEON_RAMP_RATES,
16  ISY_VALUE_UNKNOWN,
17  PROP_RAMP_RATE,
18  TAG_ADDRESS,
19  UOM_INDEX as ISY_UOM_INDEX,
20  UOM_TO_STATES,
21 )
22 from pyisy.helpers import EventListener, NodeProperty
23 from pyisy.nodes import Node, NodeChangedEvent
24 
25 from homeassistant.components.select import SelectEntity, SelectEntityDescription
26 from homeassistant.config_entries import ConfigEntry
27 from homeassistant.const import (
28  STATE_UNAVAILABLE,
29  STATE_UNKNOWN,
30  EntityCategory,
31  Platform,
32  UnitOfTime,
33 )
34 from homeassistant.core import HomeAssistant, callback
35 from homeassistant.exceptions import HomeAssistantError
36 from homeassistant.helpers.device_registry import DeviceInfo
37 from homeassistant.helpers.entity_platform import AddEntitiesCallback
38 from homeassistant.helpers.restore_state import RestoreEntity
39 
40 from .const import _LOGGER, DOMAIN, UOM_INDEX
41 from .entity import ISYAuxControlEntity
42 from .models import IsyData
43 
44 
45 def time_string(i: int) -> str:
46  """Return a formatted ramp rate time string."""
47  if i >= 60:
48  return f"{(float(i)/60):.1f} {UnitOfTime.MINUTES}"
49  return f"{i} {UnitOfTime.SECONDS}"
50 
51 
52 RAMP_RATE_OPTIONS = [time_string(rate) for rate in INSTEON_RAMP_RATES.values()]
53 BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE}
54 
55 
57  hass: HomeAssistant,
58  config_entry: ConfigEntry,
59  async_add_entities: AddEntitiesCallback,
60 ) -> None:
61  """Set up ISY/IoX select entities from config entry."""
62  isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id]
63  device_info = isy_data.devices
64  entities: list[
65  ISYAuxControlIndexSelectEntity
66  | ISYRampRateSelectEntity
67  | ISYBacklightSelectEntity
68  ] = []
69 
70  for node, control in isy_data.aux_properties[Platform.SELECT]:
71  name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title()
72  if node.address != node.primary_node:
73  name = f"{node.name} {name}"
74 
75  options = []
76  if control == PROP_RAMP_RATE:
77  options = RAMP_RATE_OPTIONS
78  elif control == CMD_BACKLIGHT:
79  options = BACKLIGHT_INDEX
80  elif uom := node.aux_properties[control].uom == UOM_INDEX:
81  if options_dict := UOM_TO_STATES.get(uom):
82  options = list(options_dict.values())
83 
84  description = SelectEntityDescription(
85  key=f"{node.address}_{control}",
86  name=name,
87  entity_category=EntityCategory.CONFIG,
88  options=options,
89  )
90  entity_detail = {
91  "node": node,
92  "control": control,
93  "unique_id": f"{isy_data.uid_base(node)}_{control}",
94  "description": description,
95  "device_info": device_info.get(node.primary_node),
96  }
97 
98  if control == PROP_RAMP_RATE:
99  entities.append(ISYRampRateSelectEntity(**entity_detail))
100  continue
101  if control == CMD_BACKLIGHT:
102  entities.append(ISYBacklightSelectEntity(**entity_detail))
103  continue
104  if node.uom == UOM_INDEX and options:
105  entities.append(ISYAuxControlIndexSelectEntity(**entity_detail))
106  continue
107  # Future: support Node Server custom index UOMs
108  _LOGGER.debug(
109  "ISY missing node index unit definitions for %s: %s", node.name, name
110  )
111  async_add_entities(entities)
112 
113 
115  """Representation of a ISY/IoX Aux Control Ramp Rate Select entity."""
116 
117  @property
118  def current_option(self) -> str | None:
119  """Return the selected entity option to represent the entity state."""
120  node_prop: NodeProperty = self._node_node.aux_properties[self._control_control]
121  if node_prop.value == ISY_VALUE_UNKNOWN:
122  return None
123 
124  return RAMP_RATE_OPTIONS[int(node_prop.value)]
125 
126  async def async_select_option(self, option: str) -> None:
127  """Change the selected option."""
128 
129  await self._node_node.set_ramp_rate(RAMP_RATE_OPTIONS.index(option))
130 
131 
133  """Representation of a ISY/IoX Aux Control Index Select entity."""
134 
135  @property
136  def current_option(self) -> str | None:
137  """Return the selected entity option to represent the entity state."""
138  node_prop: NodeProperty = self._node_node.aux_properties[self._control_control]
139  if node_prop.value == ISY_VALUE_UNKNOWN:
140  return None
141 
142  if options_dict := UOM_TO_STATES.get(node_prop.uom):
143  return cast(str, options_dict.get(node_prop.value, node_prop.value))
144  return cast(str, node_prop.formatted)
145 
146  async def async_select_option(self, option: str) -> None:
147  """Change the selected option."""
148  node_prop: NodeProperty = self._node_node.aux_properties[self._control_control]
149 
150  await self._node_node.send_cmd(
151  self._control_control, val=self.optionsoptions.index(option), uom=node_prop.uom
152  )
153 
154 
156  """Representation of a ISY/IoX Backlight Select entity."""
157 
158  _assumed_state = True # Backlight values aren't read from device
159 
160  def __init__(
161  self,
162  node: Node,
163  control: str,
164  unique_id: str,
165  description: SelectEntityDescription,
166  device_info: DeviceInfo | None,
167  ) -> None:
168  """Initialize the ISY Backlight Select entity."""
169  super().__init__(node, control, unique_id, description, device_info)
170  self._memory_change_handler_memory_change_handler: EventListener | None = None
171  self._attr_current_option_attr_current_option = None
172 
173  async def async_added_to_hass(self) -> None:
174  """Load the last known state when added to hass."""
175  await super().async_added_to_hass()
176  if (
177  last_state := await self.async_get_last_stateasync_get_last_state()
178  ) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
179  self._attr_current_option_attr_current_option = last_state.state
180 
181  # Listen to memory writing events to update state if changed in ISY
182  self._memory_change_handler_memory_change_handler = self._node_node.isy.nodes.status_events.subscribe(
183  self.async_on_memory_writeasync_on_memory_write,
184  event_filter={
185  TAG_ADDRESS: self._node_node.address,
186  ATTR_ACTION: DEV_MEMORY,
187  },
188  key=self.unique_idunique_id,
189  )
190 
191  @callback
192  def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None:
193  """Handle a memory write event from the ISY Node."""
194  if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()):
195  return # This was not a backlight event
196  option = BACKLIGHT_INDEX[event.event_info["value"]]
197  if option == self._attr_current_option_attr_current_option:
198  return # Change was from this entity, don't update twice
199  self._attr_current_option_attr_current_option = option
200  self.async_write_ha_stateasync_write_ha_state()
201 
202  async def async_select_option(self, option: str) -> None:
203  """Change the selected option."""
204 
205  if not await self._node_node.send_cmd(
206  CMD_BACKLIGHT, val=BACKLIGHT_INDEX.index(option), uom=ISY_UOM_INDEX
207  ):
208  raise HomeAssistantError(
209  f"Could not set backlight to {option} for {self._node.address}"
210  )
211  self._attr_current_option_attr_current_option = option
212  self.async_write_ha_stateasync_write_ha_state()
None __init__(self, Node node, str control, str unique_id, SelectEntityDescription description, DeviceInfo|None device_info)
Definition: select.py:167
None async_on_memory_write(self, NodeChangedEvent event, str key)
Definition: select.py:192
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: select.py:60