Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash on serialization error #328

Merged
merged 2 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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