aiohomematic 2025.9.1__py3-none-any.whl → 2025.9.2__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/caches/dynamic.py +1 -6
- aiohomematic/central/__init__.py +34 -23
- aiohomematic/central/xml_rpc_server.py +1 -1
- aiohomematic/client/__init__.py +35 -29
- aiohomematic/client/json_rpc.py +44 -12
- aiohomematic/client/xml_rpc.py +53 -20
- aiohomematic/const.py +2 -2
- aiohomematic/decorators.py +56 -21
- aiohomematic/model/__init__.py +1 -1
- aiohomematic/model/calculated/__init__.py +1 -1
- aiohomematic/model/calculated/climate.py +1 -1
- aiohomematic/model/calculated/data_point.py +2 -2
- aiohomematic/model/calculated/operating_voltage_level.py +7 -21
- aiohomematic/model/calculated/support.py +20 -0
- aiohomematic/model/custom/__init__.py +1 -1
- aiohomematic/model/custom/climate.py +18 -18
- aiohomematic/model/custom/cover.py +1 -1
- aiohomematic/model/custom/data_point.py +1 -1
- aiohomematic/model/custom/light.py +1 -1
- aiohomematic/model/custom/lock.py +1 -1
- aiohomematic/model/custom/siren.py +1 -1
- aiohomematic/model/custom/switch.py +1 -1
- aiohomematic/model/custom/valve.py +1 -1
- aiohomematic/model/data_point.py +18 -18
- aiohomematic/model/device.py +21 -20
- aiohomematic/model/event.py +3 -8
- aiohomematic/model/generic/__init__.py +1 -1
- aiohomematic/model/generic/binary_sensor.py +1 -1
- aiohomematic/model/generic/button.py +1 -1
- aiohomematic/model/generic/data_point.py +3 -5
- aiohomematic/model/generic/number.py +1 -1
- aiohomematic/model/generic/select.py +1 -1
- aiohomematic/model/generic/sensor.py +1 -1
- aiohomematic/model/generic/switch.py +4 -4
- aiohomematic/model/generic/text.py +1 -1
- aiohomematic/model/hub/binary_sensor.py +1 -1
- aiohomematic/model/hub/button.py +2 -2
- aiohomematic/model/hub/data_point.py +4 -7
- aiohomematic/model/hub/number.py +1 -1
- aiohomematic/model/hub/select.py +2 -2
- aiohomematic/model/hub/sensor.py +1 -1
- aiohomematic/model/hub/switch.py +3 -3
- aiohomematic/model/hub/text.py +1 -1
- aiohomematic/model/support.py +1 -40
- aiohomematic/model/update.py +5 -4
- aiohomematic/property_decorators.py +327 -0
- aiohomematic/support.py +70 -85
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/METADATA +7 -5
- aiohomematic-2025.9.2.dist-info/RECORD +78 -0
- aiohomematic_support/client_local.py +5 -5
- aiohomematic/model/decorators.py +0 -194
- aiohomematic-2025.9.1.dist-info/RECORD +0 -78
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/WHEEL +0 -0
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/licenses/LICENSE +0 -0
- {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/top_level.txt +0 -0
aiohomematic/caches/dynamic.py
CHANGED
|
@@ -27,13 +27,11 @@ from collections.abc import Mapping
|
|
|
27
27
|
from datetime import datetime
|
|
28
28
|
import logging
|
|
29
29
|
from typing import Any, Final, cast
|
|
30
|
-
from urllib.parse import unquote
|
|
31
30
|
|
|
32
31
|
from aiohomematic import central as hmcu
|
|
33
32
|
from aiohomematic.const import (
|
|
34
33
|
DP_KEY_VALUE,
|
|
35
34
|
INIT_DATETIME,
|
|
36
|
-
ISO_8859_1,
|
|
37
35
|
LAST_COMMAND_SEND_STORE_TIMEOUT,
|
|
38
36
|
MAX_CACHE_AGE,
|
|
39
37
|
NO_CACHE_ENTRY,
|
|
@@ -312,10 +310,7 @@ class CentralDataCache:
|
|
|
312
310
|
|
|
313
311
|
def add_data(self, interface: Interface, all_device_data: Mapping[str, Any]) -> None:
|
|
314
312
|
"""Add data to cache."""
|
|
315
|
-
self._value_cache[interface] =
|
|
316
|
-
unquote(string=k, encoding=ISO_8859_1): unquote(string=v, encoding=ISO_8859_1) if isinstance(v, str) else v
|
|
317
|
-
for k, v in all_device_data.items()
|
|
318
|
-
}
|
|
313
|
+
self._value_cache[interface] = all_device_data
|
|
319
314
|
self._refreshed_at[interface] = datetime.now()
|
|
320
315
|
|
|
321
316
|
def get_data(
|
aiohomematic/central/__init__.py
CHANGED
|
@@ -149,7 +149,6 @@ from aiohomematic.exceptions import (
|
|
|
149
149
|
from aiohomematic.model import create_data_points_and_events
|
|
150
150
|
from aiohomematic.model.custom import CustomDataPoint, create_custom_data_points
|
|
151
151
|
from aiohomematic.model.data_point import BaseParameterDataPoint, CallbackDataPoint
|
|
152
|
-
from aiohomematic.model.decorators import info_property
|
|
153
152
|
from aiohomematic.model.device import Channel, Device
|
|
154
153
|
from aiohomematic.model.event import GenericEvent
|
|
155
154
|
from aiohomematic.model.generic import GenericDataPoint
|
|
@@ -160,8 +159,16 @@ from aiohomematic.model.hub import (
|
|
|
160
159
|
Hub,
|
|
161
160
|
ProgramDpType,
|
|
162
161
|
)
|
|
163
|
-
from aiohomematic.
|
|
164
|
-
from aiohomematic.support import
|
|
162
|
+
from aiohomematic.property_decorators import info_property
|
|
163
|
+
from aiohomematic.support import (
|
|
164
|
+
LogContextMixin,
|
|
165
|
+
PayloadMixin,
|
|
166
|
+
check_config,
|
|
167
|
+
extract_exc_args,
|
|
168
|
+
get_channel_no,
|
|
169
|
+
get_device_address,
|
|
170
|
+
get_ip_addr,
|
|
171
|
+
)
|
|
165
172
|
|
|
166
173
|
__all__ = ["CentralConfig", "CentralUnit", "INTERFACE_EVENT_SCHEMA"]
|
|
167
174
|
|
|
@@ -183,7 +190,7 @@ INTERFACE_EVENT_SCHEMA = vol.Schema(
|
|
|
183
190
|
)
|
|
184
191
|
|
|
185
192
|
|
|
186
|
-
class CentralUnit(PayloadMixin):
|
|
193
|
+
class CentralUnit(LogContextMixin, PayloadMixin):
|
|
187
194
|
"""Central unit that collects everything to handle communication from/to CCU/Homegear."""
|
|
188
195
|
|
|
189
196
|
def __init__(self, central_config: CentralConfig) -> None:
|
|
@@ -212,7 +219,7 @@ class CentralUnit(PayloadMixin):
|
|
|
212
219
|
# {interface_id, client}
|
|
213
220
|
self._clients: Final[dict[str, hmcl.Client]] = {}
|
|
214
221
|
self._data_point_key_event_subscriptions: Final[
|
|
215
|
-
dict[DataPointKey, list[Callable[[Any], Coroutine[Any, Any, None]]]]
|
|
222
|
+
dict[DataPointKey, list[Callable[[Any, datetime], Coroutine[Any, Any, None]]]]
|
|
216
223
|
] = {}
|
|
217
224
|
self._data_point_path_event_subscriptions: Final[dict[str, DataPointKey]] = {}
|
|
218
225
|
self._sysvar_data_point_event_subscriptions: Final[dict[str, Callable]] = {}
|
|
@@ -237,7 +244,7 @@ class CentralUnit(PayloadMixin):
|
|
|
237
244
|
self._hub: Hub = Hub(central=self)
|
|
238
245
|
self._version: str | None = None
|
|
239
246
|
# store last event received datetime by interface_id
|
|
240
|
-
self.
|
|
247
|
+
self._last_event_seen_for_interface: Final[dict[str, datetime]] = {}
|
|
241
248
|
self._xml_rpc_callback_ip: str = IP_ANY_V4
|
|
242
249
|
self._listen_ip_addr: str = IP_ANY_V4
|
|
243
250
|
self._listen_port: int = PORT_ANY
|
|
@@ -252,7 +259,7 @@ class CentralUnit(PayloadMixin):
|
|
|
252
259
|
"""Return the xml rpc server callback ip address."""
|
|
253
260
|
return self._xml_rpc_callback_ip
|
|
254
261
|
|
|
255
|
-
@info_property
|
|
262
|
+
@info_property(log_context=True)
|
|
256
263
|
def url(self) -> str:
|
|
257
264
|
"""Return the central url."""
|
|
258
265
|
return self._url
|
|
@@ -362,14 +369,14 @@ class CentralUnit(PayloadMixin):
|
|
|
362
369
|
"""Return the loop support."""
|
|
363
370
|
return self._looper
|
|
364
371
|
|
|
365
|
-
@info_property
|
|
372
|
+
@info_property(log_context=True)
|
|
366
373
|
def model(self) -> str | None:
|
|
367
374
|
"""Return the model of the backend."""
|
|
368
375
|
if not self._model and (client := self.primary_client):
|
|
369
376
|
self._model = client.model
|
|
370
377
|
return self._model
|
|
371
378
|
|
|
372
|
-
@info_property
|
|
379
|
+
@info_property(log_context=True)
|
|
373
380
|
def name(self) -> str:
|
|
374
381
|
"""Return the name of the backend."""
|
|
375
382
|
return self._config.name
|
|
@@ -1109,7 +1116,7 @@ class CentralUnit(PayloadMixin):
|
|
|
1109
1116
|
if not self.has_client(interface_id=interface_id):
|
|
1110
1117
|
return
|
|
1111
1118
|
|
|
1112
|
-
self.
|
|
1119
|
+
self.set_last_event_seen_for_interface(interface_id=interface_id)
|
|
1113
1120
|
# No need to check the response of a XmlRPC-PING
|
|
1114
1121
|
if parameter == Parameter.PONG:
|
|
1115
1122
|
if "#" in value:
|
|
@@ -1133,9 +1140,10 @@ class CentralUnit(PayloadMixin):
|
|
|
1133
1140
|
|
|
1134
1141
|
if dpk in self._data_point_key_event_subscriptions:
|
|
1135
1142
|
try:
|
|
1143
|
+
received_at = datetime.now()
|
|
1136
1144
|
for callback_handler in self._data_point_key_event_subscriptions[dpk]:
|
|
1137
1145
|
if callable(callback_handler):
|
|
1138
|
-
await callback_handler(value)
|
|
1146
|
+
await callback_handler(value, received_at)
|
|
1139
1147
|
except RuntimeError as rterr: # pragma: no cover
|
|
1140
1148
|
_LOGGER_EVENT.debug(
|
|
1141
1149
|
"EVENT: RuntimeError [%s]. Failed to call callback for: %s, %s, %s",
|
|
@@ -1184,7 +1192,10 @@ class CentralUnit(PayloadMixin):
|
|
|
1184
1192
|
try:
|
|
1185
1193
|
callback_handler = self._sysvar_data_point_event_subscriptions[state_path]
|
|
1186
1194
|
if callable(callback_handler):
|
|
1187
|
-
|
|
1195
|
+
received_at = datetime.now()
|
|
1196
|
+
self._looper.create_task(
|
|
1197
|
+
callback_handler(value, received_at), name=f"sysvar-data-point-event-{state_path}"
|
|
1198
|
+
)
|
|
1188
1199
|
except RuntimeError as rterr: # pragma: no cover
|
|
1189
1200
|
_LOGGER_EVENT.debug(
|
|
1190
1201
|
"EVENT: RuntimeError [%s]. Failed to call callback for: %s",
|
|
@@ -1207,7 +1218,7 @@ class CentralUnit(PayloadMixin):
|
|
|
1207
1218
|
|
|
1208
1219
|
def add_event_subscription(self, data_point: BaseParameterDataPoint) -> None:
|
|
1209
1220
|
"""Add data_point to central event subscription."""
|
|
1210
|
-
if isinstance(data_point,
|
|
1221
|
+
if isinstance(data_point, GenericDataPoint | GenericEvent) and (
|
|
1211
1222
|
data_point.is_readable or data_point.supports_events
|
|
1212
1223
|
):
|
|
1213
1224
|
if data_point.dpk not in self._data_point_key_event_subscriptions:
|
|
@@ -1219,13 +1230,13 @@ class CentralUnit(PayloadMixin):
|
|
|
1219
1230
|
):
|
|
1220
1231
|
self._data_point_path_event_subscriptions[data_point.state_path] = data_point.dpk
|
|
1221
1232
|
|
|
1222
|
-
@inspector
|
|
1233
|
+
@inspector
|
|
1223
1234
|
async def create_central_links(self) -> None:
|
|
1224
1235
|
"""Create a central links to support press events on all channels with click events."""
|
|
1225
1236
|
for device in self.devices:
|
|
1226
1237
|
await device.create_central_links()
|
|
1227
1238
|
|
|
1228
|
-
@inspector
|
|
1239
|
+
@inspector
|
|
1229
1240
|
async def remove_central_links(self) -> None:
|
|
1230
1241
|
"""Remove central links."""
|
|
1231
1242
|
for device in self.devices:
|
|
@@ -1248,19 +1259,19 @@ class CentralUnit(PayloadMixin):
|
|
|
1248
1259
|
|
|
1249
1260
|
def remove_event_subscription(self, data_point: BaseParameterDataPoint) -> None:
|
|
1250
1261
|
"""Remove event subscription from central collections."""
|
|
1251
|
-
if isinstance(data_point,
|
|
1262
|
+
if isinstance(data_point, GenericDataPoint | GenericEvent) and data_point.supports_events:
|
|
1252
1263
|
if data_point.dpk in self._data_point_key_event_subscriptions:
|
|
1253
1264
|
del self._data_point_key_event_subscriptions[data_point.dpk]
|
|
1254
1265
|
if data_point.state_path in self._data_point_path_event_subscriptions:
|
|
1255
1266
|
del self._data_point_path_event_subscriptions[data_point.state_path]
|
|
1256
1267
|
|
|
1257
|
-
def
|
|
1258
|
-
"""Return the last event
|
|
1259
|
-
return self.
|
|
1268
|
+
def get_last_event_seen_for_interface(self, interface_id: str) -> datetime | None:
|
|
1269
|
+
"""Return the last event seen for an interface."""
|
|
1270
|
+
return self._last_event_seen_for_interface.get(interface_id)
|
|
1260
1271
|
|
|
1261
|
-
def
|
|
1262
|
-
"""Set the last event
|
|
1263
|
-
self.
|
|
1272
|
+
def set_last_event_seen_for_interface(self, interface_id: str) -> None:
|
|
1273
|
+
"""Set the last event seen for an interface."""
|
|
1274
|
+
self._last_event_seen_for_interface[interface_id] = datetime.now()
|
|
1264
1275
|
|
|
1265
1276
|
async def execute_program(self, pid: str) -> bool:
|
|
1266
1277
|
"""Execute a program on CCU / Homegear."""
|
|
@@ -1703,7 +1714,7 @@ class _Scheduler(threading.Thread):
|
|
|
1703
1714
|
_LOGGER.debug("REFRESH_CLIENT_DATA: Loading data for %s", self._central.name)
|
|
1704
1715
|
for client in poll_clients:
|
|
1705
1716
|
await self._central.load_and_refresh_data_point_data(interface=client.interface)
|
|
1706
|
-
self._central.
|
|
1717
|
+
self._central.set_last_event_seen_for_interface(interface_id=client.interface_id)
|
|
1707
1718
|
|
|
1708
1719
|
@inspector(re_raise=False)
|
|
1709
1720
|
async def _refresh_sysvar_data(self) -> None:
|
|
@@ -57,7 +57,7 @@ class RPCFunctions:
|
|
|
57
57
|
action="error",
|
|
58
58
|
err=err,
|
|
59
59
|
level=logging.WARNING,
|
|
60
|
-
|
|
60
|
+
log_context={"interface_id": interface_id, "error_code": int(error_code)},
|
|
61
61
|
)
|
|
62
62
|
_LOGGER.warning(
|
|
63
63
|
"ERROR failed: interface_id = %s, error_code = %i, message = %s",
|
aiohomematic/client/__init__.py
CHANGED
|
@@ -93,7 +93,9 @@ from aiohomematic.decorators import inspector, measure_execution_time
|
|
|
93
93
|
from aiohomematic.exceptions import BaseHomematicException, ClientException, NoConnectionException
|
|
94
94
|
from aiohomematic.model.device import Device
|
|
95
95
|
from aiohomematic.model.support import convert_value
|
|
96
|
+
from aiohomematic.property_decorators import info_property
|
|
96
97
|
from aiohomematic.support import (
|
|
98
|
+
LogContextMixin,
|
|
97
99
|
build_xml_rpc_headers,
|
|
98
100
|
build_xml_rpc_uri,
|
|
99
101
|
extract_exc_args,
|
|
@@ -124,7 +126,7 @@ _CCU_JSON_VALUE_TYPE: Final = {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
class Client(ABC):
|
|
129
|
+
class Client(ABC, LogContextMixin):
|
|
128
130
|
"""Client object to access the backends via XML-RPC or JSON-RPC."""
|
|
129
131
|
|
|
130
132
|
def __init__(self, client_config: _ClientConfig) -> None:
|
|
@@ -144,6 +146,7 @@ class Client(ABC):
|
|
|
144
146
|
self._system_information: SystemInformation
|
|
145
147
|
self.modified_at: datetime = INIT_DATETIME
|
|
146
148
|
|
|
149
|
+
@inspector
|
|
147
150
|
async def init_client(self) -> None:
|
|
148
151
|
"""Init the client."""
|
|
149
152
|
self._system_information = await self._get_system_information()
|
|
@@ -168,7 +171,7 @@ class Client(ABC):
|
|
|
168
171
|
"""Return the interface of the client."""
|
|
169
172
|
return self._config.interface
|
|
170
173
|
|
|
171
|
-
@
|
|
174
|
+
@info_property(log_context=True)
|
|
172
175
|
def interface_id(self) -> str:
|
|
173
176
|
"""Return the interface id of the client."""
|
|
174
177
|
return self._config.interface_id
|
|
@@ -384,7 +387,9 @@ class Client(ABC):
|
|
|
384
387
|
"""Return if XmlRPC-Server is alive based on received events for this client."""
|
|
385
388
|
if not self.supports_ping_pong:
|
|
386
389
|
return True
|
|
387
|
-
if (
|
|
390
|
+
if (
|
|
391
|
+
last_events_dt := self.central.get_last_event_seen_for_interface(interface_id=self.interface_id)
|
|
392
|
+
) is not None:
|
|
388
393
|
if (seconds_since_last_event := (datetime.now() - last_events_dt).total_seconds()) > CALLBACK_WARN_INTERVAL:
|
|
389
394
|
if self._is_callback_alive:
|
|
390
395
|
self.central.fire_interface_event(
|
|
@@ -418,12 +423,12 @@ class Client(ABC):
|
|
|
418
423
|
"""Send ping to CCU to generate PONG event."""
|
|
419
424
|
|
|
420
425
|
@abstractmethod
|
|
421
|
-
@inspector
|
|
426
|
+
@inspector
|
|
422
427
|
async def execute_program(self, pid: str) -> bool:
|
|
423
428
|
"""Execute a program on CCU / Homegear."""
|
|
424
429
|
|
|
425
430
|
@abstractmethod
|
|
426
|
-
@inspector
|
|
431
|
+
@inspector
|
|
427
432
|
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
428
433
|
"""Set the program state on CCU / Homegear."""
|
|
429
434
|
|
|
@@ -433,12 +438,12 @@ class Client(ABC):
|
|
|
433
438
|
"""Set a system variable on CCU / Homegear."""
|
|
434
439
|
|
|
435
440
|
@abstractmethod
|
|
436
|
-
@inspector
|
|
441
|
+
@inspector
|
|
437
442
|
async def delete_system_variable(self, name: str) -> bool:
|
|
438
443
|
"""Delete a system variable from CCU / Homegear."""
|
|
439
444
|
|
|
440
445
|
@abstractmethod
|
|
441
|
-
@inspector
|
|
446
|
+
@inspector
|
|
442
447
|
async def get_system_variable(self, name: str) -> Any:
|
|
443
448
|
"""Get single system variable from CCU / Homegear."""
|
|
444
449
|
|
|
@@ -489,7 +494,7 @@ class Client(ABC):
|
|
|
489
494
|
_LOGGER.warning("GET_DEVICE_DESCRIPTIONS failed: %s [%s]", bhexc.name, extract_exc_args(exc=bhexc))
|
|
490
495
|
return None
|
|
491
496
|
|
|
492
|
-
@inspector
|
|
497
|
+
@inspector
|
|
493
498
|
async def add_link(self, sender_address: str, receiver_address: str, name: str, description: str) -> None:
|
|
494
499
|
"""Return a list of links."""
|
|
495
500
|
try:
|
|
@@ -499,7 +504,7 @@ class Client(ABC):
|
|
|
499
504
|
f"ADD_LINK failed with for: {sender_address}/{receiver_address}/{name}/{description}: {extract_exc_args(exc=bhexc)}"
|
|
500
505
|
) from bhexc
|
|
501
506
|
|
|
502
|
-
@inspector
|
|
507
|
+
@inspector
|
|
503
508
|
async def remove_link(self, sender_address: str, receiver_address: str) -> None:
|
|
504
509
|
"""Return a list of links."""
|
|
505
510
|
try:
|
|
@@ -509,7 +514,7 @@ class Client(ABC):
|
|
|
509
514
|
f"REMOVE_LINK failed with for: {sender_address}/{receiver_address}: {extract_exc_args(exc=bhexc)}"
|
|
510
515
|
) from bhexc
|
|
511
516
|
|
|
512
|
-
@inspector
|
|
517
|
+
@inspector
|
|
513
518
|
async def get_link_peers(self, address: str) -> tuple[str, ...] | None:
|
|
514
519
|
"""Return a list of link pers."""
|
|
515
520
|
try:
|
|
@@ -519,7 +524,7 @@ class Client(ABC):
|
|
|
519
524
|
f"GET_LINK_PEERS failed with for: {address}: {extract_exc_args(exc=bhexc)}"
|
|
520
525
|
) from bhexc
|
|
521
526
|
|
|
522
|
-
@inspector
|
|
527
|
+
@inspector
|
|
523
528
|
async def get_links(self, address: str, flags: int) -> dict[str, Any]:
|
|
524
529
|
"""Return a list of links."""
|
|
525
530
|
try:
|
|
@@ -527,7 +532,7 @@ class Client(ABC):
|
|
|
527
532
|
except BaseHomematicException as bhexc:
|
|
528
533
|
raise ClientException(f"GET_LINKS failed with for: {address}: {extract_exc_args(exc=bhexc)}") from bhexc
|
|
529
534
|
|
|
530
|
-
@inspector
|
|
535
|
+
@inspector
|
|
531
536
|
async def get_metadata(self, address: str, data_id: str) -> dict[str, Any]:
|
|
532
537
|
"""Return the metadata for an object."""
|
|
533
538
|
try:
|
|
@@ -537,7 +542,7 @@ class Client(ABC):
|
|
|
537
542
|
f"GET_METADATA failed with for: {address}/{data_id}: {extract_exc_args(exc=bhexc)}"
|
|
538
543
|
) from bhexc
|
|
539
544
|
|
|
540
|
-
@inspector
|
|
545
|
+
@inspector
|
|
541
546
|
async def set_metadata(self, address: str, data_id: str, value: dict[str, Any]) -> dict[str, Any]:
|
|
542
547
|
"""Write the metadata for an object."""
|
|
543
548
|
try:
|
|
@@ -694,7 +699,7 @@ class Client(ABC):
|
|
|
694
699
|
check_against_pd=check_against_pd,
|
|
695
700
|
)
|
|
696
701
|
|
|
697
|
-
@inspector
|
|
702
|
+
@inspector
|
|
698
703
|
async def get_paramset(
|
|
699
704
|
self,
|
|
700
705
|
address: str,
|
|
@@ -957,7 +962,7 @@ class Client(ABC):
|
|
|
957
962
|
)
|
|
958
963
|
return None
|
|
959
964
|
|
|
960
|
-
@inspector
|
|
965
|
+
@inspector
|
|
961
966
|
async def get_all_paramset_descriptions(
|
|
962
967
|
self, device_descriptions: tuple[DeviceDescription, ...]
|
|
963
968
|
) -> dict[str, dict[ParamsetKey, dict[str, ParameterData]]]:
|
|
@@ -967,7 +972,7 @@ class Client(ABC):
|
|
|
967
972
|
all_paramsets.update(await self.get_paramset_descriptions(device_description=device_description))
|
|
968
973
|
return all_paramsets
|
|
969
974
|
|
|
970
|
-
@inspector
|
|
975
|
+
@inspector
|
|
971
976
|
async def has_program_ids(self, channel_hmid: str) -> bool:
|
|
972
977
|
"""Return if a channel has program ids."""
|
|
973
978
|
return False
|
|
@@ -985,12 +990,12 @@ class Client(ABC):
|
|
|
985
990
|
)
|
|
986
991
|
return None
|
|
987
992
|
|
|
988
|
-
@inspector
|
|
993
|
+
@inspector
|
|
989
994
|
async def report_value_usage(self, address: str, value_id: str, ref_counter: int) -> bool:
|
|
990
995
|
"""Report value usage."""
|
|
991
996
|
return False
|
|
992
997
|
|
|
993
|
-
@inspector
|
|
998
|
+
@inspector
|
|
994
999
|
async def update_device_firmware(self, device_address: str) -> bool:
|
|
995
1000
|
"""Update the firmware of a homematic device."""
|
|
996
1001
|
if device := self.central.get_device(address=device_address):
|
|
@@ -1133,22 +1138,22 @@ class ClientCCU(Client):
|
|
|
1133
1138
|
self.modified_at = INIT_DATETIME
|
|
1134
1139
|
return False
|
|
1135
1140
|
|
|
1136
|
-
@inspector
|
|
1141
|
+
@inspector
|
|
1137
1142
|
async def execute_program(self, pid: str) -> bool:
|
|
1138
1143
|
"""Execute a program on CCU."""
|
|
1139
1144
|
return await self._json_rpc_client.execute_program(pid=pid)
|
|
1140
1145
|
|
|
1141
|
-
@inspector
|
|
1146
|
+
@inspector
|
|
1142
1147
|
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
1143
1148
|
"""Set the program state on CCU."""
|
|
1144
1149
|
return await self._json_rpc_client.set_program_state(pid=pid, state=state)
|
|
1145
1150
|
|
|
1146
|
-
@inspector
|
|
1151
|
+
@inspector
|
|
1147
1152
|
async def has_program_ids(self, channel_hmid: str) -> bool:
|
|
1148
1153
|
"""Return if a channel has program ids."""
|
|
1149
1154
|
return await self._json_rpc_client.has_program_ids(channel_hmid=channel_hmid)
|
|
1150
1155
|
|
|
1151
|
-
@inspector
|
|
1156
|
+
@inspector
|
|
1152
1157
|
async def report_value_usage(self, address: str, value_id: str, ref_counter: int) -> bool:
|
|
1153
1158
|
"""Report value usage."""
|
|
1154
1159
|
try:
|
|
@@ -1163,12 +1168,12 @@ class ClientCCU(Client):
|
|
|
1163
1168
|
"""Set a system variable on CCU / Homegear."""
|
|
1164
1169
|
return await self._json_rpc_client.set_system_variable(legacy_name=legacy_name, value=value)
|
|
1165
1170
|
|
|
1166
|
-
@inspector
|
|
1171
|
+
@inspector
|
|
1167
1172
|
async def delete_system_variable(self, name: str) -> bool:
|
|
1168
1173
|
"""Delete a system variable from CCU / Homegear."""
|
|
1169
1174
|
return await self._json_rpc_client.delete_system_variable(name=name)
|
|
1170
1175
|
|
|
1171
|
-
@inspector
|
|
1176
|
+
@inspector
|
|
1172
1177
|
async def get_system_variable(self, name: str) -> Any:
|
|
1173
1178
|
"""Get single system variable from CCU / Homegear."""
|
|
1174
1179
|
return await self._json_rpc_client.get_system_variable(name=name)
|
|
@@ -1217,6 +1222,7 @@ class ClientCCU(Client):
|
|
|
1217
1222
|
class ClientJsonCCU(ClientCCU):
|
|
1218
1223
|
"""Client implementation for CCU backend."""
|
|
1219
1224
|
|
|
1225
|
+
@inspector
|
|
1220
1226
|
async def init_client(self) -> None:
|
|
1221
1227
|
"""Init the client."""
|
|
1222
1228
|
self._system_information = await self._get_system_information()
|
|
@@ -1243,7 +1249,7 @@ class ClientJsonCCU(ClientCCU):
|
|
|
1243
1249
|
_LOGGER.warning("GET_DEVICE_DESCRIPTIONS failed: %s [%s]", bhexc.name, extract_exc_args(exc=bhexc))
|
|
1244
1250
|
return None
|
|
1245
1251
|
|
|
1246
|
-
@inspector
|
|
1252
|
+
@inspector
|
|
1247
1253
|
async def get_paramset(
|
|
1248
1254
|
self,
|
|
1249
1255
|
address: str,
|
|
@@ -1473,12 +1479,12 @@ class ClientHomegear(Client):
|
|
|
1473
1479
|
self.modified_at = INIT_DATETIME
|
|
1474
1480
|
return False
|
|
1475
1481
|
|
|
1476
|
-
@inspector
|
|
1482
|
+
@inspector
|
|
1477
1483
|
async def execute_program(self, pid: str) -> bool:
|
|
1478
1484
|
"""Execute a program on Homegear."""
|
|
1479
1485
|
return True
|
|
1480
1486
|
|
|
1481
|
-
@inspector
|
|
1487
|
+
@inspector
|
|
1482
1488
|
async def set_program_state(self, pid: str, state: bool) -> bool:
|
|
1483
1489
|
"""Set the program state on Homegear."""
|
|
1484
1490
|
return True
|
|
@@ -1489,13 +1495,13 @@ class ClientHomegear(Client):
|
|
|
1489
1495
|
await self._proxy.setSystemVariable(legacy_name, value)
|
|
1490
1496
|
return True
|
|
1491
1497
|
|
|
1492
|
-
@inspector
|
|
1498
|
+
@inspector
|
|
1493
1499
|
async def delete_system_variable(self, name: str) -> bool:
|
|
1494
1500
|
"""Delete a system variable from CCU / Homegear."""
|
|
1495
1501
|
await self._proxy.deleteSystemVariable(name)
|
|
1496
1502
|
return True
|
|
1497
1503
|
|
|
1498
|
-
@inspector
|
|
1504
|
+
@inspector
|
|
1499
1505
|
async def get_system_variable(self, name: str) -> Any:
|
|
1500
1506
|
"""Get single system variable from CCU / Homegear."""
|
|
1501
1507
|
return await self._proxy.getSystemVariable(name)
|
aiohomematic/client/json_rpc.py
CHANGED
|
@@ -47,10 +47,12 @@ from urllib.parse import unquote
|
|
|
47
47
|
|
|
48
48
|
from aiohttp import (
|
|
49
49
|
ClientConnectorCertificateError,
|
|
50
|
+
ClientConnectorError,
|
|
50
51
|
ClientError,
|
|
51
52
|
ClientResponse,
|
|
52
53
|
ClientSession,
|
|
53
54
|
ClientTimeout,
|
|
55
|
+
ContentTypeError,
|
|
54
56
|
TCPConnector,
|
|
55
57
|
)
|
|
56
58
|
import orjson
|
|
@@ -89,7 +91,9 @@ from aiohomematic.exceptions import (
|
|
|
89
91
|
UnsupportedException,
|
|
90
92
|
)
|
|
91
93
|
from aiohomematic.model.support import convert_value
|
|
94
|
+
from aiohomematic.property_decorators import info_property
|
|
92
95
|
from aiohomematic.support import (
|
|
96
|
+
LogContextMixin,
|
|
93
97
|
cleanup_text_from_html_tags,
|
|
94
98
|
element_matches_key,
|
|
95
99
|
extract_exc_args,
|
|
@@ -175,7 +179,7 @@ _PARALLEL_EXECUTION_LIMITED_JSONRPC_METHODS: Final = (
|
|
|
175
179
|
)
|
|
176
180
|
|
|
177
181
|
|
|
178
|
-
class JsonRpcAioHttpClient:
|
|
182
|
+
class JsonRpcAioHttpClient(LogContextMixin):
|
|
179
183
|
"""Connection to CCU JSON-RPC Server."""
|
|
180
184
|
|
|
181
185
|
def __init__(
|
|
@@ -213,6 +217,16 @@ class JsonRpcAioHttpClient:
|
|
|
213
217
|
"""If session exists, then it is activated."""
|
|
214
218
|
return self._session_id is not None
|
|
215
219
|
|
|
220
|
+
@info_property(log_context=True)
|
|
221
|
+
def url(self) -> str | None:
|
|
222
|
+
"""Return url."""
|
|
223
|
+
return self._url
|
|
224
|
+
|
|
225
|
+
@info_property(log_context=True)
|
|
226
|
+
def tls(self) -> bool:
|
|
227
|
+
"""Return tls."""
|
|
228
|
+
return self._tls
|
|
229
|
+
|
|
216
230
|
async def _login_or_renew(self) -> bool:
|
|
217
231
|
"""Renew JSON-RPC session or perform login."""
|
|
218
232
|
if not self.is_activated:
|
|
@@ -425,7 +439,7 @@ class JsonRpcAioHttpClient:
|
|
|
425
439
|
action=str(method),
|
|
426
440
|
err=exc,
|
|
427
441
|
level=logging.WARNING,
|
|
428
|
-
|
|
442
|
+
log_context=self.log_context,
|
|
429
443
|
)
|
|
430
444
|
_LOGGER.debug("POST: %s", exc)
|
|
431
445
|
raise exc
|
|
@@ -443,7 +457,7 @@ class JsonRpcAioHttpClient:
|
|
|
443
457
|
action=str(method),
|
|
444
458
|
err=exc,
|
|
445
459
|
level=logging.WARNING,
|
|
446
|
-
|
|
460
|
+
log_context=dict(self.log_context) | {"status": response.status},
|
|
447
461
|
)
|
|
448
462
|
raise exc
|
|
449
463
|
raise ClientException(message)
|
|
@@ -457,9 +471,22 @@ class JsonRpcAioHttpClient:
|
|
|
457
471
|
action=str(method),
|
|
458
472
|
err=bhe,
|
|
459
473
|
level=logging.WARNING,
|
|
460
|
-
|
|
474
|
+
log_context=self.log_context,
|
|
461
475
|
)
|
|
462
476
|
raise
|
|
477
|
+
|
|
478
|
+
except ClientConnectorError as cceerr:
|
|
479
|
+
self.clear_session()
|
|
480
|
+
message = f"ClientConnectorError[{cceerr}]"
|
|
481
|
+
log_boundary_error(
|
|
482
|
+
logger=_LOGGER,
|
|
483
|
+
boundary="json-rpc",
|
|
484
|
+
action=str(method),
|
|
485
|
+
err=cceerr,
|
|
486
|
+
level=logging.ERROR,
|
|
487
|
+
log_context=self.log_context,
|
|
488
|
+
)
|
|
489
|
+
raise ClientException(message) from cceerr
|
|
463
490
|
except ClientConnectorCertificateError as cccerr:
|
|
464
491
|
self.clear_session()
|
|
465
492
|
message = f"ClientConnectorCertificateError[{cccerr}]"
|
|
@@ -474,7 +501,7 @@ class JsonRpcAioHttpClient:
|
|
|
474
501
|
action=str(method),
|
|
475
502
|
err=cccerr,
|
|
476
503
|
level=logging.ERROR,
|
|
477
|
-
|
|
504
|
+
log_context=self.log_context,
|
|
478
505
|
)
|
|
479
506
|
raise ClientException(message) from cccerr
|
|
480
507
|
except (ClientError, OSError) as err:
|
|
@@ -485,7 +512,7 @@ class JsonRpcAioHttpClient:
|
|
|
485
512
|
action=str(method),
|
|
486
513
|
err=err,
|
|
487
514
|
level=logging.ERROR,
|
|
488
|
-
|
|
515
|
+
log_context=self.log_context,
|
|
489
516
|
)
|
|
490
517
|
raise NoConnectionException(err) from err
|
|
491
518
|
except (TypeError, Exception) as exc:
|
|
@@ -496,7 +523,7 @@ class JsonRpcAioHttpClient:
|
|
|
496
523
|
action=str(method),
|
|
497
524
|
err=exc,
|
|
498
525
|
level=logging.ERROR,
|
|
499
|
-
|
|
526
|
+
log_context=self.log_context,
|
|
500
527
|
)
|
|
501
528
|
raise ClientException(exc) from exc
|
|
502
529
|
|
|
@@ -1014,7 +1041,7 @@ class JsonRpcAioHttpClient:
|
|
|
1014
1041
|
|
|
1015
1042
|
async def get_all_device_data(self, interface: Interface) -> Mapping[str, Any]:
|
|
1016
1043
|
"""Get the all device data of the backend."""
|
|
1017
|
-
all_device_data: dict[str,
|
|
1044
|
+
all_device_data: dict[str, Any] = {}
|
|
1018
1045
|
params = {
|
|
1019
1046
|
_JsonKey.INTERFACE: interface,
|
|
1020
1047
|
}
|
|
@@ -1023,12 +1050,17 @@ class JsonRpcAioHttpClient:
|
|
|
1023
1050
|
|
|
1024
1051
|
_LOGGER.debug("GET_ALL_DEVICE_DATA: Getting all device data for interface %s", interface)
|
|
1025
1052
|
if json_result := response[_JsonKey.RESULT]:
|
|
1026
|
-
all_device_data =
|
|
1027
|
-
|
|
1028
|
-
|
|
1053
|
+
all_device_data = {
|
|
1054
|
+
unquote(string=k, encoding=ISO_8859_1): unquote(string=v, encoding=ISO_8859_1)
|
|
1055
|
+
if isinstance(v, str)
|
|
1056
|
+
else v
|
|
1057
|
+
for k, v in json_result.items()
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
except (ContentTypeError, JSONDecodeError) as cerr:
|
|
1029
1061
|
raise ClientException(
|
|
1030
1062
|
f"GET_ALL_DEVICE_DATA failed: Unable to fetch device data for interface {interface}"
|
|
1031
|
-
) from
|
|
1063
|
+
) from cerr
|
|
1032
1064
|
|
|
1033
1065
|
return all_device_data
|
|
1034
1066
|
|