Merge pull request #98 from mbillow/claude/device-support-issue-86-cooktop
Add standalone induction-cooktop support (issue #86)
This commit is contained in:
@@ -4,7 +4,8 @@ from typing import Optional
|
||||
from ._base import DeviceRegistry
|
||||
from . import (
|
||||
air_purifier, airconditioner, cooktop, dehumidifier, dishwasher, dryer,
|
||||
oven, range as _range, range_hood, refrigerator, washer, water_purifier,
|
||||
induction_cooktop, oven, range as _range, range_hood, refrigerator,
|
||||
washer, water_purifier,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
@@ -22,6 +23,7 @@ _REGISTRY_BY_KEY: dict[str, DeviceRegistry] = {
|
||||
'dehumidifier': dehumidifier.REGISTRY,
|
||||
'dishwasher': dishwasher.REGISTRY,
|
||||
'dryer': dryer.REGISTRY,
|
||||
'induction_cooktop': induction_cooktop.REGISTRY,
|
||||
'oven': oven.REGISTRY,
|
||||
'hood': range_hood.REGISTRY,
|
||||
'range': _range.REGISTRY,
|
||||
@@ -198,6 +200,16 @@ def for_device_by_model(model_num: str, description: str) -> Optional[DeviceRegi
|
||||
# oneUiVersion and doesn't match the washer/dryer/dishwasher prefix map.
|
||||
if key is None and '-OVEN-' in (model_num or '').upper():
|
||||
key = 'oven'
|
||||
# Standalone induction cooktops (e.g. TP1X_DA-KS-COOKTOP-01011, issue
|
||||
# #86) -- same board family and '/cooktop/status/vs/0' resource shape
|
||||
# as the range combo above, but no oven attached at all. Distinct from
|
||||
# the '_COOKTOP'/'_GB_CT_' underscore-delimited check above: that one
|
||||
# matches a different, older gas-cooktop family (cooktop.py's NA9300K
|
||||
# class, burner state embedded in /mode/vs/0's options array) whose
|
||||
# modelNum token is underscore-delimited, not hyphenated like this
|
||||
# board family's.
|
||||
if key is None and '-COOKTOP-' in (model_num or '').upper():
|
||||
key = 'induction_cooktop'
|
||||
return _REGISTRY_BY_KEY.get(key) if key else None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Standalone induction-cooktop device registry (Samsung
|
||||
TP1X_DA-KS-COOKTOP-class, issue #86) -- the cooktop half of the
|
||||
range/oven board family (see capabilities/range.py), but with no oven
|
||||
attached at all.
|
||||
|
||||
Reuses range.py's COOKTOP_STATUS/COOKTOP_SPEC/COOKTOP_SAFETY/PROBE_STATUS
|
||||
wholesale (identical resource shapes to the range combo's cooktop half)
|
||||
and cooktop.PAIRED_HOOD_STATUS for the Bluetooth-paired range hood some
|
||||
units pair with. Distinct registry key from cooktop.REGISTRY ('cooktop')
|
||||
-- that family is the unrelated NA9300K-class gas cooktop (burner state
|
||||
embedded in /mode/vs/0's options array, a completely different OCF
|
||||
surface that happens to share the English word "cooktop").
|
||||
"""
|
||||
from ..capabilities import common, cooktop as cooktop_caps, ignored
|
||||
from ..capabilities import range as range_caps
|
||||
from ._base import DeviceRegistry, _build
|
||||
|
||||
REGISTRY = DeviceRegistry(
|
||||
name='induction_cooktop',
|
||||
capabilities=_build([
|
||||
*ignored.IGNORED,
|
||||
*common.UNIVERSAL,
|
||||
*common.POWER,
|
||||
range_caps.COOKTOP_STATUS,
|
||||
range_caps.COOKTOP_SPEC,
|
||||
range_caps.COOKTOP_SAFETY,
|
||||
range_caps.PROBE_STATUS,
|
||||
cooktop_caps.PAIRED_HOOD_STATUS,
|
||||
]),
|
||||
)
|
||||
@@ -131,4 +131,12 @@ IGNORED: list[Capability] = [
|
||||
# parallel write-capable capability around an unverified generic OCF
|
||||
# write contract.
|
||||
Capability(href='/operational/state/0'),
|
||||
|
||||
# Cooktop guided-cooking/recipe status (issue #86, TP1X_DA-KS-COOKTOP
|
||||
# family): sequenceNumber, operationBurnerNumber, a stageInfo block, and
|
||||
# a textData.menu string -- every field empty/zero on the only dump
|
||||
# seen so far (device idle, no guided-cooking program active). Same
|
||||
# "don't guess" treatment as the microwave family's /recipe/cook/vs/0.
|
||||
# Revisit if a dump with an active recipe surfaces.
|
||||
Capability(href='/cooktop/recipe/status/vs/0'),
|
||||
]
|
||||
|
||||
@@ -24,7 +24,8 @@ write pattern already proven safe elsewhere in this codebase (oven setpoint,
|
||||
icemaker toggles).
|
||||
"""
|
||||
from ..capability import Capability
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc
|
||||
from ..entities import BinarySensorDesc, SelectDesc, SensorDesc, SwitchDesc
|
||||
from .common import normalize_temp_unit
|
||||
|
||||
# Observed as high as 4 (this issue's dump); user-reported hardware with 5
|
||||
# burners exists. Kept a little above both since exists_fn gates unused
|
||||
@@ -52,6 +53,10 @@ def _burner_hot_surface_fn(i):
|
||||
return lambda burner_list: get_state(burner_list) not in (None, 'normal')
|
||||
|
||||
|
||||
def _burner_pan_detection_fn(i):
|
||||
return lambda burner_list: bool(_burner_field_fn(i, 'panDetection')(burner_list))
|
||||
|
||||
|
||||
def _power_level_options(resources):
|
||||
spec = resources.get('/cooktop/spec/vs/0') or {}
|
||||
return list(spec.get('supportedPowerLevelList') or [])
|
||||
@@ -100,15 +105,44 @@ def _burner_entities(i):
|
||||
translation_placeholders={'number': str(n)},
|
||||
exists_fn=exists,
|
||||
value_fn=_burner_hot_surface_fn(i)),
|
||||
BinarySensorDesc(key=f'burner_{i}_pan_detected', field='burnerList',
|
||||
icon='mdi:pot',
|
||||
translation_key='burner_pan_detected',
|
||||
translation_placeholders={'number': str(n)},
|
||||
entity_category='diagnostic',
|
||||
exists_fn=exists,
|
||||
value_fn=_burner_pan_detection_fn(i)),
|
||||
)
|
||||
|
||||
|
||||
def _child_lock_write(p, rep, href=None):
|
||||
if p not in ('On', 'Off'):
|
||||
return None
|
||||
return ['cooktop', 'status', 'vs', '0'], {'childLock': p.lower()}
|
||||
|
||||
|
||||
COOKTOP_STATUS = Capability(
|
||||
href='/cooktop/status/vs/0',
|
||||
poll_tier='hot',
|
||||
entities=(
|
||||
SensorDesc(key='cooktop_state', field='operationState',
|
||||
icon='mdi:pot-steam'),
|
||||
# The cooktop section's own on/off (issue #86) -- distinct from
|
||||
# common.POWER's /power/0 or /power/vs/0, which some boards in this
|
||||
# family (the range combo) additionally carry for the whole
|
||||
# appliance. Read-only: no live device to confirm remotely turning
|
||||
# a cooktop on wouldn't leave a burner active unattended.
|
||||
BinarySensorDesc(key='cooktop_power', field='power',
|
||||
device_class='power', icon='mdi:pot-steam',
|
||||
value_fn=lambda v: str(v).lower() == 'on'),
|
||||
# Safe to write -- a lock toggle, not a heat control -- via a
|
||||
# direct single-field PUT (no RMW needed; unlike burnerList this
|
||||
# is a lone scalar, not an array of siblings to preserve).
|
||||
SwitchDesc(key='cooktop_child_lock', field='childLock',
|
||||
device_class='lock', entity_category='config',
|
||||
icon='mdi:lock',
|
||||
value_fn=lambda v: str(v).lower() == 'on',
|
||||
write_fn=_child_lock_write),
|
||||
*[e for i in range(MAX_BURNERS) for e in _burner_entities(i)],
|
||||
),
|
||||
)
|
||||
@@ -134,6 +168,30 @@ COOKTOP_SAFETY = Capability(
|
||||
),
|
||||
)
|
||||
|
||||
# Bluetooth meat probe (issue #86). All-idle sentinel values when
|
||||
# disconnected (operationBurnerNumber -1, temperatures 0) -- no special
|
||||
# gating on those, matching cooktop.PAIRED_HOOD_STATUS's own precedent of
|
||||
# showing a disconnected accessory's fields plainly rather than hiding the
|
||||
# whole capability.
|
||||
PROBE_STATUS = Capability(
|
||||
href='/bluetooth/probe/status/vs/0',
|
||||
poll_tier='warm',
|
||||
entities=(
|
||||
BinarySensorDesc(key='probe_connected', field='connectionState',
|
||||
device_class='connectivity',
|
||||
value_fn=lambda v: str(v).lower() == 'connected'),
|
||||
SensorDesc(key='probe_battery', field='batteryPercentage',
|
||||
device_class='battery', state_class='measurement',
|
||||
unit='%', entity_category='diagnostic'),
|
||||
SensorDesc(key='probe_temperature', field='currentTemperature',
|
||||
device_class='temperature', state_class='measurement',
|
||||
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
|
||||
SensorDesc(key='probe_target_temperature', field='targetTemperature',
|
||||
device_class='temperature', entity_category='diagnostic',
|
||||
unit_fn=lambda rep: normalize_temp_unit(rep.get('temperatureUnit'), '°C')),
|
||||
),
|
||||
)
|
||||
|
||||
# Some range boards (issue #74's NE63B8411SS) report no /cooktop/status/vs/0
|
||||
# burner array at all -- their local API only exposes this coarse
|
||||
# monitoring resource for the cooktop half, with no per-burner detail.
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
"burner_hot_surface": {
|
||||
"name": "Burner {number} hot surface"
|
||||
},
|
||||
"burner_pan_detected": {
|
||||
"name": "Burner {number} pan detected"
|
||||
},
|
||||
"cloud_connected": {
|
||||
"name": "Cloud connected"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Cooktop power"
|
||||
},
|
||||
"cooktop_safety_shutoff_enabled": {
|
||||
"name": "Hot surface auto-shutoff enabled"
|
||||
},
|
||||
@@ -61,6 +67,9 @@
|
||||
"power_state": {
|
||||
"name": "Power state"
|
||||
},
|
||||
"probe_connected": {
|
||||
"name": "Probe connected"
|
||||
},
|
||||
"power_switch": {
|
||||
"name": "Power"
|
||||
},
|
||||
@@ -626,6 +635,15 @@
|
||||
"power_watts": {
|
||||
"name": "Power"
|
||||
},
|
||||
"probe_battery": {
|
||||
"name": "Probe battery"
|
||||
},
|
||||
"probe_target_temperature": {
|
||||
"name": "Probe target temperature"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"name": "Probe temperature"
|
||||
},
|
||||
"progress": {
|
||||
"name": "Progress"
|
||||
},
|
||||
@@ -726,6 +744,9 @@
|
||||
"coldwater_lock": {
|
||||
"name": "Cold water lock"
|
||||
},
|
||||
"cooktop_child_lock": {
|
||||
"name": "Child lock"
|
||||
},
|
||||
"defrost_delay": {
|
||||
"name": "Defrost delay"
|
||||
},
|
||||
|
||||
@@ -10,9 +10,15 @@
|
||||
"burner_hot_surface": {
|
||||
"name": "Brander {number} heet oppervlak"
|
||||
},
|
||||
"burner_pan_detected": {
|
||||
"name": "Brander {number} pan gedetecteerd"
|
||||
},
|
||||
"cloud_connected": {
|
||||
"name": "Verbonden met cloud"
|
||||
},
|
||||
"cooktop_power": {
|
||||
"name": "Kookplaat voeding"
|
||||
},
|
||||
"cooktop_safety_shutoff_enabled": {
|
||||
"name": "Automatische uitschakeling heet oppervlak ingeschakeld"
|
||||
},
|
||||
@@ -61,6 +67,9 @@
|
||||
"power_state": {
|
||||
"name": "Voedingsstatus"
|
||||
},
|
||||
"probe_connected": {
|
||||
"name": "Sonde verbonden"
|
||||
},
|
||||
"power_switch": {
|
||||
"name": "Voeding"
|
||||
},
|
||||
@@ -626,6 +635,15 @@
|
||||
"power_watts": {
|
||||
"name": "Vermogen"
|
||||
},
|
||||
"probe_battery": {
|
||||
"name": "Sonde batterij"
|
||||
},
|
||||
"probe_target_temperature": {
|
||||
"name": "Doeltemperatuur sonde"
|
||||
},
|
||||
"probe_temperature": {
|
||||
"name": "Temperatuur sonde"
|
||||
},
|
||||
"progress": {
|
||||
"name": "Voortgang"
|
||||
},
|
||||
@@ -726,6 +744,9 @@
|
||||
"coldwater_lock": {
|
||||
"name": "Koudwaterslot"
|
||||
},
|
||||
"cooktop_child_lock": {
|
||||
"name": "Kinderslot"
|
||||
},
|
||||
"defrost_delay": {
|
||||
"name": "Ontdooien uitstellen"
|
||||
},
|
||||
|
||||
+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