Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helpers for LCN component."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from copy import deepcopy
7 from itertools import chain
8 import re
9 from typing import cast
10 
11 import pypck
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import (
15  CONF_ADDRESS,
16  CONF_BINARY_SENSORS,
17  CONF_COVERS,
18  CONF_DEVICES,
19  CONF_DOMAIN,
20  CONF_ENTITIES,
21  CONF_LIGHTS,
22  CONF_NAME,
23  CONF_RESOURCE,
24  CONF_SENSORS,
25  CONF_SOURCE,
26  CONF_SWITCHES,
27 )
28 from homeassistant.core import HomeAssistant
29 from homeassistant.helpers import device_registry as dr, entity_registry as er
30 from homeassistant.helpers.typing import ConfigType
31 
32 from .const import (
33  BINSENSOR_PORTS,
34  CONF_CLIMATES,
35  CONF_HARDWARE_SERIAL,
36  CONF_HARDWARE_TYPE,
37  CONF_OUTPUT,
38  CONF_SCENES,
39  CONF_SOFTWARE_SERIAL,
40  CONNECTION,
41  DOMAIN,
42  LED_PORTS,
43  LOGICOP_PORTS,
44  OUTPUT_PORTS,
45  S0_INPUTS,
46  SETPOINTS,
47  THRESHOLDS,
48  VARIABLES,
49 )
50 
51 # typing
52 type AddressType = tuple[int, int, bool]
53 type DeviceConnectionType = pypck.module.ModuleConnection | pypck.module.GroupConnection
54 
55 type InputType = type[pypck.inputs.Input]
56 
57 # Regex for address validation
58 PATTERN_ADDRESS = re.compile(
59  "^((?P<conn_id>\\w+)\\.)?s?(?P<seg_id>\\d+)\\.(?P<type>m|g)?(?P<id>\\d+)$"
60 )
61 
62 
63 DOMAIN_LOOKUP = {
64  CONF_BINARY_SENSORS: "binary_sensor",
65  CONF_CLIMATES: "climate",
66  CONF_COVERS: "cover",
67  CONF_LIGHTS: "light",
68  CONF_SCENES: "scene",
69  CONF_SENSORS: "sensor",
70  CONF_SWITCHES: "switch",
71 }
72 
73 
75  hass: HomeAssistant, address: AddressType, config_entry: ConfigEntry
76 ) -> DeviceConnectionType:
77  """Return a lcn device_connection."""
78  host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION]
79  addr = pypck.lcn_addr.LcnAddr(*address)
80  return host_connection.get_address_conn(addr)
81 
82 
83 def get_resource(domain_name: str, domain_data: ConfigType) -> str:
84  """Return the resource for the specified domain_data."""
85  if domain_name in ("switch", "light"):
86  return cast(str, domain_data["output"])
87  if domain_name in ("binary_sensor", "sensor"):
88  return cast(str, domain_data["source"])
89  if domain_name == "cover":
90  return cast(str, domain_data["motor"])
91  if domain_name == "climate":
92  return f'{domain_data["source"]}.{domain_data["setpoint"]}'
93  if domain_name == "scene":
94  return f'{domain_data["register"]}.{domain_data["scene"]}'
95  raise ValueError("Unknown domain")
96 
97 
98 def get_device_model(domain_name: str, domain_data: ConfigType) -> str:
99  """Return the model for the specified domain_data."""
100  if domain_name in ("switch", "light"):
101  return "Output" if domain_data[CONF_OUTPUT] in OUTPUT_PORTS else "Relay"
102  if domain_name in ("binary_sensor", "sensor"):
103  if domain_data[CONF_SOURCE] in BINSENSOR_PORTS:
104  return "Binary Sensor"
105  if domain_data[CONF_SOURCE] in chain(
106  VARIABLES, SETPOINTS, THRESHOLDS, S0_INPUTS
107  ):
108  return "Variable"
109  if domain_data[CONF_SOURCE] in LED_PORTS:
110  return "Led"
111  if domain_data[CONF_SOURCE] in LOGICOP_PORTS:
112  return "Logical Operation"
113  return "Key"
114  if domain_name == "cover":
115  return "Motor"
116  if domain_name == "climate":
117  return "Regulator"
118  if domain_name == "scene":
119  return "Scene"
120  raise ValueError("Unknown domain")
121 
122 
124  entry_id: str,
125  address: AddressType,
126  resource: str | None = None,
127 ) -> str:
128  """Generate a unique_id from the given parameters."""
129  unique_id = entry_id
130  is_group = "g" if address[2] else "m"
131  unique_id += f"-{is_group}{address[0]:03d}{address[1]:03d}"
132  if resource:
133  unique_id += f"-{resource}".lower()
134  return unique_id
135 
136 
138  hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
139 ) -> None:
140  """Remove orphans from entity registry which are not in entry data."""
141  entity_registry = er.async_get(hass)
142 
143  # Find all entities that are referenced in the config entry.
144  references_config_entry = {
145  entity_entry.entity_id
146  for entity_entry in er.async_entries_for_config_entry(entity_registry, entry_id)
147  }
148 
149  # Find all entities that are referenced by the entry_data.
150  references_entry_data = set()
151  for entity_data in imported_entry_data[CONF_ENTITIES]:
152  entity_unique_id = generate_unique_id(
153  entry_id, entity_data[CONF_ADDRESS], entity_data[CONF_RESOURCE]
154  )
155  entity_id = entity_registry.async_get_entity_id(
156  entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id
157  )
158  if entity_id is not None:
159  references_entry_data.add(entity_id)
160 
161  orphaned_ids = references_config_entry - references_entry_data
162  for orphaned_id in orphaned_ids:
163  entity_registry.async_remove(orphaned_id)
164 
165 
167  hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
168 ) -> None:
169  """Remove orphans from device registry which are not in entry data."""
170  device_registry = dr.async_get(hass)
171  entity_registry = er.async_get(hass)
172 
173  # Find all devices that are referenced in the entity registry.
174  references_entities = {
175  entry.device_id
176  for entry in entity_registry.entities.get_entries_for_config_entry_id(entry_id)
177  }
178 
179  # Find device that references the host.
180  references_host = set()
181  host_device = device_registry.async_get_device(identifiers={(DOMAIN, entry_id)})
182  if host_device is not None:
183  references_host.add(host_device.id)
184 
185  # Find all devices that are referenced by the entry_data.
186  references_entry_data = set()
187  for device_data in imported_entry_data[CONF_DEVICES]:
188  device_unique_id = generate_unique_id(entry_id, device_data[CONF_ADDRESS])
189  device = device_registry.async_get_device(
190  identifiers={(DOMAIN, device_unique_id)}
191  )
192  if device is not None:
193  references_entry_data.add(device.id)
194 
195  orphaned_ids = (
196  {
197  entry.id
198  for entry in dr.async_entries_for_config_entry(device_registry, entry_id)
199  }
200  - references_entities
201  - references_host
202  - references_entry_data
203  )
204 
205  for device_id in orphaned_ids:
206  device_registry.async_remove_device(device_id)
207 
208 
209 def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
210  """Register LCN host for given config_entry in device registry."""
211  device_registry = dr.async_get(hass)
212 
213  device_registry.async_get_or_create(
214  config_entry_id=config_entry.entry_id,
215  identifiers={(DOMAIN, config_entry.entry_id)},
216  manufacturer="Issendorff",
217  name=config_entry.title,
218  model="LCN-PCHK",
219  )
220 
221 
223  hass: HomeAssistant, config_entry: ConfigEntry
224 ) -> None:
225  """Register LCN modules and groups defined in config_entry as devices in device registry.
226 
227  The name of all given device_connections is collected and the devices
228  are updated.
229  """
230  device_registry = dr.async_get(hass)
231 
232  host_identifiers = (DOMAIN, config_entry.entry_id)
233 
234  for device_config in config_entry.data[CONF_DEVICES]:
235  address = device_config[CONF_ADDRESS]
236  device_name = device_config[CONF_NAME]
237  identifiers = {(DOMAIN, generate_unique_id(config_entry.entry_id, address))}
238 
239  if device_config[CONF_ADDRESS][2]: # is group
240  device_model = f"LCN group (g{address[0]:03d}{address[1]:03d})"
241  sw_version = None
242  else: # is module
243  hardware_type = device_config[CONF_HARDWARE_TYPE]
244  if hardware_type in pypck.lcn_defs.HARDWARE_DESCRIPTIONS:
245  hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[hardware_type]
246  else:
247  hardware_name = pypck.lcn_defs.HARDWARE_DESCRIPTIONS[-1]
248  device_model = f"{hardware_name} (m{address[0]:03d}{address[1]:03d})"
249  sw_version = f"{device_config[CONF_SOFTWARE_SERIAL]:06X}"
250 
251  device_registry.async_get_or_create(
252  config_entry_id=config_entry.entry_id,
253  identifiers=identifiers,
254  via_device=host_identifiers,
255  manufacturer="Issendorff",
256  sw_version=sw_version,
257  name=device_name,
258  model=device_model,
259  )
260 
261 
263  device_connection: DeviceConnectionType, device_config: ConfigType
264 ) -> None:
265  """Fill missing values in device_config with infos from LCN bus."""
266  # fetch serial info if device is module
267  if not (is_group := device_config[CONF_ADDRESS][2]): # is module
268  await device_connection.serial_known
269  if device_config[CONF_HARDWARE_SERIAL] == -1:
270  device_config[CONF_HARDWARE_SERIAL] = device_connection.hardware_serial
271  if device_config[CONF_SOFTWARE_SERIAL] == -1:
272  device_config[CONF_SOFTWARE_SERIAL] = device_connection.software_serial
273  if device_config[CONF_HARDWARE_TYPE] == -1:
274  device_config[CONF_HARDWARE_TYPE] = device_connection.hardware_type.value
275 
276  # fetch name if device is module
277  if device_config[CONF_NAME] != "":
278  return
279 
280  device_name = ""
281  if not is_group:
282  device_name = await device_connection.request_name()
283  if is_group or device_name == "":
284  module_type = "Group" if is_group else "Module"
285  device_name = (
286  f"{module_type} "
287  f"{device_config[CONF_ADDRESS][0]:03d}/"
288  f"{device_config[CONF_ADDRESS][1]:03d}"
289  )
290  device_config[CONF_NAME] = device_name
291 
292 
294  hass: HomeAssistant, config_entry: ConfigEntry
295 ) -> None:
296  """Fill missing values in config_entry with infos from LCN bus."""
297  device_configs = deepcopy(config_entry.data[CONF_DEVICES])
298  coros = []
299  for device_config in device_configs:
300  device_connection = get_device_connection(
301  hass, device_config[CONF_ADDRESS], config_entry
302  )
303  coros.append(async_update_device_config(device_connection, device_config))
304 
305  await asyncio.gather(*coros)
306 
307  new_data = {**config_entry.data, CONF_DEVICES: device_configs}
308 
309  # schedule config_entry for save
310  hass.config_entries.async_update_entry(config_entry, data=new_data)
311 
312 
314  address: AddressType, config_entry: ConfigEntry
315 ) -> ConfigType | None:
316  """Return the device configuration for given address and ConfigEntry."""
317  for device_config in config_entry.data[CONF_DEVICES]:
318  if tuple(device_config[CONF_ADDRESS]) == address:
319  return cast(ConfigType, device_config)
320  return None
321 
322 
323 def is_address(value: str) -> tuple[AddressType, str]:
324  """Validate the given address string.
325 
326  Examples for S000M005 at myhome:
327  myhome.s000.m005
328  myhome.s0.m5
329  myhome.0.5 ("m" is implicit if missing)
330 
331  Examples for s000g011
332  myhome.0.g11
333  myhome.s0.g11
334  """
335  if matcher := PATTERN_ADDRESS.match(value):
336  is_group = matcher.group("type") == "g"
337  addr = (int(matcher.group("seg_id")), int(matcher.group("id")), is_group)
338  conn_id = matcher.group("conn_id")
339  return addr, conn_id
340  raise ValueError(f"{value} is not a valid address string")
341 
342 
343 def is_states_string(states_string: str) -> list[str]:
344  """Validate the given states string and return states list."""
345  if len(states_string) != 8:
346  raise ValueError("Invalid length of states string")
347  states = {"1": "ON", "0": "OFF", "T": "TOGGLE", "-": "NOCHANGE"}
348  return [states[state_string] for state_string in states_string]
ConfigType|None get_device_config(AddressType address, ConfigEntry config_entry)
Definition: helpers.py:315
str generate_unique_id(str entry_id, AddressType address, str|None resource=None)
Definition: helpers.py:127
None purge_device_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
Definition: helpers.py:168
None async_update_device_config(DeviceConnectionType device_connection, ConfigType device_config)
Definition: helpers.py:264
tuple[AddressType, str] is_address(str value)
Definition: helpers.py:323
list[str] is_states_string(str states_string)
Definition: helpers.py:343
None register_lcn_host_device(HomeAssistant hass, ConfigEntry config_entry)
Definition: helpers.py:209
None register_lcn_address_devices(HomeAssistant hass, ConfigEntry config_entry)
Definition: helpers.py:224
None purge_entity_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
Definition: helpers.py:139
DeviceConnectionType get_device_connection(HomeAssistant hass, AddressType address, ConfigEntry config_entry)
Definition: helpers.py:76
str get_device_model(str domain_name, ConfigType domain_data)
Definition: helpers.py:98
str get_resource(str domain_name, ConfigType domain_data)
Definition: helpers.py:83
None async_update_config_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: helpers.py:295