Commit Graph
131 Commits
Author SHA1 Message Date
Marc Billow febc8fa4a0 fix: keep energy sensors on empty-stub reps; gate power on field presence
Review follow-up. An explicit exists_fn bypasses entity._is_included's
empty-{} stub carve-out, so the sentinel-aware energy meter would drop
power_watts/energy_kwh when /device/0 returns a not-yet-fetched stub for
/energy/consumption/vs/0. Restore the carve-out (`not rep or ...`), and
hide power_watts when instantaneousPower is absent from a populated rep
(not just when it's the -500 sentinel) so a partial rep can't spawn a
phantom power sensor.
2026-07-21 01:39:49 +00:00
Marc Billow 9264b912f9 feat: full dryer support + consistent laundry capability surface
First full dryer dump (issue #14, DV90BB5245AES1) surfaced 5 unbound
hrefs and an "incomplete capability coverage" repair. Handle them and,
while here, make the washer/dryer/dishwasher families consistent instead
of each carrying a bespoke variant of the same controls.

Dryer coverage:
- /power/0, /kidslock/0, /remotectrl/0: bind via the OCF-native + vendor
  fallback pairs (prefer the standard OCF resource, fall back to -vs).
- /buzzersound/vs/0: new Buzzer sound select.
- /course/vs/0: cycle select shared with washer/dishwasher; ignore the
  /st/dryercourse/vs/0 re-encoding (mirror of /st/washercourse/vs/0).

Consistency / de-duplication:
- Move generic OCF controls (power/kids-lock/remote-control fallback
  pairs, energy meter) into common.py; every registry uses them.
- Move shared laundry controls (buzzer, job-beginning-status, and the
  /course/vs/0 cycle-select machinery) into laundry.py; washer and
  dishwasher stop hand-rolling their own copies.
- Energy meter is now sentinel-aware everywhere: the dead '-500'
  instantaneousPower reading no longer shows a misleading 0 W (fixes it
  on dryers and dishwashers, matching the earlier washer fix).
- Job-beginning-status reads x.com.samsung.da.currentStatus, the field
  every dump actually carries; the dryer sensor was previously blank.

Adds a scrubbed dryer fixture, golden, and capability tests, plus an
.claude/skills/adding-device-support skill capturing the dump-reading,
OCF-vs-vendor, entity-taxonomy, and coverage workflow. Bumps to 0.6.0.
2026-07-21 01:15:12 +00:00
Marc Billow 84788174f8 Merge pull request #15 from mbillow/claude/device-port-detection-vp0q8d
feat: auto-detect DTLS port across the full 49152-49160 range
v0.5.0
2026-07-20 16:55:31 -05:00
Marc Billow 00c3ebaf75 feat: auto-detect DTLS port across the full 49152-49160 range
The config flow only probed 49154/49155, so appliances whose local
CoAP/DTLS API binds elsewhere in the ephemeral range (e.g. a dishwasher
answering on 49153) could never be added.

Add a fast UDP liveness sweep across 49152-49160 that uses the ICMP
port-unreachable / ECONNREFUSED asymmetry to find the live port(s)
before attempting the expensive DTLS handshake. Closed ports fall out
immediately; a live-but-silent port is kept as a candidate. The real
handshake then runs only against discovered ports, preferring the
historically known 49154/49155 when several look live.

Also drop the probe's /device/0 GET deadline from a bare 15s literal to
a named PROBE_GET_TIMEOUT_S constant (10s) — the slowest observed full
dump is ~8s, and 10s matches the per-resource read timeout used
elsewhere.

Bumps version to 0.5.0.
2026-07-20 21:52:35 +00:00
Marc Billow 4cc073c88a Merge pull request #12 from mbillow/claude/readme-cleanup-b3ib70
docs: simplify and update README
v0.4.1
2026-07-18 23:34:43 -05:00
Marc Billow 91013ece60 fix: translate machine_state sensor values; bump to 0.4.1
machine_state was rendering the raw lowercase OCF values (idle/active/
pause) untranslated in the UI. Add device_class=enum, options, and a
translation_key matching the existing pattern used by ice_making_status
and connection_mode, in both the shared operational.py capability and
oven.py's separate machine_state sensor.

Bump patch version for this fix plus the ice_type 'off' translation and
iot_class correction earlier on this branch.
2026-07-19 04:33:23 +00:00
Marc Billow 795763f3ca fix: add missing ice_type 'off' state translation
The refrigerator fixture's x.com.samsung.da.iceType.supported list
includes "Off" alongside the whiskey_iceball_* values, but strings.json
only translated the whiskey_iceball options, leaving "Off" to render
untranslated in the ice-type select.
2026-07-19 04:18:59 +00:00
Marc Billow a7ed230569 fix: correct iot_class to local_push
Coordinator prefers CoAP OBSERVE push notifications (observe.py) over
polling when the device supports it, falling back to polling only when
observe mode isn't available or drops.
2026-07-19 04:11:01 +00:00
Marc Billow 9fee5b5ec0 docs: simplify smartthings-local intro blurb 2026-07-19 04:07:52 +00:00
Marc Billow f24d6a0ae6 docs: simplify and update README
Add missing washer support to the appliance table, fix stale test-count
and repo-layout claims, trim protocol-internals jargon that belongs to
the smartthings-local library rather than this integration, and point
"Adding a new appliance type" at HA's own diagnostics download instead
of a gitignored local script.
2026-07-19 04:05:37 +00:00
Marc Billow 29b145ccdd Merge pull request #11 from mbillow/claude/issue-9-investigation-mdnwcd
chore: bump manifest version to 0.4.0
v0.4.0
2026-07-18 13:27:57 -05:00
Marc Billow 5fbf58af30 chore: bump manifest version to 0.4.0 2026-07-18 18:26:58 +00:00
Marc Billow 9ae59889d4 Merge pull request #10 from mbillow/claude/issue-9-investigation-mdnwcd
fix(washer): stale idle progress%, add detergent/softener dosing entities (#9)
2026-07-18 13:25:33 -05:00
Marc Billow 35f76ce2df feat(washer): add label translations for detergent/softener dosing selects
Assumes the LevelCtrl code scheme is None/Low/Medium/High (00-03) on both
dispensers -- code 00 has no on-screen equivalent in the app's 3-choice
Faible/Moyen/Élevé picker, assumed to be what "Activation" off collapses
to -- and Level2Ctrl is Soft/Medium/Hard for detergent water hardness,
1x/2x/3x for softener concentration. detergent_quantity and
softener_quantity share one translation_key (same vocabulary), same
pattern as fridge.py's shared 'brightness_level' key.

Not cross-device verified: only one dump + screenshot set (issue #9) to go
on, and the softener concentration reading doesn't cleanly match its
screenshot (assumed to be a setting changed between dump and screenshots,
not a different code scheme -- see the comment in washer.py).
2026-07-18 18:24:10 +00:00
Marc Billow 2a6bbc9dfb fix(washer): stale idle progress%, add detergent/softener dosing entities (#9)
progress_percentage lacked the active-state gate already applied to
progress/cycle_active/finish_time, so it kept showing a stale device value
(e.g. 1%) while idle -- now zeroed the same way. Shared by dryer/dishwasher/
oven via operational.py's OPERATIONAL_STATE.

Also exposes detergent/softener auto-dispense quantity, water hardness/
concentration, and low-reservoir alarms from /course/vs/0's options array,
using the same decode/RMW helpers already used for course selection and
drum-clean tracking. Gated by exists_fn so washer models without these
fields (e.g. the existing test fixture) are unaffected.

Water consumption is unaffected -- common.WATER_METER is already wired
into the washer registry; this reporter's device just doesn't expose
/water/consumption/vs/0.
2026-07-18 18:15:41 +00:00
Marc Billow aee19fd953 Merge branch 'fix-washer-fridge-oven-issues'
* fix-washer-fridge-oven-issues:
  chore: bump manifest version to 0.3.0
  fix(washer,fridge,oven): correct energy/temperature-unit bugs from issues #6/#7
v0.3.0
2026-07-15 16:02:05 -05:00
Marc Billow 37bd063c0a chore: bump manifest version to 0.3.0 2026-07-15 15:59:24 -05:00
Marc Billow e875266405 fix(washer,fridge,oven): correct energy/temperature-unit bugs from issues #6/#7
Washer (#6): instantaneousPower is a dead sentinel ('-500') on every
TP1-class washer dump collected so far, and cumulativePower is absent
outright on at least one model. WASHER_ENERGY_METER now hides both
sensors instead of showing a misleading "0 W"/perpetual "unavailable".

Fridge (#7): temperature sensors/setpoints hardcoded '°F', ignoring the
unit each device actually reports per-reading -- fixed via a new
unit_fn hook read live from the resource. Also corrects
DEFROST_BLOCK_STATUS's polarity (DEFROST_BLOCK_ON means actively
defrosting, not "blocked", confirmed against live dumps) and adds
REFRIGERATION_FALLBACK for /refrigeration/0, closing the last unbound
href surfaced by issue #7's diagnostic dump.

Oven: applies the same live-unit-reading fix defensively to
OVEN_SETPOINT, which shares the same aggregate resource shape.
2026-07-15 11:41:21 -05:00
Marc Billow 9b48ca0e82 Merge pull request #5 from mbillow/manifest-version-bump
chore: bump manifest version to 0.2.1
v0.2.1
2026-07-11 20:27:19 -05:00
Marc Billow e66f4fd2ff chore: bump manifest version to 0.2.1
Was left at 0.1.0 across the last two releases. Also de-hardcode the
diagnostics test's expected version so this doesn't happen again --
it now reads manifest.json directly instead of a copy-pasted literal.
2026-07-11 20:26:52 -05:00
Marc Billow bf3b3a8bbd Merge pull request #4 from mbillow/washer-drum-clean
feat(washer): expose Drum Clean+ maintenance status
2026-07-11 20:20:12 -05:00
Marc Billow dd541b19a1 feat(washer): expose Drum Clean+ maintenance status
Adds cycles-until-due and last-cleaned sensors decoded from the same
options[] array the course selector already reads (DrumCleanProposal_N -
WashingTimes_N for cycles remaining, DrumCleanLog_<iso> for last-cleaned),
verified byte-for-byte against a live app screenshot ("Potreba cistenia po
37 cykloch" / "Naposledy cistene pred 10 dnami").
2026-07-11 20:18:28 -05:00
Marc Billow cf22f3b3fe Merge pull request #3 from mbillow/worktree-washer-device-type
feat(washer): add washer device type support
v0.2.0
2026-07-11 20:03:55 -05:00
Marc Billow 211cf74296 feat(washer,dishwasher): live per-device cycle selector, localized names
Course selection is now a writable select entity sourced from each
device's own x.com.samsung.da.editCourseList (via a new options-as-callable
form on SelectDesc, reading the coordinator's full resource snapshot
instead of just the entity's own href), rather than a hardcoded course
table baked into Python. A MostUsed_ field on the same resource was
considered as a fallback but rejected after byte-level analysis showed it
doesn't reliably encode a course list. When a device never populates
editCourseList, the selector isn't created at all (exists_fn now takes
(rep, resources) to check a sibling href, matching the existing
match_fn(rep, resources) pattern).

Display names moved out of Python into strings.json/translations under
entity.select.{washer,dishwasher}_cycle.state.*, matching this
integration's existing translation_key convention (fridge.py's ice_type,
flex_zone_mode, etc.) instead of hardcoding English names in code.
2026-07-11 20:00:12 -05:00
Marc Billow 29b9e36699 fix(washer): ignore remaining unbound hrefs (cycleinterface, drlc, operational/state OCF-native)
discover() flagged /cycleinterface/vs/0, /drlc/0, and /operational/state/0
as unbound on real washer dumps, which would surface a spurious device
coverage gap for washer owners. All three are noise: cycleinterface is
empty on every dump seen, drlc/0 is an OCF-native duplicate of the
already-ignored /drlc/vs/0, and operational/state/0 is a read-only
OCF-native duplicate of /operational/state/vs/0 (already modeled by the
richer, write-capable operational.OPERATIONAL_STATE).
2026-07-11 13:30:00 -05:00
Marc Billow f4eca9a834 test(washer): add washer fixture and golden-regression coverage 2026-07-11 13:01:01 -05:00
Marc Billow 22c5295d18 feat(washer): wire model-number fallback into discovery and config flow 2026-07-11 12:53:57 -05:00
Marc Billow 2852a21c4a feat(washer): add model-number-based device-type detection fallback 2026-07-11 12:49:08 -05:00
Marc Billow 779c2f9881 feat(washer): add washer device registry 2026-07-11 12:45:05 -05:00
Marc Billow 1e70380ae3 chore(washer): declare washer's redundant/empty hrefs as ignored 2026-07-11 12:42:23 -05:00
Marc Billow 219cb70b77 fix(operational): support delayEndTime as an alternate delay-start field 2026-07-11 12:38:17 -05:00
Marc Billow 1b5e39ba46 feat(washer): prefer OCF-native power/kidslock/remotectrl hrefs, fall back to -vs 2026-07-11 12:35:47 -05:00
Marc Billow 2868938d37 feat(washer): add washer-specific capabilities 2026-07-11 12:31:20 -05:00
Marc Billow 82f1588da4 ci: drop the HACS brands ignore now that real brand images ship
The ignore existed only until custom_components/localthings/brand/ had
real assets; that landed in c3fb4f5.
v0.1.0
2026-07-10 20:34:36 -05:00
Marc Billow 9804903fbd fix(select): stop lowercasing option display values that aren't translated
_normalize() lowercased every select's options/state for display, but
that's only needed for entities with a translation_key (whose
strings.json lookup requires lowercase keys, per hassfest). Untranslated
selects (Cycle, Smart Dry, Sound mode, LED brightness) had no lookup to
protect and were just getting mangled -- "AI Wash" became "ai wash",
"ExtraHigh" became "extrahigh", etc.

Replace with _display(), which only lowercases for translation_key
entities and otherwise passes the device's own casing through, with two
cosmetic fixups: a fully lowercase wire value (e.g. "voice") is
title-cased, and a PascalCase value (e.g. "ExtraHigh") gets a space at
the case boundary. Already human-friendly values pass through
untouched. Avoids hand-authoring strings.json translations for
open-ended, per-model option lists (e.g. dishwasher cycle names) that
would silently regress on any value we didn't enumerate.
2026-07-10 20:34:32 -05:00
Marc Billow 87df3a84df fix(dishwasher): model delay start as a duration, not a wall-clock time
x.com.samsung.da.delayStartTime is HH:MM:SS until the cycle starts
(e.g. "01:00" means 1 hour from pressing start), not a time of day. It
was wired up as a time entity with a suspicious hour % 24 wrap -- a
sign it was never really a clock time. Replace with a number entity
(0-24h, 1h steps) that reads/writes the field as elapsed hours.
2026-07-10 20:34:25 -05:00
Marc Billow c3fb4f5f44 feat: add Samsung brand images for the new brands-proxy API
Home Assistant 2026.3+ serves brand icons/logos for custom integrations
from a local brand/ directory before falling back to the CDN (see
developers.home-assistant.io/blog/2026/02/24/brands-proxy-api). Copied
Samsung's existing icon/logo assets from home-assistant/brands'
core_brands/samsung so the integration shows proper branding instead of
a placeholder. No manifest.json change needed -- has_branding is
auto-detected from the directory's presence.
2026-07-10 19:57:29 -05:00
Marc Billow 72c5de3e8d docs: rename display name to LocalThings
"Local Things" read as two separate words; the integration is one name,
LocalThings (as in local SmartThings control).
2026-07-10 19:50:47 -05:00
Marc Billow d6fef45029 feat(sensor): add disabled-by-default connection-mode diagnostic sensor
Exposes whether a device is currently in observe (push) or poll mode,
for troubleshooting the observe-mode feature without digging through
logs. Disabled by default since it's not everyday-use information.
2026-07-10 19:50:43 -05:00
Marc Billow 569029c116 refactor(observe): stop tearing down live sessions on data-drift or poll hiccups
Replace the breadth/silence downgrade heuristic with a simpler rule: a
still-live OBSERVE session is never torn down on a sweep/cache mismatch
(the 30s sweep already corrects the cache regardless of mode) -- only a
proven reconnect invalidates subscriptions. A mismatch instead triggers
extra hot/warm subpolls this cycle as a bounded fallback.

Also stop treating a summary-poll block-level ACK timeout as session
death: distinguish TimeoutError (transfer was progressing, session
likely alive) from ConnectionError (session actually closed), backed by
a consecutive-timeout counter so a genuinely dead channel still
recovers. This was causing a flaky/slow device (e.g. a dishwasher) to
flap observe<->poll every ~45s on nothing but a slow blockwise GET.

Also remove the dead is_active/active_when scaffolding (never wired
into the coordinator) and add DEBUG logging for each OBSERVE notify
received, including its href.
2026-07-10 19:50:40 -05:00
Marc Billow 7024b170fe fix(observe): re-sync to poll mode on reconnect, harden notified-set race
A poll-failure-triggered reconnect while in observe mode left OBSERVE
silently dead: the new session had zero subscriptions, mode stayed
'observe' so the hot/warm sub-poll fallback stayed disabled, and the
ObserveRefreshTask kept retrying against the closed session instead of
the live one. Downgrading to poll mode on reconnect hands recovery to
the existing poll-mode retry path, which re-subscribes on the new
session.

Also snapshot ObserveManager._notified before intersecting it against
the subscribed set, closing a rare set-mutated-during-iteration race
between the DTLS reader thread (on_notification) and the executor
thread computing the grace-period success fraction.
2026-07-09 20:36:56 -05:00
Marc Billow 8042fe715b feat(observe): surface observe-mode state in diagnostics 2026-07-09 20:05:58 -05:00
Marc Billow ff9a20eff9 feat(observe): wire StateCache and ObserveManager into the coordinator 2026-07-09 19:42:23 -05:00
Marc Billow ce3247bf55 feat(observe): manage ObserveRefreshTask lifecycle 2026-07-09 18:50:39 -05:00
Marc Billow e6eada2367 feat(observe): detect missed notifications via sweep comparison 2026-07-09 18:43:20 -05:00
Marc Billow eecbc19dde fix(observe): clear stale notifications before re-attempting observe mode 2026-07-09 18:36:36 -05:00
Marc Billow 97260441f1 feat(observe): add subscribe-and-wait mode selection 2026-07-09 18:33:03 -05:00
Marc Billow eba73118e9 feat(observe): add ObserveManager with write-settle guard 2026-07-09 18:28:46 -05:00
Marc Billow 3ab0d5553e refactor(fridge): use HA's own translation system for enum entities
Replace the ad-hoc option_names dicts (hardcoded English, manual
reverse-mapping) with the idiomatic HA approach: normalize raw Samsung
enum values to lowercase at the select/sensor boundary and let
strings.json/translations/en.json supply the display text, keyed by
the normalized value. select.py maps the chosen (lowercased) option
back to the device's original casing before writing. The ice-making
status sensor now also declares device_class=enum with an options
list, as HA expects for enum sensor state translations.

Also drop the hardcoded _BZONE_MODES list in favor of discovering
beverage zone modes from the device's own roomSupportedModes field,
matching how flex_zone_mode and ice_type already work.
2026-07-09 16:22:52 -05:00
Marc Billow b386063e79 fix(fridge): translate enum select/sensor values in Python, not strings.json
hassfest rejects translation keys containing uppercase letters, but the
raw Samsung enum values for flex zone, beverage zone, ice type, and ice
making status are uppercase (e.g. CV_TTYPE_RF9000A_FREEZE). Follow the
existing dryer-course pattern instead: map raw device values to
human-readable names in Python via a new SelectDesc.option_names field
(mirrored read/write in select.py) and a value_fn for the sensor, and
drop the now-unused translation_key entries from strings.json/en.json.
2026-07-09 14:12:58 -05:00