use std::io::Read; use std::path::PathBuf; use clap::Parser; use color_eyre::{eyre::WrapErr, Result}; use serde_json::json; use std::fmt::Display; use tracing::debug; use tracing_error::ErrorLayer; use tracing_subscriber::{prelude::*, EnvFilter, Registry}; use tracing_tree::HierarchicalLayer; mod homeassistant; mod output; /// Bert #[derive(Parser, Debug)] #[clap(version, author, about)] struct Opts { /// Home Assistant host #[clap(long, env = "HNP_HOST")] host: String, /// Media player entity ID #[clap(short, long, env = "HNP_ENTITY")] entity: String, /// API token #[clap(short, long, env = "HNP_TOKEN")] token: Option, /// File with the API token #[clap(long, env, env = "HNP_TOKEN_FILE")] token_file: Option, /// Use HTTP instead of HTTPS #[clap(short, long, env = "HNP_INSECURE")] insecure: bool, /// Output format #[clap( value_enum, short, long, env = "HNP_OUTPUT_FORMAT", default_value = "waybar" )] format: output::OutputFormat, #[clap(short, long, env = "HNP_DEBUG")] debug: bool, #[clap(subcommand)] cmd: Option, } #[derive(Parser, Debug, Clone, Copy)] enum Command { /// Toggle playback PlayPause, /// Raise volume VolumeUp, /// Lower volume VolumeDown, /// Set volume to value Volume { volume: u8 }, /// Mute volume Mute, /// Unmute volume Unmute, } impl Display for Command { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Command::PlayPause => write!(f, "Play/Pause"), Command::VolumeUp => write!(f, "Volume Up"), Command::VolumeDown => write!(f, "Volume Down"), Command::Volume { volume } => write!(f, "Volume {volume}"), Command::Mute => write!(f, "Mute volume"), Command::Unmute => write!(f, "Unmute volume"), } } } #[tokio::main] async fn main() -> Result<()> { let opts = Opts::parse(); let Opts { host, entity, insecure, debug, cmd, token, token_file, format, } = opts; setup(debug).wrap_err("Setup failed")?; let token = if let Some(token) = token { Some(token) } else if let Some(token_file) = token_file { if let Ok(mut file) = std::fs::File::open(token_file) { let mut buf = String::new(); file.read_to_string(&mut buf).ok(); Some(buf) } else { None } } else { println!("No API token given. Use either --token or --token-file"); None }; if let Some(token) = token { if let Some(cmd) = cmd { debug!("Calling service '{cmd}' on entity '{entity}'"); call_service(cmd, host, entity, insecure, token) .await .wrap_err_with(|| format!("Unable to execute command {cmd}"))?; } else { debug!("Retrieving state of '{entity}' on '{host}'"); let ha = homeassistant::HomeAssistant::builder() .host(host) .entity(entity) .token(token) .insecure(insecure) .format(format) .build(); ha.open_connection().await.unwrap(); } } Ok(()) } fn setup(debug: bool) -> Result<()> { if debug { std::env::set_var("RUST_LOG", "debug"); } color_eyre::install()?; Registry::default() .with(EnvFilter::from_default_env()) .with( HierarchicalLayer::new(2) .with_targets(true) .with_bracketed_fields(true), ) .with(ErrorLayer::default()) .init(); Ok(()) } #[tracing::instrument] async fn call_service( command: Command, host: String, entity: String, insecure: bool, token: String, ) -> Result<()> { let cmd = match command { Command::PlayPause => "media_play_pause", Command::VolumeUp => "volume_up", Command::VolumeDown => "volume_down", Command::Volume { .. } => "volume_set", Command::Mute => "volume_mute", Command::Unmute => "volume_mute", }; let url = format!( "{}://{}/api/services/media_player/{}", if insecure { "http" } else { "https" }, host, cmd ); let body = match command { Command::Volume { volume } => { json!({ "entity_id": entity, "volume_level": volume.clamp(0, 100) as f32 / 100. }) .to_string() } Command::Mute => json!({ "entity_id": entity, "is_volume_muted": true }).to_string(), Command::Unmute => json!({ "entity_id": entity, "is_volume_muted": false }).to_string(), _ => json!({ "entity_id": entity }).to_string(), }; // let client = reqwest::blocking::Client::new(); let client = reqwest::Client::new(); let response = client .post(url) .body(body) .bearer_auth(token) .header("Accept", "application/json") .header("Content-Type", "application/json") .send() .await? .json::() .await?; debug!("{:#?}", response); Ok(()) }