aiohomematic 2025.11.3__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 +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. aiohomematic-2025.11.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,327 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Module with base class for calculated data points."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Callable
8
+ from datetime import datetime
9
+ import logging
10
+ from typing import Any, Final, cast
11
+
12
+ from aiohomematic.const import (
13
+ CALLBACK_TYPE,
14
+ INIT_DATETIME,
15
+ CallSource,
16
+ CalulatedParameter,
17
+ DataPointKey,
18
+ DataPointUsage,
19
+ Operations,
20
+ ParameterType,
21
+ ParamsetKey,
22
+ )
23
+ from aiohomematic.model import device as hmd
24
+ from aiohomematic.model.custom import definition as hmed
25
+ from aiohomematic.model.data_point import BaseDataPoint
26
+ from aiohomematic.model.generic import DpDummy, data_point as hmge
27
+ from aiohomematic.model.support import (
28
+ DataPointNameData,
29
+ DataPointPathData,
30
+ GenericParameterType,
31
+ PathData,
32
+ generate_unique_id,
33
+ get_data_point_name_data,
34
+ )
35
+ from aiohomematic.property_decorators import config_property, hm_property, state_property
36
+
37
+ _LOGGER: Final = logging.getLogger(__name__)
38
+
39
+
40
+ class CalculatedDataPoint[ParameterT: GenericParameterType](BaseDataPoint):
41
+ """Base class for calculated data point."""
42
+
43
+ __slots__ = (
44
+ "_data_points",
45
+ "_default",
46
+ "_max",
47
+ "_min",
48
+ "_multiplier",
49
+ "_operations",
50
+ "_service",
51
+ "_type",
52
+ "_unit",
53
+ "_unregister_callbacks",
54
+ "_values",
55
+ "_visible",
56
+ )
57
+
58
+ _calculated_parameter: CalulatedParameter = None # type: ignore[assignment]
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ channel: hmd.Channel,
64
+ ) -> None:
65
+ """Initialize the data point."""
66
+ self._unregister_callbacks: list[CALLBACK_TYPE] = []
67
+ unique_id = generate_unique_id(
68
+ central=channel.central, address=channel.address, parameter=self._calculated_parameter, prefix="calculated"
69
+ )
70
+ super().__init__(
71
+ channel=channel,
72
+ unique_id=unique_id,
73
+ is_in_multiple_channels=hmed.is_multi_channel_device(model=channel.device.model, category=self.category),
74
+ )
75
+ self._data_points: Final[list[hmge.GenericDataPoint]] = []
76
+ self._type: ParameterType = None # type: ignore[assignment]
77
+ self._values: tuple[str, ...] | None = None
78
+ self._max: ParameterT = None # type: ignore[assignment]
79
+ self._min: ParameterT = None # type: ignore[assignment]
80
+ self._default: ParameterT = None # type: ignore[assignment]
81
+ self._visible: bool = True
82
+ self._service: bool = False
83
+ self._operations: int = 5
84
+ self._unit: str | None = None
85
+ self._multiplier: float = 1.0
86
+ self._init_data_point_fields()
87
+
88
+ def _init_data_point_fields(self) -> None:
89
+ """Init the data point fields."""
90
+ _LOGGER.debug(
91
+ "INIT_DATA_POINT_FIELDS: Initialising the data point fields for %s",
92
+ self.full_name,
93
+ )
94
+
95
+ def _add_data_point[DataPointT: hmge.GenericDataPoint](
96
+ self, *, parameter: str, paramset_key: ParamsetKey | None, data_point_type: type[DataPointT]
97
+ ) -> DataPointT:
98
+ """Add a new data point."""
99
+ if generic_data_point := self._channel.get_generic_data_point(parameter=parameter, paramset_key=paramset_key):
100
+ self._data_points.append(generic_data_point)
101
+ self._unregister_callbacks.append(
102
+ generic_data_point.register_internal_data_point_updated_callback(cb=self.emit_data_point_updated_event)
103
+ )
104
+ return cast(data_point_type, generic_data_point) # type: ignore[valid-type]
105
+ return cast(
106
+ data_point_type, # type:ignore[valid-type]
107
+ DpDummy(channel=self._channel, param_field=parameter),
108
+ )
109
+
110
+ def _add_device_data_point[DataPointT: hmge.GenericDataPoint](
111
+ self,
112
+ *,
113
+ channel_address: str,
114
+ parameter: str,
115
+ paramset_key: ParamsetKey | None,
116
+ data_point_type: type[DataPointT],
117
+ ) -> DataPointT:
118
+ """Add a new data point."""
119
+ if generic_data_point := self._channel.device.get_generic_data_point(
120
+ channel_address=channel_address, parameter=parameter, paramset_key=paramset_key
121
+ ):
122
+ self._data_points.append(generic_data_point)
123
+ self._unregister_callbacks.append(
124
+ generic_data_point.register_internal_data_point_updated_callback(cb=self.emit_data_point_updated_event)
125
+ )
126
+ return cast(data_point_type, generic_data_point) # type: ignore[valid-type]
127
+ return cast(
128
+ data_point_type, # type:ignore[valid-type]
129
+ DpDummy(channel=self._channel, param_field=parameter),
130
+ )
131
+
132
+ @property
133
+ def is_readable(self) -> bool:
134
+ """Return, if data_point is readable."""
135
+ return bool(self._operations & Operations.READ)
136
+
137
+ @staticmethod
138
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
139
+ """Return if this calculated data point is relevant for the channel."""
140
+ return False
141
+
142
+ @property
143
+ def is_writeable(self) -> bool:
144
+ """Return, if data_point is writeable."""
145
+ return bool(self._operations & Operations.WRITE)
146
+
147
+ @property
148
+ def default(self) -> ParameterT:
149
+ """Return default value."""
150
+ return self._default
151
+
152
+ @hm_property(cached=True)
153
+ def dpk(self) -> DataPointKey:
154
+ """Return data_point key value."""
155
+ return DataPointKey(
156
+ interface_id=self._device.interface_id,
157
+ channel_address=self._channel.address,
158
+ paramset_key=ParamsetKey.CALCULATED,
159
+ parameter=self._calculated_parameter,
160
+ )
161
+
162
+ @property
163
+ def hmtype(self) -> ParameterType:
164
+ """Return the Homematic type."""
165
+ return self._type
166
+
167
+ @config_property
168
+ def max(self) -> ParameterT:
169
+ """Return max value."""
170
+ return self._max
171
+
172
+ @config_property
173
+ def min(self) -> ParameterT:
174
+ """Return min value."""
175
+ return self._min
176
+
177
+ @property
178
+ def multiplier(self) -> float:
179
+ """Return multiplier value."""
180
+ return self._multiplier
181
+
182
+ @property
183
+ def parameter(self) -> str:
184
+ """Return parameter name."""
185
+ return self._calculated_parameter
186
+
187
+ @property
188
+ def paramset_key(self) -> ParamsetKey:
189
+ """Return paramset_key name."""
190
+ return ParamsetKey.CALCULATED
191
+
192
+ @property
193
+ def service(self) -> bool:
194
+ """Return the if data_point is visible in ccu."""
195
+ return self._service
196
+
197
+ @property
198
+ def supports_events(self) -> bool:
199
+ """Return, if data_point is supports events."""
200
+ return bool(self._operations & Operations.EVENT)
201
+
202
+ @config_property
203
+ def unit(self) -> str | None:
204
+ """Return unit value."""
205
+ return self._unit
206
+
207
+ @config_property
208
+ def values(self) -> tuple[str, ...] | None:
209
+ """Return the values."""
210
+ return self._values
211
+
212
+ @property
213
+ def visible(self) -> bool:
214
+ """Return the if data_point is visible in ccu."""
215
+ return self._visible
216
+
217
+ @state_property
218
+ def modified_at(self) -> datetime:
219
+ """Return the latest last update timestamp."""
220
+ modified_at: datetime = INIT_DATETIME
221
+ for dp in self._readable_data_points:
222
+ if (data_point_modified_at := dp.modified_at) and data_point_modified_at > modified_at:
223
+ modified_at = data_point_modified_at
224
+ return modified_at
225
+
226
+ @state_property
227
+ def refreshed_at(self) -> datetime:
228
+ """Return the latest last refresh timestamp."""
229
+ refreshed_at: datetime = INIT_DATETIME
230
+ for dp in self._readable_data_points:
231
+ if (data_point_refreshed_at := dp.refreshed_at) and data_point_refreshed_at > refreshed_at:
232
+ refreshed_at = data_point_refreshed_at
233
+ return refreshed_at
234
+
235
+ @property
236
+ def has_data_points(self) -> bool:
237
+ """Return if there are data points."""
238
+ return len(self._data_points) > 0
239
+
240
+ @property
241
+ def is_valid(self) -> bool:
242
+ """Return if the state is valid."""
243
+ return all(dp.is_valid for dp in self._relevant_data_points)
244
+
245
+ @property
246
+ def state_uncertain(self) -> bool:
247
+ """Return, if the state is uncertain."""
248
+ return any(dp.state_uncertain for dp in self._relevant_data_points)
249
+
250
+ @property
251
+ def _readable_data_points(self) -> tuple[hmge.GenericDataPoint, ...]:
252
+ """Returns the list of readable data points."""
253
+ return tuple(dp for dp in self._data_points if dp.is_readable)
254
+
255
+ @property
256
+ def _relevant_data_points(self) -> tuple[hmge.GenericDataPoint, ...]:
257
+ """Returns the list of relevant data points. To be overridden by subclasses."""
258
+ return self._readable_data_points
259
+
260
+ @property
261
+ def _relevant_values_data_points(self) -> tuple[hmge.GenericDataPoint, ...]:
262
+ """Returns the list of relevant VALUES data points. To be overridden by subclasses."""
263
+ return tuple(dp for dp in self._readable_data_points if dp.paramset_key == ParamsetKey.VALUES)
264
+
265
+ @property
266
+ def data_point_name_postfix(self) -> str:
267
+ """Return the data point name postfix."""
268
+ return ""
269
+
270
+ def _get_path_data(self) -> PathData:
271
+ """Return the path data of the data_point."""
272
+ return DataPointPathData(
273
+ interface=self._device.client.interface,
274
+ address=self._device.address,
275
+ channel_no=self._channel.no,
276
+ kind=self._category,
277
+ )
278
+
279
+ def _get_data_point_name(self) -> DataPointNameData:
280
+ """Create the name for the data point."""
281
+ return get_data_point_name_data(channel=self._channel, parameter=self._calculated_parameter)
282
+
283
+ def _get_data_point_usage(self) -> DataPointUsage:
284
+ """Generate the usage for the data point."""
285
+ return DataPointUsage.DATA_POINT
286
+
287
+ def _get_signature(self) -> str:
288
+ """Return the signature of the data_point."""
289
+ return f"{self._category}/{self._channel.device.model}/{self._calculated_parameter}"
290
+
291
+ async def load_data_point_value(self, *, call_source: CallSource, direct_call: bool = False) -> None:
292
+ """Init the data point values."""
293
+ for dp in self._readable_data_points:
294
+ await dp.load_data_point_value(call_source=call_source, direct_call=direct_call)
295
+ self.emit_data_point_updated_event()
296
+
297
+ def is_state_change(self, **kwargs: Any) -> bool:
298
+ """
299
+ Check if the state changes due to kwargs.
300
+
301
+ If the state is uncertain, the state should also marked as changed.
302
+ """
303
+ if self.state_uncertain:
304
+ return True
305
+ _LOGGER.debug("NO_STATE_CHANGE: %s", self.name)
306
+ return False
307
+
308
+ @property
309
+ def _should_emit_data_point_updated_callback(self) -> bool:
310
+ """Check if a data point has been updated or refreshed."""
311
+ if self.emitted_event_recently: # pylint: disable=using-constant-test
312
+ return False
313
+
314
+ if (relevant_values_data_point := self._relevant_values_data_points) is not None and len(
315
+ relevant_values_data_point
316
+ ) <= 1:
317
+ return True
318
+
319
+ return all(dp.emitted_event_recently for dp in relevant_values_data_point)
320
+
321
+ def _unregister_data_point_updated_callback(self, *, cb: Callable, custom_id: str) -> None:
322
+ """Unregister update callback."""
323
+ for unregister in self._unregister_callbacks:
324
+ if unregister is not None:
325
+ unregister()
326
+
327
+ super()._unregister_data_point_updated_callback(cb=cb, custom_id=custom_id)
@@ -0,0 +1,299 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2021-2025
3
+ """Module for calculating the operating voltage level in the sensor category."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from enum import StrEnum
10
+ import logging
11
+ from typing import Any, Final
12
+
13
+ from aiohomematic.const import CalulatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
14
+ from aiohomematic.model import device as hmd
15
+ from aiohomematic.model.calculated.data_point import CalculatedDataPoint
16
+ from aiohomematic.model.calculated.support import calculate_operating_voltage_level
17
+ from aiohomematic.model.generic import DpFloat, DpSensor
18
+ from aiohomematic.property_decorators import state_property
19
+ from aiohomematic.support import element_matches_key, extract_exc_args
20
+
21
+ _BATTERY_QTY: Final = "Battery Qty"
22
+ _BATTERY_TYPE: Final = "Battery Type"
23
+ _LOW_BAT_LIMIT: Final = "Low Battery Limit"
24
+ _LOW_BAT_LIMIT_DEFAULT: Final = "Low Battery Limit Default"
25
+ _VOLTAGE_MAX: Final = "Voltage max"
26
+
27
+ _LOGGER: Final = logging.getLogger(__name__)
28
+
29
+
30
+ class OperatingVoltageLevel[SensorT: float | None](CalculatedDataPoint[SensorT]):
31
+ """Implementation of a calculated sensor for operating voltage level."""
32
+
33
+ __slots__ = (
34
+ "_battery_data",
35
+ "_dp_low_bat_limit",
36
+ "_dp_operating_voltage",
37
+ "_low_bat_limit_default",
38
+ "_voltage_max",
39
+ )
40
+
41
+ _calculated_parameter = CalulatedParameter.OPERATING_VOLTAGE_LEVEL
42
+ _category = DataPointCategory.SENSOR
43
+
44
+ def __init__(self, *, channel: hmd.Channel) -> None:
45
+ """Initialize the data point."""
46
+ super().__init__(channel=channel)
47
+ self._type = ParameterType.FLOAT
48
+ self._unit = "%"
49
+
50
+ def _init_data_point_fields(self) -> None:
51
+ """Init the data point fields."""
52
+ super()._init_data_point_fields()
53
+ self._battery_data = _get_battery_data(model=self._channel.device.model)
54
+
55
+ operating_voltage: DpSensor = self._add_data_point(
56
+ parameter=Parameter.OPERATING_VOLTAGE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
57
+ )
58
+
59
+ self._dp_operating_voltage: DpSensor = (
60
+ operating_voltage
61
+ if isinstance(operating_voltage, DpSensor)
62
+ else self._add_data_point(
63
+ parameter=Parameter.BATTERY_STATE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
64
+ )
65
+ )
66
+
67
+ low_bat_limit: DpSensor = self._add_data_point(
68
+ parameter=Parameter.LOW_BAT_LIMIT, paramset_key=ParamsetKey.MASTER, data_point_type=DpSensor
69
+ )
70
+
71
+ self._dp_low_bat_limit: DpFloat = (
72
+ low_bat_limit
73
+ if isinstance(low_bat_limit, DpFloat)
74
+ else self._add_device_data_point(
75
+ channel_address=self.channel.device.address,
76
+ parameter=Parameter.LOW_BAT_LIMIT,
77
+ paramset_key=ParamsetKey.MASTER,
78
+ data_point_type=DpFloat,
79
+ )
80
+ )
81
+
82
+ self._low_bat_limit_default = (
83
+ float(self._dp_low_bat_limit.default) if isinstance(self._dp_low_bat_limit, DpFloat) else None
84
+ )
85
+ self._voltage_max = (
86
+ float(_BatteryVoltage.get(self._battery_data.battery) * self._battery_data.quantity) # type: ignore[operator]
87
+ if self._battery_data is not None
88
+ else None
89
+ )
90
+
91
+ @staticmethod
92
+ def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
93
+ """Return if this calculated data point is relevant for the model."""
94
+ if element_matches_key(
95
+ search_elements=_IGNORE_OPERATING_VOLTAGE_LEVEL_MODELS, compare_with=channel.device.model
96
+ ):
97
+ return False
98
+ return element_matches_key(
99
+ search_elements=_OPERATING_VOLTAGE_LEVEL_MODELS.keys(), compare_with=channel.device.model
100
+ ) and (
101
+ (
102
+ channel.get_generic_data_point(
103
+ parameter=Parameter.OPERATING_VOLTAGE,
104
+ paramset_key=ParamsetKey.VALUES,
105
+ )
106
+ and channel.get_generic_data_point(parameter=Parameter.LOW_BAT_LIMIT, paramset_key=ParamsetKey.MASTER)
107
+ )
108
+ is not None
109
+ or (
110
+ channel.get_generic_data_point(
111
+ parameter=Parameter.BATTERY_STATE,
112
+ paramset_key=ParamsetKey.VALUES,
113
+ )
114
+ and channel.device.get_generic_data_point(
115
+ channel_address=channel.device.address,
116
+ parameter=Parameter.LOW_BAT_LIMIT,
117
+ paramset_key=ParamsetKey.MASTER,
118
+ )
119
+ )
120
+ is not None
121
+ )
122
+
123
+ @state_property
124
+ def additional_information(self) -> dict[str, Any]:
125
+ """Return additional information about the entity."""
126
+ ainfo = super().additional_information
127
+ if self._battery_data is not None:
128
+ ainfo.update(
129
+ {
130
+ _BATTERY_QTY: self._battery_data.quantity,
131
+ _BATTERY_TYPE: self._battery_data.battery,
132
+ _LOW_BAT_LIMIT: f"{self._low_bat_limit}V",
133
+ _LOW_BAT_LIMIT_DEFAULT: f"{self._low_bat_limit_default}V",
134
+ _VOLTAGE_MAX: f"{self._voltage_max}V",
135
+ }
136
+ )
137
+ return ainfo
138
+
139
+ @state_property
140
+ def value(self) -> float | None:
141
+ """Return the value."""
142
+ try:
143
+ return calculate_operating_voltage_level(
144
+ operating_voltage=self._dp_operating_voltage.value,
145
+ low_bat_limit=self._low_bat_limit_default,
146
+ voltage_max=self._voltage_max,
147
+ )
148
+ except Exception as exc:
149
+ _LOGGER.debug(
150
+ "OperatingVoltageLevel: Failed to calculate sensor for %s: %s",
151
+ self._channel.name,
152
+ extract_exc_args(exc=exc),
153
+ )
154
+ return None
155
+
156
+ @property
157
+ def _low_bat_limit(self) -> float | None:
158
+ """Return the min value."""
159
+ return (
160
+ float(self._dp_low_bat_limit.value)
161
+ if self._dp_low_bat_limit is not None and self._dp_low_bat_limit.value is not None
162
+ else None
163
+ )
164
+
165
+
166
+ class _BatteryType(StrEnum):
167
+ CR2032 = "CR2032"
168
+ LR44 = "LR44"
169
+ R03 = "AAA"
170
+ R14 = "BABY"
171
+ R6 = "AA"
172
+ UNKNOWN = "UNKNOWN"
173
+
174
+
175
+ _BatteryVoltage: Final[Mapping[_BatteryType, float]] = {
176
+ _BatteryType.CR2032: 3.0,
177
+ _BatteryType.LR44: 1.5,
178
+ _BatteryType.R03: 1.5,
179
+ _BatteryType.R14: 1.5,
180
+ _BatteryType.R6: 1.5,
181
+ }
182
+
183
+
184
+ @dataclass(frozen=True, kw_only=True, slots=True)
185
+ class _BatteryData:
186
+ model: str
187
+ battery: _BatteryType
188
+ quantity: int = 1
189
+
190
+
191
+ # This list is sorted. models with shorted model types are sorted to
192
+ _BATTERY_DATA: Final = (
193
+ # HM long model str
194
+ _BatteryData(model="HM-CC-RT-DN", battery=_BatteryType.R6, quantity=2),
195
+ _BatteryData(model="HM-Dis-EP-WM55", battery=_BatteryType.R03, quantity=2),
196
+ _BatteryData(model="HM-ES-TX-WM", battery=_BatteryType.R6, quantity=4),
197
+ _BatteryData(model="HM-OU-CFM-TW", battery=_BatteryType.R14, quantity=2),
198
+ _BatteryData(model="HM-PB-2-FM", battery=_BatteryType.R03, quantity=2),
199
+ _BatteryData(model="HM-PB-2-WM55", battery=_BatteryType.R03, quantity=2),
200
+ _BatteryData(model="HM-PB-6-WM55", battery=_BatteryType.R03, quantity=2),
201
+ _BatteryData(model="HM-PBI-4-FM", battery=_BatteryType.CR2032),
202
+ _BatteryData(model="HM-RC-4-2", battery=_BatteryType.R03),
203
+ _BatteryData(model="HM-RC-8", battery=_BatteryType.R03, quantity=2),
204
+ _BatteryData(model="HM-RC-Key4-3", battery=_BatteryType.R03),
205
+ _BatteryData(model="HM-SCI-3-FM", battery=_BatteryType.CR2032),
206
+ _BatteryData(model="HM-Sec-Key", battery=_BatteryType.R6, quantity=3),
207
+ _BatteryData(model="HM-Sec-MDIR-2", battery=_BatteryType.R6, quantity=3),
208
+ _BatteryData(model="HM-Sec-RHS", battery=_BatteryType.LR44, quantity=2),
209
+ _BatteryData(model="HM-Sec-SC-2", battery=_BatteryType.LR44, quantity=2),
210
+ _BatteryData(model="HM-Sec-SCo", battery=_BatteryType.R03),
211
+ _BatteryData(model="HM-Sec-SD-2", battery=_BatteryType.UNKNOWN),
212
+ _BatteryData(model="HM-Sec-Sir-WM", battery=_BatteryType.R14, quantity=2),
213
+ _BatteryData(model="HM-Sec-TiS", battery=_BatteryType.CR2032),
214
+ _BatteryData(model="HM-Sec-Win", battery=_BatteryType.UNKNOWN),
215
+ _BatteryData(model="HM-Sen-MDIR-O-2", battery=_BatteryType.R6, quantity=3),
216
+ _BatteryData(model="HM-Sen-MDIR-SM", battery=_BatteryType.R6, quantity=3),
217
+ _BatteryData(model="HM-Sen-MDIR-WM55", battery=_BatteryType.R03, quantity=2),
218
+ _BatteryData(model="HM-SwI-3-FM", battery=_BatteryType.CR2032),
219
+ _BatteryData(model="HM-TC-IT-WM-W-EU", battery=_BatteryType.R03, quantity=2),
220
+ _BatteryData(model="HM-WDS10-TH-O", battery=_BatteryType.R6, quantity=2),
221
+ _BatteryData(model="HM-WDS30-OT2-SM", battery=_BatteryType.R6, quantity=2),
222
+ _BatteryData(model="HM-WDS30-T-O", battery=_BatteryType.R03, quantity=2),
223
+ _BatteryData(model="HM-WDS40-TH-I", battery=_BatteryType.R6, quantity=2),
224
+ # HM short model str
225
+ _BatteryData(model="HM-Sec-SD", battery=_BatteryType.R6, quantity=3),
226
+ # HmIP model > 4
227
+ _BatteryData(model="HmIP-ASIR-O", battery=_BatteryType.UNKNOWN),
228
+ _BatteryData(model="HmIP-DSD-PCB", battery=_BatteryType.R03, quantity=2),
229
+ _BatteryData(model="HmIP-PCBS-BAT", battery=_BatteryType.UNKNOWN),
230
+ _BatteryData(model="HmIP-SMI55", battery=_BatteryType.R03, quantity=2),
231
+ _BatteryData(model="HmIP-SMO230", battery=_BatteryType.UNKNOWN),
232
+ _BatteryData(model="HmIP-STE2-PCB", battery=_BatteryType.R6, quantity=2),
233
+ _BatteryData(model="HmIP-SWDO-I", battery=_BatteryType.R03, quantity=2),
234
+ _BatteryData(model="HmIP-SWDO-PL", battery=_BatteryType.R03, quantity=2),
235
+ _BatteryData(model="HmIP-WTH-B-2", battery=_BatteryType.R6, quantity=2),
236
+ _BatteryData(model="HmIP-eTRV-CL", battery=_BatteryType.R6, quantity=4),
237
+ # HmIP model 4
238
+ _BatteryData(model="ELV-SH-SW1-BAT", battery=_BatteryType.R6, quantity=2),
239
+ _BatteryData(model="ELV-SH-TACO", battery=_BatteryType.R03, quantity=1),
240
+ _BatteryData(model="HmIP-ASIR", battery=_BatteryType.R6, quantity=3),
241
+ _BatteryData(model="HmIP-FCI1", battery=_BatteryType.CR2032),
242
+ _BatteryData(model="HmIP-FCI6", battery=_BatteryType.R03),
243
+ _BatteryData(model="HmIP-MP3P", battery=_BatteryType.R14, quantity=2),
244
+ _BatteryData(model="HmIP-RCB1", battery=_BatteryType.R03, quantity=2),
245
+ _BatteryData(model="HmIP-SPDR", battery=_BatteryType.R6, quantity=2),
246
+ _BatteryData(model="HmIP-STHD", battery=_BatteryType.R03, quantity=2),
247
+ _BatteryData(model="HmIP-STHO", battery=_BatteryType.R6, quantity=2),
248
+ _BatteryData(model="HmIP-SWDM", battery=_BatteryType.R03, quantity=2),
249
+ _BatteryData(model="HmIP-SWDO", battery=_BatteryType.R03),
250
+ _BatteryData(model="HmIP-SWSD", battery=_BatteryType.UNKNOWN),
251
+ _BatteryData(model="HmIP-eTRV", battery=_BatteryType.R6, quantity=2),
252
+ # HmIP model 3
253
+ _BatteryData(model="ELV-SH-CTH", battery=_BatteryType.CR2032),
254
+ _BatteryData(model="ELV-SH-WSM", battery=_BatteryType.R6, quantity=2),
255
+ _BatteryData(model="HmIP-DBB", battery=_BatteryType.R03),
256
+ _BatteryData(model="HmIP-DLD", battery=_BatteryType.R6, quantity=3),
257
+ _BatteryData(model="HmIP-DLS", battery=_BatteryType.CR2032),
258
+ _BatteryData(model="HmIP-ESI", battery=_BatteryType.R6, quantity=2),
259
+ _BatteryData(model="HmIP-KRC", battery=_BatteryType.R03),
260
+ _BatteryData(model="HmIP-RC8", battery=_BatteryType.R03, quantity=2),
261
+ _BatteryData(model="HmIP-SAM", battery=_BatteryType.R6, quantity=2),
262
+ _BatteryData(model="HmIP-SCI", battery=_BatteryType.R03, quantity=2),
263
+ _BatteryData(model="HmIP-SLO", battery=_BatteryType.R6, quantity=2),
264
+ _BatteryData(model="HmIP-SMI", battery=_BatteryType.R6, quantity=2),
265
+ _BatteryData(model="HmIP-SMO", battery=_BatteryType.R6, quantity=2),
266
+ _BatteryData(model="HmIP-SPI", battery=_BatteryType.R6, quantity=2),
267
+ _BatteryData(model="HmIP-SRH", battery=_BatteryType.R03),
268
+ _BatteryData(model="HmIP-STH", battery=_BatteryType.R03, quantity=2),
269
+ _BatteryData(model="HmIP-STV", battery=_BatteryType.R03, quantity=2),
270
+ _BatteryData(model="HmIP-SWD", battery=_BatteryType.R03, quantity=2),
271
+ _BatteryData(model="HmIP-SWO", battery=_BatteryType.R6, quantity=3),
272
+ _BatteryData(model="HmIP-WGC", battery=_BatteryType.R6, quantity=2),
273
+ _BatteryData(model="HmIP-WKP", battery=_BatteryType.R03, quantity=2),
274
+ _BatteryData(model="HmIP-WRC", battery=_BatteryType.R03, quantity=2),
275
+ _BatteryData(model="HmIP-WSM", battery=_BatteryType.R6, quantity=2),
276
+ _BatteryData(model="HmIP-WTH", battery=_BatteryType.R03, quantity=2),
277
+ )
278
+
279
+ _OPERATING_VOLTAGE_LEVEL_MODELS: Final[Mapping[str, _BatteryData]] = {
280
+ battery.model: battery for battery in _BATTERY_DATA if battery.battery != _BatteryType.UNKNOWN
281
+ }
282
+
283
+ _IGNORE_OPERATING_VOLTAGE_LEVEL_MODELS: Final[tuple[str, ...]] = tuple(
284
+ [battery.model for battery in _BATTERY_DATA if battery.battery == _BatteryType.UNKNOWN]
285
+ )
286
+
287
+
288
+ def _get_battery_data(model: str) -> _BatteryData | None:
289
+ """Return the battery data by model."""
290
+ model_l = model.lower()
291
+ for battery_data in _OPERATING_VOLTAGE_LEVEL_MODELS.values():
292
+ if battery_data.model.lower() == model_l:
293
+ return battery_data
294
+
295
+ for battery_data in _OPERATING_VOLTAGE_LEVEL_MODELS.values():
296
+ if model_l.startswith(battery_data.model.lower()):
297
+ return battery_data
298
+
299
+ return None