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