fix(pine): announce the first-ever mesh message + halve announce lag

The seeded announce automation blocked state transitions out of None/
unavailable — but None is exactly the sensor's state before the first
received message (and after the in-memory store restarts empty), so a
fresh node's first DM was silently swallowed (framework-pt 2026-07-22).
Only 'unknown' (the HA-restart marker) still blocks; rest sensors don't
restore state, so restart announce-storms remain impossible. Seeder
upgrades an already-seeded automations.yaml by replacing exactly the v1
clause, leaving user edits untouched.

Sensor scan_interval 30->15s: the sensor only carries the latest message
id, so two messages inside one poll window coalesce into one announce —
halving the window halves both the lag and the coalescing odds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-22 15:51:18 -04:00
parent 6171168927
commit a9cd164301

View File

@ -123,7 +123,11 @@ rest:
- resource: http://host.containers.internal/api/pine/status
headers:
Authorization: "Bearer {token}"
scan_interval: 30
# 15s: the mesh-message sensor only carries the LATEST message id, so two
# messages inside one poll window coalesce and only the newest announces
# (observed on framework-pt 2026-07-22). Halving the window halves both
# the announce lag and the coalescing odds; the endpoint is local + cheap.
scan_interval: 15
sensor:
- name: "Archy Block Height"
unique_id: archy_block_height
@ -337,7 +341,14 @@ const MESH_ANNOUNCE_AUTOMATION_ID: &str = "archy_mesh_announce";
/// Announce new mesh messages on every Assist satellite. Only seeded into an
/// empty/missing automations.yaml — merging into user-authored YAML risks
/// mangling it, and anyone with existing automations can paste this one from
/// the docs. Trigger conditions skip HA-restart transitions (unknown -> id).
/// the docs. Trigger conditions skip HA-restart transitions (unknown -> id)
/// but deliberately allow `None`/`unavailable` -> id: `None` is the sensor's
/// state before the first-ever received message (and again whenever the mesh
/// message store restarts empty), and `unavailable` follows any endpoint
/// blip — blocking those swallowed the first DM a fresh node ever received
/// (framework-pt, 2026-07-22). HA restarts still can't re-announce an old
/// message: rest sensors don't restore state, so the post-restart transition
/// is always from `unknown`.
const MESH_ANNOUNCE_AUTOMATION: &str = r#"- id: archy_mesh_announce
alias: Announce mesh messages on Pine
description: Speak new mesh messages on the Pine speaker (seeded by Archipelago)
@ -348,7 +359,7 @@ const MESH_ANNOUNCE_AUTOMATION: &str = r#"- id: archy_mesh_announce
- condition: template
value_template: >-
{{ trigger.from_state is not none
and trigger.from_state.state not in ['unknown', 'unavailable', 'None']
and trigger.from_state.state != 'unknown'
and trigger.to_state.state not in ['unknown', 'unavailable', 'None']
and trigger.from_state.state != trigger.to_state.state }}
actions:
@ -371,6 +382,22 @@ async fn seed_mesh_announce_automation() -> bool {
let path = std::path::Path::new(HA_CONFIG_DIR).join("automations.yaml");
let existing = tokio::fs::read_to_string(&path).await.unwrap_or_default();
if existing.contains(MESH_ANNOUNCE_AUTOMATION_ID) {
// v1 of the seeded condition also blocked `None`/`unavailable` -> id
// transitions, swallowing the first-ever received message. Upgrade by
// replacing exactly that clause; anything else in the file (including
// user-added automations) is left untouched.
const V1_CLAUSE: &str =
"trigger.from_state.state not in ['unknown', 'unavailable', 'None']";
const V2_CLAUSE: &str = "trigger.from_state.state != 'unknown'";
if existing.contains(V1_CLAUSE) {
let upgraded = existing.replace(V1_CLAUSE, V2_CLAUSE);
if let Err(e) = tokio::fs::write(&path, upgraded).await {
warn!("pine/HA seed: upgrading automations.yaml failed: {e}");
return false;
}
info!("pine/HA seed: mesh announce automation upgraded (first message announces)");
return true;
}
return false;
}
let trimmed = existing.trim();