Merge pull request #102 from mbillow/claude/device-support-issue-77-freezer
Device support issue #77: freezer
This commit is contained in:
@@ -89,6 +89,22 @@ _CONSUMER_PREFIX_TO_KEY: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def _model_num_segments(model_num: str) -> list[str]:
|
||||
"""Underscore-delimited segments of modelNum's pipe-prefix.
|
||||
|
||||
Most boards wrap a token in underscores on both sides ('..._REF_...'),
|
||||
which a plain substring check catches fine. But the ARTIK051_DONGLE_REF
|
||||
family (issues #77, #83) reports modelNum as
|
||||
'<board>_DONGLE_REF|<rest...>' -- REF is the *last* segment before the
|
||||
pipe, with no trailing underscore, so '_REF_' never matches and the
|
||||
device silently fell back to 'unknown'. Splitting on '_' and checking
|
||||
segment membership catches both shapes without caring which side (if
|
||||
either) has a delimiter.
|
||||
"""
|
||||
prefix = (model_num or '').split('|', 1)[0]
|
||||
return prefix.split('_')
|
||||
|
||||
|
||||
def _consumer_model_key(description: str) -> Optional[str]:
|
||||
"""Registry key from the consumer-model token in `description`, or None.
|
||||
|
||||
@@ -123,7 +139,7 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
known type, None otherwise.
|
||||
"""
|
||||
key = _consumer_model_key(description)
|
||||
if key is None and '_REF_' in (model_num or ''):
|
||||
if key is None and 'REF' in _model_num_segments(model_num):
|
||||
key = 'refrigerator'
|
||||
# Room air conditioners (e.g. ARTIK051_PRAC_20K) report no oneUiVersion and
|
||||
# a modelNum carrying the '_PRAC_' (Package Room Air Conditioner) token.
|
||||
|
||||
@@ -601,15 +601,27 @@ FLEX_ZONE = Capability(
|
||||
# Generic door pattern capability (href=None — use as pattern_cap only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _door_open_state(rep):
|
||||
"""Most /door/* resources report bare `openState`, but the
|
||||
ARTIK051_DONGLE_REF family's /door/onedoorfreezer/vs/0 (issues #77, #83)
|
||||
reports the vendor-prefixed `x.com.samsung.da.openState` instead. This
|
||||
capability still binds either way (href_prefix match doesn't care about
|
||||
field names), but a plain `field=` lookup against the wrong key means
|
||||
the entity exists and is permanently unavailable -- check both."""
|
||||
v = rep.get('openState')
|
||||
if v is None:
|
||||
v = rep.get('x.com.samsung.da.openState')
|
||||
return v == 'Open'
|
||||
|
||||
|
||||
DOOR_GENERIC = Capability(
|
||||
href=None,
|
||||
href_prefix='/door/',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
BinarySensorDesc(key='open', field='openState',
|
||||
BinarySensorDesc(key='open', rep_fn=_door_open_state,
|
||||
translation_key='instance_open',
|
||||
use_instance_name=True, device_class='door',
|
||||
value_fn=lambda v: v == 'Open'),
|
||||
use_instance_name=True, device_class='door'),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"defrost_active",
|
||||
"defrost_delay",
|
||||
"diagnosis_status",
|
||||
"door_onedoorfreezer_open",
|
||||
"energy_kwh",
|
||||
"energy_last_month_kwh",
|
||||
"energy_saved_kwh",
|
||||
"energy_this_month_kwh",
|
||||
"firmware_update",
|
||||
"freezer_setpoint",
|
||||
"freezer_temperature",
|
||||
"ice_maker_enabled",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"sabbath_mode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"cooler_setpoint",
|
||||
"cooler_temperature",
|
||||
"defrost_active",
|
||||
"defrost_delay",
|
||||
"diagnosis_status",
|
||||
"door_cooler_open",
|
||||
"door_onedoorfreezer_open",
|
||||
"energy_kwh",
|
||||
"energy_last_month_kwh",
|
||||
"energy_saved_kwh",
|
||||
"energy_this_month_kwh",
|
||||
"firmware_update",
|
||||
"ice_maker_enabled",
|
||||
"power_energy_kwh",
|
||||
"power_watts",
|
||||
"rapid_freezing",
|
||||
"rapid_fridge",
|
||||
"sabbath_mode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/bespoke/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.BespokeProduct": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/cooler/0",
|
||||
"rep": {
|
||||
"openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/onedoorfreezer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+01:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00090741|00090028001130300100000000000000",
|
||||
"x.com.samsung.da.description": "ARTIK_REF_17K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "011120190711",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "14040703,ffffffff",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"RVACATION_ON",
|
||||
"RVACATION_OFF"
|
||||
],
|
||||
"modes": [
|
||||
"RVACATION_OFF"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"RVACATION_ON",
|
||||
"RVACATION_OFF"
|
||||
],
|
||||
"x.com.samsung.da.modes": [
|
||||
"RVACATION_OFF"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/0",
|
||||
"rep": {
|
||||
"defrost": false,
|
||||
"rapidFreeze": false,
|
||||
"rapidCool": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.rapidFridge": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sabbath/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/cooler/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/cooler/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
1.0,
|
||||
7.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Fridge",
|
||||
"x.com.samsung.da.desired": "3",
|
||||
"x.com.samsung.da.current": "3",
|
||||
"x.com.samsung.da.maximum": "7",
|
||||
"x.com.samsung.da.minimum": "1",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/bespoke/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.BespokeProduct": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/defrost/delay/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/diagnosis/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.diagnosisStart": "Ready"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/door/onedoorfreezer/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/doors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Door",
|
||||
"x.com.samsung.da.openState": "Close"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.timeoffset": "+02:00",
|
||||
"x.com.samsung.supprtedtype": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/icemaker/status/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000",
|
||||
"x.com.samsung.da.description": "ARTIK_REF_17K",
|
||||
"x.com.samsung.da.serialNum": "**REDACTED**",
|
||||
"x.com.samsung.da.otnDUID": "**REDACTED**",
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "WiFi Module",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "011120190711",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Micom",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "2103120d,ffffffff",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/0",
|
||||
"rep": {
|
||||
"supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_REPORT_MODEL",
|
||||
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.supportedModes": [
|
||||
"HOMECARE_WIZARD_V2",
|
||||
"ENERGY_REPORT_MODEL",
|
||||
"18K_REF_OUTDOOR_CONTROL_V2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "Micom",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/0",
|
||||
"rep": {
|
||||
"defrost": false,
|
||||
"rapidFreeze": false,
|
||||
"rapidCool": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/refrigeration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.rapidFreezing": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/sabbath/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/current/freezer/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": -20.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperature/desired/freezer/0",
|
||||
"rep": {
|
||||
"range": [
|
||||
-23.0,
|
||||
-15.0
|
||||
],
|
||||
"units": "C",
|
||||
"temperature": -20.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/temperatures/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Freezer",
|
||||
"x.com.samsung.da.desired": "-20",
|
||||
"x.com.samsung.da.current": "-20",
|
||||
"x.com.samsung.da.maximum": "-15",
|
||||
"x.com.samsung.da.minimum": "-23",
|
||||
"x.com.samsung.da.unit": "Celsius"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -114,6 +114,25 @@ class TestWasherRegistry:
|
||||
assert href in registry.capabilities, f"{href} missing from washer registry"
|
||||
|
||||
|
||||
class TestModelNumSegments:
|
||||
def test_splits_pipe_prefix_on_underscore(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('ARTIK051_DONGLE_REF|00127641|000800200014') == [
|
||||
'ARTIK051', 'DONGLE', 'REF',
|
||||
]
|
||||
|
||||
def test_ignores_everything_after_first_pipe(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('TP2X_RAC_20K|abc|REF_should_not_appear') == [
|
||||
'TP2X', 'RAC', '20K',
|
||||
]
|
||||
|
||||
def test_empty_for_none_or_empty_input(self):
|
||||
from custom_components.localthings.registry.by_type import _model_num_segments
|
||||
assert _model_num_segments('') == ['']
|
||||
assert _model_num_segments(None) == ['']
|
||||
|
||||
|
||||
class TestConsumerModelKey:
|
||||
def test_finds_key_in_last_segment(self):
|
||||
from custom_components.localthings.registry.by_type import _consumer_model_key
|
||||
@@ -216,6 +235,20 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_refrigerator_dongle_ref_pipe_delimited_modelnum(self):
|
||||
"""Issues #77/#83: the ARTIK051_DONGLE_REF family's modelNum is
|
||||
'<board>_DONGLE_REF|<rest>' -- REF is the last underscore segment
|
||||
before the pipe, with no trailing underscore, so the plain '_REF_'
|
||||
substring check used to miss it entirely and the device fell back
|
||||
to 'unknown' with only common capabilities."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'ARTIK051_DONGLE_REF|00127641|00080020001430300100000000000000',
|
||||
'ARTIK_REF_17K',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'refrigerator'
|
||||
|
||||
def test_airconditioner_via_prac_token(self):
|
||||
"""Issue #17: a room AC (ARTIK051_PRAC_20K) reports no oneUiVersion and
|
||||
an unrecognized consumer token ('20K'); it falls back to the '_PRAC_'
|
||||
|
||||
@@ -20,7 +20,30 @@ class TestTempCurrentGeneric:
|
||||
assert desc.unit_fn({'temperature': 5.0}) == '°F'
|
||||
|
||||
|
||||
class TestTempSetpoint:
|
||||
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'
|
||||
@@ -231,6 +254,75 @@ class TestArtik051AndTp2xFixturesHaveCompleteCoverage:
|
||||
assert state['flex_zone_mode'] == 'CV_FDR_BEVERAGE'
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -216,6 +216,45 @@ def test_registry_reproduces_golden_state_keys_for_artik051_ref_17k():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref_cooler():
|
||||
"""RR40M7165WW (issue #78) -- the same ARTIK051_DONGLE_REF household
|
||||
dongle family as issue #77's freezer, but the fridge half: reports
|
||||
/door/cooler/0 *and* /door/onedoorfreezer/vs/0 (the latter apparently
|
||||
shared firmware naming, not an actual second freezer compartment) plus
|
||||
/temperature/{current,desired}/cooler/0. Same pipe-delimited modelNum
|
||||
detection gap and DOOR_GENERIC field-name gap as #77, same fix."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref_cooler')
|
||||
golden = json.loads((GOLDEN / 'refrigerator_artik051_dongle_ref_cooler.json').read_text())
|
||||
state_keys = _new_state_keys('refrigerator_artik051_dongle_ref_cooler', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_artik051_dongle_ref():
|
||||
"""ARTIK051_DONGLE_REF standalone freezer (issues #77/#83) -- reports no
|
||||
oneUiVersion and a pipe-delimited modelNum ('..._DONGLE_REF|<rest>')
|
||||
that the old '_REF_' substring check missed entirely; resolved via the
|
||||
segment-based check in for_device_by_model. Its door
|
||||
(/door/onedoorfreezer/vs/0) and temperature
|
||||
(/temperature/{current,desired}/freezer/0) hrefs only bind through
|
||||
fridge.py's pattern capabilities, which the 'unknown' fallback never
|
||||
tries -- so this also regression-tests that those resources bind at
|
||||
all once routed to the right registry."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('refrigerator_artik051_dongle_ref')
|
||||
golden = json.loads((GOLDEN / 'refrigerator_artik051_dongle_ref.json').read_text())
|
||||
state_keys = _new_state_keys('refrigerator_artik051_dongle_ref', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_tp2x_ref_20k():
|
||||
"""TP2X_REF_20K -- CV_FDR_-prefixed flex zone (issue #32) plus the extra
|
||||
energy fields (cumulativeConsumption/monthlyConsumption/
|
||||
|
||||
Reference in New Issue
Block a user