mirror of
https://github.com/ruvnet/RuView
synced 2026-07-26 18:01:48 +00:00
bc47812351
Iter 28. Closes the per-node lifecycle on the MQTT side: HA can now
distinguish a node that is healthy + publishing zero events (nothing
detected) from a node that has lost the broker connection. Discovery
payloads now reference the availability topic so every entity inherits
the device-level offline marker.
Added (gated on `feature = "std"`):
- src/availability.rs:
* PAYLOAD_AVAILABLE = "online", PAYLOAD_NOT_AVAILABLE = "offline"
* availability_topic(node_id) -> "ruview/<node>/bfld/availability"
* online_message / offline_message constructors returning TopicMessage
* publish_availability_online / publish_availability_offline
bootstrap helpers through Publish trait
- pub use the full availability surface from lib.rs
Discovery integration (src/ha_discovery.rs):
- Every entity config payload now carries:
"availability_topic": "ruview/<node>/bfld/availability"
"payload_available": "online"
"payload_not_available": "offline"
HA uses these to grey out entities device-wide when the broker LWT
fires or the node explicitly publishes "offline" during shutdown.
tests/availability_topic.rs (10 named tests, all green):
availability_topic_format_matches_documented_path
online_message_is_retained_friendly_payload
offline_message_is_retained_friendly_payload
publish_online_lands_one_message
publish_offline_lands_one_message
discovery_payload_includes_availability_topic_field
(all 6 Anonymous-class discovery payloads carry the field)
discovery_payload_includes_payload_available_and_not_available_strings
restricted_class_discovery_still_carries_availability_fields
(availability is not an identity field; class 3 retains it)
bootstrap_sequence_online_then_discovery_lands_in_order
*** End-to-end bootstrap proof: publish_availability_online +
publish_discovery produces 1 + 6 = 7 messages, "online"
first, six homeassistant/.../config payloads after. ***
graceful_shutdown_sequence_publishes_offline_message_last
ACs progressed:
- ADR-122 §2.2 — availability topic now in place. Operators get HA
online/offline indication without configuring LWT explicitly on
rumqttc — the offline_message constructor + publish_availability_offline
cover the explicit-shutdown path. Real LWT wiring (rumqttc's
MqttOptions::set_last_will) is a follow-up.
- ADR-122 AC1 + AC4 — discovery now includes availability_topic, which
HA needs to render the device as a unit; iter-26 tests continue to
pass with the augmented payload (verified by full-suite count: 187 + 10).
Test config:
- cargo test --no-default-features → 72 passed (availability cfg-out)
- cargo test → 203 passed (193 + 10)
Out of scope (next iter target):
- Wire rumqttc::MqttOptions::set_last_will(...) so the broker
auto-publishes "offline" when the TCP session drops; needs a small
helper on RumqttPublisher to build options with LWT pre-configured.
- GitHub Actions workflow with mosquitto Docker so iter-24 live test
runs in CI.
Co-Authored-By: claude-flow <ruv@ruv.net>
80 lines
3.0 KiB
Rust
80 lines
3.0 KiB
Rust
//! `ruview/<node_id>/bfld/availability` topic helpers. ADR-122 §2.2.
|
|
//!
|
|
//! HA expects each device to publish an availability topic so the UI can grey
|
|
//! out entities when the device is offline. Convention:
|
|
//!
|
|
//! - Publish `"online"` with `retain = true` immediately after broker CONNECT.
|
|
//! - Configure the MQTT client's Last Will and Testament (LWT) to publish
|
|
//! `"offline"` (also retained) so the broker auto-marks the device offline
|
|
//! when the TCP session drops without a clean DISCONNECT.
|
|
//!
|
|
//! HA discovery payloads (iter 26) reference this same topic via the
|
|
//! `availability_topic` field so every BFLD entity inherits the marker.
|
|
|
|
#![cfg(feature = "std")]
|
|
|
|
use crate::mqtt_topics::{Publish, TopicMessage};
|
|
|
|
/// Payload string published when the node is healthy.
|
|
pub const PAYLOAD_AVAILABLE: &str = "online";
|
|
|
|
/// Payload string published when the node has disconnected.
|
|
pub const PAYLOAD_NOT_AVAILABLE: &str = "offline";
|
|
|
|
/// Build the canonical `ruview/<node_id>/bfld/availability` topic string.
|
|
#[must_use]
|
|
pub fn availability_topic(node_id: &str) -> String {
|
|
let mut s = String::with_capacity(7 + node_id.len() + 19);
|
|
s.push_str("ruview/");
|
|
s.push_str(node_id);
|
|
s.push_str("/bfld/availability");
|
|
s
|
|
}
|
|
|
|
/// Build the `(topic, "online")` pair to publish on broker connect.
|
|
#[must_use]
|
|
pub fn online_message(node_id: &str) -> TopicMessage {
|
|
TopicMessage {
|
|
topic: availability_topic(node_id),
|
|
payload: PAYLOAD_AVAILABLE.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Build the `(topic, "offline")` pair — usually configured as the broker LWT
|
|
/// rather than published explicitly, but provided here for explicit-shutdown
|
|
/// scenarios (graceful stop, planned maintenance) where the operator wants
|
|
/// HA to update immediately rather than waiting for the LWT keep-alive timeout.
|
|
#[must_use]
|
|
pub fn offline_message(node_id: &str) -> TopicMessage {
|
|
TopicMessage {
|
|
topic: availability_topic(node_id),
|
|
payload: PAYLOAD_NOT_AVAILABLE.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Bootstrap helper: publish the `"online"` availability marker through
|
|
/// `publisher`. Pairs with `publish_discovery` (iter 27) and `publish_event`
|
|
/// (iter 22) for the full startup sequence:
|
|
///
|
|
/// ```ignore
|
|
/// publish_availability_online(&mut retained_pub, "seed-01")?; // "online", retained
|
|
/// publish_discovery(&mut retained_pub, "seed-01", PrivacyClass::Anonymous)?;
|
|
/// // ... then BfldPipelineHandle::spawn(pipeline, state_pub) for the per-frame loop
|
|
/// ```
|
|
pub fn publish_availability_online<P: Publish>(
|
|
publisher: &mut P,
|
|
node_id: &str,
|
|
) -> Result<(), P::Error> {
|
|
publisher.publish(&online_message(node_id))
|
|
}
|
|
|
|
/// Bootstrap helper: publish the `"offline"` availability marker through
|
|
/// `publisher`. Use during a graceful shutdown so HA reflects the state
|
|
/// immediately instead of waiting for the broker LWT timeout.
|
|
pub fn publish_availability_offline<P: Publish>(
|
|
publisher: &mut P,
|
|
node_id: &str,
|
|
) -> Result<(), P::Error> {
|
|
publisher.publish(&offline_message(node_id))
|
|
}
|