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:
129
pilot-v2/src/commands/mod.rs
Normal file
129
pilot-v2/src/commands/mod.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user