Merge pull request #98 from mbillow/claude/device-support-issue-86-cooktop
Add standalone induction-cooktop support (issue #86)
This commit is contained in:
13 files changed
+569
-2
No files matched your search
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"burner_0_hot_surface",
|
||||
"burner_0_pan_detected",
|
||||
"burner_0_power_level",
|
||||
"burner_0_state",
|
||||
"burner_1_hot_surface",
|
||||
"burner_1_pan_detected",
|
||||
"burner_1_power_level",
|
||||
"burner_1_state",
|
||||
"burner_2_hot_surface",
|
||||
"burner_2_pan_detected",
|
||||
"burner_2_power_level",
|
||||
"burner_2_state",
|
||||
"cooktop_child_lock",
|
||||
"cooktop_power",
|
||||
"cooktop_state",
|
||||
"energy_kwh",
|
||||
"firmware_update",
|
||||
"paired_hood_connected",
|
||||
"paired_hood_fan_speed",
|
||||
"paired_hood_firmware",
|
||||
"paired_hood_light",
|
||||
"paired_hood_model",
|
||||
"paired_hood_power",
|
||||
"probe_battery",
|
||||
"probe_connected",
|
||||
"probe_target_temperature",
|
||||
"probe_temperature"
|
||||
]
|
||||
}
|
||||
Vendored
+6
@@ -2,20 +2,26 @@
|
||||
"state_keys": [
|
||||
"alarm_code",
|
||||
"burner_0_hot_surface",
|
||||
"burner_0_pan_detected",
|
||||
"burner_0_power_level",
|
||||
"burner_0_state",
|
||||
"burner_1_hot_surface",
|
||||
"burner_1_pan_detected",
|
||||
"burner_1_power_level",
|
||||
"burner_1_state",
|
||||
"burner_2_hot_surface",
|
||||
"burner_2_pan_detected",
|
||||
"burner_2_power_level",
|
||||
"burner_2_state",
|
||||
"burner_3_hot_surface",
|
||||
"burner_3_pan_detected",
|
||||
"burner_3_power_level",
|
||||
"burner_3_state",
|
||||
"child_lock",
|
||||
"cloud_connected",
|
||||
"cook_time",
|
||||
"cooktop_child_lock",
|
||||
"cooktop_power",
|
||||
"cooktop_safety_shutoff_enabled",
|
||||
"cooktop_state",
|
||||
"current_temp_c",
|
||||
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"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.s"
|
||||
],
|
||||
"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_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-26T06:30:18"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/bluetooth/hood/status/vs/0",
|
||||
"rep": {
|
||||
"connectionState": "disconnected",
|
||||
"micomModelId": "",
|
||||
"firmwareVersion": "",
|
||||
"power": "off",
|
||||
"fanSpeed": 0,
|
||||
"lampState": "off",
|
||||
"timer": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/bluetooth/probe/status/vs/0",
|
||||
"rep": {
|
||||
"connectionState": "disconnected",
|
||||
"batteryState": "normal",
|
||||
"batteryPercentage": 0,
|
||||
"operationBurnerNumber": -1,
|
||||
"temperatureUnit": "C",
|
||||
"targetTemperature": 0,
|
||||
"currentTemperature": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/recipe/status/vs/0",
|
||||
"rep": {
|
||||
"sequenceNumber": 160,
|
||||
"operationBurnerNumber": 2,
|
||||
"stageInfo": {
|
||||
"numberOfStages": 0,
|
||||
"cookingTimeList": []
|
||||
},
|
||||
"textData": {
|
||||
"menu": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/spec/vs/0",
|
||||
"rep": {
|
||||
"deviceType": "cooktop",
|
||||
"micomModelId": "NV8500T-/KO4",
|
||||
"supportedFeatureList": [
|
||||
"kitchenService"
|
||||
],
|
||||
"numberOfBurners": 3,
|
||||
"supportedBurnerList": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"supportedPowerLevelList": [
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"boost",
|
||||
"0",
|
||||
"keepWarm"
|
||||
],
|
||||
"supportedRecipeProtocolVersion": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/cooktop/status/vs/0",
|
||||
"rep": {
|
||||
"sequenceNumber": 3214,
|
||||
"power": "off",
|
||||
"childLock": "off",
|
||||
"operationState": "ready",
|
||||
"additionalStateList": [
|
||||
""
|
||||
],
|
||||
"burnerList": [
|
||||
{
|
||||
"burnerNumber": 0,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"burnerNumber": 1,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"burnerNumber": 2,
|
||||
"mode": "normal",
|
||||
"operationState": "ready",
|
||||
"powerLevel": "0",
|
||||
"hotSurfaceState": "normal",
|
||||
"panDetection": true,
|
||||
"timer": {
|
||||
"operationState": "ready",
|
||||
"cookingTime": 0,
|
||||
"remainingTime": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.instantaneousPower": "-500",
|
||||
"x.com.samsung.da.instantaneousPowerUnit": "W",
|
||||
"x.com.samsung.da.cumulativePower": "212700",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "TP1X_DA-KS-COOKTOP-01011|40459741|50000203001711000A00000000000000",
|
||||
"x.com.samsung.da.description": "NV8500T-/KO4",
|
||||
"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": "Version",
|
||||
"x.com.samsung.da.type": "Software",
|
||||
"x.com.samsung.da.number": "26033000",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "1",
|
||||
"x.com.samsung.da.description": "Version",
|
||||
"x.com.samsung.da.type": "Firmware",
|
||||
"x.com.samsung.da.number": "04597A23021100, 04596A23080300",
|
||||
"x.com.samsung.da.newVersionAvailable": "0"
|
||||
}
|
||||
],
|
||||
"x.com.samsung.da.diagProtocolType": "BLE_OCF",
|
||||
"x.com.samsung.da.diagMinVersion": "3.0",
|
||||
"x.com.samsung.da.diagLogType": [
|
||||
"errCode",
|
||||
"dump"
|
||||
],
|
||||
"x.com.samsung.da.diagDumpType": "file",
|
||||
"x.com.samsung.da.diagEndPoint": "SSM",
|
||||
"x.com.samsung.da.diagMnid": "0AJT",
|
||||
"x.com.samsung.da.diagSetupid": "KCA",
|
||||
"x.com.samsung.da.diagTsId": "DA01"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false",
|
||||
"x.com.samsung.da.newVersionNo": "00000000",
|
||||
"x.com.samsung.da.currentVersionInfo": "00000000",
|
||||
"flashingProgress": "",
|
||||
"otnTarget": "main",
|
||||
"otnStatus": "None",
|
||||
"otnCompleteDate": "noHistory",
|
||||
"otnList": [
|
||||
{
|
||||
"type": "WIFI",
|
||||
"modelId": "AKS-WW-TP1-21-COOKTOP-2",
|
||||
"versions": [
|
||||
"40260330"
|
||||
],
|
||||
"visVersion": "260330"
|
||||
},
|
||||
{
|
||||
"type": "Micom",
|
||||
"modelId": "05114045974140459641",
|
||||
"versions": [
|
||||
"23021100",
|
||||
"23080300"
|
||||
],
|
||||
"visVersion": "230803"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/timezone/vs/0",
|
||||
"rep": {
|
||||
"timezoneid": "Asia/Seoul",
|
||||
"offset": "+09:00",
|
||||
"DST": "OFF"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -351,6 +351,28 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'oven'
|
||||
|
||||
def test_induction_cooktop_via_hyphenated_cooktop_token(self):
|
||||
"""Issue #86: a standalone induction cooktop (NV8500T-/KO4) reports
|
||||
no oneUiVersion and an unrecognized consumer token ('NV'); it falls
|
||||
back to the hyphenated '-COOKTOP-' token in modelNum -- distinct
|
||||
from the underscore-delimited '_COOKTOP'/'_GB_CT_' check, which is
|
||||
the unrelated older NA9300K gas-cooktop family."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'TP1X_DA-KS-COOKTOP-01011|40459741|50000203001711000A00000000000000',
|
||||
'NV8500T-/KO4',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'induction_cooktop'
|
||||
|
||||
def test_hyphenated_cooktop_token_not_confused_with_range(self):
|
||||
"""'-COOKTOP-' and '-RANGE-' must route to distinct registries even
|
||||
though both are TP1X_DA-KS- board-family siblings."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model('TP1X_DA-KS-COOKTOP-01011', 'NV8500T-/KO4')
|
||||
assert reg is not None
|
||||
assert reg.name != 'range'
|
||||
|
||||
def test_unknown_model_returns_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD')
|
||||
|
||||
@@ -385,6 +385,17 @@ def test_registry_reproduces_golden_state_keys_for_range():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_induction_cooktop():
|
||||
"""Standalone induction cooktop, no oven attached (model
|
||||
TP1X_DA-KS-COOKTOP-01011, issue #86) -- reports no oneUiVersion and a
|
||||
hyphenated '-COOKTOP-' modelNum token, resolved via
|
||||
for_device_by_model into its own 'induction_cooktop' registry (not
|
||||
cooktop.REGISTRY, which is the unrelated NA9300K gas-cooktop family)."""
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('induction_cooktop')
|
||||
golden = json.loads((GOLDEN / 'induction_cooktop.json').read_text())
|
||||
state_keys = _new_state_keys('induction_cooktop', resources)
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_no_info():
|
||||
"""NE63B8411SS (issue #74) -- reports no oneUiVersion *and* no
|
||||
/information/vs/0 at all, so neither for_device nor
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Tests for the standalone induction-cooktop registry (issue #86)."""
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model, induction_cooktop
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _cooktop():
|
||||
resources = _load_device('induction_cooktop')
|
||||
info = resources['/information/vs/0']
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
|
||||
def _state():
|
||||
reg, resources = _cooktop()
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_model_resolves_to_induction_cooktop_registry():
|
||||
reg, _ = _cooktop()
|
||||
assert reg is not None and reg.name == 'induction_cooktop'
|
||||
assert reg is induction_cooktop.REGISTRY
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
"""Every resource in the issue #86 dump binds or is ignored -- clears
|
||||
the coverage-gap repair."""
|
||||
reg, resources = _cooktop()
|
||||
unbound = []
|
||||
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_expected_entities_present():
|
||||
state = _state()
|
||||
for key in (
|
||||
'cooktop_state', 'cooktop_power', 'cooktop_child_lock',
|
||||
'burner_0_power_level', 'burner_0_state', 'burner_0_hot_surface',
|
||||
'burner_0_pan_detected', 'burner_2_pan_detected',
|
||||
'paired_hood_connected', 'probe_connected', 'probe_temperature',
|
||||
'energy_kwh', 'alarm_code',
|
||||
):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_unreported_burners_gated_out():
|
||||
"""Dump reports numberOfBurners=3 (indices 0-2) -- burner slot 3+ of
|
||||
range.MAX_BURNERS must not appear as entities."""
|
||||
state = _state()
|
||||
assert 'burner_3_power_level' not in state
|
||||
|
||||
|
||||
def test_recipe_status_href_is_ignored_not_unbound():
|
||||
"""/cooktop/recipe/status/vs/0 is idle/empty on this dump -- covered by
|
||||
ignored.py, not modeled as an entity (same treatment as the microwave
|
||||
family's /recipe/cook/vs/0)."""
|
||||
reg, _ = _cooktop()
|
||||
ignored_hrefs = {cap.href for caps in reg.capabilities.values() for cap in caps
|
||||
if cap.entities == ()}
|
||||
assert '/cooktop/recipe/status/vs/0' in ignored_hrefs
|
||||
@@ -82,3 +82,41 @@ def test_burner_hot_surface_true_when_not_normal():
|
||||
if e.key == 'burner_0_hot_surface')
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'hot'}]) is True
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'hotSurfaceState': 'normal'}]) is False
|
||||
|
||||
|
||||
def test_burner_pan_detected(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities
|
||||
if e.key == 'burner_0_pan_detected')
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'panDetection': True}]) is True
|
||||
assert desc.value_fn([{'burnerNumber': 0, 'panDetection': False}]) is False
|
||||
assert desc.value_fn([{'burnerNumber': 1, 'panDetection': True}]) is False # different burner
|
||||
|
||||
|
||||
def test_cooktop_power_is_read_only(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities if e.key == 'cooktop_power')
|
||||
assert desc.value_fn('On') is True
|
||||
assert desc.value_fn('Off') is False
|
||||
assert not hasattr(desc, 'write_fn') # BinarySensorDesc has no write path at all
|
||||
|
||||
|
||||
def test_cooktop_child_lock_write(): # issue #86
|
||||
desc = next(e for e in range_caps.COOKTOP_STATUS.entities if e.key == 'cooktop_child_lock')
|
||||
assert desc.value_fn('on') is True
|
||||
assert desc.value_fn('off') is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['cooktop', 'status', 'vs', '0']
|
||||
assert body == {'childLock': 'on'}
|
||||
assert desc.write_fn('Bogus', {}) is None
|
||||
|
||||
|
||||
def test_probe_status_temperature_unit_reads_live_field(): # issue #86
|
||||
desc = next(e for e in range_caps.PROBE_STATUS.entities if e.key == 'probe_temperature')
|
||||
assert desc.unit_fn({'temperatureUnit': 'C'}) == '°C'
|
||||
assert desc.unit_fn({'temperatureUnit': 'F'}) == '°F'
|
||||
assert desc.unit_fn({}) == '°C'
|
||||
|
||||
|
||||
def test_probe_status_connected_value_fn(): # issue #86
|
||||
desc = next(e for e in range_caps.PROBE_STATUS.entities if e.key == 'probe_connected')
|
||||
assert desc.value_fn('Connected') is True
|
||||
assert desc.value_fn('Disconnected') is False
|
||||
Reference in new issue
Block a user