Apply ruff format to code merged from PR #251/#275

PR #251 and PR #275 predate this repo's ruff-format adoption on those
files; running the formatter (single->double quotes, line wrapping,
trailing-comma cleanup) keeps the merged code consistent with the rest
of the codebase. No behavior change.
This commit is contained in:
Marc Billow
2026-08-08 21:24:02 +00:00
parent 68dee12eb4
commit 8ed3d1467f
10 changed files with 222 additions and 136 deletions
+5 -10
View File
@@ -658,7 +658,8 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _subdevice_probe_priority( def _subdevice_probe_priority(
self, resources: dict[str, dict], self,
resources: dict[str, dict],
) -> tuple[str, ...]: ) -> tuple[str, ...]:
"""Return live primary-entity hrefs in hot/warm-first order. """Return live primary-entity hrefs in hot/warm-first order.
@@ -676,23 +677,17 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
if registry is None: if registry is None:
return () return ()
tier_rank = {'hot': 0, 'warm': 1, 'cold': 2} tier_rank = {"hot": 0, "warm": 1, "cold": 2}
ranked = [] ranked = []
for order, href in enumerate(resources): for order, href in enumerate(resources):
primary_caps = [ primary_caps = [
capability capability
for capability in registry.capabilities.get(href, ()) for capability in registry.capabilities.get(href, ())
if any( if any(desc.entity_category is None for desc in capability.entities)
desc.entity_category is None
for desc in capability.entities
)
] ]
if not primary_caps: if not primary_caps:
continue continue
rank = min( rank = min(tier_rank.get(capability.poll_tier, 2) for capability in primary_caps)
tier_rank.get(capability.poll_tier, 2)
for capability in primary_caps
)
ranked.append((rank, order, href)) ranked.append((rank, order, href))
return tuple(href for _, _, href in sorted(ranked)) return tuple(href for _, _, href in sorted(ranked))
@@ -20,9 +20,9 @@ Door-LED keys use NO `x.com.samsung.da.` prefix -- `setBrightness` /
`setNightLight` -- preserved exactly as they appear in the OCF resource rep. `setNightLight` -- preserved exactly as they appear in the OCF resource rep.
""" """
import string
from datetime import UTC, datetime from datetime import UTC, datetime
from datetime import time as dt_time from datetime import time as dt_time
import string
from ...catalog import has_entity_translation from ...catalog import has_entity_translation
from ..capability import Capability from ..capability import Capability
@@ -324,7 +324,7 @@ def cycle_write(p, rep, href=None):
} }
def personal_course_labels(resources, href='/wm/personalcourse/vs/0'): def personal_course_labels(resources, href="/wm/personalcourse/vs/0"):
"""Return device-provided personal course names keyed by course code. """Return device-provided personal course names keyed by course code.
Populated entries use a small TLV payload. The leading field is Populated entries use a small TLV payload. The leading field is
@@ -335,10 +335,10 @@ def personal_course_labels(resources, href='/wm/personalcourse/vs/0'):
""" """
rep = resources.get(href) or {} rep = resources.get(href) or {}
labels = {} labels = {}
for entry in rep.get('x.com.samsung.da.courses') or []: for entry in rep.get("x.com.samsung.da.courses") or []:
if not isinstance(entry, str) or '_' not in entry: if not isinstance(entry, str) or "_" not in entry:
continue continue
code, encoded = entry.split('_', 1) code, encoded = entry.split("_", 1)
try: try:
payload = bytes.fromhex(encoded) payload = bytes.fromhex(encoded)
except ValueError: except ValueError:
@@ -349,7 +349,7 @@ def personal_course_labels(resources, href='/wm/personalcourse/vs/0'):
if name_length == 0 or len(payload) < 2 + name_length: if name_length == 0 or len(payload) < 2 + name_length:
continue continue
try: try:
name = payload[2:2 + name_length].decode('utf-8') name = payload[2 : 2 + name_length].decode("utf-8")
except UnicodeDecodeError: except UnicodeDecodeError:
continue continue
if name.strip() and name.isprintable(): if name.strip() and name.isprintable():
@@ -364,7 +364,7 @@ def washer_cycle_fallback(value, resources):
if label := personal_course_labels(resources).get(value.upper()): if label := personal_course_labels(resources).get(value.upper()):
return label return label
if len(value) == 2 and all(char in string.hexdigits for char in value): if len(value) == 2 and all(char in string.hexdigits for char in value):
return f'Unknown (0x{value.upper()})' return f"Unknown (0x{value.upper()})"
return None return None
@@ -236,7 +236,9 @@ def _get_raw(sess, path_segs: tuple[str, ...], timeout: float = 10.0):
def _get_batch( def _get_batch(
sess, path_segs: tuple[str, ...], timeout: float = 10.0, sess,
path_segs: tuple[str, ...],
timeout: float = 10.0,
) -> dict[str, dict]: ) -> dict[str, dict]:
"""GET a Samsung Collection resource and parse it the same way """GET a Samsung Collection resource and parse it the same way
/device/0 itself is parsed (parse_device0_batch): a [devcol-rep, /device/0 itself is parsed (parse_device0_batch): a [devcol-rep,
@@ -246,7 +248,9 @@ def _get_batch(
def _get_property( def _get_property(
sess, path_segs: tuple[str, ...], timeout: float = 10.0, sess,
path_segs: tuple[str, ...],
timeout: float = 10.0,
) -> dict: ) -> dict:
"""GET a plain OCF Property-map resource (a bare dict, not a Collection """GET a plain OCF Property-map resource (a bare dict, not a Collection
batch). Used for `/multidevice/vs/0` (issue #177 follow-up): listed in batch). Used for `/multidevice/vs/0` (issue #177 follow-up): listed in
@@ -301,10 +305,7 @@ def enumerate_subdevices(
# casing, and probing it twice would materialize the same physical # casing, and probing it twice would materialize the same physical
# subdevice as two Subdevice candidates. # subdevice as two Subdevice candidates.
probed_ids: set[str] = set() probed_ids: set[str] = set()
deadline = ( deadline = time.monotonic() + max(0.0, time_budget) if time_budget is not None else None
time.monotonic() + max(0.0, time_budget)
if time_budget is not None else None
)
budget_exhausted = False budget_exhausted = False
def _next_timeout(maximum: float) -> float | None: def _next_timeout(maximum: float) -> float | None:
+1 -2
View File
@@ -99,8 +99,7 @@ class LocalThingsSelect(LocalThingsEntity, SelectEntity):
"""Normalize both current state and options through one path.""" """Normalize both current state and options through one path."""
display_fn = self._bound.desc.display_fn display_fn = self._bound.desc.display_fn
fallback_fn = ( fallback_fn = (
(lambda raw: display_fn(raw, self._resources)) (lambda raw: display_fn(raw, self._resources)) if display_fn is not None else None
if display_fn is not None else None
) )
return _display(value, self.translation_key, fallback_fn) return _display(value, self.translation_key, fallback_fn)
+21 -21
View File
@@ -67,28 +67,28 @@ class TestCourseHelpers:
assigning an inferred meaning to any standard course code. assigning an inferred meaning to any standard course code.
""" """
resources = { resources = {
'/wm/personalcourse/vs/0': { "/wm/personalcourse/vs/0": {
'x.com.samsung.da.courses': [ "x.com.samsung.da.courses": [
'F1_0106EC868DEC98B7021EED8CACED8BB020EB93B1', "F1_0106EC868DEC98B7021EED8CACED8BB020EB93B1",
'F2_00', "F2_00",
'F3_0109EC9A94EAB8B0EBB3B40220ECA084EC9AA9', "F3_0109EC9A94EAB8B0EBB3B40220ECA084EC9AA9",
], ],
}, },
} }
first_name = bytes.fromhex('EC868DEC98B7').decode('utf-8') first_name = bytes.fromhex("EC868DEC98B7").decode("utf-8")
second_name = bytes.fromhex('EC9A94EAB8B0EBB3B4').decode('utf-8') second_name = bytes.fromhex("EC9A94EAB8B0EBB3B4").decode("utf-8")
assert laundry.personal_course_labels(resources) == { assert laundry.personal_course_labels(resources) == {
'F1': first_name, "F1": first_name,
'F3': second_name, "F3": second_name,
} }
def test_personal_course_names_reject_malformed_payloads(self): def test_personal_course_names_reject_malformed_payloads(self):
resources = { resources = {
'/wm/personalcourse/vs/0': { "/wm/personalcourse/vs/0": {
'x.com.samsung.da.courses': [ "x.com.samsung.da.courses": [
'F1_not-hex', "F1_not-hex",
'F2_0106AA', "F2_0106AA",
'F3_020141', "F3_020141",
None, None,
], ],
}, },
@@ -97,15 +97,15 @@ class TestCourseHelpers:
def test_washer_cycle_fallback_uses_labels_then_safe_unknown_text(self): def test_washer_cycle_fallback_uses_labels_then_safe_unknown_text(self):
resources = { resources = {
'/wm/personalcourse/vs/0': { "/wm/personalcourse/vs/0": {
'x.com.samsung.da.courses': ['F1_0106EC868DEC98B7'], "x.com.samsung.da.courses": ["F1_0106EC868DEC98B7"],
}, },
} }
expected = bytes.fromhex('EC868DEC98B7').decode('utf-8') expected = bytes.fromhex("EC868DEC98B7").decode("utf-8")
assert laundry.washer_cycle_fallback('F1', resources) == expected assert laundry.washer_cycle_fallback("F1", resources) == expected
assert laundry.washer_cycle_fallback('69', resources) == 'Unknown (0x69)' assert laundry.washer_cycle_fallback("69", resources) == "Unknown (0x69)"
assert laundry.washer_cycle_fallback('6f', resources) == 'Unknown (0x6F)' assert laundry.washer_cycle_fallback("6f", resources) == "Unknown (0x6F)"
assert laundry.washer_cycle_fallback('Normal', resources) is None assert laundry.washer_cycle_fallback("Normal", resources) is None
class TestCourseCodesFromSupportedOptions: class TestCourseCodesFromSupportedOptions:
+1 -3
View File
@@ -49,6 +49,4 @@ def test_display_passes_through_non_string_values():
def test_display_uses_fallback_when_translation_has_no_state_table(): def test_display_uses_fallback_when_translation_has_no_state_table():
assert _display('69', 'cycle', lambda value: f'Unknown (0x{value})') == ( assert _display("69", "cycle", lambda value: f"Unknown (0x{value})") == ("Unknown (0x69)")
'Unknown (0x69)'
)
+1
View File
@@ -277,6 +277,7 @@ async def test_fac_bora_2in1_unique_ids_include_subdevice_prefix(hass: HomeAssis
# capture instead of the synthetic sessions test_subdevices.py uses. # capture instead of the synthetic sessions test_subdevices.py uses.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def test_flat_probe_priority_puts_live_climate_state_before_cold_metrics( async def test_flat_probe_priority_puts_live_climate_state_before_cold_metrics(
hass: HomeAssistant, hass: HomeAssistant,
): ):
+22 -20
View File
@@ -360,6 +360,7 @@ def test_enumeration_budget_bounds_silent_prefixed_fallback_and_uses_priority(
live-state href is then attempted first, and the last Property probe is live-state href is then attempted first, and the last Property probe is
clamped to exactly the time left in the shared enumeration budget. clamped to exactly the time left in the shared enumeration budget.
""" """
class Clock: class Clock:
now = 0.0 now = 0.0
@@ -381,20 +382,20 @@ def test_enumeration_budget_bounds_silent_prefixed_fallback_and_uses_priority(
clock = Clock() clock = Clock()
session = SilentSession(clock) session = SilentSession(clock)
monkeypatch.setattr(subdevices_module.time, 'monotonic', clock.monotonic) monkeypatch.setattr(subdevices_module.time, "monotonic", clock.monotonic)
resources = { resources = {
'/subdevices/vs/0': { "/subdevices/vs/0": {
'x.com.samsung.da.subdeviceIdList': [_UUID], "x.com.samsung.da.subdeviceIdList": [_UUID],
}, },
'/power/vs/0': {'power': 'On'}, "/power/vs/0": {"power": "On"},
'/mode/vs/0': {'mode': 'Cool'}, "/mode/vs/0": {"mode": "Cool"},
} }
found, extra = enumerate_subdevices( found, extra = enumerate_subdevices(
session, session,
resources, resources,
oic_res_links=[], oic_res_links=[],
preferred_hrefs=('/mode/vs/0',), preferred_hrefs=("/mode/vs/0",),
time_budget=7.0, time_budget=7.0,
collection_timeout=4.0, collection_timeout=4.0,
property_timeout=2.0, property_timeout=2.0,
@@ -404,9 +405,9 @@ def test_enumeration_budget_bounds_silent_prefixed_fallback_and_uses_priority(
assert extra == {} assert extra == {}
assert clock.now == 7.0 assert clock.now == 7.0
assert session.calls == [ assert session.calls == [
((_UUID, 'device', '0'), 4.0), ((_UUID, "device", "0"), 4.0),
((_UUID, 'mode', 'vs', '0'), 2.0), ((_UUID, "mode", "vs", "0"), 2.0),
((_UUID, 'power', 'vs', '0'), 1.0), ((_UUID, "power", "vs", "0"), 1.0),
] ]
@@ -414,6 +415,7 @@ def test_enumeration_keeps_preferred_response_found_before_budget_expires(
monkeypatch, monkeypatch,
): ):
"""A useful early response survives later silent probes hitting the cap.""" """A useful early response survives later silent probes hitting the cap."""
class Clock: class Clock:
now = 0.0 now = 0.0
@@ -425,8 +427,8 @@ def test_enumeration_keeps_preferred_response_found_before_budget_expires(
self.clock = clock self.clock = clock
def get(self, path, timeout=10.0): def get(self, path, timeout=10.0):
if tuple(path) == (_UUID, 'mode', 'vs', '0'): if tuple(path) == (_UUID, "mode", "vs", "0"):
return 0x45, cbor2.dumps({'mode': 'Cool'}) return 0x45, cbor2.dumps({"mode": "Cool"})
self.clock.now += timeout self.clock.now += timeout
raise TimeoutError raise TimeoutError
@@ -434,30 +436,30 @@ def test_enumeration_keeps_preferred_response_found_before_budget_expires(
pass pass
clock = Clock() clock = Clock()
monkeypatch.setattr(subdevices_module.time, 'monotonic', clock.monotonic) monkeypatch.setattr(subdevices_module.time, "monotonic", clock.monotonic)
resources = { resources = {
'/subdevices/vs/0': { "/subdevices/vs/0": {
'x.com.samsung.da.subdeviceIdList': [_UUID], "x.com.samsung.da.subdeviceIdList": [_UUID],
}, },
'/power/vs/0': {'power': 'On'}, "/power/vs/0": {"power": "On"},
'/mode/vs/0': {'mode': 'Cool'}, "/mode/vs/0": {"mode": "Cool"},
} }
found, extra = enumerate_subdevices( found, extra = enumerate_subdevices(
PartlyResponsiveSession(clock), PartlyResponsiveSession(clock),
resources, resources,
oic_res_links=[], oic_res_links=[],
preferred_hrefs=('/mode/vs/0',), preferred_hrefs=("/mode/vs/0",),
time_budget=7.0, time_budget=7.0,
collection_timeout=4.0, collection_timeout=4.0,
property_timeout=2.0, property_timeout=2.0,
) )
assert [(subdevice.kind, subdevice.key) for subdevice in found] == [ assert [(subdevice.kind, subdevice.key) for subdevice in found] == [
('prefixed', _UUID), ("prefixed", _UUID),
] ]
assert found[0].flat_hrefs == ('/mode/vs/0',) assert found[0].flat_hrefs == ("/mode/vs/0",)
assert extra == {f'/{_UUID}/mode/vs/0': {'mode': 'Cool'}} assert extra == {f"/{_UUID}/mode/vs/0": {"mode": "Cool"}}
assert clock.now == 7.0 assert clock.now == 7.0
+138 -64
View File
@@ -115,90 +115,164 @@ def test_no_catalog_carries_unresolved_core_references():
def test_confirmed_korean_table_02_washer_course_names(): def test_confirmed_korean_table_02_washer_course_names():
states = _load('en')['entity']['select']['washer_cycle_table_02']['state'] states = _load("en")["entity"]["select"]["washer_cycle_table_02"]["state"]
assert {code: states[code] for code in ( assert {
'69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', code: states[code]
'72', '73', '74', '75', '76', '77', '78', '79', '88', for code in (
)} == { "69",
'69': 'AI Wash', "6a",
'6a': 'Wool', "6b",
'6b': 'Denim', "6c",
'6c': 'Blouses', "6d",
'6d': 'Delicates', "6e",
'6e': 'Active Wear', "6f",
'6f': 'Bedding', "70",
'70': 'Towels', "71",
'71': 'Quick Wash', "72",
'72': 'Shirts', "73",
'73': 'Sanitize', "74",
'74': 'Drum Clean', "75",
'75': 'Outdoor', "76",
'76': 'Baby Care', "77",
'77': 'Cottons', "78",
'78': 'Rinse + Spin', "79",
'79': 'Spin Only', "88",
'88': 'Pet Care', )
} == {
"69": "AI Wash",
"6a": "Wool",
"6b": "Denim",
"6c": "Blouses",
"6d": "Delicates",
"6e": "Active Wear",
"6f": "Bedding",
"70": "Towels",
"71": "Quick Wash",
"72": "Shirts",
"73": "Sanitize",
"74": "Drum Clean",
"75": "Outdoor",
"76": "Baby Care",
"77": "Cottons",
"78": "Rinse + Spin",
"79": "Spin Only",
"88": "Pet Care",
} }
def test_confirmed_dishwasher_course_names(): def test_confirmed_dishwasher_course_names():
states = _load('en')['entity']['select']['dishwasher_cycle']['state'] states = _load("en")["entity"]["select"]["dishwasher_cycle"]["state"]
assert {code: states[code] for code in ( assert {
'82', '8a', 'a7', 'a8', '8c', '88', code: states[code]
)} == { for code in (
'82': 'Auto', "82",
'8a': 'Normal', "8a",
'a7': 'Heavy', "a7",
'a8': 'Express', "a8",
'8c': 'Extra Silence', "8c",
'88': 'Self Clean', "88",
)
} == {
"82": "Auto",
"8a": "Normal",
"a7": "Heavy",
"a8": "Express",
"8c": "Extra Silence",
"88": "Self Clean",
} }
def test_confirmed_course_names_are_localized(): def test_confirmed_course_names_are_localized():
washer_codes = ( washer_codes = (
'69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', "69",
'72', '73', '74', '75', '76', '77', '78', '79', '88', "6a",
"6b",
"6c",
"6d",
"6e",
"6f",
"70",
"71",
"72",
"73",
"74",
"75",
"76",
"77",
"78",
"79",
"88",
) )
dishwasher_codes = ('82', '8a', 'a7', 'a8', '8c', '88') dishwasher_codes = ("82", "8a", "a7", "a8", "8c", "88")
expected = { expected = {
'cs': { "cs": {
'washer': ( "washer": (
'AI praní', 'Vlna', 'Džíny', 'Halenky', 'Jemné prádlo', "AI praní",
'Sportovní oblečení', 'Ložní prádlo', 'Ručníky', "Vlna",
'Rychlé praní', 'Košile', 'Dezinfekce', 'Čištění bubnu', "Džíny",
'Outdoor', 'Dětské potřeby', 'Bavlna', "Halenky",
'Máchání + odstřeďování', 'Pouze odstřeďování', "Jemné prádlo",
'Péče o domácí mazlíčky', "Sportovní oblečení",
"Ložní prádlo",
"Ručníky",
"Rychlé praní",
"Košile",
"Dezinfekce",
"Čištění bubnu",
"Outdoor",
"Dětské potřeby",
"Bavlna",
"Máchání + odstřeďování",
"Pouze odstřeďování",
"Péče o domácí mazlíčky",
), ),
'dishwasher': ( "dishwasher": (
'Automatický', 'Normální', 'Intenzivní', 'Expresní', "Automatický",
'Extra tichý', 'Samočištění', "Normální",
"Intenzivní",
"Expresní",
"Extra tichý",
"Samočištění",
), ),
}, },
'nl': { "nl": {
'washer': ( "washer": (
'AI Wash', 'Wol', 'Spijkergoed', 'Blouses', 'Fijne was', "AI Wash",
'Sportkleding', 'Beddengoed', 'Handdoeken', 'Snelle was', "Wol",
'Overhemden', 'Hygiëne', 'Trommel reinigen', 'Outdoor', "Spijkergoed",
'Babyverzorging', 'Katoen', 'Spoelen + centrifugeren', "Blouses",
'Alleen centrifugeren', 'Pet Care', "Fijne was",
"Sportkleding",
"Beddengoed",
"Handdoeken",
"Snelle was",
"Overhemden",
"Hygiëne",
"Trommel reinigen",
"Outdoor",
"Babyverzorging",
"Katoen",
"Spoelen + centrifugeren",
"Alleen centrifugeren",
"Pet Care",
), ),
'dishwasher': ( "dishwasher": (
'Auto', 'Normaal', 'Intensief', 'Express', 'Extra stil', "Auto",
'Zelfreiniging', "Normaal",
"Intensief",
"Express",
"Extra stil",
"Zelfreiniging",
), ),
}, },
} }
for language, translations in expected.items(): for language, translations in expected.items():
catalog = _load(language)['entity']['select'] catalog = _load(language)["entity"]["select"]
washer = catalog['washer_cycle_table_02']['state'] washer = catalog["washer_cycle_table_02"]["state"]
dishwasher = catalog['dishwasher_cycle']['state'] dishwasher = catalog["dishwasher_cycle"]["state"]
assert tuple(washer[code] for code in washer_codes) == translations['washer'] assert tuple(washer[code] for code in washer_codes) == translations["washer"]
assert tuple(dishwasher[code] for code in dishwasher_codes) == ( assert tuple(dishwasher[code] for code in dishwasher_codes) == (translations["dishwasher"])
translations['dishwasher']
)
# The hood fan is its device's primary feature: fan.py sets _attr_name = None # The hood fan is its device's primary feature: fan.py sets _attr_name = None
+19 -3
View File
@@ -125,10 +125,26 @@ class TestWasherCourse:
from custom_components.localthings.catalog import translated_states from custom_components.localthings.catalog import translated_states
confirmed = { confirmed = {
'69', '6a', '6b', '6c', '6d', '6e', '6f', '70', '71', "69",
'72', '73', '74', '75', '76', '77', '78', '79', '88', "6a",
"6b",
"6c",
"6d",
"6e",
"6f",
"70",
"71",
"72",
"73",
"74",
"75",
"76",
"77",
"78",
"79",
"88",
} }
assert confirmed <= translated_states('select', 'washer_cycle_table_02') assert confirmed <= translated_states("select", "washer_cycle_table_02")
def test_missing_course_option_returns_none(self): def test_missing_course_option_returns_none(self):
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == "cycle") desc = next(e for e in washer.WASHER_COURSE.entities if e.key == "cycle")