Commit Graph
20 Commits
Author SHA1 Message Date
Marc Billow aa9ec0f0d9 refactor: extract mqtt_demo/ — move bridge, config, logger, appliance descriptors 2026-07-04 16:11:10 -05:00
Marc Billow 24fc197a9e refactor: extract ocf/ — fold sensors.index_links into StateCache.index_device_tree 2026-07-04 16:02:08 -05:00
Marc Billow 10773bb8c8 refactor: extract protocol/ — split CoAP wire helpers from DtlsCoapSession 2026-07-04 15:55:12 -05:00
Marc Billow d33b46171a test: characterize CoAP wire encode/decode before extracting protocol/ 2026-07-04 15:50:38 -05:00
Marc Billow cae0592324 test: add pytest as a dev dependency 2026-07-04 15:49:39 -05:00
Quite Yellow 3cdfc6a3e6 Merge pull request #7 from QuiteYellow/artik-fridge
Merge PR #1: ARTIK051 fridge + session-recovery robustness
2026-07-02 21:24:28 +01:00
Jack Nagy b34f0e8248 Docs: fridge notes + firmware-family limitation callout
- Add ARTIK051_REF_17K row to the tested-combinations table with a
  link to aminorjourney's PR.
- New 'Firmware families — a limitation' section under Part 1
  explaining that descriptors are firmware-family-specific with no
  runtime feature detection, so the wrong descriptor produces
  half-broken sensors rather than a clean error.
- New 'Fridge (ARTIK051)' section under Per-appliance notes with the
  capability table + firmware-specific observations (port 49155,
  minimal /oic/res, vestigial /hass paths, collection-resource door
  model vs newer per-instance-resource fridges).
- Update config-keys reference: CLASS list gains 'fridge',
  OCF_PORT defaults list gains fridge=49155.
2026-07-02 21:22:00 +01:00
Aminorjourney 015e4a4e9e Session-recovery robustness (from PR #1)
- Suppress ConnectionError log noise from in-flight requests draining
  after a session close (poll_scheduler + keepalive log at DEBUG).
- Null KeepaliveTask.on_unreachable during a forced reconnect so the
  dying session's keepalive can't flip HA availability offline after
  the new session is already healthy. Ported into PR #5's
  _maybe_force_reconnect flow.

Drops the _session_stop / publish-health force-close mechanism from
the original PR — PR #5's last_success_ts + _maybe_force_reconnect
already covers the 'session dead, restart it' goal via a different
path, and running both means two paths force-closing the same session
on the same failure.
2026-07-02 21:15:00 +01:00
Aminorjourney 93f78f99a2 feat: add Samsung ARTIK051_REF_17K fridge-freezer descriptor
- Full resource map discovered via live CoAP-DTLS session against
  ARTIK051_REF_17K fridge-freezer (firmware DA-REF-ART-COMMON-1_20201124)
- seed_path: /device/0 (returns 32 links including hidden appliance resources)
- Entities: fridge/freezer temp + setpoints, 3x door sensors (fridge,
  freezer, convertible zone), power (W), energy (kWh, total_increasing),
  water filter usage/status, ice maker, rapid fridge/freeze, sabbath mode
- Port: 49155 (not 49154)

First known public documentation of this firmware's local CoAP-DTLS
resource layout.
2026-07-02 21:10:00 +01:00
Jack Nagy e4c6702c48 Add MIT license 2026-07-01 20:30:48 +01:00
Jack Nagy e9a30d96f9 Drop dangling local-tools/ references from README 2026-06-30 19:51:36 +01:00
Quite Yellow 788408da9a Update README.md 2026-06-30 19:49:02 +01:00
Quite Yellow 27a614fcb8 Merge pull request #5 from QuiteYellow/production-hardening
Production-hardening pass: half-open detection, cascade throttle, OBSERVE refresh, lamp/door coupling
2026-06-30 19:36:09 +01:00
Quite Yellow 19946e21d7 Merge pull request #3 from QuiteYellow/cert-refactor
Ship setup_cert.py to repo root, auto-fetch all CA materials
2026-06-30 19:33:41 +01:00
Jack Nagy 36acf3a209 Production-hardening pass: half-open detection, cascade throttle, OBSERVE refresh, lamp/door coupling
Four failure modes observed in-house since the polling-first refactor
(709fdf4):

1. Half-open DTLS sessions where the socket stays writable but the peer
   has gone silent. Ping sends succeed against a wedged peer because
   RT-OCF doesn't reliably emit a RST; only successful polls prove the
   session is live.

   - PollScheduler exposes last_success_ts (bumped on every 2.05).
   - KeepaliveTask takes liveness_fn(); ticks fail if no 2.05 in the
     last 60s, even when the ping send succeeded.
   - Bridge force-closes the session after 120s unreachable so
     run_forever() breaks out of sess.join() and reconnects.

2. RT-OCF cascade under load. One wedged path can eat 8s of timeout,
   the next tier tick fires immediately and stacks another attempt,
   and the device wedges harder.

   - PollTier.timeout_s per-tier override (hot=2s, warm=4s, sweep=15s).
   - On TimeoutError, the href goes into a 5-60s cooldown via the
     existing _defer_until mechanism.
   - take_window_stats() now reports successful-poll RTT separately
     from a timeout count, exposed as the "Poll Timeouts (window)"
     diagnostic entity in HA.
   - Active-window throttle: if the previous health window saw >=3
     timeouts and is_active=True, drop back to idle cadence -- stops
     stacking polls on a stalled responder.

3. OBSERVE table aging across cloud-auth blips. The device stays
   DTLS-reachable but the on-device stack clears its observer table
   during the blip, so push delivery stays dead even after upstream
   recovers.

   - New ObserveRefreshTask per bridge; every 6h derregs all current
     observer tokens and re-subscribes on the existing session.

4. Oven lamp/door coupling. Oven hardware auto-drives the lamp from
   door state but /mode/vs/0 is warm-tier (30s) so HA showed stale lamp
   during a cook.

   - Track door + lamp value-change timestamps in descriptor_state.
     When the door transition is newer, derive lamp from door_open.
     When an HA optimistic write is newer, the cache value wins.

Also: ANSI-coloured WARNING/ERROR lines (NO_COLOR=1 opt-out), jittered
reconnect backoff so dryer + oven don't sync up after a router blip.

In-house verification: running on dryer + oven since 2026-06-03.
2026-06-30 19:27:59 +01:00
Jack Nagy 0374f8cf68 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 709fdf4.

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 709fdf444d 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
Jack Nagy 8775f7a930 Add oven controls and fix Samsung-OCF write semantics
Major session of local-OCF reverse engineering against the NV7000BS
oven and DV5000T dryer. Surfaces a working set of HA entities for the
oven and resolves several Samsung-quirk regressions in the bridge's
write path.

Key behavioural fixes:
- OBSERVE registrations now use single-byte tokens. Samsung RT-OCF
  silently drops registrations with TKL>1; same 4-byte tokens work
  fine for GET/POST. Symptom was that writes returned 2.04 but the
  appliance never pushed state changes.
- Per-session random starting tokens + MID. Samsung retains observer
  state across DTLS reconnects from the same cert; reusing tokens on
  reconnect silently no-ops.
- OBSERVE deregister sent on DtlsCoapSession.close(), with a stop-
  watcher thread in PushBridge.session_once() so SIGINT/SIGTERM
  actually reaches close() instead of hanging in sess.join().
- pyOpenSSL is not thread-safe — reader-loop conn.* calls now hold
  the same _send_lock the sender uses, dispatching decrypted packets
  outside the lock so the auto-ACK send doesn't deadlock.
- Periodic CoAP Ping (RFC 7252 §4.4) keepalive to keep DTLS warm.
- Post-write Block2 fetchback REMOVED. It was the root cause of
  every "setpoint/operationTime/modes revert ~3s after write"
  symptom — Samsung's stack treats a read on a freshly-written
  resource as a signal to invalidate that write. OBSERVE pushes
  keep HA in sync without the verification GET.

HA-facing changes (oven):
- New entities: Lamp (light), Sound (switch), Fast preheat, Natural
  steam, Setpoint (number), Cook time (number), Stop cycle (button).
- Cooking mode surfaced as a read-only sensor — the oven owns the
  modes field once a cycle is active and rolls local writes back.
- Cook time writes operationTime + remainingTime on
  /operational/state/vs/0 (discovered via OBSERVE capture of
  SmartThings mid-cycle changes — UpperTimerSet on /mode/vs/0
  options is vestigial and doesn't drive the running cycle).
- New cycle_active MQTT availability topic. Writes the oven only
  honours mid-cycle (setpoint, cook time, fast preheat, natural
  steam, stop) gate on it via avail_with_cycle / avail_with_remote_
  and_cycle. Sound + Lamp remain always-available.
- Cycle Start deliberately NOT exposed. Every byte-level approxi-
  mation of SmartThings's working start sequence is rejected at
  the firmware level. Empty discovery payloads remove the previous
  Start button and Cooking-mode select cleanly from HA.

Diagnostics:
- DEBUG_BRIDGE=1 env var enables verbose tracing (rx CON/NON/ACK/
  RST per frame, full link-tree dump at seed, /oic/res directory,
  REP changes on /operational/state, /oven, /power, mode options).
  Quiet in production.
2026-05-31 20:43:47 +01:00
Quite Yellow 85580d81b7 Update README.md 2026-05-31 15:51:43 +01:00
Jack Nagy c99ef324fc Initial commit 2026-05-31 15:50:15 +01:00