Fix Home Assistant MQTT discovery compliance

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 07:39:24 +01:00
parent 1640754df8
commit df871ddf6e
4 changed files with 40 additions and 4 deletions

View File

@@ -6,6 +6,10 @@
device: device:
name: $hostname # Use $hostname for automatic hostname, or a custom name like "my-pc" name: $hostname # Use $hostname for automatic hostname, or a custom name like "my-pc"
identifiers: ["$hostname"] 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: mqtt:
host: "127.0.0.1" host: "127.0.0.1"

View File

@@ -2,6 +2,10 @@
device: device:
name: $hostname name: $hostname
identifiers: ["$hostname"] identifiers: ["$hostname"]
manufacturer: "Asus"
model: "Laptop"
sw_version: "2.0.0"
suggested_area: "Bureau"
mqtt: mqtt:
host: "10.0.0.3" host: "10.0.0.3"

View File

@@ -20,6 +20,25 @@ pub struct Config {
pub struct Device { pub struct Device {
pub name: String, pub name: String,
pub identifiers: Vec<String>, pub identifiers: Vec<String>,
#[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<String>,
}
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)] #[derive(Debug, Clone, Deserialize)]

View File

@@ -12,6 +12,8 @@ struct DeviceInfo {
manufacturer: String, manufacturer: String,
model: String, model: String,
sw_version: String, sw_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
suggested_area: Option<String>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -20,6 +22,8 @@ struct EntityConfig<'a> {
unique_id: String, unique_id: String,
state_topic: String, state_topic: String,
availability_topic: String, availability_topic: String,
payload_available: &'a str,
payload_not_available: &'a str,
device: DeviceInfo, device: DeviceInfo,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
command_topic: Option<String>, command_topic: Option<String>,
@@ -42,15 +46,16 @@ pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> {
let device = DeviceInfo { let device = DeviceInfo {
identifiers: cfg.device.identifiers.clone(), identifiers: cfg.device.identifiers.clone(),
name: cfg.device.name.clone(), name: cfg.device.name.clone(),
manufacturer: "Pilot".to_string(), manufacturer: cfg.device.manufacturer.clone(),
model: "v2".to_string(), model: cfg.device.model.clone(),
sw_version: "2.0.0".to_string(), sw_version: cfg.device.sw_version.clone(),
suggested_area: cfg.device.suggested_area.clone(),
}; };
let availability = format!("{}/availability", base); let availability = format!("{}/availability", base);
let sensors = vec![ 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_used_mb", "Memory Used", Some("MB"), None, Some("mdi:memory")),
("memory_total_mb", "Memory Total", 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")), ("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), unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/state/{}", base, key), state_topic: format!("{}/state/{}", base, key),
availability_topic: availability.clone(), availability_topic: availability.clone(),
payload_available: "online",
payload_not_available: "offline",
device: DeviceInfo { ..device.clone() }, device: DeviceInfo { ..device.clone() },
command_topic: None, command_topic: None,
payload_on: 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), unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/state/{}", base, key), state_topic: format!("{}/state/{}", base, key),
availability_topic: availability.clone(), availability_topic: availability.clone(),
payload_available: "online",
payload_not_available: "offline",
device: DeviceInfo { ..device.clone() }, device: DeviceInfo { ..device.clone() },
command_topic: Some(format!("{}/{}", base, cmd)), command_topic: Some(format!("{}/{}", base, cmd)),
payload_on: Some("ON"), payload_on: Some("ON"),