Major updates: - Complete Rust rewrite (pilot-v2/) with working MQTT client - Fixed MQTT event loop deadlock (background task pattern) - Battery telemetry for Linux (auto-detected via /sys/class/power_supply) - Home Assistant auto-discovery for all sensors and switches - Comprehensive documentation (AVANCEMENT.md, CLAUDE.md, roadmap) - Docker test environment with Mosquitto broker - Helper scripts for development and testing Features working: ✅ MQTT connectivity with LWT ✅ YAML configuration with validation ✅ Telemetry: CPU, memory, IP, battery (Linux) ✅ Commands: shutdown, reboot, sleep, screen (dry-run tested) ✅ HA discovery and integration ✅ Allowlist and cooldown protection Ready for testing on real hardware. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
116 lines
4.1 KiB
Rust
116 lines
4.1 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,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct EntityConfig<'a> {
|
|
name: &'a str,
|
|
unique_id: String,
|
|
state_topic: String,
|
|
availability_topic: String,
|
|
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: "Pilot".to_string(),
|
|
model: "v2".to_string(),
|
|
sw_version: "2.0.0".to_string(),
|
|
};
|
|
|
|
let availability = format!("{}/availability", base);
|
|
|
|
let sensors = vec![
|
|
("cpu_usage", "CPU Usage", Some("%"), Some("power"), 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 = EntityConfig {
|
|
name,
|
|
unique_id: format!("{}_{}", cfg.device.name, key),
|
|
state_topic: format!("{}/state/{}", base, key),
|
|
availability_topic: availability.clone(),
|
|
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/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key);
|
|
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 = EntityConfig {
|
|
name,
|
|
unique_id: format!("{}_{}", cfg.device.name, key),
|
|
state_topic: format!("{}/state/{}", base, key),
|
|
availability_topic: availability.clone(),
|
|
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/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key);
|
|
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(())
|
|
}
|