diff --git a/matter_server/common/helpers/json.py b/matter_server/common/helpers/json.py index 856c24a2..9ce22f06 100644 --- a/matter_server/common/helpers/json.py +++ b/matter_server/common/helpers/json.py @@ -4,7 +4,9 @@ from dataclasses import is_dataclass from typing import Any +from chip.clusters.Attribute import ValueDecodeFailure from chip.clusters.Types import Nullable +from chip.tlv import float32, uint import orjson from .util import dataclass_to_dict @@ -20,10 +22,14 @@ def json_encoder_default(obj: Any) -> Any: """ if getattr(obj, "do_not_serialize", None): return None + if isinstance(obj, ValueDecodeFailure): + return None if isinstance(obj, (set, tuple)): return list(obj) - if isinstance(obj, float): + if isinstance(obj, float32): return float(obj) + if isinstance(obj, uint): + return int(obj) if hasattr(obj, "as_dict"): return obj.as_dict() if is_dataclass(obj): diff --git a/matter_server/common/helpers/util.py b/matter_server/common/helpers/util.py index d91d7bfa..b62da6a3 100644 --- a/matter_server/common/helpers/util.py +++ b/matter_server/common/helpers/util.py @@ -1,7 +1,7 @@ """Utils for Matter server (and client).""" from __future__ import annotations -from base64 import b64decode, b64encode +from base64 import b64decode import binascii from dataclasses import MISSING, asdict, fields, is_dataclass from datetime import datetime @@ -22,7 +22,7 @@ ) from chip.clusters.ClusterObjects import ClusterAttributeDescriptor -from chip.clusters.Types import Nullable, NullValue +from chip.clusters.Types import Nullable from chip.tlv import float32, uint if TYPE_CHECKING: @@ -59,44 +59,17 @@ def parse_attribute_path(attribute_path: str) -> tuple[int, int, int]: return (int(endpoint_id_str), int(cluster_id_str), int(attribute_id_str)) -def dataclass_to_dict(obj_in: DataclassInstance, skip_none: bool = False) -> dict: - """Convert dataclass instance to dict, optionally skip None values.""" - if skip_none: - dict_obj = asdict( - obj_in, dict_factory=lambda x: {k: v for (k, v) in x if v is not None} - ) - else: - dict_obj = asdict(obj_in) - - def _convert_value(value: Any) -> Any: - """Do some common conversions.""" - if isinstance(value, list): - return [_convert_value(x) for x in value] - if isinstance(value, Nullable) or value == NullValue: - return None - if isinstance(value, dict): - return _clean_dict(value) - if isinstance(value, Enum): - return value.value - if isinstance(value, bytes): - return b64encode(value).decode("utf-8") - if isinstance(value, float32): - return float(value) - if type(value) == type: - return f"{value.__module__}.{value.__qualname__}" - if isinstance(value, Exception): - return None - return value +def dataclass_to_dict(obj_in: DataclassInstance) -> dict: + """Convert dataclass instance to dict.""" - def _clean_dict(_dict_obj: dict) -> dict: - _final = {} - for key, value in _dict_obj.items(): - if isinstance(key, int): - key = str(key) - _final[key] = _convert_value(value) - return _final - - return _clean_dict(dict_obj) + return asdict( + obj_in, + dict_factory=lambda x: { + # ensure the dict key is a string + str(k): v + for (k, v) in x + }, + ) def parse_utc_timestamp(datetime_string: str) -> datetime: diff --git a/matter_server/server/device_controller.py b/matter_server/server/device_controller.py index 7c877fa4..a0cc2509 100644 --- a/matter_server/server/device_controller.py +++ b/matter_server/server/device_controller.py @@ -12,6 +12,7 @@ from chip.ChipDeviceCtrl import CommissionableNode from chip.clusters import Attribute, Objects as Clusters +from chip.clusters.Attribute import ValueDecodeFailure from chip.clusters.ClusterObjects import ALL_CLUSTERS, Cluster from chip.exceptions import ChipStackError @@ -23,6 +24,7 @@ NodeNotResolving, ) from ..common.helpers.api import api_command +from ..common.helpers.json import json_dumps from ..common.helpers.util import ( create_attribute_path, create_attribute_path_from_attribute, @@ -429,6 +431,10 @@ def attribute_updated_callback( ) -> None: assert self.server.loop is not None new_value = transaction.GetAttribute(path) + # failsafe: ignore ValueDecodeErrors + # these are set by the SDK if parsing the value failed miserably + if isinstance(new_value, ValueDecodeFailure): + return node_logger.debug("Attribute updated: %s - new value: %s", path, new_value) attr_path = str(path.Path) node.attributes[attr_path] = new_value @@ -456,7 +462,6 @@ def event_callback( assert self.server.loop is not None node_logger.debug("Received node event: %s", data) self.event_history.append(data) - # TODO: This callback does not seem to fire ever or my test devices do not have events self.server.loop.call_soon_threadsafe( self.server.signal_event, EventType.NODE_EVENT, data ) @@ -627,6 +632,25 @@ def _parse_attributes_from_read_result( attribute_path = create_attribute_path( endpoint, cluster_cls.id, attr_cls.attribute_id ) + # failsafe: ignore ValueDecodeErrors + # these are set by the SDK if parsing the value failed miserably + if isinstance(attr_value, ValueDecodeFailure): + continue + # failsafe: make sure the attribute is serializable + # there is a chance we receive malformed data from the sdk + # due to all magic parsing to/from TLV. + # skip an attribute in that case to prevent serialization issues + # of the whole node. + try: + json_dumps(attr_value) + except TypeError as err: + LOGGER.warning( + "Unserializable data found - " + "skip attribute %s - error details: %s", + attribute_path, + err, + ) + continue result[attribute_path] = attr_value return result