Conflict was purely additive: both sides appended golden-regression
tests at the same point in the file, and each side's last test shared
the single trailing assert block. Kept every test from both sides, each
with its own copy of that assert.
One real semantic merge on top of that. #136 (on main) remodelled the
legacy ARTIK051 board's beep from a buzzer_volume Number to a beep
Switch, and HJcom's ARTIK051_DONGLE_FAC_18K is exactly that board
generation (is_legacy_board -> True), so its golden -- written before
that change existed -- still expected buzzer_volume. Regenerated it:
buzzer_volume/unit1_buzzer_volume -> beep/unit1_beep on the master and
the second indoor unit alike, with nothing else moving and still no
unit2_ keys. That is main's intended behavior reaching the sub-unit for
free, which is the point of binding siblings through the same registry.
967 passing. Re-ran the corpus-wide unique_id audit over all 57
fixtures (main added five this branch had never seen): no collisions
within a platform.
The skill is what tells the next person how to read a diagnostics dump,
and this branch changed the dump. Without these edits it describes the
old shape and, in one place, leads somewhere that fails silently.
The trap: on a multi-unit appliance a sibling's coverage gap appears in
unbound_hrefs as the *real* href it was seen on -- /foo/vs/1, or
/<uuid>/foo/vs/0. The skill's own rule is "every href must resolve, or
the repair fires", so the natural next move is to bind the href in front
of you. Binding runs against each unit's canonical view, so a registry
entry for an indexed or prefixed href matches nothing on any device: no
error, no entity, gap still open. Section 8 now says registry hrefs are
always canonical and nothing under capabilities/ or by_type/ should ever
mention a unit index.
Section 1 documents the four new blocks (sub_units, sub_units_skipped,
sub_unit_probes, multidevice) and that `resources` is now this unit's
own. Section 2 notes that a sibling's block is canonicalized precisely
so it drops into the standalone-discovery recipe unchanged -- the reason
that canonicalization exists is invisible unless stated. Section 10
covers the fixture's optional oic_res/seeds/probes keys, _load_device_full,
and why a multi-unit golden carries prefixed keys while the master's stay
bare.
Section 11 is new: the ordered triage for "one of my units is missing",
which is the read that would have turned #177 from days of archaeology
into a few minutes -- probes first (did we look?), then the skipped
candidates' own reps (did we reject it, and was that right?), then the
board's own count, then which pattern the board uses.
Section 5's "don't guess" rule also needed a boundary. It reads as
covering all speculative traffic, but this codebase deliberately probes
hrefs no dump contains -- read_identity and enumerate_sub_units both do.
A RETRIEVE is non-mutating and a 4.04 is tolerated throughout that path;
it's guessing a *write* against live hardware, or inventing an entity
from a field you can't explain, that the rule is actually about.
Samsung 2-in-1 air conditioners put more than one logical indoor unit
behind a single IP and a single DTLS session. Only the unit the config
entry was set up against was ever discovered; the second one -- a whole
physical appliance the user can see in SmartThings -- had no entities at
all. Two reporters turned out to have two different mechanisms:
ARTIK051_DONGLE_FAC_18K -- indexed siblings. /oic/res registers the
whole tree discoverable and lists three complete parallel resource
sets whose trailing path segment is the index (/mode/vs/0, /mode/vs/1,
...), on OCF-standard and vendor hrefs alike. /device/0's batch
carries only the index-0 hrefs, so a sibling is reachable only through
its own /device/<n> collection.
TP2X_FAC_BORA_21K -- UUID-prefixed tree. /oic/res hides the appliance
tree entirely (which is why a direct /device/1 probe returns nothing
on this board). /subdevices/vs/0 carries subdeviceIdList instead, and
that UUID appears as a literal href prefix; /<uuid>/information/vs/0
was confirmed live to return the wall unit's own model and serial
(TP2X_FAC_BORA_RAC_21K) against the master's TP2X_FAC_BORA_21K.
The detection signals don't overlap on either board, so no
disambiguation is needed -- enumeration checks both and takes what
answers.
Both patterns are the same thing underneath: a logical unit is a seed
collection path to poll plus an href transform between the canonical
href the registry knows and the actual on-the-wire href. That is the
whole abstraction (SubUnit), applied at four boundaries -- discovery,
the coordinator, the adapter, and the platforms. Capabilities, the
registry and the climate composite stay written against canonical hrefs
and are untouched.
Uniqueness comes from a key_prefix inside the flattened state key, so
the master unit's keys are byte-identical to every release before this
and every existing golden file is an unchanged regression guard. Each
sub-unit gets its own device-registry entry linked by via_device and
named from its own /information/vs/<n>, so it lands in its own room
rather than crowding the master's device page.
A sub-unit materializes only when it yields at least one primary
(non-diagnostic) entity with a populated value. That gate is not
decoration: the reporter's /device/2 is an unused slot that SmartThings
shows disabled, yet it answers with a full 14-href batch, and it
flattens to exactly one non-None value -- a diagnostic alarm_code
derived from an empty /alarms/vs/2. Without the entity-category filter
it becomes a phantom third climate card. The rule is deliberately
domain-agnostic rather than a list of HVAC hrefs, so a multi-drum
washer (#19) gets the same treatment with no new curation. Units that
answer but fail the gate are logged and reported in diagnostics, so a
genuinely missing unit stays diagnosable from a dump.
Enumeration fetches things that must not then be treated as appliance
state. A rejected candidate's seed has to be read to evaluate the gate,
but only units that pass are polled again, and StateCache has no
eviction -- so discovery runs before the first cache apply and those
reps are held aside for diagnostics rather than frozen into the cache
forever. /multidevice/vs/0 is probed on every device regardless of
family, so merging it into the resources dict would have reached
discovery on any board whose registry doesn't ignore that href -- only
the air conditioner one does -- raising a spurious coverage-gap repair
for a washer or fridge whose firmware answers it. It is corroborating
metadata (numofsubdevice, confirmed read-only) and now lives beside the
resources rather than in them.
Diagnostics reports each unit separately: top-level `resources` is this
unit's own and only its own, which is what the module docstring and the
adding-device-support skill have always claimed it was, and each
sibling or rejected candidate carries its own reps canonicalized so a
block reads exactly like the master's instead of needing to be
de-indexed by hand.
Fixtures are real captures. The ARTIK051_DONGLE_FAC_18K one is entirely
verbatim, both sibling seeds and the hand-read /multidevice/vs/0
included. The TP2X_FAC_BORA one has a real device0, oic_res and
sub-unit /information/vs/0, with the remainder of that unit's tree
constructed and documented as such in seeds_note; /<uuid>/device/0 is
the one part of that pattern still inferred rather than observed, and
can't be tested through the debug panel because a Collection returns a
list.
- config_flow: the #192 port-rescue made the "every port refused" fast-fail
permanently unreachable (PREFERRED_PROBE_PORTS is always non-empty and
always rescued), so removed the dead branch instead of leaving it as
misleading dead code. A dead host now fails via the handshake loop's own
error, which carries the real per-port reason.
- oven._has_option: added the is_stub_rep carve-out cooktop.py's identical
per-token exists_fn already has on the same kind of href, so a not-yet
sub-polled /mode/vs/0 doesn't permanently exclude energy_saving/
cooktop_on_alert before their first real fetch lands.
- airconditioner.ENERGY_METER_LEGACY: build via dataclasses.replace() like
its ENERGY_METER_GENERIC sibling, instead of hand-copying href/poll_tier
(which would silently drift if common.ENERGY_METER ever gains a field).
- Fixed two stale comments: is_legacy_board()'s docstring still listed
Volume among tokens needing legacy-only gating, though 'beep' now applies
unconditionally across board generations; and common.py's UNIVERSAL
invariant comment didn't mention that airconditioner also now excludes
ENERGY_METER from the wholesale bundle.
- #191 (CAC token): added the fixture/golden/capability-test coverage the
AVT token in the same branch got, including an honestly-documented list
of the ten hrefs this board generation doesn't cover yet.
- fridge.cooler_temperature_setpoint: dropped the hardcoded "N °C" state
labels -- the resource's own unit field isn't necessarily Celsius on a
different model reporting the same href, and the options themselves are
already read live via options_field, so a static per-value label risked
asserting the wrong unit for a future device sharing this capability.
- airconditioner._legacy_cumulative_power_kwh: parse with float (matching
common.wh_to_kwh's own numeric parsing) instead of this module's
integer-only _int, so a decimal-formatted reading doesn't raise.
- test_config_flow: the port-rescue test bound the real 49154 directly,
which could collide with an actually-running service on some machine;
now monkeypatches PREFERRED_PROBE_PORTS to an OS-assigned port instead.
- oven.py: consolidated six byte-for-byte-identical single-token options
write_fns (lamp/sound/fast_preheat/natural_steam/energy_saving/
cooktop_on_alert) into one _option_switch_write(prefix) factory.
845 tests passing (up from 841 -- 4 new CAC coverage tests).
/power/0 and /power/vs/0 had no poll_tier, so they only ever refreshed on
the once-per-30s summary poll -- everything else that drives real-time
switch/climate/fan state (e.g. remote-control enablement) is already on
the faster subscribe/subpoll 'warm' cadence for exactly this reason. Scoped
to just this one tier bump; the model-specific priority-flip claim in the
same issue thread isn't independently verified, so it isn't part of this
change.
Both switches were shipped unconditionally (no exists_fn) as an unverified
guess -- the module docstring already flagged them as "unproven." Every
range/oven fixture in the corpus, including the new issue #183 dump,
reports neither fastpreheat_* nor NaturalSteam_* in /mode/vs/0's options at
all, so both were phantom controls: always read as off, and toggling them
wrote a token the firmware never recognized in the first place. That
matches the reporter's exact complaint ("doesn't appear to do anything").
Also bound two tokens confirmed present on this dump but never modeled at
all: EnergySaving_On (the 120-hour energy-saving standby from the app) and
BurnerOnAlert_Off (cooktop-on alert), following the same single-token
options-merge pattern as the existing lamp/sound/fast_preheat switches.
Child lock, the setpoint mismatch, and the missing cook-start control from
this issue are not code bugs -- see the issue comment for what was verified
and what still needs more information from the reporter.
Every ARTIK051_KRAC_18K-generation unit confirmed on hardware (three units
across two reporters) only ever carries Volume_100 or Volume_Mute in
/mode/vs/0's options -- never an intermediate value -- so the existing
buzzer_volume Number (0-100, step 10) modeled a control this firmware
doesn't have. Worse, its write path could never produce the literal
'Mute' token needed to actually turn the beep off, since it only ever
wrote a plain integer string. The 'beep' switch already used on newer
boards is the correct model here too; it's no longer gated off the legacy
board generation, and the Number entity is removed.
The WindFree-preset-not-applying report earlier in this issue self-resolved
per the reporter's own follow-up testing, so no code change was needed for
that part.
RT42DG6630B1FZ is a single-door "cooler only" fridge that reports its
setpoint on /temperature/definite/cooler/vs/0 -- a vendor resource outside
both TEMP_CURRENT_GENERIC's '/temperature/current/' and TEMP_SETPOINT's
'/temperature/desired/' href prefixes, so it was entirely unbound and the
setting stayed app-only. Its supportedList (1/2/3/4/7 °C) isn't a
contiguous range, so this is modeled as a select over the device's own
live options rather than a NumberDesc that would let a user pick an
unsupported value like 5 or 6.
AVT-WW-TP1-23-AXX500 (AX053B810HGD) reported device_type 'unknown' with
empty oneUiVersion, falling back to common caps. It's the same BESPOKE
Cube Air lineage as A-VTWW-TP2-21-COMMON (issue #151), just with the
'-WW-' delimiter shifted one letter left ('A-VTWW-' -> 'AVT-WW-'), which
splits into an 'AVT'/'WW' token pair the existing whole-token 'VTWW' entry
can't see. Added 'AVT' as its own board token onto the same air_purifier
registry -- the resource surface (wind/strength fan, HEPA filter, air
quality sensors, alarms) already matched with zero unbound hrefs once
routed there, so no new capabilities were needed.
ARTIK051_KRAC_18K-generation boards report /energy/consumption/vs/0's
cumulativePower in centiwatt-hours, not the plain Wh every other AC board
family reports -- confirmed against the reporter's own SmartThings-app
reading (raw 117430000 vs. the app's authoritative 1,174.30 kWh is exactly
a /100000 factor, not the shared wh_to_kwh's /1000 alone). Split
common.ENERGY_METER into generic/legacy variants on the airconditioner
registry, discriminated by the existing is_legacy_board() check, so every
other AC family keeps the unmodified shared capability.
The liveness sweep's ICMP-based verdict isn't trustworthy on every network
path -- a segregated-VLAN report showed it calling three closed ports live
while missing the one port a concurrent nmap scan found genuinely
open|filtered, which also happened to be one of our two historically
confirmed DTLS ports. Rather than trust a "not live" verdict against that
prior, always give PREFERRED_PROBE_PORTS a real handshake attempt even when
the sweep excludes them, bounded to at most those two extra attempts.
This is a stop-gap for the reported failure mode, not a full fix for the
sweep's underlying unreliability -- left a comment on the issue with the
diagnosis and flagged the sweep itself for a deeper redesign.
The 0.16.0 device-type simplification dropped oneUiVersion detection on
the assumption every device it typed was already reachable via a modelNum
board token. Cassette AC units (TP1X_DA-AC-CAC-01001_0000) were the one
exception -- they only ever resolved through oneUiVersion's "Air
conditioner" string, since 'CAC' was never added to the board-token
table -- so they silently fell back to common caps and lost their
climate entity.
/oic/res's baseline-Interface response only lists resources with the
discoverable policy bit set, and a real dump (issue #177 follow-up,
TP1X_REF_21K) confirms /device/0's whole x.com.samsung.da.* tree is
registered without it -- so a second logical Device's Collection, if
one exists, would be just as invisible to /oic/res as /device/0 is.
Probe /device/1 and /device/2 directly instead: a plain non-mutating
RETRIEVE, tolerated-404 same as every other speculative read in this
module. Parsed with the same parse_device0_batch used for /device/0
itself, and folded into identity.raw so diagnostics can tell "checked,
found nothing" apart from "never checked".
/oic/res is OCF's baseline resource-discovery endpoint: a unicast
RETRIEVE returns every href/Collection the connection hosts, not just
the one /device/0 seed path the coordinator polls. Relevant to the OCF
"Composite Device" model (issue #177) -- a single physical unit sharing
one IP/session across more than one logical Device, each exposed as its
own Collection resource (same rt shape as our own /device/0). Nothing
routes on this yet; captured alongside the existing /oic/p and /oic/d
reads so a report from a multi-unit device shows us whether its
firmware actually implements that model before any code assumes it does.
status on both /mds/absencepowersaving/vs/0 and
/option/motiondetectwind/stateful/vs/0 is a bare On/Off boolean, the same
shape already shipped writable elsewhere in this file (MUTE_ONCE,
AUTO_CLEAN, AIR_PURIFY) without a live-confirmed write either -- worst
case a wrong token no-ops. The paired mode selects (switchPowerSaveMode,
motion-detect modes) stay read-only: their behavioral effect on live HVAC
isn't inferable from the dump, same reasoning as ANOMALY_LOAD's mode field.
Both FilterRemind_*/RemindBeep_* option-array tokens are already
present -- and both On and Off already confirmed -- on the existing
ME7500D fixtures (issue #152), so this is a straight sibling of the
Sound/Lamp switches rather than new discovery work. Gated with
exists_fn like Lamp since the MW7300B combi dump has neither token.
Doesn't address the rest of issue #181 (power-level slider,
non-reported cooking modes, 3-level light, child lock, send-to-
microwave) -- those need write-contract confirmation this dump
doesn't carry.
README.md and requirements-dev.txt already document that the test
harness needs Python 3.13+ (pytest-homeassistant-custom-component
doesn't resolve below it), but only in prose. Add requires-python so
pip fails fast with a clear message on an older interpreter instead of
a wall of "Requires-Python >=3.13" version-list noise.
Lennox-branded heat pump on the Samsung RAC board family (modelNum
TP1X_LNX-AC-RAC-01001_0000) already routes correctly via the existing
'-RAC-' token, but its dump has two resources no prior AC fixture
carried: /mds/absencepowersaving/vs/0 and
/option/motiondetectwind/stateful/vs/0. Bind both as read-only
sensors, matching the CURRENT_LIMIT/ANOMALY_LOAD precedent -- nothing
in the dump confirms write safety on live HVAC hardware.
Three things this branch left stale.
"Adding a new appliance type" step 4 told contributors to key the registry
on the lowercased suffix of oneUiVersion and pointed at _type_key() for the
transform. Neither exists any more, and oneUiVersion no longer routes at
all. Describe the board-token table instead, including the two rules that
keep it a table: whole-token matching covers every delimiter spelling, and
an entry must name the specific device type rather than the board family
that contains it.
The repo-layout line for identity.py said "Reads device identity for type
detection". It has never fed type detection -- it reads /oic/p and /oic/d
for the HA device registry, and now also carries OCF's device-type
declaration into diagnostics.
The test setup installed pytest-homeassistant-custom-component and
homeassistant unpinned on top of requirements-dev.txt, which already pulls
both in at matching versions, and used whatever `python3` resolves to. On
3.12 or older nothing resolves and the install fails outright with a wall of
version-conflict output that doesn't name the real cause. Say 3.13+, drop
the redundant install, and note CI runs 3.14.
Two things a review of this branch turned up.
/oic/d's `n` is free text the owner sets from the SmartThings app, so it may
carry a person's name. Nothing in the /device/0 dump has ever exposed it --
it only became reachable when diagnostics started reporting /oic/d earlier
in this branch, which would have started carrying it into public issue
reports. Redact it. `rt`, the device-type signal the block exists for, is
untouched, and no /device/0 resource uses a bare 'n' key, so nothing else
changes.
The unknown-device-type warning logged only modelNum. That line is what a
user pastes into an issue, and modelNum alone can't identify a washer from a
dryer -- both report the shared DA_WM_ laundry board, and detection reads
the consumer-model code out of `description` for exactly that reason. Log
both fields.
oneUiVersion looks like the signal you'd want -- the device naming its own
type, '7.0 Dishwasher' -- and it was the first thing detection consulted. It
never earned the position:
- Only 7 of 49 fixtures report it at all.
- All 7 resolve to the same registry from their modelNum board token alone.
- No device-support issue has ever been fixed by adding a mapping for it.
Every one went through modelNum. The alias keys it needed in
_REGISTRY_BY_KEY ('airpurifier', 'air_conditioner', 'hood') were
speculative when the registries were first written and never used since.
So it bought a key-normalizing helper (_type_key), a lookup with a suffix
fallback (for_device), three alias keys, and a second config-flow step whose
only reason to exist was phrasing a sentence about oneUiVersion -- for a
signal that has never once been decisive.
Remove it from detection. It stays in diagnostics, where it's genuinely
useful: it names the firmware generation ('7.0 Air conditioner' is Tizen
Lite), which matters when triaging an issue.
Detection order was also duplicated in four places -- the coordinator, the
config flow's probe, the golden-regression harness, and the skill -- which
is how the harness and the shipped order drift apart. Collapse it into
by_type.resolve(resources), and call that everywhere.
The two "appliance type not recognized" config steps become one. They
differed only in whether they blamed a missing oneUiVersion, which is not a
distinction a user can act on, and never was.
Verified by the full suite (795 passing), including every golden regression
-- so entity output is byte-identical for all 49 device fixtures.
TestOneUiVersionIsNotConsulted locks in the premise rather than just the
outcome: for every dump that reports a oneUiVersion, the model strings alone
must still reach a registry. If a future device breaks that, the test says
so instead of the device silently losing half its entities.
Also note in requirements-dev.txt that Python 3.13 resolves the pinned
harness floor -- 3.12 and older resolve nothing and fail the whole install.
for_device_by_model() had grown to 21 sequential `if key is None` branches
and 102 comment lines against 59 lines of code -- 33 of the repo's 243
commits have touched this file. Most of that bulk came from one wrong
primitive: substring matching on a delimited string.
Samsung spells the same board family with either delimiter, so '_RAC_' and
'-RAC-' each needed their own rule, and 'ARTIK051_DONGLE_REF' (issues #77,
#83) matched no '_TOKEN_' spelling at all because REF lands at the end of
the pipe-prefix with no trailing underscore -- which is what
_model_num_segments() existed to work around. Which field a rule searched
(modelNum, or modelNum + description) was historical accident. Collisions
like WAC vs WA were resolved by one `if` physically preceding another,
invisible in the code and explained at length in prose.
Tokenize on any non-alphanumeric run, upper-case, and look the tokens up in
a flat table. Every delimiter spelling collapses to one entry, both fields
go through the same matcher in a documented order (modelNum, then
description, then the fuzzy consumer prefix), and specificity is a property
of the table rather than of line ordering.
Two behaviours are preserved deliberately:
- modelNum is matched before description, which is what keeps the legacy
gas cooktop correct: it reports 'ARTIK051_GB_CT_001' (CT) alongside
'ARTIK051_GLOBAL_COOKTOP' (COOKTOP, which otherwise means induction).
It is the only known device whose two fields disagree.
- _consumer_model_key still splits on '_' only. Widening it to '-' would
read the dishwasher's 'ADW-WW-RTL-24-AILITE' board segment as a bare 'WW'
washer.
Verified identical: all 49 device fixtures resolve to the same registry
before and after, and every existing for_device_by_model test case passes
unchanged. The table also picks up two families that previously depended on
oneUiVersion alone (TP1X_DA-AC-AIR air purifiers, ADW dishwashers), so they
now survive firmware that omits it.
TestBoardTokenAmbiguity guards the one property the flat lookup needs --
that no real model string contains two tokens naming different device types
-- across the whole fixture corpus, so a newly added dump exercises it
automatically.
The skill gains a section on routing: what each detection stage is for, the
rules for adding a token (name the specific type, never the board family;
never add a delimiter spelling; two-letter tokens are a last resort), when
to reach for the consumer prefix or a resource signature instead, and the
measured stake -- an unrouted device loses roughly half its entities.
Device-type detection currently parses board part numbers out of
/information/vs/0's modelNum. OCF has a standard field for exactly this
question -- /oic/d's `rt` -- and read_identity() already fetches the
resource, but kept only `n` and threw the rest away. No captured dump has
ever included it either: /device/0 batch responses don't carry /oic/d, and
diagnostics didn't report it, so there's no evidence on whether real
hardware populates it usefully.
Keep `rt` as DeviceIdentity.device_types, keep both raw payloads whole
(we don't yet know which of their fields identify a type), and surface
them in diagnostics so incoming issue reports answer the question.
Nothing routes on it yet.
/oic/d and /oic/p identify the unit with bare two-letter keys -- 'di' and
'pi' -- as sensitive as the serial number redact.py already covers but far
too short to match on: 'di' alone is a substring of 'condition', 'display'
and 'dispenser'. Add a whole-key match alongside the substring rules.
Issue #172: Samsung Microwave units (ME8000T-/AA0) omit /information/vs/0 and have empty oneUiVersion, falling back to unknown device type. Route via /oven/vs/0 and MicroWave modes in supportedModes.
An Opus review of merged PR #170 found the cleanLevel-scalar existence
gate on AIR_QUALITY doesn't hold up as a general rule: three fixtures
in this repo (air_purifier_device.json, air_purifier_vtww_device.json,
range_hood_device.json) carry genuinely populated Dust/FineDust/
SuperFineDust readings with no such scalar, so requiring it risks
silently dropping real air-quality readings on AC hardware this repo
hasn't seen yet. Reverted _has_sensor_type to item-type presence only
(as before #170) and moved the #166 fix to enabled_default=False on
all five entities instead -- same conservative, non-existence-gated
treatment already used for tropical_night_mode and the fridge/cooktop
precedents it was modeled on. Golden fixtures and tests updated to
match; the five sensors are bound-but-disabled on windfree/#17-style
boards again rather than unbound.
Also added icons for the AC fan_mode values #170 missed -- the raw
numeric labels ("1".."5") that TP1X_DA-AC-RAC-01001 and the window-AC
board report instead of turbo/max -- and swapped the whole fan-speed
icon family to mdi:fan-speed-1/2/3 for a more purpose-built look than
the generic speedometer, applied consistently to both the AC climate
card and the air purifier fan. Fixed motiondirect/motionindirect to
match core's smartthings integration's arrow pairing (previously
inverted).
Known limitation, not fixed here: enabled_default only affects newly
registered entities. Anyone who already has tropical_night_mode or the
five air-quality sensors enabled from #164 (a narrow window before
this fix, but real) won't see them auto-disable -- they'd need to
disable them by hand in Settings > Devices > Entities. A real fix
needs a one-time entity-registry migration, which this integration has
no existing infrastructure or test coverage for; scoping that felt
like its own follow-up rather than something to bolt on here.
HA core's bundled smartthings integration (the cloud counterpart to
this same Samsung AC feature set) uses mdi:weather-dust for its
wind_free preset rather than a generic windy icon -- a better fit for
a feature about avoiding direct airflow, not blowing harder. Match it
for both the AC climate preset and the air purifier fan preset.
HA only consults icon-translation state icons when the entity has no
static icon of its own (Entity.icon, if set, always wins -- see
homeassistant.helpers.entity's state_attributes construction). Audited
every entity with a labelled state/state_attributes catalog in
translations/en.json against its descriptor's icon= setting: every
select (cycles, courses, brightness levels, ...) and most sensors
already carry a fixed icon in code, so per-state icons there would be
silently shadowed. The three that don't -- air_purifier_fan's
preset_mode, machine_state, and connection_mode -- get one per value
here.
HA's core climate component already ships default icons for common
preset_mode/fan_mode values (eco, away, sleep, auto, low/medium/high,
...), but this integration's own values -- WindFree (nano/nanosleep),
Quiet, Smart, Speed, Long wind, the motion-aware direct/indirect
presets, Dry comfort, 2-Step, and the turbo/max fan speeds some boards
report -- fall outside that vocabulary and rendered with the generic
circle-dot fallback (the icon the #169 screenshot is missing). Adds
icons.json with an icon per value, mirroring the state-label catalog
these same values already have in translations/en.json.
Issue #166 (ARxxTXFCAWKNEU, board ARTIK051_PRAC_20K) reported tropical
night mode, clean level, dust, fine dust, odor, and super fine dust
entities showing up even though the reporter's units have no such
physical features. All six were added in #164.
The Sleep_<N> options token backing tropical_night_mode is present in
every AC dump on record regardless of confirmed reality, so there's no
usable signal at boot time -- it's now registered but disabled by
default (matching the precedent already set by fridge.rack_count /
cooktop.paired_hood_model), letting units that do have it opt in.
/sensors/vs/0's item-type list has the same problem (all five types
always listed, permanently zero on this board), but there turned out
to be a real tell: a top-level x.com.samsung.da.cleanLevel scalar is
present only alongside genuinely populated readings on every dump on
record (tp1x_da_ac_rac_01011, the tp1x_da_ac_air air purifier fixture)
and absent on every all-zero ARTIK051_PRAC_20K dump, including both
#166 units and the original windfree/#17 fixtures this capability was
first verified against -- which, per their /information/vs/0, turn out
to be the same board revision as #166's units, so that "verification"
never actually proved a real sensor either. AIR_QUALITY's exists_fn now
requires that scalar, and the windfree/airconditioner golden fixtures
are updated to match (those five entities no longer bind there).
discover() only emitted a BoundEntity per capability *entity*, so a
coverage-only Capability (entities=(), used to mark a href as handled
elsewhere -- e.g. the AC climate card's wind/strength, wind/direction,
temperature/control hrefs) produced zero rows. The coordinator computed
its hot/warm href lists by walking `bound`, so every such href's
poll_tier was silently discarded and it fell back to the ~30s summary
poll only -- no sub-poll cadence and never attempted for OCF OBSERVE.
This is the root cause of issue #166's "up to a minute" lag for
remote-driven fan-speed changes: /wind/strength/vs/0 carries poll_tier
'warm' via airconditioner.COVERAGE but never reached
_hot_hrefs/_warm_hrefs, so it wasn't in the OBSERVE-attempt href list
and only refreshed on the summary poll.
discover() now takes an optional tier_log(href, poll_tier) callback
fired for every href a capability matches, entities or not. The
coordinator uses it directly instead of deriving tiers from `bound`.
Samsung pre-populates /alarms/vs/0 with one row per supported alarm type,
each carrying a '<Name>_OFF' placeholder code (no 'Deleted' state at all)
when that alarm isn't firing. common._active_alarm_codes only ever
filtered on 'Deleted' state, so every device using this shared capability
(including the AC family) showed these inert placeholders --
'ErrorCode_OFF', 'FilterAlarm_OFF' -- as if they were live alarms.
Confirmed the '_OFF' suffix convention holds across every alarm code seen
in this repo's fixtures so far (ErrorCode_OFF, FilterAlarm_OFF, OV_E_OFF,
CT_E_OFF, WaterTankFull_OFF, AC_V_0002_OFF, all placeholders; DoorA_Opened,
FilterAlarm, SNSF_Reached, all genuinely active with no suffix) -- issue
#166's own dump has both a FilterAlarm_OFF placeholder and, on a second
unit, a live FilterAlarm/state=Created alert, which is what motivated
generalizing range_hood.py's existing (but narrower, ErrorCode_OFF-only)
special case into the shared helper instead of duplicating it further.
The other three points in #166 (filter-usage percentage vs. filterStatus
disagreement, an "air purification" config toggle the reporter says has no
physical effect, a "beep on/off" control) don't have a confirmed code fix:
the percentage math already matches the device's own filterUsage/
filterCapacity fields (filterStatus is a separate device-computed field we
already relay verbatim, not something we derive), the air-purify resource
is correctly wired to what the board reports and its absence from the
official app's own options list suggests an inert shared-board-profile stub
rather than an integration bug, and a "Beep volume" NumberDesc keyed off
the same Volume_100 option both dumps report already exists (0 mutes it).
Two real bugs, both latent (no shipped fixture exercised them), plus a
consistency gap and a couple of correctness/DRY nits flagged by review:
- async_set_fan_mode resolved a fan_mode label against the static
_FAN_TO_DEVICE reverse map before checking whether the resulting code is
actually one of the unit's own supportedModes. A board using non-standard
wind-strength codes while still spelling a standard-looking label in
modesName (e.g. codes "31"-"33" named "Low"/"High"/"Turbo") would silently
write a code ("1"/"3"/"4") the device never advertised. Now validates the
static hit against the unit's own supported codes before trusting it,
falling through to the live modesName scan otherwise.
- air_purifier.WIND_STRENGTH_FAN reused key='fan', the same key as FAN in
the same registry -- BoundEntity's unique_id is built from key alone, not
href, so a board reporting both hrefs would have one fan entity silently
shadow the other. Renamed to 'wind_strength_fan' (translation_key
unchanged). No shipped fixture reports both hrefs today, but the two caps
living in the same registry made this a real latent hazard, the exact one
AIRFLOW_GENERIC's own comment already documents and deliberately avoids.
- microwave.py's cooking_mode select still used a static, union-of-all-
dumps mode list, even though both shipped microwave fixtures already
report x.com.samsung.da.supportedModes on /mode/vs/0 -- the same shape
oven._oven_mode_options was just built to prefer over exactly this kind
of static list (issue #138's follow-up, this same PR's skill update).
ME7500D advertises 4 modes; the select was offering 11. Applied the same
live-first, static-fallback pattern.
- Added the issue #152 fixture the microwave lamp fix was missing (the
SKILL.md step this PR itself added asks for one).
- climate.py's _legacy_airflow rebuilt a 2-key presence dict from
coordinator.resource()'s truthiness, which collapses "href absent" and
"href present with an empty {} rep" to the same falsy value -- while
is_legacy_board (and discover()'s own binding) test key membership, not
truthiness. Simplified to pass last_resources through directly, matching
is_legacy_board's actual contract instead of a cheaper approximation of
it, so the "can never disagree" claim in both docstrings is actually true.
- Hoisted the 'power' payload branch duplicated verbatim across
_airflow_fan_write/_fan_write/_wind_strength_fan_write into one
_power_write helper (registry/capabilities/air_purifier.py).
- Removed two now-unused imports (test_air_dresser_capabilities.py,
test_air_purifier_vtww_fan.py) and replaced a tautological
code-in-_DEVICE_TO_FAN check with one that actually exercises the live
climate entity's fan_modes/fan_mode (test_climate_ac_modes.py).
- Fixed a pre-existing (not from this PR) no-op test on main --
test_registry_reproduces_golden_state_keys_for_induction_cooktop computed
golden/state_keys and never asserted on them.
756 tests pass.
TP1X_REF_21K's EU region variant reports a bare resource-monitoring
poll-interval config (minPeriod in ms) the US variant doesn't -- the only
unbound href keeping the coverage-gap repair open. Door sensors, the
reporter's actual ask, were already covered generically by
fridge.DOOR_GENERIC/DOORS_FALLACK.
This board (a floor-standing + wall-mounted indoor unit pair sharing one
outdoor unit and one local IP) reports no oneUiVersion and carries the
'_FAC_' modelNum token, which no existing routing rule matched -- it fell
back to 'unknown' and exposed nothing but a power switch, with no climate
entity generated at all (both issues' reported symptom).
Once routed to the existing airconditioner registry, it binds cleanly
against the exact same CLIMATE composite every other room-AC family uses --
same Cool/Dry/Wind/AIComfort mode vocabulary, same wind-strength/humidity/
filter/energy resource shapes already modeled. Only two hrefs are unique to
this board: /subdevices/vs/0 (an opaque paired-subdevice id list -- issue
#150 asked whether the second indoor unit can be controlled separately;
it can't through this or any other resource in the dump, the same
"remote device ids, not locally actionable" role as the existing
/remotedeviceinfo/vs/0 ignore) and /runn/vs/0 (a single undocumented int
with no supported-values list to interpret). Both added to _AC_IGNORED
rather than guessed at.
The lamp SwitchDesc was modeled on issue #137's dump, which only ever
showed 'Lamp_Off' -- 'On' was never actually confirmed as the paired
value. Issue #152's ME7500D dump (same TP1X_DA-KS-MICROWAVE-01051 family)
is the first to report a real non-Off value, and it's 'Lamp_High' (a
brightness level), not 'Lamp_On'. So the switch always read as off
regardless of the device's real state, and toggling it on wrote a token
('On') the device has never been observed to accept -- matching the
reported "light control does not have any effect."
value_fn now treats any non-Off/non-absent value as on; write_fn now
writes back 'High'/'Off', the two tokens actually confirmed live, instead
of the never-confirmed 'On'.
The reporter's suspicion about the fan is unconfirmed and this dump's own
/hood/fanspeed/vs/0 shape already matches the no-separate-power case
fan.py's LocalThingsRangeHoodFan handles correctly (issues #137/#142), so
no fan change was needed here. A second dump attached in a comment on this
issue (model ME8000T, a large combi wall-oven with a very different mode
vocabulary) reports its own distinct gap and doesn't resolve to any known
device type at all -- that's a separate, substantial device-support task
left for its own follow-up rather than folded into this fix.
This board reports no oneUiVersion and no modelNum token any existing
family routed on, so it fell back to unknown -- exposing nothing but power
even though most of its resources (air quality sensors, HEPA filter,
device-active, diagnosis, plumbing hrefs) are already handled generically
by the existing air_purifier registry via the '-VTWW-' modelNum fallback.
The one genuinely new piece is its fan: this board reports wind strength as
numeric codes ("87"/"89"/"90"/"91") on /wind/strength/vs/0 with a separate
modesName array ("SMART"/"MAX"/"WINDFREE"/"Sleep") giving the actual names,
unlike the existing TP1X_DA-AC-AIR family where supportedModes IS the name
list already. Generalized LocalThingsAirPurifierFan to resolve a mode code
through modesName when present (same live-label pattern as climate.py's AC
wind-strength fix, issue #155) instead of adding a second hardcoded fan
class, and added WIND_STRENGTH_FAN reusing the same 'air_purifier_fan'
translation catalog -- both board generations land on the identical
smart/max/windfree/sleep vocabulary already labelled there.
/mode/convenient/vs/0 is empty on this dump and added to COVERAGE alongside
the existing plumbing hrefs this board also shares with the TP1X_DA-AC-AIR
family.