aiohomematic 2025.8.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aiohomematic might be problematic. Click here for more details.
- aiohomematic/__init__.py +47 -0
- aiohomematic/async_support.py +146 -0
- aiohomematic/caches/__init__.py +10 -0
- aiohomematic/caches/dynamic.py +554 -0
- aiohomematic/caches/persistent.py +459 -0
- aiohomematic/caches/visibility.py +774 -0
- aiohomematic/central/__init__.py +2034 -0
- aiohomematic/central/decorators.py +110 -0
- aiohomematic/central/xml_rpc_server.py +267 -0
- aiohomematic/client/__init__.py +1746 -0
- aiohomematic/client/json_rpc.py +1193 -0
- aiohomematic/client/xml_rpc.py +222 -0
- aiohomematic/const.py +795 -0
- aiohomematic/context.py +8 -0
- aiohomematic/converter.py +82 -0
- aiohomematic/decorators.py +188 -0
- aiohomematic/exceptions.py +145 -0
- aiohomematic/hmcli.py +159 -0
- aiohomematic/model/__init__.py +137 -0
- aiohomematic/model/calculated/__init__.py +65 -0
- aiohomematic/model/calculated/climate.py +230 -0
- aiohomematic/model/calculated/data_point.py +319 -0
- aiohomematic/model/calculated/operating_voltage_level.py +311 -0
- aiohomematic/model/calculated/support.py +174 -0
- aiohomematic/model/custom/__init__.py +175 -0
- aiohomematic/model/custom/climate.py +1334 -0
- aiohomematic/model/custom/const.py +146 -0
- aiohomematic/model/custom/cover.py +741 -0
- aiohomematic/model/custom/data_point.py +318 -0
- aiohomematic/model/custom/definition.py +861 -0
- aiohomematic/model/custom/light.py +1092 -0
- aiohomematic/model/custom/lock.py +389 -0
- aiohomematic/model/custom/siren.py +268 -0
- aiohomematic/model/custom/support.py +40 -0
- aiohomematic/model/custom/switch.py +172 -0
- aiohomematic/model/custom/valve.py +112 -0
- aiohomematic/model/data_point.py +1109 -0
- aiohomematic/model/decorators.py +173 -0
- aiohomematic/model/device.py +1347 -0
- aiohomematic/model/event.py +210 -0
- aiohomematic/model/generic/__init__.py +211 -0
- aiohomematic/model/generic/action.py +32 -0
- aiohomematic/model/generic/binary_sensor.py +28 -0
- aiohomematic/model/generic/button.py +25 -0
- aiohomematic/model/generic/data_point.py +162 -0
- aiohomematic/model/generic/number.py +73 -0
- aiohomematic/model/generic/select.py +36 -0
- aiohomematic/model/generic/sensor.py +72 -0
- aiohomematic/model/generic/switch.py +52 -0
- aiohomematic/model/generic/text.py +27 -0
- aiohomematic/model/hub/__init__.py +334 -0
- aiohomematic/model/hub/binary_sensor.py +22 -0
- aiohomematic/model/hub/button.py +26 -0
- aiohomematic/model/hub/data_point.py +332 -0
- aiohomematic/model/hub/number.py +37 -0
- aiohomematic/model/hub/select.py +47 -0
- aiohomematic/model/hub/sensor.py +35 -0
- aiohomematic/model/hub/switch.py +42 -0
- aiohomematic/model/hub/text.py +28 -0
- aiohomematic/model/support.py +599 -0
- aiohomematic/model/update.py +136 -0
- aiohomematic/py.typed +0 -0
- aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
- aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
- aiohomematic/rega_scripts/get_serial.fn +44 -0
- aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
- aiohomematic/rega_scripts/set_program_state.fn +12 -0
- aiohomematic/rega_scripts/set_system_variable.fn +15 -0
- aiohomematic/support.py +482 -0
- aiohomematic/validator.py +65 -0
- aiohomematic-2025.8.6.dist-info/METADATA +69 -0
- aiohomematic-2025.8.6.dist-info/RECORD +77 -0
- aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
- aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
- aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
- aiohomematic_support/__init__.py +1 -0
- aiohomematic_support/client_local.py +349 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Module for data points implemented using the number category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from aiohomematic.const import DataPointCategory
|
|
8
|
+
from aiohomematic.model.decorators import state_property
|
|
9
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseDpNumber[NumberParameterT: int | float | None](GenericDataPoint[NumberParameterT, int | float | str]):
|
|
13
|
+
"""
|
|
14
|
+
Implementation of a number.
|
|
15
|
+
|
|
16
|
+
This is a default data point that gets automatically generated.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
_category = DataPointCategory.NUMBER
|
|
22
|
+
|
|
23
|
+
def _prepare_number_for_sending(
|
|
24
|
+
self, value: int | float | str, type_converter: type, do_validate: bool = True
|
|
25
|
+
) -> NumberParameterT:
|
|
26
|
+
"""Prepare value before sending."""
|
|
27
|
+
if not do_validate or (
|
|
28
|
+
value is not None and isinstance(value, int | float) and self._min <= type_converter(value) <= self._max
|
|
29
|
+
):
|
|
30
|
+
return cast(NumberParameterT, type_converter(value))
|
|
31
|
+
if self._special and isinstance(value, str) and value in self._special:
|
|
32
|
+
return cast(NumberParameterT, type_converter(self._special[value]))
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"NUMBER failed: Invalid value: {value} (min: {self._min}, max: {self._max}, special:{self._special})"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DpFloat(BaseDpNumber[float | None]):
|
|
39
|
+
"""
|
|
40
|
+
Implementation of a Float.
|
|
41
|
+
|
|
42
|
+
This is a default data point that gets automatically generated.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
__slots__ = ()
|
|
46
|
+
|
|
47
|
+
def _prepare_value_for_sending(self, value: int | float | str, do_validate: bool = True) -> float | None:
|
|
48
|
+
"""Prepare value before sending."""
|
|
49
|
+
return self._prepare_number_for_sending(value=value, type_converter=float, do_validate=do_validate)
|
|
50
|
+
|
|
51
|
+
@state_property
|
|
52
|
+
def value(self) -> float | None:
|
|
53
|
+
"""Return the value of the data_point."""
|
|
54
|
+
return cast(float | None, self._value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DpInteger(BaseDpNumber[int | None]):
|
|
58
|
+
"""
|
|
59
|
+
Implementation of an Integer.
|
|
60
|
+
|
|
61
|
+
This is a default data point that gets automatically generated.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
__slots__ = ()
|
|
65
|
+
|
|
66
|
+
def _prepare_value_for_sending(self, value: int | float | str, do_validate: bool = True) -> int | None:
|
|
67
|
+
"""Prepare value before sending."""
|
|
68
|
+
return self._prepare_number_for_sending(value=value, type_converter=int, do_validate=do_validate)
|
|
69
|
+
|
|
70
|
+
@state_property
|
|
71
|
+
def value(self) -> int | None:
|
|
72
|
+
"""Return the value of the data_point."""
|
|
73
|
+
return cast(int | None, self._value)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Module for data points implemented using the select category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aiohomematic.const import DataPointCategory
|
|
6
|
+
from aiohomematic.model.decorators import state_property
|
|
7
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
8
|
+
from aiohomematic.model.support import get_value_from_value_list
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DpSelect(GenericDataPoint[int | str, int | float | str]):
|
|
12
|
+
"""
|
|
13
|
+
Implementation of a select data_point.
|
|
14
|
+
|
|
15
|
+
This is a default data point that gets automatically generated.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__slots__ = ()
|
|
19
|
+
|
|
20
|
+
_category = DataPointCategory.SELECT
|
|
21
|
+
|
|
22
|
+
@state_property
|
|
23
|
+
def value(self) -> str | None:
|
|
24
|
+
"""Get the value of the data_point."""
|
|
25
|
+
if (value := get_value_from_value_list(value=self._value, value_list=self.values)) is not None:
|
|
26
|
+
return value
|
|
27
|
+
return str(self._default)
|
|
28
|
+
|
|
29
|
+
def _prepare_value_for_sending(self, value: int | float | str, do_validate: bool = True) -> int:
|
|
30
|
+
"""Prepare value before sending."""
|
|
31
|
+
# We allow setting the value via index as well, just in case.
|
|
32
|
+
if isinstance(value, int | float) and self._values and 0 <= value < len(self._values):
|
|
33
|
+
return int(value)
|
|
34
|
+
if self._values and value in self._values:
|
|
35
|
+
return self._values.index(value)
|
|
36
|
+
raise ValueError(f"Value not in value_list for {self.name}/{self.unique_id}")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Module for data points implemented using the sensor category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Final, cast
|
|
8
|
+
|
|
9
|
+
from aiohomematic.const import DataPointCategory, Parameter, ParameterType
|
|
10
|
+
from aiohomematic.model.decorators import state_property
|
|
11
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
12
|
+
from aiohomematic.model.support import check_length_and_log, get_value_from_value_list
|
|
13
|
+
|
|
14
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DpSensor[SensorT: float | int | str | None](GenericDataPoint[SensorT, None]):
|
|
18
|
+
"""
|
|
19
|
+
Implementation of a sensor.
|
|
20
|
+
|
|
21
|
+
This is a default data point that gets automatically generated.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__slots__ = ()
|
|
25
|
+
|
|
26
|
+
_category = DataPointCategory.SENSOR
|
|
27
|
+
|
|
28
|
+
@state_property
|
|
29
|
+
def value(self) -> SensorT:
|
|
30
|
+
"""Return the value."""
|
|
31
|
+
if (value := get_value_from_value_list(value=self._value, value_list=self.values)) is not None:
|
|
32
|
+
return cast(SensorT, value)
|
|
33
|
+
if convert_func := self._get_converter_func():
|
|
34
|
+
return cast(SensorT, convert_func(self._value))
|
|
35
|
+
return cast(
|
|
36
|
+
SensorT,
|
|
37
|
+
check_length_and_log(name=self.name, value=self._value)
|
|
38
|
+
if self._type == ParameterType.STRING
|
|
39
|
+
else self._value,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def _get_converter_func(self) -> Any:
|
|
43
|
+
"""Return a converter based on sensor."""
|
|
44
|
+
if convert_func := _VALUE_CONVERTERS_BY_PARAM.get(self.parameter):
|
|
45
|
+
return convert_func
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fix_rssi(value: Any) -> int | None:
|
|
50
|
+
"""
|
|
51
|
+
Fix rssi value.
|
|
52
|
+
|
|
53
|
+
See https://github.com/sukramj/aiohomematic/blob/devel/docs/rssi_fix.md.
|
|
54
|
+
"""
|
|
55
|
+
if value is None:
|
|
56
|
+
return None
|
|
57
|
+
if isinstance(value, int):
|
|
58
|
+
if -127 < value < 0:
|
|
59
|
+
return value
|
|
60
|
+
if 1 < value < 127:
|
|
61
|
+
return value * -1
|
|
62
|
+
if -256 < value < -129:
|
|
63
|
+
return (value * -1) - 256
|
|
64
|
+
if 129 < value < 256:
|
|
65
|
+
return value - 256
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_VALUE_CONVERTERS_BY_PARAM: Mapping[str, Any] = {
|
|
70
|
+
Parameter.RSSI_PEER: _fix_rssi,
|
|
71
|
+
Parameter.RSSI_DEVICE: _fix_rssi,
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Module for data points implemented using the switch category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from aiohomematic.const import DataPointCategory, Parameter, ParameterType
|
|
8
|
+
from aiohomematic.decorators import inspector
|
|
9
|
+
from aiohomematic.model.data_point import CallParameterCollector
|
|
10
|
+
from aiohomematic.model.decorators import state_property
|
|
11
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DpSwitch(GenericDataPoint[bool | None, bool]):
|
|
15
|
+
"""
|
|
16
|
+
Implementation of a switch.
|
|
17
|
+
|
|
18
|
+
This is a default data point that gets automatically generated.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__slots__ = ()
|
|
22
|
+
|
|
23
|
+
_category = DataPointCategory.SWITCH
|
|
24
|
+
|
|
25
|
+
@state_property
|
|
26
|
+
def value(self) -> bool | None:
|
|
27
|
+
"""Get the value of the data_point."""
|
|
28
|
+
if self._type == ParameterType.ACTION:
|
|
29
|
+
return False
|
|
30
|
+
return cast(bool | None, self._value)
|
|
31
|
+
|
|
32
|
+
@inspector()
|
|
33
|
+
async def turn_on(self, collector: CallParameterCollector | None = None, on_time: float | None = None) -> None:
|
|
34
|
+
"""Turn the switch on."""
|
|
35
|
+
if on_time is not None:
|
|
36
|
+
await self.set_on_time(on_time=on_time)
|
|
37
|
+
await self.send_value(value=True, collector=collector)
|
|
38
|
+
|
|
39
|
+
@inspector()
|
|
40
|
+
async def turn_off(self, collector: CallParameterCollector | None = None) -> None:
|
|
41
|
+
"""Turn the switch off."""
|
|
42
|
+
await self.send_value(value=False, collector=collector)
|
|
43
|
+
|
|
44
|
+
@inspector()
|
|
45
|
+
async def set_on_time(self, on_time: float) -> None:
|
|
46
|
+
"""Set the on time value in seconds."""
|
|
47
|
+
await self._client.set_value(
|
|
48
|
+
channel_address=self._channel.address,
|
|
49
|
+
paramset_key=self._paramset_key,
|
|
50
|
+
parameter=Parameter.ON_TIME,
|
|
51
|
+
value=float(on_time),
|
|
52
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Module for data points implemented using the text category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from aiohomematic.const import DataPointCategory
|
|
8
|
+
from aiohomematic.model.decorators import state_property
|
|
9
|
+
from aiohomematic.model.generic.data_point import GenericDataPoint
|
|
10
|
+
from aiohomematic.model.support import check_length_and_log
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DpText(GenericDataPoint[str, str]):
|
|
14
|
+
"""
|
|
15
|
+
Implementation of a text.
|
|
16
|
+
|
|
17
|
+
This is a default data point that gets automatically generated.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
__slots__ = ()
|
|
21
|
+
|
|
22
|
+
_category = DataPointCategory.TEXT
|
|
23
|
+
|
|
24
|
+
@state_property
|
|
25
|
+
def value(self) -> str | None:
|
|
26
|
+
"""Get the value of the data_point."""
|
|
27
|
+
return cast(str | None, check_length_and_log(name=self.name, value=self._value))
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hub (backend) data points for AioHomematic.
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
- This module reflects the state and capabilities of the backend (CCU/Homegear)
|
|
6
|
+
at the hub level. It exposes backend programs and system variables as data
|
|
7
|
+
points that can be observed and acted upon by higher layers (e.g.,
|
|
8
|
+
integrations).
|
|
9
|
+
|
|
10
|
+
Responsibilities
|
|
11
|
+
- Fetch current lists of programs and system variables from the central unit.
|
|
12
|
+
- Create and maintain concrete hub data point instances for those items.
|
|
13
|
+
- Keep hub data points in sync with the backend (update values, add/remove).
|
|
14
|
+
- Notify the system about newly created hub data points via backend events.
|
|
15
|
+
|
|
16
|
+
Public API (selected)
|
|
17
|
+
- Hub: Orchestrates scanning and synchronization of hub-level data points.
|
|
18
|
+
- ProgramDpButton / ProgramDpSwitch: Represent a backend program as an
|
|
19
|
+
invocable button or a switch-like control, respectively.
|
|
20
|
+
- Sysvar data points: Map system variables to appropriate types:
|
|
21
|
+
- SysvarDpSensor, SysvarDpBinarySensor, SysvarDpSelect, SysvarDpNumber,
|
|
22
|
+
SysvarDpSwitch, SysvarDpText.
|
|
23
|
+
- __all__: Exposes the classes and types intended for external consumption.
|
|
24
|
+
|
|
25
|
+
Lifecycle and Flow
|
|
26
|
+
1. fetch_program_data / fetch_sysvar_data (async) are scheduled or triggered
|
|
27
|
+
manually depending on configuration and availability of the central unit.
|
|
28
|
+
2. On fetch:
|
|
29
|
+
- The module retrieves program/sysvar lists from the primary client.
|
|
30
|
+
- It identifies removed items and cleans up corresponding data points.
|
|
31
|
+
- It updates existing data points or creates new ones as needed.
|
|
32
|
+
3. For newly created hub data points, a BackendSystemEvent.HUB_REFRESHED event
|
|
33
|
+
is emitted with a categorized mapping of the new points for consumers.
|
|
34
|
+
|
|
35
|
+
Type Mapping for System Variables
|
|
36
|
+
- Based on SysvarType and the extended_sysvar flag, system variables are
|
|
37
|
+
represented by the most suitable hub data point class. For example:
|
|
38
|
+
- ALARM/LOGIC → binary_sensor or switch (if extended)
|
|
39
|
+
- LIST (extended) → select
|
|
40
|
+
- FLOAT/INTEGER (extended) → number
|
|
41
|
+
- STRING (extended) → text
|
|
42
|
+
- Any other case → generic sensor
|
|
43
|
+
|
|
44
|
+
Concurrency and Reliability
|
|
45
|
+
- Fetch operations are protected by semaphores to avoid concurrent updates of
|
|
46
|
+
the same kind (programs or sysvars).
|
|
47
|
+
- The inspector decorator helps ensure exceptions do not propagate unexpectedly
|
|
48
|
+
when fetching; errors are logged and the system continues operating.
|
|
49
|
+
|
|
50
|
+
Backend Specifics and Cleanup
|
|
51
|
+
- For CCU backends, certain internal variables (e.g., legacy "OldVal*",
|
|
52
|
+
"pcCCUID") are filtered out to avoid exposing irrelevant state.
|
|
53
|
+
|
|
54
|
+
Categories and New Data Point Discovery
|
|
55
|
+
- Newly created hub data points are grouped into HUB_CATEGORIES and returned as
|
|
56
|
+
a mapping, so subscribers can register and present them appropriately.
|
|
57
|
+
|
|
58
|
+
Related Modules
|
|
59
|
+
- aiohomematic.model.hub.data_point: Base types for hub-level data points.
|
|
60
|
+
- aiohomematic.central: Central unit coordination and backend communication.
|
|
61
|
+
- aiohomematic.const: Shared constants, enums, and data structures.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
- Typical usage occurs inside the central unit scheduling:
|
|
65
|
+
hub = Hub(central)
|
|
66
|
+
await hub.fetch_program_data(scheduled=True)
|
|
67
|
+
await hub.fetch_sysvar_data(scheduled=True)
|
|
68
|
+
|
|
69
|
+
This module complements device/channel data points by reflecting control center
|
|
70
|
+
state and enabling automations at the backend level.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
from __future__ import annotations
|
|
75
|
+
|
|
76
|
+
import asyncio
|
|
77
|
+
from collections.abc import Collection, Mapping, Set as AbstractSet
|
|
78
|
+
from datetime import datetime
|
|
79
|
+
import logging
|
|
80
|
+
from typing import Final, NamedTuple
|
|
81
|
+
|
|
82
|
+
from aiohomematic import central as hmcu
|
|
83
|
+
from aiohomematic.const import (
|
|
84
|
+
HUB_CATEGORIES,
|
|
85
|
+
Backend,
|
|
86
|
+
BackendSystemEvent,
|
|
87
|
+
DataPointCategory,
|
|
88
|
+
ProgramData,
|
|
89
|
+
SystemVariableData,
|
|
90
|
+
SysvarType,
|
|
91
|
+
)
|
|
92
|
+
from aiohomematic.decorators import inspector
|
|
93
|
+
from aiohomematic.model.hub.binary_sensor import SysvarDpBinarySensor
|
|
94
|
+
from aiohomematic.model.hub.button import ProgramDpButton
|
|
95
|
+
from aiohomematic.model.hub.data_point import GenericHubDataPoint, GenericProgramDataPoint, GenericSysvarDataPoint
|
|
96
|
+
from aiohomematic.model.hub.number import SysvarDpNumber
|
|
97
|
+
from aiohomematic.model.hub.select import SysvarDpSelect
|
|
98
|
+
from aiohomematic.model.hub.sensor import SysvarDpSensor
|
|
99
|
+
from aiohomematic.model.hub.switch import ProgramDpSwitch, SysvarDpSwitch
|
|
100
|
+
from aiohomematic.model.hub.text import SysvarDpText
|
|
101
|
+
|
|
102
|
+
__all__ = [
|
|
103
|
+
"GenericProgramDataPoint",
|
|
104
|
+
"GenericSysvarDataPoint",
|
|
105
|
+
"Hub",
|
|
106
|
+
"ProgramDpButton",
|
|
107
|
+
"ProgramDpSwitch",
|
|
108
|
+
"ProgramDpType",
|
|
109
|
+
"SysvarDpBinarySensor",
|
|
110
|
+
"SysvarDpNumber",
|
|
111
|
+
"SysvarDpSelect",
|
|
112
|
+
"SysvarDpSensor",
|
|
113
|
+
"SysvarDpSwitch",
|
|
114
|
+
"SysvarDpText",
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
118
|
+
|
|
119
|
+
_EXCLUDED: Final = [
|
|
120
|
+
"OldVal",
|
|
121
|
+
"pcCCUID",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ProgramDpType(NamedTuple):
|
|
126
|
+
"""Key for data points."""
|
|
127
|
+
|
|
128
|
+
pid: str
|
|
129
|
+
button: ProgramDpButton
|
|
130
|
+
switch: ProgramDpSwitch
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Hub:
|
|
134
|
+
"""The HomeMatic hub. (CCU/HomeGear)."""
|
|
135
|
+
|
|
136
|
+
__slots__ = (
|
|
137
|
+
"_sema_fetch_sysvars",
|
|
138
|
+
"_sema_fetch_programs",
|
|
139
|
+
"_central",
|
|
140
|
+
"_config",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __init__(self, central: hmcu.CentralUnit) -> None:
|
|
144
|
+
"""Initialize HomeMatic hub."""
|
|
145
|
+
self._sema_fetch_sysvars: Final = asyncio.Semaphore()
|
|
146
|
+
self._sema_fetch_programs: Final = asyncio.Semaphore()
|
|
147
|
+
self._central: Final = central
|
|
148
|
+
self._config: Final = central.config
|
|
149
|
+
|
|
150
|
+
@inspector(re_raise=False)
|
|
151
|
+
async def fetch_sysvar_data(self, scheduled: bool) -> None:
|
|
152
|
+
"""Fetch sysvar data for the hub."""
|
|
153
|
+
if self._config.enable_sysvar_scan:
|
|
154
|
+
_LOGGER.debug(
|
|
155
|
+
"FETCH_SYSVAR_DATA: %s fetching of system variables for %s",
|
|
156
|
+
"Scheduled" if scheduled else "Manual",
|
|
157
|
+
self._central.name,
|
|
158
|
+
)
|
|
159
|
+
async with self._sema_fetch_sysvars:
|
|
160
|
+
if self._central.available:
|
|
161
|
+
await self._update_sysvar_data_points()
|
|
162
|
+
|
|
163
|
+
@inspector(re_raise=False)
|
|
164
|
+
async def fetch_program_data(self, scheduled: bool) -> None:
|
|
165
|
+
"""Fetch program data for the hub."""
|
|
166
|
+
if self._config.enable_program_scan:
|
|
167
|
+
_LOGGER.debug(
|
|
168
|
+
"FETCH_PROGRAM_DATA: %s fetching of programs for %s",
|
|
169
|
+
"Scheduled" if scheduled else "Manual",
|
|
170
|
+
self._central.name,
|
|
171
|
+
)
|
|
172
|
+
async with self._sema_fetch_programs:
|
|
173
|
+
if self._central.available:
|
|
174
|
+
await self._update_program_data_points()
|
|
175
|
+
|
|
176
|
+
async def _update_program_data_points(self) -> None:
|
|
177
|
+
"""Retrieve all program data and update program values."""
|
|
178
|
+
if not (client := self._central.primary_client):
|
|
179
|
+
return
|
|
180
|
+
if (programs := await client.get_all_programs(markers=self._config.program_markers)) is None:
|
|
181
|
+
_LOGGER.debug("UPDATE_PROGRAM_DATA_POINTS: Unable to retrieve programs for %s", self._central.name)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
_LOGGER.debug(
|
|
185
|
+
"UPDATE_PROGRAM_DATA_POINTS: %i programs received for %s",
|
|
186
|
+
len(programs),
|
|
187
|
+
self._central.name,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if missing_program_ids := self._identify_missing_program_ids(programs=programs):
|
|
191
|
+
self._remove_program_data_point(ids=missing_program_ids)
|
|
192
|
+
|
|
193
|
+
new_programs: list[GenericProgramDataPoint] = []
|
|
194
|
+
|
|
195
|
+
for program_data in programs:
|
|
196
|
+
if program_dp := self._central.get_program_data_point(pid=program_data.pid):
|
|
197
|
+
program_dp.button.update_data(data=program_data)
|
|
198
|
+
program_dp.switch.update_data(data=program_data)
|
|
199
|
+
else:
|
|
200
|
+
program_dp = self._create_program_dp(data=program_data)
|
|
201
|
+
new_programs.append(program_dp.button)
|
|
202
|
+
new_programs.append(program_dp.switch)
|
|
203
|
+
|
|
204
|
+
if new_programs:
|
|
205
|
+
self._central.fire_backend_system_callback(
|
|
206
|
+
system_event=BackendSystemEvent.HUB_REFRESHED,
|
|
207
|
+
new_hub_data_points=_get_new_hub_data_points(data_points=new_programs),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def _update_sysvar_data_points(self) -> None:
|
|
211
|
+
"""Retrieve all variable data and update hmvariable values."""
|
|
212
|
+
if not (client := self._central.primary_client):
|
|
213
|
+
return
|
|
214
|
+
if (variables := await client.get_all_system_variables(markers=self._config.sysvar_markers)) is None:
|
|
215
|
+
_LOGGER.debug("UPDATE_SYSVAR_DATA_POINTS: Unable to retrieve sysvars for %s", self._central.name)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
_LOGGER.debug(
|
|
219
|
+
"UPDATE_SYSVAR_DATA_POINTS: %i sysvars received for %s",
|
|
220
|
+
len(variables),
|
|
221
|
+
self._central.name,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# remove some variables in case of CCU Backend
|
|
225
|
+
# - OldValue(s) are for internal calculations
|
|
226
|
+
if self._central.model is Backend.CCU:
|
|
227
|
+
variables = _clean_variables(variables)
|
|
228
|
+
|
|
229
|
+
if missing_variable_ids := self._identify_missing_variable_ids(variables=variables):
|
|
230
|
+
self._remove_sysvar_data_point(del_data_point_ids=missing_variable_ids)
|
|
231
|
+
|
|
232
|
+
new_sysvars: list[GenericSysvarDataPoint] = []
|
|
233
|
+
|
|
234
|
+
for sysvar in variables:
|
|
235
|
+
if dp := self._central.get_sysvar_data_point(vid=sysvar.vid):
|
|
236
|
+
dp.write_value(value=sysvar.value, write_at=datetime.now())
|
|
237
|
+
else:
|
|
238
|
+
new_sysvars.append(self._create_system_variable(data=sysvar))
|
|
239
|
+
|
|
240
|
+
if new_sysvars:
|
|
241
|
+
self._central.fire_backend_system_callback(
|
|
242
|
+
system_event=BackendSystemEvent.HUB_REFRESHED,
|
|
243
|
+
new_hub_data_points=_get_new_hub_data_points(data_points=new_sysvars),
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def _create_program_dp(self, data: ProgramData) -> ProgramDpType:
|
|
247
|
+
"""Create program as data_point."""
|
|
248
|
+
program_dp = ProgramDpType(
|
|
249
|
+
pid=data.pid,
|
|
250
|
+
button=ProgramDpButton(central=self._central, data=data),
|
|
251
|
+
switch=ProgramDpSwitch(central=self._central, data=data),
|
|
252
|
+
)
|
|
253
|
+
self._central.add_program_data_point(program_dp=program_dp)
|
|
254
|
+
return program_dp
|
|
255
|
+
|
|
256
|
+
def _create_system_variable(self, data: SystemVariableData) -> GenericSysvarDataPoint:
|
|
257
|
+
"""Create system variable as data_point."""
|
|
258
|
+
sysvar_dp = self._create_sysvar_data_point(data=data)
|
|
259
|
+
self._central.add_sysvar_data_point(sysvar_data_point=sysvar_dp)
|
|
260
|
+
return sysvar_dp
|
|
261
|
+
|
|
262
|
+
def _create_sysvar_data_point(self, data: SystemVariableData) -> GenericSysvarDataPoint:
|
|
263
|
+
"""Create sysvar data_point."""
|
|
264
|
+
data_type = data.data_type
|
|
265
|
+
extended_sysvar = data.extended_sysvar
|
|
266
|
+
if data_type:
|
|
267
|
+
if data_type in (SysvarType.ALARM, SysvarType.LOGIC):
|
|
268
|
+
if extended_sysvar:
|
|
269
|
+
return SysvarDpSwitch(central=self._central, data=data)
|
|
270
|
+
return SysvarDpBinarySensor(central=self._central, data=data)
|
|
271
|
+
if data_type == SysvarType.LIST and extended_sysvar:
|
|
272
|
+
return SysvarDpSelect(central=self._central, data=data)
|
|
273
|
+
if data_type in (SysvarType.FLOAT, SysvarType.INTEGER) and extended_sysvar:
|
|
274
|
+
return SysvarDpNumber(central=self._central, data=data)
|
|
275
|
+
if data_type == SysvarType.STRING and extended_sysvar:
|
|
276
|
+
return SysvarDpText(central=self._central, data=data)
|
|
277
|
+
|
|
278
|
+
return SysvarDpSensor(central=self._central, data=data)
|
|
279
|
+
|
|
280
|
+
def _remove_program_data_point(self, ids: set[str]) -> None:
|
|
281
|
+
"""Remove sysvar data_point from hub."""
|
|
282
|
+
for pid in ids:
|
|
283
|
+
self._central.remove_program_button(pid=pid)
|
|
284
|
+
|
|
285
|
+
def _remove_sysvar_data_point(self, del_data_point_ids: set[str]) -> None:
|
|
286
|
+
"""Remove sysvar data_point from hub."""
|
|
287
|
+
for vid in del_data_point_ids:
|
|
288
|
+
self._central.remove_sysvar_data_point(vid=vid)
|
|
289
|
+
|
|
290
|
+
def _identify_missing_program_ids(self, programs: tuple[ProgramData, ...]) -> set[str]:
|
|
291
|
+
"""Identify missing programs."""
|
|
292
|
+
return {
|
|
293
|
+
program_dp.pid
|
|
294
|
+
for program_dp in self._central.program_data_points
|
|
295
|
+
if program_dp.pid not in [x.pid for x in programs]
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
def _identify_missing_variable_ids(self, variables: tuple[SystemVariableData, ...]) -> set[str]:
|
|
299
|
+
"""Identify missing variables."""
|
|
300
|
+
variable_ids: dict[str, bool] = {x.vid: x.extended_sysvar for x in variables}
|
|
301
|
+
missing_variable_ids: list[str] = []
|
|
302
|
+
for svdp in self._central.sysvar_data_points:
|
|
303
|
+
if svdp.data_type == SysvarType.STRING:
|
|
304
|
+
continue
|
|
305
|
+
if (vid := svdp.vid) is not None and (
|
|
306
|
+
vid not in variable_ids or (svdp.is_extended is not variable_ids.get(vid))
|
|
307
|
+
):
|
|
308
|
+
missing_variable_ids.append(vid)
|
|
309
|
+
return set(missing_variable_ids)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_excluded(variable: str, excludes: list[str]) -> bool:
|
|
313
|
+
"""Check if variable is excluded by exclude_list."""
|
|
314
|
+
return any(marker in variable for marker in excludes)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _clean_variables(variables: tuple[SystemVariableData, ...]) -> tuple[SystemVariableData, ...]:
|
|
318
|
+
"""Clean variables by removing excluded."""
|
|
319
|
+
return tuple(sv for sv in variables if not _is_excluded(sv.legacy_name, _EXCLUDED))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _get_new_hub_data_points(
|
|
323
|
+
data_points: Collection[GenericHubDataPoint],
|
|
324
|
+
) -> Mapping[DataPointCategory, AbstractSet[GenericHubDataPoint]]:
|
|
325
|
+
"""Return data points as category dict."""
|
|
326
|
+
hub_data_points: dict[DataPointCategory, set[GenericHubDataPoint]] = {}
|
|
327
|
+
for hub_category in HUB_CATEGORIES:
|
|
328
|
+
hub_data_points[hub_category] = set()
|
|
329
|
+
|
|
330
|
+
for dp in data_points:
|
|
331
|
+
if dp.is_registered is False:
|
|
332
|
+
hub_data_points[dp.category].add(dp)
|
|
333
|
+
|
|
334
|
+
return hub_data_points
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Module for hub data points implemented using the binary_sensor category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aiohomematic.const import DataPointCategory
|
|
6
|
+
from aiohomematic.model.decorators import state_property
|
|
7
|
+
from aiohomematic.model.hub.data_point import GenericSysvarDataPoint
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SysvarDpBinarySensor(GenericSysvarDataPoint):
|
|
11
|
+
"""Implementation of a sysvar binary_sensor."""
|
|
12
|
+
|
|
13
|
+
__slots__ = ()
|
|
14
|
+
|
|
15
|
+
_category = DataPointCategory.HUB_BINARY_SENSOR
|
|
16
|
+
|
|
17
|
+
@state_property
|
|
18
|
+
def value(self) -> bool | None:
|
|
19
|
+
"""Return the value of the data_point."""
|
|
20
|
+
if self._value is not None:
|
|
21
|
+
return bool(self._value)
|
|
22
|
+
return None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Module for hub data points implemented using the button category."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from aiohomematic.const import DataPointCategory
|
|
6
|
+
from aiohomematic.decorators import inspector
|
|
7
|
+
from aiohomematic.model.decorators import state_property
|
|
8
|
+
from aiohomematic.model.hub.data_point import GenericProgramDataPoint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProgramDpButton(GenericProgramDataPoint):
|
|
12
|
+
"""Class for a HomeMatic program button."""
|
|
13
|
+
|
|
14
|
+
__slots__ = ()
|
|
15
|
+
|
|
16
|
+
_category = DataPointCategory.HUB_BUTTON
|
|
17
|
+
|
|
18
|
+
@state_property
|
|
19
|
+
def available(self) -> bool:
|
|
20
|
+
"""Return the availability of the device."""
|
|
21
|
+
return self._is_active and self._central.available
|
|
22
|
+
|
|
23
|
+
@inspector()
|
|
24
|
+
async def press(self) -> None:
|
|
25
|
+
"""Handle the button press."""
|
|
26
|
+
await self.central.execute_program(pid=self.pid)
|