Home Assistant Unofficial Reference 2024.12.1
device_condition.py
Go to the documentation of this file.
1 """Provide the device conditions for Z-Wave JS."""
2 
3 from __future__ import annotations
4 
5 from typing import cast
6 
7 import voluptuous as vol
8 from zwave_js_server.const import CommandClass
9 from zwave_js_server.model.value import ConfigurationValue
10 
11 from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
12 from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
13 from homeassistant.core import HomeAssistant, callback
14 from homeassistant.exceptions import HomeAssistantError
15 from homeassistant.helpers import condition, config_validation as cv
16 from homeassistant.helpers.typing import ConfigType, TemplateVarsType
17 
18 from .config_validation import VALUE_SCHEMA
19 from .const import (
20  ATTR_COMMAND_CLASS,
21  ATTR_ENDPOINT,
22  ATTR_PROPERTY,
23  ATTR_PROPERTY_KEY,
24  ATTR_VALUE,
25  DOMAIN,
26 )
27 from .device_automation_helpers import (
28  CONF_SUBTYPE,
29  CONF_VALUE_ID,
30  NODE_STATUSES,
31  async_bypass_dynamic_config_validation,
32  generate_config_parameter_subtype,
33 )
34 from .helpers import (
35  async_get_node_from_device_id,
36  check_type_schema_map,
37  get_value_state_schema,
38  get_zwave_value_from_config,
39  remove_keys_with_empty_values,
40 )
41 
42 CONF_STATUS = "status"
43 
44 NODE_STATUS_TYPE = "node_status"
45 CONFIG_PARAMETER_TYPE = "config_parameter"
46 VALUE_TYPE = "value"
47 CONDITION_TYPES = {NODE_STATUS_TYPE, CONFIG_PARAMETER_TYPE, VALUE_TYPE}
48 
49 NODE_STATUS_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
50  {
51  vol.Required(CONF_TYPE): NODE_STATUS_TYPE,
52  vol.Required(CONF_STATUS): vol.In(NODE_STATUSES),
53  }
54 )
55 
56 CONFIG_PARAMETER_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
57  {
58  vol.Required(CONF_TYPE): CONFIG_PARAMETER_TYPE,
59  vol.Required(CONF_VALUE_ID): cv.string,
60  vol.Required(CONF_SUBTYPE): cv.string,
61  vol.Optional(ATTR_VALUE): vol.Coerce(int),
62  }
63 )
64 
65 VALUE_CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend(
66  {
67  vol.Required(CONF_TYPE): VALUE_TYPE,
68  vol.Required(ATTR_COMMAND_CLASS): vol.In([cc.value for cc in CommandClass]),
69  vol.Required(ATTR_PROPERTY): vol.Any(vol.Coerce(int), cv.string),
70  vol.Optional(ATTR_PROPERTY_KEY): vol.Any(vol.Coerce(int), cv.string),
71  vol.Optional(ATTR_ENDPOINT): vol.Coerce(int),
72  vol.Required(ATTR_VALUE): VALUE_SCHEMA,
73  }
74 )
75 
76 TYPE_SCHEMA_MAP = {
77  NODE_STATUS_TYPE: NODE_STATUS_CONDITION_SCHEMA,
78  CONFIG_PARAMETER_TYPE: CONFIG_PARAMETER_CONDITION_SCHEMA,
79  VALUE_TYPE: VALUE_CONDITION_SCHEMA,
80 }
81 
82 
83 CONDITION_TYPE_SCHEMA = vol.Schema(
84  {vol.Required(CONF_TYPE): vol.In(TYPE_SCHEMA_MAP)}, extra=vol.ALLOW_EXTRA
85 )
86 
87 CONDITION_SCHEMA = vol.All(
88  remove_keys_with_empty_values,
89  CONDITION_TYPE_SCHEMA,
90  check_type_schema_map(TYPE_SCHEMA_MAP),
91 )
92 
93 
95  hass: HomeAssistant, config: ConfigType
96 ) -> ConfigType:
97  """Validate config."""
98  config = CONDITION_SCHEMA(config)
99 
100  # We return early if the config entry for this device is not ready because we can't
101  # validate the value without knowing the state of the device
102  try:
103  bypass_dynamic_config_validation = async_bypass_dynamic_config_validation(
104  hass, config[CONF_DEVICE_ID]
105  )
106  except ValueError as err:
108  f"Device {config[CONF_DEVICE_ID]} not found"
109  ) from err
110 
111  if bypass_dynamic_config_validation:
112  return config
113 
114  if config[CONF_TYPE] == VALUE_TYPE:
115  try:
116  node = async_get_node_from_device_id(hass, config[CONF_DEVICE_ID])
117  get_zwave_value_from_config(node, config)
118  except vol.Invalid as err:
119  raise InvalidDeviceAutomationConfig(err.msg) from err
120 
121  return config
122 
123 
125  hass: HomeAssistant, device_id: str
126 ) -> list[dict[str, str]]:
127  """List device conditions for Z-Wave JS devices."""
128  conditions: list[dict] = []
129  base_condition = {
130  CONF_CONDITION: "device",
131  CONF_DEVICE_ID: device_id,
132  CONF_DOMAIN: DOMAIN,
133  }
134  node = async_get_node_from_device_id(hass, device_id)
135 
136  if node.client.driver and node.client.driver.controller.own_node == node:
137  return conditions
138 
139  # Any value's value condition
140  conditions.append({**base_condition, CONF_TYPE: VALUE_TYPE})
141 
142  # Node status conditions
143  conditions.append({**base_condition, CONF_TYPE: NODE_STATUS_TYPE})
144 
145  # Config parameter conditions
146  conditions.extend(
147  [
148  {
149  **base_condition,
150  CONF_VALUE_ID: config_value.value_id,
151  CONF_TYPE: CONFIG_PARAMETER_TYPE,
152  CONF_SUBTYPE: generate_config_parameter_subtype(config_value),
153  }
154  for config_value in node.get_configuration_values().values()
155  ]
156  )
157 
158  return conditions
159 
160 
161 @callback
163  hass: HomeAssistant, config: ConfigType
164 ) -> condition.ConditionCheckerType:
165  """Create a function to test a device condition."""
166  condition_type = config[CONF_TYPE]
167  device_id = config[CONF_DEVICE_ID]
168 
169  @callback
170  def test_node_status(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
171  """Test if node status is a certain state."""
172  node = async_get_node_from_device_id(hass, device_id)
173  return bool(node.status.name.lower() == config[CONF_STATUS])
174 
175  if condition_type == NODE_STATUS_TYPE:
176  return test_node_status
177 
178  @callback
179  def test_config_parameter(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
180  """Test if config parameter is a certain state."""
181  node = async_get_node_from_device_id(hass, device_id)
182  config_value = cast(ConfigurationValue, node.values[config[CONF_VALUE_ID]])
183  return bool(config_value.value == config[ATTR_VALUE])
184 
185  if condition_type == CONFIG_PARAMETER_TYPE:
186  return test_config_parameter
187 
188  @callback
189  def test_value(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
190  """Test if value is a certain state."""
191  node = async_get_node_from_device_id(hass, device_id)
192  value = get_zwave_value_from_config(node, config)
193  return bool(value.value == config[ATTR_VALUE])
194 
195  if condition_type == VALUE_TYPE:
196  return test_value
197 
198  raise HomeAssistantError(f"Unhandled condition type {condition_type}")
199 
200 
202  hass: HomeAssistant, config: ConfigType
203 ) -> dict[str, vol.Schema]:
204  """List condition capabilities."""
205  device_id = config[CONF_DEVICE_ID]
206  node = async_get_node_from_device_id(hass, device_id)
207 
208  # Add additional fields to the automation trigger UI
209  if config[CONF_TYPE] == CONFIG_PARAMETER_TYPE:
210  value_id = config[CONF_VALUE_ID]
211  value_schema = get_value_state_schema(node.values[value_id])
212  if value_schema is None:
213  return {}
214  return {"extra_fields": vol.Schema({vol.Required(ATTR_VALUE): value_schema})}
215 
216  if config[CONF_TYPE] == VALUE_TYPE:
217  # Only show command classes on this node and exclude Configuration CC since it
218  # is already covered
219  return {
220  "extra_fields": vol.Schema(
221  {
222  vol.Required(ATTR_COMMAND_CLASS): vol.In(
223  {
224  CommandClass(cc.id).value: cc.name
225  for cc in sorted(
226  node.command_classes, key=lambda cc: cc.name
227  )
228  if cc.id != CommandClass.CONFIGURATION
229  }
230  ),
231  vol.Required(ATTR_PROPERTY): cv.string,
232  vol.Optional(ATTR_PROPERTY_KEY): cv.string,
233  vol.Optional(ATTR_ENDPOINT): cv.string,
234  vol.Required(ATTR_VALUE): cv.string,
235  }
236  )
237  }
238 
239  if config[CONF_TYPE] == NODE_STATUS_TYPE:
240  return {
241  "extra_fields": vol.Schema(
242  {vol.Required(CONF_STATUS): vol.In(NODE_STATUSES)}
243  )
244  }
245 
246  return {}
bool async_bypass_dynamic_config_validation(HomeAssistant hass, str device_id)
ConfigType async_validate_condition_config(HomeAssistant hass, ConfigType config)
condition.ConditionCheckerType async_condition_from_config(HomeAssistant hass, ConfigType config)
dict[str, vol.Schema] async_get_condition_capabilities(HomeAssistant hass, ConfigType config)
list[dict[str, str]] async_get_conditions(HomeAssistant hass, str device_id)
Callable[[ConfigType], ConfigType] check_type_schema_map(dict[str, vol.Schema] schema_map)
Definition: helpers.py:461
VolSchemaType|vol.Coerce|vol.In|None get_value_state_schema(ZwaveValue value)
Definition: helpers.py:482
ZwaveValue get_zwave_value_from_config(ZwaveNode node, ConfigType config)
Definition: helpers.py:396
ZwaveNode async_get_node_from_device_id(HomeAssistant hass, str device_id, dr.DeviceRegistry|None dev_reg=None)
Definition: helpers.py:253