diff --git a/core/archipelago/src/api/rpc/package/pine_ha.rs b/core/archipelago/src/api/rpc/package/pine_ha.rs index dc1efdad..2e045ec3 100644 --- a/core/archipelago/src/api/rpc/package/pine_ha.rs +++ b/core/archipelago/src/api/rpc/package/pine_ha.rs @@ -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();