Files
localthings/tests/test_fridge_capabilities.py
T
Marc Billow 81dbc6fa03 Key devices on the OCF device ID instead of the serial number
Two Samsung air purifiers of the same model report the identical, well
formed serialNum `BS7SP9AW400114A` (issue #381). Since the entry's
unique_id, the device registry identifiers and every entity unique_id
were all minted from that string, the second unit was refused as already
configured, and would have collided entity-for-entity even if it hadn't
been.

This is the third firmware family to ship an unusable serialNum, after
`Nothing(SVC)` (#83) and the flash-unset sentinel (#189), and the first
one no heuristic can catch: the value is well formed, it's just shared.
`is_placeholder_serial` was a dead end.

So identity moves onto /oic/d's `di`, falling back to /oic/p's `pi`, then
the serial, then the host. `di` is what the protocol already uses to
address the endpoint -- if it were wrong or shared, OCF discovery and the
DTLS association wouldn't work at all -- and it's device-scoped, where
`pi` is platform-scoped and would be shared by a board hosting several
logical devices. Both units in #381 report a distinct `di`. A board that
answers neither resource lands exactly where it did before, so no
existing hardware regresses.

The re-key can't happen in async_migrate_entry: the UUID is only readable
from the device, and an entry can load entirely from its snapshot while
the appliance is off (#295). So v3 -> v4 only records the legacy key, and
the coordinator adopts the UUID on the first live poll, rewriting the
entity registry, the device registry (including subdevice identifiers)
and the entry's unique_id together. Rewriting rather than recreating is
what lets a user keep entity_ids, names, areas, statistics and every
automation that references them.

Three rules keep that adoption from misfiring:

- A poll that reads no UUID never demotes a UUID-keyed entry back onto
  its serial, so one failed reconnect doesn't re-key every entity.
- A changed UUID is followed only when the serial still corroborates it
  (a factory reset may regenerate `di`) or when the entry was keyed on
  its IP, which was never an identity to defend.
- When the identity is rejected as a different appliance, the serial
  isn't adopted either -- otherwise the intruder would gain exactly the
  corroboration needed to win the next poll.

Also stop redacting `di`/`pi` from diagnostics. They're randomly assigned
per-unit UUIDs, not account data, and blanking them is what made the
first #381 diagnostics download unable to answer the only question it was
requested to answer. The owner-set device name stays redacted.

Fixes #381
2026-08-17 05:24:22 +00:00

696 lines
29 KiB
Python

"""Tests for fridge-specific capabilities."""
from collections.abc import Callable
from typing import ClassVar, cast
from custom_components.localthings.coordinator import LocalThingsCoordinator
from custom_components.localthings.registry.capabilities import fridge
from custom_components.localthings.registry.entities import (
NumberDesc,
SelectDesc,
SensorDesc,
)
class TestTempCurrentGeneric:
"""Issue #7: unit must be read live from the device, not assumed to be
Fahrenheit -- a TP1X_REF_21K fridge (RL38C6B0CWW/EG) reports the same
resource in Celsius."""
def test_unit_reads_celsius(self):
desc = next(e for e in fridge.TEMP_CURRENT_GENERIC.entities if isinstance(e, SensorDesc))
assert desc.unit_fn is not None
assert desc.unit_fn({"temperature": 3.0, "units": "C"}) == "°C"
def test_unit_reads_fahrenheit(self):
desc = next(e for e in fridge.TEMP_CURRENT_GENERIC.entities if isinstance(e, SensorDesc))
assert desc.unit_fn is not None
assert desc.unit_fn({"temperature": 5.0, "units": "F"}) == "°F"
def test_unit_defaults_to_fahrenheit_when_missing(self):
desc = next(e for e in fridge.TEMP_CURRENT_GENERIC.entities if isinstance(e, SensorDesc))
assert desc.unit_fn is not None
assert desc.unit_fn({"temperature": 5.0}) == "°F"
class TestDoorGeneric:
"""Issues #77/#83: DOOR_GENERIC used to read only the bare `openState`
field. Most /door/* resources use that, but the ARTIK051_DONGLE_REF
family's /door/onedoorfreezer/vs/0 reports the vendor-prefixed
`x.com.samsung.da.openState` instead -- the pattern capability still
bound the href either way, but the entity's value was always None."""
def test_reads_bare_open_state(self):
desc = fridge.DOOR_GENERIC.entities[0]
assert desc.rep_fn is not None
assert desc.rep_fn({"openState": "Open"}) is True
assert desc.rep_fn({"openState": "Close"}) is False
def test_reads_vendor_prefixed_open_state(self):
desc = fridge.DOOR_GENERIC.entities[0]
assert desc.rep_fn is not None
assert desc.rep_fn({"x.com.samsung.da.openState": "Open"}) is True
assert desc.rep_fn({"x.com.samsung.da.openState": "Close"}) is False
def test_prefers_bare_field_when_both_present(self):
desc = fridge.DOOR_GENERIC.entities[0]
assert desc.rep_fn is not None
rep = {"openState": "Open", "x.com.samsung.da.openState": "Close"}
assert desc.rep_fn(rep) is True
class TestTempSetpointGeneric:
def test_unit_reads_celsius(self):
desc = next(e for e in fridge.TEMP_SETPOINT.entities if isinstance(e, NumberDesc))
assert desc.unit_fn is not None
assert desc.unit_fn({"temperature": -19.0, "units": "C"}) == "°C"
class TestTemperaturesFallback:
"""Aggregate /temperatures/vs/0 -- per-item unit from
x.com.samsung.da.unit ('Celsius'/'Fahrenheit')."""
def test_freezer_unit_celsius(self):
desc = next(
e
for e in fridge.TEMPERATURES_FALLBACK.entities
if e.key == "freezer_temperature" and isinstance(e, SensorDesc)
)
assert desc.unit_fn is not None
rep = {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.description": "Freezer",
"x.com.samsung.da.current": "-19",
"x.com.samsung.da.unit": "Celsius",
},
{
"x.com.samsung.da.description": "Fridge",
"x.com.samsung.da.current": "3",
"x.com.samsung.da.unit": "Celsius",
},
]
}
assert desc.unit_fn(rep) == "°C"
assert desc.value_fn(rep["x.com.samsung.da.items"]) == -19
def test_fridge_unit_fahrenheit(self):
desc = next(
e
for e in fridge.TEMPERATURES_FALLBACK.entities
if e.key == "fridge_temperature" and isinstance(e, SensorDesc)
)
assert desc.unit_fn is not None
rep = {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.description": "Fridge",
"x.com.samsung.da.current": "37",
"x.com.samsung.da.unit": "Fahrenheit",
},
]
}
assert desc.unit_fn(rep) == "°F"
def test_unit_defaults_to_fahrenheit_when_item_missing(self):
desc = next(
e
for e in fridge.TEMPERATURES_FALLBACK.entities
if e.key == "freezer_temperature" and isinstance(e, SensorDesc)
)
assert desc.unit_fn is not None
assert desc.unit_fn({"x.com.samsung.da.items": []}) == "°F"
class TestDefrostBlockStatus:
"""DEFROST_BLOCK_ON means the defrost cycle is actively running, not
that defrost is being withheld -- confirmed against live dumps showing
it ON while defrost_delay is off."""
def test_key_and_name_reflect_active_defrosting(self):
desc = fridge.DEFROST_BLOCK_STATUS.entities[0]
assert desc.key == "defrost_active"
def test_value_fn(self):
desc = fridge.DEFROST_BLOCK_STATUS.entities[0]
assert desc.value_fn(["DEFROST_BLOCK_ON"]) is True
assert desc.value_fn(["DEFROST_BLOCK_OFF"]) is False
class TestRefrigerationFallback:
"""/refrigeration/0 duplicates two of REFRIGERATION's (/refrigeration/vs/0)
fields under different names (rapidFreeze/rapidCool vs
rapidFreezing/rapidFridge) but also carries one genuinely new field
(defrost) that has no vs-href equivalent."""
def test_href(self):
assert fridge.REFRIGERATION_FALLBACK.href == "/refrigeration/0"
def test_defrost_active_is_fallback_of_defrost_block_status(self):
# Duplicates DEFROST_BLOCK_STATUS's defrost_active
# (/defrost/block/vs/0) -- only a true fallback when that richer
# href is absent (issue #7's device: /refrigeration/0 only).
desc = next(e for e in fridge.REFRIGERATION_FALLBACK.entities if e.key == "defrost_active")
assert desc.value_fn(True) is True
assert desc.value_fn(False) is False
assert desc.exists_fn is not None
assert desc.exists_fn({}, {"/refrigeration/0": {}}) is True
assert desc.exists_fn({}, {"/refrigeration/0": {}, "/defrost/block/vs/0": {}}) is False
def test_rapid_switches_hidden_when_vs_href_present(self):
for key in ("rapid_fridge", "rapid_freezing"):
desc = next(e for e in fridge.REFRIGERATION_FALLBACK.entities if e.key == key)
assert desc.exists_fn is not None
assert desc.exists_fn({}, {"/refrigeration/vs/0": {}, "/refrigeration/0": {}}) is False
def test_rapid_switches_shown_when_vs_href_absent(self):
for key in ("rapid_fridge", "rapid_freezing"):
desc = next(e for e in fridge.REFRIGERATION_FALLBACK.entities if e.key == key)
assert desc.exists_fn is not None
assert desc.exists_fn({}, {"/refrigeration/0": {}}) is True
class TestFlexZone:
"""Issue #32 (surfaced by #26/#27's TP2X/Bespoke dumps): the flex-zone
entry's prefix varies by fridge family (CV_TTYPE_RF9000A_ on RF9000-class,
CV_FDR_ on TP1X/Bespoke-class). The old hardcoded-prefix match left
CV_FDR_-family fridges with an always-unknown select, and a write would
have appended a duplicate flag instead of replacing the existing one."""
def test_reads_rf9000_prefix(self):
rep = {
"x.com.samsung.da.modes": ["CV_TTYPE_RF9000A_BEVERAGE", "WATERFILTER_DISABLE"],
"x.com.samsung.da.supportedOptions": [
"CV_TTYPE_RF9000A_FREEZE",
"CV_TTYPE_RF9000A_BEVERAGE",
],
}
assert fridge._flex_zone_current(rep) == "CV_TTYPE_RF9000A_BEVERAGE"
def test_reads_cv_fdr_prefix(self):
rep = {
"x.com.samsung.da.modes": ["CVN_CONVERTIBLE_ZONE", "CV_FDR_MEAT", "WATERFILTER_ENABLE"],
"x.com.samsung.da.supportedOptions": [
"CV_FDR_WINE",
"CV_FDR_DELI",
"CV_FDR_BEVERAGE",
"CV_FDR_MEAT",
],
}
assert fridge._flex_zone_current(rep) == "CV_FDR_MEAT"
def test_write_replaces_rather_than_duplicates(self):
rep = {
"x.com.samsung.da.modes": ["CVN_CONVERTIBLE_ZONE", "CV_FDR_MEAT", "WATERFILTER_ENABLE"],
"x.com.samsung.da.supportedOptions": [
"CV_FDR_WINE",
"CV_FDR_DELI",
"CV_FDR_BEVERAGE",
"CV_FDR_MEAT",
],
}
path, payload = fridge._flex_zone_write("CV_FDR_WINE", rep)
assert path == ["mode", "vs", "0"]
modes = payload["x.com.samsung.da.modes"]
assert modes.count("CV_FDR_WINE") == 1
assert "CV_FDR_MEAT" not in modes
assert "CVN_CONVERTIBLE_ZONE" in modes and "WATERFILTER_ENABLE" in modes
def test_exists_only_when_a_current_value_resolves(self):
"""Issue #26's kimchi-refrigerator family also populates /mode/vs/0's
modes/supportedOptions, but its supportedOptions tokens carry a
"_[n]:[n]" suffix modes never repeats, so no item ever overlaps --
the entity used to bind anyway (supportedOptions is nonempty) and
get stuck on "unknown" forever."""
no_overlap_rep = {
"x.com.samsung.da.modes": ["KIMCHIT_STORAGE_FREEZER_NORMAL"],
"x.com.samsung.da.supportedOptions": ["KIMCHIT_STORAGE_FREEZER_NORMAL_[0]:[0]"],
}
desc = fridge.FLEX_ZONE.entities[0]
assert desc.exists_fn is not None
assert desc.exists_fn(no_overlap_rep, {}) is False
overlap_rep = {
"x.com.samsung.da.modes": ["CV_FDR_MEAT"],
"x.com.samsung.da.supportedOptions": ["CV_FDR_WINE", "CV_FDR_MEAT"],
}
assert desc.exists_fn(overlap_rep, {}) is True
class TestTp1xNativeDuplicateResources:
"""The US TP1X_REF_21K publishes two native mirrors in addition to the
richer vendor resources. They must count as covered without producing
duplicate entities or guessing unverified write contracts."""
def test_duplicate_capabilities_have_no_entities(self):
assert fridge.DEFROST_DELAY_NATIVE_DUPLICATE.entities == ()
assert fridge.ICEMAKER_STATUS_NATIVE_DUPLICATE.entities == ()
def test_us_fixture_has_complete_coverage(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_us")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["defrost_delay"] is False
assert "ice_maker_enabled" not in state
assert state["icemaker_one_enabled"] is True
assert state["icemaker_two_enabled"] is True
assert state["selfcheck_error"] == "DA_ERROR_NONE"
class TestPantryZone:
"""Cool Select Zone pantry compartment on ARTIK051_REF_17K -- issue #20."""
def test_href(self):
assert fridge.PANTRY_ZONE.href == "/status/pantry/one/vs/0"
def test_write(self):
desc = next(e for e in fridge.PANTRY_ZONE.entities if isinstance(e, SelectDesc))
assert desc.write_fn is not None
result = desc.write_fn("FDR_WINE", {})
assert result is not None
path, body = result
assert path == ["status", "pantry", "one", "vs", "0"]
assert body == {"x.com.samsung.da.mode": "FDR_WINE"}
class TestDefiniteTemperatureCooler:
"""Discrete cooler setpoint (RT42DG6630B1FZ, issue #186) -- a single-door
fridge whose /temperature/definite/cooler/vs/0 falls outside both
TEMP_CURRENT_GENERIC and TEMP_SETPOINT's href prefixes, so temperature
control was entirely unbound before this capability was added."""
def test_href(self):
assert fridge.DEFINITE_TEMPERATURE_COOLER.href == "/temperature/definite/cooler/vs/0"
def test_write(self):
desc = next(
e for e in fridge.DEFINITE_TEMPERATURE_COOLER.entities if isinstance(e, SelectDesc)
)
assert desc.write_fn is not None
result = desc.write_fn("3", {})
assert result is not None
path, body = result
assert path == ["temperature", "definite", "cooler", "vs", "0"]
assert body == {"x.com.samsung.da.definite.desired": "3"}
def test_no_unbound_hrefs_and_discrete_options(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_definite_cooler")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["cooler_temperature_setpoint"] == "2"
rep = resources["/temperature/definite/cooler/vs/0"]
assert rep["x.com.samsung.da.definite.supportedList"] == ["1", "2", "3", "4", "7"]
class TestDefiniteTemperatureFreezer:
"""Discrete freezer setpoint (TP1X_REF_21K, issue #229) -- the same
definite-setpoint pattern as DEFINITE_TEMPERATURE_COOLER above, on a
fridge/freezer combo that reports it for *both* compartments. Identical
field shape, just negative supportedList values."""
def test_href(self):
assert fridge.DEFINITE_TEMPERATURE_FREEZER.href == "/temperature/definite/freezer/vs/0"
def test_write(self):
desc = next(
e for e in fridge.DEFINITE_TEMPERATURE_FREEZER.entities if isinstance(e, SelectDesc)
)
assert desc.write_fn is not None
result = desc.write_fn("-19", {})
assert result is not None
path, body = result
assert path == ["temperature", "definite", "freezer", "vs", "0"]
assert body == {"x.com.samsung.da.definite.desired": "-19"}
def test_no_unbound_hrefs_and_discrete_options(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp1x_ref_21k_definite")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["cooler_temperature_setpoint"] == "4"
assert state["freezer_temperature_setpoint"] == "-19"
rep = resources["/temperature/definite/freezer/vs/0"]
assert rep["x.com.samsung.da.definite.supportedList"] == [
"-23",
"-21",
"-19",
"-17",
"-15",
]
class TestKimchiZone:
"""Kimchi-refrigerator compartments (TP2X_REF_20K-class, issue #26) --
top/middle/bottom each report an identically-shaped currentMode/
supportMode resource under /status/kimchi/<slot>/vs/0."""
def test_href_prefix(self):
assert fridge.KIMCHI_ZONE.href_prefix == "/status/kimchi/"
assert fridge.KIMCHI_DOOR_GENERIC.href_prefix == "/kimchidoors/"
def test_write_derives_path_from_href(self):
desc = next(e for e in fridge.KIMCHI_ZONE.entities if isinstance(e, SelectDesc))
assert desc.write_fn is not None
write_fn = cast("Callable[..., tuple[list[str], dict] | None]", desc.write_fn)
rep = {"x.com.samsung.da.supportMode": ["KIMCHI_STORAGE_COLD"]}
result = write_fn("KIMCHI_STORAGE_COLD", rep, href="/status/kimchi/middle/vs/0")
assert result is not None
path, body = result
assert path == ["status", "kimchi", "middle", "vs", "0"]
assert body == {"x.com.samsung.da.currentMode": "KIMCHI_STORAGE_COLD"}
def test_write_without_href_is_rejected(self):
desc = next(e for e in fridge.KIMCHI_ZONE.entities if isinstance(e, SelectDesc))
assert desc.write_fn is not None
rep = {"x.com.samsung.da.supportMode": ["KIMCHI_STORAGE_COLD"]}
assert desc.write_fn("KIMCHI_STORAGE_COLD", rep) is None
def test_write_rejects_value_outside_supportmode(self):
"""A value the compartment never advertised is rejected rather than
written blind -- this write path is unconfirmed against real
hardware (module docstring above KIMCHI_ZONE), so a bad value here
is a food-safety-adjacent outcome, not just a cosmetic one."""
desc = next(e for e in fridge.KIMCHI_ZONE.entities if isinstance(e, SelectDesc))
assert desc.write_fn is not None
write_fn = cast("Callable[..., tuple[list[str], dict] | None]", desc.write_fn)
rep = {"x.com.samsung.da.supportMode": ["KIMCHI_STORAGE_COLD"]}
assert (
write_fn(
"KIMCHI_STORAGE_WARM",
rep,
href="/status/kimchi/middle/vs/0",
)
is None
)
def test_ripening_status_passes_through_device_value(self):
"""No device_class='enum' catalog entry exists for this sensor, so
lowercasing it would only make the raw device token un-translatable
by HA -- pass the device's own casing straight through instead."""
desc = next(e for e in fridge.KIMCHI_ZONE.entities if e.key == "ripening_status")
assert desc.value_fn("Off") == "Off"
def test_door_reuses_open_state_helper(self):
desc = fridge.KIMCHI_DOOR_GENERIC.entities[0]
assert desc.rep_fn is not None
assert desc.rep_fn({"x.com.samsung.da.openState": "Open"}) is True
assert desc.rep_fn({"x.com.samsung.da.openState": "Close"}) is False
async def test_zone_mode_select_round_trips_through_display_casing(self):
"""kimchi_zone_mode's displayed value (lowercase, catalog-translated)
and the raw device code it writes back can silently drift apart --
this is the one place that casing conversion could break. Runs
through the real discovery/select pipeline against the tp2x_ref_20k
kimchi fixture rather than a hand-built descriptor, so it also
catches use_instance_name key derivation going wrong."""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from custom_components.localthings.registry.entities import SelectDesc
from custom_components.localthings.select import LocalThingsSelect
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp2x_ref_20k_kimchi")
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
)
mode_bound = next(
b
for b in bound
if isinstance(b.desc, SelectDesc) and b.href == "/status/kimchi/middle/vs/0"
)
class _FakeCoordinator:
device_key = "TEST-SERIAL"
def __init__(self, resources, data):
self.last_resources = resources
self.data = data
self.commands = []
async def async_send_command(self, bound, value):
self.commands.append(value)
coordinator = _FakeCoordinator(resources, flatten(bound, resources))
entity = LocalThingsSelect(cast(LocalThingsCoordinator, coordinator), mode_bound)
assert entity.current_option == "kimchi_storage_normal"
assert "kimchi_storage_cold" in entity.options
await entity.async_select_option("kimchi_storage_cold")
assert coordinator.commands == ["KIMCHI_STORAGE_COLD"]
class TestArtik051AndTp2xFixturesHaveCompleteCoverage:
"""issue #20 (ARTIK051_REF_17K) and #26 (TP2X_REF_20K) both triggered
the incomplete-capability-coverage repair; both must resolve to zero
unbound hrefs now that /diagnosis/vs/0 (dishwasher.DIAGNOSIS, reused for
fridges), /status/pantry/one/vs/0 (PANTRY_ZONE), and the OCF-native
defrost/icemaker mirrors are covered."""
def test_artik051_ref_17k(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_artik051_ref_17k")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["pantry_zone_mode"] == "FDR_DRINKS"
def test_tp2x_ref_20k(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp2x_ref_20k")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["flex_zone_mode"] == "CV_FDR_BEVERAGE"
def test_tp2x_ref_20k_kimchi(self):
"""A different physical unit sharing the same modelNum string (issue
#26, second reporter) -- a 3-compartment kimchi refrigerator instead
of the flex-zone fridge above. /kimchidoors/top/vs/0 and
/status/kimchi/{top,middle,bottom}/vs/0 were previously unbound."""
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_tp2x_ref_20k_kimchi")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["top_mode"] == "STORAGE_FREEZER_NORMAL"
assert state["middle_mode"] == "KIMCHI_STORAGE_NORMAL"
assert state["bottom_mode"] == "KIMCHI_STORAGE_NORMAL"
assert state["top_open"] is False
assert "middle_open" not in state # no /kimchidoors/middle/vs/0 reported
assert "flex_zone_mode" not in state # no resolvable overlap on this family
class TestArtik051DongleRefFixtureCoverage:
"""Issues #77/#83: the ARTIK051_DONGLE_REF standalone freezer resolves
to the refrigerator registry (segment-based modelNum detection) with
zero unbound hrefs, and its door/temperature entities -- which only
bind through pattern capabilities the 'unknown' fallback never tried --
now carry real values."""
def test_no_unbound_hrefs_and_expected_entities(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import (
for_device_by_model,
refrigerator,
)
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_artik051_dongle_ref")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"], info["x.com.samsung.da.description"]
)
assert reg is not None and reg.name == "refrigerator"
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["door_onedoorfreezer_open"] is False # rep reports 'Close'
assert state["freezer_temperature"] == -20.0
assert state["freezer_setpoint"] == -20.0
class TestArtik051DongleRefCoolerFixtureCoverage:
"""Issue #78: RR40M7165WW, the fridge half of the same household
ARTIK051_DONGLE_REF dongle setup as issue #77's freezer. Notably reports
*two* door hrefs (/door/cooler/0 and /door/onedoorfreezer/vs/0) despite
being a single-door fridge, not a fridge/freezer combo -- apparently
shared firmware naming across the product line, not a real second
compartment. Both must still resolve to zero unbound hrefs and real
values with the same fix as #77."""
def test_no_unbound_hrefs_and_expected_entities(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import (
for_device_by_model,
refrigerator,
)
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator_artik051_dongle_ref_cooler")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"], info["x.com.samsung.da.description"]
)
assert reg is not None and reg.name == "refrigerator"
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["door_cooler_open"] is False
assert state["door_onedoorfreezer_open"] is False
assert state["cooler_temperature"] == 3.0
assert state["cooler_setpoint"] == 3.0
class TestRefrigeratorAiEnergyLevelFixtureCoverage:
"""AI energy-saving level (common.AI_ENERGY_LEVEL, see
test_common_capabilities.py) is exercised end-to-end here against the
refrigerator fixture/registry.
refrigerator_device.json's supportedAiLevel was extended to two entries
(['1', '2']) specifically to exercise the select branch -- the real
captured TP1X_REF_21K_US dump only ever reports one entry (switch
branch), so the select is synthetic-fixture-only for now."""
def test_synthetic_fixture_has_complete_coverage(self):
from custom_components.localthings.registry.adapter import flatten
from custom_components.localthings.registry.by_type import refrigerator
from custom_components.localthings.registry.discovery import discover
from tests.conftest import _load_device
resources = _load_device("refrigerator")
unbound = []
bound = discover(
resources,
refrigerator.REGISTRY.capabilities,
refrigerator.REGISTRY.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
state = flatten(bound, resources)
assert state["ai_energy_level"] == "1"
class TestSwitchOffIsNotInverted:
"""Regression: several fridge SwitchDesc write_fns used ``'On' if p else 'Off'``.
The switch platform passes the literal string ``'Off'`` on turn-off (and
``'On'`` on turn-on). ``'Off'`` is a non-empty, truthy string, so
``'On' if p else 'Off'`` always evaluated to ``'On'`` — turning the switch
OFF silently re-sent ``On`` and the switch could never be turned off. The
guard must compare ``p == 'On'`` (as the other capability files already do).
"""
# (switch descriptor, payload key it writes)
CASES: ClassVar[list] = [
(fridge.ICEMAKER_NIGHTTIME.entities[0], "ice.night.status"),
(fridge.STATUS_LOCK.entities[0], "x.com.samsung.da.ado.devicecontrol"),
(fridge.STATUS_LOCK.entities[1], "x.com.samsung.da.device.sound"),
(fridge.DEFROST_DELAY.entities[0], "x.com.samsung.da.delayDefrost"),
(fridge.WELCOME_LIGHTING.entities[0], "status"),
(fridge.CABINET_LIGHT.entities[1], "light.dimming.status"),
(fridge.ICEMAKER_STATUS_FALLBACK.entities[0], "x.com.samsung.da.iceMaker"),
]
def test_turning_off_sends_off(self):
for desc, key in self.CASES:
_segs, payload = desc.write_fn("Off", {})
assert payload[key] == "Off", (
f"{desc.key}: OFF must send 'Off', got {payload[key]!r} (inverted)"
)
def test_turning_on_sends_on(self):
for desc, key in self.CASES:
_segs, payload = desc.write_fn("On", {})
assert payload[key] == "On", f"{desc.key}: ON must send 'On', got {payload[key]!r}"