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,208 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Event model for AioHomematic.
|
|
5
|
+
|
|
6
|
+
This module defines the event data point hierarchy used to expose Homematic
|
|
7
|
+
button presses, device errors, and impulse notifications to applications.
|
|
8
|
+
|
|
9
|
+
Included classes:
|
|
10
|
+
- GenericEvent: Base event that integrates with the common data point API
|
|
11
|
+
(category, usage, names/paths, callbacks) and provides emit_event handling.
|
|
12
|
+
- ClickEvent: Represents key press events (EventType.KEYPRESS).
|
|
13
|
+
- DeviceErrorEvent: Represents device error signaling with special value change
|
|
14
|
+
semantics before emitting an event (EventType.DEVICE_ERROR).
|
|
15
|
+
- ImpulseEvent: Represents impulse events (EventType.IMPULSE).
|
|
16
|
+
|
|
17
|
+
Factory helpers:
|
|
18
|
+
- create_event_and_append_to_channel: Determines the appropriate event type for
|
|
19
|
+
a given parameter description and attaches an instance to the channel.
|
|
20
|
+
|
|
21
|
+
Typical flow:
|
|
22
|
+
1) During device initialization, model.create_data_points_and_events inspects
|
|
23
|
+
paramset descriptions.
|
|
24
|
+
2) For parameters that support Operations.EVENT and match known event names
|
|
25
|
+
(CLICK_EVENTS, DEVICE_ERROR_EVENTS, IMPULSE_EVENTS), an event data point is
|
|
26
|
+
created and registered on the channel.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from datetime import datetime
|
|
32
|
+
import logging
|
|
33
|
+
from typing import Any, Final
|
|
34
|
+
|
|
35
|
+
from aiohomematic import support as hms
|
|
36
|
+
from aiohomematic.async_support import loop_check
|
|
37
|
+
from aiohomematic.const import (
|
|
38
|
+
CLICK_EVENTS,
|
|
39
|
+
DATA_POINT_EVENTS,
|
|
40
|
+
DEVICE_ERROR_EVENTS,
|
|
41
|
+
IMPULSE_EVENTS,
|
|
42
|
+
DataPointCategory,
|
|
43
|
+
DataPointUsage,
|
|
44
|
+
EventType,
|
|
45
|
+
Operations,
|
|
46
|
+
ParameterData,
|
|
47
|
+
ParamsetKey,
|
|
48
|
+
)
|
|
49
|
+
from aiohomematic.decorators import inspector
|
|
50
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
51
|
+
from aiohomematic.model import device as hmd
|
|
52
|
+
from aiohomematic.model.data_point import BaseParameterDataPoint
|
|
53
|
+
from aiohomematic.model.support import DataPointNameData, get_event_name
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"ClickEvent",
|
|
57
|
+
"DeviceErrorEvent",
|
|
58
|
+
"GenericEvent",
|
|
59
|
+
"ImpulseEvent",
|
|
60
|
+
"create_event_and_append_to_channel",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GenericEvent(BaseParameterDataPoint[Any, Any]):
|
|
67
|
+
"""Base class for events."""
|
|
68
|
+
|
|
69
|
+
__slots__ = ("_event_type",)
|
|
70
|
+
|
|
71
|
+
_category = DataPointCategory.EVENT
|
|
72
|
+
_event_type: EventType
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
channel: hmd.Channel,
|
|
78
|
+
parameter: str,
|
|
79
|
+
parameter_data: ParameterData,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Initialize the event handler."""
|
|
82
|
+
super().__init__(
|
|
83
|
+
channel=channel,
|
|
84
|
+
paramset_key=ParamsetKey.VALUES,
|
|
85
|
+
parameter=parameter,
|
|
86
|
+
parameter_data=parameter_data,
|
|
87
|
+
unique_id_prefix=f"event_{channel.central.name}",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def usage(self) -> DataPointUsage:
|
|
92
|
+
"""Return the data_point usage."""
|
|
93
|
+
if (forced_by_com := self._enabled_by_channel_operation_mode) is None:
|
|
94
|
+
return self._get_data_point_usage()
|
|
95
|
+
return DataPointUsage.EVENT if forced_by_com else DataPointUsage.NO_CREATE # pylint: disable=using-constant-test
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def event_type(self) -> EventType:
|
|
99
|
+
"""Return the event_type of the event."""
|
|
100
|
+
return self._event_type
|
|
101
|
+
|
|
102
|
+
async def event(self, *, value: Any, received_at: datetime) -> None:
|
|
103
|
+
"""Handle event for which this handler has subscribed."""
|
|
104
|
+
if self.event_type in DATA_POINT_EVENTS:
|
|
105
|
+
self.emit_data_point_updated_event()
|
|
106
|
+
self._set_modified_at(modified_at=received_at)
|
|
107
|
+
self.emit_event(value=value)
|
|
108
|
+
|
|
109
|
+
@loop_check
|
|
110
|
+
def emit_event(self, *, value: Any) -> None:
|
|
111
|
+
"""Do what is needed to emit an event."""
|
|
112
|
+
self._central.emit_homematic_callback(event_type=self.event_type, event_data=self.get_event_data(value=value))
|
|
113
|
+
|
|
114
|
+
def _get_data_point_name(self) -> DataPointNameData:
|
|
115
|
+
"""Create the name for the data_point."""
|
|
116
|
+
return get_event_name(
|
|
117
|
+
channel=self._channel,
|
|
118
|
+
parameter=self._parameter,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _get_data_point_usage(self) -> DataPointUsage:
|
|
122
|
+
"""Generate the usage for the data_point."""
|
|
123
|
+
return DataPointUsage.EVENT
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ClickEvent(GenericEvent):
|
|
127
|
+
"""class for handling click events."""
|
|
128
|
+
|
|
129
|
+
__slots__ = ()
|
|
130
|
+
|
|
131
|
+
_event_type = EventType.KEYPRESS
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class DeviceErrorEvent(GenericEvent):
|
|
135
|
+
"""class for handling device error events."""
|
|
136
|
+
|
|
137
|
+
__slots__ = ()
|
|
138
|
+
|
|
139
|
+
_event_type = EventType.DEVICE_ERROR
|
|
140
|
+
|
|
141
|
+
async def event(self, *, value: Any, received_at: datetime) -> None:
|
|
142
|
+
"""Handle event for which this handler has subscribed."""
|
|
143
|
+
old_value, new_value = self.write_value(value=value, write_at=received_at)
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
isinstance(new_value, bool)
|
|
147
|
+
and ((old_value is None and new_value is True) or (isinstance(old_value, bool) and old_value != new_value))
|
|
148
|
+
) or (
|
|
149
|
+
isinstance(new_value, int)
|
|
150
|
+
and ((old_value is None and new_value > 0) or (isinstance(old_value, int) and old_value != new_value))
|
|
151
|
+
):
|
|
152
|
+
self.emit_event(value=new_value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ImpulseEvent(GenericEvent):
|
|
156
|
+
"""class for handling impulse events."""
|
|
157
|
+
|
|
158
|
+
__slots__ = ()
|
|
159
|
+
|
|
160
|
+
_event_type = EventType.IMPULSE
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@inspector
|
|
164
|
+
def create_event_and_append_to_channel(channel: hmd.Channel, parameter: str, parameter_data: ParameterData) -> None:
|
|
165
|
+
"""Create action event data_point."""
|
|
166
|
+
_LOGGER.debug(
|
|
167
|
+
"CREATE_EVENT_AND_APPEND_TO_DEVICE: Creating event for %s, %s, %s",
|
|
168
|
+
channel.address,
|
|
169
|
+
parameter,
|
|
170
|
+
channel.device.interface_id,
|
|
171
|
+
)
|
|
172
|
+
if (event_t := _determine_event_type(parameter=parameter, parameter_data=parameter_data)) and (
|
|
173
|
+
event := _safe_create_event(
|
|
174
|
+
event_t=event_t, channel=channel, parameter=parameter, parameter_data=parameter_data
|
|
175
|
+
)
|
|
176
|
+
):
|
|
177
|
+
channel.add_data_point(data_point=event)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _determine_event_type(parameter: str, parameter_data: ParameterData) -> type[GenericEvent] | None:
|
|
181
|
+
event_t: type[GenericEvent] | None = None
|
|
182
|
+
if parameter_data["OPERATIONS"] & Operations.EVENT:
|
|
183
|
+
if parameter in CLICK_EVENTS:
|
|
184
|
+
event_t = ClickEvent
|
|
185
|
+
if parameter.startswith(DEVICE_ERROR_EVENTS):
|
|
186
|
+
event_t = DeviceErrorEvent
|
|
187
|
+
if parameter in IMPULSE_EVENTS:
|
|
188
|
+
event_t = ImpulseEvent
|
|
189
|
+
return event_t
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _safe_create_event(
|
|
193
|
+
event_t: type[GenericEvent],
|
|
194
|
+
channel: hmd.Channel,
|
|
195
|
+
parameter: str,
|
|
196
|
+
parameter_data: ParameterData,
|
|
197
|
+
) -> GenericEvent:
|
|
198
|
+
"""Safely create a event and handle exceptions."""
|
|
199
|
+
try:
|
|
200
|
+
return event_t(
|
|
201
|
+
channel=channel,
|
|
202
|
+
parameter=parameter,
|
|
203
|
+
parameter_data=parameter_data,
|
|
204
|
+
)
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
raise AioHomematicException(
|
|
207
|
+
f"CREATE_EVENT_AND_APPEND_TO_CHANNEL: Unable to create event:{hms.extract_exc_args(exc=exc)}"
|
|
208
|
+
) from exc
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Generic data points for AioHomematic.
|
|
5
|
+
|
|
6
|
+
Overview
|
|
7
|
+
- This subpackage provides the default, device-agnostic data point classes
|
|
8
|
+
(switch, number, sensor, select, text, button, binary_sensor) used for most
|
|
9
|
+
parameters across Homematic devices.
|
|
10
|
+
- It also exposes a central factory function that selects the appropriate data
|
|
11
|
+
point class for a parameter based on its description provided by the backend.
|
|
12
|
+
|
|
13
|
+
Factory
|
|
14
|
+
- create_data_point_and_append_to_channel(channel, paramset_key, parameter, parameter_data)
|
|
15
|
+
inspects ParameterData (TYPE, OPERATIONS, FLAGS, etc.) to determine which
|
|
16
|
+
GenericDataPoint subclass to instantiate, creates it safely and appends it to
|
|
17
|
+
the given channel.
|
|
18
|
+
|
|
19
|
+
Mapping rules (simplified)
|
|
20
|
+
- TYPE==ACTION:
|
|
21
|
+
- OPERATIONS==WRITE -> DpButton (for specific button-like actions or virtual
|
|
22
|
+
remotes) else DpAction; otherwise, when also readable, treat as DpSwitch.
|
|
23
|
+
- TYPE in {BOOL, ENUM, FLOAT, INTEGER, STRING} with WRITE capabilities ->
|
|
24
|
+
DpSwitch, DpSelect, DpFloat, DpInteger, DpText respectively.
|
|
25
|
+
- Read-only parameters (no WRITE) become sensors; BOOL-like sensors are mapped
|
|
26
|
+
to DpBinarySensor when heuristics indicate binary semantics.
|
|
27
|
+
|
|
28
|
+
Special cases
|
|
29
|
+
- Virtual remote models and click parameters are recognized and mapped to
|
|
30
|
+
button-style data points.
|
|
31
|
+
- Certain device/parameter combinations may be wrapped into a different
|
|
32
|
+
category (e.g., switch shown as sensor) when the parameter is not meant to be
|
|
33
|
+
user-visible or is better represented as a sensor, depending on configuration
|
|
34
|
+
and device model.
|
|
35
|
+
|
|
36
|
+
Exports
|
|
37
|
+
- Generic data point base and concrete types: GenericDataPoint, DpSwitch,
|
|
38
|
+
DpAction, DpButton, DpBinarySensor, DpSelect, DpFloat, DpInteger, DpText,
|
|
39
|
+
DpSensor, BaseDpNumber.
|
|
40
|
+
- Factory: create_data_point_and_append_to_channel.
|
|
41
|
+
|
|
42
|
+
See Also
|
|
43
|
+
- aiohomematic.model.custom: Custom data points for specific devices/features.
|
|
44
|
+
- aiohomematic.model.calculated: Calculated/derived data points.
|
|
45
|
+
- aiohomematic.model.device: Device and channel abstractions used here.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from __future__ import annotations
|
|
50
|
+
|
|
51
|
+
from collections.abc import Mapping
|
|
52
|
+
import logging
|
|
53
|
+
from typing import Final
|
|
54
|
+
|
|
55
|
+
from aiohomematic import support as hms
|
|
56
|
+
from aiohomematic.const import (
|
|
57
|
+
CLICK_EVENTS,
|
|
58
|
+
VIRTUAL_REMOTE_MODELS,
|
|
59
|
+
Operations,
|
|
60
|
+
Parameter,
|
|
61
|
+
ParameterData,
|
|
62
|
+
ParameterType,
|
|
63
|
+
ParamsetKey,
|
|
64
|
+
)
|
|
65
|
+
from aiohomematic.decorators import inspector
|
|
66
|
+
from aiohomematic.exceptions import AioHomematicException
|
|
67
|
+
from aiohomematic.model import device as hmd
|
|
68
|
+
from aiohomematic.model.generic.action import DpAction
|
|
69
|
+
from aiohomematic.model.generic.binary_sensor import DpBinarySensor
|
|
70
|
+
from aiohomematic.model.generic.button import DpButton
|
|
71
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
72
|
+
from aiohomematic.model.generic.dummy import DpDummy
|
|
73
|
+
from aiohomematic.model.generic.number import BaseDpNumber, DpFloat, DpInteger
|
|
74
|
+
from aiohomematic.model.generic.select import DpSelect
|
|
75
|
+
from aiohomematic.model.generic.sensor import DpSensor
|
|
76
|
+
from aiohomematic.model.generic.switch import DpSwitch
|
|
77
|
+
from aiohomematic.model.generic.text import DpText
|
|
78
|
+
from aiohomematic.model.support import is_binary_sensor
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"BaseDpNumber",
|
|
82
|
+
"DpAction",
|
|
83
|
+
"DpBinarySensor",
|
|
84
|
+
"DpButton",
|
|
85
|
+
"DpDummy",
|
|
86
|
+
"DpFloat",
|
|
87
|
+
"DpInteger",
|
|
88
|
+
"DpSelect",
|
|
89
|
+
"DpSensor",
|
|
90
|
+
"DpSwitch",
|
|
91
|
+
"DpText",
|
|
92
|
+
"GenericDataPoint",
|
|
93
|
+
"create_data_point_and_append_to_channel",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
97
|
+
_BUTTON_ACTIONS: Final[tuple[str, ...]] = ("RESET_MOTION", "RESET_PRESENCE")
|
|
98
|
+
|
|
99
|
+
# data points that should be wrapped in a new data point on a new category.
|
|
100
|
+
_SWITCH_DP_TO_SENSOR: Final[Mapping[str | tuple[str, ...], Parameter]] = {
|
|
101
|
+
("HmIP-eTRV", "HmIP-HEATING"): Parameter.LEVEL,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@inspector
|
|
106
|
+
def create_data_point_and_append_to_channel(
|
|
107
|
+
*,
|
|
108
|
+
channel: hmd.Channel,
|
|
109
|
+
paramset_key: ParamsetKey,
|
|
110
|
+
parameter: str,
|
|
111
|
+
parameter_data: ParameterData,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Decides which generic category should be used, and creates the required data points."""
|
|
114
|
+
_LOGGER.debug(
|
|
115
|
+
"CREATE_DATA_POINTS: Creating data_point for %s, %s, %s",
|
|
116
|
+
channel.address,
|
|
117
|
+
parameter,
|
|
118
|
+
channel.device.interface_id,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if (dp_t := _determine_data_point_type(channel=channel, parameter=parameter, parameter_data=parameter_data)) and (
|
|
122
|
+
dp := _safe_create_data_point(
|
|
123
|
+
dp_t=dp_t, channel=channel, paramset_key=paramset_key, parameter=parameter, parameter_data=parameter_data
|
|
124
|
+
)
|
|
125
|
+
):
|
|
126
|
+
_LOGGER.debug(
|
|
127
|
+
"CREATE_DATA_POINT_AND_APPEND_TO_CHANNEL: %s: %s %s",
|
|
128
|
+
dp.category,
|
|
129
|
+
channel.address,
|
|
130
|
+
parameter,
|
|
131
|
+
)
|
|
132
|
+
channel.add_data_point(data_point=dp)
|
|
133
|
+
if _check_switch_to_sensor(data_point=dp):
|
|
134
|
+
dp.force_to_sensor()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _determine_data_point_type(
|
|
138
|
+
*, channel: hmd.Channel, parameter: str, parameter_data: ParameterData
|
|
139
|
+
) -> type[GenericDataPoint] | None:
|
|
140
|
+
"""Determine the type of data point based on parameter and operations."""
|
|
141
|
+
p_type = parameter_data["TYPE"]
|
|
142
|
+
p_operations = parameter_data["OPERATIONS"]
|
|
143
|
+
dp_t: type[GenericDataPoint] | None = None
|
|
144
|
+
if p_operations & Operations.WRITE:
|
|
145
|
+
if p_type == ParameterType.ACTION:
|
|
146
|
+
if p_operations == Operations.WRITE:
|
|
147
|
+
if parameter in _BUTTON_ACTIONS or channel.device.model in VIRTUAL_REMOTE_MODELS:
|
|
148
|
+
dp_t = DpButton
|
|
149
|
+
else:
|
|
150
|
+
dp_t = DpAction
|
|
151
|
+
elif parameter in CLICK_EVENTS:
|
|
152
|
+
dp_t = DpButton
|
|
153
|
+
else:
|
|
154
|
+
dp_t = DpSwitch
|
|
155
|
+
elif p_operations == Operations.WRITE:
|
|
156
|
+
dp_t = DpAction
|
|
157
|
+
elif p_type == ParameterType.BOOL:
|
|
158
|
+
dp_t = DpSwitch
|
|
159
|
+
elif p_type == ParameterType.ENUM:
|
|
160
|
+
dp_t = DpSelect
|
|
161
|
+
elif p_type == ParameterType.FLOAT:
|
|
162
|
+
dp_t = DpFloat
|
|
163
|
+
elif p_type == ParameterType.INTEGER:
|
|
164
|
+
dp_t = DpInteger
|
|
165
|
+
elif p_type == ParameterType.STRING:
|
|
166
|
+
dp_t = DpText
|
|
167
|
+
elif parameter not in CLICK_EVENTS:
|
|
168
|
+
# Also check, if sensor could be a binary_sensor due to.
|
|
169
|
+
if is_binary_sensor(parameter_data):
|
|
170
|
+
parameter_data["TYPE"] = ParameterType.BOOL
|
|
171
|
+
dp_t = DpBinarySensor
|
|
172
|
+
else:
|
|
173
|
+
dp_t = DpSensor
|
|
174
|
+
|
|
175
|
+
return dp_t
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _safe_create_data_point(
|
|
179
|
+
*,
|
|
180
|
+
dp_t: type[GenericDataPoint],
|
|
181
|
+
channel: hmd.Channel,
|
|
182
|
+
paramset_key: ParamsetKey,
|
|
183
|
+
parameter: str,
|
|
184
|
+
parameter_data: ParameterData,
|
|
185
|
+
) -> GenericDataPoint:
|
|
186
|
+
"""Safely create a data point and handle exceptions."""
|
|
187
|
+
try:
|
|
188
|
+
return dp_t(
|
|
189
|
+
channel=channel,
|
|
190
|
+
paramset_key=paramset_key,
|
|
191
|
+
parameter=parameter,
|
|
192
|
+
parameter_data=parameter_data,
|
|
193
|
+
)
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
raise AioHomematicException(
|
|
196
|
+
f"CREATE_DATA_POINT_AND_APPEND_TO_CHANNEL: Unable to create data_point:{hms.extract_exc_args(exc=exc)}"
|
|
197
|
+
) from exc
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _check_switch_to_sensor(*, data_point: GenericDataPoint) -> bool:
|
|
201
|
+
"""Check if parameter of a device should be wrapped to a different category."""
|
|
202
|
+
if data_point.device.central.parameter_visibility.parameter_is_un_ignored(
|
|
203
|
+
channel=data_point.channel,
|
|
204
|
+
paramset_key=data_point.paramset_key,
|
|
205
|
+
parameter=data_point.parameter,
|
|
206
|
+
):
|
|
207
|
+
return False
|
|
208
|
+
for devices, parameter in _SWITCH_DP_TO_SENSOR.items():
|
|
209
|
+
if (
|
|
210
|
+
hms.element_matches_key(
|
|
211
|
+
search_elements=devices,
|
|
212
|
+
compare_with=data_point.device.model,
|
|
213
|
+
)
|
|
214
|
+
and data_point.parameter == parameter
|
|
215
|
+
):
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""
|
|
4
|
+
Module for action data points.
|
|
5
|
+
|
|
6
|
+
Actions are used to send data for write only parameters to backend.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from aiohomematic.const import DataPointCategory
|
|
14
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
15
|
+
from aiohomematic.model.support import get_index_of_value_from_value_list
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DpAction(GenericDataPoint[None, Any]):
|
|
19
|
+
"""
|
|
20
|
+
Implementation of an action.
|
|
21
|
+
|
|
22
|
+
This is an internal default category that gets automatically generated.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = ()
|
|
26
|
+
|
|
27
|
+
_category = DataPointCategory.ACTION
|
|
28
|
+
_validate_state_change = False
|
|
29
|
+
|
|
30
|
+
def _prepare_value_for_sending(self, *, value: Any, do_validate: bool = True) -> Any:
|
|
31
|
+
"""Prepare value before sending."""
|
|
32
|
+
if (index := get_index_of_value_from_value_list(value=value, value_list=self._values)) is not None:
|
|
33
|
+
return index
|
|
34
|
+
return value
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Module for data points implemented using the binary_sensor category."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from aiohomematic.const import DataPointCategory
|
|
10
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
11
|
+
from aiohomematic.property_decorators import state_property
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DpBinarySensor(GenericDataPoint[bool | None, bool]):
|
|
15
|
+
"""
|
|
16
|
+
Implementation of a binary_sensor.
|
|
17
|
+
|
|
18
|
+
This is a default data point that gets automatically generated.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ()
|
|
22
|
+
|
|
23
|
+
_category = DataPointCategory.BINARY_SENSOR
|
|
24
|
+
|
|
25
|
+
@state_property
|
|
26
|
+
def value(self) -> bool | None:
|
|
27
|
+
"""Return the value of the data_point."""
|
|
28
|
+
if self._value is not None:
|
|
29
|
+
return cast(bool | None, self._value)
|
|
30
|
+
return cast(bool | None, self._default)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Module for data points implemented using the button category."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from aiohomematic.const import DataPointCategory
|
|
8
|
+
from aiohomematic.decorators import inspector
|
|
9
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DpButton(GenericDataPoint[None, bool]):
|
|
13
|
+
"""
|
|
14
|
+
Implementation of a button.
|
|
15
|
+
|
|
16
|
+
This is a default data point that gets automatically generated.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
_category = DataPointCategory.BUTTON
|
|
22
|
+
_validate_state_change = False
|
|
23
|
+
|
|
24
|
+
@inspector
|
|
25
|
+
async def press(self) -> None:
|
|
26
|
+
"""Handle the button press."""
|
|
27
|
+
await self.send_value(value=True)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2025
|
|
3
|
+
"""Generic python representation of a backend parameter."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
from aiohomematic.const import (
|
|
12
|
+
DP_KEY_VALUE,
|
|
13
|
+
CallSource,
|
|
14
|
+
DataPointUsage,
|
|
15
|
+
EventType,
|
|
16
|
+
Parameter,
|
|
17
|
+
ParameterData,
|
|
18
|
+
ParamsetKey,
|
|
19
|
+
)
|
|
20
|
+
from aiohomematic.decorators import inspector
|
|
21
|
+
from aiohomematic.exceptions import ValidationException
|
|
22
|
+
from aiohomematic.model import data_point as hme, device as hmd
|
|
23
|
+
from aiohomematic.model.support import DataPointNameData, GenericParameterType, get_data_point_name_data
|
|
24
|
+
from aiohomematic.property_decorators import hm_property
|
|
25
|
+
|
|
26
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GenericDataPoint[ParameterT: GenericParameterType, InputParameterT: GenericParameterType](
|
|
30
|
+
hme.BaseParameterDataPoint
|
|
31
|
+
):
|
|
32
|
+
"""Base class for generic data point."""
|
|
33
|
+
|
|
34
|
+
__slots__ = ("_cached_usage",)
|
|
35
|
+
|
|
36
|
+
_validate_state_change: bool = True
|
|
37
|
+
is_hmtype: bool = True
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
channel: hmd.Channel,
|
|
43
|
+
paramset_key: ParamsetKey,
|
|
44
|
+
parameter: str,
|
|
45
|
+
parameter_data: ParameterData,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Init the generic data_point."""
|
|
48
|
+
super().__init__(
|
|
49
|
+
channel=channel,
|
|
50
|
+
paramset_key=paramset_key,
|
|
51
|
+
parameter=parameter,
|
|
52
|
+
parameter_data=parameter_data,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
@hm_property(cached=True)
|
|
56
|
+
def usage(self) -> DataPointUsage:
|
|
57
|
+
"""Return the data_point usage."""
|
|
58
|
+
if self._is_forced_sensor or self._is_un_ignored:
|
|
59
|
+
return DataPointUsage.DATA_POINT
|
|
60
|
+
if (force_enabled := self._enabled_by_channel_operation_mode) is None:
|
|
61
|
+
return self._get_data_point_usage()
|
|
62
|
+
return DataPointUsage.DATA_POINT if force_enabled else DataPointUsage.NO_CREATE # pylint: disable=using-constant-test
|
|
63
|
+
|
|
64
|
+
async def event(self, *, value: Any, received_at: datetime) -> None:
|
|
65
|
+
"""Handle event for which this data_point has subscribed."""
|
|
66
|
+
self._device.client.last_value_send_cache.remove_last_value_send(
|
|
67
|
+
dpk=self.dpk,
|
|
68
|
+
value=value,
|
|
69
|
+
)
|
|
70
|
+
old_value, new_value = self.write_value(value=value, write_at=received_at)
|
|
71
|
+
if old_value == new_value:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if self._parameter == Parameter.CONFIG_PENDING and new_value is False and old_value is True:
|
|
75
|
+
# reload paramset_descriptions
|
|
76
|
+
await self._device.reload_paramset_descriptions()
|
|
77
|
+
|
|
78
|
+
# reload master data
|
|
79
|
+
for dp in self._device.get_readable_data_points(paramset_key=ParamsetKey.MASTER):
|
|
80
|
+
await dp.load_data_point_value(call_source=CallSource.MANUAL_OR_SCHEDULED, direct_call=True)
|
|
81
|
+
|
|
82
|
+
# re init link peers
|
|
83
|
+
await self._device.re_init_link_peers()
|
|
84
|
+
|
|
85
|
+
# send device availability events
|
|
86
|
+
if self._parameter in (
|
|
87
|
+
Parameter.UN_REACH,
|
|
88
|
+
Parameter.STICKY_UN_REACH,
|
|
89
|
+
):
|
|
90
|
+
self._device.emit_device_updated_callback()
|
|
91
|
+
self._central.emit_homematic_callback(
|
|
92
|
+
event_type=EventType.DEVICE_AVAILABILITY,
|
|
93
|
+
event_data=self.get_event_data(value=new_value),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@inspector
|
|
97
|
+
async def send_value(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
value: InputParameterT,
|
|
101
|
+
collector: hme.CallParameterCollector | None = None,
|
|
102
|
+
collector_order: int = 50,
|
|
103
|
+
do_validate: bool = True,
|
|
104
|
+
) -> set[DP_KEY_VALUE]:
|
|
105
|
+
"""Send value to ccu, or use collector if set."""
|
|
106
|
+
if not self.is_writeable:
|
|
107
|
+
_LOGGER.error("SEND_VALUE: writing to non-writable data_point %s is not possible", self.full_name)
|
|
108
|
+
return set()
|
|
109
|
+
try:
|
|
110
|
+
prepared_value = self._prepare_value_for_sending(value=value, do_validate=do_validate)
|
|
111
|
+
except (ValueError, ValidationException) as verr:
|
|
112
|
+
_LOGGER.warning(verr)
|
|
113
|
+
return set()
|
|
114
|
+
|
|
115
|
+
converted_value = self._convert_value(value=prepared_value)
|
|
116
|
+
# if collector is set, then add value to collector
|
|
117
|
+
if collector:
|
|
118
|
+
collector.add_data_point(data_point=self, value=converted_value, collector_order=collector_order)
|
|
119
|
+
return set()
|
|
120
|
+
|
|
121
|
+
# if collector is not set, then send value directly
|
|
122
|
+
if self._validate_state_change and not self.is_state_change(value=converted_value):
|
|
123
|
+
return set()
|
|
124
|
+
|
|
125
|
+
return await self._client.set_value(
|
|
126
|
+
channel_address=self._channel.address,
|
|
127
|
+
paramset_key=self._paramset_key,
|
|
128
|
+
parameter=self._parameter,
|
|
129
|
+
value=converted_value,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _prepare_value_for_sending(self, *, value: InputParameterT, do_validate: bool = True) -> ParameterT:
|
|
133
|
+
"""Prepare value, if required, before send."""
|
|
134
|
+
return value # type: ignore[return-value]
|
|
135
|
+
|
|
136
|
+
def _get_data_point_name(self) -> DataPointNameData:
|
|
137
|
+
"""Create the name for the data_point."""
|
|
138
|
+
return get_data_point_name_data(
|
|
139
|
+
channel=self._channel,
|
|
140
|
+
parameter=self._parameter,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def _get_data_point_usage(self) -> DataPointUsage:
|
|
144
|
+
"""Generate the usage for the data_point."""
|
|
145
|
+
if self._forced_usage:
|
|
146
|
+
return self._forced_usage
|
|
147
|
+
if self._central.parameter_visibility.parameter_is_hidden(
|
|
148
|
+
channel=self._channel,
|
|
149
|
+
paramset_key=self._paramset_key,
|
|
150
|
+
parameter=self._parameter,
|
|
151
|
+
):
|
|
152
|
+
return DataPointUsage.NO_CREATE
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
DataPointUsage.NO_CREATE
|
|
156
|
+
if (self._device.has_custom_data_point_definition and not self._device.allow_undefined_generic_data_points)
|
|
157
|
+
else DataPointUsage.DATA_POINT
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def is_state_change(self, *, value: ParameterT) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
Check if the state/value changes.
|
|
163
|
+
|
|
164
|
+
If the state is uncertain, the state should also marked as changed.
|
|
165
|
+
"""
|
|
166
|
+
if value != self._value:
|
|
167
|
+
return True
|
|
168
|
+
if self.state_uncertain:
|
|
169
|
+
return True
|
|
170
|
+
_LOGGER.debug("NO_STATE_CHANGE: %s", self.name)
|
|
171
|
+
return False
|