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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +61 -0
  2. aiohomematic/async_support.py +212 -0
  3. aiohomematic/central/__init__.py +2309 -0
  4. aiohomematic/central/decorators.py +155 -0
  5. aiohomematic/central/rpc_server.py +295 -0
  6. aiohomematic/client/__init__.py +1848 -0
  7. aiohomematic/client/_rpc_errors.py +81 -0
  8. aiohomematic/client/json_rpc.py +1326 -0
  9. aiohomematic/client/rpc_proxy.py +311 -0
  10. aiohomematic/const.py +1127 -0
  11. aiohomematic/context.py +18 -0
  12. aiohomematic/converter.py +108 -0
  13. aiohomematic/decorators.py +302 -0
  14. aiohomematic/exceptions.py +164 -0
  15. aiohomematic/hmcli.py +186 -0
  16. aiohomematic/model/__init__.py +140 -0
  17. aiohomematic/model/calculated/__init__.py +84 -0
  18. aiohomematic/model/calculated/climate.py +290 -0
  19. aiohomematic/model/calculated/data_point.py +327 -0
  20. aiohomematic/model/calculated/operating_voltage_level.py +299 -0
  21. aiohomematic/model/calculated/support.py +234 -0
  22. aiohomematic/model/custom/__init__.py +177 -0
  23. aiohomematic/model/custom/climate.py +1532 -0
  24. aiohomematic/model/custom/cover.py +792 -0
  25. aiohomematic/model/custom/data_point.py +334 -0
  26. aiohomematic/model/custom/definition.py +871 -0
  27. aiohomematic/model/custom/light.py +1128 -0
  28. aiohomematic/model/custom/lock.py +394 -0
  29. aiohomematic/model/custom/siren.py +275 -0
  30. aiohomematic/model/custom/support.py +41 -0
  31. aiohomematic/model/custom/switch.py +175 -0
  32. aiohomematic/model/custom/valve.py +114 -0
  33. aiohomematic/model/data_point.py +1123 -0
  34. aiohomematic/model/device.py +1445 -0
  35. aiohomematic/model/event.py +208 -0
  36. aiohomematic/model/generic/__init__.py +217 -0
  37. aiohomematic/model/generic/action.py +34 -0
  38. aiohomematic/model/generic/binary_sensor.py +30 -0
  39. aiohomematic/model/generic/button.py +27 -0
  40. aiohomematic/model/generic/data_point.py +171 -0
  41. aiohomematic/model/generic/dummy.py +147 -0
  42. aiohomematic/model/generic/number.py +76 -0
  43. aiohomematic/model/generic/select.py +39 -0
  44. aiohomematic/model/generic/sensor.py +74 -0
  45. aiohomematic/model/generic/switch.py +54 -0
  46. aiohomematic/model/generic/text.py +29 -0
  47. aiohomematic/model/hub/__init__.py +333 -0
  48. aiohomematic/model/hub/binary_sensor.py +24 -0
  49. aiohomematic/model/hub/button.py +28 -0
  50. aiohomematic/model/hub/data_point.py +340 -0
  51. aiohomematic/model/hub/number.py +39 -0
  52. aiohomematic/model/hub/select.py +49 -0
  53. aiohomematic/model/hub/sensor.py +37 -0
  54. aiohomematic/model/hub/switch.py +44 -0
  55. aiohomematic/model/hub/text.py +30 -0
  56. aiohomematic/model/support.py +586 -0
  57. aiohomematic/model/update.py +143 -0
  58. aiohomematic/property_decorators.py +496 -0
  59. aiohomematic/py.typed +0 -0
  60. aiohomematic/rega_scripts/fetch_all_device_data.fn +92 -0
  61. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  62. aiohomematic/rega_scripts/get_serial.fn +44 -0
  63. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  64. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  65. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  66. aiohomematic/store/__init__.py +34 -0
  67. aiohomematic/store/dynamic.py +551 -0
  68. aiohomematic/store/persistent.py +988 -0
  69. aiohomematic/store/visibility.py +812 -0
  70. aiohomematic/support.py +664 -0
  71. aiohomematic/validator.py +112 -0
  72. aiohomematic-2025.11.3.dist-info/METADATA +144 -0
  73. aiohomematic-2025.11.3.dist-info/RECORD +77 -0
  74. aiohomematic-2025.11.3.dist-info/WHEEL +5 -0
  75. aiohomematic-2025.11.3.dist-info/entry_points.txt +2 -0
  76. aiohomematic-2025.11.3.dist-info/licenses/LICENSE +21 -0
  77. 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