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
aiohomematic/hmcli.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
#!/usr/bin/python3
|
|
4
|
+
"""
|
|
5
|
+
Commandline tool to query Homematic hubs via XML-RPC.
|
|
6
|
+
|
|
7
|
+
Public API of this module is defined by __all__.
|
|
8
|
+
|
|
9
|
+
This module provides a command-line interface; as a library surface it only
|
|
10
|
+
exposes the 'main' entrypoint for invocation. All other names are internal.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Any
|
|
19
|
+
from xmlrpc.client import ServerProxy
|
|
20
|
+
|
|
21
|
+
from aiohomematic import __version__
|
|
22
|
+
from aiohomematic.const import ParamsetKey
|
|
23
|
+
from aiohomematic.support import build_xml_rpc_headers, build_xml_rpc_uri, get_tls_context
|
|
24
|
+
|
|
25
|
+
# Define public API for this module (CLI only)
|
|
26
|
+
__all__ = ["main"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
"""Start the cli."""
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
description="Commandline tool to query Homematic hubs via XML-RPC",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--host",
|
|
37
|
+
"-H",
|
|
38
|
+
required=True,
|
|
39
|
+
type=str,
|
|
40
|
+
help="Hostname / IP address to connect to",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--port",
|
|
44
|
+
"-p",
|
|
45
|
+
required=True,
|
|
46
|
+
type=int,
|
|
47
|
+
help="Port to connect to",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--path",
|
|
51
|
+
type=str,
|
|
52
|
+
help="Path, used for heating groups",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--username",
|
|
56
|
+
"-U",
|
|
57
|
+
nargs="?",
|
|
58
|
+
help="Username required for access",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--password",
|
|
62
|
+
"-P",
|
|
63
|
+
nargs="?",
|
|
64
|
+
help="Password required for access",
|
|
65
|
+
)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--tls",
|
|
68
|
+
"-t",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="Enable TLS encryption",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--verify",
|
|
74
|
+
"-v",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Verify TLS encryption",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--json",
|
|
80
|
+
"-j",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Output as JSON",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--address",
|
|
86
|
+
"-a",
|
|
87
|
+
required=True,
|
|
88
|
+
type=str,
|
|
89
|
+
help="Address of Homematic device, including channel",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--paramset_key",
|
|
93
|
+
default=ParamsetKey.VALUES,
|
|
94
|
+
choices=[ParamsetKey.VALUES, ParamsetKey.MASTER],
|
|
95
|
+
help="Paramset of Homematic device. Default: VALUES",
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--parameter",
|
|
99
|
+
required=True,
|
|
100
|
+
help="Parameter of Homematic device",
|
|
101
|
+
)
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--value",
|
|
104
|
+
type=str,
|
|
105
|
+
help="Value to set for parameter. Use 0/1 for boolean",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--type",
|
|
109
|
+
choices=["int", "float", "bool"],
|
|
110
|
+
help="Type of value when setting a value. Using str if not provided",
|
|
111
|
+
)
|
|
112
|
+
args = parser.parse_args()
|
|
113
|
+
|
|
114
|
+
url = build_xml_rpc_uri(
|
|
115
|
+
host=args.host,
|
|
116
|
+
port=args.port,
|
|
117
|
+
path=args.path,
|
|
118
|
+
tls=args.tls,
|
|
119
|
+
)
|
|
120
|
+
headers = build_xml_rpc_headers(username=args.username, password=args.password)
|
|
121
|
+
context = None
|
|
122
|
+
if args.tls:
|
|
123
|
+
context = get_tls_context(verify_tls=args.verify)
|
|
124
|
+
proxy = ServerProxy(url, context=context, headers=headers)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
if args.paramset_key == ParamsetKey.VALUES and args.value is None:
|
|
128
|
+
result = proxy.getValue(args.address, args.parameter)
|
|
129
|
+
if args.json:
|
|
130
|
+
print(
|
|
131
|
+
json.dumps(
|
|
132
|
+
{"address": args.address, "parameter": args.parameter, "value": result}, ensure_ascii=False
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
print(result)
|
|
137
|
+
sys.exit(0)
|
|
138
|
+
elif args.paramset_key == ParamsetKey.VALUES and args.value:
|
|
139
|
+
value: Any
|
|
140
|
+
if args.type == "int":
|
|
141
|
+
value = int(args.value)
|
|
142
|
+
elif args.type == "float":
|
|
143
|
+
value = float(args.value)
|
|
144
|
+
elif args.type == "bool":
|
|
145
|
+
value = bool(int(args.value))
|
|
146
|
+
else:
|
|
147
|
+
value = args.value
|
|
148
|
+
proxy.setValue(args.address, args.parameter, value)
|
|
149
|
+
sys.exit(0)
|
|
150
|
+
elif args.paramset_key == ParamsetKey.MASTER and args.value is None:
|
|
151
|
+
paramset: dict[str, Any] | None
|
|
152
|
+
if (paramset := proxy.getParamset(args.address, args.paramset_key)) and (args.parameter in paramset): # type: ignore[assignment]
|
|
153
|
+
result = paramset[args.parameter]
|
|
154
|
+
if args.json:
|
|
155
|
+
print(
|
|
156
|
+
json.dumps(
|
|
157
|
+
{
|
|
158
|
+
"address": args.address,
|
|
159
|
+
"paramset_key": args.paramset_key,
|
|
160
|
+
"parameter": args.parameter,
|
|
161
|
+
"value": result,
|
|
162
|
+
},
|
|
163
|
+
ensure_ascii=False,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
print(result)
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
elif args.paramset_key == ParamsetKey.MASTER and args.value:
|
|
170
|
+
if args.type == "int":
|
|
171
|
+
value = int(args.value)
|
|
172
|
+
elif args.type == "float":
|
|
173
|
+
value = float(args.value)
|
|
174
|
+
elif args.type == "bool":
|
|
175
|
+
value = bool(int(args.value))
|
|
176
|
+
else:
|
|
177
|
+
value = args.value
|
|
178
|
+
proxy.putParamset(args.address, args.paramset_key, {args.parameter: value})
|
|
179
|
+
sys.exit(0)
|
|
180
|
+
except Exception as ex:
|
|
181
|
+
print(str(ex), file=sys.stderr)
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Data point and event model for AioHomematic.
|
|
5
|
+
|
|
6
|
+
This package wires together the model subpackages that turn device/channel
|
|
7
|
+
parameter descriptions into concrete data points and events:
|
|
8
|
+
- generic: Default data point types (switch, number, sensor, select, etc.).
|
|
9
|
+
- custom: Higher-level composites and device-specific behaviors.
|
|
10
|
+
- calculated: Derived metrics (e.g., dew point, apparent temperature).
|
|
11
|
+
- hub: Program and system variable data points from the backend hub.
|
|
12
|
+
|
|
13
|
+
The create_data_points_and_events entrypoint inspects a device’s paramset
|
|
14
|
+
information, applies visibility rules, creates events where appropriate, and
|
|
15
|
+
instantiates the required data point objects. It is invoked during device
|
|
16
|
+
initialization to populate the runtime model used by the central unit.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from typing import Final
|
|
23
|
+
|
|
24
|
+
from aiohomematic.const import (
|
|
25
|
+
CLICK_EVENTS,
|
|
26
|
+
DEVICE_ERROR_EVENTS,
|
|
27
|
+
IMPULSE_EVENTS,
|
|
28
|
+
Flag,
|
|
29
|
+
Operations,
|
|
30
|
+
Parameter,
|
|
31
|
+
ParameterData,
|
|
32
|
+
ParamsetKey,
|
|
33
|
+
)
|
|
34
|
+
from aiohomematic.decorators import inspector
|
|
35
|
+
from aiohomematic.model import device as hmd
|
|
36
|
+
from aiohomematic.model.calculated import create_calculated_data_points
|
|
37
|
+
from aiohomematic.model.event import create_event_and_append_to_channel
|
|
38
|
+
from aiohomematic.model.generic import create_data_point_and_append_to_channel
|
|
39
|
+
|
|
40
|
+
__all__ = ["create_data_points_and_events"]
|
|
41
|
+
|
|
42
|
+
# Some parameters are marked as INTERNAL in the paramset and not considered by default,
|
|
43
|
+
# but some are required and should be added here.
|
|
44
|
+
_ALLOWED_INTERNAL_PARAMETERS: Final[tuple[Parameter, ...]] = (Parameter.DIRECTION,)
|
|
45
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@inspector
|
|
49
|
+
def create_data_points_and_events(*, device: hmd.Device) -> None:
|
|
50
|
+
"""Create the data points associated to this device."""
|
|
51
|
+
for channel in device.channels.values():
|
|
52
|
+
for paramset_key, paramsset_key_descriptions in channel.paramset_descriptions.items():
|
|
53
|
+
if not device.central.parameter_visibility.is_relevant_paramset(
|
|
54
|
+
channel=channel,
|
|
55
|
+
paramset_key=paramset_key,
|
|
56
|
+
):
|
|
57
|
+
continue
|
|
58
|
+
for (
|
|
59
|
+
parameter,
|
|
60
|
+
parameter_data,
|
|
61
|
+
) in paramsset_key_descriptions.items():
|
|
62
|
+
parameter_is_un_ignored = channel.device.central.parameter_visibility.parameter_is_un_ignored(
|
|
63
|
+
channel=channel,
|
|
64
|
+
paramset_key=paramset_key,
|
|
65
|
+
parameter=parameter,
|
|
66
|
+
)
|
|
67
|
+
if channel.device.central.parameter_visibility.should_skip_parameter(
|
|
68
|
+
channel=channel,
|
|
69
|
+
paramset_key=paramset_key,
|
|
70
|
+
parameter=parameter,
|
|
71
|
+
parameter_is_un_ignored=parameter_is_un_ignored,
|
|
72
|
+
):
|
|
73
|
+
continue
|
|
74
|
+
_process_parameter(
|
|
75
|
+
channel=channel,
|
|
76
|
+
paramset_key=paramset_key,
|
|
77
|
+
parameter=parameter,
|
|
78
|
+
parameter_data=parameter_data,
|
|
79
|
+
parameter_is_un_ignored=parameter_is_un_ignored,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
create_calculated_data_points(channel=channel)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _process_parameter(
|
|
86
|
+
*,
|
|
87
|
+
channel: hmd.Channel,
|
|
88
|
+
paramset_key: ParamsetKey,
|
|
89
|
+
parameter: str,
|
|
90
|
+
parameter_data: ParameterData,
|
|
91
|
+
parameter_is_un_ignored: bool,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Process individual parameter to create data points and events."""
|
|
94
|
+
|
|
95
|
+
if paramset_key == ParamsetKey.MASTER and parameter_data["OPERATIONS"] == 0:
|
|
96
|
+
# required to fix hm master paramset operation values
|
|
97
|
+
parameter_data["OPERATIONS"] = 3
|
|
98
|
+
|
|
99
|
+
if _should_create_event(parameter_data=parameter_data, parameter=parameter):
|
|
100
|
+
create_event_and_append_to_channel(
|
|
101
|
+
channel=channel,
|
|
102
|
+
parameter=parameter,
|
|
103
|
+
parameter_data=parameter_data,
|
|
104
|
+
)
|
|
105
|
+
if _should_skip_data_point(
|
|
106
|
+
parameter_data=parameter_data, parameter=parameter, parameter_is_un_ignored=parameter_is_un_ignored
|
|
107
|
+
):
|
|
108
|
+
_LOGGER.debug(
|
|
109
|
+
"CREATE_DATA_POINTS: Skipping %s (no event or internal)",
|
|
110
|
+
parameter,
|
|
111
|
+
)
|
|
112
|
+
return
|
|
113
|
+
# CLICK_EVENTS are allowed for Buttons
|
|
114
|
+
if parameter not in IMPULSE_EVENTS and (not parameter.startswith(DEVICE_ERROR_EVENTS) or parameter_is_un_ignored):
|
|
115
|
+
create_data_point_and_append_to_channel(
|
|
116
|
+
channel=channel,
|
|
117
|
+
paramset_key=paramset_key,
|
|
118
|
+
parameter=parameter,
|
|
119
|
+
parameter_data=parameter_data,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _should_create_event(*, parameter_data: ParameterData, parameter: str) -> bool:
|
|
124
|
+
"""Determine if an event should be created for the parameter."""
|
|
125
|
+
return bool(
|
|
126
|
+
parameter_data["OPERATIONS"] & Operations.EVENT
|
|
127
|
+
and (parameter in CLICK_EVENTS or parameter.startswith(DEVICE_ERROR_EVENTS) or parameter in IMPULSE_EVENTS)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _should_skip_data_point(*, parameter_data: ParameterData, parameter: str, parameter_is_un_ignored: bool) -> bool:
|
|
132
|
+
"""Determine if a data point should be skipped."""
|
|
133
|
+
return bool(
|
|
134
|
+
(not parameter_data["OPERATIONS"] & Operations.EVENT and not parameter_data["OPERATIONS"] & Operations.WRITE)
|
|
135
|
+
or (
|
|
136
|
+
parameter_data["FLAGS"] & Flag.INTERNAL
|
|
137
|
+
and parameter not in _ALLOWED_INTERNAL_PARAMETERS
|
|
138
|
+
and not parameter_is_un_ignored
|
|
139
|
+
)
|
|
140
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Calculated (derived) data points for AioHomematic.
|
|
5
|
+
|
|
6
|
+
This subpackage provides data points whose values are computed from one or more
|
|
7
|
+
underlying device data points. Typical examples include climate-related metrics
|
|
8
|
+
(such as dew point, apparent temperature, frost point, vapor concentration) and
|
|
9
|
+
battery/voltage related assessments (such as operating voltage level).
|
|
10
|
+
|
|
11
|
+
How it works:
|
|
12
|
+
- Each calculated data point is a lightweight model that subscribes to one or
|
|
13
|
+
more generic data points of a channel and recomputes its value when any of
|
|
14
|
+
the source data points change.
|
|
15
|
+
- Relevance is determined per channel. A calculated data point class exposes an
|
|
16
|
+
"is_relevant_for_model" method that decides if the channel provides the
|
|
17
|
+
necessary inputs.
|
|
18
|
+
- Creation is handled centrally via the factory function below.
|
|
19
|
+
|
|
20
|
+
Factory:
|
|
21
|
+
- create_calculated_data_points(channel): Iterates over the known calculated
|
|
22
|
+
implementations, checks their relevance against the given channel, and, if
|
|
23
|
+
applicable, creates and attaches instances to the channel so they behave like
|
|
24
|
+
normal read-only data points.
|
|
25
|
+
|
|
26
|
+
Modules/classes:
|
|
27
|
+
- ApparentTemperature, DewPoint, DewPointSpread, Enthalphy, FrostPoint, VaporConcentration: Climate-related
|
|
28
|
+
sensors implemented in climate.py using well-known formulas (see
|
|
29
|
+
aiohomematic.model.calculated.support for details and references).
|
|
30
|
+
- OperatingVoltageLevel: Interprets battery/voltage values and exposes a human
|
|
31
|
+
readable operating level classification.
|
|
32
|
+
|
|
33
|
+
These calculated data points complement generic and custom data points by
|
|
34
|
+
exposing useful metrics not directly provided by the device/firmware.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import logging
|
|
40
|
+
from typing import Final
|
|
41
|
+
|
|
42
|
+
from aiohomematic.decorators import inspector
|
|
43
|
+
from aiohomematic.model import device as hmd
|
|
44
|
+
from aiohomematic.model.calculated.climate import (
|
|
45
|
+
ApparentTemperature,
|
|
46
|
+
DewPoint,
|
|
47
|
+
DewPointSpread,
|
|
48
|
+
Enthalpy,
|
|
49
|
+
FrostPoint,
|
|
50
|
+
VaporConcentration,
|
|
51
|
+
)
|
|
52
|
+
from aiohomematic.model.calculated.data_point import CalculatedDataPoint
|
|
53
|
+
from aiohomematic.model.calculated.operating_voltage_level import OperatingVoltageLevel
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"ApparentTemperature",
|
|
57
|
+
"CalculatedDataPoint",
|
|
58
|
+
"DewPoint",
|
|
59
|
+
"DewPointSpread",
|
|
60
|
+
"Enthalpy",
|
|
61
|
+
"FrostPoint",
|
|
62
|
+
"OperatingVoltageLevel",
|
|
63
|
+
"VaporConcentration",
|
|
64
|
+
"create_calculated_data_points",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
_CALCULATED_DATA_POINTS: Final = (
|
|
68
|
+
ApparentTemperature,
|
|
69
|
+
DewPoint,
|
|
70
|
+
DewPointSpread,
|
|
71
|
+
Enthalpy,
|
|
72
|
+
FrostPoint,
|
|
73
|
+
OperatingVoltageLevel,
|
|
74
|
+
VaporConcentration,
|
|
75
|
+
)
|
|
76
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@inspector
|
|
80
|
+
def create_calculated_data_points(*, channel: hmd.Channel) -> None:
|
|
81
|
+
"""Decides which data point category should be used, and creates the required data points."""
|
|
82
|
+
for dp in _CALCULATED_DATA_POINTS:
|
|
83
|
+
if dp.is_relevant_for_model(channel=channel):
|
|
84
|
+
channel.add_data_point(data_point=dp(channel=channel))
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Module for calculating the apparent temperature in the sensor category."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
from aiohomematic.const import CalulatedParameter, DataPointCategory, Parameter, ParameterType, ParamsetKey
|
|
11
|
+
from aiohomematic.model import device as hmd
|
|
12
|
+
from aiohomematic.model.calculated.data_point import CalculatedDataPoint
|
|
13
|
+
from aiohomematic.model.calculated.support import (
|
|
14
|
+
calculate_apparent_temperature,
|
|
15
|
+
calculate_dew_point,
|
|
16
|
+
calculate_dew_point_spread,
|
|
17
|
+
calculate_enthalpy,
|
|
18
|
+
calculate_frost_point,
|
|
19
|
+
calculate_vapor_concentration,
|
|
20
|
+
)
|
|
21
|
+
from aiohomematic.model.generic import DpSensor
|
|
22
|
+
from aiohomematic.property_decorators import state_property
|
|
23
|
+
from aiohomematic.support import element_matches_key
|
|
24
|
+
|
|
25
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseClimateSensor[SensorT: float | None](CalculatedDataPoint[SensorT]):
|
|
29
|
+
"""Implementation of a calculated climate sensor."""
|
|
30
|
+
|
|
31
|
+
__slots__ = (
|
|
32
|
+
"_dp_temperature",
|
|
33
|
+
"_dp_humidity",
|
|
34
|
+
"_dp_wind_speed",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_category = DataPointCategory.SENSOR
|
|
38
|
+
|
|
39
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
40
|
+
"""Initialize the data point."""
|
|
41
|
+
|
|
42
|
+
super().__init__(channel=channel)
|
|
43
|
+
self._type = ParameterType.FLOAT
|
|
44
|
+
|
|
45
|
+
def _init_data_point_fields(self) -> None:
|
|
46
|
+
"""Init the data point fields."""
|
|
47
|
+
super()._init_data_point_fields()
|
|
48
|
+
self._dp_temperature: DpSensor = (
|
|
49
|
+
self._add_data_point(
|
|
50
|
+
parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
|
|
51
|
+
)
|
|
52
|
+
if self._channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES)
|
|
53
|
+
else self._add_data_point(
|
|
54
|
+
parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
self._dp_humidity: DpSensor = (
|
|
58
|
+
self._add_data_point(
|
|
59
|
+
parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
|
|
60
|
+
)
|
|
61
|
+
if self._channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
|
|
62
|
+
else self._add_data_point(
|
|
63
|
+
parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ApparentTemperature(BaseClimateSensor):
|
|
69
|
+
"""Implementation of a calculated sensor for apparent temperature."""
|
|
70
|
+
|
|
71
|
+
__slots__ = ()
|
|
72
|
+
|
|
73
|
+
_calculated_parameter = CalulatedParameter.APPARENT_TEMPERATURE
|
|
74
|
+
|
|
75
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
76
|
+
"""Initialize the data point."""
|
|
77
|
+
super().__init__(channel=channel)
|
|
78
|
+
self._unit = "°C"
|
|
79
|
+
|
|
80
|
+
def _init_data_point_fields(self) -> None:
|
|
81
|
+
"""Init the data point fields."""
|
|
82
|
+
super()._init_data_point_fields()
|
|
83
|
+
self._dp_wind_speed: DpSensor = self._add_data_point(
|
|
84
|
+
parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES, data_point_type=DpSensor
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
89
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
90
|
+
return (
|
|
91
|
+
element_matches_key(
|
|
92
|
+
search_elements=_RELEVANT_MODELS_APPARENT_TEMPERATURE, compare_with=channel.device.model
|
|
93
|
+
)
|
|
94
|
+
and channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
|
|
95
|
+
is not None
|
|
96
|
+
and channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES)
|
|
97
|
+
is not None
|
|
98
|
+
and channel.get_generic_data_point(parameter=Parameter.WIND_SPEED, paramset_key=ParamsetKey.VALUES)
|
|
99
|
+
is not None
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@state_property
|
|
103
|
+
def value(self) -> float | None:
|
|
104
|
+
"""Return the value."""
|
|
105
|
+
if (
|
|
106
|
+
self._dp_temperature.value is not None
|
|
107
|
+
and self._dp_humidity.value is not None
|
|
108
|
+
and self._dp_wind_speed.value is not None
|
|
109
|
+
):
|
|
110
|
+
return calculate_apparent_temperature(
|
|
111
|
+
temperature=self._dp_temperature.value,
|
|
112
|
+
humidity=self._dp_humidity.value,
|
|
113
|
+
wind_speed=self._dp_wind_speed.value,
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DewPoint(BaseClimateSensor):
|
|
119
|
+
"""Implementation of a calculated sensor for dew point."""
|
|
120
|
+
|
|
121
|
+
__slots__ = ()
|
|
122
|
+
|
|
123
|
+
_calculated_parameter = CalulatedParameter.DEW_POINT
|
|
124
|
+
|
|
125
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
126
|
+
"""Initialize the data point."""
|
|
127
|
+
super().__init__(channel=channel)
|
|
128
|
+
self._unit = "°C"
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
132
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
133
|
+
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
134
|
+
|
|
135
|
+
@state_property
|
|
136
|
+
def value(self) -> float | None:
|
|
137
|
+
"""Return the value."""
|
|
138
|
+
if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
|
|
139
|
+
return calculate_dew_point(
|
|
140
|
+
temperature=self._dp_temperature.value,
|
|
141
|
+
humidity=self._dp_humidity.value,
|
|
142
|
+
)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class DewPointSpread(BaseClimateSensor):
|
|
147
|
+
"""Implementation of a calculated sensor for dew point spread."""
|
|
148
|
+
|
|
149
|
+
__slots__ = ()
|
|
150
|
+
|
|
151
|
+
_calculated_parameter = CalulatedParameter.DEW_POINT_SPREAD
|
|
152
|
+
|
|
153
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
154
|
+
"""Initialize the data point."""
|
|
155
|
+
super().__init__(channel=channel)
|
|
156
|
+
self._unit = "K"
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
160
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
161
|
+
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
162
|
+
|
|
163
|
+
@state_property
|
|
164
|
+
def value(self) -> float | None:
|
|
165
|
+
"""Return the value."""
|
|
166
|
+
if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
|
|
167
|
+
return calculate_dew_point_spread(
|
|
168
|
+
temperature=self._dp_temperature.value,
|
|
169
|
+
humidity=self._dp_humidity.value,
|
|
170
|
+
)
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Enthalpy(BaseClimateSensor):
|
|
175
|
+
"""Implementation of a calculated sensor for enthalpy."""
|
|
176
|
+
|
|
177
|
+
__slots__ = ()
|
|
178
|
+
|
|
179
|
+
_calculated_parameter = CalulatedParameter.ENTHALPY
|
|
180
|
+
|
|
181
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
182
|
+
"""Initialize the data point."""
|
|
183
|
+
super().__init__(channel=channel)
|
|
184
|
+
self._unit = "kJ/kg"
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
188
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
189
|
+
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
190
|
+
|
|
191
|
+
@state_property
|
|
192
|
+
def value(self) -> float | None:
|
|
193
|
+
"""Return the value."""
|
|
194
|
+
if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
|
|
195
|
+
return calculate_enthalpy(
|
|
196
|
+
temperature=self._dp_temperature.value,
|
|
197
|
+
humidity=self._dp_humidity.value,
|
|
198
|
+
)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class FrostPoint(BaseClimateSensor):
|
|
203
|
+
"""Implementation of a calculated sensor for frost point."""
|
|
204
|
+
|
|
205
|
+
__slots__ = ()
|
|
206
|
+
|
|
207
|
+
_calculated_parameter = CalulatedParameter.FROST_POINT
|
|
208
|
+
|
|
209
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
210
|
+
"""Initialize the data point."""
|
|
211
|
+
super().__init__(channel=channel)
|
|
212
|
+
self._unit = "°C"
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
216
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
217
|
+
return _is_relevant_for_model_temperature_and_humidity(
|
|
218
|
+
channel=channel, relevant_models=_RELEVANT_MODELS_FROST_POINT
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@state_property
|
|
222
|
+
def value(self) -> float | None:
|
|
223
|
+
"""Return the value."""
|
|
224
|
+
if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
|
|
225
|
+
return calculate_frost_point(
|
|
226
|
+
temperature=self._dp_temperature.value,
|
|
227
|
+
humidity=self._dp_humidity.value,
|
|
228
|
+
)
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class VaporConcentration(BaseClimateSensor):
|
|
233
|
+
"""Implementation of a calculated sensor for vapor concentration."""
|
|
234
|
+
|
|
235
|
+
__slots__ = ()
|
|
236
|
+
|
|
237
|
+
_calculated_parameter = CalulatedParameter.VAPOR_CONCENTRATION
|
|
238
|
+
|
|
239
|
+
def __init__(self, *, channel: hmd.Channel) -> None:
|
|
240
|
+
"""Initialize the data point."""
|
|
241
|
+
super().__init__(channel=channel)
|
|
242
|
+
self._unit = "g/m³"
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def is_relevant_for_model(*, channel: hmd.Channel) -> bool:
|
|
246
|
+
"""Return if this calculated data point is relevant for the model."""
|
|
247
|
+
return _is_relevant_for_model_temperature_and_humidity(channel=channel)
|
|
248
|
+
|
|
249
|
+
@state_property
|
|
250
|
+
def value(self) -> float | None:
|
|
251
|
+
"""Return the value."""
|
|
252
|
+
if self._dp_temperature.value is not None and self._dp_humidity.value is not None:
|
|
253
|
+
return calculate_vapor_concentration(
|
|
254
|
+
temperature=self._dp_temperature.value,
|
|
255
|
+
humidity=self._dp_humidity.value,
|
|
256
|
+
)
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _is_relevant_for_model_temperature_and_humidity(
|
|
261
|
+
channel: hmd.Channel, relevant_models: tuple[str, ...] | None = None
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""Return if this calculated data point is relevant for the model with temperature and humidity."""
|
|
264
|
+
return (
|
|
265
|
+
(
|
|
266
|
+
relevant_models is not None
|
|
267
|
+
and element_matches_key(search_elements=relevant_models, compare_with=channel.device.model)
|
|
268
|
+
)
|
|
269
|
+
or relevant_models is None
|
|
270
|
+
) and (
|
|
271
|
+
(
|
|
272
|
+
channel.get_generic_data_point(parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES) is not None
|
|
273
|
+
or channel.get_generic_data_point(parameter=Parameter.ACTUAL_TEMPERATURE, paramset_key=ParamsetKey.VALUES)
|
|
274
|
+
is not None
|
|
275
|
+
)
|
|
276
|
+
and (
|
|
277
|
+
channel.get_generic_data_point(parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES) is not None
|
|
278
|
+
or channel.get_generic_data_point(parameter=Parameter.ACTUAL_HUMIDITY, paramset_key=ParamsetKey.VALUES)
|
|
279
|
+
is not None
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_RELEVANT_MODELS_APPARENT_TEMPERATURE: Final[tuple[str, ...]] = ("HmIP-SWO",)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
_RELEVANT_MODELS_FROST_POINT: Final[tuple[str, ...]] = (
|
|
288
|
+
"HmIP-STHO",
|
|
289
|
+
"HmIP-SWO",
|
|
290
|
+
)
|