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.
This commit is contained in:
@@ -3,13 +3,15 @@
|
||||
|
||||
One process supervises N Samsung appliances over their OCF CoAP-DTLS
|
||||
local APIs, publishing state + HA discovery to MQTT. Each appliance
|
||||
runs its own DTLS session in its own thread. MQTT is shared.
|
||||
runs its own DTLS session in its own thread, with an in-session
|
||||
PollScheduler driving state freshness and a KeepaliveTask driving
|
||||
DTLS-layer liveness. MQTT is shared.
|
||||
|
||||
Config is env-var driven:
|
||||
* APPLIANCE_COUNT plus APPLIANCE_<n>_{CLASS,IP,OCF_PORT,TOPIC,NAME}
|
||||
define the appliances to bridge.
|
||||
* Shared keys (MQTT_*, HA_DISCOVERY_PREFIX, CERT_PATH, KEY_PATH,
|
||||
HEALTH_INTERVAL_S, HEARTBEAT_INTERVAL_S) apply to all.
|
||||
HEALTH_INTERVAL_S, PING_INTERVAL_S) apply to all.
|
||||
|
||||
Reconnects on session errors per-appliance; shuts down cleanly on
|
||||
SIGINT / SIGTERM."""
|
||||
@@ -146,36 +148,12 @@ def main():
|
||||
break
|
||||
return loop
|
||||
|
||||
def make_heartbeat(b: PushBridge):
|
||||
def loop():
|
||||
while not b.stop.is_set():
|
||||
if b.stop.wait(shared.HEARTBEAT_INTERVAL_S):
|
||||
break
|
||||
b.heartbeat()
|
||||
return loop
|
||||
|
||||
def make_ping(b: PushBridge):
|
||||
def loop():
|
||||
while not b.stop.is_set():
|
||||
if b.stop.wait(shared.PING_INTERVAL_S):
|
||||
break
|
||||
b.ping_once()
|
||||
return loop
|
||||
|
||||
for b in bridges:
|
||||
tag = b.app.klass
|
||||
threads.append(threading.Thread(
|
||||
target=b.run_forever, daemon=True, name=f'{tag}-session'))
|
||||
threads.append(threading.Thread(
|
||||
target=make_health(b), daemon=True, name=f'{tag}-health'))
|
||||
if shared.HEARTBEAT_INTERVAL_S > 0:
|
||||
threads.append(threading.Thread(
|
||||
target=make_heartbeat(b), daemon=True,
|
||||
name=f'{tag}-heartbeat'))
|
||||
if shared.PING_INTERVAL_S > 0:
|
||||
threads.append(threading.Thread(
|
||||
target=make_ping(b), daemon=True,
|
||||
name=f'{tag}-ping'))
|
||||
|
||||
stopping = threading.Event()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user