From df871ddf6e5453e1d10e4b19997bae935dbbe048 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Tue, 30 Dec 2025 07:39:24 +0100 Subject: [PATCH] Fix Home Assistant MQTT discovery compliance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes several issues with the MQTT discovery payload to ensure full compliance with Home Assistant's MQTT discovery specification and match the working configuration from the v1 implementation. Changes: - Add payload_available ("online") and payload_not_available ("offline") fields to EntityConfig struct for proper availability handling in HA - Make device_info parameters (manufacturer, model, sw_version, suggested_area) configurable via config.yaml instead of hardcoded values - Remove incorrect device_class "power" from cpu_usage sensor (power is for Watts, not %) - Update config.example.yaml with documented device_info fields The discovery payload now correctly includes all required fields for HA to properly register and display the device with its sensors and switches. Tested on physical PC (asus) with MQTT broker at 10.0.0.3:1883. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- config/config.example.yaml | 4 ++++ pilot-v2/config.yaml | 4 ++++ pilot-v2/src/config/mod.rs | 19 +++++++++++++++++++ pilot-v2/src/ha/mod.rs | 17 +++++++++++++---- 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/config/config.example.yaml b/config/config.example.yaml index 5d516ba..68ece8c 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -6,6 +6,10 @@ device: name: $hostname # Use $hostname for automatic hostname, or a custom name like "my-pc" identifiers: ["$hostname"] + manufacturer: "Pilot" # Manufacturer name shown in Home Assistant + model: "PC Agent" # Model name (e.g., "Laptop", "Desktop", "Server") + sw_version: "2.0.0" # Software version + suggested_area: "Bureau" # Suggested area in Home Assistant (optional) mqtt: host: "127.0.0.1" diff --git a/pilot-v2/config.yaml b/pilot-v2/config.yaml index 1e68850..c478bdf 100644 --- a/pilot-v2/config.yaml +++ b/pilot-v2/config.yaml @@ -2,6 +2,10 @@ device: name: $hostname identifiers: ["$hostname"] + manufacturer: "Asus" + model: "Laptop" + sw_version: "2.0.0" + suggested_area: "Bureau" mqtt: host: "10.0.0.3" diff --git a/pilot-v2/src/config/mod.rs b/pilot-v2/src/config/mod.rs index c338df1..8e64faa 100644 --- a/pilot-v2/src/config/mod.rs +++ b/pilot-v2/src/config/mod.rs @@ -20,6 +20,25 @@ pub struct Config { pub struct Device { pub name: String, pub identifiers: Vec, + #[serde(default = "default_manufacturer")] + pub manufacturer: String, + #[serde(default = "default_model")] + pub model: String, + #[serde(default = "default_sw_version")] + pub sw_version: String, + pub suggested_area: Option, +} + +fn default_manufacturer() -> String { + "Pilot".to_string() +} + +fn default_model() -> String { + "PC Agent".to_string() +} + +fn default_sw_version() -> String { + "2.0.0".to_string() } #[derive(Debug, Clone, Deserialize)] diff --git a/pilot-v2/src/ha/mod.rs b/pilot-v2/src/ha/mod.rs index 6221886..1bc302f 100644 --- a/pilot-v2/src/ha/mod.rs +++ b/pilot-v2/src/ha/mod.rs @@ -12,6 +12,8 @@ struct DeviceInfo { manufacturer: String, model: String, sw_version: String, + #[serde(skip_serializing_if = "Option::is_none")] + suggested_area: Option, } #[derive(Serialize)] @@ -20,6 +22,8 @@ struct EntityConfig<'a> { unique_id: String, state_topic: String, availability_topic: String, + payload_available: &'a str, + payload_not_available: &'a str, device: DeviceInfo, #[serde(skip_serializing_if = "Option::is_none")] command_topic: Option, @@ -42,15 +46,16 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { let device = DeviceInfo { identifiers: cfg.device.identifiers.clone(), name: cfg.device.name.clone(), - manufacturer: "Pilot".to_string(), - model: "v2".to_string(), - sw_version: "2.0.0".to_string(), + manufacturer: cfg.device.manufacturer.clone(), + model: cfg.device.model.clone(), + sw_version: cfg.device.sw_version.clone(), + suggested_area: cfg.device.suggested_area.clone(), }; let availability = format!("{}/availability", base); let sensors = vec![ - ("cpu_usage", "CPU Usage", Some("%"), Some("power"), Some("mdi:chip")), + ("cpu_usage", "CPU Usage", Some("%"), None, Some("mdi:chip")), ("memory_used_mb", "Memory Used", Some("MB"), None, Some("mdi:memory")), ("memory_total_mb", "Memory Total", Some("MB"), None, Some("mdi:memory")), ("ip_address", "IP Address", None, None, Some("mdi:ip")), @@ -65,6 +70,8 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { unique_id: format!("{}_{}", cfg.device.name, key), state_topic: format!("{}/state/{}", base, key), availability_topic: availability.clone(), + payload_available: "online", + payload_not_available: "offline", device: DeviceInfo { ..device.clone() }, command_topic: None, payload_on: None, @@ -90,6 +97,8 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> { unique_id: format!("{}_{}", cfg.device.name, key), state_topic: format!("{}/state/{}", base, key), availability_topic: availability.clone(), + payload_available: "online", + payload_not_available: "offline", device: DeviceInfo { ..device.clone() }, command_topic: Some(format!("{}/{}", base, cmd)), payload_on: Some("ON"),