thermal+ha: don't lose boot-time pages; stop HA gracefully

van-thermal fires its first sample seconds after start, and the Pi often
boots hot — order after network-online.target and retry network failures
in the Pushover send path (3 attempts, 15s apart) so that page survives
DNS not being up yet. Non-200 responses still don't retry.

The Quadlet gets StopTimeout=120: podman's default 10s window SIGKILLed
HA mid-flush and the recorder complained about an unclean sqlite shutdown
on every start. Verified clean after a full stop/start cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-07-07 16:06:16 -04:00
co-authored by Claude Fable 5
parent 3add42a0cb
commit 6fe683000d
3 changed files with 25 additions and 11 deletions
+3
View File
@@ -24,6 +24,9 @@ Environment=TZ=America/Toronto
# recovery. HA is rootful + host-net anyway. # recovery. HA is rootful + host-net anyway.
PodmanArgs=--security-opt apparmor=unconfined PodmanArgs=--security-opt apparmor=unconfined
AddCapability=NET_ADMIN NET_RAW AddCapability=NET_ADMIN NET_RAW
# Podman's default 10s stop window SIGKILLs HA mid-flush and the recorder
# complains about an unclean sqlite shutdown on the next start.
StopTimeout=120
[Service] [Service]
Restart=always Restart=always
+17 -10
View File
@@ -73,7 +73,7 @@ def load_creds(cfg):
return None return None
def pushover(cfg, title, message, priority=0): def pushover(cfg, title, message, priority=0, attempts=3, retry_delay=15):
creds = load_creds(cfg) creds = load_creds(cfg)
if not creds: if not creds:
log(f"pushover skipped (no credentials): {title} — {message}", "warn") log(f"pushover skipped (no credentials): {title} — {message}", "warn")
@@ -84,15 +84,22 @@ def pushover(cfg, title, message, priority=0):
"message": message, "priority": priority, "message": message, "priority": priority,
}).encode() }).encode()
req = urllib.request.Request("https://api.pushover.net/1/messages.json", data=data) req = urllib.request.Request("https://api.pushover.net/1/messages.json", data=data)
try: # Retry network failures (DNS not up yet at boot, WAN flap) — they block the
with urllib.request.urlopen(req, timeout=10) as resp: # sample loop briefly, which is fine at this cadence. A non-200 means Pushover
ok = resp.status == 200 # rejected the request (bad token etc.); retrying won't change that.
if not ok: for attempt in range(1, attempts + 1):
log(f"pushover HTTP {resp.status}", "warn") try:
return ok with urllib.request.urlopen(req, timeout=10) as resp:
except Exception as e: ok = resp.status == 200
log(f"pushover send failed: {e}", "warn") if not ok:
return False log(f"pushover HTTP {resp.status}", "warn")
return ok
except Exception as e:
last = attempt == attempts
log(f"pushover send failed ({attempt}/{attempts}): {e}", "warn")
if not last:
time.sleep(retry_delay)
return False
def load_config(): def load_config():
+5 -1
View File
@@ -1,6 +1,10 @@
[Unit] [Unit]
Description=Temperature monitor (CPU + NVMe) for the campervan router Description=Temperature monitor (CPU + NVMe) for the campervan router
After=local-fs.target # Order after network-online so a boot-time threshold crossing (common: the Pi
# boots hot) can actually reach Pushover — the very first sample fires within
# seconds of start. If no WAN comes up, wait-online times out and we start anyway.
After=local-fs.target network-online.target
Wants=network-online.target
[Service] [Service]
Type=simple Type=simple