read_resource: a Collection's list body is not an empty resource (#335)

`_raw_read_blocking` decoded the CBOR body and kept it only when it was a
Property map, so a Collection -- which answers the `[devcol rep, {href,
rep}, ...]` batch `parse_device0_batch` reads -- came back as `2.05` with
`rep: {}`. That renders as "the resource exists and has nothing in it",
which is the opposite of what a populated batch means, and `/device/0`
itself would have read the same way.

It cost a real result: issue #335's board answers `/sec/devices` (the
`x.com.samsung.devcol` sibling of `/device/0`, and the one remaining place
a composite appliance could be enumerating its indoor units) with exactly
that empty-looking 2.05, and it was nearly written off as a dead end.

The read path now returns the decoded body alongside `rep`, and the service
response carries it as `body` whenever it isn't the map `rep` already has --
omitted for the ordinary case rather than duplicating every rep in every
response. Records the probe round this came out of: indexed leaves 4.04 on
that board, and the UUID prefix confirmed routable by a positive control, so
Patterns A/B/C are ruled out there on evidence rather than on absence.
This commit is contained in:
Marc Billow committed 2026-08-13 02:43:12 +00:00
1 parent 11c71a62e8
commit e5cd212a34
5 files changed
+131 -45

No files matched your search

+29 -2
View File
@@ -56,11 +56,16 @@ class _FakeSession:
self._post_code = post_code
self._get_reps: dict[str, list[dict]] = {}
def queue_get(self, href: str, rep: dict) -> None:
def queue_get(self, href: str, rep: dict | list) -> None:
"""Queue one more canned rep for `href`'s next GET. Once an href's
queue is down to one entry, that entry keeps answering every
further GET -- a test only needs to queue the values that
actually change across calls."""
actually change across calls.
A list models a Collection's answer (the `[devcol rep, {href, rep},
...]` batch), which is not a Property map and so is a shape the
read path has to carry separately -- see the collection test below.
"""
self._get_reps.setdefault(href.strip("/"), []).append(rep)
def post(self, path_segs, payload, timeout=None):
@@ -576,6 +581,28 @@ async def test_read_resource_with_href_does_live_get(hass, coordinator, device_i
assert response["href"] == "/mode/vs/0"
assert response["actual_href"] == "/mode/vs/0"
assert response["rep"] == {"x.field": "live"}
# No duplicate copy of a Property map that `rep` already carries.
assert "body" not in response
async def test_read_resource_surfaces_a_collections_list_body(hass, coordinator, device_id):
"""A Collection answers a CBOR list, not a Property map, so `rep` can't
hold it (issue #335: `/sec/devices` came back as an accepted-but-empty
2.05, which reads as "exists, nothing in it" -- the opposite of what a
populated batch means)."""
batch = [
{"rt": ["x.com.samsung.devcol", "oic.wk.col"]},
{"href": "/mode/vs/0", "rep": {"x.field": "live"}},
]
fake = _FakeSession()
fake.queue_get("sec/devices", batch)
coordinator._session = fake
response = await _call_read(hass, device_id, href="/sec/devices")
assert response["code"] == "2.05"
assert response["rep"] == {}
assert response["body"] == batch
async def test_read_resource_without_href_returns_cached_snapshot_and_does_not_get(