Merge pull request #225 from atc722/agent/qooker-support

Route Samsung Bespoke Qooker to microwave registry
This commit is contained in:
Marc Billow authored and GitHub committed 2026-08-02 12:22:10 -05:00
commit e309d3e7b8
6 files changed
+341 -22

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
{
"state_keys": [
"alarm_code",
"cavity_state",
"child_lock",
"cloud_connected",
"cook_time",
"cooking_mode",
"current_temp_c",
"cycle_active",
"door_open",
"energy_kwh",
"finish_time",
"firmware_update",
"machine_state",
"operation_time_minutes",
"power_level",
"power_switch",
"progress_percentage",
"setpoint",
"sound"
]
}
+183
View File
@@ -0,0 +1,183 @@
{
"meta": {
"model": "TP2X_DA-KS-OVEN-01011 (MW7500A-/KO0)",
"device_type": "microwave",
"source": "local read-only DTLS diagnostics (scrubbed)",
"note": "Samsung Bespoke Qooker declares oic.d.oven and carries an OVEN board token, but its verified local surface exposes MicroWave mode and cavity powerLevel. It binds cleanly to the microwave registry."
},
"device0": [
{
"href": "/configuration/vs/0",
"rep": {
"x.com.samsung.da.clockSyncRequest": "false"
}
},
{
"href": "/connected/vs/0",
"rep": {
"x.com.samsung.da.connected": "On"
}
},
{
"href": "/doors/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Door",
"x.com.samsung.da.openState": "Open"
}
]
}
},
{
"href": "/energy/consumption/vs/0",
"rep": {
"x.com.samsung.da.instantaneousPower": "-500",
"x.com.samsung.da.instantaneousPowerUnit": "W",
"x.com.samsung.da.cumulativePower": "91700",
"x.com.samsung.da.cumulativeUnit": "Wh"
}
},
{
"href": "/information/vs/0",
"rep": {
"x.com.samsung.da.modelNum": "TP2X_DA-KS-OVEN-01011",
"x.com.samsung.da.description": "MW7500A-/KO0",
"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": "240717",
"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": "DE92-04576A_21090900, DE92-04428A_23051100",
"x.com.samsung.da.newVersionAvailable": "0"
}
],
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
"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": "612",
"x.com.samsung.da.diagMinVersion": "1.0"
}
},
{
"href": "/kidslock/vs/0",
"rep": {
"x.com.samsung.da.kidsLock": "Ready"
}
},
{
"href": "/mode/vs/0",
"rep": {
"x.com.samsung.da.supportedModes": [
"NoOperation",
"Defrost",
"MicroWave",
"AutocookCustom",
"ToastSlicedBread",
"ToastCroissant",
"ToastBagle",
"AirFryer",
"Grill",
"Deodorization"
],
"x.com.samsung.da.modes": [
"NoOperation"
],
"x.com.samsung.da.options": [
"DeviceType_MW7500A-/KO0",
"ScreenTimeOut_5min",
"TimeSystem_24",
"Sound_On"
],
"x.com.samsung.da.modeSpec": "[{\"mode\":\"NoOperation\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"NotSupported\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"Defrost\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:30:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"MicroWave\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"01:30:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"700W\",\"powerListLength\":\"3\",\"powerListData\":[\"300W\",\"450W\",\"700W\"]},{\"mode\":\"AutocookCustom\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"ToastSlicedBread\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"00:10:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"ToastCroissant\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"00:10:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"ToastBagle\",\"version\":\"0100\",\"default\":\"NotSupported\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"NotSupported\",\"tempMaxC\":\"NotSupported\",\"tempDefaultC\":\"NotSupported\",\"tempListLengthC\":\"0\",\"tempMinF\":\"NotSupported\",\"tempMaxF\":\"NotSupported\",\"tempDefaultF\":\"NotSupported\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:00:10\",\"timeMax\":\"00:10:00\",\"timeDefault\":\"00:00:30\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\"},{\"mode\":\"AirFLine truncated
}
},
{
"href": "/operational/state/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.operationTime": "00:00:00",
"x.com.samsung.da.remainingTime": "00:00:00",
"x.com.samsung.da.progressPercentage": "0"
}
},
{
"href": "/otninformation/vs/0",
"rep": {
"x.com.samsung.da.target": "",
"x.com.samsung.da.newVersionAvailable": "false"
}
},
{
"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": "OV_E_OFF",
"x.com.samsung.da.triggeredTime": "2026-07-31T04:03:50"
}
]
}
},
{
"href": "/oven/vs/0",
"rep": {
"x.com.samsung.da.state": "Ready",
"x.com.samsung.da.recipe": "00000000000000",
"x.com.samsung.da.powerLevel": "0W"
}
},
{
"href": "/power/vs/0",
"rep": {
"x.com.samsung.da.power": "Off"
}
},
{
"href": "/recipe/cook/vs/0",
"rep": {
"x.com.samsung.da.recipeDisplay": "{\"language\":\"ko_KR\",\"menu\":\"\",\"servingSize\":\"\",\"option\":\"\"}"
}
},
{
"href": "/temperatures/vs/0",
"rep": {
"x.com.samsung.da.items": [
{
"x.com.samsung.da.id": "0",
"x.com.samsung.da.description": "Temperature",
"x.com.samsung.da.desired": "0",
"x.com.samsung.da.current": "1",
"x.com.samsung.da.increment": "5",
"x.com.samsung.da.unit": "Celsius"
}
]
}
}
]
}
+72 -7
View File
@@ -716,22 +716,63 @@ class TestResolve:
assert reg is not None
assert reg.name == 'refrigerator'
def test_prefers_model_strings_over_resource_signature(self, all_device_fixtures):
"""Every fixture with usable model strings resolves the same way
through `resolve` as through `for_device_by_model` directly."""
def test_prefers_resource_signatures_over_model_strings(self, all_device_fixtures):
"""A strong live-resource signature wins over model metadata.
Across the fixture corpus Qooker is the only intentional disagreement:
its OVEN model token says oven while its /oven + MicroWave surface says
microwave. Locking the disagreement set keeps resource-first routing
from silently becoming greedy as new signatures or fixtures land.
"""
from custom_components.localthings.registry.by_type import (
resolve, for_device_by_model,
resolve, for_device_by_model, for_device_by_resources,
)
disagreements = {}
for name, resources in all_device_fixtures.items():
info = resources.get('/information/vs/0', {})
by_resources = for_device_by_resources(resources)
by_model = for_device_by_model(
info.get('x.com.samsung.da.modelNum', ''),
info.get('x.com.samsung.da.description', ''),
)
if by_model is not None:
assert resolve(resources) is by_model, name
if by_resources is None or by_model is None:
continue
assert resolve(resources) is by_resources, name
if by_resources is not by_model:
disagreements[name] = (by_resources.name, by_model.name)
def test_falls_back_to_resource_signature(self, all_device_fixtures):
assert disagreements == {
'qooker_mw7500a': ('microwave', 'oven'),
}
def test_qooker_microwave_surface_overrides_generic_oven_oic_type(self):
"""MW7500A declares oic.d.oven and carries an OVEN board token, but
its verified local API exposes MicroWave mode on an oven cavity. That
strong two-resource signature must win ahead of generic OIC metadata."""
from custom_components.localthings.registry.by_type import (
for_device_by_model,
for_device_by_oic_type,
for_device_by_resources,
resolve,
)
from tests.conftest import _load_device
resources = _load_device('qooker_mw7500a')
info = resources['/information/vs/0']
by_resources = for_device_by_resources(resources)
by_oic = for_device_by_oic_type(('oic.wk.d', 'oic.d.oven'))
by_model = for_device_by_model(
info['x.com.samsung.da.modelNum'],
info['x.com.samsung.da.description'],
)
reg = resolve(resources, device_types=('oic.wk.d', 'oic.d.oven'))
assert by_resources is not None and by_resources.name == 'microwave'
assert by_oic is not None and by_oic.name == 'oven'
assert by_model is not None and by_model.name == 'oven'
assert reg is by_resources
def test_resource_signature_types_dumps_without_information(self, all_device_fixtures):
"""The three dumps with no /information/vs/0 still type."""
from custom_components.localthings.registry.by_type import resolve
for name in ('cooktop', 'range_ne63a6511', 'range_no_info'):
@@ -838,3 +879,27 @@ class TestForDeviceByResources:
assert reg is not None
assert reg.name == 'microwave'
def test_microwave_mode_without_oven_cavity_is_not_matched(self):
"""The mode vocabulary alone is too common to preempt metadata."""
from custom_components.localthings.registry.by_type import for_device_by_resources
resources = {
'/mode/vs/0': {
'x.com.samsung.da.supportedModes': ['MicroWave'],
},
}
assert for_device_by_resources(resources) is None
def test_scalar_supported_modes_is_not_a_microwave_signature(self):
"""Malformed scalar data must not gain resource-first precedence."""
from custom_components.localthings.registry.by_type import for_device_by_resources
resources = {
'/mode/vs/0': {
'x.com.samsung.da.supportedModes': 'MicroWave',
},
'/oven/vs/0': {'x.com.samsung.da.state': 'Ready'},
}
assert for_device_by_resources(resources) is None
+16
View File
@@ -686,6 +686,22 @@ def test_registry_reproduces_golden_state_keys_for_microwave_mw7300b():
)
def test_registry_reproduces_golden_state_keys_for_qooker_mw7500a():
"""Bespoke Qooker MW7500A uses Samsung's OVEN board/type metadata but
proves its microwave semantics through MicroWave mode plus cavity
powerLevel. The routing correction must expose microwave state keys
without the misleading oven_mode/oven_state entities."""
from tests.conftest import _load_device
resources = _load_device('qooker_mw7500a')
golden = json.loads((GOLDEN / 'qooker_mw7500a.json').read_text())
state_keys = _new_state_keys('qooker_mw7500a', 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_microwave_me7500d():
"""TP1X_DA-KS-MICROWAVE-01051 plain microwave (model ME7500D, issues
#137/#142) -- the same microwave registry as MW7300B above, but this
+25 -1
View File
@@ -1,6 +1,9 @@
"""Unit tests for the microwave-family capabilities (issue #121/#66 split
into their own device type instead of being folded into oven.py)."""
from custom_components.localthings.registry.by_type import for_device_by_model
from custom_components.localthings.registry.by_type import (
for_device_by_model,
resolve,
)
from custom_components.localthings.registry.capabilities import microwave
from custom_components.localthings.registry.discovery import discover
@@ -39,6 +42,27 @@ def test_microwave_hood_fan_fixture_resolves_and_has_no_unbound_hrefs():
assert unbound == []
def test_qooker_fixture_resolves_as_microwave_and_has_no_unbound_hrefs():
"""Bespoke Qooker MW7500A is microwave-shaped despite both its OCF type
and internal board token saying oven."""
from tests.conftest import _load_device
resources = _load_device('qooker_mw7500a')
reg = resolve(resources, device_types=('oic.wk.d', 'oic.d.oven'))
assert reg is not None
assert reg.name == 'microwave'
unbound = []
bound = discover(
resources, reg.capabilities, reg.pattern_capabilities,
log=unbound.append,
)
assert unbound == []
keys = {entity.desc.key for entity in bound}
assert {'cooking_mode', 'power_level', 'setpoint'} <= keys
assert 'oven_mode' not in keys
# ---------------------------------------------------------------------------
# MICROWAVE_SETPOINT — NumberDesc with RMW write semantics
# ---------------------------------------------------------------------------