Files
pilot/pilot-v2/src/ha/mod.rs
T
gilles ff3fc65eef Remove 'type' field from discovery payload per HA spec
The type field is not required in the discovery payload JSON.
The entity type is determined by the discovery topic itself:
- homeassistant/sensor/.../config indicates a sensor
- homeassistant/switch/.../config indicates a switch

This aligns with the official Home Assistant MQTT discovery documentation.

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

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

126 lines
4.7 KiB
Rust

// Ce module regroupe la publication Home Assistant discovery.
use anyhow::{Context, Result};
use rumqttc::AsyncClient;
use serde::Serialize;
use crate::config::{base_device_topic, Config};
#[derive(Clone, Serialize)]
struct DeviceInfo {
identifiers: Vec<String>,
name: String,
manufacturer: String,
model: String,
sw_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
suggested_area: Option<String>,
}
#[derive(Serialize)]
struct EntityConfig<'a> {
name: &'a str,
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
payload_on: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
payload_off: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
unit_of_measurement: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
device_class: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<&'a str>,
}
// Publie les entites HA discovery pour les capteurs et commandes standard.
pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> {
let base = base_device_topic(cfg);
let prefix = cfg.mqtt.discovery_prefix.trim_end_matches('/');
let device = DeviceInfo {
identifiers: cfg.device.identifiers.clone(),
name: cfg.device.name.clone(),
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 sensors = vec![
("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")),
("power_state", "Power State", None, None, Some("mdi:power")),
("battery_level", "Battery Level", Some("%"), Some("battery"), Some("mdi:battery")),
("battery_state", "Battery State", None, None, Some("mdi:battery-charging")),
];
for (key, name, unit, class, icon) in sensors {
let entity_name = format!("{}_{}", key, cfg.device.name);
let entity = EntityConfig {
name: &entity_name,
unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/{}", base, key),
availability_topic: format!("{}/{}/available", base, key),
payload_available: "online",
payload_not_available: "offline",
device: DeviceInfo { ..device.clone() },
command_topic: None,
payload_on: None,
payload_off: None,
unit_of_measurement: unit,
device_class: class,
icon,
};
let topic = format!("{}/sensor/{}/{}/config", prefix, cfg.device.name, entity_name);
publish_discovery(client, &topic, &entity).await?;
}
let switches = vec![
("shutdown", "Shutdown", "cmd/shutdown/set"),
("reboot", "Reboot", "cmd/reboot/set"),
("sleep", "Sleep", "cmd/sleep/set"),
("screen", "Screen", "cmd/screen/set"),
];
for (key, name, cmd) in switches {
let entity_name = format!("{}_{}", key, cfg.device.name);
let entity = EntityConfig {
name: &entity_name,
unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/{}/state", base, key),
availability_topic: format!("{}/{}/available", base, key),
payload_available: "online",
payload_not_available: "offline",
device: DeviceInfo { ..device.clone() },
command_topic: Some(format!("{}/{}", base, cmd)),
payload_on: Some("ON"),
payload_off: Some("OFF"),
unit_of_measurement: None,
device_class: Some("switch"),
icon: Some("mdi:power"),
};
let topic = format!("{}/switch/{}/{}/config", prefix, cfg.device.name, entity_name);
publish_discovery(client, &topic, &entity).await?;
}
Ok(())
}
async fn publish_discovery<T: Serialize>(client: &AsyncClient, topic: &str, payload: &T) -> Result<()> {
let data = serde_json::to_vec(payload).context("serialize discovery")?;
client
.publish(topic, rumqttc::QoS::AtLeastOnce, true, data)
.await
.context("publish discovery")?;
Ok(())
}