Home Assistant Unofficial Reference 2024.12.1
number.py
Go to the documentation of this file.
1 """Support for ISY number entities."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import replace
6 from typing import Any
7 
8 from pyisy.constants import (
9  ATTR_ACTION,
10  CMD_BACKLIGHT,
11  DEV_BL_ADDR,
12  DEV_CMD_MEMORY_WRITE,
13  DEV_MEMORY,
14  ISY_VALUE_UNKNOWN,
15  PROP_ON_LEVEL,
16  TAG_ADDRESS,
17  UOM_PERCENTAGE,
18 )
19 from pyisy.helpers import EventListener, NodeProperty
20 from pyisy.nodes import Node, NodeChangedEvent
21 from pyisy.variables import Variable
22 
24  NumberEntity,
25  NumberEntityDescription,
26  NumberMode,
27  RestoreNumber,
28 )
29 from homeassistant.config_entries import ConfigEntry
30 from homeassistant.const import (
31  CONF_VARIABLES,
32  PERCENTAGE,
33  STATE_UNAVAILABLE,
34  STATE_UNKNOWN,
35  EntityCategory,
36  Platform,
37 )
38 from homeassistant.core import HomeAssistant, callback
39 from homeassistant.exceptions import HomeAssistantError
40 from homeassistant.helpers.device_registry import DeviceInfo
41 from homeassistant.helpers.entity_platform import AddEntitiesCallback
43  percentage_to_ranged_value,
44  ranged_value_to_percentage,
45 )
46 
47 from .const import (
48  CONF_VAR_SENSOR_STRING,
49  DEFAULT_VAR_SENSOR_STRING,
50  DOMAIN,
51  UOM_8_BIT_RANGE,
52 )
53 from .entity import ISYAuxControlEntity
54 from .helpers import convert_isy_value_to_hass
55 from .models import IsyData
56 
57 ISY_MAX_SIZE = (2**32) / 2
58 ON_RANGE = (1, 255) # Off is not included
59 CONTROL_DESC = {
60  PROP_ON_LEVEL: NumberEntityDescription(
61  key=PROP_ON_LEVEL,
62  native_unit_of_measurement=PERCENTAGE,
63  entity_category=EntityCategory.CONFIG,
64  native_min_value=1.0,
65  native_max_value=100.0,
66  native_step=1.0,
67  ),
68  CMD_BACKLIGHT: NumberEntityDescription(
69  key=CMD_BACKLIGHT,
70  native_unit_of_measurement=PERCENTAGE,
71  entity_category=EntityCategory.CONFIG,
72  native_min_value=0.0,
73  native_max_value=100.0,
74  native_step=1.0,
75  ),
76 }
77 BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE}
78 
79 
81  hass: HomeAssistant,
82  config_entry: ConfigEntry,
83  async_add_entities: AddEntitiesCallback,
84 ) -> None:
85  """Set up ISY/IoX number entities from config entry."""
86  isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id]
87  device_info = isy_data.devices
88  entities: list[
89  ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity
90  ] = []
91  var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING)
92 
93  for node in isy_data.variables[Platform.NUMBER]:
94  step = 10 ** (-1 * int(node.prec))
95  min_max = ISY_MAX_SIZE / (10 ** int(node.prec))
96  description = NumberEntityDescription(
97  key=node.address,
98  name=node.name,
99  entity_registry_enabled_default=var_id in node.name,
100  native_unit_of_measurement=None,
101  native_step=step,
102  native_min_value=-min_max,
103  native_max_value=min_max,
104  )
105  description_init = replace(
106  description,
107  key=f"{node.address}_init",
108  name=f"{node.name} Initial Value",
109  entity_category=EntityCategory.CONFIG,
110  )
111 
112  entities.append(
114  node,
115  unique_id=isy_data.uid_base(node),
116  description=description,
117  device_info=device_info[CONF_VARIABLES],
118  )
119  )
120  entities.append(
122  node=node,
123  unique_id=f"{isy_data.uid_base(node)}_init",
124  description=description_init,
125  device_info=device_info[CONF_VARIABLES],
126  init_entity=True,
127  )
128  )
129 
130  for node, control in isy_data.aux_properties[Platform.NUMBER]:
131  entity_init_info = {
132  "node": node,
133  "control": control,
134  "unique_id": f"{isy_data.uid_base(node)}_{control}",
135  "description": CONTROL_DESC[control],
136  "device_info": device_info.get(node.primary_node),
137  }
138  if control == CMD_BACKLIGHT:
139  entities.append(ISYBacklightNumberEntity(**entity_init_info))
140  continue
141  entities.append(ISYAuxControlNumberEntity(**entity_init_info))
142  async_add_entities(entities)
143 
144 
146  """Representation of a ISY/IoX Aux Control Number entity."""
147 
148  _attr_mode = NumberMode.SLIDER
149 
150  @property
151  def native_value(self) -> float | int | None:
152  """Return the state of the variable."""
153  node_prop: NodeProperty = self._node_node.aux_properties[self._control_control_control]
154  if node_prop.value == ISY_VALUE_UNKNOWN:
155  return None
156 
157  if (
158  self.entity_descriptionentity_description.native_unit_of_measurement == PERCENTAGE
159  and node_prop.uom == UOM_8_BIT_RANGE # Insteon 0-255
160  ):
161  return ranged_value_to_percentage(ON_RANGE, node_prop.value)
162  return int(node_prop.value)
163 
164  async def async_set_native_value(self, value: float) -> None:
165  """Update the current value."""
166  node_prop: NodeProperty = self._node_node.aux_properties[self._control_control_control]
167 
168  if self.entity_descriptionentity_description.native_unit_of_measurement == PERCENTAGE:
169  value = (
170  percentage_to_ranged_value(ON_RANGE, round(value))
171  if node_prop.uom == UOM_8_BIT_RANGE
172  else value
173  )
174  if self._control_control_control == PROP_ON_LEVEL:
175  await self._node_node.set_on_level(value)
176  return
177 
178  if not await self._node_node.send_cmd(self._control_control_control, val=value, uom=node_prop.uom):
179  raise HomeAssistantError(
180  f"Could not set {self.name} to {value} for {self._node.address}"
181  )
182 
183 
185  """Representation of an ISY variable as a number entity device."""
186 
187  _attr_has_entity_name = False
188  _attr_should_poll = False
189  _init_entity: bool
190  _node: Variable
191  entity_description: NumberEntityDescription
192 
193  def __init__(
194  self,
195  node: Variable,
196  unique_id: str,
197  description: NumberEntityDescription,
198  device_info: DeviceInfo,
199  init_entity: bool = False,
200  ) -> None:
201  """Initialize the ISY variable number."""
202  self._node_node = node
203  self.entity_descriptionentity_description = description
204  self._change_handler_change_handler: EventListener | None = None
205 
206  # Two entities are created for each variable, one for current value and one for initial.
207  # Initial value entities are disabled by default
208  self._init_entity_init_entity = init_entity
209  self._attr_unique_id_attr_unique_id = unique_id
210  self._attr_device_info_attr_device_info = device_info
211 
212  async def async_added_to_hass(self) -> None:
213  """Subscribe to the node change events."""
214  self._change_handler_change_handler = self._node_node.status_events.subscribe(self.async_on_updateasync_on_update)
215 
216  @callback
217  def async_on_update(self, event: NodeProperty) -> None:
218  """Handle the update event from the ISY Node."""
219  self.async_write_ha_stateasync_write_ha_state()
220 
221  @property
222  def native_value(self) -> float | int | None:
223  """Return the state of the variable."""
225  self._node_node.init if self._init_entity_init_entity else self._node_node.status,
226  "",
227  self._node_node.prec,
228  )
229 
230  @property
231  def extra_state_attributes(self) -> dict[str, Any]:
232  """Get the state attributes for the device."""
233  return {
234  "last_edited": self._node_node.last_edited,
235  }
236 
237  async def async_set_native_value(self, value: float) -> None:
238  """Set new value."""
239  if not await self._node_node.set_value(value, init=self._init_entity_init_entity):
240  raise HomeAssistantError(
241  f"Could not set {self.name} to {value} for {self._node.address}"
242  )
243 
244 
246  """Representation of a ISY/IoX Backlight Number entity."""
247 
248  _assumed_state = True # Backlight values aren't read from device
249 
250  def __init__(
251  self,
252  node: Node,
253  control: str,
254  unique_id: str,
255  description: NumberEntityDescription,
256  device_info: DeviceInfo | None,
257  ) -> None:
258  """Initialize the ISY Backlight number entity."""
259  super().__init__(node, control, unique_id, description, device_info)
260  self._memory_change_handler_memory_change_handler: EventListener | None = None
261  self._attr_native_value_attr_native_value = 0
262 
263  async def async_added_to_hass(self) -> None:
264  """Load the last known state when added to hass."""
265  await super().async_added_to_hass()
266  if (last_state := await self.async_get_last_stateasync_get_last_state()) and (
267  last_number_data := await self.async_get_last_number_dataasync_get_last_number_data()
268  ):
269  if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
270  self._attr_native_value_attr_native_value = last_number_data.native_value
271 
272  # Listen to memory writing events to update state if changed in ISY
273  self._memory_change_handler_memory_change_handler = self._node_node.isy.nodes.status_events.subscribe(
274  self.async_on_memory_writeasync_on_memory_write,
275  event_filter={
276  TAG_ADDRESS: self._node_node.address,
277  ATTR_ACTION: DEV_MEMORY,
278  },
279  key=self.unique_idunique_id,
280  )
281 
282  @callback
283  def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None:
284  """Handle a memory write event from the ISY Node."""
285  if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()):
286  return # This was not a backlight event
287  value = ranged_value_to_percentage((0, 127), event.event_info["value"])
288  if value == self._attr_native_value_attr_native_value:
289  return # Change was from this entity, don't update twice
290  self._attr_native_value_attr_native_value = value
291  self.async_write_ha_stateasync_write_ha_state()
292 
293  async def async_set_native_value(self, value: float) -> None:
294  """Update the current value."""
295 
296  if not await self._node_node.send_cmd(
297  CMD_BACKLIGHT, val=int(value), uom=UOM_PERCENTAGE
298  ):
299  raise HomeAssistantError(
300  f"Could not set backlight to {value}% for {self._node.address}"
301  )
302  self._attr_native_value_attr_native_value = value
303  self.async_write_ha_stateasync_write_ha_state()
None __init__(self, Node node, str control, str unique_id, NumberEntityDescription description, DeviceInfo|None device_info)
Definition: number.py:257
None async_on_memory_write(self, NodeChangedEvent event, str key)
Definition: number.py:283
None __init__(self, Variable node, str unique_id, NumberEntityDescription description, DeviceInfo device_info, bool init_entity=False)
Definition: number.py:200
NumberExtraStoredData|None async_get_last_number_data(self)
Definition: __init__.py:549
float|int|None convert_isy_value_to_hass(float|None value, str|None uom, int|str precision, int|None fallback_precision=None)
Definition: helpers.py:438
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: number.py:84
float percentage_to_ranged_value(tuple[float, float] low_high_range, float percentage)
Definition: percentage.py:81
int ranged_value_to_percentage(tuple[float, float] low_high_range, float value)
Definition: percentage.py:64