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.
- aiohomematic/__init__.py +61 -0
- aiohomematic/async_support.py +212 -0
- aiohomematic/central/__init__.py +2309 -0
- aiohomematic/central/decorators.py +155 -0
- aiohomematic/central/rpc_server.py +295 -0
- aiohomematic/client/__init__.py +1848 -0
- aiohomematic/client/_rpc_errors.py +81 -0
- aiohomematic/client/json_rpc.py +1326 -0
- aiohomematic/client/rpc_proxy.py +311 -0
- aiohomematic/const.py +1127 -0
- aiohomematic/context.py +18 -0
- aiohomematic/converter.py +108 -0
- aiohomematic/decorators.py +302 -0
- aiohomematic/exceptions.py +164 -0
- aiohomematic/hmcli.py +186 -0
- aiohomematic/model/__init__.py +140 -0
- aiohomematic/model/calculated/__init__.py +84 -0
- aiohomematic/model/calculated/climate.py +290 -0
- aiohomematic/model/calculated/data_point.py +327 -0
- aiohomematic/model/calculated/operating_voltage_level.py +299 -0
- aiohomematic/model/calculated/support.py +234 -0
- aiohomematic/model/custom/__init__.py +177 -0
- aiohomematic/model/custom/climate.py +1532 -0
- aiohomematic/model/custom/cover.py +792 -0
- aiohomematic/model/custom/data_point.py +334 -0
- aiohomematic/model/custom/definition.py +871 -0
- aiohomematic/model/custom/light.py +1128 -0
- aiohomematic/model/custom/lock.py +394 -0
- aiohomematic/model/custom/siren.py +275 -0
- aiohomematic/model/custom/support.py +41 -0
- aiohomematic/model/custom/switch.py +175 -0
- aiohomematic/model/custom/valve.py +114 -0
- aiohomematic/model/data_point.py +1123 -0
- aiohomematic/model/device.py +1445 -0
- aiohomematic/model/event.py +208 -0
- aiohomematic/model/generic/__init__.py +217 -0
- aiohomematic/model/generic/action.py +34 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +27 -0
- aiohomematic/model/generic/data_point.py +171 -0
- aiohomematic/model/generic/dummy.py +147 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +39 -0
- aiohomematic/model/generic/sensor.py +74 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +29 -0
- aiohomematic/model/hub/__init__.py +333 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/data_point.py +340 -0
- aiohomematic/model/hub/number.py +39 -0
- aiohomematic/model/hub/select.py +49 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +44 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/support.py +586 -0
- aiohomematic/model/update.py +143 -0
- aiohomematic/property_decorators.py +496 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/store/__init__.py +34 -0
- aiohomematic/store/dynamic.py +551 -0
- aiohomematic/store/persistent.py +988 -0
- aiohomematic/store/visibility.py +812 -0
- aiohomematic/support.py +664 -0
- aiohomematic/validator.py +112 -0
- aiohomematic-2025.11.3.dist-info/METADATA +144 -0
- aiohomematic-2025.11.3.dist-info/RECORD +77 -0
- aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
- aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
- aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
- 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
|