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>
This commit is contained in:
Gilles Soulier
2025-12-30 06:23:00 +01:00
parent b7e67c6501
commit c5381b7112
4453 changed files with 78647 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
// Ce module declare les interfaces des commandes systeme et le parsing basique.
use anyhow::{bail, Result};
use std::time::{Duration, Instant};
use tracing::info;
// Actions d'alimentation supportees (shutdown/reboot/sleep).
pub trait PowerControl {
fn shutdown(&self) -> Result<()>;
fn reboot(&self) -> Result<()>;
fn sleep(&self) -> Result<()>;
}
// Actions d'ecran supportees (on/off).
pub trait ScreenControl {
fn screen_on(&self) -> Result<()>;
fn screen_off(&self) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandAction {
Shutdown,
Reboot,
Sleep,
Screen,
}
#[derive(Debug, Clone, Copy)]
pub enum CommandValue {
On,
Off,
}
// Decode une action depuis le topic cmd/<action>/set.
pub fn parse_action(topic: &str) -> Result<CommandAction> {
let parts: Vec<&str> = topic.split('/').collect();
if parts.len() < 2 {
bail!("topic too short");
}
let action = parts[parts.len() - 2];
match action {
"shutdown" => Ok(CommandAction::Shutdown),
"reboot" => Ok(CommandAction::Reboot),
"sleep" => Ok(CommandAction::Sleep),
"screen" => Ok(CommandAction::Screen),
_ => bail!("unknown action"),
}
}
// Decode une valeur ON/OFF (insensible a la casse).
pub fn parse_value(payload: &[u8]) -> Result<CommandValue> {
let raw = String::from_utf8_lossy(payload).trim().to_uppercase();
match raw.as_str() {
"ON" => Ok(CommandValue::On),
"OFF" => Ok(CommandValue::Off),
_ => bail!("invalid payload"),
}
}
// Verifie si l'action est autorisee par l'allowlist (vide = tout autoriser).
pub fn allowlist_allows(allowlist: &[String], action: CommandAction) -> bool {
if allowlist.is_empty() {
return true;
}
let name = action_name(action);
allowlist.iter().any(|item| item == name)
}
// Verifie le cooldown et renvoie true si l'action est autorisee.
pub fn allow_command(
last_exec: &mut std::collections::HashMap<CommandAction, Instant>,
cooldown_s: u64,
action: CommandAction,
) -> bool {
let now = Instant::now();
if let Some(prev) = last_exec.get(&action) {
if now.duration_since(*prev) < Duration::from_secs(cooldown_s) {
return false;
}
}
last_exec.insert(action, now);
true
}
// Execute une commande en mode dry-run (journalise seulement).
pub fn execute_dry_run(action: CommandAction, value: CommandValue) -> Result<()> {
info!(?action, ?value, "dry-run command");
Ok(())
}
// Convertit une action en nom utilise par la config.
pub fn action_name(action: CommandAction) -> &'static str {
match action {
CommandAction::Shutdown => "shutdown",
CommandAction::Reboot => "reboot",
CommandAction::Sleep => "sleep",
CommandAction::Screen => "screen",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_action_ok() {
let topic = "pilot/device/cmd/shutdown/set";
assert_eq!(parse_action(topic).unwrap(), CommandAction::Shutdown);
}
#[test]
fn parse_value_ok() {
assert!(matches!(parse_value(b"ON").unwrap(), CommandValue::On));
assert!(matches!(parse_value(b"off").unwrap(), CommandValue::Off));
}
#[test]
fn allowlist_checks() {
let list = vec!["shutdown".to_string(), "screen".to_string()];
assert!(allowlist_allows(&list, CommandAction::Shutdown));
assert!(!allowlist_allows(&list, CommandAction::Reboot));
}
#[test]
fn cooldown_blocks_second_call() {
let mut last_exec = std::collections::HashMap::new();
assert!(allow_command(&mut last_exec, 60, CommandAction::Shutdown));
assert!(!allow_command(&mut last_exec, 60, CommandAction::Shutdown));
}
}