From df5b704f3e34b3878b1b1c6056100459f28eeed1 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Fri, 7 Aug 2026 14:18:08 +0000 Subject: [PATCH] Close gas-cooktop coverage gap for TP2X_DA-KS-COOKTOP-000001 (issue #314) /alarms/vs/0 and /kidslock/vs/0 were the dump's two unbound hrefs -- both are the exact shapes common.UNIVERSAL already models elsewhere (common.ALARMS, common.KIDS_LOCK_VS_FALLBACK), picked individually rather than pulling in all of UNIVERSAL to match this registry's existing hand-picked-common style. The six-vs-three burner count the reporter originally asked about is expected behavior (the board's own /mode/vs/0 options genuinely advertise six OperationState slots on hardware with three physical burners, with no per-device signal to tell real slots from phantom ones) -- already explained on the issue; this commit is scoped to the coverage warning. --- .../localthings/registry/by_type/cooktop.py | 6 + .../fixtures/gas_cooktop_tp2x_ks_device.json | 138 ++++++++++++++++++ .../fixtures/golden/gas_cooktop_tp2x_ks.json | 24 +++ .../test_gas_cooktop_tp2x_ks_capabilities.py | 55 +++++++ tests/test_golden_regression.py | 19 +++ 5 files changed, 242 insertions(+) create mode 100644 tests/fixtures/gas_cooktop_tp2x_ks_device.json create mode 100644 tests/fixtures/golden/gas_cooktop_tp2x_ks.json create mode 100644 tests/test_gas_cooktop_tp2x_ks_capabilities.py diff --git a/custom_components/localthings/registry/by_type/cooktop.py b/custom_components/localthings/registry/by_type/cooktop.py index b2fcb83..81cdf54 100644 --- a/custom_components/localthings/registry/by_type/cooktop.py +++ b/custom_components/localthings/registry/by_type/cooktop.py @@ -22,6 +22,12 @@ REGISTRY = DeviceRegistry( cooktop.COOKTOP_CONNECTED, cooktop.PAIRED_HOOD_STATUS, common.FIRMWARE_UPDATE, + # issue #314: /alarms/vs/0 and /kidslock/vs/0 are the same + # generic shapes common.UNIVERSAL already models elsewhere -- + # picked individually rather than pulling in all of UNIVERSAL, + # matching this registry's existing hand-picked-common style. + common.ALARMS, + common.KIDS_LOCK_VS_FALLBACK, ] ), ) diff --git a/tests/fixtures/gas_cooktop_tp2x_ks_device.json b/tests/fixtures/gas_cooktop_tp2x_ks_device.json new file mode 100644 index 0000000..88cf7c5 --- /dev/null +++ b/tests/fixtures/gas_cooktop_tp2x_ks_device.json @@ -0,0 +1,138 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/alarms/vs/0", + "rep": { + "rt": [ + "x.com.samsung.da.alarms" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ], + "x.com.samsung.da.items": [ + { + "x.com.samsung.da.id": "0", + "x.com.samsung.da.description": "Alarm", + "x.com.samsung.da.alarmType": "Device", + "x.com.samsung.da.code": "CT_E", + "x.com.samsung.da.triggeredTime": "2026-08-06T14:24:02" + } + ] + } + }, + { + "href": "/bluetooth/hood/status/vs/0", + "rep": { + "connectionState": "disconnected", + "micomModelId": "", + "firmwareVersion": "", + "power": "off", + "fanSpeed": 0, + "lampState": "off", + "timer": {}, + "rt": [ + "bluetoothHoodStatus" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/configuration/vs/0", + "rep": {} + }, + { + "href": "/connected/vs/0", + "rep": { + "x.com.samsung.da.connected": "On", + "rt": [ + "x.com.samsung.da.connected" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.options": [ + "DeviceType_NV8000T-/KO0", + "Pause_Off", + "SyncFlex_Off", + "FlexCoil_0", + "MainTimerCurrent_0", + "MainTimerSet_0", + "MainTimerState_Ready", + "IndependentTimerCheck_Enable", + "OperationState0_Ready", + "HotSurface0_Normal", + "PowerLevel0_0", + "OperationState1_Ready", + "HotSurface1_Normal", + "PowerLevel1_0", + "OperationState2_Ready", + "HotSurface2_Normal", + "PowerLevel2_0", + "OperationState3_Ready", + "HotSurface3_Normal", + "PowerLevel3_0", + "OperationState4_Ready", + "HotSurface4_Normal", + "PowerLevel4_0", + "OperationState5_Ready", + "HotSurface5_Normal", + "PowerLevel5_0" + ], + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/otninformation/vs/0", + "rep": { + "x.com.samsung.da.target": "", + "x.com.samsung.da.newVersionAvailable": "false" + } + }, + { + "href": "/power/vs/0", + "rep": { + "x.com.samsung.da.power": "Off", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + } + ] +} diff --git a/tests/fixtures/golden/gas_cooktop_tp2x_ks.json b/tests/fixtures/golden/gas_cooktop_tp2x_ks.json new file mode 100644 index 0000000..0492b0a --- /dev/null +++ b/tests/fixtures/golden/gas_cooktop_tp2x_ks.json @@ -0,0 +1,24 @@ +{ + "state_keys": [ + "alarm_code", + "any_burner_active", + "burner_0_state", + "burner_1_state", + "burner_2_state", + "burner_3_state", + "burner_4_state", + "burner_5_state", + "child_lock", + "cloud_connected", + "firmware_update", + "main_timer_current", + "main_timer_state", + "paired_hood_connected", + "paired_hood_fan_speed", + "paired_hood_firmware", + "paired_hood_light", + "paired_hood_model", + "paired_hood_power", + "power_state" + ] +} diff --git a/tests/test_gas_cooktop_tp2x_ks_capabilities.py b/tests/test_gas_cooktop_tp2x_ks_capabilities.py new file mode 100644 index 0000000..7677618 --- /dev/null +++ b/tests/test_gas_cooktop_tp2x_ks_capabilities.py @@ -0,0 +1,55 @@ +"""Tests for the TP2X_DA-KS-COOKTOP-000001 gas cooktop (issue #314). + +The reporter's actual complaint (six advertised burner slots when only +three are physically present) is expected: the registry declares a +generous static superset of slots and gates each on the live options array +per cooktop.py's own comment; there's no per-device signal that +distinguishes an advertised-but-nonexistent slot from a real one, so the +extra entities are disabled by the user, not filtered by the integration. +This file locks in the actual gap from that issue -- /alarms/vs/0 and +/kidslock/vs/0 -- now bound via common.ALARMS/KIDS_LOCK_VS_FALLBACK. +""" + +from custom_components.localthings.registry.adapter import flatten +from custom_components.localthings.registry.by_type import resolve +from custom_components.localthings.registry.discovery import discover +from tests.conftest import _load_device + + +def _bound(): + resources = _load_device("gas_cooktop_tp2x_ks") + reg = resolve(resources) + return discover(resources, reg.capabilities, reg.pattern_capabilities), resources + + +def test_resolves_to_gas_cooktop_registry(): + resources = _load_device("gas_cooktop_tp2x_ks") + reg = resolve(resources) + assert reg is not None and reg.name == "gas_cooktop" + + +def test_no_unbound_hrefs(): + _, resources = _bound() + reg = resolve(resources) + unbound = [] + discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append) + assert unbound == [] + + +def test_alarm_code_and_child_lock_bound(): + bound, resources = _bound() + state = flatten(bound, resources) + assert state["alarm_code"] == "CT_E" + # x.com.samsung.da.kidsLock == "Ready" reads as unlocked, same polarity + # as every other board KIDS_LOCK_VS_FALLBACK covers. + assert state["child_lock"] is True + + +def test_advertises_six_burner_slots_only_three_of_which_are_real(): + """Not a bug to fix -- the board's own /mode/vs/0 options genuinely + list six OperationState slots (issue #314); the reporter's physical + cooktop only has three. Locks in the current, documented behavior.""" + bound, resources = _bound() + state = flatten(bound, resources) + for i in range(6): + assert f"burner_{i}_state" in state diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index e84d0ea..19b0983 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -231,6 +231,25 @@ def test_registry_reproduces_golden_state_keys_for_water_purifier_coffee(): ) +def test_registry_reproduces_golden_state_keys_for_gas_cooktop_tp2x_ks(): + """TP2X_DA-KS-COOKTOP-000001 (issue #314) -- routes via + for_device_by_resources' DeviceType_/OperationState signature, same as + the original cooktop fixture. /alarms/vs/0 and /kidslock/vs/0 were the + dump's only unbound hrefs; both are common.UNIVERSAL shapes this + registry now picks up individually (common.ALARMS, + common.KIDS_LOCK_VS_FALLBACK).""" + from tests.conftest import _load_device + + resources = _load_device("gas_cooktop_tp2x_ks") + golden = json.loads((GOLDEN / "gas_cooktop_tp2x_ks.json").read_text()) + state_keys = _new_state_keys("gas_cooktop_tp2x_ks", 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_cooktop(): from tests.conftest import _load_device