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