Add AddWash controls and sensors for washers
This commit is contained in:
@@ -16,7 +16,7 @@ array.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||||
from .laundry import (
|
from .laundry import (
|
||||||
bool_option_exists,
|
bool_option_exists,
|
||||||
bool_option_switch,
|
bool_option_switch,
|
||||||
@@ -264,6 +264,90 @@ def _bool_option_switch(key, icon, prefix, availability_field):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# AddWash -- the little door for adding a forgotten sock mid-cycle -- rides
|
||||||
|
# three independent tokens on the same options[] array:
|
||||||
|
#
|
||||||
|
# AddWashSet_<0-7> the alarm setting, and the only writable one:
|
||||||
|
# a 3-bit mask over the moments it fires, bit 0
|
||||||
|
# rinse, bit 1 final rinse, bit 2 spin.
|
||||||
|
# AddWashAvailable_<0-7> the same three bits, but what the running
|
||||||
|
# course still permits.
|
||||||
|
# AddWashIndicator_On/Off the panel lamp: laundry may go in right now.
|
||||||
|
#
|
||||||
|
# Bit order confirmed by watching a WW6500 run a cycle: AddWashAvailable
|
||||||
|
# shed one bit as each moment passed (7 through Rinse, then 6, 4, and 0 as
|
||||||
|
# Spin began) and reset to 7 at the end, while the lamp tracked the phase
|
||||||
|
# with the alarm switched off throughout.
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wash_mask(rep, prefix):
|
||||||
|
"""One of the 3-bit AddWash masks, or None when its token is absent,
|
||||||
|
malformed, or outside 0-7. Never 0 for a missing token: 0 is a real
|
||||||
|
value, and a mask this model can't represent is a wrong model rather
|
||||||
|
than something to write back."""
|
||||||
|
raw = option_value(rep.get("x.com.samsung.da.options"), prefix)
|
||||||
|
try:
|
||||||
|
mask = int(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
return mask if 0 <= mask <= 0b111 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wash_any(prefix):
|
||||||
|
"""Whether any of the three moments is set in `prefix`'s mask."""
|
||||||
|
|
||||||
|
def read(rep):
|
||||||
|
mask = _add_wash_mask(rep, prefix)
|
||||||
|
return None if mask is None else mask != 0
|
||||||
|
|
||||||
|
return read
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wash_set_write(mask):
|
||||||
|
return ["course", "vs", "0"], {
|
||||||
|
"x.com.samsung.da.options": option_write("AddWashSet", str(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"):
|
||||||
|
return None
|
||||||
|
return _add_wash_set_write(0b111 if p == "On" else 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wash_bit_switch(key, icon, bit):
|
||||||
|
"""One moment the alarm fires at, as its own bit of the mask.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read(rep):
|
||||||
|
mask = _add_wash_mask(rep, "AddWashSet")
|
||||||
|
return None if mask is None else bool(mask >> bit & 1)
|
||||||
|
|
||||||
|
def write(p, rep, href=None):
|
||||||
|
mask = _add_wash_mask(rep, "AddWashSet")
|
||||||
|
if p not in ("On", "Off") or mask is None:
|
||||||
|
return None
|
||||||
|
return _add_wash_set_write(mask | 1 << bit if p == "On" else mask & ~(1 << bit))
|
||||||
|
|
||||||
|
return SwitchDesc(
|
||||||
|
key=key,
|
||||||
|
icon=icon,
|
||||||
|
entity_category="config",
|
||||||
|
exists_fn=bool_option_exists("AddWashSet"),
|
||||||
|
rep_fn=read,
|
||||||
|
write_fn=write,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_wash_indicator(rep):
|
||||||
|
raw = option_value(rep.get("x.com.samsung.da.options"), "AddWashIndicator")
|
||||||
|
return raw.lower() == "on" if isinstance(raw, str) else None
|
||||||
|
|
||||||
|
|
||||||
WASHER_COURSE = Capability(
|
WASHER_COURSE = Capability(
|
||||||
href="/course/vs/0",
|
href="/course/vs/0",
|
||||||
entities=(
|
entities=(
|
||||||
@@ -350,5 +434,29 @@ WASHER_COURSE = Capability(
|
|||||||
_bool_option_switch(
|
_bool_option_switch(
|
||||||
"intensive", "mdi:washing-machine", "IntensiveSetting", "IntensiveAvailableSet"
|
"intensive", "mdi:washing-machine", "IntensiveSetting", "IntensiveAvailableSet"
|
||||||
),
|
),
|
||||||
|
SwitchDesc(
|
||||||
|
key="add_wash_alarm",
|
||||||
|
icon="mdi:bell-ring",
|
||||||
|
entity_category="config",
|
||||||
|
exists_fn=bool_option_exists("AddWashSet"),
|
||||||
|
rep_fn=_add_wash_any("AddWashSet"),
|
||||||
|
write_fn=_add_wash_alarm_write,
|
||||||
|
),
|
||||||
|
_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),
|
||||||
|
BinarySensorDesc(
|
||||||
|
key="add_wash_available",
|
||||||
|
icon="mdi:tshirt-crew-outline",
|
||||||
|
entity_category="diagnostic",
|
||||||
|
exists_fn=bool_option_exists("AddWashAvailable"),
|
||||||
|
rep_fn=_add_wash_any("AddWashAvailable"),
|
||||||
|
),
|
||||||
|
BinarySensorDesc(
|
||||||
|
key="add_wash_indicator",
|
||||||
|
icon="mdi:door-open",
|
||||||
|
exists_fn=bool_option_exists("AddWashIndicator"),
|
||||||
|
rep_fn=_add_wash_indicator,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Málo aviváže"
|
"name": "Málo aviváže"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash povoleno"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash připraveno"
|
||||||
|
},
|
||||||
"stick_ble_connected": {
|
"stick_ble_connected": {
|
||||||
"name": "Tyč připojena přes BLE"
|
"name": "Tyč připojena přes BLE"
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1357,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intenzivní"
|
"name": "Intenzivní"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "Alarm AddWash"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash při máchání"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash při posledním máchání"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash při odstřeďování"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lampa"
|
"name": "Lampa"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -103,6 +103,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Weichspüler niedrig"
|
"name": "Weichspüler niedrig"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash erlaubt"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash bereit"
|
||||||
|
},
|
||||||
"pouring": {
|
"pouring": {
|
||||||
"name": "Ausgabe läuft"
|
"name": "Ausgabe läuft"
|
||||||
},
|
},
|
||||||
@@ -1345,6 +1351,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intensiv"
|
"name": "Intensiv"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "AddWash-Hinweis"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash Spülen"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash letztes Spülen"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash Schleudern"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lampe"
|
"name": "Lampe"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Softener low"
|
"name": "Softener low"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash allowed"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash ready"
|
||||||
|
},
|
||||||
"stick_ble_connected": {
|
"stick_ble_connected": {
|
||||||
"name": "Stick BLE connected"
|
"name": "Stick BLE connected"
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1357,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intensive"
|
"name": "Intensive"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "AddWash alarm"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash rinse"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash final rinse"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash spin"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lamp"
|
"name": "Lamp"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -303,6 +303,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Poco suavizante"
|
"name": "Poco suavizante"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash permitido"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash disponible"
|
||||||
|
},
|
||||||
"auto_clean_running": {
|
"auto_clean_running": {
|
||||||
"name": "Limpieza automática en curso"
|
"name": "Limpieza automática en curso"
|
||||||
},
|
},
|
||||||
@@ -1548,6 +1554,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intensivo"
|
"name": "Intensivo"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "Aviso AddWash"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash en aclarado"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash en último aclarado"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash en centrifugado"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lámpara"
|
"name": "Lámpara"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Aggiungi ammorbidente"
|
"name": "Aggiungi ammorbidente"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash consentito"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash disponibile"
|
||||||
|
},
|
||||||
"stick_ble_connected": {
|
"stick_ble_connected": {
|
||||||
"name": "Scopa elettrica connessa via BLE"
|
"name": "Scopa elettrica connessa via BLE"
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1357,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intensivo"
|
"name": "Intensivo"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "Avviso AddWash"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash al risciacquo"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash all'ultimo risciacquo"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash alla centrifuga"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lampada"
|
"name": "Lampada"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "섬유유연제 부족"
|
"name": "섬유유연제 부족"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "애드워시 허용"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "애드워시 가능"
|
||||||
|
},
|
||||||
"stick_ble_connected": {
|
"stick_ble_connected": {
|
||||||
"name": "스틱 BLE 연결됨"
|
"name": "스틱 BLE 연결됨"
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1357,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "강력"
|
"name": "강력"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "애드워시 알림"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "애드워시 헹굼"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "애드워시 마지막 헹굼"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "애드워시 탈수"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "램프"
|
"name": "램프"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
"softener_low": {
|
"softener_low": {
|
||||||
"name": "Wasverzachter bijna op"
|
"name": "Wasverzachter bijna op"
|
||||||
},
|
},
|
||||||
|
"add_wash_available": {
|
||||||
|
"name": "AddWash toegestaan"
|
||||||
|
},
|
||||||
|
"add_wash_indicator": {
|
||||||
|
"name": "AddWash beschikbaar"
|
||||||
|
},
|
||||||
"stick_ble_connected": {
|
"stick_ble_connected": {
|
||||||
"name": "Steel via BLE verbonden"
|
"name": "Steel via BLE verbonden"
|
||||||
}
|
}
|
||||||
@@ -1351,6 +1357,18 @@
|
|||||||
"intensive": {
|
"intensive": {
|
||||||
"name": "Intensief"
|
"name": "Intensief"
|
||||||
},
|
},
|
||||||
|
"add_wash_alarm": {
|
||||||
|
"name": "AddWash-melding"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_rinse": {
|
||||||
|
"name": "AddWash bij spoelen"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_final_rinse": {
|
||||||
|
"name": "AddWash laatste spoeling"
|
||||||
|
},
|
||||||
|
"add_wash_alarm_spin": {
|
||||||
|
"name": "AddWash centrifugeren"
|
||||||
|
},
|
||||||
"lamp": {
|
"lamp": {
|
||||||
"name": "Lamp"
|
"name": "Lamp"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"state_keys": [
|
"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",
|
"alarm_code",
|
||||||
"bubble_soak",
|
"bubble_soak",
|
||||||
"child_lock",
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -389,6 +389,22 @@ 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."""
|
||||||
|
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():
|
def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
||||||
"""ARTIK051_REF_17K's Cool Select Zone pantry compartment
|
"""ARTIK051_REF_17K's Cool Select Zone pantry compartment
|
||||||
(/status/pantry/one/vs/0) -- issue #20."""
|
(/status/pantry/one/vs/0) -- issue #20."""
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"""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 _flatten(fixture):
|
||||||
|
resources = _load_device(fixture)
|
||||||
|
reg = resolve(resources)
|
||||||
|
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"]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == key)
|
||||||
|
assert desc.exists_fn(_rep("Course_5C"), {}) is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("key,token", ENTITY_TOKENS.items())
|
||||||
|
def test_present_once_the_token_appears(self, key, token):
|
||||||
|
desc = next(e for e in washer.WASHER_COURSE.entities if e.key == key)
|
||||||
|
assert desc.exists_fn(_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 e.exists_fn(_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_no_unbound_hrefs(self):
|
||||||
|
resources = _load_device("washer_ww6500")
|
||||||
|
reg = resolve(resources)
|
||||||
|
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