Merge pull request #225 from atc722/agent/qooker-support
Route Samsung Bespoke Qooker to microwave registry
This commit is contained in:
6 files changed
+341
-22
No files matched your search
+23
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user