RT42DG6630B1FZ is a single-door "cooler only" fridge that reports its setpoint on /temperature/definite/cooler/vs/0 -- a vendor resource outside both TEMP_CURRENT_GENERIC's '/temperature/current/' and TEMP_SETPOINT's '/temperature/desired/' href prefixes, so it was entirely unbound and the setting stayed app-only. Its supportedList (1/2/3/4/7 °C) isn't a contiguous range, so this is modeled as a select over the device's own live options rather than a NumberDesc that would let a user pick an unsupported value like 5 or 6.
565 lines
25 KiB
Python
565 lines
25 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 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 = fridge.DEFINITE_TEMPERATURE_COOLER.entities[0]
|
|
path, body = desc.write_fn('3', {})
|
|
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 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]
|
|
rep = {'x.com.samsung.da.supportMode': ['KIMCHI_STORAGE_COLD']}
|
|
path, body = desc.write_fn(
|
|
'KIMCHI_STORAGE_COLD', rep, 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]
|
|
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 = fridge.KIMCHI_ZONE.entities[0]
|
|
rep = {'x.com.samsung.da.supportMode': ['KIMCHI_STORAGE_COLD']}
|
|
assert desc.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({'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_serial = '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(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 = [
|
|
(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}"
|
|
)
|