diff --git a/custom_components/localthings/registry/capabilities/washer.py b/custom_components/localthings/registry/capabilities/washer.py index 97d26a4..594adf1 100644 --- a/custom_components/localthings/registry/capabilities/washer.py +++ b/custom_components/localthings/registry/capabilities/washer.py @@ -310,7 +310,10 @@ def _add_wash_set_write(mask): def _add_wash_alarm_write(p, rep, href=None): - if p not in ("On", "Off") or not rep.get("x.com.samsung.da.options"): + # Gated on the mask being readable, like the per-moment writes: a device + # reporting a wider mask than these three bits would otherwise have it + # truncated to 7 here, silently dropping a moment it supports. + if p not in ("On", "Off") or _add_wash_mask(rep, "AddWashSet") is None: return None return _add_wash_set_write(0b111 if p == "On" else 0) @@ -320,7 +323,10 @@ def _add_wash_bit_switch(key, icon, bit): The mask is the only state, so switching the last moment off lands on 0 and takes the alarm with it, and switching one on from 0 turns the alarm - back on. + back on. The corollary is that switching the master off and on again + writes 7, resetting a rinse-only selection to all three moments -- the + appliance remembers no previous subset either, so there is nothing to + restore. """ def read(rep): @@ -445,6 +451,10 @@ WASHER_COURSE = Capability( _add_wash_bit_switch("add_wash_alarm_rinse", "mdi:water", 0), _add_wash_bit_switch("add_wash_alarm_final_rinse", "mdi:water-check", 1), _add_wash_bit_switch("add_wash_alarm_spin", "mdi:sync", 2), + # On at rest: an idle washer reports AddWashAvailable_7 and the mask + # only empties as the cycle consumes each moment. This says the cycle + # permits AddWash, not that laundry can go in now -- that is + # add_wash_indicator. BinarySensorDesc( key="add_wash_available", icon="mdi:tshirt-crew-outline", diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index 5a3d805..63e8b4b 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -390,9 +390,12 @@ def test_registry_reproduces_golden_state_keys_for_washer_dryer_combo(): def test_registry_reproduces_golden_state_keys_for_washer_ww6500(): - """DA_WM_A51_20 front-loader, routed by /oic/d's oic.d.washer rather - than a board token. Reports the AddWash tokens and an empty - /wm/editcourse/vs/0, so its cycle list comes from supportedOptions.""" + """DA_WM_A51_20 front-loader, typed solely by the WW consumer prefix in + its /information/vs/0 description: A51 is not a board token, so with the + description blanked this device resolves to nothing and drops to the + unknown-device fallback. That is the fragile route this test pins. + Reports the AddWash tokens and an empty /wm/editcourse/vs/0, so its cycle + list comes from supportedOptions.""" from tests.conftest import _load_device resources = _load_device("washer_ww6500") diff --git a/tests/test_washer_add_wash.py b/tests/test_washer_add_wash.py index 305bb0c..24138b0 100644 --- a/tests/test_washer_add_wash.py +++ b/tests/test_washer_add_wash.py @@ -86,6 +86,17 @@ class TestAlarmMasterSwitch: desc = _desc("add_wash_alarm", SwitchDesc) assert _write(desc, "On", {}) is None + @pytest.mark.parametrize("raw", ["15", "On", "-1"]) + def test_refuses_to_write_over_a_mask_it_cannot_read(self, raw): + """A device reporting a wider mask must not have it truncated to 7. + The master is gated exactly like the per-moment writes, so an + unrecognized mask leaves every AddWash switch read-only.""" + desc = _desc("add_wash_alarm", SwitchDesc) + rep = _rep(f"AddWashSet_{raw}") + assert desc.rep_fn(rep) is None + assert _write(desc, "On", rep) is None + assert _write(desc, "Off", rep) is None + class TestAlarmMomentSwitches: @pytest.mark.parametrize("key,bit", BITS.items()) @@ -197,6 +208,17 @@ class TestAgainstTheWW6500Dump: """DA_WM_A51_20_COMMON_WW6500, captured with the alarm off, the course permitting all three moments and the lamp dark.""" + def test_types_only_by_the_description_consumer_prefix(self): + """A51 is not a board token, so the WW prefix in the description is + the only thing routing this device -- see the golden test's docstring. + Pinned here so the claim can't quietly stop being true.""" + from custom_components.localthings.registry.by_type import for_device_by_model + + info = _load_device("washer_ww6500")["/information/vs/0"] + model = info["x.com.samsung.da.modelNum"] + assert for_device_by_model(model, info["x.com.samsung.da.description"]).name == "washer" + assert for_device_by_model(model, "") is None + def test_no_unbound_hrefs(self): resources = _load_device("washer_ww6500") reg = resolve(resources)