Files
pilot/pilot-v2/src/mqtt/mod.rs
T
gilles 60b622c6be Align MQTT discovery with v1 format and HA specification
This commit fixes the MQTT discovery payload structure to match the working
v1 implementation and comply with Home Assistant's MQTT discovery specification.

Key changes:
- Add "type" field to discovery payload (sensor/switch)
- Update discovery topic format: homeassistant/{component}/{node_id}/{entity_name}/config
- Fix entity naming: {metric}_{device} instead of descriptive names
- Separate state topics for sensors vs switches:
  * Sensors: pilot/{device}/{metric} (no /state suffix)
  * Switches: pilot/{device}/{metric}/state (with /state suffix)
- Add per-entity availability topics: pilot/{device}/{metric}/available
- Add publish_switch_state() function for proper switch state publishing

Discovery topic examples:
- homeassistant/sensor/asus/cpu_usage_asus/config
- homeassistant/switch/asus/shutdown_asus/config

State topic examples:
- pilot/asus/cpu_usage (sensor)
- pilot/asus/shutdown/state (switch)

This matches the Dell 5520 v1 configuration that works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 08:14:44 +01:00

148 lines
4.2 KiB
Rust

// Ce module gere la connexion MQTT et les publications de base.
use anyhow::{Context, Result};
use rumqttc::{AsyncClient, EventLoop, LastWill, MqttOptions, QoS};
use serde::Serialize;
use std::time::Duration;
use crate::config::{base_device_topic, Config};
pub struct MqttHandle {
pub client: AsyncClient,
pub event_loop: EventLoop,
}
#[derive(Debug, Serialize)]
pub struct Status {
pub version: String,
pub os: String,
pub uptime_s: u64,
pub last_error: String,
pub backends: Backends,
}
#[derive(Debug, Serialize)]
pub struct Backends {
pub power: String,
pub screen: String,
}
#[derive(Debug, Serialize)]
pub struct Capabilities {
pub telemetry: Vec<String>,
pub commands: Vec<String>,
pub gpu: bool,
}
// Cree un client MQTT configure selon le YAML.
pub fn connect(cfg: &Config) -> Result<MqttHandle> {
let client_id = if cfg.mqtt.client_id.trim().is_empty() {
cfg.device.name.clone()
} else {
cfg.mqtt.client_id.clone()
};
let mut options = MqttOptions::new(client_id, cfg.mqtt.host.clone(), cfg.mqtt.port);
options.set_keep_alive(Duration::from_secs(cfg.mqtt.keepalive_s));
if !cfg.mqtt.username.trim().is_empty() || !cfg.mqtt.password.trim().is_empty() {
options.set_credentials(cfg.mqtt.username.clone(), cfg.mqtt.password.clone());
}
let will_topic = format!("{}/availability", base_device_topic(cfg));
let will = LastWill::new(will_topic, "offline", qos(cfg), true);
options.set_last_will(will);
let (client, event_loop) = AsyncClient::new(options, 10);
Ok(MqttHandle { client, event_loop })
}
// Publie availability en retained pour indiquer online/offline.
pub async fn publish_availability(client: &AsyncClient, cfg: &Config, online: bool) -> Result<()> {
let topic = format!("{}/availability", base_device_topic(cfg));
let payload = if online { "online" } else { "offline" };
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish availability")?;
Ok(())
}
// Publie un status JSON (version, OS, backends, etc.).
pub async fn publish_status(
client: &AsyncClient,
cfg: &Config,
status: &Status,
) -> Result<()> {
let topic = format!("{}/status", base_device_topic(cfg));
let payload = serde_json::to_vec(status).context("serialize status")?;
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish status")?;
Ok(())
}
// Publie les capacites actives (telemetrie/commandes).
pub async fn publish_capabilities(
client: &AsyncClient,
cfg: &Config,
capabilities: &Capabilities,
) -> Result<()> {
let topic = format!("{}/capabilities", base_device_topic(cfg));
let payload = serde_json::to_vec(capabilities).context("serialize capabilities")?;
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish capabilities")?;
Ok(())
}
// Publie une valeur de capteur (sensors: sans /state à la fin).
pub async fn publish_state(
client: &AsyncClient,
cfg: &Config,
name: &str,
value: &str,
) -> Result<()> {
let topic = format!("{}/{}", base_device_topic(cfg), name);
client
.publish(topic, qos(cfg), cfg.mqtt.retain_states, value)
.await
.context("publish state")?;
Ok(())
}
// Publie l'état d'un switch (avec /state à la fin).
pub async fn publish_switch_state(
client: &AsyncClient,
cfg: &Config,
name: &str,
value: &str,
) -> Result<()> {
let topic = format!("{}/{}/state", base_device_topic(cfg), name);
client
.publish(topic, qos(cfg), cfg.mqtt.retain_states, value)
.await
.context("publish switch state")?;
Ok(())
}
// S'abonne aux commandes standard (cmd/<action>/set).
pub async fn subscribe_commands(client: &AsyncClient, cfg: &Config) -> Result<()> {
let topic = format!("{}/cmd/+/set", base_device_topic(cfg));
client
.subscribe(topic, qos(cfg))
.await
.context("subscribe commands")?;
Ok(())
}
// Convertit le QoS configure (0/1/2) en enum rumqttc.
fn qos(cfg: &Config) -> QoS {
match cfg.mqtt.qos {
1 => QoS::AtLeastOnce,
2 => QoS::ExactlyOnce,
_ => QoS::AtMostOnce,
}
}