diff --git a/custom_components/localthings/registry/by_type/__init__.py b/custom_components/localthings/registry/by_type/__init__.py index 25165e2..063d739 100644 --- a/custom_components/localthings/registry/by_type/__init__.py +++ b/custom_components/localthings/registry/by_type/__init__.py @@ -225,6 +225,7 @@ _OIC_TYPE_TO_KEY: dict[str, str] = { "oic.d.dishwasher": "dishwasher", "oic.d.dryer": "dryer", "oic.d.oven": "oven", + "oic.d.range": "range", # issue #324 -- oven+cooktop combo, no /information/vs/0 "oic.d.refrigerator": "refrigerator", "oic.d.krefrigerator": "refrigerator", # issue #328 -- kimchi refrigerator "oic.d.washer": "washer", diff --git a/tests/conftest.py b/tests/conftest.py index b0054f4..aa466fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -70,7 +70,7 @@ class FakeCoapSession: pass -def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]): +def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list], device_types=()): """Run the *whole* subdevice-aware discovery pipeline against fixture data, HA-free -- mirrors exactly what LocalThingsCoordinator does across _enumerate_subdevices_blocking + _run_discovery (issue #177), so a test @@ -78,6 +78,12 @@ def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]): it. See the adding-device-support skill's section 2 for the plain (non-subdevice) equivalent this extends. + `device_types` is the master's own /oic/d `rt` (see + discover_partitioned's `oic_device_types` param) -- only needed for a + board with no /information/vs/0 at all to route from (issue #324's + range, whose modelNum-based fallback has nothing to read), so it + defaults to () for every fixture that resolves by board token instead. + Returns `(bound, materialized, skipped, full_resources, device_type_name)`: - `bound`: every BoundEntity, main + every materialized subdevice. - `materialized`/`skipped`: Subdevice / SkippedSubdevice lists straight from @@ -101,6 +107,7 @@ def _discover_full(resources: dict[str, dict], oic_res, seeds: dict[str, list]): candidates, resolve, CAPABILITIES, + oic_device_types=device_types, ) return bound, materialized, skipped, full_resources, device_type_name diff --git a/tests/fixtures/golden/range_tp1x_da_ks_range_0101x.json b/tests/fixtures/golden/range_tp1x_da_ks_range_0101x.json new file mode 100644 index 0000000..a6585fa --- /dev/null +++ b/tests/fixtures/golden/range_tp1x_da_ks_range_0101x.json @@ -0,0 +1,39 @@ +{ + "state_keys": [ + "alarm_code", + "child_lock", + "cloud_connected", + "cook_time", + "cooktop_on_alert", + "cooktop_running_state", + "current_temp_c", + "cycle_active", + "door_open", + "energy_saving", + "finish_time", + "firmware_update", + "lamp", + "machine_state", + "operation_time_minutes", + "oven_mode", + "oven_setpoint", + "oven_state", + "power_switch", + "progress_percentage", + "remote_control", + "sound", + "subdevice1_cloud_connected", + "subdevice1_cook_time", + "subdevice1_current_temp_c", + "subdevice1_cycle_active", + "subdevice1_finish_time", + "subdevice1_machine_state", + "subdevice1_operation_time_minutes", + "subdevice1_oven_mode", + "subdevice1_oven_setpoint", + "subdevice1_oven_state", + "subdevice1_progress_percentage", + "subdevice1_sound", + "warming_center_state" + ] +} \ No newline at end of file diff --git a/tests/fixtures/range_tp1x_da_ks_range_0101x_device.json b/tests/fixtures/range_tp1x_da_ks_range_0101x_device.json new file mode 100644 index 0000000..56b24b5 --- /dev/null +++ b/tests/fixtures/range_tp1x_da_ks_range_0101x_device.json @@ -0,0 +1,470 @@ +{ + "device0": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "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": "On", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "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-08-07T15:56:50" + } + ] + } + }, + { + "href": "/kidslock/vs/0", + "rep": { + "x.com.samsung.da.kidsLock": "Ready" + } + }, + { + "href": "/configuration/vs/0", + "rep": {} + }, + { + "href": "/oven/vs/0", + "rep": { + "x.com.samsung.da.state": "Ready", + "rt": [ + "x.com.samsung.da.oven" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/remotectrl/vs/0", + "rep": { + "x.com.samsung.da.remoteControlEnabled": "false", + "rt": [ + "x.com.samsung.da.configuration" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "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": "175", + "x.com.samsung.da.increment": "5", + "x.com.samsung.da.unit": "Fahrenheit" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.desired": "0", + "x.com.samsung.da.current": "0", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Fahrenheit" + } + ], + "rt": [ + "x.com.samsung.da.temperatures" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/mode/vs/0", + "rep": { + "x.com.samsung.da.supportedModes": [ + "UpperBroil", + "UpperConvectionBake", + "UpperConvectionRoast" + ], + "x.com.samsung.da.modes": [ + "NoOperation" + ], + "x.com.samsung.da.options": [ + "DeviceType_NE9801T-/AA0", + "SettingPossible_7", + "meatprobe_disconnected", + "UpperLamp_Off", + "Sound_Off", + "AdjustingTemp_0", + "Sabbath_Off", + "EnergySaving_Off", + "BurnerOnAlert_Off" + ], + "x.com.samsung.da.defaultMode": "UpperConvectionBake", + "x.com.samsung.da.modeSpec": "[{\"mode\":\"Bake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"Broil\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"61441\",\"tempMaxC\":\"61442\",\"tempDefaultC\":\"61441\",\"tempListLengthC\":\"2\",\"tempListDataC\":[\"61441\",\"61442\"],\"tempMinF\":\"61441\",\"tempMaxF\":\"61442\",\"tempDefaultF\":\"61441\",\"tempListLengthF\":\"2\",\"tempListDataF\":[\"61441\",\"61442\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionBake\",\"version\":\"0100\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"ConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"285\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"550\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"KeepWarm\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"80\",\"tempMaxC\":\"80\",\"tempDefaultC\":\"80\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"80\"],\"tempMinF\":\"175\",\"tempMaxF\":\"175\",\"tempDefaultF\":\"175\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"175\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"BreadProof\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Single\",\"tempMinC\":\"35\",\"tempMaxC\":\"35\",\"tempDefaultC\":\"35\",\"tempListLengthC\":\"1\",\"tempListDataC\":[\"35\"],\"tempMinF\":\"95\",\"tempMaxF\":\"95\",\"tempDefaultF\":\"95\",\"tempListLengthF\":\"1\",\"tempListDataF\":[\"95\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"AirFryer\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"175\",\"tempMaxC\":\"260\",\"tempDefaultC\":\"220\",\"tempListLengthC\":\"0\",\"tempMinF\":\"350\",\"tempMaxF\":\"500\",\"tempDefaultF\":\"425\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"Dehydrate\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Single\",\"tempMinC\":\"40\",\"tempMaxC\":\"105\",\"tempDefaultC\":\"65\",\"tempListLengthC\":\"0\",\"tempMinF\":\"100\",\"tempMaxF\":\"225\",\"tempDefaultF\":\"150\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"UpperBroil\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Setting\",\"cavity\":\"Upper\",\"tempMinC\":\"61441\",\"tempMaxC\":\"61442\",\"tempDefaultC\":\"61441\",\"tempListLengthC\":\"2\",\"tempListDataC\":[\"61441\",\"61442\"],\"tempMinF\":\"61441\",\"tempMaxF\":\"61442\",\"tempDefaultF\":\"61441\",\"tempListLengthF\":\"2\",\"tempListDataF\":[\"61441\",\"61442\"],\"timeMin\":\"NotSupported\",\"timeMax\":\"NotSupported\",\"timeDefault\":\"NotSupported\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"UpperConvectionBake\",\"version\":\"0100\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Upper\",\"tempMinC\":\"80\",\"tempMaxC\":\"250\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"480\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"UpperConvectionRoast\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Upper\",\"tempMinC\":\"80\",\"tempMaxC\":\"250\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"480\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"LowerBake\",\"version\":\"0100\",\"default\":\"Normal\",\"control\":\"Start&Setting\",\"cavity\":\"Lower\",\"tempMinC\":\"80\",\"tempMaxC\":\"250\",\"tempDefaultC\":\"175\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"480\",\"tempDefaultF\":\"350\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"LowerConvectionBake\",\"version\":\"0100\",\"default\":\"Default\",\"control\":\"Start&Setting\",\"cavity\":\"Lower\",\"tempMinC\":\"80\",\"tempMaxC\":\"250\",\"tempDefaultC\":\"160\",\"tempListLengthC\":\"0\",\"tempMinF\":\"175\",\"tempMaxF\":\"480\",\"tempDefaultF\":\"325\",\"tempListLengthF\":\"0\",\"timeMin\":\"00:01:00\",\"timeMax\":\"09:59:00\",\"timeDefault\":\"01:00:00\",\"probeMinC\":\"NotSupported\",\"probeMaxC\":\"NotSupported\",\"probeDefaultC\":\"NotSupported\",\"probeMinF\":\"NotSupported\",\"probeMaxF\":\"NotSupported\",\"probeDefaultF\":\"NotSupported\",\"powerDefault\":\"NotSupported\",\"powerListLength\":\"0\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"SelfClean\",\"version\":\"0100\",\"default\":\"Normal\",\"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\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"},{\"mode\":\"SteamClean\",\"version\":\"0100\",\"default\":\"Normal\",\"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\",\"tempIntervalC\":\"0\",\"tempIntervalF\":\"0\",\"probeIntervalC\":\"0\",\"probeIntervalF\":\"0\"}]", + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "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": "1", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "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": "Close", + "x.com.samsung.da.lock": "Unlock" + } + ], + "rt": [ + "x.com.samsung.da.doors" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/connected/vs/0", + "rep": { + "x.com.samsung.da.connected": "On" + } + }, + { + "href": "/cooktopmonitoring/vs/0", + "rep": { + "x.com.samsung.da.warmingCenterState": "Off", + "x.com.samsung.da.cooktopMonitoring": "0", + "x.com.samsung.da.cooktopRunningState": "Ready", + "supportedHoodLampStateList": [ + "Run", + "Ready" + ] + } + } + ], + "oic_res": [ + { + "di": "**REDACTED**", + "links": [ + { + "href": "/oic/sec/doxm", + "rt": [ + "oic.r.doxm" + ], + "if": [ + "oic.if.baseline" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/oic/sec/pstat", + "rt": [ + "oic.r.pstat" + ], + "if": [ + "oic.if.baseline" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/oic/d", + "rt": [ + "oic.wk.d", + "oic.d.range" + ], + "if": [ + "oic.if.baseline", + "oic.if.r" + ], + "p": { + "bm": 1, + "sec": false, + "x.org.iotivity.tcp": 0 + } + }, + { + "href": "/oic/p", + "rt": [ + "oic.wk.p" + ], + "if": [ + "oic.if.baseline", + "oic.if.r" + ], + "p": { + "bm": 1, + "sec": false, + "x.org.iotivity.tcp": 0 + } + }, + { + "href": "/EasySetupResURI", + "rt": [ + "oic.r.easysetup" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/WiFiConfResURI", + "rt": [ + "oic.wk.wifi" + ], + "if": [ + "oic.if.baseline" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/CoapCloudConfResURI", + "rt": [ + "oic.wk.cloudserver" + ], + "if": [ + "oic.if.baseline" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/DevConfResURI", + "rt": [ + "oic.wk.devconf" + ], + "if": [ + "oic.if.baseline" + ], + "p": { + "bm": 1, + "sec": true, + "port": 49155, + "x.org.iotivity.tls": 0 + } + }, + { + "href": "/sec/provisioninginfo", + "rt": [ + "x.com.samsung.provisioninginfo" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ], + "p": { + "bm": 1, + "sec": false, + "x.org.iotivity.tcp": 0 + } + }, + { + "href": "/sec/accesspointlist", + "rt": [ + "x.com.samsung.accesspointlist" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ], + "p": { + "bm": 1, + "sec": false, + "x.org.iotivity.tcp": 0 + } + } + ] + } + ], + "seeds": { + "/device/1": [ + { + "rt": [ + "x.com.samsung.devcol", + "oic.wk.col" + ], + "if": [ + "oic.if.baseline", + "oic.if.ll", + "oic.if.b" + ] + }, + { + "href": "/oven/vs/1", + "rep": { + "x.com.samsung.da.state": "Ready", + "rt": [ + "x.com.samsung.da.oven" + ], + "if": [ + "oic.if.baseline", + "oic.if.s" + ] + } + }, + { + "href": "/temperatures/vs/1", + "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": "175", + "x.com.samsung.da.increment": "5", + "x.com.samsung.da.unit": "Fahrenheit" + }, + { + "x.com.samsung.da.id": "1", + "x.com.samsung.da.description": "Temperature", + "x.com.samsung.da.desired": "0", + "x.com.samsung.da.current": "0", + "x.com.samsung.da.increment": "1", + "x.com.samsung.da.unit": "Fahrenheit" + } + ], + "rt": [ + "x.com.samsung.da.temperatures" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/mode/vs/1", + "rep": { + "x.com.samsung.da.supportedModes": [ + "LowerBake", + "LowerConvectionBake" + ], + "x.com.samsung.da.modes": [ + "NoOperation" + ], + "x.com.samsung.da.defaultMode": "LowerConvectionBake", + "rt": [ + "x.com.samsung.da.mode" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/operational/state/vs/1", + "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": "1", + "rt": [ + "x.com.samsung.da.operation" + ], + "if": [ + "oic.if.baseline", + "oic.if.a" + ] + } + }, + { + "href": "/connected/vs/1", + "rep": { + "x.com.samsung.da.connected": "On" + } + } + ] + }, + "seeds_note": "device0 and oic_res are verbatim diagnostics captures (issue #324). The /device/1 seed batch is reconstructed, not captured live: the diagnostics report only carries the subdevice's already-canonicalized resources (subdevices[0].resources), so this reindexes each trailing '.../vs/0' href to '.../vs/1' per subdevices.Subdevice.to_actual's own indexed-sibling rule, rather than recording the raw wire response." +} \ No newline at end of file diff --git a/tests/test_golden_regression.py b/tests/test_golden_regression.py index e626e1d..a0ce829 100644 --- a/tests/test_golden_regression.py +++ b/tests/test_golden_regression.py @@ -1132,12 +1132,16 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner_fac_bora(): ) -def _new_subdevice_aware_state_keys(name): +def _new_subdevice_aware_state_keys(name, device_types=()): """Like _new_state_keys, but runs the full subdevice-aware pipeline (enumerate_subdevices + discover_partitioned, issue #177) instead of a single discover() call, so the golden for a composite-device fixture captures every materialized subdevice's keys - (subdevice1_-/subdevice__-prefixed), not just the master's.""" + (subdevice1_-/subdevice__-prefixed), not just the master's. + + `device_types` threads through to discover_partitioned's own + `oic_device_types` -- needed for a board with no /information/vs/0 at + all to route from (issue #324's range).""" from custom_components.localthings.registry.adapter import flatten from tests.conftest import _discover_full, _load_device_full @@ -1146,6 +1150,7 @@ def _new_subdevice_aware_state_keys(name): resources, oic_res, seeds, + device_types, ) state = flatten(bound, full_resources) return sorted(state.keys()) @@ -1465,6 +1470,20 @@ def test_registry_reproduces_golden_state_keys_for_refrigerator_winecellar(): ) +def test_registry_reproduces_golden_state_keys_for_range_tp1x_da_ks_range_0101x(): + """Dual-cavity range (issue #324): no /information/vs/0 at all, so + routing depends entirely on /oic/d's oic.d.range -- and the second + cavity is a genuine Pattern A indexed subdevice at /device/1.""" + name = "range_tp1x_da_ks_range_0101x" + golden = json.loads((GOLDEN / f"{name}.json").read_text()) + state_keys = _new_subdevice_aware_state_keys(name, device_types=("oic.wk.d", "oic.d.range")) + 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_resources_from_batch_preferred_over_flat(): from tests.conftest import _resources_from_dump diff --git a/tests/test_range_tp1x_da_ks_range_0101x.py b/tests/test_range_tp1x_da_ks_range_0101x.py new file mode 100644 index 0000000..a665d9f --- /dev/null +++ b/tests/test_range_tp1x_da_ks_range_0101x.py @@ -0,0 +1,92 @@ +"""Dual-cavity range (TP1X_DA-KS-RANGE-0101X, NE63T8751SG/AA-class, issue +#324): no /information/vs/0 at all, so the modelNum-based fallback has +nothing to read -- routing depends entirely on /oic/d's own device type +(`oic.d.range`), read separately from the /device/0 batch (see +registry/identity.py and by_type/__init__.py's `_OIC_TYPE_TO_KEY`). + +The second oven cavity answers as an indexed sibling at /device/1 (Pattern +A, same mechanism as the AC family's issue #177 fixtures) -- the master's +/mode/vs/0 (defaultMode 'UpperConvectionBake') is the upper cavity, the +subdevice's canonical /mode/vs/0 (defaultMode 'LowerConvectionBake') is the +lower one. +""" + +from custom_components.localthings.registry.adapter import flatten +from custom_components.localthings.registry.by_type import for_device_by_oic_type, resolve +from tests.conftest import _discover_full, _load_device_full + +FIXTURE = "range_tp1x_da_ks_range_0101x" +DEVICE_TYPES = ("oic.wk.d", "oic.d.range") + + +def _discover(): + resources, oic_res, seeds = _load_device_full(FIXTURE) + return _discover_full(resources, oic_res, seeds, DEVICE_TYPES) + + +def test_resolves_via_oic_type(): + reg = for_device_by_oic_type(DEVICE_TYPES) + assert reg is not None and reg.name == "range" + + +def test_resolves_via_the_full_resolve_entrypoint(): + resources, _oic_res, _seeds = _load_device_full(FIXTURE) + reg = resolve(resources, device_types=DEVICE_TYPES) + assert reg is not None and reg.name == "range" + + +def test_no_unbound_hrefs(): + """Every resource in the issue #324 dump binds or is ignored, on both + the master (upper cavity) and the materialized second cavity -- clears + the coverage-gap repair.""" + resources, oic_res, seeds = _load_device_full(FIXTURE) + unbound = [] + from custom_components.localthings.registry.registry import CAPABILITIES + from custom_components.localthings.registry.subdevices import ( + discover_partitioned, + enumerate_subdevices, + ) + from tests.conftest import FakeCoapSession + + sess = FakeCoapSession(seeds) + candidates, extra = enumerate_subdevices(sess, resources, oic_res) + full_resources = {**resources, **extra} + discover_partitioned( + full_resources, + candidates, + resolve, + CAPABILITIES, + log=unbound.append, + oic_device_types=DEVICE_TYPES, + ) + assert unbound == [] + + +def test_second_cavity_materializes_as_an_indexed_subdevice(): + _bound, materialized, skipped, _full_resources, device_type_name = _discover() + assert device_type_name == "range" + assert skipped == [] + assert [(s.kind, s.key) for s in materialized] == [("indexed", "1")] + assert materialized[0].seed_path == ("device", "1") + + +def test_both_cavities_expose_distinct_oven_state(): + bound, _materialized, _skipped, full_resources, _name = _discover() + state = flatten(bound, full_resources) + for key in ("oven_mode", "oven_setpoint", "oven_state", "machine_state", "door_open"): + assert key in state, key + # Second cavity's entities carry the subdevice1_ prefix (adapter._key) -- + # the master's own keys stay unprefixed, so both cavities get distinct + # unique_ids rather than colliding on the same entity key. + assert "subdevice1_oven_mode" in state + assert "subdevice1_oven_setpoint" in state + + +def test_cooktop_monitoring_present_on_the_master_only(): + """The cooktop half belongs to the appliance as a whole, not either oven + cavity -- /cooktopmonitoring/vs/0 has no per-cavity index.""" + bound, _materialized, _skipped, full_resources, _name = _discover() + state = flatten(bound, full_resources) + assert "cooktop_running_state" in state + assert "warming_center_state" in state + assert "subdevice1_cooktop_running_state" not in state