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.

Files changed (55) hide show
  1. aiohomematic/caches/dynamic.py +1 -6
  2. aiohomematic/central/__init__.py +34 -23
  3. aiohomematic/central/xml_rpc_server.py +1 -1
  4. aiohomematic/client/__init__.py +35 -29
  5. aiohomematic/client/json_rpc.py +44 -12
  6. aiohomematic/client/xml_rpc.py +53 -20
  7. aiohomematic/const.py +2 -2
  8. aiohomematic/decorators.py +56 -21
  9. aiohomematic/model/__init__.py +1 -1
  10. aiohomematic/model/calculated/__init__.py +1 -1
  11. aiohomematic/model/calculated/climate.py +1 -1
  12. aiohomematic/model/calculated/data_point.py +2 -2
  13. aiohomematic/model/calculated/operating_voltage_level.py +7 -21
  14. aiohomematic/model/calculated/support.py +20 -0
  15. aiohomematic/model/custom/__init__.py +1 -1
  16. aiohomematic/model/custom/climate.py +18 -18
  17. aiohomematic/model/custom/cover.py +1 -1
  18. aiohomematic/model/custom/data_point.py +1 -1
  19. aiohomematic/model/custom/light.py +1 -1
  20. aiohomematic/model/custom/lock.py +1 -1
  21. aiohomematic/model/custom/siren.py +1 -1
  22. aiohomematic/model/custom/switch.py +1 -1
  23. aiohomematic/model/custom/valve.py +1 -1
  24. aiohomematic/model/data_point.py +18 -18
  25. aiohomematic/model/device.py +21 -20
  26. aiohomematic/model/event.py +3 -8
  27. aiohomematic/model/generic/__init__.py +1 -1
  28. aiohomematic/model/generic/binary_sensor.py +1 -1
  29. aiohomematic/model/generic/button.py +1 -1
  30. aiohomematic/model/generic/data_point.py +3 -5
  31. aiohomematic/model/generic/number.py +1 -1
  32. aiohomematic/model/generic/select.py +1 -1
  33. aiohomematic/model/generic/sensor.py +1 -1
  34. aiohomematic/model/generic/switch.py +4 -4
  35. aiohomematic/model/generic/text.py +1 -1
  36. aiohomematic/model/hub/binary_sensor.py +1 -1
  37. aiohomematic/model/hub/button.py +2 -2
  38. aiohomematic/model/hub/data_point.py +4 -7
  39. aiohomematic/model/hub/number.py +1 -1
  40. aiohomematic/model/hub/select.py +2 -2
  41. aiohomematic/model/hub/sensor.py +1 -1
  42. aiohomematic/model/hub/switch.py +3 -3
  43. aiohomematic/model/hub/text.py +1 -1
  44. aiohomematic/model/support.py +1 -40
  45. aiohomematic/model/update.py +5 -4
  46. aiohomematic/property_decorators.py +327 -0
  47. aiohomematic/support.py +70 -85
  48. {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/METADATA +7 -5
  49. aiohomematic-2025.9.2.dist-info/RECORD +78 -0
  50. aiohomematic_support/client_local.py +5 -5
  51. aiohomematic/model/decorators.py +0 -194
  52. aiohomematic-2025.9.1.dist-info/RECORD +0 -78
  53. {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/WHEEL +0 -0
  54. {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/licenses/LICENSE +0 -0
  55. {aiohomematic-2025.9.1.dist-info → aiohomematic-2025.9.2.dist-info}/top_level.txt +0 -0
@@ -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(
@@ -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.model.support import PayloadMixin
164
- from aiohomematic.support import check_config, extract_exc_args, get_channel_no, get_device_address, get_ip_addr
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._last_events: Final[dict[str, datetime]] = {}
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.set_last_event_dt(interface_id=interface_id)
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
- self._looper.create_task(callback_handler(value), name=f"sysvar-data-point-event-{state_path}")
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, (GenericDataPoint, GenericEvent)) and (
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, (GenericDataPoint, GenericEvent)) and data_point.supports_events:
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 get_last_event_dt(self, interface_id: str) -> datetime | None:
1258
- """Return the last event dt."""
1259
- return self._last_events.get(interface_id)
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 set_last_event_dt(self, interface_id: str) -> None:
1262
- """Set the last event dt."""
1263
- self._last_events[interface_id] = datetime.now()
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.set_last_event_dt(interface_id=client.interface_id)
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
- context={"interface_id": interface_id, "error_code": int(error_code)},
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",
@@ -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
- @property
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 (last_events_dt := self.central.get_last_event_dt(interface_id=self.interface_id)) is not None:
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)
@@ -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
- context={"url": self._url},
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
- context={"url": self._url, "status": response.status},
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
- context={"url": self._url},
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
- context={"url": self._url},
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
- context={"url": self._url},
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
- context={"url": self._url},
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, dict[str, dict[str, Any]]] = {}
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 = json_result
1027
-
1028
- except JSONDecodeError as jderr:
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 jderr
1063
+ ) from cerr
1032
1064
 
1033
1065
  return all_device_data
1034
1066