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
aiohomematic/context.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Converters used by aiohomematic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Final, cast
|
|
8
|
+
|
|
9
|
+
from aiohomematic.const import Parameter
|
|
10
|
+
from aiohomematic.support import extract_exc_args
|
|
11
|
+
|
|
12
|
+
_LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _convert_cpv_to_hm_level(cpv: Any) -> Any:
|
|
16
|
+
"""Convert combined parameter value for hm level."""
|
|
17
|
+
if isinstance(cpv, str) and cpv.startswith("0x"):
|
|
18
|
+
return ast.literal_eval(cpv) / 100 / 2
|
|
19
|
+
return cpv
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _convert_cpv_to_hmip_level(cpv: Any) -> Any:
|
|
23
|
+
"""Convert combined parameter value for hmip level."""
|
|
24
|
+
return int(cpv) / 100
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def convert_hm_level_to_cpv(hm_level: Any) -> Any:
|
|
28
|
+
"""Convert hm level to combined parameter value."""
|
|
29
|
+
return format(int(hm_level * 100 * 2), "#04x")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
CONVERTABLE_PARAMETERS: Final = (Parameter.COMBINED_PARAMETER, Parameter.LEVEL_COMBINED)
|
|
33
|
+
|
|
34
|
+
_COMBINED_PARAMETER_TO_HM_CONVERTER: Final = {
|
|
35
|
+
Parameter.LEVEL_COMBINED: _convert_cpv_to_hm_level,
|
|
36
|
+
Parameter.LEVEL: _convert_cpv_to_hmip_level,
|
|
37
|
+
Parameter.LEVEL_2: _convert_cpv_to_hmip_level,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_COMBINED_PARAMETER_NAMES: Final = {"L": Parameter.LEVEL, "L2": Parameter.LEVEL_2}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _convert_combined_parameter_to_paramset(cpv: str) -> dict[str, Any]:
|
|
44
|
+
"""Convert combined parameter to paramset."""
|
|
45
|
+
paramset: dict[str, Any] = {}
|
|
46
|
+
for cp_param_value in cpv.split(","):
|
|
47
|
+
cp_param, value = cp_param_value.split("=")
|
|
48
|
+
if parameter := _COMBINED_PARAMETER_NAMES.get(cp_param):
|
|
49
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(parameter):
|
|
50
|
+
paramset[parameter] = converter(value)
|
|
51
|
+
else:
|
|
52
|
+
paramset[parameter] = value
|
|
53
|
+
return paramset
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _convert_level_combined_to_paramset(lcv: str) -> dict[str, Any]:
|
|
57
|
+
"""Convert combined parameter to paramset."""
|
|
58
|
+
if "," in lcv:
|
|
59
|
+
l1_value, l2_value = lcv.split(",")
|
|
60
|
+
if converter := _COMBINED_PARAMETER_TO_HM_CONVERTER.get(Parameter.LEVEL_COMBINED):
|
|
61
|
+
return {
|
|
62
|
+
Parameter.LEVEL: converter(l1_value),
|
|
63
|
+
Parameter.LEVEL_SLATS: converter(l2_value),
|
|
64
|
+
}
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_COMBINED_PARAMETER_TO_PARAMSET_CONVERTER: Final = {
|
|
69
|
+
Parameter.COMBINED_PARAMETER: _convert_combined_parameter_to_paramset,
|
|
70
|
+
Parameter.LEVEL_COMBINED: _convert_level_combined_to_paramset,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def convert_combined_parameter_to_paramset(parameter: str, cpv: str) -> dict[str, Any]:
|
|
75
|
+
"""Convert combined parameter to paramset."""
|
|
76
|
+
try:
|
|
77
|
+
if converter := _COMBINED_PARAMETER_TO_PARAMSET_CONVERTER.get(parameter): # type: ignore[call-overload]
|
|
78
|
+
return cast(dict[str, Any], converter(cpv))
|
|
79
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: No converter found for %s: %s", parameter, cpv)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
_LOGGER.debug("CONVERT_COMBINED_PARAMETER_TO_PARAMSET: Convert failed %s", extract_exc_args(exc=exc))
|
|
82
|
+
return {}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Common Decorators used within aiohomematic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
from time import monotonic
|
|
10
|
+
from typing import Any, Final, ParamSpec, TypeVar, cast
|
|
11
|
+
|
|
12
|
+
from aiohomematic.context import IN_SERVICE_VAR
|
|
13
|
+
from aiohomematic.exceptions import BaseHomematicException
|
|
14
|
+
from aiohomematic.support import extract_exc_args
|
|
15
|
+
|
|
16
|
+
P = ParamSpec("P")
|
|
17
|
+
R = TypeVar("R")
|
|
18
|
+
|
|
19
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def inspector( # noqa: C901
|
|
23
|
+
log_level: int = logging.ERROR,
|
|
24
|
+
re_raise: bool = True,
|
|
25
|
+
no_raise_return: Any = None,
|
|
26
|
+
measure_performance: bool = False,
|
|
27
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
28
|
+
"""
|
|
29
|
+
Support with exception handling and performance measurement.
|
|
30
|
+
|
|
31
|
+
A decorator that works for both synchronous and asynchronous functions,
|
|
32
|
+
providing common functionality such as exception handling and performance measurement.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
log_level: Logging level for exceptions.
|
|
36
|
+
re_raise: Whether to re-raise exceptions.
|
|
37
|
+
no_raise_return: Value to return when an exception is caught and not re-raised.
|
|
38
|
+
measure_performance: Whether to measure function execution time.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A decorator that wraps sync or async functions.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def create_wrapped_decorator(func: Callable[P, R]) -> Callable[P, R]: # noqa: C901
|
|
46
|
+
"""
|
|
47
|
+
Decorate function for wrapping sync or async functions.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
func: The function to decorate.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The decorated function.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def handle_exception(exc: Exception, func: Callable, is_sub_service_call: bool, is_homematic: bool) -> R:
|
|
58
|
+
"""Handle exceptions for decorated functions."""
|
|
59
|
+
if not is_sub_service_call and log_level > logging.NOTSET:
|
|
60
|
+
message = f"{func.__name__.upper()} failed: {extract_exc_args(exc=exc)}"
|
|
61
|
+
logging.getLogger(func.__module__).log(level=log_level, msg=message)
|
|
62
|
+
if re_raise or not is_homematic:
|
|
63
|
+
raise exc
|
|
64
|
+
return cast(R, no_raise_return)
|
|
65
|
+
|
|
66
|
+
@wraps(func)
|
|
67
|
+
def wrap_sync_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
68
|
+
"""Wrap sync functions."""
|
|
69
|
+
|
|
70
|
+
start = monotonic() if measure_performance and _LOGGER.isEnabledFor(level=logging.DEBUG) else None
|
|
71
|
+
token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
|
|
72
|
+
try:
|
|
73
|
+
return_value: R = func(*args, **kwargs)
|
|
74
|
+
except BaseHomematicException as bhexc:
|
|
75
|
+
if token:
|
|
76
|
+
IN_SERVICE_VAR.reset(token)
|
|
77
|
+
return handle_exception(
|
|
78
|
+
exc=bhexc, func=func, is_sub_service_call=IN_SERVICE_VAR.get(), is_homematic=True
|
|
79
|
+
)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
if token:
|
|
82
|
+
IN_SERVICE_VAR.reset(token)
|
|
83
|
+
return handle_exception(
|
|
84
|
+
exc=exc, func=func, is_sub_service_call=IN_SERVICE_VAR.get(), is_homematic=False
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
if token:
|
|
88
|
+
IN_SERVICE_VAR.reset(token)
|
|
89
|
+
return return_value
|
|
90
|
+
finally:
|
|
91
|
+
if start:
|
|
92
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
93
|
+
|
|
94
|
+
@wraps(func)
|
|
95
|
+
async def wrap_async_function(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
96
|
+
"""Wrap async functions."""
|
|
97
|
+
|
|
98
|
+
start = monotonic() if measure_performance and _LOGGER.isEnabledFor(level=logging.DEBUG) else None
|
|
99
|
+
token = IN_SERVICE_VAR.set(True) if not IN_SERVICE_VAR.get() else None
|
|
100
|
+
try:
|
|
101
|
+
return_value = await func(*args, **kwargs) # type: ignore[misc] # Await the async call
|
|
102
|
+
except BaseHomematicException as bhexc:
|
|
103
|
+
if token:
|
|
104
|
+
IN_SERVICE_VAR.reset(token)
|
|
105
|
+
return handle_exception(
|
|
106
|
+
exc=bhexc, func=func, is_sub_service_call=IN_SERVICE_VAR.get(), is_homematic=True
|
|
107
|
+
)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
if token:
|
|
110
|
+
IN_SERVICE_VAR.reset(token)
|
|
111
|
+
return handle_exception(
|
|
112
|
+
exc=exc, func=func, is_sub_service_call=IN_SERVICE_VAR.get(), is_homematic=False
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
if token:
|
|
116
|
+
IN_SERVICE_VAR.reset(token)
|
|
117
|
+
return cast(R, return_value)
|
|
118
|
+
finally:
|
|
119
|
+
if start:
|
|
120
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
121
|
+
|
|
122
|
+
# Check if the function is a coroutine or not and select the appropriate wrapper
|
|
123
|
+
if inspect.iscoroutinefunction(func):
|
|
124
|
+
setattr(wrap_async_function, "ha_service", True)
|
|
125
|
+
return wrap_async_function # type: ignore[return-value]
|
|
126
|
+
setattr(wrap_sync_function, "ha_service", True)
|
|
127
|
+
return wrap_sync_function
|
|
128
|
+
|
|
129
|
+
return create_wrapped_decorator
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _log_performance_message(func: Callable, start: float, *args: P.args, **kwargs: P.kwargs) -> None: # type: ignore[valid-type]
|
|
133
|
+
delta = monotonic() - start
|
|
134
|
+
caller = str(args[0]) if len(args) > 0 else ""
|
|
135
|
+
|
|
136
|
+
iface: str = ""
|
|
137
|
+
if interface := str(kwargs.get("interface", "")):
|
|
138
|
+
iface = f"interface: {interface}"
|
|
139
|
+
if interface_id := kwargs.get("interface_id", ""):
|
|
140
|
+
iface = f"interface_id: {interface_id}"
|
|
141
|
+
|
|
142
|
+
message = f"Execution of {func.__name__.upper()} took {delta}s from {caller}"
|
|
143
|
+
if iface:
|
|
144
|
+
message += f"/{iface}"
|
|
145
|
+
|
|
146
|
+
_LOGGER.info(message)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_service_calls(obj: object) -> dict[str, Callable]:
|
|
150
|
+
"""Get all methods decorated with the "bind_collector" or "service_call" decorator."""
|
|
151
|
+
return {
|
|
152
|
+
name: getattr(obj, name)
|
|
153
|
+
for name in dir(obj)
|
|
154
|
+
if not name.startswith("_")
|
|
155
|
+
and name not in ("service_methods", "service_method_names")
|
|
156
|
+
and callable(getattr(obj, name))
|
|
157
|
+
and hasattr(getattr(obj, name), "ha_service")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def measure_execution_time[CallableT: Callable[..., Any]](func: CallableT) -> CallableT:
|
|
162
|
+
"""Decorate function to measure the function execution time."""
|
|
163
|
+
|
|
164
|
+
@wraps(func)
|
|
165
|
+
async def async_measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
166
|
+
"""Wrap method."""
|
|
167
|
+
|
|
168
|
+
start = monotonic() if _LOGGER.isEnabledFor(level=logging.DEBUG) else None
|
|
169
|
+
try:
|
|
170
|
+
return await func(*args, **kwargs)
|
|
171
|
+
finally:
|
|
172
|
+
if start:
|
|
173
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
174
|
+
|
|
175
|
+
@wraps(func)
|
|
176
|
+
def measure_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
177
|
+
"""Wrap method."""
|
|
178
|
+
|
|
179
|
+
start = monotonic() if _LOGGER.isEnabledFor(level=logging.DEBUG) else None
|
|
180
|
+
try:
|
|
181
|
+
return func(*args, **kwargs)
|
|
182
|
+
finally:
|
|
183
|
+
if start:
|
|
184
|
+
_log_performance_message(func, start, *args, **kwargs)
|
|
185
|
+
|
|
186
|
+
if inspect.iscoroutinefunction(func):
|
|
187
|
+
return async_measure_wrapper # type: ignore[return-value]
|
|
188
|
+
return measure_wrapper # type: ignore[return-value]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Module for AioHomematicExceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from functools import wraps
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Final, cast
|
|
10
|
+
|
|
11
|
+
_LOGGER: Final = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseHomematicException(Exception):
|
|
15
|
+
"""aiohomematic base exception."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: str, *args: Any) -> None:
|
|
18
|
+
"""Init the AioHomematicException."""
|
|
19
|
+
if args and isinstance(args[0], BaseException):
|
|
20
|
+
self.name = args[0].__class__.__name__
|
|
21
|
+
args = _reduce_args(args=args[0].args)
|
|
22
|
+
else:
|
|
23
|
+
self.name = name
|
|
24
|
+
super().__init__(_reduce_args(args=args))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClientException(BaseHomematicException):
|
|
28
|
+
"""aiohomematic Client exception."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *args: Any) -> None:
|
|
31
|
+
"""Init the ClientException."""
|
|
32
|
+
super().__init__("ClientException", *args)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UnsupportedException(BaseHomematicException):
|
|
36
|
+
"""aiohomematic unsupported exception."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args: Any) -> None:
|
|
39
|
+
"""Init the UnsupportedException."""
|
|
40
|
+
super().__init__("UnsupportedException", *args)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidationException(BaseHomematicException):
|
|
44
|
+
"""aiohomematic validation exception."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *args: Any) -> None:
|
|
47
|
+
"""Init the ValidationException."""
|
|
48
|
+
super().__init__("ValidationException", *args)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NoConnectionException(BaseHomematicException):
|
|
52
|
+
"""aiohomematic NoConnectionException exception."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, *args: Any) -> None:
|
|
55
|
+
"""Init the NoConnection."""
|
|
56
|
+
super().__init__("NoConnectionException", *args)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NoClientsException(BaseHomematicException):
|
|
60
|
+
"""aiohomematic NoClientsException exception."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, *args: Any) -> None:
|
|
63
|
+
"""Init the NoClientsException."""
|
|
64
|
+
super().__init__("NoClientsException", *args)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AuthFailure(BaseHomematicException):
|
|
68
|
+
"""aiohomematic AuthFailure exception."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, *args: Any) -> None:
|
|
71
|
+
"""Init the AuthFailure."""
|
|
72
|
+
super().__init__("AuthFailure", *args)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AioHomematicException(BaseHomematicException):
|
|
76
|
+
"""aiohomematic AioHomematicException exception."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, *args: Any) -> None:
|
|
79
|
+
"""Init the AioHomematicException."""
|
|
80
|
+
super().__init__("AioHomematicException", *args)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AioHomematicConfigException(BaseHomematicException):
|
|
84
|
+
"""aiohomematic AioHomematicConfigException exception."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, *args: Any) -> None:
|
|
87
|
+
"""Init the AioHomematicConfigException."""
|
|
88
|
+
super().__init__("AioHomematicConfigException", *args)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class InternalBackendException(BaseHomematicException):
|
|
92
|
+
"""aiohomematic InternalBackendException exception."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, *args: Any) -> None:
|
|
95
|
+
"""Init the InternalBackendException."""
|
|
96
|
+
super().__init__("InternalBackendException", *args)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _reduce_args(args: tuple[Any, ...]) -> tuple[Any, ...] | Any:
|
|
100
|
+
"""Return the first arg, if there is only one arg."""
|
|
101
|
+
return args[0] if len(args) == 1 else args
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def log_exception[**P, R](
|
|
105
|
+
exc_type: type[BaseException],
|
|
106
|
+
logger: logging.Logger = _LOGGER,
|
|
107
|
+
level: int = logging.ERROR,
|
|
108
|
+
extra_msg: str = "",
|
|
109
|
+
re_raise: bool = False,
|
|
110
|
+
exc_return: Any = None,
|
|
111
|
+
) -> Callable:
|
|
112
|
+
"""Decorate methods for exception logging."""
|
|
113
|
+
|
|
114
|
+
def decorator_log_exception(
|
|
115
|
+
func: Callable[P, R | Awaitable[R]],
|
|
116
|
+
) -> Callable[P, R | Awaitable[R]]:
|
|
117
|
+
"""Decorate log exception method."""
|
|
118
|
+
|
|
119
|
+
function_name = func.__name__
|
|
120
|
+
|
|
121
|
+
@wraps(func)
|
|
122
|
+
async def async_wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
123
|
+
"""Wrap async methods."""
|
|
124
|
+
try:
|
|
125
|
+
return_value = cast(R, await func(*args, **kwargs)) # type: ignore[misc]
|
|
126
|
+
except exc_type as exc:
|
|
127
|
+
message = (
|
|
128
|
+
f"{function_name.upper()} failed: {exc_type.__name__} [{_reduce_args(args=exc.args)}] {extra_msg}"
|
|
129
|
+
)
|
|
130
|
+
logger.log(level, message)
|
|
131
|
+
if re_raise:
|
|
132
|
+
raise
|
|
133
|
+
return cast(R, exc_return)
|
|
134
|
+
return return_value
|
|
135
|
+
|
|
136
|
+
@wraps(func)
|
|
137
|
+
def wrapper_log_exception(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
138
|
+
"""Wrap sync methods."""
|
|
139
|
+
return cast(R, func(*args, **kwargs))
|
|
140
|
+
|
|
141
|
+
if inspect.iscoroutinefunction(func):
|
|
142
|
+
return async_wrapper_log_exception
|
|
143
|
+
return wrapper_log_exception
|
|
144
|
+
|
|
145
|
+
return decorator_log_exception
|
aiohomematic/hmcli.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Commandline tool to query HomeMatic hubs via XML-RPC."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any
|
|
9
|
+
from xmlrpc.client import ServerProxy
|
|
10
|
+
|
|
11
|
+
from aiohomematic import __version__
|
|
12
|
+
from aiohomematic.const import ParamsetKey
|
|
13
|
+
from aiohomematic.support import build_xml_rpc_headers, build_xml_rpc_uri, get_tls_context
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Start the cli."""
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
description="Commandline tool to query HomeMatic hubs via XML-RPC",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--host",
|
|
24
|
+
"-H",
|
|
25
|
+
required=True,
|
|
26
|
+
type=str,
|
|
27
|
+
help="Hostname / IP address to connect to",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--port",
|
|
31
|
+
"-p",
|
|
32
|
+
required=True,
|
|
33
|
+
type=int,
|
|
34
|
+
help="Port to connect to",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--path",
|
|
38
|
+
type=str,
|
|
39
|
+
help="Path, used for heating groups",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--username",
|
|
43
|
+
"-U",
|
|
44
|
+
nargs="?",
|
|
45
|
+
help="Username required for access",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--password",
|
|
49
|
+
"-P",
|
|
50
|
+
nargs="?",
|
|
51
|
+
help="Password required for access",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--tls",
|
|
55
|
+
"-t",
|
|
56
|
+
action="store_true",
|
|
57
|
+
help="Enable TLS encryption",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--verify",
|
|
61
|
+
"-v",
|
|
62
|
+
action="store_true",
|
|
63
|
+
help="Verify TLS encryption",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--json",
|
|
67
|
+
"-j",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Output as JSON",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--address",
|
|
73
|
+
"-a",
|
|
74
|
+
required=True,
|
|
75
|
+
type=str,
|
|
76
|
+
help="Address of HomeMatic device, including channel",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--paramset_key",
|
|
80
|
+
default=ParamsetKey.VALUES,
|
|
81
|
+
choices=[ParamsetKey.VALUES, ParamsetKey.MASTER],
|
|
82
|
+
help="Paramset of HomeMatic device. Default: VALUES",
|
|
83
|
+
)
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--parameter",
|
|
86
|
+
required=True,
|
|
87
|
+
help="Parameter of HomeMatic device",
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--value",
|
|
91
|
+
type=str,
|
|
92
|
+
help="Value to set for parameter. Use 0/1 for boolean",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--type",
|
|
96
|
+
choices=["int", "float", "bool"],
|
|
97
|
+
help="Type of value when setting a value. Using str if not provided",
|
|
98
|
+
)
|
|
99
|
+
args = parser.parse_args()
|
|
100
|
+
|
|
101
|
+
url = build_xml_rpc_uri(
|
|
102
|
+
host=args.host,
|
|
103
|
+
port=args.port,
|
|
104
|
+
path=args.path,
|
|
105
|
+
tls=args.tls,
|
|
106
|
+
)
|
|
107
|
+
headers = build_xml_rpc_headers(username=args.username, password=args.password)
|
|
108
|
+
context = None
|
|
109
|
+
if args.tls:
|
|
110
|
+
context = get_tls_context(verify_tls=args.verify)
|
|
111
|
+
proxy = ServerProxy(url, context=context, headers=headers)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
if args.paramset_key == ParamsetKey.VALUES and args.value is None:
|
|
115
|
+
proxy.getValue(args.address, args.parameter)
|
|
116
|
+
if args.json:
|
|
117
|
+
pass
|
|
118
|
+
else:
|
|
119
|
+
pass
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
elif args.paramset_key == ParamsetKey.VALUES and args.value:
|
|
122
|
+
value: Any
|
|
123
|
+
if args.type == "int":
|
|
124
|
+
value = int(args.value)
|
|
125
|
+
elif args.type == "float":
|
|
126
|
+
value = float(args.value)
|
|
127
|
+
elif args.type == "bool":
|
|
128
|
+
value = bool(int(args.value))
|
|
129
|
+
else:
|
|
130
|
+
value = args.value
|
|
131
|
+
proxy.setValue(args.address, args.parameter, value)
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
elif args.paramset_key == ParamsetKey.MASTER and args.value is None:
|
|
134
|
+
paramset: dict[str, Any] | None
|
|
135
|
+
if (paramset := proxy.getParamset(args.address, args.paramset_key)) and paramset.get( # type: ignore[assignment]
|
|
136
|
+
args.parameter
|
|
137
|
+
):
|
|
138
|
+
if args.json:
|
|
139
|
+
pass
|
|
140
|
+
else:
|
|
141
|
+
pass
|
|
142
|
+
sys.exit(0)
|
|
143
|
+
elif args.paramset_key == ParamsetKey.MASTER and args.value:
|
|
144
|
+
if args.type == "int":
|
|
145
|
+
value = int(args.value)
|
|
146
|
+
elif args.type == "float":
|
|
147
|
+
value = float(args.value)
|
|
148
|
+
elif args.type == "bool":
|
|
149
|
+
value = bool(int(args.value))
|
|
150
|
+
else:
|
|
151
|
+
value = args.value
|
|
152
|
+
proxy.putParamset(args.address, args.paramset_key, {args.parameter: value})
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
except Exception:
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|