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,135 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""Module for hub inbox sensor."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from slugify import slugify
|
|
12
|
+
|
|
13
|
+
from aiohomematic.const import HUB_ADDRESS, INBOX_SENSOR_NAME, DataPointCategory, HubValueType, InboxDeviceData
|
|
14
|
+
from aiohomematic.interfaces import (
|
|
15
|
+
CentralInfoProtocol,
|
|
16
|
+
ChannelProtocol,
|
|
17
|
+
ConfigProviderProtocol,
|
|
18
|
+
EventBusProviderProtocol,
|
|
19
|
+
EventPublisherProtocol,
|
|
20
|
+
HubSensorDataPointProtocol,
|
|
21
|
+
ParameterVisibilityProviderProtocol,
|
|
22
|
+
ParamsetDescriptionProviderProtocol,
|
|
23
|
+
TaskSchedulerProtocol,
|
|
24
|
+
)
|
|
25
|
+
from aiohomematic.model.data_point import CallbackDataPoint
|
|
26
|
+
from aiohomematic.model.support import HubPathData, PathData, generate_unique_id, get_hub_data_point_name_data
|
|
27
|
+
from aiohomematic.property_decorators import DelegatedProperty, Kind, config_property, state_property
|
|
28
|
+
from aiohomematic.support import PayloadMixin
|
|
29
|
+
|
|
30
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HmInboxSensor(CallbackDataPoint, HubSensorDataPointProtocol, PayloadMixin):
|
|
34
|
+
"""Class for a Homematic inbox sensor."""
|
|
35
|
+
|
|
36
|
+
__slots__ = (
|
|
37
|
+
"_cached_device_count",
|
|
38
|
+
"_devices",
|
|
39
|
+
"_name_data",
|
|
40
|
+
"_state_uncertain",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_category = DataPointCategory.HUB_SENSOR
|
|
44
|
+
_enabled_default = True
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
*,
|
|
49
|
+
config_provider: ConfigProviderProtocol,
|
|
50
|
+
central_info: CentralInfoProtocol,
|
|
51
|
+
event_bus_provider: EventBusProviderProtocol,
|
|
52
|
+
event_publisher: EventPublisherProtocol,
|
|
53
|
+
task_scheduler: TaskSchedulerProtocol,
|
|
54
|
+
paramset_description_provider: ParamsetDescriptionProviderProtocol,
|
|
55
|
+
parameter_visibility_provider: ParameterVisibilityProviderProtocol,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Initialize the data_point."""
|
|
58
|
+
PayloadMixin.__init__(self)
|
|
59
|
+
unique_id: Final = generate_unique_id(
|
|
60
|
+
config_provider=config_provider,
|
|
61
|
+
address=HUB_ADDRESS,
|
|
62
|
+
parameter=slugify(INBOX_SENSOR_NAME),
|
|
63
|
+
)
|
|
64
|
+
self._name_data: Final = get_hub_data_point_name_data(
|
|
65
|
+
channel=None, legacy_name=INBOX_SENSOR_NAME, central_name=central_info.name
|
|
66
|
+
)
|
|
67
|
+
super().__init__(
|
|
68
|
+
unique_id=unique_id,
|
|
69
|
+
central_info=central_info,
|
|
70
|
+
event_bus_provider=event_bus_provider,
|
|
71
|
+
event_publisher=event_publisher,
|
|
72
|
+
task_scheduler=task_scheduler,
|
|
73
|
+
paramset_description_provider=paramset_description_provider,
|
|
74
|
+
parameter_visibility_provider=parameter_visibility_provider,
|
|
75
|
+
)
|
|
76
|
+
self._state_uncertain: bool = True
|
|
77
|
+
self._devices: tuple[InboxDeviceData, ...] = ()
|
|
78
|
+
self._cached_device_count: int = 0
|
|
79
|
+
|
|
80
|
+
available: Final = DelegatedProperty[bool](path="_central_info.available", kind=Kind.STATE)
|
|
81
|
+
devices: Final = DelegatedProperty[tuple[InboxDeviceData, ...]](path="_devices", kind=Kind.STATE)
|
|
82
|
+
enabled_default: Final = DelegatedProperty[bool](path="_enabled_default")
|
|
83
|
+
full_name: Final = DelegatedProperty[str](path="_name_data.full_name")
|
|
84
|
+
name: Final = DelegatedProperty[str](path="_name_data.name", kind=Kind.CONFIG)
|
|
85
|
+
state_uncertain: Final = DelegatedProperty[bool](path="_state_uncertain")
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def channel(self) -> ChannelProtocol | None:
|
|
89
|
+
"""Return the identified channel."""
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def data_type(self) -> HubValueType | None:
|
|
94
|
+
"""Return the data type of the system variable."""
|
|
95
|
+
return HubValueType.INTEGER
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def description(self) -> str | None:
|
|
99
|
+
"""Return data point description."""
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def legacy_name(self) -> str | None:
|
|
104
|
+
"""Return the original name."""
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
@config_property
|
|
108
|
+
def unit(self) -> str | None:
|
|
109
|
+
"""Return the unit of the data_point."""
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@state_property
|
|
113
|
+
def value(self) -> int:
|
|
114
|
+
"""Return the count of inbox devices."""
|
|
115
|
+
return len(self._devices)
|
|
116
|
+
|
|
117
|
+
def update_data(self, *, devices: tuple[InboxDeviceData, ...], write_at: datetime) -> None:
|
|
118
|
+
"""Update the data point with new inbox devices."""
|
|
119
|
+
new_count = len(devices)
|
|
120
|
+
if self._cached_device_count != new_count or self._devices != devices:
|
|
121
|
+
self._cached_device_count = new_count
|
|
122
|
+
self._devices = devices
|
|
123
|
+
self._set_modified_at(modified_at=write_at)
|
|
124
|
+
else:
|
|
125
|
+
self._set_refreshed_at(refreshed_at=write_at)
|
|
126
|
+
self._state_uncertain = False
|
|
127
|
+
self.publish_data_point_updated_event()
|
|
128
|
+
|
|
129
|
+
def _get_path_data(self) -> PathData:
|
|
130
|
+
"""Return the path data of the data_point."""
|
|
131
|
+
return HubPathData(name=slugify(INBOX_SENSOR_NAME))
|
|
132
|
+
|
|
133
|
+
def _get_signature(self) -> str:
|
|
134
|
+
"""Return the signature of the data_point."""
|
|
135
|
+
return f"{self._category}/{self.name}"
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""Module for install mode hub data points."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Final, NamedTuple
|
|
11
|
+
|
|
12
|
+
from slugify import slugify
|
|
13
|
+
|
|
14
|
+
from aiohomematic.const import INIT_DATETIME, INSTALL_MODE_ADDRESS, DataPointCategory, HubValueType, InstallModeData
|
|
15
|
+
from aiohomematic.decorators import inspector
|
|
16
|
+
from aiohomematic.interfaces import (
|
|
17
|
+
CentralInfoProtocol,
|
|
18
|
+
ChannelLookupProtocol,
|
|
19
|
+
ChannelProtocol,
|
|
20
|
+
ConfigProviderProtocol,
|
|
21
|
+
EventBusProviderProtocol,
|
|
22
|
+
EventPublisherProtocol,
|
|
23
|
+
GenericHubDataPointProtocol,
|
|
24
|
+
GenericInstallModeDataPointProtocol,
|
|
25
|
+
ParameterVisibilityProviderProtocol,
|
|
26
|
+
ParamsetDescriptionProviderProtocol,
|
|
27
|
+
PrimaryClientProviderProtocol,
|
|
28
|
+
TaskSchedulerProtocol,
|
|
29
|
+
)
|
|
30
|
+
from aiohomematic.model.data_point import CallbackDataPoint
|
|
31
|
+
from aiohomematic.model.support import HubPathData, generate_unique_id, get_hub_data_point_name_data
|
|
32
|
+
from aiohomematic.property_decorators import DelegatedProperty, Kind, config_property, state_property
|
|
33
|
+
from aiohomematic.support import PayloadMixin
|
|
34
|
+
|
|
35
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_SYNC_INTERVAL: Final = 10 # Sync with backend every 10 seconds
|
|
38
|
+
_COUNTDOWN_UPDATE_INTERVAL: Final = 1 # Update countdown every second
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class InstallModeDpType(NamedTuple):
|
|
42
|
+
"""Tuple for install mode data points."""
|
|
43
|
+
|
|
44
|
+
button: InstallModeDpButton
|
|
45
|
+
sensor: InstallModeDpSensor
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _BaseInstallModeDataPoint(CallbackDataPoint, GenericHubDataPointProtocol, PayloadMixin):
|
|
49
|
+
"""Base class for install mode data points."""
|
|
50
|
+
|
|
51
|
+
__slots__ = (
|
|
52
|
+
"_channel",
|
|
53
|
+
"_name_data",
|
|
54
|
+
"_primary_client_provider",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
data: InstallModeData,
|
|
61
|
+
config_provider: ConfigProviderProtocol,
|
|
62
|
+
central_info: CentralInfoProtocol,
|
|
63
|
+
event_bus_provider: EventBusProviderProtocol,
|
|
64
|
+
event_publisher: EventPublisherProtocol,
|
|
65
|
+
task_scheduler: TaskSchedulerProtocol,
|
|
66
|
+
paramset_description_provider: ParamsetDescriptionProviderProtocol,
|
|
67
|
+
parameter_visibility_provider: ParameterVisibilityProviderProtocol,
|
|
68
|
+
channel_lookup: ChannelLookupProtocol,
|
|
69
|
+
primary_client_provider: PrimaryClientProviderProtocol,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize the data_point."""
|
|
72
|
+
PayloadMixin.__init__(self)
|
|
73
|
+
unique_id: Final = generate_unique_id(
|
|
74
|
+
config_provider=config_provider,
|
|
75
|
+
address=INSTALL_MODE_ADDRESS,
|
|
76
|
+
parameter=slugify(data.name),
|
|
77
|
+
)
|
|
78
|
+
self._channel = channel_lookup.identify_channel(text=data.name)
|
|
79
|
+
self._name_data: Final = get_hub_data_point_name_data(
|
|
80
|
+
channel=self._channel, legacy_name=f"{INSTALL_MODE_ADDRESS}_{data.name}", central_name=central_info.name
|
|
81
|
+
)
|
|
82
|
+
super().__init__(
|
|
83
|
+
unique_id=unique_id,
|
|
84
|
+
central_info=central_info,
|
|
85
|
+
event_bus_provider=event_bus_provider,
|
|
86
|
+
event_publisher=event_publisher,
|
|
87
|
+
task_scheduler=task_scheduler,
|
|
88
|
+
paramset_description_provider=paramset_description_provider,
|
|
89
|
+
parameter_visibility_provider=parameter_visibility_provider,
|
|
90
|
+
)
|
|
91
|
+
self._primary_client_provider: Final = primary_client_provider
|
|
92
|
+
|
|
93
|
+
channel: Final = DelegatedProperty[ChannelProtocol | None](path="_channel")
|
|
94
|
+
full_name: Final = DelegatedProperty[str](path="_name_data.full_name")
|
|
95
|
+
name: Final = DelegatedProperty[str](path="_name_data.name", kind=Kind.CONFIG)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def enabled_default(self) -> bool:
|
|
99
|
+
"""Return if the data_point should be enabled."""
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def legacy_name(self) -> str | None:
|
|
104
|
+
"""Return the original name."""
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def state_uncertain(self) -> bool:
|
|
109
|
+
"""Return if the state is uncertain."""
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
@config_property
|
|
113
|
+
def description(self) -> str | None:
|
|
114
|
+
"""Return description."""
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
@state_property
|
|
118
|
+
def available(self) -> bool:
|
|
119
|
+
"""Return the availability of the device."""
|
|
120
|
+
if client := self._primary_client_provider.primary_client:
|
|
121
|
+
return client.capabilities.install_mode and self._central_info.available
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def _get_path_data(self) -> HubPathData:
|
|
125
|
+
"""Return the path data of the data_point."""
|
|
126
|
+
return HubPathData(name=self._name_data.name)
|
|
127
|
+
|
|
128
|
+
def _get_signature(self) -> str:
|
|
129
|
+
"""Return the signature of the data_point."""
|
|
130
|
+
return f"{self._category}/{self.name}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class InstallModeDpSensor(GenericInstallModeDataPointProtocol, _BaseInstallModeDataPoint):
|
|
134
|
+
"""Sensor showing remaining install mode time."""
|
|
135
|
+
|
|
136
|
+
__slots__ = (
|
|
137
|
+
"_countdown_end",
|
|
138
|
+
"_countdown_task",
|
|
139
|
+
"_sync_task",
|
|
140
|
+
"_task_lock",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
_category = DataPointCategory.HUB_SENSOR
|
|
144
|
+
|
|
145
|
+
def __init__(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
data: InstallModeData,
|
|
149
|
+
config_provider: ConfigProviderProtocol,
|
|
150
|
+
central_info: CentralInfoProtocol,
|
|
151
|
+
event_bus_provider: EventBusProviderProtocol,
|
|
152
|
+
event_publisher: EventPublisherProtocol,
|
|
153
|
+
task_scheduler: TaskSchedulerProtocol,
|
|
154
|
+
paramset_description_provider: ParamsetDescriptionProviderProtocol,
|
|
155
|
+
parameter_visibility_provider: ParameterVisibilityProviderProtocol,
|
|
156
|
+
channel_lookup: ChannelLookupProtocol,
|
|
157
|
+
primary_client_provider: PrimaryClientProviderProtocol,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Initialize the sensor."""
|
|
160
|
+
super().__init__(
|
|
161
|
+
config_provider=config_provider,
|
|
162
|
+
central_info=central_info,
|
|
163
|
+
event_bus_provider=event_bus_provider,
|
|
164
|
+
event_publisher=event_publisher,
|
|
165
|
+
task_scheduler=task_scheduler,
|
|
166
|
+
paramset_description_provider=paramset_description_provider,
|
|
167
|
+
parameter_visibility_provider=parameter_visibility_provider,
|
|
168
|
+
channel_lookup=channel_lookup,
|
|
169
|
+
primary_client_provider=primary_client_provider,
|
|
170
|
+
data=data,
|
|
171
|
+
)
|
|
172
|
+
self._countdown_end: datetime = INIT_DATETIME
|
|
173
|
+
self._countdown_task: asyncio.Task[None] | None = None
|
|
174
|
+
self._sync_task: asyncio.Task[None] | None = None
|
|
175
|
+
self._task_lock: Final = asyncio.Lock()
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def data_type(self) -> HubValueType | None:
|
|
179
|
+
"""Return the data type of the system variable."""
|
|
180
|
+
return HubValueType.INTEGER
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def is_active(self) -> bool:
|
|
184
|
+
"""Return if install mode is active."""
|
|
185
|
+
return self.value > 0
|
|
186
|
+
|
|
187
|
+
@config_property
|
|
188
|
+
def unit(self) -> str | None:
|
|
189
|
+
"""Return the unit of the data_point."""
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
@state_property
|
|
193
|
+
def value(self) -> int:
|
|
194
|
+
"""Return remaining seconds."""
|
|
195
|
+
if self._countdown_end <= datetime.now():
|
|
196
|
+
return 0
|
|
197
|
+
return max(0, int((self._countdown_end - datetime.now()).total_seconds()))
|
|
198
|
+
|
|
199
|
+
def start_countdown(self, *, seconds: int) -> None:
|
|
200
|
+
"""Start local countdown."""
|
|
201
|
+
self._countdown_end = datetime.now() + timedelta(seconds=seconds)
|
|
202
|
+
self._task_scheduler.create_task(
|
|
203
|
+
target=self._start_tasks_locked(),
|
|
204
|
+
name="install_mode_start_tasks",
|
|
205
|
+
)
|
|
206
|
+
self.publish_data_point_updated_event()
|
|
207
|
+
|
|
208
|
+
def stop_countdown(self) -> None:
|
|
209
|
+
"""Stop countdown."""
|
|
210
|
+
self._countdown_end = INIT_DATETIME
|
|
211
|
+
self._task_scheduler.create_task(
|
|
212
|
+
target=self._stop_tasks_locked(),
|
|
213
|
+
name="install_mode_stop_tasks",
|
|
214
|
+
)
|
|
215
|
+
self.publish_data_point_updated_event()
|
|
216
|
+
|
|
217
|
+
def sync_from_backend(self, *, remaining_seconds: int) -> None:
|
|
218
|
+
"""Sync countdown from backend value."""
|
|
219
|
+
if remaining_seconds <= 0:
|
|
220
|
+
self.stop_countdown()
|
|
221
|
+
else:
|
|
222
|
+
# Only resync if significant drift (>3 seconds)
|
|
223
|
+
if abs(self.value - remaining_seconds) > 3:
|
|
224
|
+
self._countdown_end = datetime.now() + timedelta(seconds=remaining_seconds)
|
|
225
|
+
self._task_scheduler.create_task(
|
|
226
|
+
target=self._ensure_tasks_running_locked(),
|
|
227
|
+
name="install_mode_ensure_tasks",
|
|
228
|
+
)
|
|
229
|
+
self.publish_data_point_updated_event()
|
|
230
|
+
|
|
231
|
+
async def _backend_sync_loop(self) -> None:
|
|
232
|
+
"""Periodically sync with backend."""
|
|
233
|
+
try:
|
|
234
|
+
while self.is_active:
|
|
235
|
+
await asyncio.sleep(_SYNC_INTERVAL)
|
|
236
|
+
if client := self._primary_client_provider.primary_client:
|
|
237
|
+
if (backend_remaining := await client.get_install_mode()) == 0:
|
|
238
|
+
self.stop_countdown()
|
|
239
|
+
break
|
|
240
|
+
# Resync if significant drift
|
|
241
|
+
if abs(self.value - backend_remaining) > 3:
|
|
242
|
+
self._countdown_end = datetime.now() + timedelta(seconds=backend_remaining)
|
|
243
|
+
self.publish_data_point_updated_event()
|
|
244
|
+
except asyncio.CancelledError:
|
|
245
|
+
raise
|
|
246
|
+
except Exception:
|
|
247
|
+
_LOGGER.exception("INSTALL_MODE: Backend sync loop failed") # i18n-log: ignore
|
|
248
|
+
self.stop_countdown()
|
|
249
|
+
|
|
250
|
+
async def _countdown_update_loop(self) -> None:
|
|
251
|
+
"""Update countdown value every second."""
|
|
252
|
+
try:
|
|
253
|
+
while self.is_active:
|
|
254
|
+
await asyncio.sleep(_COUNTDOWN_UPDATE_INTERVAL)
|
|
255
|
+
if self.value <= 0:
|
|
256
|
+
self.stop_countdown()
|
|
257
|
+
break
|
|
258
|
+
self.publish_data_point_updated_event()
|
|
259
|
+
except asyncio.CancelledError:
|
|
260
|
+
raise
|
|
261
|
+
except Exception:
|
|
262
|
+
_LOGGER.exception("INSTALL_MODE: Countdown update loop failed") # i18n-log: ignore
|
|
263
|
+
self.stop_countdown()
|
|
264
|
+
|
|
265
|
+
async def _ensure_tasks_running_locked(self) -> None:
|
|
266
|
+
"""Ensure tasks are running with lock protection."""
|
|
267
|
+
async with self._task_lock:
|
|
268
|
+
if not self._countdown_task or self._countdown_task.done():
|
|
269
|
+
self._countdown_task = self._task_scheduler.create_task(
|
|
270
|
+
target=self._countdown_update_loop(),
|
|
271
|
+
name="install_mode_countdown",
|
|
272
|
+
)
|
|
273
|
+
if not self._sync_task or self._sync_task.done():
|
|
274
|
+
self._sync_task = self._task_scheduler.create_task(
|
|
275
|
+
target=self._backend_sync_loop(),
|
|
276
|
+
name="install_mode_sync",
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
async def _start_tasks_locked(self) -> None:
|
|
280
|
+
"""Start all tasks with lock protection."""
|
|
281
|
+
async with self._task_lock:
|
|
282
|
+
self._stop_countdown_task_unlocked()
|
|
283
|
+
self._stop_sync_task_unlocked()
|
|
284
|
+
self._countdown_task = self._task_scheduler.create_task(
|
|
285
|
+
target=self._countdown_update_loop(),
|
|
286
|
+
name="install_mode_countdown",
|
|
287
|
+
)
|
|
288
|
+
self._sync_task = self._task_scheduler.create_task(
|
|
289
|
+
target=self._backend_sync_loop(),
|
|
290
|
+
name="install_mode_sync",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _stop_countdown_task_unlocked(self) -> None:
|
|
294
|
+
"""Stop countdown task without lock. Must be called with _task_lock held."""
|
|
295
|
+
if self._countdown_task and not self._countdown_task.done():
|
|
296
|
+
self._countdown_task.cancel()
|
|
297
|
+
self._countdown_task = None
|
|
298
|
+
|
|
299
|
+
def _stop_sync_task_unlocked(self) -> None:
|
|
300
|
+
"""Stop sync task without lock. Must be called with _task_lock held."""
|
|
301
|
+
if self._sync_task and not self._sync_task.done():
|
|
302
|
+
self._sync_task.cancel()
|
|
303
|
+
self._sync_task = None
|
|
304
|
+
|
|
305
|
+
async def _stop_tasks_locked(self) -> None:
|
|
306
|
+
"""Stop all tasks with lock protection."""
|
|
307
|
+
async with self._task_lock:
|
|
308
|
+
self._stop_countdown_task_unlocked()
|
|
309
|
+
self._stop_sync_task_unlocked()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class InstallModeDpButton(_BaseInstallModeDataPoint):
|
|
313
|
+
"""Button to activate/deactivate install mode."""
|
|
314
|
+
|
|
315
|
+
__slots__ = ("_sensor",)
|
|
316
|
+
|
|
317
|
+
_category = DataPointCategory.HUB_BUTTON
|
|
318
|
+
|
|
319
|
+
def __init__(
|
|
320
|
+
self,
|
|
321
|
+
*,
|
|
322
|
+
data: InstallModeData,
|
|
323
|
+
sensor: InstallModeDpSensor,
|
|
324
|
+
config_provider: ConfigProviderProtocol,
|
|
325
|
+
central_info: CentralInfoProtocol,
|
|
326
|
+
event_bus_provider: EventBusProviderProtocol,
|
|
327
|
+
event_publisher: EventPublisherProtocol,
|
|
328
|
+
task_scheduler: TaskSchedulerProtocol,
|
|
329
|
+
paramset_description_provider: ParamsetDescriptionProviderProtocol,
|
|
330
|
+
parameter_visibility_provider: ParameterVisibilityProviderProtocol,
|
|
331
|
+
channel_lookup: ChannelLookupProtocol,
|
|
332
|
+
primary_client_provider: PrimaryClientProviderProtocol,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Initialize the button."""
|
|
335
|
+
super().__init__(
|
|
336
|
+
data=data,
|
|
337
|
+
config_provider=config_provider,
|
|
338
|
+
central_info=central_info,
|
|
339
|
+
event_bus_provider=event_bus_provider,
|
|
340
|
+
event_publisher=event_publisher,
|
|
341
|
+
task_scheduler=task_scheduler,
|
|
342
|
+
paramset_description_provider=paramset_description_provider,
|
|
343
|
+
parameter_visibility_provider=parameter_visibility_provider,
|
|
344
|
+
channel_lookup=channel_lookup,
|
|
345
|
+
primary_client_provider=primary_client_provider,
|
|
346
|
+
)
|
|
347
|
+
self._sensor: Final = sensor
|
|
348
|
+
|
|
349
|
+
sensor: Final = DelegatedProperty[GenericInstallModeDataPointProtocol](path="_sensor")
|
|
350
|
+
|
|
351
|
+
@inspector
|
|
352
|
+
async def activate(
|
|
353
|
+
self,
|
|
354
|
+
*,
|
|
355
|
+
time: int = 60,
|
|
356
|
+
device_address: str | None = None,
|
|
357
|
+
) -> bool:
|
|
358
|
+
"""
|
|
359
|
+
Activate install mode.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
time: Duration in seconds (default 60).
|
|
363
|
+
device_address: Optional device address to limit pairing.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
True if successful.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
if (client := self._primary_client_provider.primary_client) and await client.set_install_mode(
|
|
370
|
+
on=True, time=time, device_address=device_address
|
|
371
|
+
):
|
|
372
|
+
self._sensor.start_countdown(seconds=time)
|
|
373
|
+
return True
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
@inspector
|
|
377
|
+
async def deactivate(self) -> bool:
|
|
378
|
+
"""
|
|
379
|
+
Deactivate install mode.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
True if successful.
|
|
383
|
+
|
|
384
|
+
"""
|
|
385
|
+
if (client := self._primary_client_provider.primary_client) and await client.set_install_mode(on=False):
|
|
386
|
+
self._sensor.stop_countdown()
|
|
387
|
+
return True
|
|
388
|
+
return False
|
|
389
|
+
|
|
390
|
+
@inspector
|
|
391
|
+
async def press(self) -> None:
|
|
392
|
+
"""Activate install mode with default settings."""
|
|
393
|
+
await self.activate()
|