Commit Graph
32 Commits
Author SHA1 Message Date
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 38263eaa72 Merge remote-tracking branch 'origin/main' into claude/device-support-issue-88-dehumidifier
# Conflicts:
#	custom_components/localthings/registry/by_type/__init__.py
#	tests/test_by_type.py
#	tests/test_golden_regression.py
2026-07-27 03:58:53 +00:00
Marc Billow ff6e1ecff4 Rename gas cooktop registry's display name to avoid induction_cooktop confusion
Renames DeviceRegistry.name from 'cooktop' to 'gas_cooktop' for the
NA9300K-class gas-cooktop registry (PR #23), so diagnostics/device-info
labels no longer collide with the unrelated induction_cooktop family
(issue #86) -- two different OCF surfaces that happen to share the
English word "cooktop".

Safe rename: _REGISTRY_BY_KEY's 'cooktop' lookup key is unchanged, so
all three existing detection paths (oneUiVersion "Cooktop" exact
match, the legacy ARTIK051 modelNum rule, and the resource-signature
fallback) keep routing real devices exactly as before. Entity
unique_ids are built from device serial + entity key, not registry
name, so existing entities are unaffected. Only the DeviceInfo.name
and diagnostics device_type strings change, both cosmetic.
2026-07-27 03:56:32 +00:00
Marc Billow d53459d047 Add device support for Samsung water purifiers (issue #90)
The TP2X_WATERPURIFIER_20K water purifier reports no oneUiVersion and
its modelNum/description don't match any consumer-prefix or existing
board-family token, so it fell into the unknown-device-type fallback
with only common capabilities. Add a new water_purifier registry,
routed via a 'WATERPURIFIER' modelNum/description fallback rule.

Models dispense settings (type/temperature/capacity/pouring status),
sterilize and filter status, favorite-capacity presets, and the three
water/buzzer locks. Per the adding-device-support skill's "never
hard-code the one dump's values" rule: dispense-capacity bounds and
step come live from the device's own desiredCapacityRange/
capacityResolution fields (range_field/step_fn), not a hardcoded
constant, and the hot-water-temperature control is a select over the
live supportedHotTemperatures list rather than a number with invented
bounds, since only a few discrete temperatures are selectable.

/mode/vs/0 and /automation/waterpurifier/vs/0 are left unmodeled: the
former carries an opaque wizard-workflow token with no coherent
current-value contract, the latter is a static support-flags blob with
no live setting to expose.

Confirmed against the issue #90 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
2026-07-27 01:40:05 +00:00
Marc Billow 89429039fb Add device support for Samsung dehumidifiers (issue #88)
The AY18CG7500GED dehumidifier (modelNum TP1X_DA_AC_DHM_01001_0000)
shares the DA_AC_ board family with the room-AC models but carries the
'_DHM_' token instead of '_RAC_'/'_PRAC_'/'_WAC_', so it fell into the
unknown-device-type fallback. Add a new dehumidifier registry, routed
via a '_DHM_' modelNum fallback rule, distinct from airconditioner
since target humidity (not temperature) is the primary control and
there's no climate composite.

Reuses airconditioner.py's AUTO_CLEAN/AIR_FILTER/MUTE_ONCE capabilities
directly (identical resource shapes on the shared board family). Adds
a new humidity sensor + target-humidity number pair and an
operating-mode select. Per the adding-device-support skill's
"never hard-code the one dump's values" rule, the target-humidity
number has no hardcoded min/max (falls back to HA's own 0-100 default
for a percentage field) and reads its step live from the device's own
`increment` field rather than a spec-sheet-derived constant. The
operating-mode select's options come live from supportedModes.

/mode/convenient/vs/0 is left unmodeled: only supportedModes is present
on this dump, with no live current-value field to confirm a read/write
contract.

Confirmed against the issue #88 diagnostics dump with zero unbound
hrefs. Updates the README's supported-appliance-types table for the
new device type.
2026-07-27 01:29:25 +00:00
Marc Billow 35e2c79b14 brand: use updated localthing logo 2026-07-24 20:23:48 -05:00
Marc Billow 6281b40549 refactor(i18n): make the shipped catalog the single source of truth
PR #68 restated its own translation data in Python: a 60-line
TRANSLATED_SELECT_STATES table of frozensets duplicating every
entity.select.*.state key, a second _TRANSLATED_COURSE_TABLES table
naming which course tables have translations, and a strings.json that
was a 835-line byte-for-byte copy of translations/en.json save 43
[%key:...%] references. Each needed hand-syncing, and one was already
drifting.

Home Assistant loads exactly one file per language for a custom
integration -- translations/<lang>.json. It never reads strings.json and
never resolves [%key:...%]; both belong to Core's build tooling, which
custom integrations don't run through (hassfest skips a missing
strings.json and validates translations/en.json instead). So en.json is
the source, and the new catalog.py reads the keys and states back out of
it for the two decisions Python genuinely has to make:

  - select._display() normalizes a raw Samsung option to a lowercase
    state key only when the catalog knows it, else leaves the vendor's
    casing alone. Derived sets are identical to the removed literals.
  - laundry.cycle_select() keys off a device-reported course table only
    when that table has an entry, else falls back to the name-only
    'cycle' key. Translating Table_00 is now a translations-only change.

Also fixes six names that had already drifted between the Python
descriptors and the catalog, restoring HA's sentence case for two
generic ones (Auto release dry, Bubble soak) and taking the catalog's
wording for the rest, and adds a test so the vestigial descriptor names
can't silently disagree with the UI again.

Claude-Session: https://claude.ai/code/session_01GiibJZZLWVvyxq7mc7EDNp
2026-07-24 19:32:09 +00:00
Marc Billow b121d24966 Add Samsung air purifier support (ARTIK051_TVTL-class, issue #56)
Adds a by_type registry for the AX60R5080WD/SE air purifier family, verified
against two independent diagnostics dumps (issue #56 and its comment). Binds
power, alarms, energy, diagnosis (reusing dishwasher.DIAGNOSIS), the dust/
fine-dust/super-fine-dust/odor/clean-level sensors off /sensors/vs/0, filter
progress, a device-active diagnostic, and a display-light switch parsed out
of /mode/vs/0's packed options list.

Fan speed/direction (/airflow/0, /airflow/vs/0) and two other /mode/vs/0
tokens (Comode_*, Blooming_*) are exposed as read-only diagnostics rather
than full controls -- neither dump has a supported-values list to confirm
their write contracts, so they're left for a follow-up once that's
clarified in the issue thread.

Hoists range_hood's items[]-sensor-value helper into common.py
(sensor_item_value) since air_purifier now reads the same /sensors/vs/0
shape.
2026-07-23 22:12:15 +00:00
Marc Billow 13f1214790 Merge branch 'main' into contrib/cooktop-range-hood 2026-07-22 22:39:19 -05:00
themaanda e7daa21931 Merge remote-tracking branch 'upstream/main' into contrib/cooktop-range-hood
# Conflicts:
#	README.md
#	custom_components/localthings/registry/by_type/__init__.py
#	tests/test_by_type.py
#	tests/test_golden_regression.py
2026-07-22 21:53:45 -05:00
Marc Billow f783aa72b9 Add range/cooktop-oven combo to supported appliance types 2026-07-23 02:47:12 +00:00
Marc Billow c0298d57d9 fix: normalize washer dosing-select codes; stop blocking the loop in diagnostics (#9)
Two fixes for issue #9 (WW90T634DHE washer):

- washer dosing selects: the four detergent/softener dosing selects read their
  current value from `<Prefix>LevelCtrl_<code>` (un-padded, e.g. "3") but their
  options from `Supported<Prefix>LevelCtrl_<hexpairs>` (zero-padded, e.g. "03").
  HA's SelectEntity renders a select "unknown" whenever current_option is not in
  options, so all four sat "unknown" (idle and running) even though every other
  select worked -- which is why it was only those four. Normalize the current
  value to the supported code with the same integer value so it matches an
  option (and its translation); convert back to the device's native un-padded
  format on write.

- diagnostics: pkg_version("smartthings-local") reads package metadata off disk
  (listdir + open + read_text), tripping HA's event-loop blocking-call detector.
  Offload it to the executor. Audited the rest of the package: config_flow's
  socket/crypto and every coordinator DTLS call are already offloaded via
  async_add_executor_job -- this was the only blocking call left on the loop.

Also documents air-conditioner support in the README (device table, capability
module list, platform list), missed when that support landed.

Updates the washer dosing tests to the corrected value/write format and adds a
current-option-is-a-valid-option regression; adds a diagnostics test asserting
the version lookup runs off the event loop.
2026-07-21 23:49:30 +00:00
themaanda c64f58aaa5 Add cooktop and range hood support 2026-07-21 16:57:45 -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 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 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 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 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 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 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
Marc Billow fd61e882d1 chore: update readme phrasing and instructions 2026-07-07 22:33:00 -05:00
Marc Billow ee068ed78d Rewrite README prose to remove AI-writing tropes
Replaced all em-dashes with plain punctuation, broke up the mechanical
bold-lead-in bullet list in "What you get" into prose paragraphs, and
removed the "X, not Y" contrastive framing and decorative arrows.
2026-07-07 22:25:01 -05:00
Marc Billow 12d60c295e Frame linked setup_cert.py as a reference example, not an instruction
Part 2 pointed readers to run smartthings-local's setup_cert.py directly;
reword it as an example of how to obtain the CA cert/key rather than a
step to execute, since this repo makes no claim about how that script
behaves or is maintained upstream.
2026-07-07 22:20:15 -05:00
Marc Billow 44afe4f00a Point README at upstream setup_cert.py instead of vendoring a copy
Getting the AC14K_M CA cert+key is a protocol-layer concern, not an
HA-integration concern, and the two copies here had already drifted from
each other. Drop root setup_cert.py + requirements-bootstrap.txt and have
Part 2 explain why the CA cert is needed, then link to the smartthings-local
project's setup_cert.py as the canonical way to obtain it.
2026-07-07 22:19:22 -05:00
Marc Billow 4782878583 Depend on published smartthings-local package, rewrite README
Now that mbillow/localthings#8's protocol fixes are merged upstream and
smartthings-local 0.1.0 is on PyPI, drop the vendored ocf/coap_dtls.py +
ocf/observe_refresh.py transport (dead code, never instantiated) and the
duplicated ocf_root_ca.pem in favor of the real package. manifest.json and
requirements-dev.txt now pin smartthings-local>=0.1.0 instead of the
unmerged git branch.

README rewritten from scratch — it still described the old standalone
MQTT-bridge (samsung_appliance/, main.py, docker-compose bridge) that this
repo moved off of; it now documents the real architecture: a native HA
custom component with config-flow-driven cert minting and a per-device-type
capability registry, with the protocol layer split out to smartthings-local.
2026-07-07 22:08:02 -05:00
Jack Nagy 00ff961d33 Drop dangling local-tools/ references from README 2026-06-30 19:51:36 +01:00
Quite Yellow 6f703fc148 Update README.md 2026-06-30 19:49:02 +01:00
Jack Nagy caf195ea1a Ship setup_cert.py to repo root, auto-fetch all CA materials
Closes #2.

Previously the cert minting script lived in local-tools/ (gitignored)
and the README pointed at a cert-only source that didn't include the
private key or upstream chain.

setup_cert.py now lives at the repo root and live-fetches both the
peer UUID (from the relevant TLS server cert subject DN) and the
full AC14K_M + upstream chain bundle (RemoteAccessCA + CECA + ROOTCA)
from a public mirror. Each fetch has an inline workaround if the
network is restricted (UUID=..., AC14K_M_CERT_BUNDLE=...,
BRAYSTORM_URL=...). Modulus-pair check catches a wrong-key mistake
before signing. bootstrap.py removed -- imported a package that was
renamed in commit b00c2fd.

Output files use neutral client.* names. README, .env.example,
docker-compose.yml, deploy.sh, and config.py updated to match.

Provenance receipts in local-tools/cert_provenance.md.
2026-06-30 19:27:24 +01:00
Jack Nagy b00c2fd90b Refactor to polling-first architecture with OBSERVE as accelerator
State freshness now comes from a tiered PollScheduler over the persistent
DTLS session; OBSERVE registrations are kept as an opportunistic
acceleration layer. Behaviour is identical online vs air-gapped except
for worst-case freshness latency.

Adds three modules:
- StateCache: single source of truth, source-tagged change events
- PollScheduler: hot/warm/cold + sweep tiers, write-defer past the
  fetchback-revert window, per-window RTT/slow-poll tracking
- KeepaliveTask: CoAP empty-CON ping with consecutive-fail detection
  driving MQTT availability

Bridge publishes per-appliance diagnostic entities (Push Active, Last
Update Source, Poll Max RTT, Slow Polls, Poll Errors, Stalest Resource
Age, Last OBSERVE Age) under HA's Diagnostic section. Tier cadences
are descriptor-declared, calibrated against measured per-firmware
ceilings (dryer ~14 req/s, oven ~8 req/s via probe_poll_rate_combined.py).

Drops HEARTBEAT_INTERVAL_S in favour of the descriptor-declared sweep
tier; PING_INTERVAL_S now consumed by KeepaliveTask inside the bridge
rather than driven from main.py.

README explains the push/poll split and what happens when the appliance
is blocked from internet.
2026-06-03 18:38:04 +01:00
Quite Yellow f577a32c50 Update README.md 2026-05-31 15:51:43 +01:00
Jack Nagy c99ef324fc Initial commit 2026-05-31 15:50:15 +01:00