Normalize appliance enums for HA translations
Translates status values the integration previously surfaced as raw
Samsung strings: cycle progress ('Rinse' -> "Rinsing"), diagnosis state,
buzzer volume options, and the drum-clean counter's unit. Progress and
diagnosis become enum sensors so Home Assistant looks their state up in
the catalog; progress keys come from lowercasing the device's own value
rather than a hardcoded map, so adding a language is a catalog-only
change (PR #341 review).
Rebased onto main, which has since gained the issue #345 sticky hold, and
fixed up for two problems that combination exposes:
Home Assistant refuses an enum state that isn't in the sensor's options,
which takes the entity out rather than degrading it. The sticky hold froze
progress at the device's raw 'Finish' while rep_fn had been normalized to
'finish', so every completed cycle -- the exact path #345 exists to serve
-- would have produced a rejected state.
Separately, options built from the catalog can only ever list values we
have a translation for, while this registry's rule is that an unrecognized
device value renders raw. Every progress token the shipped fixtures
advertise is covered today, but Samsung ships more devices than we have
dumps for, so the sensor platform now admits the live value into its own
options: known values translate, unknown ones display untranslated instead
of breaking the entity.
The drum-clean unit moves from a native unit to the catalog because Home
Assistant rejects an entity declaring both. Note it resolves against the
default language, so the localized unit strings are inert -- kept only
because every catalog must mirror English key for key.
Also moves the diagnosis normalizer to common.py, so dryer.py doesn't
import a private symbol from dishwasher.py to get it.
This commit is contained in:
@@ -249,6 +249,14 @@ def _power_sensor_exists(rep, resources):
|
|||||||
return not model_allows_power_on_off(resources)
|
return not model_allows_power_on_off(resources)
|
||||||
|
|
||||||
|
|
||||||
|
def diagnosis_status(value):
|
||||||
|
"""'Ready' -> the catalog's 'ready'; anything else is left raw.
|
||||||
|
|
||||||
|
Shared by dishwasher and dryer, which report the same field.
|
||||||
|
"""
|
||||||
|
return "ready" if value == "Ready" else value
|
||||||
|
|
||||||
|
|
||||||
def sensor_item_value(items, sensor_type, index=0):
|
def sensor_item_value(items, sensor_type, index=0):
|
||||||
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
|
"""Pull one reading out of a `/sensors/vs/0`-style items[] list -- each
|
||||||
item is `{type, value: [...]}`; `index` picks which slot to read
|
item is `{type, value: [...]}`; `index` picks which slot to read
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ wash, auto release dry) are read locally here.
|
|||||||
|
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
from ..entities import ButtonDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||||
|
from .common import diagnosis_status
|
||||||
from .laundry import bool_option_switch, cycle_select
|
from .laundry import bool_option_switch, cycle_select
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -76,6 +77,9 @@ DIAGNOSIS = Capability(
|
|||||||
field="x.com.samsung.da.diagnosisStart",
|
field="x.com.samsung.da.diagnosisStart",
|
||||||
icon="mdi:stethoscope",
|
icon="mdi:stethoscope",
|
||||||
entity_category="diagnostic",
|
entity_category="diagnostic",
|
||||||
|
device_class="enum",
|
||||||
|
options=("ready",),
|
||||||
|
value_fn=diagnosis_status,
|
||||||
),
|
),
|
||||||
ButtonDesc(
|
ButtonDesc(
|
||||||
key="diagnosis_start",
|
key="diagnosis_start",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ the /course/vs/0 cycle select -- lives in laundry.py.
|
|||||||
|
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import SensorDesc, SwitchDesc
|
from ..entities import SensorDesc, SwitchDesc
|
||||||
|
from .common import diagnosis_status
|
||||||
from .laundry import cycle_select, drum_clean_cycles_remaining, drum_clean_last_cleaned
|
from .laundry import cycle_select, drum_clean_cycles_remaining, drum_clean_last_cleaned
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +70,6 @@ DRYER_COURSE = Capability(
|
|||||||
),
|
),
|
||||||
SensorDesc(
|
SensorDesc(
|
||||||
key="drum_clean_cycles_remaining",
|
key="drum_clean_cycles_remaining",
|
||||||
unit="cycles",
|
|
||||||
icon="mdi:tumble-dryer-alert",
|
icon="mdi:tumble-dryer-alert",
|
||||||
state_class="measurement",
|
state_class="measurement",
|
||||||
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
|
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
|
||||||
@@ -91,7 +91,12 @@ DRYER_DIAGNOSIS = Capability(
|
|||||||
poll_tier="warm",
|
poll_tier="warm",
|
||||||
entities=(
|
entities=(
|
||||||
SensorDesc(
|
SensorDesc(
|
||||||
key="diagnosis", field="x.com.samsung.da.diagnosisStart", entity_category="diagnostic"
|
key="diagnosis",
|
||||||
|
field="x.com.samsung.da.diagnosisStart",
|
||||||
|
entity_category="diagnostic",
|
||||||
|
device_class="enum",
|
||||||
|
options=("ready",),
|
||||||
|
value_fn=diagnosis_status,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Shared by dryer/dishwasher/oven/washer families.
|
|||||||
import math
|
import math
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
from ...catalog import translated_states
|
||||||
from ..capability import Capability
|
from ..capability import Capability
|
||||||
from ..entities import BinarySensorDesc, ButtonDesc, NumberDesc, SensorDesc
|
from ..entities import BinarySensorDesc, ButtonDesc, NumberDesc, SensorDesc
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ def _to_ocf(v):
|
|||||||
|
|
||||||
|
|
||||||
def _progress(v):
|
def _progress(v):
|
||||||
return "Idle" if v in (None, "None") else v
|
return "idle" if v in (None, "None") else str(v).lower()
|
||||||
|
|
||||||
|
|
||||||
def _int(v):
|
def _int(v):
|
||||||
@@ -201,13 +202,17 @@ OPERATIONAL_STATE = Capability(
|
|||||||
SensorDesc(
|
SensorDesc(
|
||||||
key="progress",
|
key="progress",
|
||||||
icon="mdi:progress-wrench",
|
icon="mdi:progress-wrench",
|
||||||
|
device_class="enum",
|
||||||
|
options=tuple(sorted(translated_states("sensor", "progress"))),
|
||||||
rep_fn=lambda rep: (
|
rep_fn=lambda rep: (
|
||||||
"Idle"
|
"idle"
|
||||||
if not _state_is_active(rep)
|
if not _state_is_active(rep)
|
||||||
else _progress(rep.get("x.com.samsung.da.progress"))
|
else _progress(rep.get("x.com.samsung.da.progress"))
|
||||||
),
|
),
|
||||||
sticky_fn=_just_finished,
|
sticky_fn=_just_finished,
|
||||||
sticky_value_fn=lambda rep: "Finish",
|
# The catalog key, not the device's 'Finish': rep_fn is normalized
|
||||||
|
# now, and a held value outside `options` is what HA rejects.
|
||||||
|
sticky_value_fn=lambda rep: "finish",
|
||||||
sticky_bypass_fn=_new_cycle_running,
|
sticky_bypass_fn=_new_cycle_running,
|
||||||
),
|
),
|
||||||
SensorDesc(
|
SensorDesc(
|
||||||
|
|||||||
@@ -275,7 +275,6 @@ WASHER_COURSE = Capability(
|
|||||||
),
|
),
|
||||||
SensorDesc(
|
SensorDesc(
|
||||||
key="drum_clean_cycles_remaining",
|
key="drum_clean_cycles_remaining",
|
||||||
unit="cycles",
|
|
||||||
icon="mdi:washing-machine-alert",
|
icon="mdi:washing-machine-alert",
|
||||||
state_class="measurement",
|
state_class="measurement",
|
||||||
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
|
exists_fn=lambda rep, resources: drum_clean_cycles_remaining(rep) is not None,
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ class LocalThingsSensor(LocalThingsEntity, SensorEntity):
|
|||||||
SensorDeviceClass(desc.device_class) if desc.device_class else None
|
SensorDeviceClass(desc.device_class) if desc.device_class else None
|
||||||
)
|
)
|
||||||
self._attr_state_class = SensorStateClass(desc.state_class) if desc.state_class else None
|
self._attr_state_class = SensorStateClass(desc.state_class) if desc.state_class else None
|
||||||
if desc.options:
|
# Always set, so the `options` property below can read it unguarded.
|
||||||
self._attr_options = list(desc.options)
|
self._attr_options = list(desc.options) if desc.options else None
|
||||||
self._hysteresis_value = None
|
self._hysteresis_value = None
|
||||||
self._sticky_value = None
|
self._sticky_value = None
|
||||||
self._sticky_until: float | None = None
|
self._sticky_until: float | None = None
|
||||||
@@ -63,6 +63,23 @@ class LocalThingsSensor(LocalThingsEntity, SensorEntity):
|
|||||||
return desc.unit_fn(self.coordinator.resource(self._bound.href))
|
return desc.unit_fn(self.coordinator.resource(self._bound.href))
|
||||||
return self._attr_native_unit_of_measurement
|
return self._attr_native_unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def options(self):
|
||||||
|
"""Declared options, plus whatever this device is actually reporting.
|
||||||
|
|
||||||
|
HA raises for an enum state outside `options`, which would turn any
|
||||||
|
device value we don't have a translation for into a broken entity --
|
||||||
|
the opposite of this registry's rule that an unrecognized value
|
||||||
|
renders raw. Admitting the live value keeps it displayable; it just
|
||||||
|
shows untranslated (PR #341 review).
|
||||||
|
"""
|
||||||
|
if self._attr_options is None:
|
||||||
|
return None
|
||||||
|
value = self.native_value
|
||||||
|
if not isinstance(value, str) or value in self._attr_options:
|
||||||
|
return self._attr_options
|
||||||
|
return [*self._attr_options, value]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self):
|
||||||
raw = (self.coordinator.data or {}).get(self._state_key)
|
raw = (self.coordinator.data or {}).get(self._state_key)
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "Zvuk bzučáku",
|
"name": "Zvuk bzučáku",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Vypnuto",
|
"off": "Vypnuto",
|
||||||
"on": "Zapnuto"
|
"on": "Zapnuto",
|
||||||
|
"volume_off": "Vypnuto",
|
||||||
|
"volume_low": "Nízká",
|
||||||
|
"volume_med": "Střední",
|
||||||
|
"volume_high": "Vysoká"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -930,13 +934,20 @@
|
|||||||
"name": "Teplota"
|
"name": "Teplota"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnostika"
|
"name": "Diagnostika",
|
||||||
|
"state": {
|
||||||
|
"ready": "Připraveno"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Stav diagnostiky"
|
"name": "Stav diagnostiky",
|
||||||
|
"state": {
|
||||||
|
"ready": "Připraveno"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Čištění bubnu za"
|
"name": "Čištění bubnu za",
|
||||||
|
"unit_of_measurement": "cyklů"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Poslední čištění bubnu"
|
"name": "Poslední čištění bubnu"
|
||||||
@@ -1077,7 +1088,21 @@
|
|||||||
"name": "Teplota sondy"
|
"name": "Teplota sondy"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Průběh"
|
"name": "Průběh",
|
||||||
|
"state": {
|
||||||
|
"idle": "Nečinný",
|
||||||
|
"weightsensing": "Detekce náplně",
|
||||||
|
"wash": "Praní",
|
||||||
|
"rinse": "Máchání",
|
||||||
|
"spin": "Odstřeďování",
|
||||||
|
"finish": "Dokončeno",
|
||||||
|
"steaming": "Napařování",
|
||||||
|
"airwashing": "Osvěžení vzduchem",
|
||||||
|
"drying": "Sušení",
|
||||||
|
"cooling": "Chlazení",
|
||||||
|
"predrain": "Vypouštění",
|
||||||
|
"prewash": "Předpírka"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Průběh v procentech"
|
"name": "Průběh v procentech"
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "Signalton",
|
"name": "Signalton",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Aus",
|
"off": "Aus",
|
||||||
"on": "Ein"
|
"on": "Ein",
|
||||||
|
"volume_off": "Aus",
|
||||||
|
"volume_low": "Niedrig",
|
||||||
|
"volume_med": "Mittel",
|
||||||
|
"volume_high": "Hoch"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -924,13 +928,20 @@
|
|||||||
"name": "Temperatur"
|
"name": "Temperatur"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnose"
|
"name": "Diagnose",
|
||||||
|
"state": {
|
||||||
|
"ready": "Bereit"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Diagnosestatus"
|
"name": "Diagnosestatus",
|
||||||
|
"state": {
|
||||||
|
"ready": "Bereit"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Trommelreinigung fällig in"
|
"name": "Trommelreinigung fällig in",
|
||||||
|
"unit_of_measurement": "Zyklen"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Trommel zuletzt gereinigt"
|
"name": "Trommel zuletzt gereinigt"
|
||||||
@@ -1071,7 +1082,21 @@
|
|||||||
"name": "Fühlertemperatur"
|
"name": "Fühlertemperatur"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Fortschritt"
|
"name": "Fortschritt",
|
||||||
|
"state": {
|
||||||
|
"idle": "Leerlauf",
|
||||||
|
"weightsensing": "Beladungserkennung",
|
||||||
|
"wash": "Waschen",
|
||||||
|
"rinse": "Spülen",
|
||||||
|
"spin": "Schleudern",
|
||||||
|
"finish": "Fertig",
|
||||||
|
"steaming": "Dämpfen",
|
||||||
|
"airwashing": "Luftreinigung",
|
||||||
|
"drying": "Trocknen",
|
||||||
|
"cooling": "Abkühlen",
|
||||||
|
"predrain": "Abpumpen",
|
||||||
|
"prewash": "Vorwäsche"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Fortschritt in Prozent"
|
"name": "Fortschritt in Prozent"
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "Buzzer sound",
|
"name": "Buzzer sound",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"on": "On"
|
"on": "On",
|
||||||
|
"volume_off": "Off",
|
||||||
|
"volume_low": "Low",
|
||||||
|
"volume_med": "Medium",
|
||||||
|
"volume_high": "High"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -930,13 +934,20 @@
|
|||||||
"name": "Temperature"
|
"name": "Temperature"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnosis"
|
"name": "Diagnosis",
|
||||||
|
"state": {
|
||||||
|
"ready": "Ready"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Diagnosis status"
|
"name": "Diagnosis status",
|
||||||
|
"state": {
|
||||||
|
"ready": "Ready"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Drum clean due in"
|
"name": "Drum clean due in",
|
||||||
|
"unit_of_measurement": "cycles"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Drum last cleaned"
|
"name": "Drum last cleaned"
|
||||||
@@ -1077,7 +1088,21 @@
|
|||||||
"name": "Probe temperature"
|
"name": "Probe temperature"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Progress"
|
"name": "Progress",
|
||||||
|
"state": {
|
||||||
|
"idle": "Idle",
|
||||||
|
"weightsensing": "Weight sensing",
|
||||||
|
"wash": "Washing",
|
||||||
|
"rinse": "Rinsing",
|
||||||
|
"spin": "Spinning",
|
||||||
|
"finish": "Finished",
|
||||||
|
"steaming": "Steaming",
|
||||||
|
"airwashing": "Air washing",
|
||||||
|
"drying": "Drying",
|
||||||
|
"cooling": "Cooling",
|
||||||
|
"predrain": "Pre-drain",
|
||||||
|
"prewash": "Pre-wash"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Progress percent"
|
"name": "Progress percent"
|
||||||
|
|||||||
@@ -457,7 +457,11 @@
|
|||||||
"name": "Volumen",
|
"name": "Volumen",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Apagado",
|
"off": "Apagado",
|
||||||
"on": "Encendido"
|
"on": "Encendido",
|
||||||
|
"volume_off": "Apagado",
|
||||||
|
"volume_low": "Bajo",
|
||||||
|
"volume_med": "Medio",
|
||||||
|
"volume_high": "Alto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -1123,13 +1127,20 @@
|
|||||||
"name": "Temperatura"
|
"name": "Temperatura"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnóstico"
|
"name": "Diagnóstico",
|
||||||
|
"state": {
|
||||||
|
"ready": "Listo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Estado del diagnóstico"
|
"name": "Estado del diagnóstico",
|
||||||
|
"state": {
|
||||||
|
"ready": "Listo"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Limpieza de tambor en"
|
"name": "Limpieza de tambor en",
|
||||||
|
"unit_of_measurement": "ciclos"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Última limpieza del tambor"
|
"name": "Última limpieza del tambor"
|
||||||
@@ -1270,7 +1281,21 @@
|
|||||||
"name": "Temperatura de la sonda"
|
"name": "Temperatura de la sonda"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Progreso"
|
"name": "Progreso",
|
||||||
|
"state": {
|
||||||
|
"idle": "Inactiva",
|
||||||
|
"weightsensing": "Detección de carga",
|
||||||
|
"wash": "Lavado",
|
||||||
|
"rinse": "Aclarado",
|
||||||
|
"spin": "Centrifugado",
|
||||||
|
"finish": "Finalizado",
|
||||||
|
"steaming": "Vaporización",
|
||||||
|
"airwashing": "Lavado con aire",
|
||||||
|
"drying": "Secado",
|
||||||
|
"cooling": "Enfriamiento",
|
||||||
|
"predrain": "Drenaje previo",
|
||||||
|
"prewash": "Prelavado"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Porcentaje de progreso"
|
"name": "Porcentaje de progreso"
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "Suono cicalino",
|
"name": "Suono cicalino",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Spento",
|
"off": "Spento",
|
||||||
"on": "Acceso"
|
"on": "Acceso",
|
||||||
|
"volume_off": "Spento",
|
||||||
|
"volume_low": "Basso",
|
||||||
|
"volume_med": "Medio",
|
||||||
|
"volume_high": "Alto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -930,13 +934,20 @@
|
|||||||
"name": "Temperatura"
|
"name": "Temperatura"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnosi"
|
"name": "Diagnosi",
|
||||||
|
"state": {
|
||||||
|
"ready": "Pronto"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Stato diagnosi"
|
"name": "Stato diagnosi",
|
||||||
|
"state": {
|
||||||
|
"ready": "Pronto"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Pulizia cestello fra"
|
"name": "Pulizia cestello fra",
|
||||||
|
"unit_of_measurement": "cicli"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Ultima pulizia cestello"
|
"name": "Ultima pulizia cestello"
|
||||||
@@ -1077,7 +1088,21 @@
|
|||||||
"name": "Temperatura sonda"
|
"name": "Temperatura sonda"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Avanzamento"
|
"name": "Avanzamento",
|
||||||
|
"state": {
|
||||||
|
"idle": "Inattivo",
|
||||||
|
"weightsensing": "Rilevamento del carico",
|
||||||
|
"wash": "Lavaggio",
|
||||||
|
"rinse": "Risciacquo",
|
||||||
|
"spin": "Centrifuga",
|
||||||
|
"finish": "Completato",
|
||||||
|
"steaming": "Vapore",
|
||||||
|
"airwashing": "Lavaggio ad aria",
|
||||||
|
"drying": "Asciugatura",
|
||||||
|
"cooling": "Raffreddamento",
|
||||||
|
"predrain": "Scarico preliminare",
|
||||||
|
"prewash": "Prelavaggio"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Avanzamento percentuale"
|
"name": "Avanzamento percentuale"
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "부저음",
|
"name": "부저음",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "끄기",
|
"off": "끄기",
|
||||||
"on": "켜기"
|
"on": "켜기",
|
||||||
|
"volume_off": "끔",
|
||||||
|
"volume_low": "낮음",
|
||||||
|
"volume_med": "중간",
|
||||||
|
"volume_high": "높음"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -930,13 +934,20 @@
|
|||||||
"name": "온도"
|
"name": "온도"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "진단"
|
"name": "진단",
|
||||||
|
"state": {
|
||||||
|
"ready": "준비됨"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "진단 상태"
|
"name": "진단 상태",
|
||||||
|
"state": {
|
||||||
|
"ready": "준비됨"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "통세척까지 남은 횟수"
|
"name": "통세척까지 남은 횟수",
|
||||||
|
"unit_of_measurement": "회"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "마지막 통세척"
|
"name": "마지막 통세척"
|
||||||
@@ -1077,7 +1088,21 @@
|
|||||||
"name": "탐침 온도계 현재 온도"
|
"name": "탐침 온도계 현재 온도"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "진행률"
|
"name": "진행률",
|
||||||
|
"state": {
|
||||||
|
"idle": "대기",
|
||||||
|
"weightsensing": "세탁물 감지",
|
||||||
|
"wash": "세탁",
|
||||||
|
"rinse": "헹굼",
|
||||||
|
"spin": "탈수",
|
||||||
|
"finish": "완료",
|
||||||
|
"steaming": "스팀",
|
||||||
|
"airwashing": "에어워시",
|
||||||
|
"drying": "건조",
|
||||||
|
"cooling": "냉각",
|
||||||
|
"predrain": "사전 배수",
|
||||||
|
"prewash": "애벌빨래"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "진행률"
|
"name": "진행률"
|
||||||
|
|||||||
@@ -261,7 +261,11 @@
|
|||||||
"name": "Zoemergeluid",
|
"name": "Zoemergeluid",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "Uit",
|
"off": "Uit",
|
||||||
"on": "Aan"
|
"on": "Aan",
|
||||||
|
"volume_off": "Uit",
|
||||||
|
"volume_low": "Laag",
|
||||||
|
"volume_med": "Gemiddeld",
|
||||||
|
"volume_high": "Hoog"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"discharging_time": {
|
"discharging_time": {
|
||||||
@@ -930,13 +934,20 @@
|
|||||||
"name": "Temperatuur"
|
"name": "Temperatuur"
|
||||||
},
|
},
|
||||||
"diagnosis": {
|
"diagnosis": {
|
||||||
"name": "Diagnose"
|
"name": "Diagnose",
|
||||||
|
"state": {
|
||||||
|
"ready": "Gereed"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"diagnosis_status": {
|
"diagnosis_status": {
|
||||||
"name": "Diagnosestatus"
|
"name": "Diagnosestatus",
|
||||||
|
"state": {
|
||||||
|
"ready": "Gereed"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"drum_clean_cycles_remaining": {
|
"drum_clean_cycles_remaining": {
|
||||||
"name": "Trommelreiniging over"
|
"name": "Trommelreiniging over",
|
||||||
|
"unit_of_measurement": "cycli"
|
||||||
},
|
},
|
||||||
"drum_clean_last_cleaned": {
|
"drum_clean_last_cleaned": {
|
||||||
"name": "Trommel laatst gereinigd"
|
"name": "Trommel laatst gereinigd"
|
||||||
@@ -1077,7 +1088,21 @@
|
|||||||
"name": "Sondetemperatuur"
|
"name": "Sondetemperatuur"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"name": "Voortgang"
|
"name": "Voortgang",
|
||||||
|
"state": {
|
||||||
|
"idle": "Inactief",
|
||||||
|
"weightsensing": "Beladingsdetectie",
|
||||||
|
"wash": "Wassen",
|
||||||
|
"rinse": "Spoelen",
|
||||||
|
"spin": "Centrifugeren",
|
||||||
|
"finish": "Voltooid",
|
||||||
|
"steaming": "Stomen",
|
||||||
|
"airwashing": "Luchtreiniging",
|
||||||
|
"drying": "Drogen",
|
||||||
|
"cooling": "Koelen",
|
||||||
|
"predrain": "Vooraf afpompen",
|
||||||
|
"prewash": "Voorwas"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"progress_percentage": {
|
"progress_percentage": {
|
||||||
"name": "Voortgangspercentage"
|
"name": "Voortgangspercentage"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ check the dishwasher wiring and its device-specific options.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from custom_components.localthings.registry.capabilities import dishwasher
|
from custom_components.localthings.registry.capabilities import dishwasher
|
||||||
from custom_components.localthings.registry.entities import SwitchDesc
|
from custom_components.localthings.registry.entities import SensorDesc, SwitchDesc
|
||||||
|
|
||||||
|
|
||||||
class TestCycleOptions:
|
class TestCycleOptions:
|
||||||
@@ -60,3 +60,14 @@ class TestDishwasherOptions:
|
|||||||
assert desc.exists_fn is not None
|
assert desc.exists_fn is not None
|
||||||
assert desc.exists_fn({"x.com.samsung.da.options": []}, {}) is False
|
assert desc.exists_fn({"x.com.samsung.da.options": []}, {}) is False
|
||||||
assert desc.exists_fn({"x.com.samsung.da.options": ["AutoDoorRelease_On"]}, {}) is True
|
assert desc.exists_fn({"x.com.samsung.da.options": ["AutoDoorRelease_On"]}, {}) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagnosis_status_is_a_translatable_enum():
|
||||||
|
desc = next(
|
||||||
|
e
|
||||||
|
for e in dishwasher.DIAGNOSIS.entities
|
||||||
|
if e.key == "diagnosis_status" and isinstance(e, SensorDesc)
|
||||||
|
)
|
||||||
|
assert desc.device_class == "enum"
|
||||||
|
assert desc.options == ("ready",)
|
||||||
|
assert desc.value_fn("Ready") == "ready"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from custom_components.localthings.registry.capabilities.operational import (
|
|||||||
_just_finished,
|
_just_finished,
|
||||||
_new_cycle_running,
|
_new_cycle_running,
|
||||||
)
|
)
|
||||||
from custom_components.localthings.registry.entities import NumberDesc
|
from custom_components.localthings.registry.entities import NumberDesc, SensorDesc
|
||||||
|
|
||||||
|
|
||||||
def test_machine_state_maps_samsung_to_ocf():
|
def test_machine_state_maps_samsung_to_ocf():
|
||||||
@@ -83,6 +83,21 @@ class TestNewCycleRunning:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_progress_is_a_translatable_enum():
|
||||||
|
desc = next(
|
||||||
|
e for e in OPERATIONAL_STATE.entities if e.key == "progress" and isinstance(e, SensorDesc)
|
||||||
|
)
|
||||||
|
assert desc.device_class == "enum"
|
||||||
|
assert desc.options is not None
|
||||||
|
assert "rinse" in desc.options
|
||||||
|
assert "Rinse" not in desc.options
|
||||||
|
assert desc.rep_fn is not None
|
||||||
|
assert (
|
||||||
|
desc.rep_fn({"x.com.samsung.da.state": "Run", "x.com.samsung.da.progress": "Rinse"})
|
||||||
|
== "rinse"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestProgressPercentage:
|
class TestProgressPercentage:
|
||||||
"""issue #9: device firmware leaves progressPercentage stale (e.g. '1')
|
"""issue #9: device firmware leaves progressPercentage stale (e.g. '1')
|
||||||
after a cycle ends instead of resetting it, so it must be gated on
|
after a cycle ends instead of resetting it, so it must be gated on
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import ClassVar, cast
|
|||||||
|
|
||||||
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
||||||
from custom_components.localthings.registry.capabilities.laundry import (
|
from custom_components.localthings.registry.capabilities.laundry import (
|
||||||
|
BUZZER_SOUND,
|
||||||
cycle_select,
|
cycle_select,
|
||||||
washer_cycle_fallback,
|
washer_cycle_fallback,
|
||||||
)
|
)
|
||||||
@@ -47,6 +48,25 @@ def test_options_field_unaffected():
|
|||||||
assert entity.options == ["Lo", "Hi"]
|
assert entity.options == ["Lo", "Hi"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_buzzer_volume_options_normalize_to_translation_keys():
|
||||||
|
desc = next(e for e in BUZZER_SOUND.entities if e.key == "buzzer_sound")
|
||||||
|
entity = _make_select(
|
||||||
|
desc,
|
||||||
|
"/buzzersound/vs/0",
|
||||||
|
{
|
||||||
|
"/buzzersound/vs/0": {
|
||||||
|
"supportedBuzzerSound": [
|
||||||
|
"Volume_Off",
|
||||||
|
"Volume_Low",
|
||||||
|
"Volume_Med",
|
||||||
|
"Volume_High",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert entity.options == ["volume_off", "volume_low", "volume_med", "volume_high"]
|
||||||
|
|
||||||
|
|
||||||
def test_callable_options_receives_full_resource_snapshot():
|
def test_callable_options_receives_full_resource_snapshot():
|
||||||
"""A callable options is handed the coordinator's full href->rep
|
"""A callable options is handed the coordinator's full href->rep
|
||||||
snapshot, not just this entity's own href's rep -- needed for course
|
snapshot, not just this entity's own href's rep -- needed for course
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""An enum sensor's reported state must always be inside its options.
|
||||||
|
|
||||||
|
Home Assistant raises for an enum sensor whose state isn't in `options`
|
||||||
|
(sensor/__init__.py: "provides state value ... which is not in the list of
|
||||||
|
options provided"), so a value outside the list isn't a cosmetic problem --
|
||||||
|
it takes the entity out.
|
||||||
|
|
||||||
|
Two ways that bites, both from PR #341 giving `progress` a `device_class`
|
||||||
|
of enum:
|
||||||
|
|
||||||
|
- the sticky hold (issue #345) froze the entity at the device's raw
|
||||||
|
'Finish' while `rep_fn` had been normalized to 'finish', so every
|
||||||
|
completed cycle -- the exact path #345 exists to serve -- produced a
|
||||||
|
state outside the options;
|
||||||
|
- any progress value not in the translation catalog. Every token the
|
||||||
|
shipped fixtures advertise is covered today, but this registry's rule is
|
||||||
|
that an unrecognized device value renders raw rather than breaking, and
|
||||||
|
Samsung ships more devices than we have dumps for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
||||||
|
from custom_components.localthings.registry.adapter import flatten
|
||||||
|
from custom_components.localthings.registry.capabilities.operational import OPERATIONAL_STATE
|
||||||
|
from custom_components.localthings.registry.discovery import BoundEntity
|
||||||
|
from custom_components.localthings.registry.entities import SensorDesc
|
||||||
|
from custom_components.localthings.sensor import LocalThingsSensor
|
||||||
|
|
||||||
|
_HREF = "/operational/state/vs/0"
|
||||||
|
_PROGRESS = next(
|
||||||
|
e for e in OPERATIONAL_STATE.entities if e.key == "progress" and isinstance(e, SensorDesc)
|
||||||
|
)
|
||||||
|
_ALL_BOUND = [
|
||||||
|
BoundEntity(href=_HREF, capability=OPERATIONAL_STATE, desc=desc)
|
||||||
|
for desc in OPERATIONAL_STATE.entities
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeConfigEntry:
|
||||||
|
def __init__(self):
|
||||||
|
self.options: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeCoordinator:
|
||||||
|
def __init__(self):
|
||||||
|
self.device_serial = "TEST-SERIAL"
|
||||||
|
self.config_entry = _FakeConfigEntry()
|
||||||
|
self.resources: dict[str, dict] = {}
|
||||||
|
|
||||||
|
def resource(self, href: str) -> dict:
|
||||||
|
return self.resources.get(href) or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> dict:
|
||||||
|
return flatten(_ALL_BOUND, self.resources)
|
||||||
|
|
||||||
|
|
||||||
|
def _sensor(desc):
|
||||||
|
coordinator = _FakeCoordinator()
|
||||||
|
bound = BoundEntity(href=_HREF, capability=OPERATIONAL_STATE, desc=desc)
|
||||||
|
return LocalThingsSensor(cast(LocalThingsCoordinator, coordinator), bound), coordinator
|
||||||
|
|
||||||
|
|
||||||
|
def _set(coordinator, **fields):
|
||||||
|
coordinator.resources[_HREF] = {f"x.com.samsung.da.{k}": v for k, v in fields.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_sticky_hold_freezes_at_a_value_inside_the_options():
|
||||||
|
"""Issue #345's grace window fires on every finished cycle, so a held
|
||||||
|
value outside the options would break the common path, not an edge."""
|
||||||
|
sensor, coordinator = _sensor(_PROGRESS)
|
||||||
|
|
||||||
|
_set(coordinator, state="Run", progress="Wash")
|
||||||
|
assert sensor.native_value == "wash"
|
||||||
|
|
||||||
|
# Cycle finishes, then the device drops out of active -- the hold engages.
|
||||||
|
_set(coordinator, state="Run", progress="Finish")
|
||||||
|
assert sensor.native_value in sensor.options
|
||||||
|
_set(coordinator, state="Ready", progress="Finish")
|
||||||
|
held = sensor.native_value
|
||||||
|
assert held == "finish"
|
||||||
|
assert held in sensor.options
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_progress_value_we_cannot_translate_still_reports():
|
||||||
|
"""An unrecognized device value renders raw rather than taking the
|
||||||
|
entity out -- the same rule the course tables follow."""
|
||||||
|
sensor, coordinator = _sensor(_PROGRESS)
|
||||||
|
|
||||||
|
_set(coordinator, state="Run", progress="SomeFutureStage")
|
||||||
|
value = sensor.native_value
|
||||||
|
assert value == "somefuturestage"
|
||||||
|
assert value in sensor.options
|
||||||
|
# ...and admitting it doesn't drop the translated ones.
|
||||||
|
assert "rinse" in sensor.options
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_values_do_not_grow_the_options_list():
|
||||||
|
assert _PROGRESS.options is not None
|
||||||
|
sensor, coordinator = _sensor(_PROGRESS)
|
||||||
|
|
||||||
|
_set(coordinator, state="Run", progress="Rinse")
|
||||||
|
assert sensor.options == list(_PROGRESS.options)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_non_enum_sensor_has_no_options():
|
||||||
|
percentage = next(e for e in OPERATIONAL_STATE.entities if e.key == "progress_percentage")
|
||||||
|
sensor, coordinator = _sensor(percentage)
|
||||||
|
|
||||||
|
_set(coordinator, state="Run", progressPercentage="40")
|
||||||
|
assert sensor.options is None
|
||||||
|
assert sensor.native_value == 40
|
||||||
+42
-42
@@ -92,10 +92,10 @@ def test_holds_finish_after_state_leaves_active():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready") # device has moved on
|
_replace(coordinator, state="Ready") # device has moved on
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
|
|
||||||
def test_holds_finish_even_when_state_already_idle_at_first_observation():
|
def test_holds_finish_even_when_state_already_idle_at_first_observation():
|
||||||
@@ -106,11 +106,11 @@ def test_holds_finish_even_when_state_already_idle_at_first_observation():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Ready", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Ready", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
# Still held on a later poll, even once the device stops repeating it.
|
# Still held on a later poll, even once the device stops repeating it.
|
||||||
_replace(coordinator, state="Ready")
|
_replace(coordinator, state="Ready")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_holds_100_regardless_of_the_raw_field_at_finish():
|
def test_progress_percentage_holds_100_regardless_of_the_raw_field_at_finish():
|
||||||
@@ -132,10 +132,10 @@ def test_real_data_flows_through_unheld_while_active():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Spin")
|
_replace(coordinator, state="Run", progress="Spin")
|
||||||
assert sensor.native_value == "Spin"
|
assert sensor.native_value == "spin"
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Rinse")
|
_replace(coordinator, state="Run", progress="Rinse")
|
||||||
assert sensor.native_value == "Rinse"
|
assert sensor.native_value == "rinse"
|
||||||
|
|
||||||
|
|
||||||
def test_never_finished_stays_idle():
|
def test_never_finished_stays_idle():
|
||||||
@@ -144,10 +144,10 @@ def test_never_finished_stays_idle():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Spin")
|
_replace(coordinator, state="Run", progress="Spin")
|
||||||
assert sensor.native_value == "Spin"
|
assert sensor.native_value == "spin"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready")
|
_replace(coordinator, state="Ready")
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
|
|
||||||
def test_a_new_cycle_starting_overrides_the_hold():
|
def test_a_new_cycle_starting_overrides_the_hold():
|
||||||
@@ -156,13 +156,13 @@ def test_a_new_cycle_starting_overrides_the_hold():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready")
|
_replace(coordinator, state="Ready")
|
||||||
assert sensor.native_value == "Finish" # still held
|
assert sensor.native_value == "finish" # still held
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Wash")
|
_replace(coordinator, state="Run", progress="Wash")
|
||||||
assert sensor.native_value == "Wash"
|
assert sensor.native_value == "wash"
|
||||||
|
|
||||||
|
|
||||||
def test_a_running_stage_after_finish_does_not_break_the_hold():
|
def test_a_running_stage_after_finish_does_not_break_the_hold():
|
||||||
@@ -177,24 +177,24 @@ def test_a_running_stage_after_finish_does_not_break_the_hold():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Drying", progressPercentage="40")
|
_replace(coordinator, state="Run", progress="Drying", progressPercentage="40")
|
||||||
assert sensor.native_value == "Drying"
|
assert sensor.native_value == "drying"
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Cooling", progressPercentage="95")
|
_replace(coordinator, state="Run", progress="Cooling", progressPercentage="95")
|
||||||
assert sensor.native_value == "Cooling"
|
assert sensor.native_value == "cooling"
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
# The tail: a running stage again, state already idle.
|
# The tail: a running stage again, state already idle.
|
||||||
_replace(coordinator, state="Ready", progress="Drying", progressPercentage="100")
|
_replace(coordinator, state="Ready", progress="Drying", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
# ...then the device settles, still inside the window.
|
# ...then the device settles, still inside the window.
|
||||||
_replace(coordinator, state="Ready", progress="None")
|
_replace(coordinator, state="Ready", progress="None")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
|
|
||||||
def test_progress_percentage_survives_the_same_tail():
|
def test_progress_percentage_survives_the_same_tail():
|
||||||
@@ -223,16 +223,16 @@ def test_a_paused_new_cycle_is_left_to_the_window_rather_than_released():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
_replace(coordinator, state="Pause", progress="Wash")
|
_replace(coordinator, state="Pause", progress="Wash")
|
||||||
assert sensor.native_value == "Finish" # held out, not released
|
assert sensor.native_value == "finish" # held out, not released
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
assert sensor.native_value == "Idle" # what a paused appliance always shows
|
assert sensor.native_value == "idle" # what a paused appliance always shows
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Wash")
|
_replace(coordinator, state="Run", progress="Wash")
|
||||||
assert sensor.native_value == "Wash"
|
assert sensor.native_value == "wash"
|
||||||
|
|
||||||
|
|
||||||
def test_a_flapping_finish_cannot_ratchet_an_open_window_forward():
|
def test_a_flapping_finish_cannot_ratchet_an_open_window_forward():
|
||||||
@@ -243,20 +243,20 @@ def test_a_flapping_finish_cannot_ratchet_an_open_window_forward():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
_replace(coordinator, state="Ready", progress="None")
|
_replace(coordinator, state="Ready", progress="None")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
# 0.15s of flapping so far -- the window still ends 0.3s after the
|
# 0.15s of flapping so far -- the window still ends 0.3s after the
|
||||||
# first Finish, not 0.3s after the most recent re-entry.
|
# first Finish, not 0.3s after the most recent re-entry.
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
|
|
||||||
def test_a_finish_after_the_window_closes_does_not_re_arm_it():
|
def test_a_finish_after_the_window_closes_does_not_re_arm_it():
|
||||||
@@ -274,23 +274,23 @@ def test_a_finish_after_the_window_closes_does_not_re_arm_it():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready", progress="None")
|
_replace(coordinator, state="Ready", progress="None")
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
# A real cycle in between is what makes it available again.
|
# A real cycle in between is what makes it available again.
|
||||||
_replace(coordinator, state="Run", progress="Drying")
|
_replace(coordinator, state="Run", progress="Drying")
|
||||||
assert sensor.native_value == "Drying"
|
assert sensor.native_value == "drying"
|
||||||
_replace(coordinator, state="Run", progress="Finish")
|
_replace(coordinator, state="Run", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
_replace(coordinator, state="Ready", progress="None")
|
_replace(coordinator, state="Ready", progress="None")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
|
|
||||||
def test_hold_expires_after_sticky_seconds():
|
def test_hold_expires_after_sticky_seconds():
|
||||||
@@ -304,13 +304,13 @@ def test_hold_expires_after_sticky_seconds():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready")
|
_replace(coordinator, state="Ready")
|
||||||
assert sensor.native_value == "Finish" # still within the window
|
assert sensor.native_value == "finish" # still within the window
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
|
|
||||||
def test_a_progress_stuck_at_finish_does_not_hold_open_the_window_forever():
|
def test_a_progress_stuck_at_finish_does_not_hold_open_the_window_forever():
|
||||||
@@ -325,17 +325,17 @@ def test_a_progress_stuck_at_finish_does_not_hold_open_the_window_forever():
|
|||||||
sensor, coordinator = _sensor(desc)
|
sensor, coordinator = _sensor(desc)
|
||||||
|
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
time.sleep(0.03)
|
time.sleep(0.03)
|
||||||
# Device still (incorrectly) reports Finish on every subsequent poll --
|
# Device still (incorrectly) reports Finish on every subsequent poll --
|
||||||
# must not restart the window.
|
# must not restart the window.
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
time.sleep(0.03) # 0.06s total since the first sighting -- past 0.05s
|
time.sleep(0.03) # 0.06s total since the first sighting -- past 0.05s
|
||||||
_replace(coordinator, state="Ready", progress="Finish")
|
_replace(coordinator, state="Ready", progress="Finish")
|
||||||
assert sensor.native_value == "Idle"
|
assert sensor.native_value == "idle"
|
||||||
|
|
||||||
|
|
||||||
def test_non_sticky_sensor_is_unaffected():
|
def test_non_sticky_sensor_is_unaffected():
|
||||||
@@ -361,11 +361,11 @@ def test_cycle_active_and_machine_state_are_never_held():
|
|||||||
)
|
)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert progress_sensor.native_value == "Finish"
|
assert progress_sensor.native_value == "finish"
|
||||||
assert machine_state_sensor.native_value == "active"
|
assert machine_state_sensor.native_value == "active"
|
||||||
|
|
||||||
_replace(coordinator, state="Ready")
|
_replace(coordinator, state="Ready")
|
||||||
assert progress_sensor.native_value == "Finish" # held
|
assert progress_sensor.native_value == "finish" # held
|
||||||
assert machine_state_sensor.native_value == "idle" # real-time, unaffected
|
assert machine_state_sensor.native_value == "idle" # real-time, unaffected
|
||||||
|
|
||||||
|
|
||||||
@@ -377,8 +377,8 @@ def test_a_partial_update_that_omits_progress_does_not_erase_the_hold():
|
|||||||
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
sensor, coordinator = _sensor(_PROGRESS_DESC)
|
||||||
|
|
||||||
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
_replace(coordinator, state="Run", progress="Finish", progressPercentage="100")
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|
||||||
_apply(coordinator, state="Ready") # partial merge, doesn't restate progress
|
_apply(coordinator, state="Ready") # partial merge, doesn't restate progress
|
||||||
assert coordinator.resources[_HREF]["x.com.samsung.da.progress"] == "Finish"
|
assert coordinator.resources[_HREF]["x.com.samsung.da.progress"] == "Finish"
|
||||||
assert sensor.native_value == "Finish"
|
assert sensor.native_value == "finish"
|
||||||
|
|||||||
Reference in New Issue
Block a user