Handle events for the media player entity specified

This commit is contained in:
Erwin Boskma 2022-03-28 20:29:52 +02:00
parent f3bc613b31
commit ae1a579aa2
Signed by: erwin
GPG key ID: 270B20D17394F7E5

View file

@ -2,16 +2,18 @@ use async_tungstenite::{tokio::connect_async, tungstenite};
use color_eyre::{eyre::eyre, Result}; use color_eyre::{eyre::eyre, Result};
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::debug; use serde_json::Value;
use tracing::{debug, error, trace};
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,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct Message { struct Message {
#[serde(rename = "type")] #[serde(rename = "type")]
message_type: MessageType, message_type: MessageType,
@ -21,6 +23,47 @@ struct Message {
access_token: Option<String>, access_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>, message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
event_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
success: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
event: Option<Event>,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct Event {
data: EventData,
time_fired: String,
origin: String,
context: EventContext,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct EventData {
new_state: EntityState,
old_state: EntityState,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct EventContext {
id: String,
parent_id: Option<String>,
user_id: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
struct EntityState {
entity_id: String,
last_changed: String,
state: String,
attributes: Value,
last_updated: String,
context: EventContext,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@ -48,15 +91,38 @@ enum MessageType {
ValidateConfig, ValidateConfig,
} }
impl Default for Message { impl Default for MessageType {
fn default() -> Self { fn default() -> Self {
Self { MessageType::Pong
message_type: MessageType::Pong,
ha_version: Default::default(),
access_token: Default::default(),
message: Default::default(),
} }
} }
impl Message {
fn auth() -> Self {
let mut msg = Self::default();
msg.message_type = MessageType::Auth;
msg
}
fn subscribe_events() -> Self {
let mut msg = Self::default();
msg.message_type = MessageType::SubscribeEvents;
msg
}
fn get_states() -> Self {
let mut msg = Self::default();
msg.message_type = MessageType::GetStates;
msg
}
fn to_json(self) -> String {
serde_json::to_string(&self).unwrap()
}
fn to_message(self) -> tungstenite::Message {
tungstenite::Message::from(self.to_json())
}
} }
impl HomeAssistant { impl HomeAssistant {
@ -66,10 +132,17 @@ impl HomeAssistant {
entity, entity,
insecure, insecure,
host, host,
id: 1,
} }
} }
pub(crate) async fn open_connection(self) -> Result<()> { fn incrementing_id(&mut self) -> u64 {
let id = self.id;
self.id = self.id.wrapping_add(1);
id
}
pub(crate) async fn open_connection(mut self) -> Result<()> {
let api_url = format!( let api_url = format!(
"{}://{}/api/websocket", "{}://{}/api/websocket",
if self.insecure { "ws" } else { "wss" }, if self.insecure { "ws" } else { "wss" },
@ -82,8 +155,16 @@ impl HomeAssistant {
while let Some(msg) = ws_stream.next().await { while let Some(msg) = ws_stream.next().await {
let msg = msg?; let msg = msg?;
if let Ok(response) = self.handle_message(msg).await {
ws_stream.send(response).await?; match self.handle_message(msg).await {
Ok(response_messages) => {
for response in response_messages {
ws_stream.send(response).await?
}
}
Err(e) => {
error!("{e}");
}
} }
} }
@ -94,21 +175,73 @@ impl HomeAssistant {
)) ))
} }
async fn handle_message(&self, msg: tungstenite::Message) -> Result<tungstenite::Message> { async fn handle_message(
let message: Message = serde_json::from_slice(&msg.into_data()).unwrap(); &mut self,
debug!("Received: {message:?}"); msg: tungstenite::Message,
) -> Result<Vec<tungstenite::Message>> {
trace!("{}", msg.clone().into_text()?);
let message: Message = serde_json::from_slice(&msg.into_data())?;
trace!("Received: {message:?}");
let response = match message.message_type { let response = match message.message_type {
MessageType::AuthRequired => { MessageType::AuthRequired => {
debug!("Sending Authentication Data"); debug!("Sending Authentication Data");
let mut auth = Message::default(); let mut auth = Message::auth();
auth.message_type = MessageType::Auth;
auth.access_token = Some(self.token.clone()); auth.access_token = Some(self.token.clone());
async_tungstenite::tungstenite::Message::text(serde_json::to_string(&auth).unwrap()) vec![auth.to_message()]
}
MessageType::AuthOk => {
let mut subscribe = Message::subscribe_events();
subscribe.event_type = Some("state_changed".into());
subscribe.id = Some(self.incrementing_id());
let mut get_states = Message::get_states();
get_states.id = Some(self.incrementing_id());
vec![subscribe.to_message(), get_states.to_message()]
}
MessageType::Event => {
let event = message.event.unwrap();
if event.data.new_state.entity_id == self.entity {
Self::print_state(event.data.new_state.state, event.data.new_state.attributes);
}
vec![]
}
MessageType::Result => {
debug!("{}", message.to_json());
vec![]
} }
_ => return Err(eyre!("TODO: Handle {:?}", message.message_type)), _ => return Err(eyre!("TODO: Handle {:?}", message.message_type)),
}; };
Ok(response) Ok(response)
} }
fn print_state(state: String, attributes: Value) {
if state == "playing" {
let maybe_channel = attributes["media_channel"].as_str();
let artist = attributes["media_artist"].as_str().unwrap_or("No artist");
let title = attributes["media_title"].as_str().unwrap_or("No title");
let volume_raw = attributes["volume_level"].as_f64().unwrap_or(-1.);
let volume = if volume_raw >= 0. {
format!("[{:3.0}%] ", volume_raw * 100.)
} else {
String::new()
};
let now_playing = if let Some(channel) = maybe_channel {
format!("{volume}[{channel}] {artist} - {title}")
} else {
format!("{volume}{artist} - {title}")
};
println!("{}", html_escape::encode_text(&now_playing));
} else {
println!("Sonos {state}");
}
}
} }