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.
123 lines
4.4 KiB
Python
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"] == []
|