719 Commits
Author SHA1 Message Date
Marc Billow 36f53da6a0 fix(translations): backfill cs.json with keys missing since cs.json was added
The Czech translation landed in commit d0c68fb (PR #233) and was
immediately out of date with en.json: subsequent commits added new
entity/option keys that didn't get backfilled. cs.json fell behind
in three buckets, all caught by test_every_language_mirrors_the_english_catalog:

  - 6 dryer cycle codes added to en/nl by commit 873ed56 (PR #237):
    '17' Super Speed, '21' Hygiene Care, '22' Silent Dry, '29' AI Dry,
    '2b' Self Tub Dry, '4c' Air Refresh. cs.json's dryer_cycle_table_03
    still had the pre-PR-#237 set.

  - 'finish_time_hysteresis_minutes' option added to en/nl by commit
    d905620 (PR #239). Same omission in cs.json.

  - 8 entities added to en.json by the issue-triage batch now in main
    (PR #224): binary_sensor.battery_charging, binary_sensor.child_lock,
    sensor.air_quality_standard, sensor.battery, sensor.co2,
    switch.dnd, time.dnd_end, time.dnd_start. cs.json had none of them.

nl.json was in sync, so the gap was specific to cs. Keys placed in
their natural alphabetical position within each section.
2026-07-31 15:32:36 -05:00
Marc Billow 6166f63570 Merge pull request #224 from mbillow/claude/issue-triage-batch-h4n2wq
Issue triage batch: #207, #189, #208, #181/#183, #196, #210 + contributing docs
2026-07-31 15:22:55 -05:00
Marc Billow b7d3f86ec7 fix: address code review feedback on issue-triage batch PR (#181, #183, #189, #196, #207, #208, #210)
common:
- Make KIDS_LOCK_GENERIC also a read-only BinarySensorDesc (device_class='lock'),
  flipping value_fn to not bool(v) so /kidslock/0 value=False and
  /kidslock/vs/0 kidsLock='Ready' render with the same polarity ('On'
  means open/unlocked per HA's lock device class). The old SwitchDesc
  form never honored device_class='lock' -- HA's switch platform only
  accepts 'outlet'/'switch' -- so the surface was a plain switch whose
  'On' meaning drifted across boards. Tests updated.

air_monitor:
- Add state_class='measurement' to dust/fine_dust/super_fine_dust so
  the readings feed HA long-term statistics (co2 already had it).
- Import _AIR_QUALITY_SENSORS from air_purifier instead of duplicating
  it byte-for-byte; update common.sensor_item_value's docstring to
  mention the third caller.

by_type/__init__: drop trailing whitespace on the new 'ASM' line.

translations/en.json + nl.json: move the new 'dnd' switch entry to its
correct alphabetical position (after display_light, before fast_preheat).

SKILL.md: add an explicit read-side rule to §5's educated-guesses
section -- guessed unit/device_class/state_class on a SensorDesc
silently mislabels readings forever with no 4.xx to catch it (unlike
guessed writes, which the device rejects). The prior air_monitor
docstring cited this carve-out as if it existed; now it does.

tests/water_purifier (issue #196): change the ailite fixture's
favorite.defaultTemperature from '85' to '50' so the test actually
reproduces the reported bug -- '50' is in showList only, so a
descriptor reading from supportedList would fail the assertion that
the current default is in its options list.
2026-07-31 15:17:18 -05:00
Marc Billow fd86c54f18 Merge pull request #233 from pedrodivisez/feat/ac-odor-controller-cs-translation
AC: bind SmartCoolClean odor-controller tokens; add Czech translation
2026-07-31 14:49:26 -05:00
Marc Billow e55c55c55e Merge pull request #239 from jelle514/Reduce-Estimated-finish-activity
Reduce Estimated-finish activity-log spam (washer/dryer/dishwasher)
2026-07-31 14:35:49 -05:00
Marc Billow 7a43fcb988 Merge pull request #237 from chill-uk/fix/dryer-cycle-mappings
Fix/dryer cycle mappings
2026-07-31 14:32:24 -05:00
Jelle Lauwers 9f15d06e84 Document the two new per-device options in the README
Bypass-remote-control already existed but was undocumented; finish_time
hysteresis is new. Both live under the same Configure > Device settings
menu, so cover them together as Part 4 rather than leaving a reader to
discover them by opening the options flow.
2026-07-31 20:54:29 +02:00
Jelle Lauwers 9ab3c5827c Rename finish_time debounce -> hysteresis to match actual behavior
The gate holds finish_time at its last reported value until a new one
differs by at least a configured number of minutes, regardless of how
long that difference has been building up -- a magnitude-based deadband,
not a time-based debounce (which would wait for the value to stay put for
N minutes before accepting it). debounce invited the wrong mental model
for anyone reading the option name or the code later, so rename it
throughout before the option name ships: CONF_FINISH_TIME_DEBOUNCE_MINUTES
-> CONF_FINISH_TIME_HYSTERESIS_MINUTES, SensorDesc.debounce -> hysteresis,
LocalThingsSensor._debounce/_debounced_value -> _apply_hysteresis/
_hysteresis_value, and the options-flow data key/translations to match.
2026-07-31 20:27:48 +02:00
Jelle Lauwers d90562017f Add configurable finish_time debounce to cut recorder churn further
Minute-rounding stopped identical values from re-logging, but finish_time
still updates on nearly every poll because now() + remaining is a
continuously-drifting value between the device's own remaining-time
revisions, and washers/dryers/dishwashers commonly revise that estimate
by a minute or two mid-cycle anyway -- both are real, small changes that
individually don't matter but each cost a recorder/logbook entry.

Add a per-device Options Flow setting (finish_time_debounce_minutes,
default 3) and a SensorDesc(debounce=True) opt-in. LocalThingsSensor now
caches the last value it actually reported and only adopts a new one once
it differs by at least the configured threshold, a cycle starts (no prior
cache), or a cycle ends (new value is None) -- 0 disables it entirely,
restoring today's behavior.

Also pass config_entry explicitly into DataUpdateCoordinator's
super().__init__() -- self.config_entry previously relied on an
undocumented ContextVar fallback that upstream has flagged as removed in
HA 2026.8, which the new debounce lookup needed to not be built on top of.
2026-07-31 19:25:45 +02:00
Jelle Lauwers fe9a1128ea Round finish_time to the minute to stop recorder spam
_finish_time added datetime.now(timezone.utc) (fresh seconds/
microseconds every call) to the device's remaining-time duration, so the
returned timestamp differed at the sub-minute level on nearly every poll
even when remainingTime itself hadn't changed. The recorder logged a new
history/logbook entry each time, while the UI rounds the display down to
the minute, making repeated polls look like duplicate identical entries.

Round the result down to the minute so the entity only changes state
when the estimate actually shifts.
2026-07-31 19:07:24 +02:00
Christopher 2f231b5c19 Aligned naming for Quick Dry 35 in english 2026-07-31 17:52:55 +02:00
Christopher 873ed562de Add new drying modes to English and Dutch translations 2026-07-31 17:46:26 +02:00
Petr Divis f15ee7e563 Update golden fixtures for the new odor-controller entities
CI caught it: 8 pre-existing AC fixtures also carry genuine SmartCoolClean_/
ProgressSmartClean_ tokens in /mode/vs/0 (lnx_rac_heatpump, ara_ww_tp1_22,
windfree_oscillation, tp1x_rac_01001, fac_bora, fac_bora_2in1,
fac_bora_205_flat, cac), so odor_controller_active/odor_controller_progress
now correctly bind there too -- their golden state_keys lists just hadn't
been regenerated. Verified against the actual downloaded CI job log.
2026-07-31 16:39:08 +02:00
Petr Divis d0c68fb5ac AC: bind SmartCoolClean odor-controller option tokens; add Czech translation
- airconditioner.py: odor_controller_active binary_sensor + odor_controller_progress
  sensor, read from the /mode/vs/0 SmartCoolClean_/ProgressSmartClean_ option tokens
  (SmartThings cloud custom.airConditionerOdorController capability). Read-only --
  no confirmed write command.
- Reporter's TP1X_DA-AC-RAC-01001_0000 dump: fan speed and WindFree preset already
  bind via the existing composite climate entity, no code change needed there.
- en.json/nl.json: new entity names for the two additions.
- cs.json: new, full Czech translation catalog (mirrors en.json topology 1:1).
- New fixture + golden + tests locking in the odor-controller behavior.
2026-07-31 16:02:13 +02:00
Marc Billow e99a8a6b05 Merge pull request #232 from hmmbob/agent/polish-dutch-translations
Polish Dutch translations
2026-07-31 08:03:37 -05:00
Hmmbob 87e0c43b23 Polish Dutch translations 2026-07-31 14:38:31 +02:00
perseus177 9ee0dbb8f9 feat(airconditioner): filter alarm interval on legacy ARTIK051 boards
Adds the Select the official app offers next to the filter reminder --
180/300/500/700 hours -- which this board generation keeps as a FilterAlarmTime_
token in /mode/vs/0's options[] rather than on a /filter/* resource.

Confirmed on hardware: stepping through all four radio positions in the app
moved that one token and nothing else across all 19 resources, so the token
carries the hour count verbatim; and a local write of 500 to a unit sitting on
700 was accepted and kept, surviving a restart. The cloud exposes none of this,
so it is only available locally. Gated with the counter it belongs to, so
boards carrying a real /filter/airdustfilter/vs/0 threshold keep using that one
-- the gate is covered by a test that injects the token into a newer board's
dump, since no non-legacy fixture carries it and the assertion would otherwise
pass for the wrong reason.

Also settles what filter_time measures: it counts UP -- running time
accumulated since the last filter reset, not time remaining -- which an earlier
revision of that comment explicitly left open. Three independent things agree:
the token rising while the unit runs, FilterAlarmTime_ being the threshold it
is measured against, and /alarms/vs/0's filter entry tracking the counter
across two units on one site (a live unsuffixed 'FilterAlarm'/'Created' at
FilterTime_5595 against the 'FilterAlarm_OFF'/'Deleted' placeholder at 1915).
The alarm clearing by itself the moment the counter dropped under the threshold
is the causal half of that, not just correlation.

Resetting the counter is NOT solved and no reset entity is added. The
descriptor records what was tried, and what the failures do and do not prove,
so the next attempt starts from evidence instead of from scratch. Short
version: the reset is a *command* (custom.dustFilter/resetDustFilter), not a
value write, which is why nothing that writes the counter works; I could not
work out how to drive that command locally. /actions/vs/0 is the obvious local
command channel but publishes no schema, and I did not enumerate guessed action
names against a live appliance.

Written with Claude (AI), on the author's own hardware; every result quoted
above is measured on the device rather than inferred.
2026-07-31 12:41:40 +02:00
blka f19abc7936 feat(observe): early-exit grace wait on success fraction
Replace the fixed time.sleep(GRACE_PERIOD_S) in try_enter_observe_mode
with a threading.Condition.wait_for(predicate, timeout=grace_period_s)
that returns as soon as success_fraction of subscribed hrefs have
notified. Production data (fridge TP2X_REF_20K): all 13 hrefs notify
within ~0.34s of subscribe, so every successful first-refresh / observe
retry was waiting ~14.6s of dead time. AC retry cycles showed the same
16s-fetches-that-transition pattern.

No behavior change on the fallback path (fraction not reached → wait
the full ceiling → MODE_POLL, identical to today). GRACE_PERIOD_S and
SUCCESS_FRACTION unchanged. No smartthings_local library change.
2026-07-31 09:06:19 +02:00
Marc Billow a7d57086e9 feat: add Samsung Air Monitor Plus support (#210)
New device family: a standalone, battery-powered air-quality sensor
puck (ASM-KR-TP1-22-* board) with no controllable-appliance state at
all -- no /power/*, only /energy/battery/vs/0. Routes via both /oic/d
('x.com.st.d.airqualitysensor', confirmed against the real dump) and
the 'ASM' modelNum board token as a fallback.

/sensors/vs/0 reuses air_purifier.AIR_QUALITY's existing {type, value}
items-list decode (common.sensor_item_value) for dust/fine_dust/
super_fine_dust/odor/clean_level, sharing those capabilities' catalog
entries, and adds a CO2 reading those families don't report. The
particulate sensors deliberately get no pm10/pm25/pm1 device_class:
the values read as physically consistent (coarser >= finer) but
Samsung's own two-tier dust naming doesn't confirm where this board's
three-tier split actually maps, and mislabeling a read-side unit is a
standing, not one-shot, kind of wrong -- exposed as plain named
sensors instead. Humidity, battery/charging, and the informational air
quality standard are otherwise straightforward field reads.

/dnd/vs/0 (do-not-disturb window) is a flagged educated guess: the
write contract mirrors the read side's own string/time-format shape
(the safest kind of guess) but has no idle-vs-active dump to confirm
it end-to-end, so it's called out as such in code and will need a
reporter to verify on real hardware. /keepnormalstate/vs/0 and
/sensordatasinks/vs/0 are genuinely opaque (single unexplained value,
no supported-values list) and are ignored rather than guessed at.

Locked in with a scrubbed fixture, golden, and capability tests.
2026-07-31 02:40:02 +00:00
Marc Billow 4afd11a357 skill: relax adding-device-support on guessing writes
The old "don't guess" rule banned shipping any write whose contract
wasn't already confirmed end-to-end, even when the dump gave strong
supporting evidence (a supported-values/range field, an idle-vs-active
dump diff, a pattern already confirmed on a sibling board). That's
overly conservative: a CoAP write against an invalid value gets
rejected rather than acted on, so the worst case for a wrong guess is
a no-op, not a damaged appliance -- and this project already ships
flagged guesses and asks reporters to confirm them on real hardware
routinely (issues #196, #181).

Replace it with guidance to make educated guesses and ship them, but
mark them explicitly as unconfirmed (in code comments and in a direct
ask to the reporter) rather than silently presenting a guess as a
confirmed contract. Still forbids inventing a write or entity with no
supporting evidence at all, and calls out that the "worst case is
rejected" safety margin covers invalid values, not wrong-but-valid
units/semantics.
2026-07-31 02:27:08 +00:00
Marc Billow d317a85988 Drop "one commit per issue" from CONTRIBUTING.md/AGENTS.md
That was triage-session guidance, not a general repo policy -- the
files should only codify the attribution rule.
2026-07-31 02:15:47 +00:00
Marc Billow 4d4de23fbb fix(water_purifier): read favorite-temp options from showList (#196)
favorite_hotwater_temperature's options_field read
favorite.supportedList, which is only the four fixed presets. The
SmartThings app also lets the user add one custom value to their own
display list via its "temperatures to display" editor (bounded by
/setting/waterpurifier/vs/0's hotwaterRange), and that value shows up
in favorite.showList but never in supportedList. A unit whose current
default was that custom value rendered as HA's "unknown" state, since
current_option wasn't among the (too-narrow) options list.

showList is a superset of supportedList that always includes whatever
the current default actually is. /setting/waterpurifier/vs/0's own
hot_water_temperature select is untouched -- its write contract for
values off the old preset list still isn't confirmed, so it stays
gated off on boards that don't report supportedHotTemperatures.
2026-07-31 02:14:09 +00:00
Marc Billow e46e935ba0 fix(common): make the vendor kids-lock fallback read-only (#181, #183)
KIDS_LOCK_VS_FALLBACK's write_fn wrote 'Enable', a value no dump in the
fixture corpus (washers, dryers, dishwashers, ovens, ranges, microwaves,
air purifiers, air dressers -- everything that lacks the OCF-standard
/kidslock/0) has ever reported back; every one reports 'Ready' or 'Run'.
It was never a confirmed write contract.

#181's reporter tested directly: writing the *correct* value ('Run')
still returns 4.05, and the SmartThings app itself has no control for
kids lock either -- the resource is genuinely read-only on that
hardware, not just wrong-valued. #183's reporter hit the identical
symptom (toggle does nothing) on a different device family reporting
the same Run/Ready vocabulary.

Convert the entity to a read-only BinarySensorDesc. binary_sensor's
'lock' device class is inverted from the old switch reading (On means
open/unlocked), so value_fn flips accordingly -- callers reading the
flattened 'child_lock' state key need to account for the new polarity.
KIDS_LOCK_GENERIC (the OCF-standard /kidslock/0 boolean) is untouched;
nothing suggests that one is broken.
2026-07-31 02:09:58 +00:00
Marc Billow 5bbc7aeece fix(air_dresser): bind /buzzersound/vs/0 on the TP1_21 board (#208)
DA_DF_TP1_21_COMMON reports a plain {setBuzzerSound, supportedFinishSound}
buzzersound resource -- the same shape laundry.BUZZER_SOUND already
handles for washers/dryers -- but it was unbound because the air_dresser
registry never included that capability. Add it, and lock the board in
with a scrubbed fixture, golden, and capability tests.

The reporter's actual complaint (course cycles showing as raw codes) is
a labelling gap, not a coverage one: this board's course table has no
code->name mapping anywhere in the dump, same as issue #162's board, so
there's nothing to bind here -- it needs a reporter to identify the
codes before they can be named in translations.
2026-07-31 02:03:32 +00:00
Marc Billow e642db462d fix: recognize all-same-hex-digit placeholder serials (#189)
_is_placeholder_serial only caught the ARTIK051_DONGLE_REF family's
'Nothing(SVC)' sentinel. The DA_WM_A51_20_COMMON (ARTIK051) laundry
board family reports a different one instead -- every character the
same repeated hex digit -- which passed through as a real, non-unique
serial. Two different physical units (a washer and a dryer) both
reporting the literal serialNum 'FFFFFFFFFFFFFFF' collided on the
config-entry unique_id, so the second device's config flow aborted as
already configured. Widen the check (both the config_flow.py and
coordinator.py copies) to also catch that sentinel shape.
2026-07-31 01:59:49 +00:00
Marc Billow 822c6814b6 fix(coordinator): don't let subpolls delay HA startup/shutdown (#207)
_run_subpolls is a self-limiting background loop the coordinator already
cancels and recreates every refresh cycle, but scheduling it with
async_create_task ties it into HA's startup/shutdown task tracking
anyway. A subpoll cycle in flight (up to ~27s) then delays both.
async_create_background_task is HA's supported API for exactly this
case -- a task the integration owns and manages the lifecycle of.
2026-07-31 01:58:04 +00:00
Marc Billow 61f13e7484 Add CONTRIBUTING.md and AGENTS.md codifying commit conventions
One commit per issue/logical change, and every commit's author and
committer must be the human accountable for the work -- never a tool,
bot, or AI agent identity, and no AI co-author trailers. AGENTS.md points
any AI coding agent working here back to CONTRIBUTING.md as the
authoritative source for this.
2026-07-31 01:57:11 +00:00
Marc Billow b8d04b4ae3 Merge pull request #216 from firstof9/fix-hood-fan-write-fallback
fix(hood_fan): fall back to settable min/max when supportedFanSpeed is missing
2026-07-30 20:21:02 -05:00
Marc Billow 2d09ef108a Merge pull request #220 from R3inoudR/add-vs9700-routing
Route VS9700 stick vacuum (VSWW) to vacuum_station registry
2026-07-30 20:17:07 -05:00
Marc Billow 8512aef2d2 Merge pull request #223 from mbillow/claude/oic-device-type-mapping-zyiivg
Route device type from /oic/d as the primary detection signal
2026-07-30 20:15:12 -05:00
Marc Billow 3cd22824ca Drop the per-entry provenance comments on _OIC_TYPE_TO_KEY
The "issue #N" / "OCF spec" trailing comments and the header paragraph
explaining them added noise without adding anything the value side of
the table (a real _REGISTRY_BY_KEY key) doesn't already guarantee. Update
the skill's guidance to match.
2026-07-31 01:13:09 +00:00
Marc Billow 9f4c47ea63 Update adding-device-support skill for /oic/d as primary detection
The skill still described oneUiVersion-era two-stage detection and said
"nothing routes on /oic/d yet" -- both stale now that resolve() checks
device_types first. Rewrite the routing section around the new three-stage
order, add a dedicated "Adding an /oic/d device type" section documenting
the right endpoints (/oic/d, /oic/p, /oic/res) and the issue-confirmed vs
OCF-spec provenance convention, and add explicit checklist reminders (in
the routing section and in "Lock it in") to check and fill in
_OIC_TYPE_TO_KEY whenever a dump carries a type.
2026-07-31 01:03:29 +00:00
Marc Billow 0a4b80afc4 Add OCF-spec-confirmed oic.d types: airpurifier, dishwasher, oven
Extend _OIC_TYPE_TO_KEY with three more device categories the OCF Smart
Home Device Specification defines with the same 'oic.d.<category>' shape
as the already-confirmed entries, ahead of seeing them in an actual dump.
Deliberately leave out the rest of a broader compiled oic.d/x.com.st.d
list (lights, switches, sensors, locks, cameras, TVs, generic energy
meters, oic.d.robotcleaner, ...) -- none of those map to a registry this
integration has, and robotcleaner in particular names a different product
than the vacuum_station clean-station registry.
2026-07-31 00:58:26 +00:00
Marc Billow 4f9e630ae9 Route device type from /oic/d as the primary detection signal
/oic/d's `rt` names the device's own OCF device type, which beats
parsing board part numbers whenever a dump populates it. Add
for_device_by_oic_type() and an _OIC_TYPE_TO_KEY table (airconditioner,
dryer, refrigerator, washer, plus SmartThings' x.com.st.d.stickcleaner
and x.com.st.d.steamcloset extensions), and consult it first in
resolve(), ahead of modelNum/description and the resource-signature
fallback.

Thread the master's device_types from read_identity() through
coordinator.py's discovery pass and config_flow's connection probe;
subdevices keep resolving from their own /information/vs/0 (or the
master's registry as a fallback) since they have no /oic/d of their
own read today.
2026-07-31 00:56:17 +00:00
R3inoudR 4c827bf9e4 Update __init__.py
Route VS9700 stick vacuum (VSWW) to vacuum_station registry
2026-07-30 23:14:19 +02:00
firstof9@gmail.com 8e8f08109a fix(hood_fan): fall back to settable min/max when supportedFanSpeed is missing 2026-07-30 11:33:40 -07:00
Marc Billow eb938205ab Merge pull request #215 from mbillow/claude/issue-214-device-registration-d2smcv
fix(subdevices): don't materialize a slot whose only live state is a meter
v0.17.2
2026-07-30 13:25:14 -05:00
Marc Billow 5e86f147d4 fix(subdevices): don't materialize a slot whose only live state is a meter
Issue #214: a single-split ARTIK051_KRAC_18K showed up in HA as two air
conditioners. Its /device/1 answers the same unused-slot shape the Pattern A
reporter's /device/2 does -- every operational rep empty {} -- but also
reports a populated /energy/consumption/vs/1. Running discovery against the
reporter's own quoted subdevice block reproduces their diagnostics exactly
(21 bound entities, the same six hrefs), and of those 21 the only primary
entity with a non-None value is energy_kwh: a lifetime kWh total was the
sole thing passing discover_partitioned's liveness gate and materializing
the phantom.

A single-split AC has one compressor and one energy meter, so a
whole-appliance running total appearing under a second index is the
appliance's own bookkeeping, not evidence that hardware is installed at that
slot. Exclude cumulative meters (HA's total/total_increasing state classes,
plus the energy/water/gas device classes for the descriptors that
deliberately declare no state class) from the gate. The gate never applies
to MAIN, and across the whole fixture corpus every device has at least one
non-meter live primary, so no existing device's entities change -- verified
by the golden for the new fixture being identical to the plain KRAC one.

Also implement async_remove_config_entry_device. A subdevice's HA device
outlives the discovery that created it, so a phantom materialized by an
earlier release stays in the registry with no way to delete it from the UI
-- which is the state the second reporter on that issue is in, with a
refrigerator whose diagnostics now report no subdevices at all. Devices the
entry currently provides still refuse removal. No automatic pruning:
enumeration is one-shot and a real sibling can miss a poll (issue #205 on
the reference hardware), so auto-removal would discard a live subdevice's
name, area and automations on a transient miss.

The new fixture's /device/1 seed is the reporter's verbatim capture; its
master half comes from the corpus's other KRAC unit, with the deviations
spelled out in seeds_note.

Claude-Session: https://claude.ai/code/session_01HzUMLnSWBT64o4BQkVEzXp
2026-07-30 18:23:47 +00:00
Marc Billow 5f5aecbe7d Merge pull request #206 from mbillow/fix/subdevice-flat-href-fallback-205
fix(subdevices): fall back to per-href probing when a prefixed subdevice has no /device/0 Collection
v0.17.1
2026-07-29 22:07:47 -05:00
Marc Billow ca27bf7f6e docs: de-identify reporter usernames repo-wide, add PII rule to skill
Extended the earlier de-identification (jhkwon19) to the other issue #177
reporter (HJcom), who was named in ~20 spots across coordinator.py,
diagnostics.py, subdevices.py, identity.py, capabilities/airconditioner.py,
a fixture's seeds_note, and several test docstrings/function names. Swapped
all of it for "the reporter"/"the Pattern A reporter", matching the
convention already established for the other reporter.

Added a rule to the adding-device-support skill (end of "Lock it in"):
don't put a reporter's name or GitHub username in code comments,
docstrings, seeds_note, or test/function names -- that prose ships and
sits in git history indefinitely, unlike an issue thread or a
release-notes thank-you. Use "the reporter" / "issue #NNN's reporter" /
a pattern label instead.
2026-07-30 03:06:06 +00:00
Marc Billow ce92d71a92 docs: de-identify the reporter's username in code comments
Comments/docstrings across subdevices.py, coordinator.py, the two fac_bora
fixtures, and their tests named the issue #177/#205 reporter directly.
Swapped to "the reporter"/"the Pattern B reporter" throughout -- no
behavior or test-assertion changes, string content only. HJcom (the
Pattern A reporter) is unaffected.
2026-07-30 02:50:49 +00:00
Marc Billow 8600985a36 review: address Opus findings on the subdevice flat-href fallback
- coordinator: _poll_subdevice_flat_hrefs called sess.pace() outside its
  try block and re-read self._session instead of using the caller's
  already-None-checked reference -- a session closed mid-poll (async_close()
  doesn't hold _session_lock) could crash the whole poll cycle instead of
  just dropping that one sibling. Now takes sess from the caller and guards
  pace() the same as get().
- coordinator: skip flat hrefs already covered by the hot/warm sub-poll
  tiers -- those are refreshed every few seconds by _run_subpolls already,
  so re-fetching them again on the once-per-summary-poll flat pass only adds
  GETs, not freshness. Matters because, unlike the Collection path (always
  one GET), a flat subdevice's summary-poll cost scales with its href count.
- subdevices.py module docstring: corrected an overclaim inherited from PR
  #199 that GET /<uuid>/device/0 had been "confirmed live" on jhkwon19's
  unit. Only an individual /information/vs/0 read was ever actually
  confirmed; the Collection endpoint itself has never been observed to
  answer on any known unit, which is exactly what issue #205 exposes.
- SKILL.md: fixed the numofsubdevice cross-check formula to match what
  coordinator.py actually compares (len(materialized) + 1, not
  len(subdevices) + len(subdevices_skipped)).
- Documented, not yet guarded against: a firmware that echoes state back
  under any unrecognized prefix instead of 4.04ing could pass the flat probe
  and the liveness gate, materializing a phantom duplicate of the master.
  Every board seen so far genuinely 4.04s on paths it doesn't own.
- Added test coverage the review flagged as missing: a materialized (not
  just skipped) flat subdevice re-polling end-to-end through to
  canonical_resources, the hot/warm skip itself, zero-master-hrefs and
  two-UUID no-cross-contamination edge cases, and a golden file for the
  #205 fixture (SKILL.md's "fixture + golden + test" discipline).
2026-07-30 02:44:26 +00:00
Marc Billow e78d941af3 fix(subdevices): fall back to per-href probing when a prefixed subdevice has no /device/0 Collection
Issue #205 shows the UUID-prefixed pattern's own reference device
(TP2X_FAC_BORA_21K) doesn't always answer /<uuid>/device/0, contrary to
what the pattern was built against. When that Collection GET comes back
empty, enumerate_subdevices now probes every href the master itself
answered this cycle individually under the UUID prefix, keeping whichever
ones respond. Subdevice gains a flat_hrefs field for this, and the
coordinator re-polls those hrefs individually each cycle instead of
re-fetching a Collection batch that doesn't exist.

Built a fixture from the reporter's real #205 diagnostics dump: the
fallback finds a candidate through the one href already confirmed live
under this UUID (/information/vs/0, from the #177 thread), and
discover_partitioned's liveness gate correctly holds it back since that
href alone binds no entity -- honest current state, not a guessed
resolution.
2026-07-30 02:25:26 +00:00
Marc Billow 7c31bb1682 chore: bump version to 0.17.0 v0.17.0 2026-07-30 01:28:42 +00:00
Marc Billow 2ff39d0576 Merge pull request #204 from mbillow/claude/triage-issues-196-195-lm4wki
fix(water_purifier): route AILITE_DA-REF-WATERPURIFIER boards correctly (#196)
2026-07-29 20:23:21 -05:00
Marc Billow fb2d267632 fix(water_purifier): route AILITE_DA-REF-WATERPURIFIER boards correctly (#196)
The AILITE water-purifier board (RWP70F15ANW) spells its modelNum
'...-REF-WATERPURIFIER-...', so the board-token scan hit the bare 'REF'
token before ever reaching 'WATERPURIFIER' and misrouted the device to
the refrigerator registry, whose resource surface shares almost nothing
with a water purifier -- hence the incomplete-coverage warning. Add a
documented carve-out for this one token co-occurrence and a matching
TestBoardTokenAmbiguity exception.

Also bind the water-purifier hrefs this board additionally exposes:
cup-detection status, the settings/sound/{mode,output,volume} trio (read
live, since this board's own supportedModes vocabulary differs from both
laundry's and air_purifier's hardcoded/live sets), and last-pour
statistics.

Separately, gate hot_water_temperature off when the device doesn't report
a supportedHotTemperatures list (only a hotwaterRange/hotwaterLevel pair
with no confirmed write contract) -- previously an empty options list
plus a live current value rendered as 'unknown' in HA, the second bug
reported in #196. The existing water_purifier_coffee fixture (#107) turns
out to hit the same shape, so its golden drops the entity too.

Issue #195 (TP1X_REF_21K) needed no change: its diagnostics show zero
unbound hrefs and the model already routes to the refrigerator registry,
matching the maintainer's own comment on the issue.
2026-07-29 23:23:56 +00:00
Marc Billow 1053ae421b Merge pull request #202 from firstof9/fix/fan-zerodivision-issue-201
fix(fan): fall back to settableMin/MaxFanSpeed when supportedFanSpeed is omitted (#201)
2026-07-29 18:06:41 -05:00
firstof9@gmail.com 7aa14d161c style(fan): extract _MAX_FAN_SPEED_FIELD constant 2026-07-29 16:06:12 -07:00
Marc Billow c7d265074f Merge pull request #203 from mbillow/claude/rename-unit-subdevice-4wbe6s
rename: unit/sub-unit -> subdevice, matching OCF terminology
2026-07-29 17:59:50 -05:00
Marc Billow 9e85395b44 rename: unit/sub-unit -> subdevice, matching OCF terminology
"Unit"/"sub-unit" from #199's multi-indoor-device support wasn't OCF
idiomatic -- OCF calls each component of a composite device a
"subdevice" (see subdeviceIdList), so rename SubUnit -> Subdevice
throughout: the registry module, coordinator state, BoundEntity's
subdevice field, diagnostics keys (subdevices/subdevices_skipped/
subdevice_probes), entity unique_id prefixes (unit1_/sub_<uuid>_ ->
subdevice1_/subdevice_<uuid>_), device-name fallback labels, golden
fixtures, tests, README, and the adding-device-support skill.

Breaking change to entity unique_ids and diagnostics keys, acceptable
since this hasn't been released yet.
2026-07-29 22:57:11 +00:00