|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import json |
| 6 | +import logging |
5 | 7 | import re |
6 | 8 | from collections.abc import Iterator |
7 | 9 | from typing import Any, cast |
|
31 | 33 | # pylint: disable=unused-argument, too-many-instance-attributes, too-many-locals |
32 | 34 |
|
33 | 35 | # <protocol>://<gatewayId>/<deviceAddress>[#<subsystemId>] |
34 | | -DEVICE_URL_RE = r"(?P<protocol>.+)://(?P<gatewayId>[^/]+)/(?P<deviceAddress>[^#]+)(#(?P<subsystemId>\d+))?" |
| 36 | +DEVICE_URL_RE = re.compile( |
| 37 | + r"(?P<protocol>[^:]+)://(?P<gatewayId>[^/]+)/(?P<deviceAddress>[^#]+)(#(?P<subsystemId>\d+))?" |
| 38 | +) |
| 39 | + |
| 40 | +_LOGGER = logging.getLogger(__name__) |
35 | 41 |
|
36 | 42 |
|
37 | 43 | @define(init=False, kw_only=True) |
@@ -180,7 +186,7 @@ def is_sub_device(self) -> bool: |
180 | 186 | @classmethod |
181 | 187 | def from_device_url(cls, device_url: str) -> DeviceIdentifier: |
182 | 188 | """Parse a device URL into its structured identifier components.""" |
183 | | - match = re.search(DEVICE_URL_RE, device_url) |
| 189 | + match = DEVICE_URL_RE.fullmatch(device_url) |
184 | 190 | if not match: |
185 | 191 | raise ValueError(f"Invalid device URL: {device_url}") |
186 | 192 |
|
@@ -589,8 +595,24 @@ def __init__( |
589 | 595 | # Overkiz (cloud) returns all state values as a string |
590 | 596 | # We cast them here based on the data type provided by Overkiz |
591 | 597 | # Overkiz (local) returns all state values in the right format |
592 | | - if isinstance(self.value, str) and self.type in DATA_TYPE_TO_PYTHON: |
593 | | - self.value = DATA_TYPE_TO_PYTHON[self.type](self.value) |
| 598 | + if not isinstance(self.value, str) or self.type not in DATA_TYPE_TO_PYTHON: |
| 599 | + return |
| 600 | + |
| 601 | + caster = DATA_TYPE_TO_PYTHON[self.type] |
| 602 | + if self.type in (DataType.JSON_ARRAY, DataType.JSON_OBJECT): |
| 603 | + self.value = self._cast_json_value(self.value) |
| 604 | + return |
| 605 | + |
| 606 | + self.value = caster(self.value) |
| 607 | + |
| 608 | + def _cast_json_value(self, raw_value: str) -> StateType: |
| 609 | + """Cast JSON event state values; raise on decode errors.""" |
| 610 | + try: |
| 611 | + return json.loads(raw_value) |
| 612 | + except json.JSONDecodeError as err: |
| 613 | + raise ValueError( |
| 614 | + f"Invalid JSON for event state `{self.name}` ({self.type.name}): {err}" |
| 615 | + ) from err |
594 | 616 |
|
595 | 617 |
|
596 | 618 | @define(init=False) |
@@ -1059,7 +1081,13 @@ def __init__( |
1059 | 1081 |
|
1060 | 1082 | @define(init=False, kw_only=True) |
1061 | 1083 | class Place: |
1062 | | - """Representation of a place (house/room) in a setup.""" |
| 1084 | + """Hierarchical representation of a location (house, room, area) in a setup. |
| 1085 | +
|
| 1086 | + Places form a tree structure where the root place is typically the entire house |
| 1087 | + or property, and `sub_places` contains nested child locations. This recursive |
| 1088 | + structure allows navigation from house -> floors/rooms -> individual areas. |
| 1089 | + Each place has associated metadata like creation time, label, and type identifier. |
| 1090 | + """ |
1063 | 1091 |
|
1064 | 1092 | creation_time: int |
1065 | 1093 | last_update_time: int | None = None |
|
0 commit comments