ha-now-playing/src/main.rs

208 lines
5.2 KiB
Rust
Raw Normal View History

2022-01-14 10:13:02 +01:00
use std::io::Read;
use std::path::PathBuf;
2021-11-25 23:40:07 +01:00
2021-11-23 14:22:58 +01:00
use clap::Parser;
2022-01-14 10:13:02 +01:00
use color_eyre::{eyre::WrapErr, Result};
2021-07-31 17:16:00 +02:00
use serde_json::json;
2022-01-14 10:13:02 +01:00
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;
2021-07-31 17:16:00 +02:00
/// Bert
2021-11-23 14:22:58 +01:00
#[derive(Parser, Debug)]
2021-07-31 17:16:00 +02:00
#[clap(version, author, about)]
struct Opts {
/// Home Assistant host
#[clap(long, env = "HNP_HOST")]
2021-07-31 17:16:00 +02:00
host: String,
/// Media player entity ID
#[clap(short, long, env = "HNP_ENTITY")]
2021-07-31 17:16:00 +02:00
entity: String,
/// API token
#[clap(short, long, env = "HNP_TOKEN")]
2021-11-25 23:40:07 +01:00
token: Option<String>,
/// File with the API token
#[clap(long, env, env = "HNP_TOKEN_FILE")]
2021-11-25 23:40:07 +01:00
token_file: Option<PathBuf>,
2021-07-31 17:16:00 +02:00
/// Use HTTP instead of HTTPS
#[clap(short, long, env = "HNP_INSECURE")]
2021-07-31 17:16:00 +02:00
insecure: bool,
/// Output format
#[clap(
2022-11-28 16:10:34 +01:00
value_enum,
short,
long,
env = "HNP_OUTPUT_FORMAT",
default_value = "waybar"
)]
format: output::OutputFormat,
#[clap(short, long, env = "HNP_DEBUG")]
2022-01-14 10:06:26 +01:00
debug: bool,
2021-07-31 17:16:00 +02:00
#[clap(subcommand)]
cmd: Option<Command>,
}
2021-11-23 14:22:58 +01:00
#[derive(Parser, Debug, Clone, Copy)]
2021-07-31 17:16:00 +02:00
enum Command {
/// Toggle playback
PlayPause,
/// Raise volume
VolumeUp,
/// Lower volume
VolumeDown,
/// Set volume to value
Volume { volume: u8 },
2022-04-12 21:56:16 +02:00
/// Mute volume
Mute,
/// Unmute volume
Unmute,
2021-07-31 17:16:00 +02:00
}
2022-01-14 10:13:02 +01:00
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}"),
2022-04-12 21:56:16 +02:00
Command::Mute => write!(f, "Mute volume"),
Command::Unmute => write!(f, "Unmute volume"),
2022-01-14 10:13:02 +01:00
}
2022-01-14 10:06:26 +01:00
}
}
2021-07-31 17:16:00 +02:00
#[tokio::main]
async fn main() -> Result<()> {
2021-07-31 17:16:00 +02:00
let opts = Opts::parse();
2021-11-25 23:40:07 +01:00
let Opts {
host,
entity,
insecure,
2022-01-14 10:06:26 +01:00
debug,
2021-11-25 23:40:07 +01:00
cmd,
token,
token_file,
format,
2021-11-25 23:40:07 +01:00
} = opts;
2022-01-14 10:13:02 +01:00
setup(debug).wrap_err("Setup failed")?;
2022-01-14 10:06:26 +01:00
2021-11-25 23:40:07 +01:00
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
}
2021-07-31 17:16:00 +02:00
} else {
2022-01-14 10:19:07 +01:00
println!("No API token given. Use either --token or --token-file");
2021-11-25 23:40:07 +01:00
None
};
2021-07-31 17:16:00 +02:00
2021-11-25 23:40:07 +01:00
if let Some(token) = token {
if let Some(cmd) = cmd {
debug!("Calling service '{cmd}' on entity '{entity}'");
2022-01-14 10:13:02 +01:00
call_service(cmd, host, entity, insecure, token)
.await
2022-01-14 10:13:02 +01:00
.wrap_err_with(|| format!("Unable to execute command {cmd}"))?;
2021-07-31 17:16:00 +02:00
} 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();
2021-07-31 17:16:00 +02:00
}
}
Ok(())
}
2022-01-14 10:06:26 +01:00
2022-01-14 10:13:02 +01:00
fn setup(debug: bool) -> Result<()> {
2022-01-14 10:06:26 +01:00
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())
2022-01-14 10:06:26 +01:00
.init();
Ok(())
}
#[tracing::instrument]
async fn call_service(
2021-11-25 23:40:07 +01:00
command: Command,
host: String,
entity: String,
insecure: bool,
token: String,
2022-01-14 10:13:02 +01:00
) -> Result<()> {
2021-07-31 17:16:00 +02:00
let cmd = match command {
Command::PlayPause => "media_play_pause",
Command::VolumeUp => "volume_up",
Command::VolumeDown => "volume_down",
Command::Volume { .. } => "volume_set",
2022-04-12 21:56:16 +02:00
Command::Mute => "volume_mute",
Command::Unmute => "volume_mute",
2021-07-31 17:16:00 +02:00
};
let url = format!(
"{}://{}/api/services/media_player/{}",
2021-11-25 23:40:07 +01:00
if insecure { "http" } else { "https" },
host,
2021-07-31 17:16:00 +02:00
cmd
);
let body = match command {
Command::Volume { volume } => {
json!({ "entity_id": entity, "volume_level": volume.clamp(0, 100) as f32 / 100. })
.to_string()
}
2022-04-12 21:56:16 +02:00
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(),
};
2021-07-31 17:16:00 +02:00
// let client = reqwest::blocking::Client::new();
let client = reqwest::Client::new();
2021-07-31 17:16:00 +02:00
let response = client
.post(url)
.body(body)
2021-11-25 23:40:07 +01:00
.bearer_auth(token)
2021-07-31 17:16:00 +02:00
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.send()
.await?
.json::<serde_json::Value>()
.await?;
2021-07-31 17:16:00 +02:00
2022-01-14 10:19:07 +01:00
debug!("{:#?}", response);
2021-07-31 17:16:00 +02:00
Ok(())
}