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,462 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Custom siren data points for alarm and notification devices.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from abc import abstractmethod
|
|
12
|
+
import contextlib
|
|
13
|
+
from enum import StrEnum
|
|
14
|
+
from typing import Final, TypedDict, Unpack
|
|
15
|
+
|
|
16
|
+
from aiohomematic import i18n
|
|
17
|
+
from aiohomematic.const import DataPointCategory, DeviceProfile, Field
|
|
18
|
+
from aiohomematic.exceptions import ValidationException
|
|
19
|
+
from aiohomematic.model.custom.capabilities.siren import SMOKE_SENSOR_SIREN_CAPABILITIES, SirenCapabilities
|
|
20
|
+
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
21
|
+
from aiohomematic.model.custom.field import DataPointField
|
|
22
|
+
from aiohomematic.model.custom.mixins import TimerUnitMixin
|
|
23
|
+
from aiohomematic.model.custom.registry import DeviceProfileRegistry
|
|
24
|
+
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
25
|
+
from aiohomematic.model.generic import DpAction, DpActionSelect, DpBinarySensor, DpSelect, DpSensor
|
|
26
|
+
from aiohomematic.property_decorators import DelegatedProperty, Kind, state_property
|
|
27
|
+
|
|
28
|
+
_SMOKE_DETECTOR_ALARM_STATUS_IDLE_OFF: Final = "IDLE_OFF"
|
|
29
|
+
|
|
30
|
+
# Activity states indicating playback is active
|
|
31
|
+
_ACTIVITY_STATES_ACTIVE: Final[frozenset[str]] = frozenset({"UP", "DOWN"})
|
|
32
|
+
|
|
33
|
+
# Repetitions constants
|
|
34
|
+
_NO_REPETITION: Final = "NO_REPETITION"
|
|
35
|
+
_INFINITE_REPETITIONS: Final = "INFINITE_REPETITIONS"
|
|
36
|
+
_MAX_REPETITIONS: Final = 18
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _convert_repetitions(*, repetitions: int | None) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Convert repetitions count to REPETITIONS VALUE_LIST value.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repetitions: Number of repetitions (0=none, 1-18=count, -1=infinite, None=none).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
VALUE_LIST string (NO_REPETITION, REPETITIONS_001-018, or INFINITE_REPETITIONS).
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If repetitions is outside valid range (-1 to 18).
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
if repetitions is None or repetitions == 0:
|
|
54
|
+
return _NO_REPETITION
|
|
55
|
+
|
|
56
|
+
if repetitions == -1:
|
|
57
|
+
return _INFINITE_REPETITIONS
|
|
58
|
+
|
|
59
|
+
if repetitions < -1 or repetitions > _MAX_REPETITIONS:
|
|
60
|
+
msg = f"Repetitions must be -1 (infinite), 0 (none), or 1-{_MAX_REPETITIONS}, got {repetitions}"
|
|
61
|
+
raise ValueError(msg)
|
|
62
|
+
|
|
63
|
+
return f"REPETITIONS_{repetitions:03d}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _SirenCommand(StrEnum):
|
|
67
|
+
"""Enum with siren commands."""
|
|
68
|
+
|
|
69
|
+
OFF = "INTRUSION_ALARM_OFF"
|
|
70
|
+
ON = "INTRUSION_ALARM"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SirenOnArgs(TypedDict, total=False):
|
|
74
|
+
"""Matcher for the siren arguments."""
|
|
75
|
+
|
|
76
|
+
acoustic_alarm: str
|
|
77
|
+
optical_alarm: str
|
|
78
|
+
duration: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PlaySoundArgs(TypedDict, total=False):
|
|
82
|
+
"""Arguments for play_sound method (comparable to SirenOnArgs)."""
|
|
83
|
+
|
|
84
|
+
soundfile: str | int # Soundfile from available_soundfiles or index (1-189)
|
|
85
|
+
volume: float # Volume level 0.0-1.0 (default: 0.5)
|
|
86
|
+
on_time: float # Duration in seconds (auto unit conversion via TimerUnitMixin)
|
|
87
|
+
ramp_time: float # Ramp time in seconds (auto unit conversion via TimerUnitMixin)
|
|
88
|
+
repetitions: int # 0=none, 1-18=count, -1=infinite (converted to VALUE_LIST entry)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BaseCustomDpSiren(CustomDataPoint):
|
|
92
|
+
"""Class for Homematic siren data point."""
|
|
93
|
+
|
|
94
|
+
__slots__ = ("_capabilities",)
|
|
95
|
+
|
|
96
|
+
_category = DataPointCategory.SIREN
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def capabilities(self) -> SirenCapabilities:
|
|
100
|
+
"""Return the siren capabilities."""
|
|
101
|
+
if (caps := getattr(self, "_capabilities", None)) is None:
|
|
102
|
+
caps = self._compute_capabilities()
|
|
103
|
+
object.__setattr__(self, "_capabilities", caps)
|
|
104
|
+
return caps
|
|
105
|
+
|
|
106
|
+
@state_property
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def available_lights(self) -> tuple[str, ...] | None:
|
|
109
|
+
"""Return available lights."""
|
|
110
|
+
|
|
111
|
+
@state_property
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def available_tones(self) -> tuple[str, ...] | None:
|
|
114
|
+
"""Return available tones."""
|
|
115
|
+
|
|
116
|
+
@state_property
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def is_on(self) -> bool:
|
|
119
|
+
"""Return true if siren is on."""
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
@bind_collector
|
|
123
|
+
async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
124
|
+
"""Turn the device off."""
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
@bind_collector
|
|
128
|
+
async def turn_on(
|
|
129
|
+
self,
|
|
130
|
+
*,
|
|
131
|
+
collector: CallParameterCollector | None = None,
|
|
132
|
+
**kwargs: Unpack[SirenOnArgs],
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Turn the device on."""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _compute_capabilities(self) -> SirenCapabilities:
|
|
138
|
+
"""Compute static capabilities. Implemented by subclasses."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CustomDpIpSiren(BaseCustomDpSiren):
|
|
142
|
+
"""Class for HomematicIP siren data point."""
|
|
143
|
+
|
|
144
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
145
|
+
|
|
146
|
+
# Declarative data point field definitions
|
|
147
|
+
_dp_acoustic_alarm_active: Final = DataPointField(field=Field.ACOUSTIC_ALARM_ACTIVE, dpt=DpBinarySensor)
|
|
148
|
+
_dp_acoustic_alarm_selection: Final = DataPointField(field=Field.ACOUSTIC_ALARM_SELECTION, dpt=DpActionSelect)
|
|
149
|
+
_dp_duration: Final = DataPointField(field=Field.DURATION, dpt=DpAction)
|
|
150
|
+
_dp_duration_unit: Final = DataPointField(field=Field.DURATION_UNIT, dpt=DpActionSelect)
|
|
151
|
+
_dp_optical_alarm_active: Final = DataPointField(field=Field.OPTICAL_ALARM_ACTIVE, dpt=DpBinarySensor)
|
|
152
|
+
_dp_optical_alarm_selection: Final = DataPointField(field=Field.OPTICAL_ALARM_SELECTION, dpt=DpActionSelect)
|
|
153
|
+
|
|
154
|
+
available_lights: Final = DelegatedProperty[tuple[str, ...] | None](
|
|
155
|
+
path="_dp_optical_alarm_selection.values", kind=Kind.STATE
|
|
156
|
+
)
|
|
157
|
+
available_tones: Final = DelegatedProperty[tuple[str, ...] | None](
|
|
158
|
+
path="_dp_acoustic_alarm_selection.values", kind=Kind.STATE
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@state_property
|
|
162
|
+
def is_on(self) -> bool:
|
|
163
|
+
"""Return true if siren is on."""
|
|
164
|
+
return self._dp_acoustic_alarm_active.value is True or self._dp_optical_alarm_active.value is True
|
|
165
|
+
|
|
166
|
+
@bind_collector
|
|
167
|
+
async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
168
|
+
"""Turn the device off."""
|
|
169
|
+
if (acoustic_default := self._dp_acoustic_alarm_selection.default) is not None:
|
|
170
|
+
await self._dp_acoustic_alarm_selection.send_value(value=acoustic_default, collector=collector)
|
|
171
|
+
if (optical_default := self._dp_optical_alarm_selection.default) is not None:
|
|
172
|
+
await self._dp_optical_alarm_selection.send_value(value=optical_default, collector=collector)
|
|
173
|
+
if (duration_unit_default := self._dp_duration_unit.default) is not None:
|
|
174
|
+
await self._dp_duration_unit.send_value(value=duration_unit_default, collector=collector)
|
|
175
|
+
await self._dp_duration.send_value(value=self._dp_duration.default, collector=collector)
|
|
176
|
+
|
|
177
|
+
@bind_collector
|
|
178
|
+
async def turn_on(
|
|
179
|
+
self,
|
|
180
|
+
*,
|
|
181
|
+
collector: CallParameterCollector | None = None,
|
|
182
|
+
**kwargs: Unpack[SirenOnArgs],
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Turn the device on."""
|
|
185
|
+
acoustic_alarm = (
|
|
186
|
+
kwargs.get("acoustic_alarm")
|
|
187
|
+
or self._dp_acoustic_alarm_selection.value
|
|
188
|
+
or self._dp_acoustic_alarm_selection.default
|
|
189
|
+
)
|
|
190
|
+
if self.available_tones and acoustic_alarm and acoustic_alarm not in self.available_tones:
|
|
191
|
+
raise ValidationException(
|
|
192
|
+
i18n.tr(
|
|
193
|
+
key="exception.model.custom.siren.invalid_tone",
|
|
194
|
+
full_name=self.full_name,
|
|
195
|
+
value=acoustic_alarm,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
optical_alarm = (
|
|
200
|
+
kwargs.get("optical_alarm")
|
|
201
|
+
or self._dp_optical_alarm_selection.value
|
|
202
|
+
or self._dp_optical_alarm_selection.default
|
|
203
|
+
)
|
|
204
|
+
if self.available_lights and optical_alarm and optical_alarm not in self.available_lights:
|
|
205
|
+
raise ValidationException(
|
|
206
|
+
i18n.tr(
|
|
207
|
+
key="exception.model.custom.siren.invalid_light",
|
|
208
|
+
full_name=self.full_name,
|
|
209
|
+
value=optical_alarm,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if acoustic_alarm is not None:
|
|
214
|
+
await self._dp_acoustic_alarm_selection.send_value(value=acoustic_alarm, collector=collector)
|
|
215
|
+
if optical_alarm is not None:
|
|
216
|
+
await self._dp_optical_alarm_selection.send_value(value=optical_alarm, collector=collector)
|
|
217
|
+
if (duration_unit_default := self._dp_duration_unit.default) is not None:
|
|
218
|
+
await self._dp_duration_unit.send_value(value=duration_unit_default, collector=collector)
|
|
219
|
+
duration = kwargs.get("duration") or self._dp_duration.default
|
|
220
|
+
await self._dp_duration.send_value(value=duration, collector=collector)
|
|
221
|
+
|
|
222
|
+
def _compute_capabilities(self) -> SirenCapabilities:
|
|
223
|
+
"""Compute static capabilities based on available DataPoints."""
|
|
224
|
+
return SirenCapabilities(
|
|
225
|
+
duration=True,
|
|
226
|
+
lights=self.available_lights is not None,
|
|
227
|
+
tones=self.available_tones is not None,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class CustomDpIpSirenSmoke(BaseCustomDpSiren):
|
|
232
|
+
"""Class for HomematicIP siren smoke data point."""
|
|
233
|
+
|
|
234
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
235
|
+
|
|
236
|
+
# Declarative data point field definitions
|
|
237
|
+
_dp_smoke_detector_alarm_status: Final = DataPointField(
|
|
238
|
+
field=Field.SMOKE_DETECTOR_ALARM_STATUS, dpt=DpSensor[str | None]
|
|
239
|
+
)
|
|
240
|
+
_dp_smoke_detector_command: Final = DataPointField(field=Field.SMOKE_DETECTOR_COMMAND, dpt=DpActionSelect)
|
|
241
|
+
|
|
242
|
+
@state_property
|
|
243
|
+
def available_lights(self) -> tuple[str, ...] | None:
|
|
244
|
+
"""Return available lights."""
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
@state_property
|
|
248
|
+
def available_tones(self) -> tuple[str, ...] | None:
|
|
249
|
+
"""Return available tones."""
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
@state_property
|
|
253
|
+
def is_on(self) -> bool:
|
|
254
|
+
"""Return true if siren is on."""
|
|
255
|
+
if not self._dp_smoke_detector_alarm_status.value:
|
|
256
|
+
return False
|
|
257
|
+
return bool(self._dp_smoke_detector_alarm_status.value != _SMOKE_DETECTOR_ALARM_STATUS_IDLE_OFF)
|
|
258
|
+
|
|
259
|
+
@bind_collector
|
|
260
|
+
async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
261
|
+
"""Turn the device off."""
|
|
262
|
+
await self._dp_smoke_detector_command.send_value(value=_SirenCommand.OFF, collector=collector)
|
|
263
|
+
|
|
264
|
+
@bind_collector
|
|
265
|
+
async def turn_on(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
collector: CallParameterCollector | None = None,
|
|
269
|
+
**kwargs: Unpack[SirenOnArgs],
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Turn the device on."""
|
|
272
|
+
await self._dp_smoke_detector_command.send_value(value=_SirenCommand.ON, collector=collector)
|
|
273
|
+
|
|
274
|
+
def _compute_capabilities(self) -> SirenCapabilities:
|
|
275
|
+
"""Compute static capabilities. Smoke sensor siren has no configurable options."""
|
|
276
|
+
return SMOKE_SENSOR_SIREN_CAPABILITIES
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class CustomDpSoundPlayer(TimerUnitMixin, BaseCustomDpSiren):
|
|
280
|
+
"""Class for HomematicIP sound player data point (HmIP-MP3P channel 2)."""
|
|
281
|
+
|
|
282
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
283
|
+
|
|
284
|
+
# Declarative data point field definitions for sound channel
|
|
285
|
+
# Map on_time to DURATION_VALUE/UNIT for TimerUnitMixin compatibility (no Final for overrides)
|
|
286
|
+
_dp_level: Final = DataPointField(field=Field.LEVEL, dpt=DpAction)
|
|
287
|
+
_dp_on_time_value = DataPointField(field=Field.DURATION_VALUE, dpt=DpAction)
|
|
288
|
+
_dp_on_time_unit = DataPointField(field=Field.DURATION_UNIT, dpt=DpActionSelect)
|
|
289
|
+
_dp_ramp_time_value = DataPointField(field=Field.RAMP_TIME_VALUE, dpt=DpAction)
|
|
290
|
+
_dp_ramp_time_unit = DataPointField(field=Field.RAMP_TIME_UNIT, dpt=DpActionSelect)
|
|
291
|
+
_dp_soundfile: Final = DataPointField(field=Field.SOUNDFILE, dpt=DpSelect)
|
|
292
|
+
_dp_repetitions: Final = DataPointField(field=Field.REPETITIONS, dpt=DpActionSelect)
|
|
293
|
+
_dp_direction: Final = DataPointField(field=Field.DIRECTION, dpt=DpSensor[str | None])
|
|
294
|
+
|
|
295
|
+
# Expose available options via DelegatedProperty (from ActionSelect VALUE_LISTs)
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _convert_soundfile_index(index: int) -> str:
|
|
298
|
+
"""Convert integer index to soundfile name."""
|
|
299
|
+
if index < 1 or index > 189:
|
|
300
|
+
raise ValueError(i18n.tr(key="exception.model.custom.siren.invalid_soundfile_index", index=index))
|
|
301
|
+
return f"SOUNDFILE_{index:03d}"
|
|
302
|
+
|
|
303
|
+
available_soundfiles: Final = DelegatedProperty[tuple[str, ...] | None](
|
|
304
|
+
path="_dp_soundfile.values", kind=Kind.STATE
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@state_property
|
|
308
|
+
def available_lights(self) -> tuple[str, ...] | None:
|
|
309
|
+
"""Return available lights (not supported for sound player)."""
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
@state_property
|
|
313
|
+
def available_tones(self) -> tuple[str, ...] | None:
|
|
314
|
+
"""Return available tones (soundfiles for sound player)."""
|
|
315
|
+
return self.available_soundfiles
|
|
316
|
+
|
|
317
|
+
@state_property
|
|
318
|
+
def current_soundfile(self) -> str | None:
|
|
319
|
+
"""Return currently selected soundfile."""
|
|
320
|
+
if (value := self._dp_soundfile.value) is None:
|
|
321
|
+
return None
|
|
322
|
+
return str(value)
|
|
323
|
+
|
|
324
|
+
@state_property
|
|
325
|
+
def is_on(self) -> bool:
|
|
326
|
+
"""Return true if sound is currently playing."""
|
|
327
|
+
activity = self._dp_direction.value
|
|
328
|
+
return activity is not None and activity in _ACTIVITY_STATES_ACTIVE
|
|
329
|
+
|
|
330
|
+
@bind_collector
|
|
331
|
+
async def play_sound(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
collector: CallParameterCollector | None = None,
|
|
335
|
+
**kwargs: Unpack[PlaySoundArgs],
|
|
336
|
+
) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Play a sound file on the device.
|
|
339
|
+
|
|
340
|
+
API is comparable to other siren classes, using on_time/ramp_time in seconds
|
|
341
|
+
with automatic unit conversion via TimerUnitMixin.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
collector: Optional call parameter collector.
|
|
345
|
+
**kwargs: Sound parameters from PlaySoundArgs:
|
|
346
|
+
soundfile: Soundfile from available_soundfiles or index (1-189).
|
|
347
|
+
volume: Volume level 0.0-1.0 (default: 0.5).
|
|
348
|
+
on_time: Duration in seconds (auto unit conversion, default: 10).
|
|
349
|
+
ramp_time: Ramp time in seconds (auto unit conversion, default: 0).
|
|
350
|
+
repetitions: 0=none, 1-18=count, -1=infinite (converted to VALUE_LIST).
|
|
351
|
+
|
|
352
|
+
"""
|
|
353
|
+
soundfile = kwargs.get("soundfile", "INTERNAL_SOUNDFILE")
|
|
354
|
+
volume = kwargs.get("volume", 0.5)
|
|
355
|
+
on_time = kwargs.get("on_time", 10.0)
|
|
356
|
+
ramp_time = kwargs.get("ramp_time", 0.0)
|
|
357
|
+
repetitions_value = _convert_repetitions(repetitions=kwargs.get("repetitions"))
|
|
358
|
+
|
|
359
|
+
# Convert integer to soundfile name if needed
|
|
360
|
+
if isinstance(soundfile, int):
|
|
361
|
+
soundfile = self._convert_soundfile_index(soundfile)
|
|
362
|
+
|
|
363
|
+
# Validate volume
|
|
364
|
+
if not 0.0 <= volume <= 1.0:
|
|
365
|
+
raise ValidationException(
|
|
366
|
+
i18n.tr(
|
|
367
|
+
key="exception.model.custom.siren.invalid_volume",
|
|
368
|
+
full_name=self.full_name,
|
|
369
|
+
value=volume,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Validate soundfile against available options
|
|
374
|
+
if self.available_soundfiles and soundfile not in self.available_soundfiles:
|
|
375
|
+
raise ValidationException(
|
|
376
|
+
i18n.tr(
|
|
377
|
+
key="exception.model.custom.siren.invalid_soundfile",
|
|
378
|
+
full_name=self.full_name,
|
|
379
|
+
value=soundfile,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Send parameters - order matters for batching
|
|
384
|
+
await self._dp_level.send_value(value=volume, collector=collector)
|
|
385
|
+
await self._dp_soundfile.send_value(value=soundfile, collector=collector)
|
|
386
|
+
await self._dp_repetitions.send_value(value=repetitions_value, collector=collector)
|
|
387
|
+
# Use mixin methods for automatic unit conversion
|
|
388
|
+
await self._set_ramp_time_on_value(ramp_time=ramp_time, collector=collector)
|
|
389
|
+
await self._set_on_time_value(on_time=on_time, collector=collector)
|
|
390
|
+
|
|
391
|
+
@bind_collector
|
|
392
|
+
async def stop_sound(
|
|
393
|
+
self,
|
|
394
|
+
*,
|
|
395
|
+
collector: CallParameterCollector | None = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Stop current sound playback."""
|
|
398
|
+
await self._dp_level.send_value(value=0.0, collector=collector)
|
|
399
|
+
await self._dp_on_time_value.send_value(value=0, collector=collector)
|
|
400
|
+
|
|
401
|
+
@bind_collector
|
|
402
|
+
async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
403
|
+
"""Turn the device off."""
|
|
404
|
+
await self.stop_sound(collector=collector)
|
|
405
|
+
|
|
406
|
+
@bind_collector
|
|
407
|
+
async def turn_on(
|
|
408
|
+
self,
|
|
409
|
+
*,
|
|
410
|
+
collector: CallParameterCollector | None = None,
|
|
411
|
+
**kwargs: Unpack[SirenOnArgs],
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Turn the device on."""
|
|
414
|
+
# Map SirenOnArgs to PlaySoundArgs
|
|
415
|
+
play_kwargs: PlaySoundArgs = {}
|
|
416
|
+
if "acoustic_alarm" in kwargs:
|
|
417
|
+
play_kwargs["soundfile"] = kwargs["acoustic_alarm"]
|
|
418
|
+
if "duration" in kwargs:
|
|
419
|
+
# Duration in SirenOnArgs is a string, try to parse as float (seconds)
|
|
420
|
+
with contextlib.suppress(ValueError, TypeError):
|
|
421
|
+
play_kwargs["on_time"] = float(kwargs["duration"])
|
|
422
|
+
await self.play_sound(collector=collector, **play_kwargs)
|
|
423
|
+
|
|
424
|
+
def _compute_capabilities(self) -> SirenCapabilities:
|
|
425
|
+
"""Compute static capabilities based on available DataPoints."""
|
|
426
|
+
return SirenCapabilities(
|
|
427
|
+
duration=True,
|
|
428
|
+
lights=False,
|
|
429
|
+
tones=False,
|
|
430
|
+
soundfiles=self.available_soundfiles is not None,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# =============================================================================
|
|
435
|
+
# DeviceProfileRegistry Registration
|
|
436
|
+
# =============================================================================
|
|
437
|
+
|
|
438
|
+
# IP Siren
|
|
439
|
+
DeviceProfileRegistry.register(
|
|
440
|
+
category=DataPointCategory.SIREN,
|
|
441
|
+
models="HmIP-ASIR",
|
|
442
|
+
data_point_class=CustomDpIpSiren,
|
|
443
|
+
profile_type=DeviceProfile.IP_SIREN,
|
|
444
|
+
channels=(3,),
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# IP Siren Smoke
|
|
448
|
+
DeviceProfileRegistry.register(
|
|
449
|
+
category=DataPointCategory.SIREN,
|
|
450
|
+
models="HmIP-SWSD",
|
|
451
|
+
data_point_class=CustomDpIpSirenSmoke,
|
|
452
|
+
profile_type=DeviceProfile.IP_SIREN_SMOKE,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# HmIP-MP3P Sound Player (channel 2)
|
|
456
|
+
DeviceProfileRegistry.register(
|
|
457
|
+
category=DataPointCategory.SIREN,
|
|
458
|
+
models="HmIP-MP3P",
|
|
459
|
+
data_point_class=CustomDpSoundPlayer,
|
|
460
|
+
profile_type=DeviceProfile.IP_SOUND_PLAYER,
|
|
461
|
+
channels=(2,),
|
|
462
|
+
)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2021-2026
|
|
3
|
+
"""
|
|
4
|
+
Custom switch data points for advanced switching devices.
|
|
5
|
+
|
|
6
|
+
Public API of this module is defined by __all__.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Final, Unpack
|
|
13
|
+
|
|
14
|
+
from aiohomematic.const import DataPointCategory, DeviceProfile, Field, Parameter
|
|
15
|
+
from aiohomematic.model.custom.data_point import CustomDataPoint
|
|
16
|
+
from aiohomematic.model.custom.field import DataPointField
|
|
17
|
+
from aiohomematic.model.custom.mixins import GroupStateMixin, StateChangeArgs, StateChangeTimerMixin
|
|
18
|
+
from aiohomematic.model.custom.registry import DeviceProfileRegistry, ExtendedDeviceConfig
|
|
19
|
+
from aiohomematic.model.data_point import CallParameterCollector, bind_collector
|
|
20
|
+
from aiohomematic.model.generic import DpAction, DpBinarySensor, DpSwitch
|
|
21
|
+
from aiohomematic.property_decorators import DelegatedProperty, Kind
|
|
22
|
+
|
|
23
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CustomDpSwitch(StateChangeTimerMixin, GroupStateMixin, CustomDataPoint):
|
|
27
|
+
"""Class for Homematic switch data point."""
|
|
28
|
+
|
|
29
|
+
__slots__ = () # Required to prevent __dict__ creation (descriptors are class-level)
|
|
30
|
+
|
|
31
|
+
_category = DataPointCategory.SWITCH
|
|
32
|
+
|
|
33
|
+
# Declarative data point field definitions
|
|
34
|
+
_dp_group_state = DataPointField(field=Field.GROUP_STATE, dpt=DpBinarySensor)
|
|
35
|
+
_dp_on_time_value = DataPointField(field=Field.ON_TIME_VALUE, dpt=DpAction)
|
|
36
|
+
_dp_state: Final = DataPointField(field=Field.STATE, dpt=DpSwitch)
|
|
37
|
+
|
|
38
|
+
value: Final = DelegatedProperty[bool | None](path="_dp_state.value", kind=Kind.STATE)
|
|
39
|
+
|
|
40
|
+
def is_state_change(self, **kwargs: Unpack[StateChangeArgs]) -> bool:
|
|
41
|
+
"""Check if the state changes due to kwargs."""
|
|
42
|
+
if self.is_state_change_for_on_off(**kwargs):
|
|
43
|
+
return True
|
|
44
|
+
return super().is_state_change(**kwargs)
|
|
45
|
+
|
|
46
|
+
@bind_collector
|
|
47
|
+
async def turn_off(self, *, collector: CallParameterCollector | None = None) -> None:
|
|
48
|
+
"""Turn the switch off."""
|
|
49
|
+
self.reset_timer_on_time()
|
|
50
|
+
if not self.is_state_change(off=True):
|
|
51
|
+
return
|
|
52
|
+
await self._dp_state.turn_off(collector=collector)
|
|
53
|
+
|
|
54
|
+
@bind_collector
|
|
55
|
+
async def turn_on(self, *, on_time: float | None = None, collector: CallParameterCollector | None = None) -> None:
|
|
56
|
+
"""Turn the switch on."""
|
|
57
|
+
if on_time is not None:
|
|
58
|
+
self.set_timer_on_time(on_time=on_time)
|
|
59
|
+
if not self.is_state_change(on=True):
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if (timer := self.get_and_start_timer()) is not None:
|
|
63
|
+
await self._dp_on_time_value.send_value(value=timer, collector=collector, do_validate=False)
|
|
64
|
+
await self._dp_state.turn_on(collector=collector)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# DeviceProfileRegistry Registration
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
# IP Switch (various channel configurations)
|
|
72
|
+
DeviceProfileRegistry.register(
|
|
73
|
+
category=DataPointCategory.SWITCH,
|
|
74
|
+
models=("ELV-SH-BS2", "HmIP-BS2", "HmIP-PCBS2"),
|
|
75
|
+
data_point_class=CustomDpSwitch,
|
|
76
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
77
|
+
channels=(4, 8),
|
|
78
|
+
)
|
|
79
|
+
DeviceProfileRegistry.register(
|
|
80
|
+
category=DataPointCategory.SWITCH,
|
|
81
|
+
models=(
|
|
82
|
+
"ELV-SH-PSMCI",
|
|
83
|
+
"ELV-SH-SW1-BAT",
|
|
84
|
+
"HmIP-DRSI1",
|
|
85
|
+
"HmIP-FSI",
|
|
86
|
+
"HmIP-PCBS",
|
|
87
|
+
"HmIP-PCBS-BAT",
|
|
88
|
+
"HmIP-PS",
|
|
89
|
+
"HmIP-USBSM",
|
|
90
|
+
"HmIP-WGC",
|
|
91
|
+
),
|
|
92
|
+
data_point_class=CustomDpSwitch,
|
|
93
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
94
|
+
channels=(3,),
|
|
95
|
+
)
|
|
96
|
+
DeviceProfileRegistry.register(
|
|
97
|
+
category=DataPointCategory.SWITCH,
|
|
98
|
+
models=("HmIP-BSL", "HmIP-BSM"),
|
|
99
|
+
data_point_class=CustomDpSwitch,
|
|
100
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
101
|
+
channels=(4,),
|
|
102
|
+
)
|
|
103
|
+
DeviceProfileRegistry.register(
|
|
104
|
+
category=DataPointCategory.SWITCH,
|
|
105
|
+
models="HmIP-DRSI4",
|
|
106
|
+
data_point_class=CustomDpSwitch,
|
|
107
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
108
|
+
channels=(6, 10, 14, 18),
|
|
109
|
+
)
|
|
110
|
+
DeviceProfileRegistry.register(
|
|
111
|
+
category=DataPointCategory.SWITCH,
|
|
112
|
+
models="HmIP-FSM",
|
|
113
|
+
data_point_class=CustomDpSwitch,
|
|
114
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
115
|
+
channels=(2,),
|
|
116
|
+
)
|
|
117
|
+
DeviceProfileRegistry.register(
|
|
118
|
+
category=DataPointCategory.SWITCH,
|
|
119
|
+
models="HmIP-MOD-OC8",
|
|
120
|
+
data_point_class=CustomDpSwitch,
|
|
121
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
122
|
+
channels=(10, 14, 18, 22, 26, 30, 34, 38),
|
|
123
|
+
)
|
|
124
|
+
DeviceProfileRegistry.register(
|
|
125
|
+
category=DataPointCategory.SWITCH,
|
|
126
|
+
models="HmIP-SCTH230",
|
|
127
|
+
data_point_class=CustomDpSwitch,
|
|
128
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
129
|
+
channels=(8,),
|
|
130
|
+
)
|
|
131
|
+
DeviceProfileRegistry.register(
|
|
132
|
+
category=DataPointCategory.SWITCH,
|
|
133
|
+
models="HmIP-WGT",
|
|
134
|
+
data_point_class=CustomDpSwitch,
|
|
135
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
136
|
+
channels=(4,),
|
|
137
|
+
)
|
|
138
|
+
DeviceProfileRegistry.register(
|
|
139
|
+
category=DataPointCategory.SWITCH,
|
|
140
|
+
models="HmIP-WHS2",
|
|
141
|
+
data_point_class=CustomDpSwitch,
|
|
142
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
143
|
+
channels=(2, 6),
|
|
144
|
+
)
|
|
145
|
+
DeviceProfileRegistry.register(
|
|
146
|
+
category=DataPointCategory.SWITCH,
|
|
147
|
+
models="HmIPW-DRS",
|
|
148
|
+
data_point_class=CustomDpSwitch,
|
|
149
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
150
|
+
channels=(2, 6, 10, 14, 18, 22, 26, 30),
|
|
151
|
+
)
|
|
152
|
+
DeviceProfileRegistry.register(
|
|
153
|
+
category=DataPointCategory.SWITCH,
|
|
154
|
+
models="HmIPW-FIO6",
|
|
155
|
+
data_point_class=CustomDpSwitch,
|
|
156
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
157
|
+
channels=(8, 12, 16, 20, 24, 28),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# HmIP-SMO230 (Switch with motion sensor)
|
|
161
|
+
DeviceProfileRegistry.register(
|
|
162
|
+
category=DataPointCategory.SWITCH,
|
|
163
|
+
models="HmIP-SMO230",
|
|
164
|
+
data_point_class=CustomDpSwitch,
|
|
165
|
+
profile_type=DeviceProfile.IP_SWITCH,
|
|
166
|
+
channels=(10,),
|
|
167
|
+
extended=ExtendedDeviceConfig(
|
|
168
|
+
additional_data_points={
|
|
169
|
+
1: (
|
|
170
|
+
Parameter.ILLUMINATION,
|
|
171
|
+
Parameter.MOTION,
|
|
172
|
+
Parameter.MOTION_DETECTION_ACTIVE,
|
|
173
|
+
Parameter.RESET_MOTION,
|
|
174
|
+
),
|
|
175
|
+
2: (
|
|
176
|
+
Parameter.ILLUMINATION,
|
|
177
|
+
Parameter.MOTION,
|
|
178
|
+
Parameter.MOTION_DETECTION_ACTIVE,
|
|
179
|
+
Parameter.RESET_MOTION,
|
|
180
|
+
),
|
|
181
|
+
3: (
|
|
182
|
+
Parameter.ILLUMINATION,
|
|
183
|
+
Parameter.MOTION,
|
|
184
|
+
Parameter.MOTION_DETECTION_ACTIVE,
|
|
185
|
+
Parameter.RESET_MOTION,
|
|
186
|
+
),
|
|
187
|
+
4: (
|
|
188
|
+
Parameter.ILLUMINATION,
|
|
189
|
+
Parameter.MOTION,
|
|
190
|
+
Parameter.MOTION_DETECTION_ACTIVE,
|
|
191
|
+
Parameter.RESET_MOTION,
|
|
192
|
+
),
|
|
193
|
+
}
|
|
194
|
+
),
|
|
195
|
+
)
|