Merge pull request #401 from vmonkey/feature/add-wash
Add AddWash controls and sensors for washers
This commit is contained in:
13 files changed
+807
-1
No files matched your search
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"add_wash_alarm",
|
||||
"add_wash_alarm_final_rinse",
|
||||
"add_wash_alarm_rinse",
|
||||
"add_wash_alarm_spin",
|
||||
"add_wash_available",
|
||||
"add_wash_indicator",
|
||||
"alarm_code",
|
||||
"bubble_soak",
|
||||
"child_lock",
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"add_wash_alarm",
|
||||
"add_wash_alarm_final_rinse",
|
||||
"add_wash_alarm_rinse",
|
||||
"add_wash_alarm_spin",
|
||||
"add_wash_available",
|
||||
"add_wash_indicator",
|
||||
"alarm_code",
|
||||
"child_lock",
|
||||
"completion_minutes",
|
||||
"cycle",
|
||||
"cycle_active",
|
||||
"delay_start_hours",
|
||||
"diagnosis_status",
|
||||
"energy_kwh",
|
||||
"finish_time",
|
||||
"job_beginning_status",
|
||||
"machine_state",
|
||||
"power_switch",
|
||||
"progress",
|
||||
"progress_percentage",
|
||||
"remote_control",
|
||||
"rinse_cycles",
|
||||
"spin_speed",
|
||||
"wash_temperature"
|
||||
]
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"device0": [
|
||||
{},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.cumulativePower": "2016700",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh",
|
||||
"x.com.samsung.da.cumulativeDate": "1787050800",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1787050800"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/course/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_0167",
|
||||
"Course_5C",
|
||||
"LaundryOutTime_0",
|
||||
"AddWashSet_0",
|
||||
"AddWashAvailable_7",
|
||||
"AddWashIndicator_Off",
|
||||
"QuickWash_Not_Used",
|
||||
"QuickWashSet_5B847E933FA53F",
|
||||
"UsagesDB_ok",
|
||||
"EnergyKW_396",
|
||||
"DrumCleanLog_Empty",
|
||||
"TimeSync_NotSupported"
|
||||
],
|
||||
"x.com.samsung.da.supportedOptions": [
|
||||
"35B847E933FA53F5C841E923FA53F5D8102923FA43F66841E930FA30F5E831E920FA2075F867E943FA53F60831E930FA43F61841E943FA43F6385209204A204648000913FA53F6B80009000A53E65841E920FA30F67843E923FA43F688430923FA53F"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/0",
|
||||
"rep": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cycleinterface/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.kidsLock": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/kidslock/0",
|
||||
"rep": {
|
||||
"value": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.state": "Ready",
|
||||
"x.com.samsung.da.remainingTime": "01:07:00",
|
||||
"x.com.samsung.da.progressPercentage": "1",
|
||||
"x.com.samsung.da.progress": "None",
|
||||
"x.com.samsung.da.supportedProgress": [
|
||||
"None",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/operational/state/0",
|
||||
"rep": {
|
||||
"currentMachineState": "idle",
|
||||
"machineStates": [
|
||||
"pause",
|
||||
"active",
|
||||
"idle"
|
||||
],
|
||||
"jobStates": [
|
||||
"None",
|
||||
"Wash",
|
||||
"Rinse",
|
||||
"Spin",
|
||||
"Finish"
|
||||
],
|
||||
"currentJobState": "None",
|
||||
"remainingTime": "01:07:00",
|
||||
"progressPercentage": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "DA_WM_A51_20_COMMON|FFFFFFFF|20010102001011070000000000000000",
|
||||
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON_WW6500",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON|FFFFFFFF|20010102001011070000000000000000",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "02198A230708(E257)",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "DA_WM_A51_20_COMMON",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "Unknown",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/washer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.waterTemperature": "30",
|
||||
"x.com.samsung.da.supportedWaterTemperature": [
|
||||
"None",
|
||||
"Cold",
|
||||
"20",
|
||||
"30",
|
||||
"40",
|
||||
"60",
|
||||
"95"
|
||||
],
|
||||
"x.com.samsung.da.spinLevel": "1400",
|
||||
"x.com.samsung.da.supportedSpinLevel": [
|
||||
"RinseHold",
|
||||
"NoSpin",
|
||||
"400",
|
||||
"800",
|
||||
"1200",
|
||||
"1400"
|
||||
],
|
||||
"x.com.samsung.da.rinseCycles": "3",
|
||||
"x.com.samsung.da.supportedRinseCycles": [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/st/washercourse/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.st.washerMode": "Table_00_Course_5C",
|
||||
"x.com.samsung.da.st.courseTable": "Table_00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/setting/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/editcourse/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/wm/setinfo/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.isModelSettingWithoutSC": "false",
|
||||
"x.com.samsung.da.isModelSettingPowerOnOff": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/wm/jobbeginingstatus/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.remoteControlEnabled": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/remotectrl/0",
|
||||
"rep": {
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.countryCode": "CZ"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -408,6 +408,25 @@ 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, 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")
|
||||
golden = json.loads((GOLDEN / "washer_ww6500.json").read_text())
|
||||
state_keys = _new_state_keys("washer_ww6500", resources)
|
||||
assert set(state_keys) == set(golden["state_keys"]), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
||||
"""ARTIK051_REF_17K's Cool Select Zone pantry compartment
|
||||
(/status/pantry/one/vs/0) -- issue #20."""
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
"""Tests for the AddWash entities on /course/vs/0.
|
||||
|
||||
Three independent tokens: AddWashSet (the alarm's 3-bit mask, the only
|
||||
writable one), AddWashAvailable (what the loaded course permits) and
|
||||
AddWashIndicator (the live panel lamp). See washer.py for where the bit
|
||||
meanings and the write contract come from.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import resolve
|
||||
from custom_components.localthings.registry.capabilities import washer
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import BinarySensorDesc, SwitchDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
COURSE = ["course", "vs", "0"]
|
||||
BITS = {
|
||||
"add_wash_alarm_rinse": 0,
|
||||
"add_wash_alarm_final_rinse": 1,
|
||||
"add_wash_alarm_spin": 2,
|
||||
}
|
||||
ENTITY_TOKENS = {
|
||||
"add_wash_alarm": "AddWashSet",
|
||||
"add_wash_alarm_rinse": "AddWashSet",
|
||||
"add_wash_alarm_final_rinse": "AddWashSet",
|
||||
"add_wash_alarm_spin": "AddWashSet",
|
||||
"add_wash_available": "AddWashAvailable",
|
||||
"add_wash_indicator": "AddWashIndicator",
|
||||
}
|
||||
# A value each token really carries, so presence gating is exercised against
|
||||
# what a washer reports rather than a synthetic one.
|
||||
SAMPLE = {"AddWashSet": "0", "AddWashAvailable": "7", "AddWashIndicator": "Off"}
|
||||
|
||||
|
||||
def _desc(key, kind):
|
||||
return next(e for e in washer.WASHER_COURSE.entities if e.key == key and isinstance(e, kind))
|
||||
|
||||
|
||||
def _rep(*tokens):
|
||||
return {"x.com.samsung.da.options": list(tokens)}
|
||||
|
||||
|
||||
def _write(desc, payload, rep):
|
||||
return desc.write_fn(payload, rep)
|
||||
|
||||
|
||||
def _options(result):
|
||||
"""The tokens a write_fn result carries, asserting it targets /course/vs/0."""
|
||||
path, body = result
|
||||
assert path == COURSE
|
||||
return body["x.com.samsung.da.options"]
|
||||
|
||||
|
||||
def _exists(key, rep):
|
||||
"""Whether `key`'s descriptor gates itself in for `rep`."""
|
||||
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == key)
|
||||
assert desc.exists_fn is not None
|
||||
return desc.exists_fn(rep, {})
|
||||
|
||||
|
||||
def _flatten(fixture):
|
||||
resources = _load_device(fixture)
|
||||
reg = resolve(resources)
|
||||
assert reg is not None
|
||||
return flatten(discover(resources, reg.capabilities, reg.pattern_capabilities), resources)
|
||||
|
||||
|
||||
class TestAlarmMasterSwitch:
|
||||
def test_zero_reads_off_and_seven_reads_on(self):
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
assert desc.rep_fn(_rep("AddWashSet_0")) is False
|
||||
assert desc.rep_fn(_rep("AddWashSet_7")) is True
|
||||
|
||||
@pytest.mark.parametrize("mask", range(8))
|
||||
def test_any_non_zero_mask_reads_on(self, mask):
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
assert desc.rep_fn(_rep(f"AddWashSet_{mask}")) is (mask != 0)
|
||||
|
||||
def test_turning_on_enables_every_moment(self):
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
assert _options(_write(desc, "On", _rep("AddWashSet_0"))) == ["AddWashSet_7"]
|
||||
|
||||
def test_turning_off_clears_the_mask(self):
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
assert _options(_write(desc, "Off", _rep("AddWashSet_5"))) == ["AddWashSet_0"]
|
||||
|
||||
@pytest.mark.parametrize("mask", range(1, 8))
|
||||
def test_on_over_an_alarm_already_on_keeps_the_chosen_moments(self, mask):
|
||||
"""Home Assistant calls turn_on regardless of current state, so
|
||||
re-asserting "on" over a rinse-only mask must not widen it to all
|
||||
three -- this switch reads on either way, so no state change would
|
||||
point at the loss. Reaching 7 from a subset still means off, then
|
||||
on."""
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
rep = _rep(f"AddWashSet_{mask}")
|
||||
assert desc.rep_fn(rep) is True
|
||||
assert _write(desc, "On", rep) is None
|
||||
assert _options(_write(desc, "Off", rep)) == ["AddWashSet_0"]
|
||||
assert _options(_write(desc, "On", _rep("AddWashSet_0"))) == ["AddWashSet_7"]
|
||||
|
||||
def test_rejects_a_payload_that_is_not_on_or_off(self):
|
||||
desc = _desc("add_wash_alarm", SwitchDesc)
|
||||
assert _write(desc, "7", _rep("AddWashSet_0")) is None
|
||||
|
||||
def test_rejects_a_write_against_an_empty_options_array(self):
|
||||
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())
|
||||
@pytest.mark.parametrize("mask", range(8))
|
||||
def test_every_mask_decodes_to_the_right_bits(self, key, bit, mask):
|
||||
desc = _desc(key, SwitchDesc)
|
||||
assert desc.rep_fn(_rep(f"AddWashSet_{mask}")) is bool(mask >> bit & 1)
|
||||
|
||||
def test_setting_one_moment_leaves_the_others_alone(self):
|
||||
desc = _desc("add_wash_alarm_final_rinse", SwitchDesc)
|
||||
# 5 is rinse + spin; adding the final rinse must reach 7, not 2.
|
||||
assert _options(_write(desc, "On", _rep("AddWashSet_5"))) == ["AddWashSet_7"]
|
||||
|
||||
def test_clearing_one_moment_leaves_the_others_alone(self):
|
||||
desc = _desc("add_wash_alarm_rinse", SwitchDesc)
|
||||
assert _options(_write(desc, "Off", _rep("AddWashSet_7"))) == ["AddWashSet_6"]
|
||||
|
||||
def test_clearing_the_last_moment_yields_zero(self):
|
||||
desc = _desc("add_wash_alarm_spin", SwitchDesc)
|
||||
assert _options(_write(desc, "Off", _rep("AddWashSet_4"))) == ["AddWashSet_0"]
|
||||
|
||||
def test_enabling_a_moment_from_zero_turns_the_alarm_on(self):
|
||||
"""The mask is the only state, so this is the intended outcome --
|
||||
there is no remembered combination to restore."""
|
||||
moment = _desc("add_wash_alarm_spin", SwitchDesc)
|
||||
written = _options(_write(moment, "On", _rep("AddWashSet_0")))
|
||||
assert written == ["AddWashSet_4"]
|
||||
assert _desc("add_wash_alarm", SwitchDesc).rep_fn(_rep(*written)) is True
|
||||
|
||||
@pytest.mark.parametrize("rep", [{}, _rep("AddWashSet_x"), _rep("Course_5C")])
|
||||
def test_refuses_to_write_when_the_mask_is_unreadable(self, rep):
|
||||
desc = _desc("add_wash_alarm_rinse", SwitchDesc)
|
||||
assert _write(desc, "On", rep) is None
|
||||
|
||||
|
||||
class TestMaskParsing:
|
||||
def test_a_missing_token_is_unavailable_not_zero(self):
|
||||
assert washer._add_wash_mask(_rep("Course_5C"), "AddWashSet") is None
|
||||
assert _desc("add_wash_alarm", SwitchDesc).rep_fn(_rep("Course_5C")) is None
|
||||
|
||||
def test_a_malformed_token_is_unavailable(self):
|
||||
assert washer._add_wash_mask(_rep("AddWashSet_On"), "AddWashSet") is None
|
||||
|
||||
def test_zero_is_a_real_value(self):
|
||||
assert washer._add_wash_mask(_rep("AddWashSet_0"), "AddWashSet") == 0
|
||||
|
||||
@pytest.mark.parametrize("raw", ["8", "255", "-1"])
|
||||
def test_a_mask_outside_three_bits_is_unavailable(self, raw):
|
||||
"""This models exactly three moments, so a wider value means the
|
||||
model is wrong -- refuse it rather than read-modify-write it back."""
|
||||
assert washer._add_wash_mask(_rep(f"AddWashSet_{raw}"), "AddWashSet") is None
|
||||
assert _desc("add_wash_alarm_rinse", SwitchDesc).rep_fn(_rep(f"AddWashSet_{raw}")) is None
|
||||
assert (
|
||||
_write(_desc("add_wash_alarm_spin", SwitchDesc), "On", _rep(f"AddWashSet_{raw}"))
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
class TestReadOnlySensors:
|
||||
@pytest.mark.parametrize("raw,expected", [("0", False), ("6", True), ("7", True)])
|
||||
def test_available_is_true_for_any_permitted_moment(self, raw, expected):
|
||||
desc = _desc("add_wash_available", BinarySensorDesc)
|
||||
assert desc.rep_fn(_rep(f"AddWashAvailable_{raw}")) is expected
|
||||
|
||||
@pytest.mark.parametrize("raw,expected", [("On", True), ("Off", False)])
|
||||
def test_indicator_maps_on_off(self, raw, expected):
|
||||
desc = _desc("add_wash_indicator", BinarySensorDesc)
|
||||
assert desc.rep_fn(_rep(f"AddWashIndicator_{raw}")) is expected
|
||||
|
||||
def test_indicator_ships_enabled_and_uncategorised(self):
|
||||
desc = _desc("add_wash_indicator", BinarySensorDesc)
|
||||
assert desc.enabled_default is True
|
||||
assert desc.entity_category is None
|
||||
|
||||
def test_missing_tokens_are_unavailable(self):
|
||||
assert _desc("add_wash_available", BinarySensorDesc).rep_fn(_rep()) is None
|
||||
assert _desc("add_wash_indicator", BinarySensorDesc).rep_fn(_rep()) is None
|
||||
|
||||
|
||||
class TestCapabilityDetection:
|
||||
"""Each entity self-gates on its own token, so a washer advertising only
|
||||
a subset gets only that subset."""
|
||||
|
||||
@pytest.mark.parametrize("key,token", ENTITY_TOKENS.items())
|
||||
def test_absent_on_a_washer_that_never_reports_the_token(self, key, token):
|
||||
assert _exists(key, _rep("Course_5C")) is False
|
||||
|
||||
@pytest.mark.parametrize("key,token", ENTITY_TOKENS.items())
|
||||
def test_present_once_the_token_appears(self, key, token):
|
||||
assert _exists(key, _rep(f"{token}_{SAMPLE[token]}")) is True
|
||||
|
||||
def test_a_washer_with_only_the_indicator_gets_only_that_entity(self):
|
||||
present = {
|
||||
e.key
|
||||
for e in washer.WASHER_COURSE.entities
|
||||
if e.key in ENTITY_TOKENS and _exists(e.key, _rep("AddWashIndicator_On"))
|
||||
}
|
||||
assert present == {"add_wash_indicator"}
|
||||
|
||||
def test_pre_add_wash_washers_gain_nothing(self):
|
||||
"""washer_device is a DA_WM_TP1_21 dump with no AddWash tokens."""
|
||||
state = _flatten("washer")
|
||||
assert not [key for key in state if key.startswith("add_wash")]
|
||||
|
||||
|
||||
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"]
|
||||
reg = for_device_by_model(model, info["x.com.samsung.da.description"])
|
||||
assert reg is not None and reg.name == "washer"
|
||||
assert for_device_by_model(model, "") is None
|
||||
|
||||
def test_no_unbound_hrefs(self):
|
||||
resources = _load_device("washer_ww6500")
|
||||
reg = resolve(resources)
|
||||
assert reg is not None
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
def test_reports_every_add_wash_entity(self):
|
||||
state = _flatten("washer_ww6500")
|
||||
assert {key for key in state if key.startswith("add_wash")} == {
|
||||
"add_wash_alarm",
|
||||
"add_wash_alarm_rinse",
|
||||
"add_wash_alarm_final_rinse",
|
||||
"add_wash_alarm_spin",
|
||||
"add_wash_available",
|
||||
"add_wash_indicator",
|
||||
}
|
||||
|
||||
def test_alarm_off_course_permits_lamp_dark(self):
|
||||
state = _flatten("washer_ww6500")
|
||||
assert state["add_wash_alarm"] is False
|
||||
assert all(state[f"add_wash_alarm_{m}"] is False for m in ("rinse", "final_rinse", "spin"))
|
||||
assert state["add_wash_available"] is True
|
||||
assert state["add_wash_indicator"] is False
|
||||
Reference in new issue
Block a user