Files
pilot/pilot-v2/src/ha/mod.rs
Gilles Soulier c5381b7112 Pilot v2: Core implementation + battery telemetry
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>
2025-12-30 06:23:00 +01:00

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(())
}