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.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Descriptor-based field definitions for calculated data points.
|
|
5
|
+
|
|
6
|
+
This module provides a declarative way to define data point fields,
|
|
7
|
+
eliminating boilerplate in _post_init() methods.
|
|
8
|
+
|
|
9
|
+
Public API of this module is defined by __all__.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Final, cast, overload
|
|
15
|
+
|
|
16
|
+
from aiohomematic.const import ParamsetKey
|
|
17
|
+
from aiohomematic.interfaces import GenericDataPointProtocolAny
|
|
18
|
+
from aiohomematic.property_decorators import DelegatedProperty
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from typing import Self
|
|
22
|
+
|
|
23
|
+
from aiohomematic.model.calculated import CalculatedDataPoint
|
|
24
|
+
|
|
25
|
+
__all__ = ["CalculatedDataPointField"]
|
|
26
|
+
|
|
27
|
+
# Key type for calculated data point dictionary
|
|
28
|
+
type _DataPointKey = tuple[str, ParamsetKey | None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CalculatedDataPointField[DataPointT: GenericDataPointProtocolAny]:
|
|
32
|
+
"""
|
|
33
|
+
Descriptor for declarative calculated data point field definitions.
|
|
34
|
+
|
|
35
|
+
This descriptor eliminates the need for explicit _post_init()
|
|
36
|
+
boilerplate by lazily resolving data points on first access.
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
class MyCalculatedSensor(CalculatedDataPoint):
|
|
40
|
+
# Simple field
|
|
41
|
+
_dp_wind_speed: Final = CalculatedDataPointField(
|
|
42
|
+
parameter=Parameter.WIND_SPEED,
|
|
43
|
+
paramset_key=ParamsetKey.VALUES,
|
|
44
|
+
dpt=DpSensor,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Field with fallback parameters
|
|
48
|
+
_dp_temperature: Final = CalculatedDataPointField(
|
|
49
|
+
parameter=Parameter.TEMPERATURE,
|
|
50
|
+
paramset_key=ParamsetKey.VALUES,
|
|
51
|
+
dpt=DpSensor,
|
|
52
|
+
fallback_parameters=[Parameter.ACTUAL_TEMPERATURE],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Field with device fallback (tries device address if not on channel)
|
|
56
|
+
_dp_low_bat_limit: Final = CalculatedDataPointField(
|
|
57
|
+
parameter=Parameter.LOW_BAT_LIMIT,
|
|
58
|
+
paramset_key=ParamsetKey.MASTER,
|
|
59
|
+
dpt=DpFloat,
|
|
60
|
+
use_device_fallback=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
The descriptor:
|
|
64
|
+
- Resolves the data point from _data_points dict on each access (O(1) lookup)
|
|
65
|
+
- Tries fallback_parameters in order if primary parameter doesn't exist
|
|
66
|
+
- Tries device address if use_device_fallback=True and not found on channel
|
|
67
|
+
- Returns a DpDummy fallback if no data point exists
|
|
68
|
+
- Subscribes to data point updates automatically
|
|
69
|
+
- Provides correct type information to mypy
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
__slots__ = ("_parameter", "_paramset_key", "_data_point_type", "_fallback_parameters", "_use_device_fallback")
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
parameter: str,
|
|
78
|
+
paramset_key: ParamsetKey | None,
|
|
79
|
+
dpt: type[DataPointT],
|
|
80
|
+
fallback_parameters: list[str] | None = None,
|
|
81
|
+
use_device_fallback: bool = False,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Initialize the calculated data point field descriptor.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
parameter: The parameter name identifying this data point
|
|
88
|
+
paramset_key: The paramset key (VALUES, MASTER, etc.)
|
|
89
|
+
dpt: The expected data point type (e.g., DpSensor, DpFloat)
|
|
90
|
+
fallback_parameters: Optional list of fallback parameter names to try if primary not found
|
|
91
|
+
use_device_fallback: If True, try device address (channel 0) if not found on current channel
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
self._parameter: Final = parameter
|
|
95
|
+
self._paramset_key: Final = paramset_key
|
|
96
|
+
self._data_point_type: Final = dpt
|
|
97
|
+
self._fallback_parameters: Final = fallback_parameters or []
|
|
98
|
+
self._use_device_fallback: Final = use_device_fallback
|
|
99
|
+
|
|
100
|
+
@overload
|
|
101
|
+
def __get__(self, instance: None, owner: type) -> Self: ... # kwonly: disable
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def __get__(self, instance: CalculatedDataPoint[Any], owner: type) -> DataPointT: ... # kwonly: disable
|
|
105
|
+
|
|
106
|
+
def __get__(self, instance: CalculatedDataPoint[Any] | None, owner: type) -> Self | DataPointT: # kwonly: disable
|
|
107
|
+
"""
|
|
108
|
+
Get the data point for this field.
|
|
109
|
+
|
|
110
|
+
On class-level access (instance=None), returns the descriptor itself.
|
|
111
|
+
On instance access, looks up the data point from _data_points dict.
|
|
112
|
+
"""
|
|
113
|
+
if instance is None:
|
|
114
|
+
return self # Class-level access returns descriptor
|
|
115
|
+
|
|
116
|
+
key: _DataPointKey = (self._parameter, self._paramset_key)
|
|
117
|
+
|
|
118
|
+
# Resolve from _data_points dict (O(1) lookup)
|
|
119
|
+
if found_dp := instance._data_points.get(key):
|
|
120
|
+
return cast(DataPointT, found_dp)
|
|
121
|
+
|
|
122
|
+
# Try primary parameter first, then fallbacks on current channel
|
|
123
|
+
for param in (self._parameter, *self._fallback_parameters):
|
|
124
|
+
if instance._channel.get_generic_data_point(parameter=param, paramset_key=self._paramset_key):
|
|
125
|
+
dp = instance._resolve_data_point(parameter=param, paramset_key=self._paramset_key)
|
|
126
|
+
instance._data_points[key] = dp
|
|
127
|
+
return cast(DataPointT, dp)
|
|
128
|
+
|
|
129
|
+
# Try device address (channel 0) if enabled
|
|
130
|
+
if self._use_device_fallback:
|
|
131
|
+
dp = instance._add_device_data_point(
|
|
132
|
+
channel_address=instance._channel.device.address,
|
|
133
|
+
parameter=self._parameter,
|
|
134
|
+
paramset_key=self._paramset_key,
|
|
135
|
+
dpt=self._data_point_type,
|
|
136
|
+
)
|
|
137
|
+
instance._data_points[key] = dp
|
|
138
|
+
return cast(DataPointT, dp)
|
|
139
|
+
|
|
140
|
+
# No data point found - resolve DpDummy for primary parameter
|
|
141
|
+
dp = instance._resolve_data_point(parameter=self._parameter, paramset_key=self._paramset_key)
|
|
142
|
+
instance._data_points[key] = dp
|
|
143
|
+
return cast(DataPointT, dp)
|
|
144
|
+
|
|
145
|
+
data_point_type: Final = DelegatedProperty[type[DataPointT]](path="_data_point_type")
|
|
146
|
+
parameter: Final = DelegatedProperty[str](path="_parameter")
|
|
147
|
+
paramset_key: Final = DelegatedProperty[ParamsetKey | None](path="_paramset_key")
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
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 CalculatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
|
|
14
|
+
from aiohomematic.interfaces import ChannelProtocol
|
|
15
|
+
from aiohomematic.model.calculated import CalculatedDataPoint
|
|
16
|
+
from aiohomematic.model.calculated.field import CalculatedDataPointField
|
|
17
|
+
from aiohomematic.model.calculated.support import calculate_operating_voltage_level
|
|
18
|
+
from aiohomematic.model.generic import DpFloat, DpSensor
|
|
19
|
+
from aiohomematic.property_decorators import state_property
|
|
20
|
+
from aiohomematic.support import element_matches_key, extract_exc_args
|
|
21
|
+
|
|
22
|
+
_BATTERY_QTY: Final = "Battery Qty"
|
|
23
|
+
_BATTERY_TYPE: Final = "Battery Type"
|
|
24
|
+
_LOW_BAT_LIMIT: Final = "Low Battery Limit"
|
|
25
|
+
_LOW_BAT_LIMIT_DEFAULT: Final = "Low Battery Limit Default"
|
|
26
|
+
_VOLTAGE_MAX: Final = "Voltage max"
|
|
27
|
+
|
|
28
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OperatingVoltageLevel[SensorT: float | None](CalculatedDataPoint[SensorT]):
|
|
32
|
+
"""Implementation of a calculated sensor for operating voltage level."""
|
|
33
|
+
|
|
34
|
+
__slots__ = (
|
|
35
|
+
"_battery_data",
|
|
36
|
+
"_low_bat_limit_default",
|
|
37
|
+
"_voltage_max",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_calculated_parameter = CalculatedParameter.OPERATING_VOLTAGE_LEVEL
|
|
41
|
+
_category = DataPointCategory.SENSOR
|
|
42
|
+
|
|
43
|
+
_dp_low_bat_limit: Final = CalculatedDataPointField(
|
|
44
|
+
parameter=Parameter.LOW_BAT_LIMIT,
|
|
45
|
+
paramset_key=ParamsetKey.MASTER,
|
|
46
|
+
dpt=DpFloat,
|
|
47
|
+
use_device_fallback=True,
|
|
48
|
+
)
|
|
49
|
+
_dp_operating_voltage: Final = CalculatedDataPointField(
|
|
50
|
+
parameter=Parameter.OPERATING_VOLTAGE,
|
|
51
|
+
paramset_key=ParamsetKey.VALUES,
|
|
52
|
+
dpt=DpSensor,
|
|
53
|
+
fallback_parameters=[Parameter.BATTERY_STATE],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def __init__(self, *, channel: ChannelProtocol) -> None:
|
|
57
|
+
"""Initialize the data point."""
|
|
58
|
+
super().__init__(channel=channel)
|
|
59
|
+
self._type = ParameterType.FLOAT
|
|
60
|
+
self._unit = "%"
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
|
|
64
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
65
|
+
if element_matches_key(
|
|
66
|
+
search_elements=_IGNORE_OPERATING_VOLTAGE_LEVEL_MODELS, compare_with=channel.device.model
|
|
67
|
+
):
|
|
68
|
+
return False
|
|
69
|
+
return element_matches_key(
|
|
70
|
+
search_elements=_OPERATING_VOLTAGE_LEVEL_MODELS.keys(), compare_with=channel.device.model
|
|
71
|
+
) and (
|
|
72
|
+
(
|
|
73
|
+
channel.get_generic_data_point(
|
|
74
|
+
parameter=Parameter.OPERATING_VOLTAGE,
|
|
75
|
+
paramset_key=ParamsetKey.VALUES,
|
|
76
|
+
)
|
|
77
|
+
and channel.get_generic_data_point(parameter=Parameter.LOW_BAT_LIMIT, paramset_key=ParamsetKey.MASTER)
|
|
78
|
+
)
|
|
79
|
+
is not None
|
|
80
|
+
or (
|
|
81
|
+
channel.get_generic_data_point(
|
|
82
|
+
parameter=Parameter.BATTERY_STATE,
|
|
83
|
+
paramset_key=ParamsetKey.VALUES,
|
|
84
|
+
)
|
|
85
|
+
and channel.device.get_generic_data_point(
|
|
86
|
+
channel_address=channel.device.address,
|
|
87
|
+
parameter=Parameter.LOW_BAT_LIMIT,
|
|
88
|
+
paramset_key=ParamsetKey.MASTER,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
is not None
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _low_bat_limit(self) -> float | None:
|
|
96
|
+
"""Return the min value."""
|
|
97
|
+
return (
|
|
98
|
+
float(self._dp_low_bat_limit.value)
|
|
99
|
+
if self._dp_low_bat_limit is not None and self._dp_low_bat_limit.value is not None
|
|
100
|
+
else None
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@state_property
|
|
104
|
+
def additional_information(self) -> dict[str, Any]:
|
|
105
|
+
"""Return additional information about the data point."""
|
|
106
|
+
ainfo = super().additional_information
|
|
107
|
+
if self._battery_data is not None:
|
|
108
|
+
ainfo.update(
|
|
109
|
+
{
|
|
110
|
+
_BATTERY_QTY: self._battery_data.quantity,
|
|
111
|
+
_BATTERY_TYPE: self._battery_data.battery,
|
|
112
|
+
_LOW_BAT_LIMIT: f"{self._low_bat_limit}V",
|
|
113
|
+
_LOW_BAT_LIMIT_DEFAULT: f"{self._low_bat_limit_default}V",
|
|
114
|
+
_VOLTAGE_MAX: f"{self._voltage_max}V",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
return ainfo
|
|
118
|
+
|
|
119
|
+
@state_property
|
|
120
|
+
def value(self) -> float | None:
|
|
121
|
+
"""Return the value."""
|
|
122
|
+
try:
|
|
123
|
+
return calculate_operating_voltage_level(
|
|
124
|
+
operating_voltage=self._dp_operating_voltage.value,
|
|
125
|
+
low_bat_limit=self._low_bat_limit_default,
|
|
126
|
+
voltage_max=self._voltage_max,
|
|
127
|
+
)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
_LOGGER.debug(
|
|
130
|
+
"OperatingVoltageLevel: Failed to calculate sensor for %s: %s",
|
|
131
|
+
self._channel.name,
|
|
132
|
+
extract_exc_args(exc=exc),
|
|
133
|
+
)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def _post_init(self) -> None:
|
|
137
|
+
"""Post action after initialisation of the data point fields."""
|
|
138
|
+
super()._post_init()
|
|
139
|
+
|
|
140
|
+
self._battery_data = _get_battery_data(model=self._channel.device.model)
|
|
141
|
+
self._low_bat_limit_default = (
|
|
142
|
+
float(self._dp_low_bat_limit.default)
|
|
143
|
+
if isinstance(self._dp_low_bat_limit, DpFloat) and self._dp_low_bat_limit.default is not None
|
|
144
|
+
else None
|
|
145
|
+
)
|
|
146
|
+
self._voltage_max = (
|
|
147
|
+
float(_BatteryVoltage[self._battery_data.battery] * self._battery_data.quantity)
|
|
148
|
+
if self._battery_data is not None
|
|
149
|
+
else None
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _BatteryType(StrEnum):
|
|
154
|
+
CR2032 = "CR2032"
|
|
155
|
+
LR44 = "LR44"
|
|
156
|
+
R03 = "AAA"
|
|
157
|
+
R14 = "BABY"
|
|
158
|
+
R6 = "AA"
|
|
159
|
+
UNKNOWN = "UNKNOWN"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
_BatteryVoltage: Final[Mapping[_BatteryType, float]] = {
|
|
163
|
+
_BatteryType.CR2032: 3.0,
|
|
164
|
+
_BatteryType.LR44: 1.5,
|
|
165
|
+
_BatteryType.R03: 1.5,
|
|
166
|
+
_BatteryType.R14: 1.5,
|
|
167
|
+
_BatteryType.R6: 1.5,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
172
|
+
class _BatteryData:
|
|
173
|
+
model: str
|
|
174
|
+
battery: _BatteryType
|
|
175
|
+
quantity: int = 1
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# This list is sorted. models with shorted model types are sorted to
|
|
179
|
+
_BATTERY_DATA: Final = (
|
|
180
|
+
# HM long model str
|
|
181
|
+
_BatteryData(model="HM-CC-RT-DN", battery=_BatteryType.R6, quantity=2),
|
|
182
|
+
_BatteryData(model="HM-Dis-EP-WM55", battery=_BatteryType.R03, quantity=2),
|
|
183
|
+
_BatteryData(model="HM-ES-TX-WM", battery=_BatteryType.R6, quantity=4),
|
|
184
|
+
_BatteryData(model="HM-OU-CFM-TW", battery=_BatteryType.R14, quantity=2),
|
|
185
|
+
_BatteryData(model="HM-PB-2-FM", battery=_BatteryType.R03, quantity=2),
|
|
186
|
+
_BatteryData(model="HM-PB-2-WM55", battery=_BatteryType.R03, quantity=2),
|
|
187
|
+
_BatteryData(model="HM-PB-6-WM55", battery=_BatteryType.R03, quantity=2),
|
|
188
|
+
_BatteryData(model="HM-PBI-4-FM", battery=_BatteryType.CR2032),
|
|
189
|
+
_BatteryData(model="HM-RC-4-2", battery=_BatteryType.R03),
|
|
190
|
+
_BatteryData(model="HM-RC-8", battery=_BatteryType.R03, quantity=2),
|
|
191
|
+
_BatteryData(model="HM-RC-Key4-3", battery=_BatteryType.R03),
|
|
192
|
+
_BatteryData(model="HM-SCI-3-FM", battery=_BatteryType.CR2032),
|
|
193
|
+
_BatteryData(model="HM-Sec-Key", battery=_BatteryType.R6, quantity=3),
|
|
194
|
+
_BatteryData(model="HM-Sec-MDIR-2", battery=_BatteryType.R6, quantity=3),
|
|
195
|
+
_BatteryData(model="HM-Sec-RHS", battery=_BatteryType.LR44, quantity=2),
|
|
196
|
+
_BatteryData(model="HM-Sec-SC-2", battery=_BatteryType.LR44, quantity=2),
|
|
197
|
+
_BatteryData(model="HM-Sec-SCo", battery=_BatteryType.R03),
|
|
198
|
+
_BatteryData(model="HM-Sec-SD-2", battery=_BatteryType.UNKNOWN),
|
|
199
|
+
_BatteryData(model="HM-Sec-Sir-WM", battery=_BatteryType.R14, quantity=2),
|
|
200
|
+
_BatteryData(model="HM-Sec-TiS", battery=_BatteryType.CR2032),
|
|
201
|
+
_BatteryData(model="HM-Sec-Win", battery=_BatteryType.UNKNOWN),
|
|
202
|
+
_BatteryData(model="HM-Sen-MDIR-O-2", battery=_BatteryType.R6, quantity=3),
|
|
203
|
+
_BatteryData(model="HM-Sen-MDIR-SM", battery=_BatteryType.R6, quantity=3),
|
|
204
|
+
_BatteryData(model="HM-Sen-MDIR-WM55", battery=_BatteryType.R03, quantity=2),
|
|
205
|
+
_BatteryData(model="HM-SwI-3-FM", battery=_BatteryType.CR2032),
|
|
206
|
+
_BatteryData(model="HM-TC-IT-WM-W-EU", battery=_BatteryType.R03, quantity=2),
|
|
207
|
+
_BatteryData(model="HM-WDS10-TH-O", battery=_BatteryType.R6, quantity=2),
|
|
208
|
+
_BatteryData(model="HM-WDS30-OT2-SM", battery=_BatteryType.R6, quantity=2),
|
|
209
|
+
_BatteryData(model="HM-WDS30-T-O", battery=_BatteryType.R03, quantity=2),
|
|
210
|
+
_BatteryData(model="HM-WDS40-TH-I", battery=_BatteryType.R6, quantity=2),
|
|
211
|
+
# HM short model str
|
|
212
|
+
_BatteryData(model="HM-Sec-SD", battery=_BatteryType.R6, quantity=3),
|
|
213
|
+
# HmIP model > 4
|
|
214
|
+
_BatteryData(model="HmIP-ASIR-O", battery=_BatteryType.UNKNOWN),
|
|
215
|
+
_BatteryData(model="HmIP-DSD-PCB", battery=_BatteryType.R03, quantity=2),
|
|
216
|
+
_BatteryData(model="HmIP-PCBS-BAT", battery=_BatteryType.UNKNOWN),
|
|
217
|
+
_BatteryData(model="HmIP-SMI55", battery=_BatteryType.R03, quantity=2),
|
|
218
|
+
_BatteryData(model="HmIP-SMO230", battery=_BatteryType.UNKNOWN),
|
|
219
|
+
_BatteryData(model="HmIP-STE2-PCB", battery=_BatteryType.R6, quantity=2),
|
|
220
|
+
_BatteryData(model="HmIP-SWDO-I", battery=_BatteryType.R03, quantity=2),
|
|
221
|
+
_BatteryData(model="HmIP-SWDO-PL", battery=_BatteryType.R03, quantity=2),
|
|
222
|
+
_BatteryData(model="HmIP-WTH-B-2", battery=_BatteryType.R6, quantity=2),
|
|
223
|
+
_BatteryData(model="HmIP-eTRV-CL", battery=_BatteryType.R6, quantity=4),
|
|
224
|
+
# HmIP model 4
|
|
225
|
+
_BatteryData(model="ELV-SH-SW1-BAT", battery=_BatteryType.R6, quantity=2),
|
|
226
|
+
_BatteryData(model="ELV-SH-TACO", battery=_BatteryType.R03, quantity=1),
|
|
227
|
+
_BatteryData(model="HmIP-ASIR", battery=_BatteryType.R6, quantity=3),
|
|
228
|
+
_BatteryData(model="HmIP-FCI1", battery=_BatteryType.CR2032),
|
|
229
|
+
_BatteryData(model="HmIP-FCI6", battery=_BatteryType.R03),
|
|
230
|
+
_BatteryData(model="HmIP-MP3P", battery=_BatteryType.R14, quantity=2),
|
|
231
|
+
_BatteryData(model="HmIP-RCB1", battery=_BatteryType.R03, quantity=2),
|
|
232
|
+
_BatteryData(model="HmIP-SPDR", battery=_BatteryType.R6, quantity=2),
|
|
233
|
+
_BatteryData(model="HmIP-STHD", battery=_BatteryType.R03, quantity=2),
|
|
234
|
+
_BatteryData(model="HmIP-STHO", battery=_BatteryType.R6, quantity=2),
|
|
235
|
+
_BatteryData(model="HmIP-SWDM", battery=_BatteryType.R03, quantity=2),
|
|
236
|
+
_BatteryData(model="HmIP-SWDO", battery=_BatteryType.R03),
|
|
237
|
+
_BatteryData(model="HmIP-SWSD", battery=_BatteryType.UNKNOWN),
|
|
238
|
+
_BatteryData(model="HmIP-eTRV", battery=_BatteryType.R6, quantity=2),
|
|
239
|
+
# HmIP model 3
|
|
240
|
+
_BatteryData(model="ELV-SH-CTH", battery=_BatteryType.CR2032),
|
|
241
|
+
_BatteryData(model="ELV-SH-WSM", battery=_BatteryType.R6, quantity=2),
|
|
242
|
+
_BatteryData(model="HmIP-DBB", battery=_BatteryType.R03),
|
|
243
|
+
_BatteryData(model="HmIP-DLD", battery=_BatteryType.R6, quantity=3),
|
|
244
|
+
_BatteryData(model="HmIP-DLS", battery=_BatteryType.CR2032),
|
|
245
|
+
_BatteryData(model="HmIP-ESI", battery=_BatteryType.R6, quantity=2),
|
|
246
|
+
_BatteryData(model="HmIP-KRC", battery=_BatteryType.R03),
|
|
247
|
+
_BatteryData(model="HmIP-RC8", battery=_BatteryType.R03, quantity=2),
|
|
248
|
+
_BatteryData(model="HmIP-SAM", battery=_BatteryType.R6, quantity=2),
|
|
249
|
+
_BatteryData(model="HmIP-SCI", battery=_BatteryType.R03, quantity=2),
|
|
250
|
+
_BatteryData(model="HmIP-SLO", battery=_BatteryType.R6, quantity=2),
|
|
251
|
+
_BatteryData(model="HmIP-SMI", battery=_BatteryType.R6, quantity=2),
|
|
252
|
+
_BatteryData(model="HmIP-SMO", battery=_BatteryType.R6, quantity=2),
|
|
253
|
+
_BatteryData(model="HmIP-SPI", battery=_BatteryType.R6, quantity=2),
|
|
254
|
+
_BatteryData(model="HmIP-SRH", battery=_BatteryType.R03),
|
|
255
|
+
_BatteryData(model="HmIP-STH", battery=_BatteryType.R03, quantity=2),
|
|
256
|
+
_BatteryData(model="HmIP-STV", battery=_BatteryType.R03, quantity=2),
|
|
257
|
+
_BatteryData(model="HmIP-SWD", battery=_BatteryType.R03, quantity=2),
|
|
258
|
+
_BatteryData(model="HmIP-SWO", battery=_BatteryType.R6, quantity=3),
|
|
259
|
+
_BatteryData(model="HmIP-WGC", battery=_BatteryType.R6, quantity=2),
|
|
260
|
+
_BatteryData(model="HmIP-WKP", battery=_BatteryType.R03, quantity=2),
|
|
261
|
+
_BatteryData(model="HmIP-WRC", battery=_BatteryType.R03, quantity=2),
|
|
262
|
+
_BatteryData(model="HmIP-WSM", battery=_BatteryType.R6, quantity=2),
|
|
263
|
+
_BatteryData(model="HmIP-WTH", battery=_BatteryType.R03, quantity=2),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
_OPERATING_VOLTAGE_LEVEL_MODELS: Final[Mapping[str, _BatteryData]] = {
|
|
267
|
+
battery.model: battery for battery in _BATTERY_DATA if battery.battery != _BatteryType.UNKNOWN
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_IGNORE_OPERATING_VOLTAGE_LEVEL_MODELS: Final[tuple[str, ...]] = tuple(
|
|
271
|
+
[battery.model for battery in _BATTERY_DATA if battery.battery == _BatteryType.UNKNOWN]
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _get_battery_data(*, model: str) -> _BatteryData | None:
|
|
276
|
+
"""Return the battery data by model."""
|
|
277
|
+
model_l = model.lower()
|
|
278
|
+
for battery_data in _OPERATING_VOLTAGE_LEVEL_MODELS.values():
|
|
279
|
+
if battery_data.model.lower() == model_l:
|
|
280
|
+
return battery_data
|
|
281
|
+
|
|
282
|
+
for battery_data in _OPERATING_VOLTAGE_LEVEL_MODELS.values():
|
|
283
|
+
if model_l.startswith(battery_data.model.lower()):
|
|
284
|
+
return battery_data
|
|
285
|
+
|
|
286
|
+
return None
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
A number of functions used to calculate values based on existing data.
|
|
5
|
+
|
|
6
|
+
Climate related formula are based on:
|
|
7
|
+
- thermal comfort (https://github.com/dolezsa/thermal_comfort) ground works.
|
|
8
|
+
- https://gist.github.com/E3V3A/8f9f0aa18380d4ab2546cd50b725a219
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import math
|
|
15
|
+
from typing import Final
|
|
16
|
+
|
|
17
|
+
from aiohomematic.support import extract_exc_args
|
|
18
|
+
|
|
19
|
+
_DEFAULT_PRESSURE_HPA: Final = 1013.25
|
|
20
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def calculate_dew_point_spread(*, temperature: float, humidity: int) -> float | None:
|
|
24
|
+
"""
|
|
25
|
+
Calculate the dew point spread.
|
|
26
|
+
|
|
27
|
+
Dew point spread = Difference between current air temperature and dew point.
|
|
28
|
+
Specifies the safety margin against condensation(K).
|
|
29
|
+
"""
|
|
30
|
+
if dew_point := calculate_dew_point(temperature=temperature, humidity=humidity):
|
|
31
|
+
return round(temperature - dew_point, 2)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def calculate_enthalpy(
|
|
36
|
+
*, temperature: float, humidity: int, pressure_hPa: float = _DEFAULT_PRESSURE_HPA
|
|
37
|
+
) -> float | None:
|
|
38
|
+
"""
|
|
39
|
+
Calculate the enthalpy based on temperature and humidity.
|
|
40
|
+
|
|
41
|
+
Calculates the specific enthalpy of humid air in kJ/kg (relative to dry air).
|
|
42
|
+
temperature: Air temperature in °C
|
|
43
|
+
humidity: Relative humidity in %
|
|
44
|
+
pressure_hPa: Air pressure (default: 1013.25 hPa)
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
# Saturation vapor pressure according to Magnus in hPa
|
|
48
|
+
e_s = 6.112 * math.exp((17.62 * temperature) / (243.12 + temperature))
|
|
49
|
+
e = humidity / 100.0 * e_s # aktueller Dampfdruck in hPa
|
|
50
|
+
|
|
51
|
+
# Mixing ratio (g water / kg dry air)
|
|
52
|
+
r = 622 * e / (pressure_hPa - e)
|
|
53
|
+
|
|
54
|
+
# Specific enthalpy (kJ/kg dry air)
|
|
55
|
+
h = 1.006 * temperature + r * (2501 + 1.86 * temperature) / 1000 # in kJ/kg
|
|
56
|
+
return round(h, 2)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _calculate_heat_index(*, temperature: float, humidity: int) -> float:
|
|
60
|
+
"""
|
|
61
|
+
Calculate the Heat Index (feels like temperature) based on the NOAA equation.
|
|
62
|
+
|
|
63
|
+
References:
|
|
64
|
+
[1] https://en.wikipedia.org/wiki/Heat_index
|
|
65
|
+
[2] http://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
|
|
66
|
+
[3] https://github.com/geanders/weathermetrics/blob/master/R/heat_index.R
|
|
67
|
+
[4] https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3801457/
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
# SI units (Celsius)
|
|
71
|
+
c1 = -8.78469475556
|
|
72
|
+
c2 = 1.61139411
|
|
73
|
+
c3 = 2.33854883889
|
|
74
|
+
c4 = -0.14611605
|
|
75
|
+
c5 = -0.012308094
|
|
76
|
+
c6 = -0.0164248277778
|
|
77
|
+
c7 = 0.002211732
|
|
78
|
+
c8 = 0.00072546
|
|
79
|
+
c9 = -0.000003582
|
|
80
|
+
|
|
81
|
+
temperature_fahrenheit = (temperature * 9 / 5) + 32
|
|
82
|
+
heat_index_fahrenheit = 0.5 * (
|
|
83
|
+
temperature_fahrenheit + 61.0 + (temperature_fahrenheit - 68.0) * 1.2 + humidity * 0.094
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if ((heat_index_fahrenheit + temperature_fahrenheit) / 2) >= 80: # [°F]
|
|
87
|
+
# temperature > 27C and humidity > 40 %
|
|
88
|
+
heat_index_celsius = math.fsum(
|
|
89
|
+
[
|
|
90
|
+
c1,
|
|
91
|
+
c2 * temperature,
|
|
92
|
+
c3 * humidity,
|
|
93
|
+
c4 * temperature * humidity,
|
|
94
|
+
c5 * temperature**2,
|
|
95
|
+
c6 * humidity**2,
|
|
96
|
+
c7 * temperature**2 * humidity,
|
|
97
|
+
c8 * temperature * humidity**2,
|
|
98
|
+
c9 * temperature**2 * humidity**2,
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
heat_index_celsius = (heat_index_fahrenheit - 32) * 5 / 9
|
|
103
|
+
|
|
104
|
+
return heat_index_celsius
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _calculate_wind_chill(*, temperature: float, wind_speed: float) -> float | None:
|
|
108
|
+
"""
|
|
109
|
+
Calculate the Wind Chill (feels like temperature) based on NOAA.
|
|
110
|
+
|
|
111
|
+
References:
|
|
112
|
+
[1] https://en.wikipedia.org/wiki/Wind_chill
|
|
113
|
+
[2] https://www.wpc.ncep.noaa.gov/html/windchill.shtml
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
# Wind Chill Temperature is only defined for temperatures at or below 10°C and wind speeds above 4.8 Km/h.
|
|
117
|
+
if temperature > 10 or wind_speed <= 4.8: # if temperature > 50 or wind_speed <= 3: # (°F, Mph)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
return float(13.12 + (0.6215 * temperature) - 11.37 * wind_speed**0.16 + 0.3965 * temperature * wind_speed**0.16)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def calculate_vapor_concentration(*, temperature: float, humidity: int) -> float | None:
|
|
124
|
+
"""Calculate the vapor concentration."""
|
|
125
|
+
try:
|
|
126
|
+
abs_temperature = temperature + 273.15
|
|
127
|
+
vapor_concentration = 6.112
|
|
128
|
+
vapor_concentration *= math.exp((17.67 * temperature) / (243.5 + temperature))
|
|
129
|
+
vapor_concentration *= humidity
|
|
130
|
+
vapor_concentration *= 2.1674
|
|
131
|
+
vapor_concentration /= abs_temperature
|
|
132
|
+
|
|
133
|
+
return round(vapor_concentration, 2)
|
|
134
|
+
except ValueError as verr:
|
|
135
|
+
_LOGGER.debug(
|
|
136
|
+
"Unable to calculate 'vapor concentration' with temperature: %s, humidity: %s (%s)",
|
|
137
|
+
temperature,
|
|
138
|
+
humidity,
|
|
139
|
+
extract_exc_args(exc=verr),
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def calculate_apparent_temperature(*, temperature: float, humidity: int, wind_speed: float) -> float | None:
|
|
145
|
+
"""Calculate the apparent temperature based on NOAA."""
|
|
146
|
+
try:
|
|
147
|
+
if temperature <= 10 and wind_speed > 4.8:
|
|
148
|
+
# Wind Chill for low temp cases (and wind)
|
|
149
|
+
apparent_temperature = _calculate_wind_chill(temperature=temperature, wind_speed=wind_speed)
|
|
150
|
+
elif temperature >= 26.7:
|
|
151
|
+
# Heat Index for High temp cases
|
|
152
|
+
apparent_temperature = _calculate_heat_index(temperature=temperature, humidity=humidity)
|
|
153
|
+
else:
|
|
154
|
+
apparent_temperature = temperature
|
|
155
|
+
|
|
156
|
+
return round(apparent_temperature, 1) # type: ignore[arg-type]
|
|
157
|
+
except ValueError as verr:
|
|
158
|
+
if temperature == 0.0 and humidity == 0:
|
|
159
|
+
return 0.0
|
|
160
|
+
_LOGGER.debug(
|
|
161
|
+
"Unable to calculate 'apparent temperature' with temperature: %s, humidity: %s (%s)",
|
|
162
|
+
temperature,
|
|
163
|
+
humidity,
|
|
164
|
+
extract_exc_args(exc=verr),
|
|
165
|
+
)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def calculate_dew_point(*, temperature: float, humidity: int) -> float | None:
|
|
170
|
+
"""Calculate the dew point."""
|
|
171
|
+
try:
|
|
172
|
+
a0 = 373.15 / (273.15 + temperature)
|
|
173
|
+
s = -7.90298 * (a0 - 1)
|
|
174
|
+
s += 5.02808 * math.log(a0, 10)
|
|
175
|
+
s += -1.3816e-7 * (pow(10, (11.344 * (1 - 1 / a0))) - 1)
|
|
176
|
+
s += 8.1328e-3 * (pow(10, (-3.49149 * (a0 - 1))) - 1)
|
|
177
|
+
s += math.log(1013.246, 10)
|
|
178
|
+
vp = pow(10, s - 3) * humidity
|
|
179
|
+
td = math.log(vp / 0.61078)
|
|
180
|
+
|
|
181
|
+
return round((241.88 * td) / (17.558 - td), 1)
|
|
182
|
+
except ValueError as verr:
|
|
183
|
+
if temperature == 0.0 and humidity == 0:
|
|
184
|
+
return 0.0
|
|
185
|
+
_LOGGER.debug(
|
|
186
|
+
"Unable to calculate 'dew point' with temperature: %s, humidity: %s (%s)",
|
|
187
|
+
temperature,
|
|
188
|
+
humidity,
|
|
189
|
+
extract_exc_args(exc=verr),
|
|
190
|
+
)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def calculate_frost_point(*, temperature: float, humidity: int) -> float | None:
|
|
195
|
+
"""Calculate the frost point."""
|
|
196
|
+
try:
|
|
197
|
+
if (dew_point := calculate_dew_point(temperature=temperature, humidity=humidity)) is None:
|
|
198
|
+
return None
|
|
199
|
+
t = temperature + 273.15
|
|
200
|
+
td = dew_point + 273.15
|
|
201
|
+
|
|
202
|
+
return round((td + (2671.02 / ((2954.61 / t) + 2.193665 * math.log(t) - 13.3448)) - t) - 273.15, 1)
|
|
203
|
+
except ValueError as verr:
|
|
204
|
+
if temperature == 0.0 and humidity == 0:
|
|
205
|
+
return 0.0
|
|
206
|
+
_LOGGER.debug(
|
|
207
|
+
"Unable to calculate 'frost point' with temperature: %s, humidity: %s (%s)",
|
|
208
|
+
temperature,
|
|
209
|
+
humidity,
|
|
210
|
+
extract_exc_args(exc=verr),
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def calculate_operating_voltage_level(
|
|
216
|
+
*, operating_voltage: float | None, low_bat_limit: float | None, voltage_max: float | None
|
|
217
|
+
) -> float | None:
|
|
218
|
+
"""Return the operating voltage level."""
|
|
219
|
+
if operating_voltage is None or low_bat_limit is None or voltage_max is None:
|
|
220
|
+
return None
|
|
221
|
+
return max(
|
|
222
|
+
0,
|
|
223
|
+
min(
|
|
224
|
+
100,
|
|
225
|
+
float(
|
|
226
|
+
round(
|
|
227
|
+
((float(operating_voltage) - low_bat_limit) / (voltage_max - low_bat_limit) * 100),
|
|
228
|
+
1,
|
|
229
|
+
)
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
)
|