Files
localthings/tests/test_fridge_capabilities.py
T
Marc Billow 265d94eded Add kimchi-refrigerator compartment coverage (issue #26)
TP2X_REF_20K-class 3-compartment kimchi refrigerators report each
compartment's storage mode and ripening status/timer on
/status/kimchi/<slot>/vs/0, plus a top-compartment door sensor on
/kimchidoors/top/vs/0 -- all previously unbound. Bind them as pattern
capabilities (fridge.KIMCHI_ZONE, fridge.KIMCHI_DOOR_GENERIC), deriving
the per-compartment entity key and display name from the href's
top/middle/bottom segment, the same way DOOR_GENERIC/TEMP_CURRENT_GENERIC
already do.

Storage-mode option labels were translated directly from the reporter's
own SmartThings app screenshots rather than guessed from the raw device
codes or their English paraphrase, confirming the on-screen option order
matches supportMode's array order (including the freezer triplet's
-19/-21/-17°C -> Standard/Strong/Weak mapping).

Also tighten FLEX_ZONE's exists_fn: this device's /mode/vs/0 also
populates modes/supportedOptions, but with a token shape that never
overlaps (a "_[n]:[n]" suffix supportedOptions carries that modes never
repeats), so the existing "supportedOptions is nonempty" check let the
entity bind anyway and get stuck permanently on "unknown". Requiring an
actual resolvable value keeps it working for the RF9000/Bespoke-class
fridges it was built for while leaving it absent here.
2026-07-28 00:35:21 +00:00

467 lines
21 KiB
Python

"""Tests for fridge-specific capabilities."""
from custom_components.localthings.registry.capabilities import fridge
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 = fridge.TEMP_CURRENT_GENERIC.entities[0]
assert desc.unit_fn({'temperature': 3.0, 'units': 'C'}) == '°C'
def test_unit_reads_fahrenheit(self):
desc = fridge.TEMP_CURRENT_GENERIC.entities[0]
assert desc.unit_fn({'temperature': 5.0, 'units': 'F'}) == '°F'
def test_unit_defaults_to_fahrenheit_when_missing(self):
desc = fridge.TEMP_CURRENT_GENERIC.entities[0]
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({'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({'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]
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 = fridge.TEMP_SETPOINT.entities[0]
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')
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')
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')
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({}, {'/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({}, {'/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({}, {'/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(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 = fridge.PANTRY_ZONE.entities[0]
path, body = desc.write_fn('FDR_WINE', {})
assert path == ['status', 'pantry', 'one', 'vs', '0']
assert body == {'x.com.samsung.da.mode': 'FDR_WINE'}
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 = fridge.KIMCHI_ZONE.entities[0]
path, body = desc.write_fn(
'KIMCHI_STORAGE_COLD', {}, href='/status/kimchi/middle/vs/0')
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 = fridge.KIMCHI_ZONE.entities[0]
assert desc.write_fn('KIMCHI_STORAGE_COLD', {}) is None
def test_ripening_status_is_lowercased(self):
desc = next(e for e in fridge.KIMCHI_ZONE.entities if e.key == 'ripening_status')
assert desc.value_fn('Off') == 'off'
assert desc.value_fn(None) is None
def test_door_reuses_open_state_helper(self):
desc = fridge.KIMCHI_DOOR_GENERIC.entities[0]
assert desc.rep_fn({'x.com.samsung.da.openState': 'Open'}) is True
assert desc.rep_fn({'x.com.samsung.da.openState': 'Close'}) is False
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 = [
(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}"
)