Add --format to select between json and i3blocks
This commit is contained in:
parent
293050d070
commit
9847da6b34
7 changed files with 329 additions and 17 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -432,6 +432,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
|
|
|
@ -15,6 +15,7 @@ html-escape = "0.2.11"
|
||||||
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
reqwest = { version = "0.11.10", features = ["blocking", "json"] }
|
||||||
serde = { version = "1.0.136", features = ["derive"] }
|
serde = { version = "1.0.136", features = ["derive"] }
|
||||||
serde_json = "1.0.79"
|
serde_json = "1.0.79"
|
||||||
|
thiserror = "1.0.30"
|
||||||
tokio = { version = "1.17.0", features = ["full"] }
|
tokio = { version = "1.17.0", features = ["full"] }
|
||||||
tracing = "0.1.32"
|
tracing = "0.1.32"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
|
|
|
@ -1,16 +1,82 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use async_tungstenite::{tokio::connect_async, tungstenite};
|
use async_tungstenite::{tokio::connect_async, tungstenite};
|
||||||
use color_eyre::{eyre::eyre, Result};
|
use color_eyre::{eyre::bail, Result};
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use thiserror::Error;
|
||||||
use tracing::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
|
use crate::output::{i3blocks::I3Blocks, waybar::Waybar, OutputFormat};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum HomeAssistantError {
|
||||||
|
#[error("empty message received")]
|
||||||
|
EmptyMessage,
|
||||||
|
#[error("unhandled message {0}")]
|
||||||
|
UnhandledMessage(String),
|
||||||
|
#[error("error deserializing message: {0}")]
|
||||||
|
JsonError(#[from] serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct HomeAssistantBuilder {
|
||||||
|
host: Option<String>,
|
||||||
|
entity: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
|
insecure: bool,
|
||||||
|
format: OutputFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HomeAssistantBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn host(mut self, host: String) -> Self {
|
||||||
|
self.host = Some(host);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn entity(mut self, entity: String) -> Self {
|
||||||
|
self.entity = Some(entity);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn token(mut self, token: String) -> Self {
|
||||||
|
self.token = Some(token);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn insecure(mut self, insecure: bool) -> Self {
|
||||||
|
self.insecure = insecure;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format(mut self, format: OutputFormat) -> Self {
|
||||||
|
self.format = format;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build(self) -> HomeAssistant {
|
||||||
|
HomeAssistant::new(
|
||||||
|
self.host.unwrap(),
|
||||||
|
self.entity.unwrap(),
|
||||||
|
self.insecure,
|
||||||
|
self.token.unwrap(),
|
||||||
|
self.format,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct HomeAssistant {
|
pub(crate) struct HomeAssistant {
|
||||||
token: String,
|
token: String,
|
||||||
entity: String,
|
entity: String,
|
||||||
insecure: bool,
|
insecure: bool,
|
||||||
host: String,
|
host: String,
|
||||||
id: u64,
|
id: u64,
|
||||||
|
format: OutputFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||||
|
@ -97,6 +163,33 @@ impl Default for MessageType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for MessageType {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MessageType::AuthRequired => write!(f, "AuthRequired"),
|
||||||
|
MessageType::Auth => write!(f, "Auth"),
|
||||||
|
MessageType::AuthOk => write!(f, "AuthOk"),
|
||||||
|
MessageType::AuthInvalid => write!(f, "AuthInvalid"),
|
||||||
|
MessageType::Result => write!(f, "Result"),
|
||||||
|
MessageType::SubscribeEvents => write!(f, "SubscribeEvents"),
|
||||||
|
MessageType::Event => write!(f, "Event"),
|
||||||
|
MessageType::SubscribeTrigger => write!(f, "SubscribeTrigger"),
|
||||||
|
MessageType::UnsubscribeEvents => write!(f, "UnsubscribeEvents"),
|
||||||
|
MessageType::FireEvent => write!(f, "FireEvent"),
|
||||||
|
MessageType::CallService => write!(f, "CallService"),
|
||||||
|
MessageType::GetStates => write!(f, "GetStates"),
|
||||||
|
MessageType::GetConfig => write!(f, "GetConfig"),
|
||||||
|
MessageType::GetServices => write!(f, "GetServices"),
|
||||||
|
MessageType::GetPanels => write!(f, "GetPanels"),
|
||||||
|
MessageType::CameraThumbnail => write!(f, "CameraThumbnail"),
|
||||||
|
MessageType::MediaPlayerThumbnail => write!(f, "MediaPlayerThumbnail"),
|
||||||
|
MessageType::Ping => write!(f, "Ping"),
|
||||||
|
MessageType::Pong => write!(f, "Pong"),
|
||||||
|
MessageType::ValidateConfig => write!(f, "ValidateConfig"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
fn auth() -> Self {
|
fn auth() -> Self {
|
||||||
let mut msg = Self::default();
|
let mut msg = Self::default();
|
||||||
|
@ -126,16 +219,27 @@ impl Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HomeAssistant {
|
impl HomeAssistant {
|
||||||
pub(crate) fn new(host: String, entity: String, insecure: bool, token: String) -> Self {
|
pub(crate) fn new(
|
||||||
|
host: String,
|
||||||
|
entity: String,
|
||||||
|
insecure: bool,
|
||||||
|
token: String,
|
||||||
|
format: OutputFormat,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
token,
|
token,
|
||||||
entity,
|
entity,
|
||||||
insecure,
|
insecure,
|
||||||
host,
|
host,
|
||||||
|
format,
|
||||||
id: 1,
|
id: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn builder() -> HomeAssistantBuilder {
|
||||||
|
HomeAssistantBuilder::new()
|
||||||
|
}
|
||||||
|
|
||||||
fn incrementing_id(&mut self) -> u64 {
|
fn incrementing_id(&mut self) -> u64 {
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
self.id = self.id.wrapping_add(1);
|
self.id = self.id.wrapping_add(1);
|
||||||
|
@ -163,22 +267,34 @@ impl HomeAssistant {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
if let Some(err) = e.downcast_ref::<HomeAssistantError>() {
|
||||||
|
match err {
|
||||||
|
HomeAssistantError::EmptyMessage => {
|
||||||
|
debug!("Received empty message, ignoring...")
|
||||||
|
}
|
||||||
|
HomeAssistantError::UnhandledMessage(message_type) => debug!(
|
||||||
|
"Received '{message_type}', we're currently not handling that."
|
||||||
|
),
|
||||||
|
HomeAssistantError::JsonError(e) => error!("{e}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("{e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(eyre!(
|
Ok(())
|
||||||
"TODO: Connect to {} and subscribe to {}",
|
|
||||||
api_url,
|
|
||||||
self.entity
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_message(
|
async fn handle_message(
|
||||||
&mut self,
|
&mut self,
|
||||||
msg: tungstenite::Message,
|
msg: tungstenite::Message,
|
||||||
) -> Result<Vec<tungstenite::Message>> {
|
) -> Result<Vec<tungstenite::Message>> {
|
||||||
|
if msg.len() == 0 {
|
||||||
|
bail!(HomeAssistantError::EmptyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
trace!("{}", msg.clone().into_text()?);
|
trace!("{}", msg.clone().into_text()?);
|
||||||
let message: Message = serde_json::from_slice(&msg.into_data())?;
|
let message: Message = serde_json::from_slice(&msg.into_data())?;
|
||||||
trace!("Received: {message:?}");
|
trace!("Received: {message:?}");
|
||||||
|
@ -205,22 +321,26 @@ impl HomeAssistant {
|
||||||
let event = message.event.unwrap();
|
let event = message.event.unwrap();
|
||||||
|
|
||||||
if event.data.new_state.entity_id == self.entity {
|
if event.data.new_state.entity_id == self.entity {
|
||||||
Self::print_state(event.data.new_state.state, event.data.new_state.attributes);
|
let output = self
|
||||||
|
.format_state(event.data.new_state.state, event.data.new_state.attributes);
|
||||||
|
println!("{output}");
|
||||||
}
|
}
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
MessageType::Result => {
|
MessageType::Result => {
|
||||||
debug!("{}", message.to_json());
|
trace!("{}", message.to_json());
|
||||||
|
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
_ => return Err(eyre!("TODO: Handle {:?}", message.message_type)),
|
_ => bail!(HomeAssistantError::UnhandledMessage(
|
||||||
|
message.message_type.to_string()
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_state(state: String, attributes: Value) {
|
fn format_state(&self, state: String, attributes: Value) -> String {
|
||||||
if state == "playing" {
|
if state == "playing" {
|
||||||
let maybe_channel = attributes["media_channel"].as_str();
|
let maybe_channel = attributes["media_channel"].as_str();
|
||||||
let artist = attributes["media_artist"].as_str().unwrap_or("No artist");
|
let artist = attributes["media_artist"].as_str().unwrap_or("No artist");
|
||||||
|
@ -242,9 +362,28 @@ impl HomeAssistant {
|
||||||
format!("{volume}{artist} - {title}")
|
format!("{volume}{artist} - {title}")
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{}", html_escape::encode_text(&now_playing));
|
let text = html_escape::encode_text(&now_playing).to_string();
|
||||||
|
|
||||||
|
match self.format {
|
||||||
|
OutputFormat::Waybar => Waybar::builder()
|
||||||
|
.text(text)
|
||||||
|
.percentage((volume_raw * 100.) as u8)
|
||||||
|
.build()
|
||||||
|
.to_string(),
|
||||||
|
OutputFormat::I3Blocks => I3Blocks::builder().text(text).build().to_string(),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("Sonos {state}");
|
// println!("Sonos {state}");
|
||||||
|
match self.format {
|
||||||
|
OutputFormat::Waybar => Waybar::builder()
|
||||||
|
.text(format!("Sonos {state}"))
|
||||||
|
.build()
|
||||||
|
.to_string(),
|
||||||
|
OutputFormat::I3Blocks => I3Blocks::builder()
|
||||||
|
.text(format!("Sonos {state}"))
|
||||||
|
.build()
|
||||||
|
.to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
src/main.rs
15
src/main.rs
|
@ -37,6 +37,10 @@ struct Opts {
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
insecure: bool,
|
insecure: bool,
|
||||||
|
|
||||||
|
/// Output format
|
||||||
|
#[clap(arg_enum, short, long, default_value = "waybar")]
|
||||||
|
format: output::OutputFormat,
|
||||||
|
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
debug: bool,
|
debug: bool,
|
||||||
|
|
||||||
|
@ -79,6 +83,7 @@ async fn main() -> Result<()> {
|
||||||
cmd,
|
cmd,
|
||||||
token,
|
token,
|
||||||
token_file,
|
token_file,
|
||||||
|
format,
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
setup(debug).wrap_err("Setup failed")?;
|
setup(debug).wrap_err("Setup failed")?;
|
||||||
|
@ -106,9 +111,13 @@ async fn main() -> Result<()> {
|
||||||
.wrap_err_with(|| format!("Unable to execute command {cmd}"))?;
|
.wrap_err_with(|| format!("Unable to execute command {cmd}"))?;
|
||||||
} else {
|
} else {
|
||||||
debug!("Retrieving state of '{entity}' on '{host}'");
|
debug!("Retrieving state of '{entity}' on '{host}'");
|
||||||
// get_now_playing(host, entity, insecure, token)
|
let ha = homeassistant::HomeAssistant::builder()
|
||||||
// .wrap_err("Unable to retrieve now playing info")?;
|
.host(host)
|
||||||
let ha = homeassistant::HomeAssistant::new(host, entity, insecure, token);
|
.entity(entity)
|
||||||
|
.token(token)
|
||||||
|
.insecure(insecure)
|
||||||
|
.format(format)
|
||||||
|
.build();
|
||||||
ha.open_connection().await.unwrap();
|
ha.open_connection().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
55
src/output/i3blocks.rs
Normal file
55
src/output/i3blocks.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct I3BlocksBuilder {
|
||||||
|
text: String,
|
||||||
|
tooltip: String,
|
||||||
|
class: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl I3BlocksBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text(mut self, text: String) -> Self {
|
||||||
|
self.text = text;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tooltip(mut self, tooltip: String) -> Self {
|
||||||
|
self.tooltip = tooltip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn class(mut self, class: String) -> Self {
|
||||||
|
self.class = class;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build(self) -> I3Blocks {
|
||||||
|
I3Blocks {
|
||||||
|
text: self.text,
|
||||||
|
tooltip: self.tooltip,
|
||||||
|
class: self.class,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct I3Blocks {
|
||||||
|
text: String,
|
||||||
|
tooltip: String,
|
||||||
|
class: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl I3Blocks {
|
||||||
|
pub(crate) fn builder() -> I3BlocksBuilder {
|
||||||
|
I3BlocksBuilder::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for I3Blocks {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}\n{}\n{}", self.text, self.tooltip, self.class)
|
||||||
|
}
|
||||||
|
}
|
27
src/output/mod.rs
Normal file
27
src/output/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use clap::ArgEnum;
|
||||||
|
|
||||||
|
pub(crate) mod i3blocks;
|
||||||
|
pub(crate) mod waybar;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ArgEnum)]
|
||||||
|
pub(crate) enum OutputFormat {
|
||||||
|
Waybar,
|
||||||
|
I3Blocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for OutputFormat {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
OutputFormat::Waybar => write!(f, "waybar format"),
|
||||||
|
OutputFormat::I3Blocks => write!(f, "i3blocks format"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OutputFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Waybar
|
||||||
|
}
|
||||||
|
}
|
80
src/output/waybar.rs
Normal file
80
src/output/waybar.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// {"text": "$text", "alt": "$alt", "tooltip": "$tooltip", "class": "$class", "percentage": $percentage }
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct WaybarBuilder {
|
||||||
|
text: String,
|
||||||
|
alt: String,
|
||||||
|
tooltip: String,
|
||||||
|
class: String,
|
||||||
|
percentage: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaybarBuilder {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn text(mut self, text: String) -> Self {
|
||||||
|
self.text = text;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn alt(mut self, alt: String) -> Self {
|
||||||
|
self.alt = alt;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn tooltip(mut self, tooltip: String) -> Self {
|
||||||
|
self.tooltip = tooltip;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn class(mut self, class: String) -> Self {
|
||||||
|
self.class = class;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn percentage(mut self, percentage: u8) -> Self {
|
||||||
|
self.percentage = percentage.clamp(0, 100);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build(self) -> Waybar {
|
||||||
|
Waybar {
|
||||||
|
text: self.text,
|
||||||
|
alt: self.alt,
|
||||||
|
tooltip: self.tooltip,
|
||||||
|
class: self.class,
|
||||||
|
percentage: self.percentage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub(crate) struct Waybar {
|
||||||
|
text: String,
|
||||||
|
alt: String,
|
||||||
|
tooltip: String,
|
||||||
|
class: String,
|
||||||
|
percentage: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waybar {
|
||||||
|
pub(crate) fn builder() -> WaybarBuilder {
|
||||||
|
WaybarBuilder::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Waybar {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Ok(out) = serde_json::to_string(self) {
|
||||||
|
write!(f, "{out}")
|
||||||
|
} else {
|
||||||
|
write!(f, "Unable to serialize to JSON: {self:?}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue