Skip to content

Commit

Permalink
Fix crash on serialization error (#328)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt committed Jun 22, 2023
1 parent 0e61262 commit 381fd1a
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 41 deletions.
8 changes: 7 additions & 1 deletion matter_server/common/helpers/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
51 changes: 12 additions & 39 deletions matter_server/common/helpers/util.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
26 changes: 25 additions & 1 deletion matter_server/server/device_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 381fd1a

Please sign in to comment.