When HA restarts without a clean DTLS close_notify (crash, host reboot),
the appliance keeps an orphaned DTLS association keyed to the client's
(IP, source port). Reconnecting from a fresh ephemeral port looks like a
new peer, so the device holds the orphan until its own timer reaps it,
which is 5 to 15 min on always-on appliances (fridges), during which the
new session's first reads hang. This is the root cause behind the repeated
"DTLS handshake timeout" reconnect storms on always-on devices (#119).
Bind a deterministic source port per device so every reconnect re-handshakes
over the same 5-tuple, which the device must treat as a rebooted peer and
evict the old association for (RFC 6347 §4.2.8). Recovery drops from a
device-timer wait to a single handshake.
The port must be stable across restarts and unique per device on the HA host
(the library socket is unconnected, so a shared source port would cross-
deliver datagrams). _local_source_port() uses the host's last IPv4 octet as
the offset from DTLS_LOCAL_PORT_BASE (unique on a /24), with a CRC32 fallback
for non-IPv4 hosts.
Requires smartthings-local >= 0.1.1, which adds DtlsCoapSession(local_port=).
The fix is backwards compatible upstream: local_port defaults to None
(previous ephemeral-port behaviour).
Root-caused and verified upstream in QuiteYellow/SmartThings-Local#14
(bench-verified on oven + dryer, field-verified on an always-on fridge
across repeated restarts).
A-VSKR-TP1-22-VS9500AL connects successfully but its dump shows no
vacuum-body state at all -- no suction level, no battery, no cleaning
mode -- only the clean/auto-empty station's own dustbag, dustbin
auto-empty settings, and UV-C sanitizing-cycle status. This strongly
suggests the WiFi/DTLS module lives in the station, not the handheld
stick, so the station is the only "device" this integration's local
API can reach at all.
New vacuum_station device type (these hrefs share nothing with any
existing family, so there's no shared-href ambiguity to resolve
against another type) routed via a new '-VSKR-' modelNum fallback.
Binds with zero unbound hrefs: dust-bag full/usage sensors, auto-empty
and dustbin auto-close switches, a discharging-time select, and
clean-station status including UV-C intensive mode, operation time,
and finished/emitted timestamps. A couple of fields with unconfirmed
exact semantics (stick_status, dustbag_usage's unit) are exposed as
plain diagnostic values rather than an asserted binary/percentage
meaning.
This newer board family reports fan modes (Smart/Max/Mid/WindFree/Sleep)
directly on /mode/vs/0's top-level modes/supportedModes fields, unlike
the older ARTIK051_TVTL family this registry already supported, which
packs everything into an options[] array with no usable fan-speed
selector at all (see the module docstring's Comode_Off finding). Both
board generations share the /mode/vs/0 href, so the existing MODE
capability and a new FAN capability are discriminated by a match_fn
checking for the top-level supportedModes field, rather than adding a
new device type.
The fan entity only exposes PRESET_MODE, not an ordered percentage --
WindFree/Smart/Sleep are named behaviors, not "faster/slower" positions
relative to Max/Mid, matching how the AC family's own named convenient
modes are modeled as a preset rather than a speed number.
Also picked up the rest of this board's previously-unbound hrefs while
in there (display, HEPA filter, panel status, pet-filter mode, sound
settings), reusing airconditioner.DISPLAY_LIGHT and
airconditioner.MUTE_ONCE for the two hrefs identical to the shared
DA-AC- board family, since the "incomplete capability coverage" repair
was firing on more than just the fan gap the issue described.
TP1X_DA-KS-MICROWAVE-01041 (MW7300B) reports no oneUiVersion and an
unrecognized consumer token, so it fell back to 'unknown' and only got
common capabilities. It shares the same '/oven/vs/0' cavity resource
and '/mode/vs/0' cook-mode shape (Convection/AirFryer/Grill/MicroWave*)
as the wall oven already supported, so this reuses that registry via a
new '-MICROWAVE-' modelNum fallback rather than adding a new device
type. The only href it didn't already cover was /recipe/cook/vs/0, an
empty quick-recipe-display blob bound with no entity per the 'don't
guess' rule.
Newer TP1X_DA-AC-RAC-01011_0000 firmware (Bespoke AI WindFree Deluxe,
AR60H10D1JWNME) drops /wind/direction/vs/0 entirely and reports swing
via a separate vertical/horizontal Swing|Fix pair on
/wind/oscillation/vs/0 instead, which left swing_mode/swing_modes
silently empty and the href unbound. climate.py now falls back to the
oscillation resource when /wind/direction/vs/0 is absent, mapping the
same off/vertical/horizontal/both vocabulary the existing swing control
already uses.
Also binds the board's /anomalyload/vs/0 overload-response resource as
read-only diagnostic sensors (operation state + mode) -- the same
"don't guess" precedent as the existing CURRENT_LIMIT capability, since
nothing in the dump confirms the exact behavioral difference between
its 'Alarm' and 'PowerSaving' modes or whether toggling it is safe on
live HVAC hardware.
Most of the other gaps this issue reported (Fan-only mode, WindFree
preset labels, target-temperature channel selection, power on/off via
the vendor resource) turned out to already be fixed by the just-merged
cool-only global RAC work.
AR10/13/18BYEAAWKNME report no oneUiVersion and no '_RAC_'/'-RAC-'
token at all, so for_device_by_model() fell through to 'unknown' and
every href went uncovered. The board carries the same TP1X-class
resource surface as every other room AC already supported (mode/
convenient/wind/temperature/power/filter/humidity), so this reuses the
existing airconditioner registry via a new 'ARA-WW-' modelNum fallback
rather than adding a new device type -- confirmed against all four
reporters' dumps binding cleanly with zero unbound hrefs.
TP1X_DA-KS-RANGE-0102X already resolves via the '-RANGE-' modelNum
fallback and /cooktopmonitoring/vs/0 already binds through
range.COOKTOP_MONITORING, so this model binds with zero unbound hrefs
today -- add a fixture to lock that in.
The DA_WM_TP1_21_COMMON board family already routes through the 'WA'
consumer-model-prefix fallback and binds cleanly against the washer
registry with zero unbound hrefs, but no fixture locked that in for
this specific board generation -- add one.
- Pin the FAN_ONLY reverse-write fallback to 'Wind' (the original single
spelling) instead of letting it silently flip to 'Fan' just because
'Fan' was added second to the dict -- _device_code_for_hvac() resolves
the code from a unit's own supportedModes first, so this dict is only
a fallback for a unit reporting none at all, and that fallback
shouldn't change behavior as an unintended side effect of insertion
order.
- Add direct tests for _preset_to_ha() (pure function, previously
untested) and the FAN_ONLY fallback pin.
- Cross-reference the AC family's inverted Light_On/Light_Off polarity
against air_purifier.py's plain-polarity use of the same token name on
the same resource name, so a future refactor doesn't assume they're
the same thing.
- Fix a one-column continuation-line misalignment and a stale PR-number
reference in a comment.
Finishes PR #91's contribution (pedroperosin) with the requested review
changes applied, on our own branch:
- Preset resolution is now fully dynamic, read from each unit's own
/mode/convenient/vs/0 supportedModes instead of a static per-model
table -- any board's convenient modes (including WindFree's Nano/
NanoSleep) surface without code changes. Unlabelled codes across
existing fixtures (longwind, motionindirect, motiondirect, drycomfort)
plus the two new WindFree ones are added to en.json/nl.json.
- 'Auto' now maps to HVACMode.AUTO instead of HEAT_COOL: these are
single-setpoint "device decides" units, not two-setpoint heat+cool
ones. 'Fan' is added alongside 'Wind' as a second FAN_ONLY spelling;
a new _device_code_for_hvac() picks the code from the unit's own
supportedModes since the flat map can't disambiguate two device codes
mapping to one HA value.
- Detection gains a hyphenated '-RAC-' modelNum fallback (alongside the
existing '_RAC_') for cool-only global RAC variants whose
/otninformation/vs/0 ships no swVersionInfo block.
- Adds a display_light switch sourced from /mode/vs/0's opaque options
blob (inverted Light_On/Light_Off token) for boards with no dedicated
/light/vs/0 switch.
Write-path behavioural change (touches the path iterated on across
#9/#17/#27/#38/#54): power now targets the vendor /power/vs/0 instead of
the OCF /power/0, and _is_on() reads vendor-first. /power/0 is absent on
several known AC boards, so the previous OCF-first read/write pair could
report and act on stale state -- consistent with #53's "can turn on but
not off". Target temperature now picks its channel (OCF pair vs. vendor
items[]) based on which the unit actually reports, reading and writing
the same one.
The vendor temperature write and the new display-light write both carry
only the changed field(s), not the whole resource -- confirmed sufficient
on the wire, the device merges the rest itself. That requires the
coordinator's optimistic cache to do the same merge on the read side so a
setpoint change doesn't blank out current/min/max/unit for the settle
window: common.py gains merge_items_field() next to the existing
merge_options_field(), and async_send_command wires it in for any write
touching x.com.samsung.da.items.
New fixture/golden for the cool-only global RAC variant (TP1X_DA-AC-RAC-
01001, AI_RAC_GLOBAL_COOLONLY_3.0); display_light added to the goldens
for boards that gain the new options-based switch. Regenerated against
current main rather than copied from the original PR, since those
goldens had already shifted (current_temperature_c/humidity from issue
#75).
Some TP2X_WATERPURIFIER_20K units are coffee-capable and expose five
resources issue #90's original dump never had:
/favorite/coffee/vs/0, /favorite/hotwater/vs/0,
/brand/recipe/info/vs/0, /coffee/custom/recipe/vs/0,
/recipe/coffee/vs/0, and /recipe/coffee/deletion/vs/0.
Adds FAVORITE_HOTWATER (a switch + select pair on
/favorite/hotwater/vs/0, mirroring the existing FAVORITE_CAPACITY
pattern) and COFFEE (a switch + status sensor on
/favorite/coffee/vs/0). The remaining four hrefs are static
capability-advertisement blobs or empty on every dump seen so far --
no live "current recipe" or "current custom slot" field to expose --
so they're added to the ignored coverage list per the 'don't guess'
rule rather than modeled speculatively.
Confirmed against the issue #107 diagnostics dump with zero unbound
hrefs.
WA8000T reports no oneUiVersion and used the 'WA' consumer-model
prefix, unmapped in _CONSUMER_PREFIX_TO_KEY (only WW/WD/WF/WV were
covered), so it fell into the unknown-device-type fallback.
Adding a bare 'WA' entry collided with the unrelated '_WAC_' (Window
Air Conditioner, issue #87) board-family token: some devices report
description == modelNum, so 'WAC' shows up as its own description
segment and 'WAC'[:2] == 'WA' matched the new washer prefix before the
more specific '_WAC_' modelNum check ever ran. Fixed by reordering
for_device_by_model() to check board-family modelNum tokens first and
the fuzzier 2-letter consumer-model-prefix scan only as a fallback,
rather than patching the prefix matching itself (an earlier attempt --
requiring a digit immediately after the prefix -- broke issue #79's
real DVE50A8800 case, which has no digit there either). Added a
regression test pinning the WAC/WA disambiguation directly.
Confirmed against the issue #106 diagnostics dump with zero unbound
hrefs.
Renames DeviceRegistry.name from 'cooktop' to 'gas_cooktop' for the
NA9300K-class gas-cooktop registry (PR #23), so diagnostics/device-info
labels no longer collide with the unrelated induction_cooktop family
(issue #86) -- two different OCF surfaces that happen to share the
English word "cooktop".
Safe rename: _REGISTRY_BY_KEY's 'cooktop' lookup key is unchanged, so
all three existing detection paths (oneUiVersion "Cooktop" exact
match, the legacy ARTIK051 modelNum rule, and the resource-signature
fallback) keep routing real devices exactly as before. Entity
unique_ids are built from device serial + entity key, not registry
name, so existing entities are unaffected. Only the DeviceInfo.name
and diagnostics device_type strings change, both cosmetic.
PR #75 (WindFree AC) added CURRENT_TEMPERATURE/HUMIDITY capabilities
to the shared airconditioner registry, which every AC device picks up
-- including the Window AC from PR #87. Both branches built their
golden fixtures independently against their own base commit before
either landed, so neither saw the other's addition; once both merged,
the Window AC's fixture went stale. The two extra keys are real,
working sensors from #75's work, not a regression.
AIComfort isn't a distinct thermodynamic operation like Cool/Dry/Heat --
it's an AI-driven overlay on top of the device's own 'Auto' behavior,
confirmed by A-CAWW-TP2-20-COMMON reporting both 'Auto' and 'AIComfort'
as separate, mutually-exclusive entries in /mode/vs/0's supportedModes.
Modeled the idiomatic HA way instead of a flat _DEVICE_TO_HVAC entry:
hvac_mode reports AUTO and a new 'ai_comfort' preset carries the
distinction. Entered/left only via the preset (writes the primary mode
resource, not the convenient one) -- there's no dedicated HVACMode
value for it, so it's not offered in the hvac_mode dropdown directly.
Also adds a once-per-(href, code) warning log when a device-reported
mode has no entry in the relevant map, so a future gap like this one
surfaces in the log instead of silently vanishing -- the exact failure
mode issue #93 called out ("this class of gap is invisible without
diffing against supportedModes").
A-CAWW-TP2-20-COMMON (and likely other CAWW/TP2X-class boards) reports
'AIComfort' as a distinct entry in /mode/vs/0's supportedModes,
alongside 'Auto' (already mapped to HEAT_COOL). _read_modes() silently
drops any code missing from _DEVICE_TO_HVAC, so AIComfort was
unreachable -- one of HA's five other AC hvac_modes, unused by this
device family until now.
The other two gaps in issue #93 are already addressed elsewhere and
not duplicated here:
- Fan-only via the 'Fan' device code, and the WindFree/LongWind/
NanoSleep preset codes, are covered by the still-open PR #91, which
replaces the static preset table with a fully dynamic resolver over
the device's own supportedModes.
- The 'Left_And_Right' -> horizontal swing mapping is already on the
still-open claude/device-support-issue-75-windfree-ac branch.
The TP2X_WATERPURIFIER_20K water purifier reports no oneUiVersion and
its modelNum/description don't match any consumer-prefix or existing
board-family token, so it fell into the unknown-device-type fallback
with only common capabilities. Add a new water_purifier registry,
routed via a 'WATERPURIFIER' modelNum/description fallback rule.
Models dispense settings (type/temperature/capacity/pouring status),
sterilize and filter status, favorite-capacity presets, and the three
water/buzzer locks. Per the adding-device-support skill's "never
hard-code the one dump's values" rule: dispense-capacity bounds and
step come live from the device's own desiredCapacityRange/
capacityResolution fields (range_field/step_fn), not a hardcoded
constant, and the hot-water-temperature control is a select over the
live supportedHotTemperatures list rather than a number with invented
bounds, since only a few discrete temperatures are selectable.
/mode/vs/0 and /automation/waterpurifier/vs/0 are left unmodeled: the
former carries an opaque wizard-workflow token with no coherent
current-value contract, the latter is a static support-flags blob with
no live setting to expose.
Confirmed against the issue #90 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
The AY18CG7500GED dehumidifier (modelNum TP1X_DA_AC_DHM_01001_0000)
shares the DA_AC_ board family with the room-AC models but carries the
'_DHM_' token instead of '_RAC_'/'_PRAC_'/'_WAC_', so it fell into the
unknown-device-type fallback. Add a new dehumidifier registry, routed
via a '_DHM_' modelNum fallback rule, distinct from airconditioner
since target humidity (not temperature) is the primary control and
there's no climate composite.
Reuses airconditioner.py's AUTO_CLEAN/AIR_FILTER/MUTE_ONCE capabilities
directly (identical resource shapes on the shared board family). Adds
a new humidity sensor + target-humidity number pair and an
operating-mode select. Per the adding-device-support skill's
"never hard-code the one dump's values" rule, the target-humidity
number has no hardcoded min/max (falls back to HA's own 0-100 default
for a percentage field) and reads its step live from the device's own
`increment` field rather than a spec-sheet-derived constant. The
operating-mode select's options come live from supportedModes.
/mode/convenient/vs/0 is left unmodeled: only supportedModes is present
on this dump, with no live current-value field to confirm a read/write
contract.
Confirmed against the issue #88 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
The AW06C7155EWAZ window air conditioner (modelNum
TP1X_DA_AC_WAC_01001_0000) reports no oneUiVersion and uses the '_WAC_'
(Window Air Conditioner) modelNum token instead of the '_RAC_'/'_PRAC_'
tokens already handled by for_device_by_model. Add a fallback rule for
that token, routing it to the existing airconditioner registry.
The device's resource surface (mode/convenient/wind/temperature/power/
filter/humidity) is already fully modeled by the airconditioner
capability set, so this is purely a detection-routing fix -- confirmed
against the issue #87 diagnostics dump with zero unbound hrefs.
TP1X_DA-KS-COOKTOP-01011 (NV8500T-/KO4) is the same board family and
/cooktop/status/vs/0 resource shape as issue #44's range combo, minus
the oven -- but its modelNum uses the hyphenated '-COOKTOP-' token,
which for_device_by_model's existing '_COOKTOP' check (underscore-
delimited, matching the unrelated NA9300K gas-cooktop family in
cooktop.py) doesn't match. The device fell back to 'unknown' with only
energy/alarms from the global fallback.
Add the hyphenated token check, routing to a new 'induction_cooktop'
registry (by_type/induction_cooktop.py) that reuses range.py's
COOKTOP_STATUS/COOKTOP_SPEC/COOKTOP_SAFETY/PROBE_STATUS and
cooktop.PAIRED_HOOD_STATUS wholesale rather than pulling in range.py's
oven capabilities, which this device has no hrefs for at all.
Along the way, three fields the reporter asked for turned out to be
gaps in the shared range.py capability itself, not just missing
routing -- also present (and previously unmodeled) on issue #44's
original combo-range dump:
- /cooktop/status/vs/0's own `power` and `childLock` fields (distinct
from common.POWER's /power/0 or /power/vs/0, which a combo range
additionally carries for the whole appliance) -- childLock gets a
write_fn (a safe lock toggle, direct single-field PUT), power stays
read-only (no live device to confirm a remote write wouldn't leave a
burner active unattended).
- Each burner's `panDetection` field.
New: a Bluetooth meat-probe capability for /bluetooth/probe/status/vs/0
(read-only -- connection, battery, current/target temperature), and
/cooktop/recipe/status/vs/0 is ignored (every field empty on this idle
dump, same treatment as the microwave family's /recipe/cook/vs/0).
Regenerates the range golden fixture (gains cooktop_power/
cooktop_child_lock/burner_N_pan_detected) and adds a dedicated fixture
for the standalone cooktop.
The ARTIK051_DONGLE_REF firmware family reports the literal string
"Nothing(SVC)" as serialNum on every unit -- non-empty, so the existing
`if not serial` checks in config_flow.py's _probe_and_validate and
coordinator.py's _run_discovery don't catch it. Two such appliances on
one install (a fridge and a freezer, each its own dongle) then collide:
config_flow gives both the same unique_id and rejects the second as
"already configured" (bug 2), and even once that's worked around,
device_serial feeds every entity's unique_id too, so the second
appliance's entities get silently dropped with "does not generate
unique IDs" log lines (bug 4).
Add _is_placeholder_serial (duplicated in both modules rather than
imported, to avoid pulling config_flow into the runtime coordinator's
import graph or vice versa for a two-line check) and treat it the same
as an empty serial: fall back to host/port.
Type detection (bug 1) and the door sensor's field-name gap (bug 3),
also reported in this issue, are already fixed via the
claude/device-support-issue-77-freezer branch, which hit the same
ARTIK051_DONGLE_REF family from a different report -- not duplicated
here.
Five raw course codes were rendering unlabeled because no translation
entry existed for them, confirmed by the reporter selecting each cycle
on the physical appliance and reading back the raw code from the
entity's state:
- washer_cycle_table_02: '52' Eco Cold, '54' Towels, '60' Self Clean+
(a WF50A8600AV/US). '54' shares a display name with the existing '24'
Towels -- a different code on the same table legitimately landing on
the same label, matching the existing '21'/'65' Colors and
'27'/'5E' Rinse+Spin pairs, not a duplicate-in-error.
- dryer_cycle_table_03: '01' Normal, '06' Time dry (a DVE50A8600V/A3,
the same model added in the previous commit's detection fix).
No code changes -- select.py already derives which raw values it
normalizes from the shipped catalog, so labelling a code is purely a
translations/en.json (mirrored to nl.json) addition.
DVE50A8600V/A3 reports description
'DA_WM_TP1_21_COMMON_DVE50A8800_8600/DC92-02835A_0080' -- a paired
listing of two related model numbers (DVE50A8800 and DVE50A8600) joined
by an underscore, rather than the usual single trailing consumer-model
token. for_device_by_model only ever checked the literal last
underscore segment ('8600', which has no recognizable 2-letter prefix
on its own), so the real 'DV' token one segment earlier was never
reached and the device fell back to 'unknown' with only common
capabilities -- no dry level, cycle, or wrinkle-prevent entities.
Replace the single last-segment extraction with _consumer_model_key,
which scans segments from the end and returns the first one that
resolves. Behavior is unchanged for every existing single-token
description (the last segment still matches first); it just keeps
looking when that segment doesn't.
RR40M7165WW is the fridge half of the same household dongle setup as
issue #77's freezer -- identical pipe-delimited ARTIK051_DONGLE_REF
modelNum, so the previous commit's detection and door-sensor fixes
already cover it with no further code changes. Add its fixture as a
second, independent regression case: it exercises the 'cooler' instance
segment instead of 'freezer' for both the temperature pattern caps and
DOOR_GENERIC, and notably reports /door/onedoorfreezer/vs/0 despite
being a single-door fridge (shared firmware naming across the product
line, not an actual second compartment) -- worth having its own golden
so that stays working too.
RZ32M713EWW/EE (an ARTIK051-dongle standalone freezer) reports no
oneUiVersion and a pipe-delimited modelNum
('ARTIK051_DONGLE_REF|<rest>') -- REF is the last underscore segment
before the pipe, not wrapped in underscores on both sides like the
'..._REF_...' shape for_device_by_model's substring check expected, so
the device fell through to 'unknown' with only common capabilities:
no door sensor, no temperature sensors, nothing fridge-specific.
Replace the substring check with a segment-based one
(_model_num_segments splits the pipe-delimited prefix on '_') that
catches both shapes. Same root cause, same fix, and same device family
independently reported and root-caused in issue #83 (which also covers
two further bugs -- config-flow/coordinator serial collisions on a
second symptom of this firmware, 'Nothing(SVC)' as a literal serial --
not needed here since this reporter has a single unit; left for #83).
Once routed to the refrigerator registry, fridge.py's existing pattern
capabilities pick up /temperature/current/freezer/0 and
/temperature/desired/freezer/0 for free -- they were never the actual
problem, just unreachable under the 'unknown' fallback (which never
tries pattern capabilities at all). The door sensor needed one more
fix: /door/onedoorfreezer/vs/0 reports the vendor-prefixed
x.com.samsung.da.openState, not the bare openState DOOR_GENERIC read,
so the entity existed but stayed permanently unavailable. Check both
field names via rep_fn.
Three gaps reported against an ARTIK051_PRAC_20K WindFree unit vs. the
SmartThings integration:
1. Missing WindFree/motion convenient-mode presets -- left alone here.
PR #91 replaces climate.py's static _DEVICE_TO_PRESET table with a
generic resolver that reads any preset code straight off the unit's
own supportedModes, which already covers this (and more generically
than a per-model dict would) -- adding one here would just conflict.
2. No horizontal oscillation: /wind/direction/vs/0's supportedModes
includes Left_And_Right, which _DEVICE_TO_SWING had no mapping for.
Add it to HA's standard 'horizontal' swing constant.
3. No standalone humidity/current-temperature sensors: the climate card
already reads both internally, but nothing exposed them as entities
for history/automations. Add CURRENT_TEMPERATURE (OCF
/temperature/current/0) with a CURRENT_TEMPERATURE_VS vendor fallback
(same match_fn-gated pair shape as common.py's POWER_GENERIC/
POWER_VS_FALLBACK), and a HUMIDITY sensor reading /humidity/vs/0's
fivepercentHumidity field -- the only one of the three
humidity-shaped fields across /humidity/0 and /humidity/vs/0 that
isn't permanently stuck at 0 on every dump seen.
Regenerates the five existing AC goldens (all pick up
current_temperature_c; most pick up humidity) and adds a dedicated
fixture from the issue's WindFree dump, whose /humidity/vs/0 actually
has live fivepercentHumidity data.
NE63B8411SS reports no oneUiVersion and no /information/vs/0 resource at
all, so neither for_device nor for_device_by_model's modelNum tokens have
anything to key off -- it fell through to the unknown-device fallback,
which also mis-binds /temperatures/vs/0 against fridge.py's setpoint
capability (a collision the oven family's own capabilities are normally
excluded from the global registry specifically to avoid).
Add a resources-based signature to for_device_by_resources(): 'Bake' in
/mode/vs/0's supportedModes is oven/range-exclusive vocabulary, and paired
with the /oven/vs/0 cavity resource it reliably identifies this family
even with no model info at all. Route to 'range' when a cooktop-status
resource is also present, else plain 'oven'.
This board's cooktop half also doesn't expose the per-burner
/cooktop/status/vs/0 array range.py already models -- only the coarser
/cooktopmonitoring/vs/0 summary resource. Add a read-only
COOKTOP_MONITORING capability for it (cooktop running state, warming
center state) rather than leaving it unbound.