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.

Files changed (77) hide show
  1. aiohomematic/__init__.py +47 -0
  2. aiohomematic/async_support.py +146 -0
  3. aiohomematic/caches/__init__.py +10 -0
  4. aiohomematic/caches/dynamic.py +554 -0
  5. aiohomematic/caches/persistent.py +459 -0
  6. aiohomematic/caches/visibility.py +774 -0
  7. aiohomematic/central/__init__.py +2034 -0
  8. aiohomematic/central/decorators.py +110 -0
  9. aiohomematic/central/xml_rpc_server.py +267 -0
  10. aiohomematic/client/__init__.py +1746 -0
  11. aiohomematic/client/json_rpc.py +1193 -0
  12. aiohomematic/client/xml_rpc.py +222 -0
  13. aiohomematic/const.py +795 -0
  14. aiohomematic/context.py +8 -0
  15. aiohomematic/converter.py +82 -0
  16. aiohomematic/decorators.py +188 -0
  17. aiohomematic/exceptions.py +145 -0
  18. aiohomematic/hmcli.py +159 -0
  19. aiohomematic/model/__init__.py +137 -0
  20. aiohomematic/model/calculated/__init__.py +65 -0
  21. aiohomematic/model/calculated/climate.py +230 -0
  22. aiohomematic/model/calculated/data_point.py +319 -0
  23. aiohomematic/model/calculated/operating_voltage_level.py +311 -0
  24. aiohomematic/model/calculated/support.py +174 -0
  25. aiohomematic/model/custom/__init__.py +175 -0
  26. aiohomematic/model/custom/climate.py +1334 -0
  27. aiohomematic/model/custom/const.py +146 -0
  28. aiohomematic/model/custom/cover.py +741 -0
  29. aiohomematic/model/custom/data_point.py +318 -0
  30. aiohomematic/model/custom/definition.py +861 -0
  31. aiohomematic/model/custom/light.py +1092 -0
  32. aiohomematic/model/custom/lock.py +389 -0
  33. aiohomematic/model/custom/siren.py +268 -0
  34. aiohomematic/model/custom/support.py +40 -0
  35. aiohomematic/model/custom/switch.py +172 -0
  36. aiohomematic/model/custom/valve.py +112 -0
  37. aiohomematic/model/data_point.py +1109 -0
  38. aiohomematic/model/decorators.py +173 -0
  39. aiohomematic/model/device.py +1347 -0
  40. aiohomematic/model/event.py +210 -0
  41. aiohomematic/model/generic/__init__.py +211 -0
  42. aiohomematic/model/generic/action.py +32 -0
  43. aiohomematic/model/generic/binary_sensor.py +28 -0
  44. aiohomematic/model/generic/button.py +25 -0
  45. aiohomematic/model/generic/data_point.py +162 -0
  46. aiohomematic/model/generic/number.py +73 -0
  47. aiohomematic/model/generic/select.py +36 -0
  48. aiohomematic/model/generic/sensor.py +72 -0
  49. aiohomematic/model/generic/switch.py +52 -0
  50. aiohomematic/model/generic/text.py +27 -0
  51. aiohomematic/model/hub/__init__.py +334 -0
  52. aiohomematic/model/hub/binary_sensor.py +22 -0
  53. aiohomematic/model/hub/button.py +26 -0
  54. aiohomematic/model/hub/data_point.py +332 -0
  55. aiohomematic/model/hub/number.py +37 -0
  56. aiohomematic/model/hub/select.py +47 -0
  57. aiohomematic/model/hub/sensor.py +35 -0
  58. aiohomematic/model/hub/switch.py +42 -0
  59. aiohomematic/model/hub/text.py +28 -0
  60. aiohomematic/model/support.py +599 -0
  61. aiohomematic/model/update.py +136 -0
  62. aiohomematic/py.typed +0 -0
  63. aiohomematic/rega_scripts/fetch_all_device_data.fn +75 -0
  64. aiohomematic/rega_scripts/get_program_descriptions.fn +30 -0
  65. aiohomematic/rega_scripts/get_serial.fn +44 -0
  66. aiohomematic/rega_scripts/get_system_variable_descriptions.fn +30 -0
  67. aiohomematic/rega_scripts/set_program_state.fn +12 -0
  68. aiohomematic/rega_scripts/set_system_variable.fn +15 -0
  69. aiohomematic/support.py +482 -0
  70. aiohomematic/validator.py +65 -0
  71. aiohomematic-2025.8.6.dist-info/METADATA +69 -0
  72. aiohomematic-2025.8.6.dist-info/RECORD +77 -0
  73. aiohomematic-2025.8.6.dist-info/WHEEL +5 -0
  74. aiohomematic-2025.8.6.dist-info/licenses/LICENSE +21 -0
  75. aiohomematic-2025.8.6.dist-info/top_level.txt +2 -0
  76. aiohomematic_support/__init__.py +1 -0
  77. aiohomematic_support/client_local.py +349 -0
@@ -0,0 +1,8 @@
1
+ """Collection of context variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextvars import ContextVar
6
+
7
+ # context var for storing if call is running within a service
8
+ IN_SERVICE_VAR: ContextVar[bool] = ContextVar("in_service_var", default=False)
@@ -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()