Commit Graph
359 Commits
Author SHA1 Message Date
Marc Billow 57e06c5ccb De-duplicate the legacy ARTIK051 AC board-generation test (#161)
capabilities/airconditioner.py's is_legacy_board() (renamed from the
private _is_legacy_board -- it's now a cross-module helper) and
climate.py's _legacy_airflow() implemented the same "does this board have
/airflow/vs/0 but no /wind/strength/vs/0" test independently, one via
literal href strings and the other via coordinator.resource() truthiness.
is_legacy_board() now uses the module's own HREF_AIRFLOW/HREF_WIND_STRENGTH
constants, and _legacy_airflow() delegates to it via a minimal two-key
presence dict (cheaper than a full last_resources snapshot copy) instead of
re-implementing the check, so the token entities and the climate card's
legacy read/write paths can't drift apart on which board generation is in
play.
2026-07-28 13:31:37 +00:00
Marc Billow c8597532b0 Stop collapsing a genuine fivepercentHumidity=0 reading to unknown (#160)
#146's zero-as-"not measuring" carve-out was meant for ARTIK051 boards'
plain x.com.samsung.da.humidity field, which only populates while Air
monitoring is briefly on and zeroes out afterward. It was accidentally
applied to fivepercentHumidity too, which every other AC board relies on
and which has never been documented getting stuck at zero -- so a real 0%
reading on those boards silently became "unknown". Only the humidity
fallback field now collapses 0; fivepercentHumidity passes 0 through as a
real reading.
2026-07-28 13:28:46 +00:00
Marc Billow 1cdad7f84c Read oven cook modes live from the device instead of a hardcoded list
Follow-up to the issue #138 fix: rather than hand-adding
ConvectionRoast/KeepWarm/BreadProof/AirFryer/Dehydrate/SelfClean/SteamClean
to oven._OVEN_MODES, read them from the device's own /mode/vs/0
supportedModes when it reports one, falling back to the static
NV7000BS-era guess only when it doesn't. Matches the adding-device-support
skill's preference for device-reported option lists over hardcoded ones,
and the SelectDesc's write validation now checks the same live list it
displays instead of a separate static tuple.
2026-07-28 13:26:41 +00:00
Marc Billow 1fb27c30ae Skill: call out preferring dynamic select options over hardcoded lists
Prompted by issue #138's fix, which extended oven._OVEN_MODES with newly
confirmed modes instead of reading them from the device's own
supportedModes field via options_field -- the pattern laundry.py already
uses for buzzer/finish sound. Document that preference so future
device-support work reaches for options_field/a callable first and treats
a static tuple as a last resort, not the default.
2026-07-28 13:18:46 +00:00
Marc Billow 123455a873 Add missing oven cook modes for NE63A6511SS/AA range (issue #138)
The range/oven-combo device in issue #138 (NE63A6511SS/AA, no
/information/vs/0) already resolves cleanly to the range registry via the
issue #74 for_device_by_resources fallback, with zero unbound hrefs -- the
reporter was just on an older release (0.11.1) predating that fix.

However its /mode/vs/0 supportedModes advertises ConvectionRoast, KeepWarm,
BreadProof, AirFryer, Dehydrate, SelfClean, and SteamClean, none of which
were in oven._OVEN_MODES. Since range.py reuses oven.OVEN_MODE's SelectDesc
wholesale, those modes were silently rejected by the mode select's write
validation and missing from its options. Extend the confirmed mode list and
lock in a scrubbed fixture, golden, and test for this dump.
2026-07-28 13:12:40 +00:00
Marc Billow f5be651430 Merge pull request #146 from perseus177/artik051-krac-legacy-ac
Support ARTIK051_KRAC_18K air conditioners (issue #136)
2026-07-28 08:00:48 -05:00
Marc Billow 6b28272647 Enhance README with GitHub badges
Added badges for GitHub stars, watchers, releases, and validations.
2026-07-27 23:26:01 -05:00
Marc Billow cc570533a0 Merge pull request #149 from mbillow/claude/ticket-127-device-coverage-xsb0m4
Distinguish not-yet-fetched stub reps from confirmed-empty ones (issue #127)
v0.14.0
2026-07-27 22:25:38 -05:00
Marc Billow 6239706065 Keep entity._is_included's default gate permissive on confirmed-empty reps
Opus review of #149 caught that swapping is_stub_rep(rep) in for the
default field-presence gate (not just the 9 hand-audited exists_fn call
sites) was too broad: it silently excludes entities on ANY resource whose
normal, valid state includes reporting {} -- /alarms/vs/0's {} is
fridge.py's documented no-alarm state, not an absence signal, and it's not
the only one (job_beginning_status, diagnosis_status, sabbath_mode,
defrost_delay, ice_maker_enabled all lost entities on real fixtures under
the broader change). That's the opposite of #127's fix: a real fridge
would have dropped its alarm sensor on first-poll timing, not just its
phantom energy sensors.

Restored the default gate to include on either a stub or a genuinely-empty
rep -- verified byte-identical to the pre-#149 baseline across all 40
fixtures, apart from the 9 deliberately-audited exists_fn sites (energy
meter, self-check error, cooktop burner, range-hood auto-op), which are
unaffected and still fix #127. Added tests/test_entity.py exercising
_is_included directly (previously untested) and fixed two now-stale
"not rep" doc references the review also flagged.
2026-07-28 03:18:23 +00:00
Marc Billow 33786ad584 Distinguish not-yet-fetched stub reps from confirmed-empty ones (issue #127)
parse_device0_batch used to collapse /device/0's {"href": "..."} "no data
yet" marker into a plain {}, indistinguishable from a resource the device
had actually polled and confirmed empty. Every exists_fn using the "not
rep or ..." stub carve-out (and entity._is_included's default field-gate)
then treated both the same way, creating phantom always-"unknown" entities
for any resource a model simply doesn't support (e.g. GSzabados's fridge's
/energy/consumption/vs/0).

is_stub_rep() now recognizes only the literal {"href": ...} marker as a
stub; a genuine {} is treated as the device's real (if empty) answer and
gates the entity off like any other missing field. Updated the energy
meter, self-check error, cooktop burner, and range-hood auto-operation
exists_fn call sites, plus three golden fixtures that had baked the
phantom-entity behavior in as "expected".
2026-07-28 02:57:30 +00:00
Marc Billow e60fdf26d2 Merge pull request #148 from mbillow/claude/triage-issues-144-147-yv80ab
Fix water-purifier hot-water lock mislabel, add range-hood after-run support
2026-07-27 21:31:31 -05:00
Marc Billow 9011f84787 Model after_run_progress as a percentage, fix Dutch after-run-progress name
runningProgress's own field name states its domain, so restore unit='%' and
state_class='measurement' rather than leaving it an opaque passthrough --
that hedge made sense for activationState (no supported-values list, no
write contract to invent) but not here, where the name itself is the
evidence.

"Nadraaien voortgang" also wasn't idiomatic Dutch (nouns don't stack that
way); "Voortgang nadraaien" matches how the rest of the catalog compounds
these names.
2026-07-28 02:29:01 +00:00
Marc Billow 0640ac3477 Fix hotwater_lock key-collision hazard, address Opus review of triage fixes
An Opus review of the previous commit found that giving the switchHotwater
fallback and LOCK.hotwater_lock the same key introduced a real bug:
adapter.flatten() (the source of coordinator.data, which every switch's
is_on reads) only ever honours exists_fn, never entity.py's implicit
own-field-presence default that gates plain registration. With only one
side of the pair gated, both descriptors still wrote the same key into the
flattened state dict, and whichever was processed last -- decided by
device-reported href order, not correctness -- silently won. Reproduced
with the existing coffee fixture: reversing resource order flipped
hotwater_lock from correct (False) to a stuck True.

Fixed by gating both sides symmetrically via a shared tri-state helper that
also treats an unfetched /status/lock/vs/0 stub as "outcome pending" rather
than "confirmed absent" -- otherwise the stub window let both descriptors
pass exists_fn at once, which would have registered two switch entities
with the same unique_id. The fallback also re-asserts its own field's
presence, a check it used to get for free before it shared LOCK's key.

Also addresses two smaller findings from the same review, both in the
range-hood after-run capability (#147): runningProgress's unit='%' was a
guess from a single "0" sample with no supported-values/range field to
confirm the domain -- inconsistent with treating activationState as
read-only for the same "don't guess" reason -- so it's now a bare
passthrough sensor; and entity_category='diagnostic' was dropped from the
two read entities since after-run is a feature the user actively watches
and cancels via the (correctly uncategorized) button, not passive
diagnostics.
2026-07-28 02:25:42 +00:00
Marc Billow b47dabeb31 Merge pull request #143 from mbillow/claude/microwave-fanspeed-coverage-nmvc86
Microwave vent fan and kimchi-refrigerator compartment coverage (#137, #142, #26)
2026-07-27 21:13:03 -05:00
Marc Billow cc3ce12aa4 Fix range hood fan power targeting and kimchi mode write validation
- _speed_zero_is_off now keys off the hood resource's own
  settableMinFanSpeed/supportedFanSpeed fields instead of asking whether
  the device has any power resource at all, so a combi appliance's cavity
  /power/0 can no longer be toggled off by turning off just the vent fan.
- async_turn_on() no longer resets an already-running fan to its lowest
  speed when called without a percentage.
- _has_separate_power() reads through the O(1) resource cache instead of
  copying the full resource snapshot on every property access.
- _kimchi_mode_write rejects values the compartment didn't advertise in
  supportMode instead of writing them blind.
- kimchi_ripening_status no longer lowercases its value, since it has no
  enum catalog entry to translate the lowercased token back through.
- Documented why KIMCHI_DOOR_GENERIC isn't deduped against the /doors/vs/0
  aggregate fallback on the one fixture that reports both.

Adds regression tests for the combi-appliance power targeting, the
already-on turn_on no-op, kimchi mode write validation, and a kimchi
select display/write casing round-trip; a translation-coverage guard for
kimchi_zone_mode codes mirroring the existing AC preset one.
2026-07-28 02:10:01 +00:00
Marc Billow 5055d6bb72 Fix water-purifier hot-water lock mislabel, add range-hood after-run support
Water purifier (#144, #145): /favorite/hotwater/vs/0's switchHotwater field
is a Locked/Unlocked hot-water lock, not a "favorite enabled" flag. It's the
same lock as LOCK.hotwater_lock, just surfaced through this href on boards
that don't populate /status/lock/vs/0's hotwaterLock -- the two now share
the hotwater_lock key/translation, gated so only one is ever active.

Range hood (#147): binds /afterrun/vs/0 (after-run activation state,
progress, and a cancel button), clearing the last unbound href for
AHD-WW-TP1-22-COMMON.
2026-07-28 02:00:57 +00:00
perseus177 55c7b88a8f fix(airconditioner): label the 2Step preset and test presets by their HA value
The legacy Comode codes resolve through the same dynamic resolver as a real
convenient resource, so Nano lands on the existing 'nano' preset (already
labelled WindFree) rather than on a 'windfree' value of its own -- the new
tests asserted the label instead of the value. 2Step had no catalog entry in
either language and would have surfaced as the raw code.

Also updates the existing five-percent-humidity test, which reached into the
descriptor's field/value_fn directly, to the rep_fn the fallback needs, and
covers the fallback itself.
2026-07-28 02:38:28 +02:00
Marc Billow 265d94eded Add kimchi-refrigerator compartment coverage (issue #26)
TP2X_REF_20K-class 3-compartment kimchi refrigerators report each
compartment's storage mode and ripening status/timer on
/status/kimchi/<slot>/vs/0, plus a top-compartment door sensor on
/kimchidoors/top/vs/0 -- all previously unbound. Bind them as pattern
capabilities (fridge.KIMCHI_ZONE, fridge.KIMCHI_DOOR_GENERIC), deriving
the per-compartment entity key and display name from the href's
top/middle/bottom segment, the same way DOOR_GENERIC/TEMP_CURRENT_GENERIC
already do.

Storage-mode option labels were translated directly from the reporter's
own SmartThings app screenshots rather than guessed from the raw device
codes or their English paraphrase, confirming the on-screen option order
matches supportMode's array order (including the freezer triplet's
-19/-21/-17°C -> Standard/Strong/Weak mapping).

Also tighten FLEX_ZONE's exists_fn: this device's /mode/vs/0 also
populates modes/supportedOptions, but with a token shape that never
overlaps (a "_[n]:[n]" suffix supportedOptions carries that modes never
repeats), so the existing "supportedOptions is nonempty" check let the
entity bind anyway and get stuck permanently on "unknown". Requiring an
actual resolvable value keeps it working for the RF9000/Bespoke-class
fridges it was built for while leaving it absent here.
2026-07-28 00:35:21 +00:00
perseus177 3032b0c032 test(airconditioner): lock in the ARTIK051_KRAC_18K surface
Fixture is a scrubbed diagnostics dump from the unit the writes and
calibrations were verified on; the issue #136 unit is the same model with a
slightly different token set (no Spi, FilterTime_5460, OutdoorTemp_81), which
the presence gating handles the same way.

Covers the pieces the golden's state_keys can't: that the token entities stay
off newer boards, that an absent token yields no entity, that humidity's zero
reads as unknown, that fan/swing/preset read and write through /airflow/vs/0
and the Comode token, and that a board with /wind/* and /mode/convenient/vs/0
still takes the resource paths.

Refs #136
2026-07-28 02:28:59 +02:00
perseus177 5b30099c42 feat(airconditioner): fan, swing, presets and option-token settings on ARTIK051 boards
This board generation predates every AC dump the registry was built from and
differs in three ways, all handled here behind presence checks so no other
family's behaviour changes:

* No /wind/* resources at all. Fan speed and vane direction share a single
  /airflow/vs/0 resource, whose speedLevel uses the same 0-4 scale as
  _DEVICE_TO_FAN and whose direction uses the same codes as _DEVICE_TO_SWING,
  so the existing maps are reused rather than duplicated. The resource reports
  no supportedModes, so the full scale is offered.
* No /mode/convenient/vs/0. The convenient-mode preset is a Comode_* token in
  /mode/vs/0's options, synthesised into a convenient-shaped rep so the
  existing dynamic preset resolver keeps working unchanged. Codes were learned
  by driving one unit through its cloud integration and reading the token back
  each time: Nano is what the app calls WindFree, plus Quiet/Comfort/2Step/
  Speed (Fast Turbo) and Off.
* Several settings that newer boards expose as dedicated resources are options
  tokens here: SPI, auto clean, air monitoring, beep volume, Good Sleep,
  outdoor temperature and filter time. Writes reuse option_write's single-token
  merge, the same mechanism the display light already uses on this href.

Newer families carry some of the same tokens *alongside* dedicated resources
for those settings, so the token entities are gated on this generation's
resource shape (/airflow/vs/0 present, /wind/strength/vs/0 absent -- the same
test the climate entity's fan/swing fallback uses, so the two can never
disagree). Without that gate they duplicated auto clean on TP1X/TP2X boards
and applied a calibration from this board to theirs.

Two calibrations, both from hardware rather than from the token names:
OutdoorTemp is offset by 55 (token 75 against a 20.3 C outdoor thermometer in
the same install, token 74 against a 19.4 C forecast; Fahrenheit fits far
worse), and FilterTime is tenths of an hour (token 1710 while the official
Samsung app displayed "171 hours 0 minutes" for the same unit's filter).
Whether filter time counts up or down is deliberately not claimed: it was seen
rising while the unit ran, which contradicts the app's "remaining" wording.

Humidity now falls back to the plain x.com.samsung.da.humidity field where
fivepercentHumidity is absent, still as one entity rather than two, and 0 reads
as "not measuring" rather than 0% -- on this board the field only carries a
reading (51%, matching the same unit's cloud integration) while Air monitoring
is on, which the unit switches back off by itself after about a minute.

Fan, swing, preset, SPI and beep-volume writes were confirmed by read-back on
hardware. Good Sleep's upper bound is a guess (only 0 has been observed), and
/airflow/0 -- the OCF-standard mirror of the vendor resource -- is ignored
rather than modelled, since air_purifier.py found the opposite reliability
ordering between these two hrefs on its own family.

Refs #136
2026-07-28 02:28:33 +02:00
perseus177 75f2be7aaf fix(registry): resolve ARTIK051_KRAC_18K to the airconditioner registry
Room air conditioners on the ARTIK051 board (ARTIK051_KRAC_18K, issue #136)
report no oneUiVersion and carry a '_KRAC_' token in modelNum. The existing
'_RAC_' check can't see it -- the 'K' sits between the underscore and 'RAC' --
and the consumer-prefix fallback only covers washers/dryers/dishwashers, so
these units fell back to 'unknown': 8 of their 19 resources ended up unbound
and the device exposed nothing but a power switch.

Same ARTIK051 board family as the '_TVTL_' air purifier handled just below.
2026-07-28 02:28:13 +02:00
Marc Billow e9281aaead Bind microwave built-in vent fan's /hood/fanspeed/vs/0 (issues #137, #142)
Combi microwave units report their vent fan on the same resource shape a
standalone range hood uses, so reuse range_hood.HOOD_FAN directly in the
microwave registry. Unlike a standalone hood, this board has no sibling
/power/0 or /power/vs/0 resource, so LocalThingsRangeHoodFan now falls
back to treating fan speed 0 as the off state when no separate power
resource is present. Also gate HOOD_FAN's automatic_operation sensor on
field presence, since this board doesn't report it.
2026-07-28 00:18:38 +00:00
Marc Billow c19800ac00 Merge pull request #135 from QuiteYellow/fix/stale-dtls-session-fixed-source-port
Bind a fixed DTLS source port per device to evict stale sessions
2026-07-27 18:40:36 -05:00
Marc Billow ed5e8e57c8 Merge pull request #140 from mbillow/claude/v0-13-0-issue-triage-9zfjbj
Split microwaves into their own device type instead of the oven registry
2026-07-27 17:10:10 -05:00
Marc Billow f063666291 Merge pull request #139 from mbillow/claude/air-purifier-fan-controls-ananzq
Add real fan-speed control for ARTIK051_TVTL air purifiers (issue #56)
2026-07-27 17:06:59 -05:00
Marc Billow 5af9d951c6 Simplify README microwave row label 2026-07-27 22:05:54 +00:00
Marc Billow a1f14cd633 Split microwaves into their own device type instead of the oven registry
Microwaves (combi and plain) were routed onto the oven registry (issue
#121), which meant entities carried oven-flavored keys (oven_state,
oven_mode, oven_setpoint) and inherited oven-specific behavior that's
wrong for this family: a 30-270C setpoint range instead of this family's
actual 40-200C, a cooking-mode list missing MicroWave/MicroWaveGrill/
MicroWaveConvection/KeepWarm entirely, and a lamp switch that read/wrote
the oven's 'UpperLamp' option token instead of this family's 'Lamp' token.

Adds a microwave device type (by_type/microwave.py,
capabilities/microwave.py) that reuses the oven board family's shared
operational-state/door/connected/recipe-cook capabilities but defines its
own cooking-mode, setpoint, and cavity capabilities with the corrected
bounds/vocabulary, plus a new power_level sensor for the cavity's Watt
setting that was previously unexposed.
2026-07-27 22:05:54 +00:00
Marc Billow 2be742489b Fix airflow fan power-href preference and unique_id collision risk
Opus review of the previous commit caught two real bugs. The airflow fan's
power writes preferred /power/vs/0, copied from the TP1X fan class -- but
that order is only harmless there because TP1X never reports /power/0 at
all. This family's dumps carry both hrefs, and the power_switch entity is
unconditionally bound to /power/0 when present, so the fan was writing to
a different resource than power_switch reads/writes, leaving the two
entities disagreeing until the next poll. Flipped to prefer /power/0,
matching the range hood's fan and common.POWER_GENERIC.

Also renamed the new FanDesc's key from 'fan' to 'airflow_fan': BoundEntity
unique_ids are derived from key alone, not href, so it collided with
air_purifier.FAN's own 'fan' key on the (currently unobserved, but
unenforced) possibility of a board reporting both.

Added a platform-level test file covering the power-href preference and
percentage<->speed-code mapping, mirroring test_range_hood_fan.py's harness.
2026-07-27 21:48:23 +00:00
Marc Billow d4583b3240 Add real fan-speed control for ARTIK051_TVTL air purifiers (issue #56)
/airflow/0's speed was left read-only because the first round of diagnostics
wasn't conclusive (0 for both Auto and High, 3 for Low/Medium and Sleep) --
likely because all five dumps were captured within about a minute of each
other, faster than this integration's own poll cycle could settle each
change. A second round, captured 60-90s apart per setting on two independent
units, confirmed a clean monotonic 0-4 mapping across Auto/Sleep/Low/Medium/
High instead.

Builds an ordered-speed fan off that confirmed range, the same SET_SPEED
shape as the range hood's fan -- this board never self-reports a
supportedModes-style label list, so there's no named-preset table to
preserve, just percentage steps over the raw code. /airflow/vs/0's vendor
speedLevel stays a read-only fallback since it was unreliable in that same
second round.
2026-07-27 21:32:44 +00:00
Jack Nagy 0ebcfb00ff fix: bind a fixed DTLS source port per device to evict stale sessions
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).
2026-07-27 19:17:39 +01:00
Marc Billow 7c68d478af Bump version to 0.13.0 v0.13.0 2026-07-27 14:54:26 +00:00
Marc Billow 3c15ca810b Merge pull request #132 from mbillow/claude/triage-issues-111-127-on5l50
Triage issues #111-#131: AC/oven/air-purifier detection fixes, new vacuum-station type, reconnect log noise
2026-07-27 09:46:11 -05:00
Marc Billow e9eb38740d Deduplicate ISO-timestamp parsing and document the new device type (review follow-up, issue #131)
- vacuum_station._parse_iso_utc was a verbatim copy of
  water_purifier._parse_iso_utc; promoted to common.parse_iso_utc and
  pointed both families at it. Also made it tzinfo-aware rather than
  unconditionally overwriting with UTC -- harmless today since every
  dump seen is a bare or Z-suffixed UTC timestamp, but a board that
  ever emits a real offset would otherwise have it silently clobbered.
- Added the new vacuum_station type to the README's supported-appliance
  table, and noted that combi microwaves route through the oven
  registry.
2026-07-27 14:43:02 +00:00
Marc Billow f37c9ec4ef Fix air-purifier fan power write and silent preset-mode rejection (review follow-up, issue #130)
- The fan's power write hardcoded /power/vs/0 while is_on already read
  /power/0 as a fallback -- a board reporting only the OCF resource
  would show correct state but silently no-op on every turn-on/off.
  Mirrors LocalThingsRangeHoodFan's existing _power_payload pattern:
  target whichever power href the board actually reports.
- async_set_preset_mode fell off the loop silently on an unmatched
  mode with no log and no error, unlike the rest of this codebase's
  write-rejection handling. Logs a warning now.
- Deduplicated HEPA_FILTER's usage-percent calculation, which was an
  inline reimplementation of airconditioner._filter_usage_percent;
  promoted the shared logic to common.filter_usage_percent and pointed
  both families at it.
- Gave air_purifier.SOUND_MODE its own translation_key instead of
  defaulting to the same catalog entry laundry.SOUND_MODE uses. That
  entry's state table is {voice, tone, mute}; this board's is
  {mute, buzzer} -- sharing it left 'buzzer' with no label.
2026-07-27 14:42:34 +00:00
Marc Billow d26e7b957b Fix unreachable reconnect-warning threshold (review follow-up, issue #119)
The 60s/5-reconnect window from the original fix could never actually
fire: consecutive reconnect attempts are never closer together than one
summary poll interval (30s) plus the 5s reconnect pause, so at most ~2
timestamps can ever land inside a 60s window regardless of how unhealthy
the connection is. That silently downgraded every reconnect to INFO
permanently, including the persistently-broken case the change was
supposed to still surface at WARNING.

Widen to a 300s window with a threshold of 3, which is reachable under
sustained failures and still a reasonable proxy for the README's
"actually broken" case.
2026-07-27 14:41:43 +00:00
Marc Billow b31f4f0c40 Merge pull request #128 from pedroperosin/fix/climate-set-temperature-hvac-mode
Honour hvac_mode in the AC climate set_temperature
2026-07-27 09:30:19 -05:00
Marc Billow 8b44fd5710 Add device support for the stick-vacuum clean/auto-empty station (issue #131)
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.
2026-07-27 14:11:06 +00:00
Marc Billow 4e1eb7d03d Add fan-mode control for TP1X_DA-AC-AIR air purifiers (issue #130)
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.
2026-07-27 14:08:32 +00:00
Marc Billow e44fef7085 Route combi microwaves onto the existing oven registry (issue #121)
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.
2026-07-27 13:41:36 +00:00
Marc Billow 6f10a3b796 Support 2-axis wind oscillation and overload-protection status on newer WindFree boards (issue #126)
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.
2026-07-27 13:41:04 +00:00
Marc Billow 2c8a765036 Downgrade a lone poll-failure reconnect to info (issue #119)
Samsung's firmware occasionally drops the DTLS session briefly --
normal appliance-side behavior per the README's "Known device
behavior" section -- so the coordinator recovering from that on its
own doesn't need a WARNING. Only escalate once reconnects pile up
within a trailing 60s window (5+), matching the README's own "more
than a handful per minute" definition of an actually broken
connection.
2026-07-27 13:40:32 +00:00
Marc Billow f62b4aeec8 Detect ARA-WW-TP1-22-COMMON wall-mount RACs as airconditioner (issues #115, #116, #117, #120)
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.
2026-07-27 13:40:12 +00:00
Marc Billow 46dd91ce32 Add regression coverage for NE8300D range (issue #112)
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.
2026-07-27 13:39:51 +00:00
Marc Billow 085e80c92c Add regression coverage for WA55A7700AV washer (issue #111)
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.
2026-07-27 13:39:30 +00:00
Pedro a9f21982e7 Honour hvac_mode in the AC climate set_temperature
HA's set_temperature service forwards an optional hvac_mode to the entity, and
the entity is expected to apply it. The AC climate entity ignored it and only
wrote the setpoint, so a set_temperature call carrying hvac_mode (e.g. a
dashboard "turn on to Auto 24" button) set the temperature but never changed the
mode or powered the unit on. Apply the mode first -- which powers the unit on
when it was off -- then the setpoint.
2026-07-27 09:57:31 -03:00
Marc Billow ef23f5ec32 Merge pull request #114 from mbillow/claude/pr91-finish
Add cool-only global RAC support: WindFree, Auto mode, and display light
2026-07-27 00:35:48 -05:00
Marc Billow 5547c401ed Address independent review findings on the RAC finish-up branch
- 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.
2026-07-27 05:11:58 +00:00
Marc Billow fa8f1b8675 Add cool-only global RAC support: WindFree, Auto mode, display light
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).
2026-07-27 05:00:06 +00:00
Marc Billow af00c2b938 Merge pull request #110 from mbillow/claude/device-support-issue-107-coffee
Add coffee-maker capabilities to the water purifier registry (issue #107)
2026-07-26 23:40:03 -05:00
Marc Billow 1177f0ed64 Merge pull request #109 from mbillow/claude/device-support-issue-106-wa-washer
Add WA (top-load washer) consumer-model prefix (issue #106)
2026-07-26 23:38:19 -05:00