Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Generic Z-Wave Entity Class."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Sequence
6 from typing import Any
7 
8 from zwave_js_server.const import NodeStatus
9 from zwave_js_server.exceptions import BaseZwaveJSServerError
10 from zwave_js_server.model.driver import Driver
11 from zwave_js_server.model.value import (
12  SetValueResult,
13  Value as ZwaveValue,
14  get_value_id_str,
15 )
16 
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.core import callback
19 from homeassistant.exceptions import HomeAssistantError
20 from homeassistant.helpers.device_registry import DeviceInfo
21 from homeassistant.helpers.dispatcher import async_dispatcher_connect
22 from homeassistant.helpers.entity import Entity
23 from homeassistant.helpers.typing import UNDEFINED
24 
25 from .const import DOMAIN, EVENT_VALUE_UPDATED, LOGGER
26 from .discovery import ZwaveDiscoveryInfo
27 from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
28 
29 EVENT_VALUE_REMOVED = "value removed"
30 EVENT_DEAD = "dead"
31 EVENT_ALIVE = "alive"
32 
33 
35  """Generic Entity Class for a Z-Wave Device."""
36 
37  _attr_should_poll = False
38  _attr_has_entity_name = True
39 
40  def __init__(
41  self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
42  ) -> None:
43  """Initialize a generic Z-Wave device entity."""
44  self.config_entryconfig_entry = config_entry
45  self.driverdriver = driver
46  self.infoinfo = info
47  # entities requiring additional values, can add extra ids to this list
48  self.watched_value_idswatched_value_ids = {self.infoinfo.primary_value.value_id}
49 
50  if self.infoinfo.additional_value_ids_to_watch:
51  self.watched_value_idswatched_value_ids = self.watched_value_idswatched_value_ids.union(
52  self.infoinfo.additional_value_ids_to_watch
53  )
54 
55  # Entity class attributes
56  self._attr_name_attr_name = self.generate_namegenerate_name()
57  self._attr_unique_id_attr_unique_id = get_unique_id(driver, self.infoinfo.primary_value.value_id)
58  if self.infoinfo.entity_registry_enabled_default is False:
59  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = False
60  if self.infoinfo.entity_category is not None:
61  self._attr_entity_category_attr_entity_category = self.infoinfo.entity_category
62  if self.infoinfo.assumed_state:
63  self._attr_assumed_state_attr_assumed_state = True
64  # device is precreated in main handler
65  self._attr_device_info_attr_device_info = DeviceInfo(
66  identifiers={get_device_id(driver, self.infoinfo.node)},
67  )
68 
69  @callback
70  def on_value_update(self) -> None:
71  """Call when one of the watched values change.
72 
73  To be overridden by platforms needing this event.
74  """
75 
76  async def _async_poll_value(self, value_or_id: str | ZwaveValue) -> None:
77  """Poll a value."""
78  # We log an error instead of raising an exception because this service call
79  # occurs in a separate task and we don't want to raise the exception in that
80  # separate task because it is confusing to the user.
81  try:
82  await self.info.node.async_poll_value(value_or_id)
83  except BaseZwaveJSServerError as err:
84  LOGGER.error("Error while refreshing value %s: %s", value_or_id, err)
85 
86  async def async_poll_value(self, refresh_all_values: bool) -> None:
87  """Poll a value."""
88  if not refresh_all_values:
89  await self._async_poll_value_async_poll_value(self.infoinfo.primary_value)
90  LOGGER.info(
91  (
92  "Refreshing primary value %s for %s, "
93  "state update may be delayed for devices on battery"
94  ),
95  self.infoinfo.primary_value,
96  self.entity_identity_id,
97  )
98  return
99 
100  for value_id in self.watched_value_idswatched_value_ids:
101  await self._async_poll_value_async_poll_value(value_id)
102 
103  LOGGER.info(
104  (
105  "Refreshing values %s for %s, state update may be delayed for "
106  "devices on battery"
107  ),
108  ", ".join(self.watched_value_idswatched_value_ids),
109  self.entity_identity_id,
110  )
111 
112  async def async_added_to_hass(self) -> None:
113  """Call when entity is added."""
114  # Add value_changed callbacks.
115  self.async_on_removeasync_on_remove(
116  self.infoinfo.node.on(EVENT_VALUE_UPDATED, self._value_changed_value_changed)
117  )
118  self.async_on_removeasync_on_remove(
119  self.infoinfo.node.on(EVENT_VALUE_REMOVED, self._value_removed_value_removed)
120  )
121  self.async_on_removeasync_on_remove(
123  self.hasshass,
124  (
125  f"{DOMAIN}_"
126  f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
127  "remove_entity"
128  ),
129  self.async_removeasync_remove,
130  )
131  )
132  self.async_on_removeasync_on_remove(
134  self.hasshass,
135  (
136  f"{DOMAIN}_"
137  f"{get_valueless_base_unique_id(self.driver, self.info.node)}_"
138  "remove_entity_on_interview_started"
139  ),
140  self.async_removeasync_remove,
141  )
142  )
143 
144  for status_event in (EVENT_ALIVE, EVENT_DEAD):
145  self.async_on_removeasync_on_remove(
146  self.infoinfo.node.on(status_event, self._node_status_alive_or_dead_node_status_alive_or_dead)
147  )
148 
149  self.async_on_removeasync_on_remove(
151  self.hasshass,
152  f"{DOMAIN}_{self.unique_id}_poll_value",
153  self.async_poll_valueasync_poll_value,
154  )
155  )
156 
158  self,
159  include_value_name: bool = False,
160  alternate_value_name: str | None = None,
161  additional_info: Sequence[str | None] | None = None,
162  name_prefix: str | None = None,
163  ) -> str:
164  """Generate entity name."""
165  primary_value = self.infoinfo.primary_value
166  name = ""
167  if (
168  hasattr(self, "entity_description")
169  and self.entity_description
170  and self.entity_description.name
171  and self.entity_description.name is not UNDEFINED
172  ):
173  name = self.entity_description.name
174 
175  if name_prefix:
176  name = f"{name_prefix} {name}".strip()
177 
178  value_name = ""
179  if alternate_value_name:
180  value_name = alternate_value_name
181  elif include_value_name:
182  value_name = (
183  primary_value.metadata.label
184  or primary_value.property_key_name
185  or primary_value.property_name
186  or ""
187  )
188 
189  name = f"{name} {value_name}".strip()
190  # Only include non empty additional info
191  if additional_info := [item for item in (additional_info or []) if item]:
192  name = f"{name} {' '.join(additional_info)}"
193 
194  # Only append endpoint to name if there are equivalent values on a lower
195  # endpoint
196  if primary_value.endpoint is not None and any(
197  get_value_id_str(
198  self.infoinfo.node,
199  primary_value.command_class,
200  primary_value.property_,
201  endpoint=endpoint_idx,
202  property_key=primary_value.property_key,
203  )
204  in self.infoinfo.node.values
205  for endpoint_idx in range(primary_value.endpoint)
206  ):
207  name += f" ({primary_value.endpoint})"
208 
209  return name.strip()
210 
211  @property
212  def available(self) -> bool:
213  """Return entity availability."""
214  return (
215  self.driverdriver.client.connected
216  and bool(self.infoinfo.node.ready)
217  and self.infoinfo.node.status != NodeStatus.DEAD
218  )
219 
220  @callback
221  def _node_status_alive_or_dead(self, event_data: dict) -> None:
222  """Call when node status changes to alive or dead.
223 
224  Should not be overridden by subclasses.
225  """
226  self.async_write_ha_stateasync_write_ha_state()
227 
228  @callback
229  def _value_changed(self, event_data: dict) -> None:
230  """Call when a value associated with our node changes.
231 
232  Should not be overridden by subclasses.
233  """
234  value_id = event_data["value"].value_id
235 
236  if value_id not in self.watched_value_idswatched_value_ids:
237  return
238 
239  value = self.infoinfo.node.values[value_id]
240 
241  LOGGER.debug(
242  "[%s] Value %s/%s changed to: %s",
243  self.entity_identity_id,
244  value.property_,
245  value.property_key_name,
246  value.value,
247  )
248 
249  self.on_value_updateon_value_update()
250  self.async_write_ha_stateasync_write_ha_state()
251 
252  @callback
253  def _value_removed(self, event_data: dict) -> None:
254  """Call when a value associated with our node is removed.
255 
256  Should not be overridden by subclasses.
257  """
258  value_id = event_data["value"].value_id
259 
260  if value_id != self.infoinfo.primary_value.value_id:
261  return
262 
263  LOGGER.debug(
264  "[%s] Primary value %s is being removed",
265  self.entity_identity_id,
266  value_id,
267  )
268 
269  self.hasshass.async_create_task(self.async_removeasync_remove())
270 
271  @callback
273  self,
274  value_property: str | int,
275  command_class: int | None = None,
276  endpoint: int | None = None,
277  value_property_key: int | str | None = None,
278  add_to_watched_value_ids: bool = True,
279  check_all_endpoints: bool = False,
280  ) -> ZwaveValue | None:
281  """Return specific ZwaveValue on this ZwaveNode."""
282  # use commandclass and endpoint from primary value if omitted
283  return_value = None
284  if command_class is None:
285  command_class = self.infoinfo.primary_value.command_class
286  if endpoint is None:
287  endpoint = self.infoinfo.primary_value.endpoint
288 
289  # lookup value by value_id
290  value_id = get_value_id_str(
291  self.infoinfo.node,
292  command_class,
293  value_property,
294  endpoint=endpoint,
295  property_key=value_property_key,
296  )
297  return_value = self.infoinfo.node.values.get(value_id)
298 
299  # If we haven't found a value and check_all_endpoints is True, we should
300  # return the first value we can find on any other endpoint
301  if return_value is None and check_all_endpoints:
302  for endpoint_idx in self.infoinfo.node.endpoints:
303  if endpoint_idx != self.infoinfo.primary_value.endpoint:
304  value_id = get_value_id_str(
305  self.infoinfo.node,
306  command_class,
307  value_property,
308  endpoint=endpoint_idx,
309  property_key=value_property_key,
310  )
311  return_value = self.infoinfo.node.values.get(value_id)
312  if return_value:
313  break
314 
315  # add to watched_ids list so we will be triggered when the value updates
316  if (
317  return_value
318  and return_value.value_id not in self.watched_value_idswatched_value_ids
319  and add_to_watched_value_ids
320  ):
321  self.watched_value_idswatched_value_ids.add(return_value.value_id)
322  return return_value
323 
324  async def _async_set_value(
325  self,
326  value: ZwaveValue,
327  new_value: Any,
328  options: dict | None = None,
329  wait_for_result: bool | None = None,
330  ) -> SetValueResult | None:
331  """Set value on node."""
332  try:
333  return await self.infoinfo.node.async_set_value(
334  value, new_value, options=options, wait_for_result=wait_for_result
335  )
336  except BaseZwaveJSServerError as err:
337  raise HomeAssistantError(
338  f"Unable to set value {value.value_id}: {err}"
339  ) from err
str generate_name(self, bool include_value_name=False, str|None alternate_value_name=None, Sequence[str|None]|None additional_info=None, str|None name_prefix=None)
Definition: entity.py:163
None __init__(self, ConfigEntry config_entry, Driver driver, ZwaveDiscoveryInfo info)
Definition: entity.py:42
SetValueResult|None _async_set_value(self, ZwaveValue value, Any new_value, dict|None options=None, bool|None wait_for_result=None)
Definition: entity.py:330
None _node_status_alive_or_dead(self, dict event_data)
Definition: entity.py:221
None _async_poll_value(self, str|ZwaveValue value_or_id)
Definition: entity.py:76
None async_poll_value(self, bool refresh_all_values)
Definition: entity.py:86
ZwaveValue|None get_zwave_value(self, str|int value_property, int|None command_class=None, int|None endpoint=None, int|str|None value_property_key=None, bool add_to_watched_value_ids=True, bool check_all_endpoints=False)
Definition: entity.py:280
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_remove(self, *bool force_remove=False)
Definition: entity.py:1387
bool add(self, _T matcher)
Definition: match.py:185
str get_device_id(ServerInfoMessage server_info, MatterEndpoint endpoint)
Definition: helpers.py:59
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103