Files
localthings/tests/test_identity.py
T
Marc Billow daf7e3787f Add ruff (lint + format) and ty (type checking) to the project
Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.

Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).

Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
2026-08-02 23:56:38 +00:00

123 lines
4.4 KiB
Python

import cbor2
from custom_components.localthings.registry.identity import read_identity
class FakeSession:
def __init__(self, table):
self.table = table # tuple(path) -> rep dict
def get(self, path, timeout=10.0):
rep = self.table.get(tuple(path))
if rep is None:
return 0x84, b"" # 4.04 not found
return 0x45, cbor2.dumps(rep)
def test_read_identity_from_oic_p_and_d():
sess = FakeSession(
{
("oic", "p"): {"mnmn": "Samsung Electronics", "mnmo": "RF9000B"},
("oic", "d"): {"n": "Family Hub"},
}
)
ident = read_identity(sess, serial="ABC123")
assert ident.manufacturer == "Samsung Electronics"
assert ident.model == "RF9000B"
assert ident.name == "Family Hub"
assert ident.serial == "ABC123"
def test_read_identity_tolerates_missing_resources():
ident = read_identity(FakeSession({}), serial=None)
assert ident.manufacturer == "Samsung"
assert ident.model == ""
assert ident.serial is None
assert ident.device_types == ()
assert ident.raw == {"/oic/p": {}, "/oic/d": {}, "/oic/res": []}
def test_read_identity_captures_oic_d_device_types():
"""/oic/d's `rt` is OCF's own device-type declaration -- captured so
diagnostics can show whether real hardware populates it usefully."""
sess = FakeSession(
{
("oic", "d"): {
"n": "Living Room AC",
"rt": ["oic.wk.d", "oic.d.airconditioner"],
},
}
)
ident = read_identity(sess, serial=None)
assert ident.device_types == ("oic.wk.d", "oic.d.airconditioner")
def test_read_identity_normalizes_scalar_and_malformed_rt():
"""Firmware that reports a bare string, or a non-list, must not explode."""
assert read_identity(
FakeSession({("oic", "d"): {"rt": "oic.d.refrigerator"}}), None
).device_types == ("oic.d.refrigerator",)
assert read_identity(FakeSession({("oic", "d"): {"rt": 42}}), None).device_types == ()
assert read_identity(
FakeSession({("oic", "d"): {"rt": ["oic.wk.d", 7, None]}}), None
).device_types == ("oic.wk.d",)
def test_read_identity_keeps_raw_payloads_for_diagnostics():
sess = FakeSession(
{
("oic", "p"): {"mnmn": "Samsung Electronics", "mnmo": "RF9000B"},
("oic", "d"): {"n": "Family Hub", "di": "abc-123"},
}
)
ident = read_identity(sess, serial=None)
assert ident.raw["/oic/p"]["mnmo"] == "RF9000B"
assert ident.raw["/oic/d"]["di"] == "abc-123"
def test_read_identity_captures_oic_res_links():
"""/oic/res is OCF's discovery endpoint. Real hardware (issue #177
follow-up, a TP1X_REF_21K fridge dump) groups the response by `di`: one
entry per logical Device, each carrying its own `links` array -- not a
flat array of individually-`di`-tagged links. Every entry that dump
returned had its discoverable policy bit set (`bm`'s bit 0); the whole
x.com.samsung.da.* tree (including /device/0 itself) did not appear at
all, meaning it's registered non-discoverable and simply invisible to
this endpoint -- see _SPECULATIVE_DEVICE_INDICES' docstring for why that
motivated probing /device/1 and /device/2 directly instead."""
sess = FakeSession(
{
("oic", "res"): [
{
"di": "aaaa",
"links": [
{
"href": "/oic/d",
"rt": ["oic.wk.d", "oic.d.refrigerator"],
"p": {"bm": 1},
},
{"href": "/oic/sec/doxm", "rt": ["oic.r.doxm"], "p": {"bm": 1}},
],
},
],
}
)
ident = read_identity(sess, serial=None)
assert ident.raw["/oic/res"] == [
{
"di": "aaaa",
"links": [
{"href": "/oic/d", "rt": ["oic.wk.d", "oic.d.refrigerator"], "p": {"bm": 1}},
{"href": "/oic/sec/doxm", "rt": ["oic.r.doxm"], "p": {"bm": 1}},
],
},
]
def test_read_identity_tolerates_malformed_oic_res():
"""A single Property map instead of an array (or anything else
non-list-shaped) must not explode -- same defensive posture as
_device_types' handling of a malformed /oic/d rt."""
ident = read_identity(FakeSession({("oic", "res"): {"not": "a list"}}), serial=None)
assert ident.raw["/oic/res"] == []