aiohomematic 2026.1.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiohomematic/__init__.py +110 -0
- aiohomematic/_log_context_protocol.py +29 -0
- aiohomematic/api.py +410 -0
- aiohomematic/async_support.py +250 -0
- aiohomematic/backend_detection.py +462 -0
- aiohomematic/central/__init__.py +103 -0
- aiohomematic/central/async_rpc_server.py +760 -0
- aiohomematic/central/central_unit.py +1152 -0
- aiohomematic/central/config.py +463 -0
- aiohomematic/central/config_builder.py +772 -0
- aiohomematic/central/connection_state.py +160 -0
- aiohomematic/central/coordinators/__init__.py +38 -0
- aiohomematic/central/coordinators/cache.py +414 -0
- aiohomematic/central/coordinators/client.py +480 -0
- aiohomematic/central/coordinators/connection_recovery.py +1141 -0
- aiohomematic/central/coordinators/device.py +1166 -0
- aiohomematic/central/coordinators/event.py +514 -0
- aiohomematic/central/coordinators/hub.py +532 -0
- aiohomematic/central/decorators.py +184 -0
- aiohomematic/central/device_registry.py +229 -0
- aiohomematic/central/events/__init__.py +104 -0
- aiohomematic/central/events/bus.py +1392 -0
- aiohomematic/central/events/integration.py +424 -0
- aiohomematic/central/events/types.py +194 -0
- aiohomematic/central/health.py +762 -0
- aiohomematic/central/rpc_server.py +353 -0
- aiohomematic/central/scheduler.py +794 -0
- aiohomematic/central/state_machine.py +391 -0
- aiohomematic/client/__init__.py +203 -0
- aiohomematic/client/_rpc_errors.py +187 -0
- aiohomematic/client/backends/__init__.py +48 -0
- aiohomematic/client/backends/base.py +335 -0
- aiohomematic/client/backends/capabilities.py +138 -0
- aiohomematic/client/backends/ccu.py +487 -0
- aiohomematic/client/backends/factory.py +116 -0
- aiohomematic/client/backends/homegear.py +294 -0
- aiohomematic/client/backends/json_ccu.py +252 -0
- aiohomematic/client/backends/protocol.py +316 -0
- aiohomematic/client/ccu.py +1857 -0
- aiohomematic/client/circuit_breaker.py +459 -0
- aiohomematic/client/config.py +64 -0
- aiohomematic/client/handlers/__init__.py +40 -0
- aiohomematic/client/handlers/backup.py +157 -0
- aiohomematic/client/handlers/base.py +79 -0
- aiohomematic/client/handlers/device_ops.py +1085 -0
- aiohomematic/client/handlers/firmware.py +144 -0
- aiohomematic/client/handlers/link_mgmt.py +199 -0
- aiohomematic/client/handlers/metadata.py +436 -0
- aiohomematic/client/handlers/programs.py +144 -0
- aiohomematic/client/handlers/sysvars.py +100 -0
- aiohomematic/client/interface_client.py +1304 -0
- aiohomematic/client/json_rpc.py +2068 -0
- aiohomematic/client/request_coalescer.py +282 -0
- aiohomematic/client/rpc_proxy.py +629 -0
- aiohomematic/client/state_machine.py +324 -0
- aiohomematic/const.py +2207 -0
- aiohomematic/context.py +275 -0
- aiohomematic/converter.py +270 -0
- aiohomematic/decorators.py +390 -0
- aiohomematic/exceptions.py +185 -0
- aiohomematic/hmcli.py +997 -0
- aiohomematic/i18n.py +193 -0
- aiohomematic/interfaces/__init__.py +407 -0
- aiohomematic/interfaces/central.py +1067 -0
- aiohomematic/interfaces/client.py +1096 -0
- aiohomematic/interfaces/coordinators.py +63 -0
- aiohomematic/interfaces/model.py +1921 -0
- aiohomematic/interfaces/operations.py +217 -0
- aiohomematic/logging_context.py +134 -0
- aiohomematic/metrics/__init__.py +125 -0
- aiohomematic/metrics/_protocols.py +140 -0
- aiohomematic/metrics/aggregator.py +534 -0
- aiohomematic/metrics/dataclasses.py +489 -0
- aiohomematic/metrics/emitter.py +292 -0
- aiohomematic/metrics/events.py +183 -0
- aiohomematic/metrics/keys.py +300 -0
- aiohomematic/metrics/observer.py +563 -0
- aiohomematic/metrics/stats.py +172 -0
- aiohomematic/model/__init__.py +189 -0
- aiohomematic/model/availability.py +65 -0
- aiohomematic/model/calculated/__init__.py +89 -0
- aiohomematic/model/calculated/climate.py +276 -0
- aiohomematic/model/calculated/data_point.py +315 -0
- aiohomematic/model/calculated/field.py +147 -0
- aiohomematic/model/calculated/operating_voltage_level.py +286 -0
- aiohomematic/model/calculated/support.py +232 -0
- aiohomematic/model/custom/__init__.py +214 -0
- aiohomematic/model/custom/capabilities/__init__.py +67 -0
- aiohomematic/model/custom/capabilities/climate.py +41 -0
- aiohomematic/model/custom/capabilities/light.py +87 -0
- aiohomematic/model/custom/capabilities/lock.py +44 -0
- aiohomematic/model/custom/capabilities/siren.py +63 -0
- aiohomematic/model/custom/climate.py +1130 -0
- aiohomematic/model/custom/cover.py +722 -0
- aiohomematic/model/custom/data_point.py +360 -0
- aiohomematic/model/custom/definition.py +300 -0
- aiohomematic/model/custom/field.py +89 -0
- aiohomematic/model/custom/light.py +1174 -0
- aiohomematic/model/custom/lock.py +322 -0
- aiohomematic/model/custom/mixins.py +445 -0
- aiohomematic/model/custom/profile.py +945 -0
- aiohomematic/model/custom/registry.py +251 -0
- aiohomematic/model/custom/siren.py +462 -0
- aiohomematic/model/custom/switch.py +195 -0
- aiohomematic/model/custom/text_display.py +289 -0
- aiohomematic/model/custom/valve.py +78 -0
- aiohomematic/model/data_point.py +1416 -0
- aiohomematic/model/device.py +1840 -0
- aiohomematic/model/event.py +216 -0
- aiohomematic/model/generic/__init__.py +327 -0
- aiohomematic/model/generic/action.py +40 -0
- aiohomematic/model/generic/action_select.py +62 -0
- aiohomematic/model/generic/binary_sensor.py +30 -0
- aiohomematic/model/generic/button.py +31 -0
- aiohomematic/model/generic/data_point.py +177 -0
- aiohomematic/model/generic/dummy.py +150 -0
- aiohomematic/model/generic/number.py +76 -0
- aiohomematic/model/generic/select.py +56 -0
- aiohomematic/model/generic/sensor.py +76 -0
- aiohomematic/model/generic/switch.py +54 -0
- aiohomematic/model/generic/text.py +33 -0
- aiohomematic/model/hub/__init__.py +100 -0
- aiohomematic/model/hub/binary_sensor.py +24 -0
- aiohomematic/model/hub/button.py +28 -0
- aiohomematic/model/hub/connectivity.py +190 -0
- aiohomematic/model/hub/data_point.py +342 -0
- aiohomematic/model/hub/hub.py +864 -0
- aiohomematic/model/hub/inbox.py +135 -0
- aiohomematic/model/hub/install_mode.py +393 -0
- aiohomematic/model/hub/metrics.py +208 -0
- aiohomematic/model/hub/number.py +42 -0
- aiohomematic/model/hub/select.py +52 -0
- aiohomematic/model/hub/sensor.py +37 -0
- aiohomematic/model/hub/switch.py +43 -0
- aiohomematic/model/hub/text.py +30 -0
- aiohomematic/model/hub/update.py +221 -0
- aiohomematic/model/support.py +592 -0
- aiohomematic/model/update.py +140 -0
- aiohomematic/model/week_profile.py +1827 -0
- aiohomematic/property_decorators.py +719 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/accept_device_in_inbox.fn +51 -0
- aiohomematic/rega_scripts/create_backup_start.fn +28 -0
- aiohomematic/rega_scripts/create_backup_status.fn +89 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +97 -0
- aiohomematic/rega_scripts/get_backend_info.fn +25 -0
- aiohomematic/rega_scripts/get_inbox_devices.fn +61 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +31 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_service_messages.fn +83 -0
- aiohomematic/rega_scripts/get_system_update_info.fn +39 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +31 -0
- aiohomematic/rega_scripts/set_program_state.fn +17 -0
- aiohomematic/rega_scripts/set_system_variable.fn +19 -0
- aiohomematic/rega_scripts/trigger_firmware_update.fn +67 -0
- aiohomematic/schemas.py +256 -0
- aiohomematic/store/__init__.py +55 -0
- aiohomematic/store/dynamic/__init__.py +43 -0
- aiohomematic/store/dynamic/command.py +250 -0
- aiohomematic/store/dynamic/data.py +175 -0
- aiohomematic/store/dynamic/details.py +187 -0
- aiohomematic/store/dynamic/ping_pong.py +416 -0
- aiohomematic/store/persistent/__init__.py +71 -0
- aiohomematic/store/persistent/base.py +285 -0
- aiohomematic/store/persistent/device.py +233 -0
- aiohomematic/store/persistent/incident.py +380 -0
- aiohomematic/store/persistent/paramset.py +241 -0
- aiohomematic/store/persistent/session.py +556 -0
- aiohomematic/store/serialization.py +150 -0
- aiohomematic/store/storage.py +689 -0
- aiohomematic/store/types.py +526 -0
- aiohomematic/store/visibility/__init__.py +40 -0
- aiohomematic/store/visibility/parser.py +141 -0
- aiohomematic/store/visibility/registry.py +722 -0
- aiohomematic/store/visibility/rules.py +307 -0
- aiohomematic/strings.json +237 -0
- aiohomematic/support.py +706 -0
- aiohomematic/tracing.py +236 -0
- aiohomematic/translations/de.json +237 -0
- aiohomematic/translations/en.json +237 -0
- aiohomematic/type_aliases.py +51 -0
- aiohomematic/validator.py +128 -0
- aiohomematic-2026.1.29.dist-info/METADATA +296 -0
- aiohomematic-2026.1.29.dist-info/RECORD +188 -0
- aiohomematic-2026.1.29.dist-info/WHEEL +5 -0
- aiohomematic-2026.1.29.dist-info/entry_points.txt +2 -0
- aiohomematic-2026.1.29.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2026.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Shared mixins for custom data point implementations.
|
|
5
|
+
|
|
6
|
+
This module provides reusable mixin classes that extract common patterns
|
|
7
|
+
from custom data point implementations to reduce code duplication.
|
|
8
|
+
|
|
9
|
+
Mixins
|
|
10
|
+
------
|
|
11
|
+
- StateChangeTimerMixin: Timer-based state change detection logic
|
|
12
|
+
- OnOffActionMixin: Common on/off action logic with timer support
|
|
13
|
+
- GroupStateMixin: Common group state property pattern
|
|
14
|
+
- PositionMixin: Position conversion logic for covers/blinds
|
|
15
|
+
- BrightnessMixin: Brightness conversion logic for lights/dimmers
|
|
16
|
+
- TimerUnitMixin: Timer unit conversion for lights with on_time/ramp_time
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
-----
|
|
20
|
+
Mixins are designed to be used with CustomDataPoint subclasses through
|
|
21
|
+
multiple inheritance::
|
|
22
|
+
|
|
23
|
+
class CustomDpSwitch(StateChangeTimerMixin, GroupStateMixin, CustomDataPoint):
|
|
24
|
+
_category = DataPointCategory.SWITCH
|
|
25
|
+
...
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from abc import abstractmethod
|
|
31
|
+
from enum import StrEnum
|
|
32
|
+
from typing import TYPE_CHECKING, Any, Protocol, TypedDict, Unpack, runtime_checkable
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from aiohomematic.model.data_point import CallParameterCollector
|
|
36
|
+
from aiohomematic.model.generic import DpAction, DpSwitch
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class StateChangeArg(StrEnum):
|
|
40
|
+
"""Common state change arguments for on/off data points."""
|
|
41
|
+
|
|
42
|
+
OFF = "off"
|
|
43
|
+
ON = "on"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StateChangeArgs(TypedDict, total=False):
|
|
47
|
+
"""Type-safe arguments for is_state_change() method."""
|
|
48
|
+
|
|
49
|
+
# On/Off state (switch, valve)
|
|
50
|
+
on: bool
|
|
51
|
+
off: bool
|
|
52
|
+
|
|
53
|
+
# Light-specific
|
|
54
|
+
brightness: int
|
|
55
|
+
hs_color: tuple[float, float]
|
|
56
|
+
color_temp_kelvin: int
|
|
57
|
+
effect: str
|
|
58
|
+
on_time: float
|
|
59
|
+
ramp_time: float
|
|
60
|
+
|
|
61
|
+
# Climate-specific
|
|
62
|
+
target_temperature: float
|
|
63
|
+
mode: Any # ClimateMode - using Any to avoid circular import
|
|
64
|
+
profile: Any # ClimateProfile - using Any to avoid circular import
|
|
65
|
+
|
|
66
|
+
# Cover-specific
|
|
67
|
+
close: bool
|
|
68
|
+
open: bool
|
|
69
|
+
position: int | float | None
|
|
70
|
+
tilt_close: bool
|
|
71
|
+
tilt_open: bool
|
|
72
|
+
tilt_position: int | float | None
|
|
73
|
+
vent: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@runtime_checkable
|
|
77
|
+
class TimerCapable(Protocol):
|
|
78
|
+
"""Protocol for data points with timer capabilities."""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def timer_on_time(self) -> float | None:
|
|
82
|
+
"""Return the on_time."""
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def timer_on_time_running(self) -> bool:
|
|
86
|
+
"""Return if on_time is running."""
|
|
87
|
+
|
|
88
|
+
def get_and_start_timer(self) -> float | None:
|
|
89
|
+
"""Get and start the timer."""
|
|
90
|
+
|
|
91
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
92
|
+
"""Check if the state changes."""
|
|
93
|
+
|
|
94
|
+
def reset_timer_on_time(self) -> None:
|
|
95
|
+
"""Reset the on_time."""
|
|
96
|
+
|
|
97
|
+
def set_timer_on_time(self, *, on_time: float) -> None:
|
|
98
|
+
"""Set the on_time."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@runtime_checkable
|
|
102
|
+
class ValueCapable(Protocol):
|
|
103
|
+
"""Protocol for data points with value property."""
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def value(self) -> bool | None:
|
|
107
|
+
"""Return the current value."""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class StateChangeTimerMixin:
|
|
111
|
+
"""
|
|
112
|
+
Mixin providing timer-based state change detection.
|
|
113
|
+
|
|
114
|
+
This mixin implements the common state change detection pattern used
|
|
115
|
+
by switch, valve, light, and similar data points that support on_time timers.
|
|
116
|
+
|
|
117
|
+
Provides:
|
|
118
|
+
- is_timer_state_change(): Timer-only state change detection
|
|
119
|
+
- is_state_change_for_on_off(): Full on/off state change detection (requires value property)
|
|
120
|
+
|
|
121
|
+
Requires the class to implement:
|
|
122
|
+
- timer_on_time property (from BaseDataPoint)
|
|
123
|
+
- timer_on_time_running property (from BaseDataPoint)
|
|
124
|
+
- value property (only for is_state_change_for_on_off)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
__slots__ = ()
|
|
128
|
+
|
|
129
|
+
# Declare expected attributes from BaseDataPoint
|
|
130
|
+
timer_on_time: float | None
|
|
131
|
+
timer_on_time_running: bool
|
|
132
|
+
# value is expected for is_state_change_for_on_off but not declared here
|
|
133
|
+
# to avoid interfering with subclasses that don't need it
|
|
134
|
+
|
|
135
|
+
def is_state_change_for_on_off(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Check if the state changes due to on/off kwargs with timer consideration.
|
|
138
|
+
|
|
139
|
+
Requires the subclass to have a `value` property returning bool | None.
|
|
140
|
+
|
|
141
|
+
Returns True if:
|
|
142
|
+
- Timer is currently running
|
|
143
|
+
- Timer on_time is set
|
|
144
|
+
- Turning on when not already on
|
|
145
|
+
- Turning off when not already off
|
|
146
|
+
"""
|
|
147
|
+
if self.is_timer_state_change():
|
|
148
|
+
return True
|
|
149
|
+
value: bool | None = getattr(self, "value", None)
|
|
150
|
+
if kwargs.get(StateChangeArg.ON) is not None and value is not True:
|
|
151
|
+
return True
|
|
152
|
+
return kwargs.get(StateChangeArg.OFF) is not None and value is not False
|
|
153
|
+
|
|
154
|
+
def is_timer_state_change(self) -> bool:
|
|
155
|
+
"""
|
|
156
|
+
Check if the state should change due to timer conditions only.
|
|
157
|
+
|
|
158
|
+
Returns True if:
|
|
159
|
+
- Timer is currently running
|
|
160
|
+
- Timer on_time is set
|
|
161
|
+
|
|
162
|
+
This is useful for data points like lights that have more complex
|
|
163
|
+
on/off logic but still need timer-based state change detection.
|
|
164
|
+
"""
|
|
165
|
+
if self.timer_on_time_running is True:
|
|
166
|
+
return True
|
|
167
|
+
return self.timer_on_time is not None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class OnOffActionMixin:
|
|
171
|
+
"""
|
|
172
|
+
Mixin providing common on/off action implementations.
|
|
173
|
+
|
|
174
|
+
This mixin provides reusable turn_on and turn_off logic that handles
|
|
175
|
+
timer management, state change detection, and value sending.
|
|
176
|
+
|
|
177
|
+
Subclasses must provide:
|
|
178
|
+
- _dp_state: DpSwitch data point
|
|
179
|
+
- _dp_on_time_value: DpAction data point for on_time
|
|
180
|
+
- Timer capability methods (from BaseDataPoint)
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
__slots__ = ()
|
|
184
|
+
|
|
185
|
+
# These are expected to be set by the implementing class
|
|
186
|
+
_dp_state: DpSwitch
|
|
187
|
+
_dp_on_time_value: DpAction
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def get_and_start_timer(self) -> float | None:
|
|
191
|
+
"""Get and start the timer."""
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
195
|
+
"""Check if the state changes due to kwargs."""
|
|
196
|
+
|
|
197
|
+
@abstractmethod
|
|
198
|
+
def reset_timer_on_time(self) -> None:
|
|
199
|
+
"""Reset the on_time."""
|
|
200
|
+
|
|
201
|
+
@abstractmethod
|
|
202
|
+
def set_timer_on_time(self, *, on_time: float) -> None:
|
|
203
|
+
"""Set the on_time."""
|
|
204
|
+
|
|
205
|
+
async def _perform_turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Perform turn off action with timer reset.
|
|
208
|
+
|
|
209
|
+
This is the common implementation for turn_off/close operations.
|
|
210
|
+
"""
|
|
211
|
+
self.reset_timer_on_time()
|
|
212
|
+
if not self.is_state_change(off=True):
|
|
213
|
+
return
|
|
214
|
+
await self._dp_state.turn_off(collector=collector)
|
|
215
|
+
|
|
216
|
+
async def _perform_turn_on(
|
|
217
|
+
self, *, on_time: float | None = None, collector: CallParameterCollector | None = None
|
|
218
|
+
) -> None:
|
|
219
|
+
"""
|
|
220
|
+
Perform turn on action with optional timer.
|
|
221
|
+
|
|
222
|
+
This is the common implementation for turn_on/open operations.
|
|
223
|
+
"""
|
|
224
|
+
if on_time is not None:
|
|
225
|
+
self.set_timer_on_time(on_time=on_time)
|
|
226
|
+
if not self.is_state_change(on=True):
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
if (timer := self.get_and_start_timer()) is not None:
|
|
230
|
+
await self._dp_on_time_value.send_value(value=timer, collector=collector, do_validate=False)
|
|
231
|
+
await self._dp_state.turn_on(collector=collector)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class GroupStateMixin:
|
|
235
|
+
"""
|
|
236
|
+
Mixin for data points that have a group state.
|
|
237
|
+
|
|
238
|
+
Provides common group_value property pattern used by switches,
|
|
239
|
+
valves, and other data points with group state tracking.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
__slots__ = ()
|
|
243
|
+
|
|
244
|
+
# Expected to be set by implementing class
|
|
245
|
+
_dp_group_state: Any # DpBinarySensor
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def group_value(self) -> bool | None:
|
|
249
|
+
"""Return the current group value."""
|
|
250
|
+
value: bool | None = self._dp_group_state.value
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class PositionMixin:
|
|
255
|
+
"""
|
|
256
|
+
Mixin for data points with position values (0-100%).
|
|
257
|
+
|
|
258
|
+
Provides common position conversion logic for covers, blinds,
|
|
259
|
+
and similar data points that work with percentage positions.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
__slots__ = ()
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def level_to_position(level: float | None, *, inverted: bool = False) -> int | None:
|
|
266
|
+
"""
|
|
267
|
+
Convert level (0.0-1.0) to position percentage (0-100).
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
level: Level value between 0.0 and 1.0.
|
|
271
|
+
inverted: If True, invert the position (100 - position).
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Position as integer percentage, or None if level is None.
|
|
275
|
+
|
|
276
|
+
"""
|
|
277
|
+
if level is None:
|
|
278
|
+
return None
|
|
279
|
+
position = int(level * 100)
|
|
280
|
+
return 100 - position if inverted else position
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def position_to_level(position: int, *, inverted: bool = False) -> float:
|
|
284
|
+
"""
|
|
285
|
+
Convert position percentage (0-100) to level (0.0-1.0).
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
position: Position as integer percentage.
|
|
289
|
+
inverted: If True, invert before conversion.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Level value between 0.0 and 1.0.
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
if inverted:
|
|
296
|
+
position = 100 - position
|
|
297
|
+
return position / 100.0
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class BrightnessMixin:
|
|
301
|
+
"""
|
|
302
|
+
Mixin for data points with brightness values (0-255 or 0-100%).
|
|
303
|
+
|
|
304
|
+
Provides common brightness conversion logic for lights and dimmers.
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
__slots__ = ()
|
|
308
|
+
|
|
309
|
+
# Constants for brightness conversion
|
|
310
|
+
_MAX_BRIGHTNESS: int = 255
|
|
311
|
+
_BRIGHTNESS_PCT_MULTIPLIER: int = 100
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def brightness_to_level(brightness: int, *, max_brightness: int = 255) -> float:
|
|
315
|
+
"""
|
|
316
|
+
Convert brightness (0-max_brightness) to level (0.0-1.0).
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
brightness: Brightness value.
|
|
320
|
+
max_brightness: Maximum brightness value (default 255).
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Level value between 0.0 and 1.0.
|
|
324
|
+
|
|
325
|
+
"""
|
|
326
|
+
return brightness / max_brightness
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def level_to_brightness(level: float | None, *, max_brightness: int = 255) -> int:
|
|
330
|
+
"""
|
|
331
|
+
Convert level (0.0-1.0) to brightness (0-max_brightness).
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
level: Level value between 0.0 and 1.0.
|
|
335
|
+
max_brightness: Maximum brightness value (default 255).
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Brightness as integer, or 0 if level is None.
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
if level is None:
|
|
342
|
+
return 0
|
|
343
|
+
return int(level * max_brightness)
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def level_to_brightness_pct(level: float | None) -> int:
|
|
347
|
+
"""
|
|
348
|
+
Convert level (0.0-1.0) to brightness percentage (0-100).
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
level: Level value between 0.0 and 1.0.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Brightness percentage as integer, or 0 if level is None.
|
|
355
|
+
|
|
356
|
+
"""
|
|
357
|
+
if level is None:
|
|
358
|
+
return 0
|
|
359
|
+
return int(level * 100)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class _TimeUnit:
|
|
363
|
+
"""Time unit constants for timer conversion."""
|
|
364
|
+
|
|
365
|
+
SECONDS: str = "S"
|
|
366
|
+
MINUTES: str = "M"
|
|
367
|
+
HOURS: str = "H"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# Marker value indicating timer is not used
|
|
371
|
+
_TIMER_NOT_USED: float = 111600.0
|
|
372
|
+
|
|
373
|
+
# Threshold for time unit conversion (max value before switching units)
|
|
374
|
+
_TIME_UNIT_THRESHOLD: int = 16343
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class TimerUnitMixin:
|
|
378
|
+
"""
|
|
379
|
+
Mixin for lights with time unit conversion for on_time and ramp_time.
|
|
380
|
+
|
|
381
|
+
Provides common timer value setting methods that handle automatic
|
|
382
|
+
unit conversion (seconds -> minutes -> hours) for large time values.
|
|
383
|
+
|
|
384
|
+
Requires the class to have:
|
|
385
|
+
- _dp_on_time_value: DpAction for on_time value
|
|
386
|
+
- _dp_on_time_unit: DpAction for on_time unit
|
|
387
|
+
- _dp_ramp_time_value: DpAction for ramp_time value
|
|
388
|
+
- _dp_ramp_time_unit: DpAction for ramp_time unit
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
__slots__ = ()
|
|
392
|
+
|
|
393
|
+
# Expected to be set by implementing class
|
|
394
|
+
_dp_on_time_value: Any
|
|
395
|
+
_dp_on_time_unit: Any
|
|
396
|
+
_dp_ramp_time_value: Any
|
|
397
|
+
_dp_ramp_time_unit: Any
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def _recalc_unit_timer(*, time: float) -> tuple[float, str]:
|
|
401
|
+
"""
|
|
402
|
+
Recalculate unit and value of timer.
|
|
403
|
+
|
|
404
|
+
Converts large time values to appropriate units:
|
|
405
|
+
- > 16343 seconds -> minutes
|
|
406
|
+
- > 16343 minutes -> hours
|
|
407
|
+
|
|
408
|
+
For the NOT_USED marker (111600), returns HOURS as unit to ensure
|
|
409
|
+
the device interprets the value correctly (111600 hours ≈ 554 days).
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
time: Time value in seconds.
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Tuple of (converted_time, unit) where unit is "S"/"M"/"H".
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
time_unit = _TimeUnit.SECONDS
|
|
419
|
+
if time == _TIMER_NOT_USED:
|
|
420
|
+
return time, _TimeUnit.HOURS
|
|
421
|
+
if time > _TIME_UNIT_THRESHOLD:
|
|
422
|
+
time /= 60
|
|
423
|
+
time_unit = _TimeUnit.MINUTES
|
|
424
|
+
if time > _TIME_UNIT_THRESHOLD:
|
|
425
|
+
time /= 60
|
|
426
|
+
time_unit = _TimeUnit.HOURS
|
|
427
|
+
return time, time_unit
|
|
428
|
+
|
|
429
|
+
async def _set_on_time_value(self, *, on_time: float, collector: Any | None = None) -> None:
|
|
430
|
+
"""Set the on time value with automatic unit conversion."""
|
|
431
|
+
on_time, on_time_unit = self._recalc_unit_timer(time=on_time)
|
|
432
|
+
if on_time_unit:
|
|
433
|
+
await self._dp_on_time_unit.send_value(value=on_time_unit, collector=collector)
|
|
434
|
+
await self._dp_on_time_value.send_value(value=float(on_time), collector=collector)
|
|
435
|
+
|
|
436
|
+
async def _set_ramp_time_off_value(self, *, ramp_time: float, collector: Any | None = None) -> None:
|
|
437
|
+
"""Set the ramp time off value with automatic unit conversion."""
|
|
438
|
+
await self._set_ramp_time_on_value(ramp_time=ramp_time, collector=collector)
|
|
439
|
+
|
|
440
|
+
async def _set_ramp_time_on_value(self, *, ramp_time: float, collector: Any | None = None) -> None:
|
|
441
|
+
"""Set the ramp time on value with automatic unit conversion."""
|
|
442
|
+
ramp_time, ramp_time_unit = self._recalc_unit_timer(time=ramp_time)
|
|
443
|
+
if ramp_time_unit:
|
|
444
|
+
await self._dp_ramp_time_unit.send_value(value=ramp_time_unit, collector=collector)
|
|
445
|
+
await self._dp_ramp_time_value.send_value(value=float(ramp_time), collector=collector)
|