aiohomematic 2025.8.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiohomematic might be problematic. Click here for more details.

Files changed (77) hide show
  1. aiohomematic/__init__.py +47 -0
  2. aiohomematic/async_support.py +146 -0
  3. aiohomematic/caches/__init__.py +10 -0
  4. aiohomematic/caches/dynamic.py +554 -0
  5. aiohomematic/caches/persistent.py +459 -0
  6. aiohomematic/caches/visibility.py +774 -0
  7. aiohomematic/central/__init__.py +2034 -0
  8. aiohomematic/central/decorators.py +110 -0
  9. aiohomematic/central/xml_rpc_server.py +267 -0
  10. aiohomematic/client/__init__.py +1746 -0
  11. aiohomematic/client/json_rpc.py +1193 -0
  12. aiohomematic/client/xml_rpc.py +222 -0
  13. aiohomematic/const.py +795 -0
  14. aiohomematic/context.py +8 -0
  15. aiohomematic/converter.py +82 -0
  16. aiohomematic/decorators.py +188 -0
  17. aiohomematic/exceptions.py +145 -0
  18. aiohomematic/hmcli.py +159 -0
  19. aiohomematic/model/__init__.py +137 -0
  20. aiohomematic/model/calculated/__init__.py +65 -0
  21. aiohomematic/model/calculated/climate.py +230 -0
  22. aiohomematic/model/calculated/data_point.py +319 -0
  23. aiohomematic/model/calculated/operating_voltage_level.py +311 -0
  24. aiohomematic/model/calculated/support.py +174 -0
  25. aiohomematic/model/custom/__init__.py +175 -0
  26. aiohomematic/model/custom/climate.py +1334 -0
  27. aiohomematic/model/custom/const.py +146 -0
  28. aiohomematic/model/custom/cover.py +741 -0
  29. aiohomematic/model/custom/data_point.py +318 -0
  30. aiohomematic/model/custom/definition.py +861 -0
  31. aiohomematic/model/custom/light.py +1092 -0
  32. aiohomematic/model/custom/lock.py +389 -0
  33. aiohomematic/model/custom/siren.py +268 -0
  34. aiohomematic/model/custom/support.py +40 -0
  35. aiohomematic/model/custom/switch.py +172 -0
  36. aiohomematic/model/custom/valve.py +112 -0
  37. aiohomematic/model/data_point.py +1109 -0
  38. aiohomematic/model/decorators.py +173 -0
  39. aiohomematic/model/device.py +1347 -0
  40. aiohomematic/model/event.py +210 -0
  41. aiohomematic/model/generic/__init__.py +211 -0
  42. aiohomematic/model/generic/action.py +32 -0
  43. aiohomematic/model/generic/binary_sensor.py +28 -0
  44. aiohomematic/model/generic/button.py +25 -0
  45. aiohomematic/model/generic/data_point.py +162 -0
  46. aiohomematic/model/generic/number.py +73 -0
  47. aiohomematic/model/generic/select.py +36 -0
  48. aiohomematic/model/generic/sensor.py +72 -0
  49. aiohomematic/model/generic/switch.py +52 -0
  50. aiohomematic/model/generic/text.py +27 -0
  51. aiohomematic/model/hub/__init__.py +334 -0
  52. aiohomematic/model/hub/binary_sensor.py +22 -0
  53. aiohomematic/model/hub/button.py +26 -0
  54. aiohomematic/model/hub/data_point.py +332 -0
  55. aiohomematic/model/hub/number.py +37 -0
  56. aiohomematic/model/hub/select.py +47 -0
  57. aiohomematic/model/hub/sensor.py +35 -0
  58. aiohomematic/model/hub/switch.py +42 -0
  59. aiohomematic/model/hub/text.py +28 -0
  60. aiohomematic/model/support.py +599 -0
  61. aiohomematic/model/update.py +136 -0
  62. aiohomematic/py.typed +0 -0
  63. aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
  64. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  65. aiohomematic/rega_scripts/get_serial.fn +44 -0
  66. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  67. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  68. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  69. aiohomematic/support.py +482 -0
  70. aiohomematic/validator.py +65 -0
  71. aiohomematic-2025.8.6.dist-info/METADATA +69 -0
  72. aiohomematic-2025.8.6.dist-info/RECORD +77 -0
  73. aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
  74. aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
  75. aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
  76. aiohomematic_support/__init__.py +1 -0
  77. aiohomematic_support/client_local.py +349 -0
@@ -0,0 +1,774 @@
1
+ """
2
+ Parameter visibility rules and cache for HomeMatic data points.
3
+
4
+ This module determines which parameters should be created, shown, hidden,
5
+ ignored, or un‑ignored for channels and devices. It centralizes the rules
6
+ that influence the visibility of data points exposed by the library.
7
+
8
+ Overview
9
+ - ParameterVisibilityCache: Computes and caches visibility decisions per model,
10
+ channel number, paramset and parameter. It consolidates rules from multiple
11
+ sources, such as model‑specific defaults, paramset relevance, hidden lists,
12
+ and explicit un‑ignore directives.
13
+ - check_ignore_parameters_is_clean: Helper to verify that ignore/un‑ignore
14
+ configuration input is consistent.
15
+
16
+ Key concepts
17
+ - Relevant MASTER parameters: Certain MASTER paramset entries are promoted to
18
+ data points for selected models/channels (e.g. climate related settings), but
19
+ they may still be hidden by default for UI purposes.
20
+ - Ignored vs un‑ignored: Parameters can be broadly ignored, with exceptions
21
+ defined via explicit un‑ignore rules that match model/channel/paramset keys.
22
+ - Event suppression: For selected devices, button click events are suppressed to
23
+ avoid noise in event streams.
24
+
25
+ The cache avoids recomputing rule lookups by storing decisions per combination
26
+ of (model/channel/paramset/parameter). It is initialized on demand using data
27
+ available on the CentralUnit and model descriptors.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections import defaultdict
33
+ from collections.abc import Mapping
34
+ from functools import cache
35
+ import logging
36
+ import re
37
+ from typing import Final
38
+
39
+ from aiohomematic import central as hmcu, support as hms
40
+ from aiohomematic.const import ADDRESS_SEPARATOR, CLICK_EVENTS, UN_IGNORE_WILDCARD, Parameter, ParamsetKey
41
+ from aiohomematic.model import hmd
42
+ from aiohomematic.model.custom import get_required_parameters
43
+ from aiohomematic.support import element_matches_key
44
+
45
+ _LOGGER: Final = logging.getLogger(__name__)
46
+ _CACHE_KEY_TYPE = tuple[str, int, ParamsetKey, str]
47
+
48
+ # Define which additional parameters from MASTER paramset should be created as data_point.
49
+ # By default these are also on the _HIDDEN_PARAMETERS, which prevents these data points
50
+ # from being display by default. Usually these enties are used within custom data points,
51
+ # and not for general display.
52
+ # {model: (channel_no, parameter)}
53
+
54
+ _RELEVANT_MASTER_PARAMSETS_BY_CHANNEL: Final[Mapping[int | str | None, tuple[Parameter, ...]]] = {
55
+ None: (Parameter.GLOBAL_BUTTON_LOCK, Parameter.LOW_BAT_LIMIT),
56
+ 0: (Parameter.GLOBAL_BUTTON_LOCK, Parameter.LOW_BAT_LIMIT),
57
+ }
58
+
59
+ _CLIMATE_MASTER_PARAMETERS: Final[tuple[Parameter, ...]] = (
60
+ Parameter.HEATING_VALVE_TYPE,
61
+ Parameter.MIN_MAX_VALUE_NOT_RELEVANT_FOR_MANU_MODE,
62
+ Parameter.OPTIMUM_START_STOP,
63
+ Parameter.TEMPERATURE_MAXIMUM,
64
+ Parameter.TEMPERATURE_MINIMUM,
65
+ Parameter.TEMPERATURE_OFFSET,
66
+ Parameter.WEEK_PROGRAM_POINTER,
67
+ )
68
+
69
+ _RELEVANT_MASTER_PARAMSETS_BY_DEVICE: Final[Mapping[str, tuple[tuple[int | None, ...], tuple[Parameter, ...]]]] = {
70
+ "ALPHA-IP-RBG": ((1,), _CLIMATE_MASTER_PARAMETERS),
71
+ "ELV-SH-TACO": ((2,), (Parameter.CHANNEL_OPERATION_MODE,)),
72
+ "HM-CC-RT-DN": (
73
+ (None,),
74
+ _CLIMATE_MASTER_PARAMETERS,
75
+ ),
76
+ "HM-CC-VG-1": (
77
+ (None,),
78
+ _CLIMATE_MASTER_PARAMETERS,
79
+ ),
80
+ "HM-TC-IT-WM-W-EU": (
81
+ (None,),
82
+ _CLIMATE_MASTER_PARAMETERS,
83
+ ),
84
+ "HmIP-BWTH": ((1, 8), _CLIMATE_MASTER_PARAMETERS),
85
+ "HmIP-DRBLI4": (
86
+ (1, 2, 3, 4, 5, 6, 7, 8, 9, 13, 17, 21),
87
+ (Parameter.CHANNEL_OPERATION_MODE,),
88
+ ),
89
+ "HmIP-DRDI3": ((1, 2, 3), (Parameter.CHANNEL_OPERATION_MODE,)),
90
+ "HmIP-DRSI1": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)),
91
+ "HmIP-DRSI4": ((1, 2, 3, 4), (Parameter.CHANNEL_OPERATION_MODE,)),
92
+ "HmIP-DSD-PCB": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)),
93
+ "HmIP-FCI1": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)),
94
+ "HmIP-FCI6": (tuple(range(1, 7)), (Parameter.CHANNEL_OPERATION_MODE,)),
95
+ "HmIP-FSI16": ((1,), (Parameter.CHANNEL_OPERATION_MODE,)),
96
+ "HmIP-HEATING": ((1,), _CLIMATE_MASTER_PARAMETERS),
97
+ "HmIP-MIO16-PCB": ((13, 14, 15, 16), (Parameter.CHANNEL_OPERATION_MODE,)),
98
+ "HmIP-MOD-RC8": (tuple(range(1, 9)), (Parameter.CHANNEL_OPERATION_MODE,)),
99
+ "HmIP-RGBW": ((0,), (Parameter.DEVICE_OPERATION_MODE,)),
100
+ "HmIP-STH": ((1,), _CLIMATE_MASTER_PARAMETERS),
101
+ "HmIP-WGT": ((8, 14), _CLIMATE_MASTER_PARAMETERS),
102
+ "HmIP-WTH": ((1,), _CLIMATE_MASTER_PARAMETERS),
103
+ "HmIP-eTRV": ((1,), _CLIMATE_MASTER_PARAMETERS),
104
+ "HmIPW-DRBL4": ((1, 5, 9, 13), (Parameter.CHANNEL_OPERATION_MODE,)),
105
+ "HmIPW-DRI16": (tuple(range(1, 17)), (Parameter.CHANNEL_OPERATION_MODE,)),
106
+ "HmIPW-DRI32": (tuple(range(1, 33)), (Parameter.CHANNEL_OPERATION_MODE,)),
107
+ "HmIPW-FIO6": (tuple(range(1, 7)), (Parameter.CHANNEL_OPERATION_MODE,)),
108
+ "HmIPW-STH": ((1,), _CLIMATE_MASTER_PARAMETERS),
109
+ }
110
+
111
+ # Ignore events for some devices
112
+ _IGNORE_DEVICES_FOR_DATA_POINT_EVENTS: Final[Mapping[str, tuple[Parameter, ...]]] = {
113
+ "HmIP-PS": CLICK_EVENTS,
114
+ }
115
+
116
+ _IGNORE_DEVICES_FOR_DATA_POINT_EVENTS_LOWER: Final[dict[str, tuple[str, ...]]] = {
117
+ model.lower(): tuple(event for event in events) for model, events in _IGNORE_DEVICES_FOR_DATA_POINT_EVENTS.items()
118
+ }
119
+
120
+ # data points that will be created, but should be hidden.
121
+ _HIDDEN_PARAMETERS: Final[tuple[Parameter, ...]] = (
122
+ Parameter.ACTIVITY_STATE,
123
+ Parameter.CHANNEL_OPERATION_MODE,
124
+ Parameter.CONFIG_PENDING,
125
+ Parameter.DIRECTION,
126
+ Parameter.ERROR,
127
+ Parameter.HEATING_VALVE_TYPE,
128
+ Parameter.LOW_BAT_LIMIT,
129
+ Parameter.MIN_MAX_VALUE_NOT_RELEVANT_FOR_MANU_MODE,
130
+ Parameter.OPTIMUM_START_STOP,
131
+ Parameter.SECTION,
132
+ Parameter.STICKY_UN_REACH,
133
+ Parameter.TEMPERATURE_MAXIMUM,
134
+ Parameter.TEMPERATURE_MINIMUM,
135
+ Parameter.TEMPERATURE_OFFSET,
136
+ Parameter.UN_REACH,
137
+ Parameter.UPDATE_PENDING,
138
+ Parameter.WORKING,
139
+ )
140
+ # Fast membership for hidden parameters
141
+ _HIDDEN_PARAMETERS_SET: Final[set[Parameter]] = set(_HIDDEN_PARAMETERS)
142
+
143
+ # Parameters within the VALUES paramset for which we don't create data points.
144
+ _IGNORED_PARAMETERS: Final[tuple[str, ...]] = (
145
+ "ACCESS_AUTHORIZATION",
146
+ "ACOUSTIC_NOTIFICATION_SELECTION",
147
+ "ADAPTION_DRIVE",
148
+ "AES_KEY",
149
+ "ALARM_COUNT",
150
+ "ALL_LEDS",
151
+ "ARROW_DOWN",
152
+ "ARROW_UP",
153
+ "BACKLIGHT",
154
+ "BEEP",
155
+ "BELL",
156
+ "BLIND",
157
+ "BOOST_STATE",
158
+ "BOOST_TIME",
159
+ "BOOT",
160
+ "BULB",
161
+ "CLEAR_ERROR",
162
+ "CLEAR_WINDOW_OPEN_SYMBOL",
163
+ "CLOCK",
164
+ "CONTROL_DIFFERENTIAL_TEMPERATURE",
165
+ "DATE_TIME_UNKNOWN",
166
+ "DECISION_VALUE",
167
+ "DEVICE_IN_BOOTLOADER",
168
+ "DISPLAY_DATA_ALIGNMENT",
169
+ "DISPLAY_DATA_BACKGROUND_COLOR",
170
+ "DISPLAY_DATA_COMMIT",
171
+ "DISPLAY_DATA_ICON",
172
+ "DISPLAY_DATA_ID",
173
+ "DISPLAY_DATA_STRING",
174
+ "DISPLAY_DATA_TEXT_COLOR",
175
+ "DOOR",
176
+ "EXTERNAL_CLOCK",
177
+ "FROST_PROTECTION",
178
+ "HUMIDITY_LIMITER",
179
+ "IDENTIFICATION_MODE_KEY_VISUAL",
180
+ "IDENTIFICATION_MODE_LCD_BACKLIGHT",
181
+ "INCLUSION_UNSUPPORTED_DEVICE",
182
+ "INHIBIT",
183
+ "INSTALL_MODE",
184
+ "INTERVAL",
185
+ "LEVEL_REAL",
186
+ "OLD_LEVEL",
187
+ "OVERFLOW",
188
+ "OVERRUN",
189
+ "PARTY_SET_POINT_TEMPERATURE",
190
+ "PARTY_TEMPERATURE",
191
+ "PARTY_TIME_END",
192
+ "PARTY_TIME_START",
193
+ "PHONE",
194
+ "PROCESS",
195
+ "QUICK_VETO_TIME",
196
+ "RAMP_STOP",
197
+ "RELOCK_DELAY",
198
+ "SCENE",
199
+ "SELF_CALIBRATION",
200
+ "SERVICE_COUNT",
201
+ "SET_SYMBOL_FOR_HEATING_PHASE",
202
+ "SHADING_SPEED",
203
+ "SHEV_POS",
204
+ "SPEED",
205
+ "STATE_UNCERTAIN",
206
+ "SUBMIT",
207
+ "SWITCH_POINT_OCCURED",
208
+ "TEMPERATURE_LIMITER",
209
+ "TEMPERATURE_OUT_OF_RANGE",
210
+ "TEXT",
211
+ "USER_COLOR",
212
+ "USER_PROGRAM",
213
+ "VALVE_ADAPTION",
214
+ "WINDOW",
215
+ "WIN_RELEASE",
216
+ "WIN_RELEASE_ACT",
217
+ )
218
+ # Fast membership for ignored VALUE parameters
219
+ _IGNORED_PARAMETERS_SET: Final[set[str]] = set(_IGNORED_PARAMETERS)
220
+
221
+
222
+ # Precompile Regex patterns for wildcard checks
223
+ # Ignore Parameter that end with
224
+ _IGNORED_PARAMETERS_END_RE: Final = re.compile(r".*(_OVERFLOW|_OVERRUN|_REPORTING|_RESULT|_STATUS|_SUBMIT)$")
225
+ # Ignore Parameter that start with
226
+ _IGNORED_PARAMETERS_START_RE: Final = re.compile(
227
+ r"^(ADJUSTING_|ERR_TTM_|HANDLE_|IDENTIFY_|PARTY_START_|PARTY_STOP_|STATUS_FLAG_|WEEK_PROGRAM_)"
228
+ )
229
+
230
+
231
+ def _parameter_is_wildcard_ignored(parameter: str) -> bool:
232
+ """Check if a parameter matches common wildcard patterns."""
233
+ return bool(_IGNORED_PARAMETERS_END_RE.match(parameter) or _IGNORED_PARAMETERS_START_RE.match(parameter))
234
+
235
+
236
+ # Parameters within the paramsets for which we create data points.
237
+ _UN_IGNORE_PARAMETERS_BY_DEVICE: Final[Mapping[str, tuple[Parameter, ...]]] = {
238
+ "HmIP-DLD": (Parameter.ERROR_JAMMED,),
239
+ "HmIP-SWSD": (Parameter.SMOKE_DETECTOR_ALARM_STATUS,),
240
+ "HM-OU-LED16": (Parameter.LED_STATUS,),
241
+ "HM-Sec-Win": (Parameter.DIRECTION, Parameter.WORKING, Parameter.ERROR, Parameter.STATUS),
242
+ "HM-Sec-Key": (Parameter.DIRECTION, Parameter.ERROR),
243
+ "HmIP-PCBS-BAT": (
244
+ Parameter.OPERATING_VOLTAGE,
245
+ Parameter.LOW_BAT,
246
+ ), # To override ignore for HmIP-PCBS
247
+ }
248
+
249
+ _UN_IGNORE_PARAMETERS_BY_MODEL_LOWER: Final[dict[str, tuple[str, ...]]] = {
250
+ model.lower(): parameters for model, parameters in _UN_IGNORE_PARAMETERS_BY_DEVICE.items()
251
+ }
252
+
253
+
254
+ @cache
255
+ def _get_parameters_for_model_prefix(model_prefix: str | None) -> tuple[str, ...] | None:
256
+ """Return the dict value by wildcard type."""
257
+ if model_prefix is None:
258
+ return None
259
+
260
+ for model, parameters in _UN_IGNORE_PARAMETERS_BY_MODEL_LOWER.items():
261
+ if model.startswith(model_prefix):
262
+ return parameters
263
+ return None
264
+
265
+
266
+ # Parameters by device within the VALUES paramset for which we don't create data points.
267
+ _IGNORE_PARAMETERS_BY_DEVICE: Final[Mapping[Parameter, tuple[str, ...]]] = {
268
+ Parameter.CURRENT_ILLUMINATION: (
269
+ "HmIP-SMI",
270
+ "HmIP-SMO",
271
+ "HmIP-SPI",
272
+ ),
273
+ Parameter.LOWBAT: (
274
+ "HM-LC-Sw1-DR",
275
+ "HM-LC-Sw1-FM",
276
+ "HM-LC-Sw1-PCB",
277
+ "HM-LC-Sw1-Pl",
278
+ "HM-LC-Sw1-Pl-DN-R1",
279
+ "HM-LC-Sw1PBU-FM",
280
+ "HM-LC-Sw2-FM",
281
+ "HM-LC-Sw4-DR",
282
+ "HM-SwI-3-FM",
283
+ ),
284
+ Parameter.LOW_BAT: ("HmIP-BWTH", "HmIP-PCBS"),
285
+ Parameter.OPERATING_VOLTAGE: (
286
+ "ELV-SH-BS2",
287
+ "HmIP-BDT",
288
+ "HmIP-BROLL",
289
+ "HmIP-BS2",
290
+ "HmIP-BSL",
291
+ "HmIP-BSM",
292
+ "HmIP-BWTH",
293
+ "HmIP-DR",
294
+ "HmIP-FDT",
295
+ "HmIP-FROLL",
296
+ "HmIP-FSM",
297
+ "HmIP-MOD-OC8",
298
+ "HmIP-PCBS",
299
+ "HmIP-PDT",
300
+ "HmIP-PMFS",
301
+ "HmIP-PS",
302
+ "HmIP-SFD",
303
+ "HmIP-SMO230",
304
+ "HmIP-WGT",
305
+ ),
306
+ Parameter.VALVE_STATE: ("HmIPW-FALMOT-C12", "HmIP-FALMOT-C12"),
307
+ }
308
+
309
+ _IGNORE_PARAMETERS_BY_DEVICE_LOWER: Final[dict[str, tuple[str, ...]]] = {
310
+ parameter: tuple(model.lower() for model in s) for parameter, s in _IGNORE_PARAMETERS_BY_DEVICE.items()
311
+ }
312
+
313
+ # Some devices have parameters on multiple channels,
314
+ # but we want to use it only from a certain channel.
315
+ _ACCEPT_PARAMETER_ONLY_ON_CHANNEL: Final[Mapping[str, int]] = {Parameter.LOWBAT: 0}
316
+
317
+
318
+ class ParameterVisibilityCache:
319
+ """Cache for parameter visibility."""
320
+
321
+ __slots__ = (
322
+ "_central",
323
+ "_custom_un_ignore_complex",
324
+ "_custom_un_ignore_values_parameters",
325
+ "_ignore_custom_device_definition_models",
326
+ "_param_ignored_cache",
327
+ "_param_un_ignored_cache",
328
+ "_raw_un_ignores",
329
+ "_relevant_master_paramsets_by_device",
330
+ "_relevant_prefix_cache",
331
+ "_required_parameters",
332
+ "_storage_folder",
333
+ "_un_ignore_parameters_by_device_paramset_key",
334
+ "_un_ignore_prefix_cache",
335
+ )
336
+
337
+ def __init__(
338
+ self,
339
+ central: hmcu.CentralUnit,
340
+ ) -> None:
341
+ """Init the parameter visibility cache."""
342
+ self._central = central
343
+ self._storage_folder: Final = central.config.storage_folder
344
+ self._required_parameters: Final = get_required_parameters()
345
+ self._raw_un_ignores: Final[tuple[str, ...]] = central.config.un_ignore_list or ()
346
+
347
+ # un_ignore from custom un_ignore files
348
+ # parameter
349
+ self._custom_un_ignore_values_parameters: Final[set[str]] = set()
350
+
351
+ # model, channel_no, paramset_key, parameter
352
+ self._custom_un_ignore_complex: Final[dict[str, dict[int | str | None, dict[str, set[str]]]]] = defaultdict(
353
+ lambda: defaultdict(lambda: defaultdict(set))
354
+ )
355
+ self._ignore_custom_device_definition_models: Final[tuple[str, ...]] = (
356
+ central.config.ignore_custom_device_definition_models
357
+ )
358
+
359
+ # model, channel_no, paramset_key, set[parameter]
360
+ self._un_ignore_parameters_by_device_paramset_key: Final[
361
+ dict[str, dict[int | None, dict[ParamsetKey, set[str]]]]
362
+ ] = defaultdict(lambda: defaultdict(lambda: defaultdict(set)))
363
+
364
+ # model, channel_no
365
+ self._relevant_master_paramsets_by_device: Final[dict[str, set[int | None]]] = defaultdict(set)
366
+ # Cache for resolving matching prefix key in _un_ignore_parameters_by_device_paramset_key
367
+ self._un_ignore_prefix_cache: dict[str, str | None] = {}
368
+ # Cache for resolving matching prefix key in _relevant_master_paramsets_by_device
369
+ self._relevant_prefix_cache: dict[str, str | None] = {}
370
+ # Per-instance memoization for repeated queries.
371
+ # Key: (model_l, channel_no, paramset_key, parameter)
372
+ self._param_ignored_cache: dict[tuple[str, int, ParamsetKey, str], bool] = {}
373
+ # Key: (model_l, channel_no, paramset_key, parameter, custom_only)
374
+ self._param_un_ignored_cache: dict[tuple[str, int, ParamsetKey, str, bool], bool] = {}
375
+ self._init()
376
+
377
+ def _init(self) -> None:
378
+ """Process cache initialisation."""
379
+ for (
380
+ model,
381
+ channels_parameter,
382
+ ) in _RELEVANT_MASTER_PARAMSETS_BY_DEVICE.items():
383
+ model_l = model.lower()
384
+ channel_nos, parameters = channels_parameter
385
+
386
+ def _add_channel(dt_l: str, params: tuple[Parameter, ...], ch_no: int | None) -> None:
387
+ self._relevant_master_paramsets_by_device[dt_l].add(ch_no)
388
+ self._un_ignore_parameters_by_device_paramset_key[dt_l][ch_no][ParamsetKey.MASTER].update(params)
389
+
390
+ if channel_nos:
391
+ for channel_no in channel_nos:
392
+ _add_channel(dt_l=model_l, params=parameters, ch_no=channel_no)
393
+ else:
394
+ _add_channel(dt_l=model_l, params=parameters, ch_no=None)
395
+
396
+ self._process_un_ignore_entries(lines=self._raw_un_ignores)
397
+
398
+ def model_is_ignored(self, model: str) -> bool:
399
+ """Check if a model should be ignored for custom data points."""
400
+ return element_matches_key(
401
+ search_elements=self._ignore_custom_device_definition_models,
402
+ compare_with=model,
403
+ )
404
+
405
+ def parameter_is_ignored(
406
+ self,
407
+ channel: hmd.Channel,
408
+ paramset_key: ParamsetKey,
409
+ parameter: str,
410
+ ) -> bool:
411
+ """Check if parameter can be ignored."""
412
+ model_l = channel.device.model.lower()
413
+ # Fast path via per-instance memoization
414
+ ch_no_key = channel.no if isinstance(channel.no, int) else (-1 if channel.no is None else channel.no)
415
+ if (cache_key := (model_l, ch_no_key, paramset_key, parameter)) in self._param_ignored_cache:
416
+ return self._param_ignored_cache[cache_key]
417
+
418
+ if paramset_key == ParamsetKey.VALUES:
419
+ if self.parameter_is_un_ignored(
420
+ channel=channel,
421
+ paramset_key=paramset_key,
422
+ parameter=parameter,
423
+ ):
424
+ return False
425
+
426
+ if (
427
+ (
428
+ (parameter in _IGNORED_PARAMETERS_SET or _parameter_is_wildcard_ignored(parameter=parameter))
429
+ and parameter not in self._required_parameters
430
+ )
431
+ or hms.element_matches_key(
432
+ search_elements=_IGNORE_PARAMETERS_BY_DEVICE_LOWER.get(parameter, []),
433
+ compare_with=model_l,
434
+ )
435
+ or hms.element_matches_key(
436
+ search_elements=_IGNORE_DEVICES_FOR_DATA_POINT_EVENTS_LOWER,
437
+ compare_with=parameter,
438
+ search_key=model_l,
439
+ do_right_wildcard_search=False,
440
+ )
441
+ ):
442
+ return True
443
+
444
+ if (
445
+ accept_channel := _ACCEPT_PARAMETER_ONLY_ON_CHANNEL.get(parameter)
446
+ ) is not None and accept_channel != channel.no:
447
+ return True
448
+ if paramset_key == ParamsetKey.MASTER:
449
+ if (
450
+ parameters := _RELEVANT_MASTER_PARAMSETS_BY_CHANNEL.get(channel.no)
451
+ ) is not None and parameter in parameters:
452
+ return False
453
+
454
+ if parameter in self._custom_un_ignore_complex[model_l][channel.no][ParamsetKey.MASTER]:
455
+ return False # pragma: no cover
456
+
457
+ # Resolve matching device type prefix once and cache per model
458
+ dt_short_key = self._un_ignore_prefix_cache.get(model_l)
459
+ if dt_short_key is None and model_l not in self._un_ignore_prefix_cache:
460
+ # Find first key that is a prefix of model_l
461
+ dt_short_key = next(
462
+ (k for k in self._un_ignore_parameters_by_device_paramset_key if model_l.startswith(k)), None
463
+ )
464
+ self._un_ignore_prefix_cache[model_l] = dt_short_key
465
+ if (
466
+ dt_short_key is not None
467
+ and parameter
468
+ not in self._un_ignore_parameters_by_device_paramset_key[dt_short_key][channel.no][ParamsetKey.MASTER]
469
+ ):
470
+ result = True
471
+ self._param_ignored_cache[cache_key] = result
472
+ return result
473
+
474
+ result = False
475
+ self._param_ignored_cache[cache_key] = result
476
+ return result
477
+
478
+ def _parameter_is_un_ignored(
479
+ self,
480
+ channel: hmd.Channel,
481
+ paramset_key: ParamsetKey,
482
+ parameter: str,
483
+ custom_only: bool = False,
484
+ ) -> bool:
485
+ """
486
+ Return if parameter is on an un_ignore list.
487
+
488
+ This can be either be the users un_ignore file, or in the
489
+ predefined _UN_IGNORE_PARAMETERS_BY_DEVICE.
490
+ """
491
+
492
+ # check if parameter is in custom_un_ignore
493
+ if paramset_key == ParamsetKey.VALUES and parameter in self._custom_un_ignore_values_parameters:
494
+ return True
495
+
496
+ # check if parameter is in custom_un_ignore with paramset_key
497
+ model_l = channel.device.model.lower()
498
+ # Fast path via per-instance memoization
499
+ ch_no_key = channel.no if isinstance(channel.no, int) else (-1 if channel.no is None else channel.no)
500
+ if (cache_key := (model_l, ch_no_key, paramset_key, parameter, custom_only)) in self._param_un_ignored_cache:
501
+ return self._param_un_ignored_cache[cache_key]
502
+
503
+ search_matrix = (
504
+ (
505
+ (model_l, channel.no),
506
+ (model_l, UN_IGNORE_WILDCARD),
507
+ (UN_IGNORE_WILDCARD, channel.no),
508
+ (UN_IGNORE_WILDCARD, UN_IGNORE_WILDCARD),
509
+ )
510
+ if paramset_key == ParamsetKey.VALUES
511
+ else ((model_l, channel.no),)
512
+ )
513
+
514
+ for ml, cno in search_matrix:
515
+ if parameter in self._custom_un_ignore_complex[ml][cno][paramset_key]:
516
+ self._param_un_ignored_cache[cache_key] = True
517
+ return True # pragma: no cover
518
+
519
+ # check if parameter is in _UN_IGNORE_PARAMETERS_BY_DEVICE
520
+ result = bool(
521
+ not custom_only
522
+ and (un_ignore_parameters := _get_parameters_for_model_prefix(model_prefix=model_l))
523
+ and parameter in un_ignore_parameters
524
+ )
525
+ self._param_un_ignored_cache[cache_key] = result
526
+ return result
527
+
528
+ def parameter_is_un_ignored(
529
+ self,
530
+ channel: hmd.Channel,
531
+ paramset_key: ParamsetKey,
532
+ parameter: str,
533
+ custom_only: bool = False,
534
+ ) -> bool:
535
+ """
536
+ Return if parameter is on an un_ignore list.
537
+
538
+ Additionally to _parameter_is_un_ignored these parameters
539
+ from _RELEVANT_MASTER_PARAMSETS_BY_DEVICE are un ignored.
540
+ """
541
+ if not custom_only:
542
+ model_l = channel.device.model.lower()
543
+ # Resolve matching device type prefix once and cache per model
544
+ dt_short_key = self._un_ignore_prefix_cache.get(model_l)
545
+ if dt_short_key is None and model_l not in self._un_ignore_prefix_cache:
546
+ dt_short_key = next(
547
+ (k for k in self._un_ignore_parameters_by_device_paramset_key if model_l.startswith(k)), None
548
+ )
549
+ self._un_ignore_prefix_cache[model_l] = dt_short_key
550
+
551
+ # check if parameter is in _RELEVANT_MASTER_PARAMSETS_BY_DEVICE
552
+ if (
553
+ dt_short_key is not None
554
+ and parameter
555
+ in self._un_ignore_parameters_by_device_paramset_key[dt_short_key][channel.no][paramset_key]
556
+ ):
557
+ return True
558
+
559
+ return self._parameter_is_un_ignored(
560
+ channel=channel,
561
+ paramset_key=paramset_key,
562
+ parameter=parameter,
563
+ custom_only=custom_only,
564
+ )
565
+
566
+ def should_skip_parameter(
567
+ self, channel: hmd.Channel, paramset_key: ParamsetKey, parameter: str, parameter_is_un_ignored: bool
568
+ ) -> bool:
569
+ """Determine if a parameter should be skipped."""
570
+ if self.parameter_is_ignored(
571
+ channel=channel,
572
+ paramset_key=paramset_key,
573
+ parameter=parameter,
574
+ ):
575
+ _LOGGER.debug(
576
+ "SHOULD_SKIP_PARAMETER: Ignoring parameter: %s [%s]",
577
+ parameter,
578
+ channel.address,
579
+ )
580
+ return True
581
+ if (
582
+ paramset_key == ParamsetKey.MASTER
583
+ and (parameters := _RELEVANT_MASTER_PARAMSETS_BY_CHANNEL.get(channel.no)) is not None
584
+ and parameter in parameters
585
+ ):
586
+ return False
587
+
588
+ return paramset_key == ParamsetKey.MASTER and not parameter_is_un_ignored
589
+
590
+ def _process_un_ignore_entries(self, lines: tuple[str, ...]) -> None:
591
+ """Batch process un_ignore entries into cache."""
592
+ for line in lines:
593
+ # ignore empty line
594
+ if not line.strip():
595
+ continue
596
+
597
+ if line_details := self._get_un_ignore_line_details(line=line):
598
+ if isinstance(line_details, str):
599
+ self._custom_un_ignore_values_parameters.add(line_details)
600
+ else:
601
+ self._add_complex_un_ignore_entry(
602
+ model=line_details[0],
603
+ channel_no=line_details[1],
604
+ parameter=line_details[2],
605
+ paramset_key=line_details[3],
606
+ )
607
+ else:
608
+ _LOGGER.warning(
609
+ "PROCESS_UN_IGNORE_ENTRY failed: No supported format detected for un ignore line '%s'. ",
610
+ line,
611
+ )
612
+
613
+ def _get_un_ignore_line_details(self, line: str) -> tuple[str, int | str | None, str, ParamsetKey] | str | None:
614
+ """
615
+ Check the format of the line for un_ignore file.
616
+
617
+ model, channel_no, paramset_key, parameter
618
+ """
619
+
620
+ model: str | None = None
621
+ channel_no: int | str | None = None
622
+ paramset_key: ParamsetKey | None = None
623
+ parameter: str | None = None
624
+
625
+ if "@" in line:
626
+ data = line.split("@")
627
+ if len(data) == 2:
628
+ if ADDRESS_SEPARATOR in data[0]:
629
+ param_data = data[0].split(ADDRESS_SEPARATOR)
630
+ if len(param_data) == 2:
631
+ parameter = param_data[0]
632
+ paramset_key = ParamsetKey(param_data[1])
633
+ else:
634
+ _LOGGER.warning(
635
+ "GET_UN_IGNORE_LINE_DETAILS failed: Could not add line '%s' to un ignore cache. "
636
+ "Only one ':' expected in param_data",
637
+ line,
638
+ )
639
+ return None
640
+ else:
641
+ _LOGGER.warning(
642
+ "GET_UN_IGNORE_LINE_DETAILS failed: Could not add line '%s' to un ignore cache. "
643
+ "No ':' before '@'",
644
+ line,
645
+ )
646
+ return None
647
+ if ADDRESS_SEPARATOR in data[1]:
648
+ channel_data = data[1].split(ADDRESS_SEPARATOR)
649
+ if len(channel_data) == 2:
650
+ model = channel_data[0].lower()
651
+ _channel_no = channel_data[1]
652
+ channel_no = (
653
+ int(_channel_no) if _channel_no.isnumeric() else None if _channel_no == "" else _channel_no
654
+ )
655
+ else:
656
+ _LOGGER.warning(
657
+ "GET_UN_IGNORE_LINE_DETAILS failed: Could not add line '%s' to un ignore cache. "
658
+ "Only one ':' expected in channel_data",
659
+ line,
660
+ )
661
+ return None
662
+ else:
663
+ _LOGGER.warning(
664
+ "GET_UN_IGNORE_LINE_DETAILS failed: Could not add line '%s' to un ignore cache. "
665
+ "No ':' after '@'",
666
+ line,
667
+ )
668
+ return None
669
+ else:
670
+ _LOGGER.warning(
671
+ "GET_UN_IGNORE_LINE_DETAILS failed: Could not add line '%s' to un ignore cache. "
672
+ "Only one @ expected",
673
+ line,
674
+ )
675
+ return None
676
+ elif ADDRESS_SEPARATOR in line:
677
+ _LOGGER.warning(
678
+ "GET_UN_IGNORE_LINE_DETAILS failed: No supported format detected for un ignore line '%s'. ",
679
+ line,
680
+ )
681
+ return None
682
+ if model == UN_IGNORE_WILDCARD and channel_no == UN_IGNORE_WILDCARD and paramset_key == ParamsetKey.VALUES:
683
+ return parameter
684
+ if model is not None and parameter is not None and paramset_key is not None:
685
+ return model, channel_no, parameter, paramset_key
686
+ return line
687
+
688
+ def _add_complex_un_ignore_entry(
689
+ self,
690
+ model: str,
691
+ channel_no: int | str | None,
692
+ paramset_key: ParamsetKey,
693
+ parameter: str,
694
+ ) -> None:
695
+ """Add line to un ignore cache."""
696
+ if paramset_key == ParamsetKey.MASTER:
697
+ if isinstance(channel_no, int) or channel_no is None:
698
+ # add master channel for a device to fetch paramset descriptions
699
+ self._relevant_master_paramsets_by_device[model].add(channel_no)
700
+ else:
701
+ _LOGGER.warning(
702
+ "ADD_UN_IGNORE_ENTRY: channel_no '%s' must be an integer or None for paramset_key MASTER.",
703
+ channel_no,
704
+ )
705
+ return
706
+ if model == UN_IGNORE_WILDCARD:
707
+ _LOGGER.warning("ADD_UN_IGNORE_ENTRY: model must be set for paramset_key MASTER.")
708
+ return
709
+
710
+ self._custom_un_ignore_complex[model][channel_no][paramset_key].add(parameter)
711
+
712
+ def parameter_is_hidden(
713
+ self,
714
+ channel: hmd.Channel,
715
+ paramset_key: ParamsetKey,
716
+ parameter: str,
717
+ ) -> bool:
718
+ """
719
+ Return if parameter should be hidden.
720
+
721
+ This is required to determine the data_point usage.
722
+ Return only hidden parameters, that are no defined in the un_ignore file.
723
+ """
724
+ return parameter in _HIDDEN_PARAMETERS_SET and not self._parameter_is_un_ignored(
725
+ channel=channel,
726
+ paramset_key=paramset_key,
727
+ parameter=parameter,
728
+ )
729
+
730
+ def is_relevant_paramset(
731
+ self,
732
+ channel: hmd.Channel,
733
+ paramset_key: ParamsetKey,
734
+ ) -> bool:
735
+ """
736
+ Return if a paramset is relevant.
737
+
738
+ Required to load MASTER paramsets, which are not initialized by default.
739
+ """
740
+ if paramset_key == ParamsetKey.VALUES:
741
+ return True
742
+ if paramset_key == ParamsetKey.MASTER:
743
+ if channel.no in _RELEVANT_MASTER_PARAMSETS_BY_CHANNEL:
744
+ return True
745
+ model_l = channel.device.model.lower()
746
+ # Resolve matching device type prefix once and cache per model
747
+ dt_short_key = self._relevant_prefix_cache.get(model_l)
748
+ if dt_short_key is None and model_l not in self._relevant_prefix_cache:
749
+ dt_short_key = next(
750
+ (k for k in self._relevant_master_paramsets_by_device if model_l.startswith(k)), None
751
+ )
752
+ self._relevant_prefix_cache[model_l] = dt_short_key
753
+ if dt_short_key is not None and channel.no in self._relevant_master_paramsets_by_device[dt_short_key]:
754
+ return True
755
+ return False
756
+
757
+
758
+ def check_ignore_parameters_is_clean() -> bool:
759
+ """Check if a required parameter is in ignored parameters."""
760
+ un_ignore_parameters_by_device: list[str] = []
761
+ for params in _UN_IGNORE_PARAMETERS_BY_DEVICE.values():
762
+ un_ignore_parameters_by_device.extend(params)
763
+
764
+ return (
765
+ len(
766
+ [
767
+ parameter
768
+ for parameter in get_required_parameters()
769
+ if (parameter in _IGNORED_PARAMETERS_SET or _parameter_is_wildcard_ignored(parameter=parameter))
770
+ and parameter not in un_ignore_parameters_by_device
771
+ ]
772
+ )
773
+ == 0
774
+ )