From 60b622c6be669f9e028c603ec96535a170c8bfeb Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Tue, 30 Dec 2025 08:14:44 +0100 Subject: [PATCH] Align MQTT discovery with v1 format and HA specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pilot-v2/src/ha/mod.rs | 23 ++++++++++++++--------- pilot-v2/src/mqtt/mod.rs | 19 +++++++++++++++++-- pilot-v2/src/runtime/mod.rs | 10 +++++----- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/pilot-v2/src/ha/mod.rs b/pilot-v2/src/ha/mod.rs index 1bc302f..90164d9 100644 --- a/pilot-v2/src/ha/mod.rs +++ b/pilot-v2/src/ha/mod.rs @@ -19,6 +19,8 @@ struct DeviceInfo { #[derive(Serialize)] struct EntityConfig<'a> { name: &'a str, + #[serde(rename = "type")] + entity_type: &'a str, unique_id: String, state_topic: String, availability_topic: String, @@ -52,7 +54,6 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { suggested_area: cfg.device.suggested_area.clone(), }; - let availability = format!("{}/availability", base); let sensors = vec![ ("cpu_usage", "CPU Usage", Some("%"), None, Some("mdi:chip")), @@ -65,11 +66,13 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { ]; for (key, name, unit, class, icon) in sensors { + let entity_name = format!("{}_{}", key, cfg.device.name); let entity = EntityConfig { - name, + name: &entity_name, + entity_type: "sensor", unique_id: format!("{}_{}", cfg.device.name, key), - state_topic: format!("{}/state/{}", base, key), - availability_topic: availability.clone(), + state_topic: format!("{}/{}", base, key), + availability_topic: format!("{}/{}/available", base, key), payload_available: "online", payload_not_available: "offline", device: DeviceInfo { ..device.clone() }, @@ -80,7 +83,7 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { device_class: class, icon, }; - let topic = format!("{}/sensor/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key); + let topic = format!("{}/sensor/{}/{}/config", prefix, cfg.device.name, entity_name); publish_discovery(client, &topic, &entity).await?; } @@ -92,11 +95,13 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { ]; for (key, name, cmd) in switches { + let entity_name = format!("{}_{}", key, cfg.device.name); let entity = EntityConfig { - name, + name: &entity_name, + entity_type: "switch", unique_id: format!("{}_{}", cfg.device.name, key), - state_topic: format!("{}/state/{}", base, key), - availability_topic: availability.clone(), + state_topic: format!("{}/{}/state", base, key), + availability_topic: format!("{}/{}/available", base, key), payload_available: "online", payload_not_available: "offline", device: DeviceInfo { ..device.clone() }, @@ -107,7 +112,7 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { device_class: Some("switch"), icon: Some("mdi:power"), }; - let topic = format!("{}/switch/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key); + let topic = format!("{}/switch/{}/{}/config", prefix, cfg.device.name, entity_name); publish_discovery(client, &topic, &entity).await?; } diff --git a/pilot-v2/src/mqtt/mod.rs b/pilot-v2/src/mqtt/mod.rs index 3f1b3b2..ca468cf 100644 --- a/pilot-v2/src/mqtt/mod.rs +++ b/pilot-v2/src/mqtt/mod.rs @@ -97,14 +97,14 @@ pub async fn publish_capabilities( Ok(()) } -// Publie une valeur de capteur ou d'etat systeme. +// 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!("{}/state/{}", base_device_topic(cfg), name); + let topic = format!("{}/{}", base_device_topic(cfg), name); client .publish(topic, qos(cfg), cfg.mqtt.retain_states, value) .await @@ -112,6 +112,21 @@ pub async fn 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)); diff --git a/pilot-v2/src/runtime/mod.rs b/pilot-v2/src/runtime/mod.rs index be1249e..eec7376 100644 --- a/pilot-v2/src/runtime/mod.rs +++ b/pilot-v2/src/runtime/mod.rs @@ -310,10 +310,10 @@ async fn handle_command( // Publie l'etat initial des switches HA (par defaut ON). async fn publish_initial_command_states(client: &rumqttc::AsyncClient, cfg: &Config) { - let _ = mqtt::publish_state(client, cfg, "shutdown", "ON").await; - let _ = mqtt::publish_state(client, cfg, "reboot", "ON").await; - let _ = mqtt::publish_state(client, cfg, "sleep", "ON").await; - let _ = mqtt::publish_state(client, cfg, "screen", "ON").await; + let _ = mqtt::publish_switch_state(client, cfg, "shutdown", "ON").await; + let _ = mqtt::publish_switch_state(client, cfg, "reboot", "ON").await; + let _ = mqtt::publish_switch_state(client, cfg, "sleep", "ON").await; + let _ = mqtt::publish_switch_state(client, cfg, "screen", "ON").await; } // Publie l'etat d'une commande pour Home Assistant. @@ -328,5 +328,5 @@ async fn publish_command_state( CommandValue::Off => "OFF", }; let name = commands::action_name(action); - mqtt::publish_state(client, cfg, name, state).await + mqtt::publish_switch_state(client, cfg, name, state).await }