207 lines
5.2 KiB
Rust
207 lines
5.2 KiB
Rust
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<String>,
|
|
|
|
/// File with the API token
|
|
#[clap(long, env, env = "HNP_TOKEN_FILE")]
|
|
token_file: Option<PathBuf>,
|
|
|
|
/// 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<Command>,
|
|
}
|
|
|
|
#[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::<serde_json::Value>()
|
|
.await?;
|
|
|
|
debug!("{:#?}", response);
|
|
|
|
Ok(())
|
|
}
|