OVEN_SETPOINT (/temperatures/vs/0) and OVEN_MODE (/mode/vs/0) collide
with fridge hrefs. Remove from ALL; only OVEN_CAVITY (/oven/vs/0) is
oven-unique. Per-device-type registries (v2 plan) will restore them in
the oven-only registry.
Also updates golden baselines to include legitimate new entities added
since original capture (cycle_active, firmware_update, power_switch for
dishwasher; cabinet_light_switch, ice1 for refrigerator).
- Make href Optional[str] = None for pattern capabilities
- Remove observe field (v2 architecture observes all hrefs)
- Add rt_filter, match_fn, key_fn fields to Capability
- Add entities default value tuple[()] to Capability
- Add key_override field to BoundEntity
- Update _key() to use key_override when present
- Update _make_observe_paths() to observe all unique hrefs (OBSERVE as accelerator)
- Guard _build() to skip pattern capabilities (href is None)
- Add test_bound_entity_key_override_takes_precedence test
- Update test_capability_defaults to remove observe assertion
All 61 tests passing (60 existing + 1 new).
C1: SwitchDesc MQTT discovery now emits payload_on/off='On'/'Off',
state_on/off='On'/'Off', and a Jinja2 boolean-to-string value_template
so HA can correctly interpret and command switch entities.
C2: POWER write_fn corrected — path is ['power','vs','0'] segment list
(not a single slash-prefixed string), and payload comparison is
p == 'On' (not truthiness) so 'Off' can actually be sent.
I2: Golden regression oracle strengthened to exact equality for both
state_keys and discovery_unique_ids; prior subset-only check allowed
over-production to go undetected. Two golden tests now fail revealing
stale fixtures (dishwasher: +3 keys; refrigerator: +9 keys) — fixtures
intentionally NOT updated, human review required.
PushBridge no longer takes a descriptor at construction time. Instead it
discovers entities from the live device cache immediately after the seed
(via registry.discover + build_runtime_descriptor), publishes HA discovery
payloads, wires on_observation, and builds command handlers — all in the
new idempotent _discover_and_publish(sess) method called from
_run_session_inner.
Changes:
- bridge.py: remove descriptor param; self.descriptor=None until first
connect; SEED_PATH constant replaces self.descriptor.seed_path; all
self.descriptor accesses guarded or moved post-discovery; inlined
_bridge_diagnostic_discovery (was in appliances/base.py); subscribe to
observe paths now happens after discovery (seed → discover → subscribe).
- config.py: klass is now optional (defaults to ''); topic_prefix/name
defaults handle empty klass.
- main.py: drop get_descriptor import and pairs loop; build bridges as
PushBridge(shared, app, cli); on_connect no longer publishes
discovery_payloads (bridge self-publishes at discovery time).
- Delete samsung_appliance/appliances/ entirely (6 files, ~2600 lines).
Generates regression oracle from DISHWASHER and REFRIGERATOR descriptors:
- tests/fixtures/golden/dishwasher.json: 21 state keys, 23 discovery entities
- tests/fixtures/golden/refrigerator.json: 26 state keys, 24 discovery entities
- tests/test_golden_regression.py: parametrized test that will turn green as
Tasks 9-12 port capabilities into the new registry
OCF rt is a schema category shared by many unrelated resources.
The stable unique resource address is the href. Update Capability,
discovery, common capabilities, tests, and plan accordingly.
Implement discovery module for converting OCF resource metadata into bound
entities, with support for multi-instance device resources and logging of
unknown capabilities.
Four failure modes observed in-house since the polling-first refactor
(b00c2fd):
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.
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.
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.
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.