Skip to content

Commit 381fd1a

Browse files
authored
Fix crash on serialization error (#328)
1 parent 0e61262 commit 381fd1a

File tree

3 files changed

+44
-41
lines changed

3 files changed

+44
-41
lines changed

matter_server/common/helpers/json.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from dataclasses import is_dataclass
55
from typing import Any
66

7+
from chip.clusters.Attribute import ValueDecodeFailure
78
from chip.clusters.Types import Nullable
9+
from chip.tlv import float32, uint
810
import orjson
911

1012
from .util import dataclass_to_dict
@@ -20,10 +22,14 @@ def json_encoder_default(obj: Any) -> Any:
2022
"""
2123
if getattr(obj, "do_not_serialize", None):
2224
return None
25+
if isinstance(obj, ValueDecodeFailure):
26+
return None
2327
if isinstance(obj, (set, tuple)):
2428
return list(obj)
25-
if isinstance(obj, float):
29+
if isinstance(obj, float32):
2630
return float(obj)
31+
if isinstance(obj, uint):
32+
return int(obj)
2733
if hasattr(obj, "as_dict"):
2834
return obj.as_dict()
2935
if is_dataclass(obj):

matter_server/common/helpers/util.py

+12-39
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Utils for Matter server (and client)."""
22
from __future__ import annotations
33

4-
from base64 import b64decode, b64encode
4+
from base64 import b64decode
55
import binascii
66
from dataclasses import MISSING, asdict, fields, is_dataclass
77
from datetime import datetime
@@ -22,7 +22,7 @@
2222
)
2323

2424
from chip.clusters.ClusterObjects import ClusterAttributeDescriptor
25-
from chip.clusters.Types import Nullable, NullValue
25+
from chip.clusters.Types import Nullable
2626
from chip.tlv import float32, uint
2727

2828
if TYPE_CHECKING:
@@ -59,44 +59,17 @@ def parse_attribute_path(attribute_path: str) -> tuple[int, int, int]:
5959
return (int(endpoint_id_str), int(cluster_id_str), int(attribute_id_str))
6060

6161

62-
def dataclass_to_dict(obj_in: DataclassInstance, skip_none: bool = False) -> dict:
63-
"""Convert dataclass instance to dict, optionally skip None values."""
64-
if skip_none:
65-
dict_obj = asdict(
66-
obj_in, dict_factory=lambda x: {k: v for (k, v) in x if v is not None}
67-
)
68-
else:
69-
dict_obj = asdict(obj_in)
70-
71-
def _convert_value(value: Any) -> Any:
72-
"""Do some common conversions."""
73-
if isinstance(value, list):
74-
return [_convert_value(x) for x in value]
75-
if isinstance(value, Nullable) or value == NullValue:
76-
return None
77-
if isinstance(value, dict):
78-
return _clean_dict(value)
79-
if isinstance(value, Enum):
80-
return value.value
81-
if isinstance(value, bytes):
82-
return b64encode(value).decode("utf-8")
83-
if isinstance(value, float32):
84-
return float(value)
85-
if type(value) == type:
86-
return f"{value.__module__}.{value.__qualname__}"
87-
if isinstance(value, Exception):
88-
return None
89-
return value
62+
def dataclass_to_dict(obj_in: DataclassInstance) -> dict:
63+
"""Convert dataclass instance to dict."""
9064

91-
def _clean_dict(_dict_obj: dict) -> dict:
92-
_final = {}
93-
for key, value in _dict_obj.items():
94-
if isinstance(key, int):
95-
key = str(key)
96-
_final[key] = _convert_value(value)
97-
return _final
98-
99-
return _clean_dict(dict_obj)
65+
return asdict(
66+
obj_in,
67+
dict_factory=lambda x: {
68+
# ensure the dict key is a string
69+
str(k): v
70+
for (k, v) in x
71+
},
72+
)
10073

10174

10275
def parse_utc_timestamp(datetime_string: str) -> datetime:

matter_server/server/device_controller.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from chip.ChipDeviceCtrl import CommissionableNode
1414
from chip.clusters import Attribute, Objects as Clusters
15+
from chip.clusters.Attribute import ValueDecodeFailure
1516
from chip.clusters.ClusterObjects import ALL_CLUSTERS, Cluster
1617
from chip.exceptions import ChipStackError
1718

@@ -23,6 +24,7 @@
2324
NodeNotResolving,
2425
)
2526
from ..common.helpers.api import api_command
27+
from ..common.helpers.json import json_dumps
2628
from ..common.helpers.util import (
2729
create_attribute_path,
2830
create_attribute_path_from_attribute,
@@ -429,6 +431,10 @@ def attribute_updated_callback(
429431
) -> None:
430432
assert self.server.loop is not None
431433
new_value = transaction.GetAttribute(path)
434+
# failsafe: ignore ValueDecodeErrors
435+
# these are set by the SDK if parsing the value failed miserably
436+
if isinstance(new_value, ValueDecodeFailure):
437+
return
432438
node_logger.debug("Attribute updated: %s - new value: %s", path, new_value)
433439
attr_path = str(path.Path)
434440
node.attributes[attr_path] = new_value
@@ -456,7 +462,6 @@ def event_callback(
456462
assert self.server.loop is not None
457463
node_logger.debug("Received node event: %s", data)
458464
self.event_history.append(data)
459-
# TODO: This callback does not seem to fire ever or my test devices do not have events
460465
self.server.loop.call_soon_threadsafe(
461466
self.server.signal_event, EventType.NODE_EVENT, data
462467
)
@@ -627,6 +632,25 @@ def _parse_attributes_from_read_result(
627632
attribute_path = create_attribute_path(
628633
endpoint, cluster_cls.id, attr_cls.attribute_id
629634
)
635+
# failsafe: ignore ValueDecodeErrors
636+
# these are set by the SDK if parsing the value failed miserably
637+
if isinstance(attr_value, ValueDecodeFailure):
638+
continue
639+
# failsafe: make sure the attribute is serializable
640+
# there is a chance we receive malformed data from the sdk
641+
# due to all magic parsing to/from TLV.
642+
# skip an attribute in that case to prevent serialization issues
643+
# of the whole node.
644+
try:
645+
json_dumps(attr_value)
646+
except TypeError as err:
647+
LOGGER.warning(
648+
"Unserializable data found - "
649+
"skip attribute %s - error details: %s",
650+
attribute_path,
651+
err,
652+
)
653+
continue
630654
result[attribute_path] = attr_value
631655
return result
632656

0 commit comments

Comments
 (0)