Home Assistant Unofficial Reference 2024.12.1
value_updated.py
Go to the documentation of this file.
1 """Offer Z-Wave JS value updated listening automation trigger."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import functools
7 
8 import voluptuous as vol
9 from zwave_js_server.const import CommandClass
10 from zwave_js_server.model.driver import Driver
11 from zwave_js_server.model.value import Value, get_value_id_str
12 
13 from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL
14 from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
15 from homeassistant.helpers import config_validation as cv, device_registry as dr
16 from homeassistant.helpers.dispatcher import async_dispatcher_connect
17 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
18 from homeassistant.helpers.typing import ConfigType
19 
20 from ..config_validation import VALUE_SCHEMA
21 from ..const import (
22  ATTR_COMMAND_CLASS,
23  ATTR_COMMAND_CLASS_NAME,
24  ATTR_CURRENT_VALUE,
25  ATTR_CURRENT_VALUE_RAW,
26  ATTR_ENDPOINT,
27  ATTR_NODE_ID,
28  ATTR_PREVIOUS_VALUE,
29  ATTR_PREVIOUS_VALUE_RAW,
30  ATTR_PROPERTY,
31  ATTR_PROPERTY_KEY,
32  ATTR_PROPERTY_KEY_NAME,
33  ATTR_PROPERTY_NAME,
34  DOMAIN,
35  EVENT_VALUE_UPDATED,
36 )
37 from ..helpers import async_get_nodes_from_targets, get_device_id
38 from .trigger_helpers import async_bypass_dynamic_config_validation
39 
40 # Platform type should be <DOMAIN>.<SUBMODULE_NAME>
41 PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}"
42 
43 ATTR_FROM = "from"
44 ATTR_TO = "to"
45 
46 TRIGGER_SCHEMA = vol.All(
47  cv.TRIGGER_BASE_SCHEMA.extend(
48  {
49  vol.Required(CONF_PLATFORM): PLATFORM_TYPE,
50  vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
51  vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
52  vol.Required(ATTR_COMMAND_CLASS): vol.In(
53  {cc.value: cc.name for cc in CommandClass}
54  ),
55  vol.Required(ATTR_PROPERTY): vol.Any(vol.Coerce(int), cv.string),
56  vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
57  vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string),
58  vol.Optional(ATTR_FROM, default=MATCH_ALL): vol.Any(
59  VALUE_SCHEMA, [VALUE_SCHEMA]
60  ),
61  vol.Optional(ATTR_TO, default=MATCH_ALL): vol.Any(
62  VALUE_SCHEMA, [VALUE_SCHEMA]
63  ),
64  },
65  ),
66  cv.has_at_least_one_key(ATTR_ENTITY_ID, ATTR_DEVICE_ID),
67 )
68 
69 
71  hass: HomeAssistant, config: ConfigType
72 ) -> ConfigType:
73  """Validate config."""
74  config = TRIGGER_SCHEMA(config)
75 
77  return config
78 
79  if not async_get_nodes_from_targets(hass, config):
80  raise vol.Invalid(
81  f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
82  )
83  return config
84 
85 
87  hass: HomeAssistant,
88  config: ConfigType,
89  action: TriggerActionType,
90  trigger_info: TriggerInfo,
91  *,
92  platform_type: str = PLATFORM_TYPE,
93 ) -> CALLBACK_TYPE:
94  """Listen for state changes based on configuration."""
95  dev_reg = dr.async_get(hass)
96  if not async_get_nodes_from_targets(hass, config, dev_reg=dev_reg):
97  raise ValueError(
98  f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s."
99  )
100 
101  from_value = config[ATTR_FROM]
102  to_value = config[ATTR_TO]
103  command_class = config[ATTR_COMMAND_CLASS]
104  property_ = config[ATTR_PROPERTY]
105  endpoint = config.get(ATTR_ENDPOINT)
106  property_key = config.get(ATTR_PROPERTY_KEY)
107  unsubs: list[Callable] = []
108  job = HassJob(action)
109 
110  trigger_data = trigger_info["trigger_data"]
111 
112  @callback
113  def async_on_value_updated(
114  value: Value, device: dr.DeviceEntry, event: dict
115  ) -> None:
116  """Handle value update."""
117  event_value: Value = event["value"]
118  if event_value != value:
119  return
120 
121  # Get previous value and its state value if it exists
122  prev_value_raw = event["args"]["prevValue"]
123  prev_value = value.metadata.states.get(str(prev_value_raw), prev_value_raw)
124  # Get current value and its state value if it exists
125  curr_value_raw = event["args"]["newValue"]
126  curr_value = value.metadata.states.get(str(curr_value_raw), curr_value_raw)
127  # Check from and to values against previous and current values respectively
128  for value_to_eval, raw_value_to_eval, match in (
129  (prev_value, prev_value_raw, from_value),
130  (curr_value, curr_value_raw, to_value),
131  ):
132  if match not in (MATCH_ALL, value_to_eval, raw_value_to_eval) and not (
133  isinstance(match, list)
134  and (value_to_eval in match or raw_value_to_eval in match)
135  ):
136  return
137 
138  device_name = device.name_by_user or device.name
139 
140  payload = {
141  **trigger_data,
142  CONF_PLATFORM: platform_type,
143  ATTR_DEVICE_ID: device.id,
144  ATTR_NODE_ID: value.node.node_id,
145  ATTR_COMMAND_CLASS: value.command_class,
146  ATTR_COMMAND_CLASS_NAME: value.command_class_name,
147  ATTR_PROPERTY: value.property_,
148  ATTR_PROPERTY_NAME: value.property_name,
149  ATTR_ENDPOINT: endpoint,
150  ATTR_PROPERTY_KEY: value.property_key,
151  ATTR_PROPERTY_KEY_NAME: value.property_key_name,
152  ATTR_PREVIOUS_VALUE: prev_value,
153  ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
154  ATTR_CURRENT_VALUE: curr_value,
155  ATTR_CURRENT_VALUE_RAW: curr_value_raw,
156  "description": f"Z-Wave value {value.value_id} updated on {device_name}",
157  }
158 
159  hass.async_run_hass_job(job, {"trigger": payload})
160 
161  @callback
162  def async_remove() -> None:
163  """Remove state listeners async."""
164  for unsub in unsubs:
165  unsub()
166  unsubs.clear()
167 
168  def _create_zwave_listeners() -> None:
169  """Create Z-Wave JS listeners."""
170  async_remove()
171  # Nodes list can come from different drivers and we will need to listen to
172  # server connections for all of them.
173  drivers: set[Driver] = set()
174  for node in async_get_nodes_from_targets(hass, config, dev_reg=dev_reg):
175  driver = node.client.driver
176  assert driver is not None # The node comes from the driver.
177  drivers.add(driver)
178  device_identifier = get_device_id(driver, node)
179  device = dev_reg.async_get_device(identifiers={device_identifier})
180  assert device
181  value_id = get_value_id_str(
182  node, command_class, property_, endpoint, property_key
183  )
184  value = node.values[value_id]
185  # We need to store the current value and device for the callback
186  unsubs.append(
187  node.on(
188  EVENT_VALUE_UPDATED,
189  functools.partial(async_on_value_updated, value, device),
190  )
191  )
192 
193  unsubs.extend(
195  hass,
196  f"{DOMAIN}_{driver.controller.home_id}_connected_to_server",
197  _create_zwave_listeners,
198  )
199  for driver in drivers
200  )
201 
202  _create_zwave_listeners()
203 
204  return async_remove
str get_device_id(ServerInfoMessage server_info, MatterEndpoint endpoint)
Definition: helpers.py:59
bool async_bypass_dynamic_config_validation(HomeAssistant hass, str device_id)
set[ZwaveNode] async_get_nodes_from_targets(HomeAssistant hass, dict[str, Any] val, er.EntityRegistry|None ent_reg=None, dr.DeviceRegistry|None dev_reg=None, logging.Logger logger=LOGGER)
Definition: helpers.py:369
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info, *str platform_type=PLATFORM_TYPE)
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
None async_remove(HomeAssistant hass, str intent_type)
Definition: intent.py:90