aiohomematic 2026.1.29__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.
Files changed (188) hide show
  1. aiohomematic/__init__.py +110 -0
  2. aiohomematic/_log_context_protocol.py +29 -0
  3. aiohomematic/api.py +410 -0
  4. aiohomematic/async_support.py +250 -0
  5. aiohomematic/backend_detection.py +462 -0
  6. aiohomematic/central/__init__.py +103 -0
  7. aiohomematic/central/async_rpc_server.py +760 -0
  8. aiohomematic/central/central_unit.py +1152 -0
  9. aiohomematic/central/config.py +463 -0
  10. aiohomematic/central/config_builder.py +772 -0
  11. aiohomematic/central/connection_state.py +160 -0
  12. aiohomematic/central/coordinators/__init__.py +38 -0
  13. aiohomematic/central/coordinators/cache.py +414 -0
  14. aiohomematic/central/coordinators/client.py +480 -0
  15. aiohomematic/central/coordinators/connection_recovery.py +1141 -0
  16. aiohomematic/central/coordinators/device.py +1166 -0
  17. aiohomematic/central/coordinators/event.py +514 -0
  18. aiohomematic/central/coordinators/hub.py +532 -0
  19. aiohomematic/central/decorators.py +184 -0
  20. aiohomematic/central/device_registry.py +229 -0
  21. aiohomematic/central/events/__init__.py +104 -0
  22. aiohomematic/central/events/bus.py +1392 -0
  23. aiohomematic/central/events/integration.py +424 -0
  24. aiohomematic/central/events/types.py +194 -0
  25. aiohomematic/central/health.py +762 -0
  26. aiohomematic/central/rpc_server.py +353 -0
  27. aiohomematic/central/scheduler.py +794 -0
  28. aiohomematic/central/state_machine.py +391 -0
  29. aiohomematic/client/__init__.py +203 -0
  30. aiohomematic/client/_rpc_errors.py +187 -0
  31. aiohomematic/client/backends/__init__.py +48 -0
  32. aiohomematic/client/backends/base.py +335 -0
  33. aiohomematic/client/backends/capabilities.py +138 -0
  34. aiohomematic/client/backends/ccu.py +487 -0
  35. aiohomematic/client/backends/factory.py +116 -0
  36. aiohomematic/client/backends/homegear.py +294 -0
  37. aiohomematic/client/backends/json_ccu.py +252 -0
  38. aiohomematic/client/backends/protocol.py +316 -0
  39. aiohomematic/client/ccu.py +1857 -0
  40. aiohomematic/client/circuit_breaker.py +459 -0
  41. aiohomematic/client/config.py +64 -0
  42. aiohomematic/client/handlers/__init__.py +40 -0
  43. aiohomematic/client/handlers/backup.py +157 -0
  44. aiohomematic/client/handlers/base.py +79 -0
  45. aiohomematic/client/handlers/device_ops.py +1085 -0
  46. aiohomematic/client/handlers/firmware.py +144 -0
  47. aiohomematic/client/handlers/link_mgmt.py +199 -0
  48. aiohomematic/client/handlers/metadata.py +436 -0
  49. aiohomematic/client/handlers/programs.py +144 -0
  50. aiohomematic/client/handlers/sysvars.py +100 -0
  51. aiohomematic/client/interface_client.py +1304 -0
  52. aiohomematic/client/json_rpc.py +2068 -0
  53. aiohomematic/client/request_coalescer.py +282 -0
  54. aiohomematic/client/rpc_proxy.py +629 -0
  55. aiohomematic/client/state_machine.py +324 -0
  56. aiohomematic/const.py +2207 -0
  57. aiohomematic/context.py +275 -0
  58. aiohomematic/converter.py +270 -0
  59. aiohomematic/decorators.py +390 -0
  60. aiohomematic/exceptions.py +185 -0
  61. aiohomematic/hmcli.py +997 -0
  62. aiohomematic/i18n.py +193 -0
  63. aiohomematic/interfaces/__init__.py +407 -0
  64. aiohomematic/interfaces/central.py +1067 -0
  65. aiohomematic/interfaces/client.py +1096 -0
  66. aiohomematic/interfaces/coordinators.py +63 -0
  67. aiohomematic/interfaces/model.py +1921 -0
  68. aiohomematic/interfaces/operations.py +217 -0
  69. aiohomematic/logging_context.py +134 -0
  70. aiohomematic/metrics/__init__.py +125 -0
  71. aiohomematic/metrics/_protocols.py +140 -0
  72. aiohomematic/metrics/aggregator.py +534 -0
  73. aiohomematic/metrics/dataclasses.py +489 -0
  74. aiohomematic/metrics/emitter.py +292 -0
  75. aiohomematic/metrics/events.py +183 -0
  76. aiohomematic/metrics/keys.py +300 -0
  77. aiohomematic/metrics/observer.py +563 -0
  78. aiohomematic/metrics/stats.py +172 -0
  79. aiohomematic/model/__init__.py +189 -0
  80. aiohomematic/model/availability.py +65 -0
  81. aiohomematic/model/calculated/__init__.py +89 -0
  82. aiohomematic/model/calculated/climate.py +276 -0
  83. aiohomematic/model/calculated/data_point.py +315 -0
  84. aiohomematic/model/calculated/field.py +147 -0
  85. aiohomematic/model/calculated/operating_voltage_level.py +286 -0
  86. aiohomematic/model/calculated/support.py +232 -0
  87. aiohomematic/model/custom/__init__.py +214 -0
  88. aiohomematic/model/custom/capabilities/__init__.py +67 -0
  89. aiohomematic/model/custom/capabilities/climate.py +41 -0
  90. aiohomematic/model/custom/capabilities/light.py +87 -0
  91. aiohomematic/model/custom/capabilities/lock.py +44 -0
  92. aiohomematic/model/custom/capabilities/siren.py +63 -0
  93. aiohomematic/model/custom/climate.py +1130 -0
  94. aiohomematic/model/custom/cover.py +722 -0
  95. aiohomematic/model/custom/data_point.py +360 -0
  96. aiohomematic/model/custom/definition.py +300 -0
  97. aiohomematic/model/custom/field.py +89 -0
  98. aiohomematic/model/custom/light.py +1174 -0
  99. aiohomematic/model/custom/lock.py +322 -0
  100. aiohomematic/model/custom/mixins.py +445 -0
  101. aiohomematic/model/custom/profile.py +945 -0
  102. aiohomematic/model/custom/registry.py +251 -0
  103. aiohomematic/model/custom/siren.py +462 -0
  104. aiohomematic/model/custom/switch.py +195 -0
  105. aiohomematic/model/custom/text_display.py +289 -0
  106. aiohomematic/model/custom/valve.py +78 -0
  107. aiohomematic/model/data_point.py +1416 -0
  108. aiohomematic/model/device.py +1840 -0
  109. aiohomematic/model/event.py +216 -0
  110. aiohomematic/model/generic/__init__.py +327 -0
  111. aiohomematic/model/generic/action.py +40 -0
  112. aiohomematic/model/generic/action_select.py +62 -0
  113. aiohomematic/model/generic/binary_sensor.py +30 -0
  114. aiohomematic/model/generic/button.py +31 -0
  115. aiohomematic/model/generic/data_point.py +177 -0
  116. aiohomematic/model/generic/dummy.py +150 -0
  117. aiohomematic/model/generic/number.py +76 -0
  118. aiohomematic/model/generic/select.py +56 -0
  119. aiohomematic/model/generic/sensor.py +76 -0
  120. aiohomematic/model/generic/switch.py +54 -0
  121. aiohomematic/model/generic/text.py +33 -0
  122. aiohomematic/model/hub/__init__.py +100 -0
  123. aiohomematic/model/hub/binary_sensor.py +24 -0
  124. aiohomematic/model/hub/button.py +28 -0
  125. aiohomematic/model/hub/connectivity.py +190 -0
  126. aiohomematic/model/hub/data_point.py +342 -0
  127. aiohomematic/model/hub/hub.py +864 -0
  128. aiohomematic/model/hub/inbox.py +135 -0
  129. aiohomematic/model/hub/install_mode.py +393 -0
  130. aiohomematic/model/hub/metrics.py +208 -0
  131. aiohomematic/model/hub/number.py +42 -0
  132. aiohomematic/model/hub/select.py +52 -0
  133. aiohomematic/model/hub/sensor.py +37 -0
  134. aiohomematic/model/hub/switch.py +43 -0
  135. aiohomematic/model/hub/text.py +30 -0
  136. aiohomematic/model/hub/update.py +221 -0
  137. aiohomematic/model/support.py +592 -0
  138. aiohomematic/model/update.py +140 -0
  139. aiohomematic/model/week_profile.py +1827 -0
  140. aiohomematic/property_decorators.py +719 -0
  141. aiohomematic/py.typed +0 -0
  142. aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
  143. aiohomematic/rega_scripts/create_backup_start.fn +28 -0
  144. aiohomematic/rega_scripts/create_backup_status.fn +89 -0
  145. aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
  146. aiohomematic/rega_scripts/get_backend_info.fn +25 -0
  147. aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
  148. aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
  149. aiohomematic/rega_scripts/get_serial.fn +44 -0
  150. aiohomematic/rega_scripts/get_service_messages.fn +83 -0
  151. aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
  152. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
  153. aiohomematic/rega_scripts/set_program_state.fn +17 -0
  154. aiohomematic/rega_scripts/set_system_variable.fn +19 -0
  155. aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
  156. aiohomematic/schemas.py +256 -0
  157. aiohomematic/store/__init__.py +55 -0
  158. aiohomematic/store/dynamic/__init__.py +43 -0
  159. aiohomematic/store/dynamic/command.py +250 -0
  160. aiohomematic/store/dynamic/data.py +175 -0
  161. aiohomematic/store/dynamic/details.py +187 -0
  162. aiohomematic/store/dynamic/ping_pong.py +416 -0
  163. aiohomematic/store/persistent/__init__.py +71 -0
  164. aiohomematic/store/persistent/base.py +285 -0
  165. aiohomematic/store/persistent/device.py +233 -0
  166. aiohomematic/store/persistent/incident.py +380 -0
  167. aiohomematic/store/persistent/paramset.py +241 -0
  168. aiohomematic/store/persistent/session.py +556 -0
  169. aiohomematic/store/serialization.py +150 -0
  170. aiohomematic/store/storage.py +689 -0
  171. aiohomematic/store/types.py +526 -0
  172. aiohomematic/store/visibility/__init__.py +40 -0
  173. aiohomematic/store/visibility/parser.py +141 -0
  174. aiohomematic/store/visibility/registry.py +722 -0
  175. aiohomematic/store/visibility/rules.py +307 -0
  176. aiohomematic/strings.json +237 -0
  177. aiohomematic/support.py +706 -0
  178. aiohomematic/tracing.py +236 -0
  179. aiohomematic/translations/de.json +237 -0
  180. aiohomematic/translations/en.json +237 -0
  181. aiohomematic/type_aliases.py +51 -0
  182. aiohomematic/validator.py +128 -0
  183. aiohomematic-2026.1.29.dist-info/METADATA +296 -0
  184. aiohomematic-2026.1.29.dist-info/RECORD +188 -0
  185. aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
  186. aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
  187. aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
  188. aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
@@ -0,0 +1,722 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2026
3
+ """
4
+ Parameter visibility registry for Homematic data points.
5
+
6
+ This module provides the ParameterVisibilityRegistry class which determines whether
7
+ parameters should be created, shown, hidden, ignored, or un-ignored for channels
8
+ and devices. It consolidates rules from multiple sources and memoizes decisions
9
+ to avoid repeated computations.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Iterable
15
+ from functools import cache
16
+ import logging
17
+ from typing import TYPE_CHECKING, Final, NamedTuple
18
+
19
+ from aiohomematic import support as hms
20
+ from aiohomematic.const import UN_IGNORE_WILDCARD, ParamsetKey
21
+ from aiohomematic.interfaces import ParameterVisibilityProviderProtocol
22
+ from aiohomematic.model.custom import get_required_parameters
23
+ from aiohomematic.store.visibility.parser import ParsedUnIgnoreLine, UnIgnoreChannelNo, parse_un_ignore_line
24
+ from aiohomematic.store.visibility.rules import (
25
+ ACCEPT_PARAMETER_ONLY_ON_CHANNEL,
26
+ HIDDEN_PARAMETERS,
27
+ IGNORE_DEVICES_FOR_DATA_POINT_EVENTS_LOWER,
28
+ IGNORE_PARAMETERS_BY_DEVICE_LOWER,
29
+ IGNORED_PARAMETERS,
30
+ RELEVANT_MASTER_PARAMSETS_BY_CHANNEL,
31
+ RELEVANT_MASTER_PARAMSETS_BY_DEVICE,
32
+ UN_IGNORE_PARAMETERS_BY_MODEL_LOWER,
33
+ ChannelNo,
34
+ ModelName,
35
+ ParameterName,
36
+ parameter_is_wildcard_ignored,
37
+ )
38
+ from aiohomematic.support import element_matches_key
39
+
40
+ if TYPE_CHECKING:
41
+ from aiohomematic.interfaces import ChannelProtocol, ConfigProviderProtocol, EventBusProviderProtocol
42
+
43
+ _LOGGER: Final = logging.getLogger(__name__)
44
+
45
+
46
+ # =============================================================================
47
+ # Typed Cache Keys
48
+ # =============================================================================
49
+
50
+
51
+ class IgnoreCacheKey(NamedTuple):
52
+ """Cache key for parameter_is_ignored lookups."""
53
+
54
+ model: ModelName
55
+ channel_no: ChannelNo
56
+ paramset_key: ParamsetKey
57
+ parameter: ParameterName
58
+
59
+
60
+ class UnIgnoreCacheKey(NamedTuple):
61
+ """Cache key for parameter_is_un_ignored lookups."""
62
+
63
+ model: ModelName
64
+ channel_no: ChannelNo
65
+ paramset_key: ParamsetKey
66
+ parameter: ParameterName
67
+ custom_only: bool
68
+
69
+
70
+ # =============================================================================
71
+ # Rule Container Classes
72
+ # =============================================================================
73
+
74
+
75
+ class ChannelParamsetRules:
76
+ """
77
+ Manage parameter rules indexed by (channel_no, paramset_key).
78
+
79
+ Replaces nested defaultdict structures with a cleaner interface.
80
+ """
81
+
82
+ __slots__ = ("_data",)
83
+
84
+ def __init__(self) -> None:
85
+ """Initialize empty rules container."""
86
+ self._data: dict[tuple[UnIgnoreChannelNo, ParamsetKey], set[ParameterName]] = {}
87
+
88
+ def add(
89
+ self,
90
+ *,
91
+ channel_no: UnIgnoreChannelNo,
92
+ paramset_key: ParamsetKey,
93
+ parameter: ParameterName,
94
+ ) -> None:
95
+ """Add a parameter to the rules for a channel/paramset combination."""
96
+ if (key := (channel_no, paramset_key)) not in self._data:
97
+ self._data[key] = set()
98
+ self._data[key].add(parameter)
99
+
100
+ def contains(
101
+ self,
102
+ *,
103
+ channel_no: UnIgnoreChannelNo,
104
+ paramset_key: ParamsetKey,
105
+ parameter: ParameterName,
106
+ ) -> bool:
107
+ """Check if a parameter exists in the rules for a channel/paramset combination."""
108
+ return parameter in self._data.get((channel_no, paramset_key), set())
109
+
110
+ def get_parameters(
111
+ self,
112
+ *,
113
+ channel_no: UnIgnoreChannelNo,
114
+ paramset_key: ParamsetKey,
115
+ ) -> set[ParameterName]:
116
+ """Return the set of parameters for a channel/paramset combination."""
117
+ return self._data.get((channel_no, paramset_key), set())
118
+
119
+ def update(
120
+ self,
121
+ *,
122
+ channel_no: UnIgnoreChannelNo,
123
+ paramset_key: ParamsetKey,
124
+ parameters: Iterable[ParameterName],
125
+ ) -> None:
126
+ """Add multiple parameters to the rules for a channel/paramset combination."""
127
+ if (key := (channel_no, paramset_key)) not in self._data:
128
+ self._data[key] = set()
129
+ self._data[key].update(parameters)
130
+
131
+
132
+ class ModelRules:
133
+ """
134
+ Manage parameter rules indexed by model name.
135
+
136
+ Each model has its own ChannelParamsetRules and a set of relevant channels.
137
+ """
138
+
139
+ __slots__ = ("_channel_rules", "_relevant_channels")
140
+
141
+ def __init__(self) -> None:
142
+ """Initialize empty model rules container."""
143
+ self._channel_rules: dict[ModelName, ChannelParamsetRules] = {}
144
+ self._relevant_channels: dict[ModelName, set[ChannelNo]] = {}
145
+
146
+ def add_parameter(
147
+ self,
148
+ *,
149
+ model: ModelName,
150
+ channel_no: UnIgnoreChannelNo,
151
+ paramset_key: ParamsetKey,
152
+ parameter: ParameterName,
153
+ ) -> None:
154
+ """Add a parameter rule for a model/channel/paramset combination."""
155
+ if model not in self._channel_rules:
156
+ self._channel_rules[model] = ChannelParamsetRules()
157
+ self._channel_rules[model].add(channel_no=channel_no, paramset_key=paramset_key, parameter=parameter)
158
+
159
+ def add_relevant_channel(self, *, model: ModelName, channel_no: ChannelNo) -> None:
160
+ """Mark a channel as relevant for MASTER paramset fetching."""
161
+ if model not in self._relevant_channels:
162
+ self._relevant_channels[model] = set()
163
+ self._relevant_channels[model].add(channel_no)
164
+
165
+ def contains(
166
+ self,
167
+ *,
168
+ model: ModelName,
169
+ channel_no: UnIgnoreChannelNo,
170
+ paramset_key: ParamsetKey,
171
+ parameter: ParameterName,
172
+ ) -> bool:
173
+ """Check if a parameter exists in the rules."""
174
+ if model not in self._channel_rules:
175
+ return False
176
+ return self._channel_rules[model].contains(
177
+ channel_no=channel_no, paramset_key=paramset_key, parameter=parameter
178
+ )
179
+
180
+ def get_models(self) -> Iterable[ModelName]:
181
+ """Return all model names with rules."""
182
+ return self._channel_rules.keys()
183
+
184
+ def get_relevant_channels(self, *, model: ModelName) -> set[ChannelNo]:
185
+ """Return the set of relevant channels for a model."""
186
+ return self._relevant_channels.get(model, set())
187
+
188
+ def has_relevant_channel(self, *, model: ModelName, channel_no: ChannelNo) -> bool:
189
+ """Check if a channel is relevant for a model."""
190
+ return channel_no in self._relevant_channels.get(model, set())
191
+
192
+ def update_parameters(
193
+ self,
194
+ *,
195
+ model: ModelName,
196
+ channel_no: UnIgnoreChannelNo,
197
+ paramset_key: ParamsetKey,
198
+ parameters: Iterable[ParameterName],
199
+ ) -> None:
200
+ """Add multiple parameter rules for a model/channel/paramset combination."""
201
+ if model not in self._channel_rules:
202
+ self._channel_rules[model] = ChannelParamsetRules()
203
+ self._channel_rules[model].update(channel_no=channel_no, paramset_key=paramset_key, parameters=parameters)
204
+
205
+
206
+ # =============================================================================
207
+ # Cached Helper Functions
208
+ # =============================================================================
209
+
210
+
211
+ @cache
212
+ def _get_parameters_for_model_prefix(*, model_prefix: str | None) -> frozenset[ParameterName] | None:
213
+ """Return un-ignore parameters for a model by prefix match."""
214
+ if model_prefix is None:
215
+ return None
216
+
217
+ for model, parameters in UN_IGNORE_PARAMETERS_BY_MODEL_LOWER.items():
218
+ if model.startswith(model_prefix):
219
+ return parameters
220
+ return None
221
+
222
+
223
+ # =============================================================================
224
+ # Parameter Visibility Registry
225
+ # =============================================================================
226
+
227
+
228
+ class ParameterVisibilityRegistry(ParameterVisibilityProviderProtocol):
229
+ """
230
+ Registry for parameter visibility decisions.
231
+
232
+ Centralizes rules that determine whether a data point parameter is created,
233
+ ignored, un-ignored, or merely hidden for UI purposes. Combines static rules
234
+ (per-model/per-channel) with dynamic user-provided overrides and memoizes
235
+ decisions per (model/channel/paramset/parameter) to avoid repeated computations.
236
+ """
237
+
238
+ __slots__ = (
239
+ "_config_provider",
240
+ "_custom_un_ignore_rules",
241
+ "_custom_un_ignore_values_parameters",
242
+ "_device_un_ignore_rules",
243
+ "_ignore_custom_device_definition_models",
244
+ "_param_ignored_cache",
245
+ "_param_un_ignored_cache",
246
+ "_raw_un_ignores",
247
+ "_relevant_master_channels",
248
+ "_relevant_prefix_cache",
249
+ "_required_parameters",
250
+ "_storage_directory",
251
+ "_un_ignore_prefix_cache",
252
+ )
253
+
254
+ def __init__(
255
+ self,
256
+ *,
257
+ config_provider: ConfigProviderProtocol,
258
+ event_bus_provider: EventBusProviderProtocol | None = None, # Kept for compatibility, unused
259
+ ) -> None:
260
+ """Initialize the parameter visibility registry."""
261
+ self._config_provider: Final = config_provider
262
+ self._storage_directory: Final = config_provider.config.storage_directory
263
+ self._required_parameters: Final = get_required_parameters()
264
+ self._raw_un_ignores: Final[frozenset[str]] = config_provider.config.un_ignore_list or frozenset()
265
+ self._ignore_custom_device_definition_models: Final[frozenset[ModelName]] = (
266
+ config_provider.config.ignore_custom_device_definition_models
267
+ )
268
+
269
+ # Simple un-ignore: parameter names that apply to all VALUES paramsets
270
+ self._custom_un_ignore_values_parameters: Final[set[ParameterName]] = set()
271
+
272
+ # Complex un-ignore: model -> channel/paramset/parameter rules
273
+ self._custom_un_ignore_rules: Final[ModelRules] = ModelRules()
274
+
275
+ # Device-specific un-ignore rules from RELEVANT_MASTER_PARAMSETS_BY_DEVICE
276
+ self._device_un_ignore_rules: Final[ModelRules] = ModelRules()
277
+
278
+ # Channels that need MASTER paramset fetching
279
+ self._relevant_master_channels: Final[dict[ModelName, set[ChannelNo]]] = {}
280
+
281
+ # Prefix resolution caches
282
+ self._un_ignore_prefix_cache: dict[ModelName, str | None] = {}
283
+ self._relevant_prefix_cache: dict[ModelName, str | None] = {}
284
+
285
+ # Per-instance memoization caches
286
+ self._param_ignored_cache: dict[IgnoreCacheKey, bool] = {}
287
+ self._param_un_ignored_cache: dict[UnIgnoreCacheKey, bool] = {}
288
+
289
+ self._init()
290
+
291
+ @property
292
+ def size(self) -> int:
293
+ """Return total size of memoization caches."""
294
+ return len(self._param_ignored_cache) + len(self._param_un_ignored_cache)
295
+
296
+ def clear_memoization_caches(self) -> None:
297
+ """Clear the per-instance memoization caches to free memory."""
298
+ self._param_ignored_cache.clear()
299
+ self._param_un_ignored_cache.clear()
300
+
301
+ def invalidate_all_caches(self) -> None:
302
+ """Invalidate all caches including prefix resolution caches."""
303
+ self.clear_memoization_caches()
304
+ self._un_ignore_prefix_cache.clear()
305
+ self._relevant_prefix_cache.clear()
306
+
307
+ def is_relevant_paramset(
308
+ self,
309
+ *,
310
+ channel: ChannelProtocol,
311
+ paramset_key: ParamsetKey,
312
+ ) -> bool:
313
+ """
314
+ Return if a paramset is relevant.
315
+
316
+ Required to load MASTER paramsets, which are not initialized by default.
317
+ """
318
+ if paramset_key == ParamsetKey.VALUES:
319
+ return True
320
+
321
+ if paramset_key == ParamsetKey.MASTER:
322
+ if channel.no in RELEVANT_MASTER_PARAMSETS_BY_CHANNEL:
323
+ return True
324
+
325
+ model_l = channel.device.model.lower()
326
+ dt_short_key = self._resolve_prefix_key(
327
+ model_l=model_l,
328
+ models=self._relevant_master_channels.keys(),
329
+ cache_dict=self._relevant_prefix_cache,
330
+ )
331
+ if dt_short_key is not None:
332
+ return channel.no in self._relevant_master_channels.get(dt_short_key, set())
333
+
334
+ return False
335
+
336
+ def model_is_ignored(self, *, model: ModelName) -> bool:
337
+ """Check if a model should be ignored for custom data points."""
338
+ return element_matches_key(
339
+ search_elements=self._ignore_custom_device_definition_models,
340
+ compare_with=model,
341
+ )
342
+
343
+ def parameter_is_hidden(
344
+ self,
345
+ *,
346
+ channel: ChannelProtocol,
347
+ paramset_key: ParamsetKey,
348
+ parameter: ParameterName,
349
+ ) -> bool:
350
+ """
351
+ Return if parameter should be hidden.
352
+
353
+ Hidden parameters are created but not displayed by default.
354
+ Returns False if the parameter is on an un-ignore list.
355
+ """
356
+ return parameter in HIDDEN_PARAMETERS and not self._parameter_is_un_ignored(
357
+ channel=channel,
358
+ paramset_key=paramset_key,
359
+ parameter=parameter,
360
+ )
361
+
362
+ def parameter_is_ignored(
363
+ self,
364
+ *,
365
+ channel: ChannelProtocol,
366
+ paramset_key: ParamsetKey,
367
+ parameter: ParameterName,
368
+ ) -> bool:
369
+ """Check if parameter should be ignored (not created as data point)."""
370
+ model_l = channel.device.model.lower()
371
+
372
+ if (cache_key := IgnoreCacheKey(model_l, channel.no, paramset_key, parameter)) in self._param_ignored_cache:
373
+ return self._param_ignored_cache[cache_key]
374
+
375
+ result = self._check_parameter_is_ignored(
376
+ channel=channel,
377
+ paramset_key=paramset_key,
378
+ parameter=parameter,
379
+ model_l=model_l,
380
+ )
381
+ self._param_ignored_cache[cache_key] = result
382
+ return result
383
+
384
+ def parameter_is_un_ignored(
385
+ self,
386
+ *,
387
+ channel: ChannelProtocol,
388
+ paramset_key: ParamsetKey,
389
+ parameter: ParameterName,
390
+ custom_only: bool = False,
391
+ ) -> bool:
392
+ """
393
+ Return if parameter is on an un-ignore list.
394
+
395
+ Includes both device-specific rules from RELEVANT_MASTER_PARAMSETS_BY_DEVICE
396
+ and custom user-provided un-ignore rules.
397
+ """
398
+ if not custom_only:
399
+ model_l = channel.device.model.lower()
400
+ dt_short_key = self._resolve_prefix_key(
401
+ model_l=model_l,
402
+ models=self._device_un_ignore_rules.get_models(),
403
+ cache_dict=self._un_ignore_prefix_cache,
404
+ )
405
+
406
+ if dt_short_key is not None and self._device_un_ignore_rules.contains(
407
+ model=dt_short_key,
408
+ channel_no=channel.no,
409
+ paramset_key=paramset_key,
410
+ parameter=parameter,
411
+ ):
412
+ return True
413
+
414
+ return self._parameter_is_un_ignored(
415
+ channel=channel,
416
+ paramset_key=paramset_key,
417
+ parameter=parameter,
418
+ custom_only=custom_only,
419
+ )
420
+
421
+ def should_skip_parameter(
422
+ self,
423
+ *,
424
+ channel: ChannelProtocol,
425
+ paramset_key: ParamsetKey,
426
+ parameter: ParameterName,
427
+ parameter_is_un_ignored: bool,
428
+ ) -> bool:
429
+ """Determine if a parameter should be skipped during data point creation."""
430
+ if self.parameter_is_ignored(
431
+ channel=channel,
432
+ paramset_key=paramset_key,
433
+ parameter=parameter,
434
+ ):
435
+ _LOGGER.debug(
436
+ "SHOULD_SKIP_PARAMETER: Ignoring parameter: %s [%s]",
437
+ parameter,
438
+ channel.address,
439
+ )
440
+ return True
441
+
442
+ if (
443
+ paramset_key == ParamsetKey.MASTER
444
+ and (parameters := RELEVANT_MASTER_PARAMSETS_BY_CHANNEL.get(channel.no)) is not None
445
+ and parameter in parameters
446
+ ):
447
+ return False
448
+
449
+ return paramset_key == ParamsetKey.MASTER and not parameter_is_un_ignored
450
+
451
+ def _check_master_parameter_is_ignored(
452
+ self,
453
+ *,
454
+ channel: ChannelProtocol,
455
+ parameter: ParameterName,
456
+ model_l: ModelName,
457
+ ) -> bool:
458
+ """Check if a MASTER parameter should be ignored."""
459
+ # Check channel-level relevance
460
+ if (parameters := RELEVANT_MASTER_PARAMSETS_BY_CHANNEL.get(channel.no)) is not None and parameter in parameters:
461
+ return False
462
+
463
+ # Check custom un-ignore rules
464
+ if self._custom_un_ignore_rules.contains(
465
+ model=model_l,
466
+ channel_no=channel.no,
467
+ paramset_key=ParamsetKey.MASTER,
468
+ parameter=parameter,
469
+ ):
470
+ return False
471
+
472
+ # Check device-specific rules
473
+ dt_short_key = self._resolve_prefix_key(
474
+ model_l=model_l,
475
+ models=self._device_un_ignore_rules.get_models(),
476
+ cache_dict=self._un_ignore_prefix_cache,
477
+ )
478
+
479
+ return dt_short_key is not None and not self._device_un_ignore_rules.contains(
480
+ model=dt_short_key,
481
+ channel_no=channel.no,
482
+ paramset_key=ParamsetKey.MASTER,
483
+ parameter=parameter,
484
+ )
485
+
486
+ def _check_parameter_is_ignored(
487
+ self,
488
+ *,
489
+ channel: ChannelProtocol,
490
+ paramset_key: ParamsetKey,
491
+ parameter: ParameterName,
492
+ model_l: ModelName,
493
+ ) -> bool:
494
+ """Check if a parameter is ignored based on paramset type."""
495
+ if paramset_key == ParamsetKey.VALUES:
496
+ return self._check_values_parameter_is_ignored(
497
+ channel=channel,
498
+ parameter=parameter,
499
+ model_l=model_l,
500
+ )
501
+
502
+ if paramset_key == ParamsetKey.MASTER:
503
+ return self._check_master_parameter_is_ignored(
504
+ channel=channel,
505
+ parameter=parameter,
506
+ model_l=model_l,
507
+ )
508
+
509
+ return False
510
+
511
+ def _check_parameter_is_un_ignored(
512
+ self,
513
+ *,
514
+ channel: ChannelProtocol,
515
+ paramset_key: ParamsetKey,
516
+ parameter: ParameterName,
517
+ model_l: ModelName,
518
+ custom_only: bool,
519
+ ) -> bool:
520
+ """Check if a parameter matches any un-ignore rule."""
521
+ # Build search matrix for wildcard matching
522
+ search_patterns: tuple[tuple[ModelName, UnIgnoreChannelNo], ...]
523
+ if paramset_key == ParamsetKey.VALUES:
524
+ search_patterns = (
525
+ (model_l, channel.no),
526
+ (model_l, UN_IGNORE_WILDCARD),
527
+ (UN_IGNORE_WILDCARD, channel.no),
528
+ (UN_IGNORE_WILDCARD, UN_IGNORE_WILDCARD),
529
+ )
530
+ else:
531
+ search_patterns = ((model_l, channel.no),)
532
+
533
+ # Check custom rules
534
+ for ml, cno in search_patterns:
535
+ if self._custom_un_ignore_rules.contains(
536
+ model=ml,
537
+ channel_no=cno,
538
+ paramset_key=paramset_key,
539
+ parameter=parameter,
540
+ ):
541
+ return True
542
+
543
+ # Check predefined un-ignore parameters
544
+ if not custom_only:
545
+ un_ignore_parameters = _get_parameters_for_model_prefix(model_prefix=model_l)
546
+ if un_ignore_parameters and parameter in un_ignore_parameters:
547
+ return True
548
+
549
+ return False
550
+
551
+ def _check_values_parameter_is_ignored(
552
+ self,
553
+ *,
554
+ channel: ChannelProtocol,
555
+ parameter: ParameterName,
556
+ model_l: ModelName,
557
+ ) -> bool:
558
+ """Check if a VALUES parameter should be ignored."""
559
+ # Check if un-ignored first
560
+ if self.parameter_is_un_ignored(
561
+ channel=channel,
562
+ paramset_key=ParamsetKey.VALUES,
563
+ parameter=parameter,
564
+ ):
565
+ return False
566
+
567
+ # Check static ignore lists
568
+ if (
569
+ parameter in IGNORED_PARAMETERS or parameter_is_wildcard_ignored(parameter=parameter)
570
+ ) and parameter not in self._required_parameters:
571
+ return True
572
+
573
+ # Check device-specific ignore lists
574
+ if hms.element_matches_key(
575
+ search_elements=IGNORE_PARAMETERS_BY_DEVICE_LOWER.get(parameter, []),
576
+ compare_with=model_l,
577
+ ):
578
+ return True
579
+
580
+ # Check event suppression
581
+ if hms.element_matches_key(
582
+ search_elements=IGNORE_DEVICES_FOR_DATA_POINT_EVENTS_LOWER,
583
+ compare_with=parameter,
584
+ search_key=model_l,
585
+ do_right_wildcard_search=False,
586
+ ):
587
+ return True
588
+
589
+ # Check channel-specific parameter rules
590
+ accept_channel = ACCEPT_PARAMETER_ONLY_ON_CHANNEL.get(parameter)
591
+ return accept_channel is not None and accept_channel != channel.no
592
+
593
+ def _init(self) -> None:
594
+ """Initialize the registry with static and configured rules."""
595
+ # Load device-specific rules from RELEVANT_MASTER_PARAMSETS_BY_DEVICE
596
+ for model, (channel_nos, parameters) in RELEVANT_MASTER_PARAMSETS_BY_DEVICE.items():
597
+ model_l = model.lower()
598
+
599
+ effective_channels = channel_nos if channel_nos else frozenset({None})
600
+ for channel_no in effective_channels:
601
+ # Track relevant channels for MASTER paramset fetching
602
+ if model_l not in self._relevant_master_channels:
603
+ self._relevant_master_channels[model_l] = set()
604
+ self._relevant_master_channels[model_l].add(channel_no)
605
+
606
+ # Add un-ignore rules
607
+ self._device_un_ignore_rules.update_parameters(
608
+ model=model_l,
609
+ channel_no=channel_no,
610
+ paramset_key=ParamsetKey.MASTER,
611
+ parameters=parameters,
612
+ )
613
+
614
+ # Process user-provided un-ignore entries
615
+ self._process_un_ignore_entries(lines=self._raw_un_ignores)
616
+
617
+ def _parameter_is_un_ignored(
618
+ self,
619
+ *,
620
+ channel: ChannelProtocol,
621
+ paramset_key: ParamsetKey,
622
+ parameter: ParameterName,
623
+ custom_only: bool = False,
624
+ ) -> bool:
625
+ """
626
+ Check if parameter is on a custom un-ignore list.
627
+
628
+ This can be either the user's un-ignore configuration or the
629
+ predefined UN_IGNORE_PARAMETERS_BY_DEVICE.
630
+ """
631
+ # Fast path: simple VALUES parameter un-ignore
632
+ if paramset_key == ParamsetKey.VALUES and parameter in self._custom_un_ignore_values_parameters:
633
+ return True
634
+
635
+ model_l = channel.device.model.lower()
636
+ cache_key = UnIgnoreCacheKey(model_l, channel.no, paramset_key, parameter, custom_only)
637
+
638
+ if cache_key in self._param_un_ignored_cache:
639
+ return self._param_un_ignored_cache[cache_key]
640
+
641
+ result = self._check_parameter_is_un_ignored(
642
+ channel=channel,
643
+ paramset_key=paramset_key,
644
+ parameter=parameter,
645
+ model_l=model_l,
646
+ custom_only=custom_only,
647
+ )
648
+ self._param_un_ignored_cache[cache_key] = result
649
+ return result
650
+
651
+ def _process_complex_un_ignore_entry(self, *, parsed: ParsedUnIgnoreLine) -> None:
652
+ """Process a complex un-ignore entry."""
653
+ entry = parsed.entry
654
+ assert entry is not None # noqa: S101
655
+
656
+ # Track MASTER channels for paramset fetching
657
+ if entry.paramset_key == ParamsetKey.MASTER and (isinstance(entry.channel_no, int) or entry.channel_no is None):
658
+ if entry.model not in self._relevant_master_channels:
659
+ self._relevant_master_channels[entry.model] = set()
660
+ self._relevant_master_channels[entry.model].add(entry.channel_no)
661
+
662
+ self._custom_un_ignore_rules.add_parameter(
663
+ model=entry.model,
664
+ channel_no=entry.channel_no,
665
+ paramset_key=entry.paramset_key,
666
+ parameter=entry.parameter,
667
+ )
668
+
669
+ def _process_un_ignore_entries(self, *, lines: Iterable[str]) -> None:
670
+ """Process un-ignore configuration entries into the registry."""
671
+ for line in lines:
672
+ if not line.strip():
673
+ continue
674
+
675
+ parsed = parse_un_ignore_line(line=line)
676
+
677
+ if parsed.is_error:
678
+ _LOGGER.error( # i18n-log: ignore
679
+ "PROCESS_UN_IGNORE_ENTRY failed: %s",
680
+ parsed.error,
681
+ )
682
+ elif parsed.is_simple:
683
+ self._custom_un_ignore_values_parameters.add(parsed.simple_parameter) # type: ignore[arg-type]
684
+ elif parsed.is_complex:
685
+ self._process_complex_un_ignore_entry(parsed=parsed)
686
+
687
+ def _resolve_prefix_key(
688
+ self,
689
+ *,
690
+ model_l: ModelName,
691
+ models: Iterable[ModelName],
692
+ cache_dict: dict[ModelName, str | None],
693
+ ) -> str | None:
694
+ """Resolve and memoize the first model key that is a prefix of model_l."""
695
+ if model_l in cache_dict:
696
+ return cache_dict[model_l]
697
+
698
+ dt_short_key = next((k for k in models if model_l.startswith(k)), None)
699
+ cache_dict[model_l] = dt_short_key
700
+ return dt_short_key
701
+
702
+
703
+ # =============================================================================
704
+ # Validation Helper
705
+ # =============================================================================
706
+
707
+
708
+ def check_ignore_parameters_is_clean() -> bool:
709
+ """Check if any required parameter is incorrectly in the ignored parameters list."""
710
+ un_ignore_parameters_by_device: list[str] = []
711
+ for params in UN_IGNORE_PARAMETERS_BY_MODEL_LOWER.values():
712
+ un_ignore_parameters_by_device.extend(params)
713
+
714
+ required = get_required_parameters()
715
+ conflicting = [
716
+ parameter
717
+ for parameter in required
718
+ if (parameter in IGNORED_PARAMETERS or parameter_is_wildcard_ignored(parameter=parameter))
719
+ and parameter not in un_ignore_parameters_by_device
720
+ ]
721
+
722
+ return len(conflicting) == 0