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 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.
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.
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.
Since entity.py started routing named descriptors through the catalog
under desc.key, SamsungEntityDescription.name has been read for its
value nowhere -- only twice as a flag, to decide whether an entity was
translated at all. That left 148 English names duplicated between Python
and translations/en.json with nothing keeping them honest: six had
already drifted, invisibly, because editing the Python side changes
nothing a user sees.
So the field is gone, and translation_key defaults to desc.key. A
descriptor now sets translation_key only to share one catalog entry
across descriptors or to point at a differently-named one, and the
catalog is the only place an entity name exists.
Every descriptor resolves to exactly the translation key, icon, entity
category, enabled-default and gating it did before -- with one
deliberate exception: the hood fan, previously the sole descriptor with
no key at all, now resolves to 'fan'. That is inert, because fan.py sets
_attr_name = None so the entity presents as the device itself.
The three helpers that forwarded a name into a descriptor
(laundry.bool_option_switch, washer._bool_option_switch, air_purifier's
sensor table) lose that parameter. test_translations.py now requires a
catalog entry for every descriptor rather than only translated ones.
Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
PR #68 restated its own translation data in Python: a 60-line
TRANSLATED_SELECT_STATES table of frozensets duplicating every
entity.select.*.state key, a second _TRANSLATED_COURSE_TABLES table
naming which course tables have translations, and a strings.json that
was a 835-line byte-for-byte copy of translations/en.json save 43
[%key:...%] references. Each needed hand-syncing, and one was already
drifting.
Home Assistant loads exactly one file per language for a custom
integration -- translations/<lang>.json. It never reads strings.json and
never resolves [%key:...%]; both belong to Core's build tooling, which
custom integrations don't run through (hassfest skips a missing
strings.json and validates translations/en.json instead). So en.json is
the source, and the new catalog.py reads the keys and states back out of
it for the two decisions Python genuinely has to make:
- select._display() normalizes a raw Samsung option to a lowercase
state key only when the catalog knows it, else leaves the vendor's
casing alone. Derived sets are identical to the removed literals.
- laundry.cycle_select() keys off a device-reported course table only
when that table has an entry, else falls back to the name-only
'cycle' key. Translating Table_00 is now a translations-only change.
Also fixes six names that had already drifted between the Python
descriptors and the catalog, restoring HA's sentence case for two
generic ones (Auto release dry, Bubble soak) and taking the catalog's
wording for the rest, and adds a test so the vestigial descriptor names
can't silently disagree with the UI again.
Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
Six fridge SwitchDesc write_fns built their payload with
`'On' if p else 'Off'`. The switch platform passes the literal
string 'Off' on turn-off, which is truthy, so the guard always
produced 'On' -- turning these switches off silently re-sent On
and they could never be turned off:
- ICEMAKER_NIGHTTIME (ice.night.status)
- STATUS_LOCK helper (devicecontrol + device.sound)
- DEFROST_DELAY (delayDefrost)
- WELCOME_LIGHTING (status)
- CABINET_LIGHT dim (light.dimming.status)
- ICEMAKER_STATUS_FALLBACK (iceMaker)
Compare `p == 'On'` instead, matching the pattern the other
capability files already use. Adds a regression test asserting
every affected write_fn sends 'Off' on 'Off' and 'On' on 'On'.