1 """Helpers for LCN component."""
3 from __future__
import annotations
6 from copy
import deepcopy
7 from itertools
import chain
9 from typing
import cast
52 type AddressType = tuple[int, int, bool]
53 type DeviceConnectionType = pypck.module.ModuleConnection | pypck.module.GroupConnection
55 type InputType = type[pypck.inputs.Input]
58 PATTERN_ADDRESS = re.compile(
59 "^((?P<conn_id>\\w+)\\.)?s?(?P<seg_id>\\d+)\\.(?P<type>m|g)?(?P<id>\\d+)$"
64 CONF_BINARY_SENSORS:
"binary_sensor",
65 CONF_CLIMATES:
"climate",
69 CONF_SENSORS:
"sensor",
70 CONF_SWITCHES:
"switch",
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)
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")
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
109 if domain_data[CONF_SOURCE]
in LED_PORTS:
111 if domain_data[CONF_SOURCE]
in LOGICOP_PORTS:
112 return "Logical Operation"
114 if domain_name ==
"cover":
116 if domain_name ==
"climate":
118 if domain_name ==
"scene":
120 raise ValueError(
"Unknown domain")
125 address: AddressType,
126 resource: str |
None =
None,
128 """Generate a unique_id from the given parameters."""
130 is_group =
"g" if address[2]
else "m"
131 unique_id += f
"-{is_group}{address[0]:03d}{address[1]:03d}"
133 unique_id += f
"-{resource}".lower()
138 hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
140 """Remove orphans from entity registry which are not in entry data."""
141 entity_registry = er.async_get(hass)
144 references_config_entry = {
145 entity_entry.entity_id
146 for entity_entry
in er.async_entries_for_config_entry(entity_registry, entry_id)
150 references_entry_data = set()
151 for entity_data
in imported_entry_data[CONF_ENTITIES]:
153 entry_id, entity_data[CONF_ADDRESS], entity_data[CONF_RESOURCE]
155 entity_id = entity_registry.async_get_entity_id(
156 entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id
158 if entity_id
is not None:
159 references_entry_data.add(entity_id)
161 orphaned_ids = references_config_entry - references_entry_data
162 for orphaned_id
in orphaned_ids:
163 entity_registry.async_remove(orphaned_id)
167 hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType
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)
174 references_entities = {
176 for entry
in entity_registry.entities.get_entries_for_config_entry_id(entry_id)
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)
186 references_entry_data = set()
187 for device_data
in imported_entry_data[CONF_DEVICES]:
189 device = device_registry.async_get_device(
190 identifiers={(DOMAIN, device_unique_id)}
192 if device
is not None:
193 references_entry_data.add(device.id)
198 for entry
in dr.async_entries_for_config_entry(device_registry, entry_id)
200 - references_entities
202 - references_entry_data
205 for device_id
in orphaned_ids:
206 device_registry.async_remove_device(device_id)
210 """Register LCN host for given config_entry in device registry."""
211 device_registry = dr.async_get(hass)
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,
223 hass: HomeAssistant, config_entry: ConfigEntry
225 """Register LCN modules and groups defined in config_entry as devices in device registry.
227 The name of all given device_connections is collected and the devices
230 device_registry = dr.async_get(hass)
232 host_identifiers = (DOMAIN, config_entry.entry_id)
234 for device_config
in config_entry.data[CONF_DEVICES]:
235 address = device_config[CONF_ADDRESS]
236 device_name = device_config[CONF_NAME]
239 if device_config[CONF_ADDRESS][2]:
240 device_model = f
"LCN group (g{address[0]:03d}{address[1]:03d})"
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]
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}"
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,
263 device_connection: DeviceConnectionType, device_config: ConfigType
265 """Fill missing values in device_config with infos from LCN bus."""
267 if not (is_group := device_config[CONF_ADDRESS][2]):
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
277 if device_config[CONF_NAME] !=
"":
282 device_name = await device_connection.request_name()
283 if is_group
or device_name ==
"":
284 module_type =
"Group" if is_group
else "Module"
287 f
"{device_config[CONF_ADDRESS][0]:03d}/"
288 f
"{device_config[CONF_ADDRESS][1]:03d}"
290 device_config[CONF_NAME] = device_name
294 hass: HomeAssistant, config_entry: ConfigEntry
296 """Fill missing values in config_entry with infos from LCN bus."""
297 device_configs = deepcopy(config_entry.data[CONF_DEVICES])
299 for device_config
in device_configs:
301 hass, device_config[CONF_ADDRESS], config_entry
305 await asyncio.gather(*coros)
307 new_data = {**config_entry.data, CONF_DEVICES: device_configs}
310 hass.config_entries.async_update_entry(config_entry, data=new_data)
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)
324 """Validate the given address string.
326 Examples for S000M005 at myhome:
329 myhome.0.5 ("m" is implicit if missing)
331 Examples for s000g011
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")
340 raise ValueError(f
"{value} is not a valid address string")
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)
str generate_unique_id(str entry_id, AddressType address, str|None resource=None)
None purge_device_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
None async_update_device_config(DeviceConnectionType device_connection, ConfigType device_config)
tuple[AddressType, str] is_address(str value)
list[str] is_states_string(str states_string)
None register_lcn_host_device(HomeAssistant hass, ConfigEntry config_entry)
None register_lcn_address_devices(HomeAssistant hass, ConfigEntry config_entry)
None purge_entity_registry(HomeAssistant hass, str entry_id, ConfigType imported_entry_data)
DeviceConnectionType get_device_connection(HomeAssistant hass, AddressType address, ConfigEntry config_entry)
str get_device_model(str domain_name, ConfigType domain_data)
str get_resource(str domain_name, ConfigType domain_data)
None async_update_config_entry(HomeAssistant hass, ConfigEntry config_entry)