// 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, pub commands: Vec, pub gpu: bool, } // Cree un client MQTT configure selon le YAML. pub fn connect(cfg: &Config) -> Result { 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//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, } }