Commit Graph
122 Commits
Author SHA1 Message Date
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
Marc Billow 5b8a079fa6 chore: prepare repo for HACS listing
- Add hacs.json and MIT LICENSE required for HACS/default-store inclusion
- Fill required manifest.json keys (documentation, issue_tracker,
  codeowners) and reorder per hassfest's key-ordering rule
- Add validate.yml workflow running hassfest and HACS validation
- Update README install note now that hacs.json is checked in
2026-07-09 14:02:46 -05:00
Marc Billow 5d1a32aa9a Map remaining coverage-gap hrefs, fix discover() false-gap bug, improve device naming
Maps the fridge/dishwasher hrefs flagged by last session's coverage-gap
Repairs issue: defrost delay (switch) + block status (diagnostic binary
sensor), master ice-maker enable, self-check trigger/status, dishwasher
diagnosis trigger/status, and last-operation-source. Adds fallback
capabilities for /doors, /temperatures, and /icemaker/status that only
bind when the richer per-instance hrefs they duplicate are absent, so
simpler devices without those hrefs still get the data. Everything else
in the original gap list moves to capabilities.ignored (Bixby audio
feedback, inert DR/energy-planner resources, redundant metadata).

Fixes discover() reporting a known href as an unregistered gap whenever
its match_fn declined to bind for that device (e.g. a filter capability
on hardware without that filter) — it's now only a gap when no capability
is registered for the href at all.

Device naming now uses x.com.samsung.da.modelNum (not the OCF /oic/p,d
metadata, which produced inconsistent names like "[dishwasher] Samsung")
to build "Samsung <Type> (<model>)".
2026-07-08 15:24:34 -05:00
Marc Billow d2699f0f5b Add diagnostics platform, config-flow gap warning, issue template
Completes the device-capability-diagnostics spec:

- diagnostics.py: the standard HA diagnostics hook, returning device type,
  one_ui_version, unbound hrefs, and the redacted raw resource tree, plus
  integration and smartthings-local version numbers. This is what the
  Repairs issue (added in the previous commit) points users at, and what
  they attach to a device-support issue.
- config_flow.py: _probe_and_validate now also reports oneUiVersion and
  whether the device type is recognized. Recognized types are unaffected;
  an unrecognized type shows a new confirm_unknown_type step explaining
  that only common capabilities will be available before creating the
  entry, so expectations are set at setup time rather than only after the
  fact via Repairs.
- .github/ISSUE_TEMPLATE/device-support.yml: structured template for
  filing a capability gap, linked from both the Repairs issue and the
  config-flow confirmation step.
- README: short section pointing at this whole mechanism.
2026-07-08 00:10:30 -05:00
Marc Billow ca754cbb01 Re-add redacted device dump fixtures
Companion to the git-filter-repo pass that just stripped every prior
version of these two files (which carried a real Samsung account email,
Bixby access token/device-ID hash, and real WiFi/BLE MAC addresses) out of
history entirely. This commit reintroduces only the already-redacted
content, so the real values are no longer reachable from any commit.
2026-07-07 23:52:38 -05:00
Marc Billow 35b47d6b5a Add coverage-gap detection: ignored-hrefs registry, Repairs issue, redaction
Implements the first half of the device-capability-diagnostics spec:

- registry/capabilities/ignored.py: known-noise hrefs (Bixby/voice
  provisioning, WiFi/BLE info, OTA/region housekeeping, and a couple of
  redundant hrefs) declared as no-entity Capability objects, so
  discover()'s existing unknown-resource reporting treats them as covered
  instead of flagging every device as having gaps. Folded into each
  by_type registry and into the global fallback CAPABILITIES set.
- discovery.py: log() callback now passes the raw href instead of a
  formatted message, so callers can collect a clean unbound-hrefs list.
- coordinator.py: wires that callback into self._unbound_hrefs, tracks
  device_type_name/one_ui_version, and raises (or clears) a Repairs issue
  when a device's type is unrecognized or it has genuinely unmodeled
  hrefs left over.
- registry/redact.py: recursive, substring-keyed redaction for anything
  that looks like account/identity data, ahead of the diagnostics.py
  platform that will consume it.

Verified against the real dishwasher/refrigerator fixtures: known-noise
hrefs no longer show up as gaps, while genuine gaps (e.g. /bespoke/vs/0
on the fridge) still do.
2026-07-07 23:49:58 -05:00
Marc Billow b67e0e5cf5 Rename ocf/ to registry/, it's a capability registry now
The DTLS/CoAP transport code that made "ocf" an accurate name moved out to
the smartthings-local package. What's left here (capability.py, entities.py,
discovery.py, adapter.py, identity.py, capabilities/, by_type/, plus the
/device/0 batch parser) is entirely the device capability registry, so name
the package for what it does.

Flattened the redundant ocf/registry/ nesting into a single top-level
registry/ package and updated every import across the platform modules and
test suite accordingly. Verified: full test suite (80/80) passes, and the
Docker dev container reconnects to both live appliances and rediscovers
their entities cleanly after the rename.
2026-07-07 22:54:57 -05:00
Marc Billow 3502a385cc Drop stale .gitignore entries from the MQTT-bridge era
Companion to the previous commit: the ocf_root_ca.pem exception and the
bridge.log/course_mapper.log ignores referenced files that no longer exist.
2026-07-07 22:45:14 -05:00
Marc Billow ab097ffae2 Remove dead MQTT-bridge-era files and stale .gitignore entries
requirements.txt (cbor2/paho-mqtt/pyOpenSSL for the old standalone bridge),
.env.example (APPLIANCE_*/MQTT_*/deploy.sh env vars), and deploy.sh (ssh+tar
deploy for the old bridge container) all referenced main.py and
samsung_appliance/, which no longer exist. Nothing in the current HA
integration, its tests, or the README depends on them; config now happens
entirely through the HA config flow, and manifest.json/requirements-dev.txt
own the Python dependencies.

Also dropped the .gitignore exception for ocf_root_ca.pem (that vendored
file was deleted along with the rest of the vendored transport) and the
now-inapplicable "Bridge runtime" log ignores.
2026-07-07 22:45:03 -05:00
Marc Billow 4c1497f5ba Bake smartthings-local into the dev Docker image
The dev container was relying on HA's runtime pip-install of manifest.json
requirements, which only fires when the integration is set up and needs
outbound network access at that exact moment; a container that had been
running since before the smartthings-local migration kept the old code
loaded in memory and never went through that install path, so restarting
it failed once it picked up the new manifest.json.

Repurpose the stale MQTT-bridge-era Dockerfile (its own code was already
deleted from this repo) to build on the official HA image with
smartthings-local pre-installed, and point docker-compose.yml at it via
`build: .`. Verified end-to-end: rebuilt the image, recreated the
container, and confirmed both live appliances (fridge, dishwasher)
reconnect and discover entities with no runtime install needed.
2026-07-07 22:38:38 -05:00