From 9aa16e3ef8655278cb39612b377f677e3f0eb124 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Sat, 6 Jul 2024 00:34:15 +0200 Subject: [PATCH] Started actually using the google home trait macro --- automation_macro/src/lib.rs | 8 +-- google-home/src/attributes.rs | 22 -------- google-home/src/device.rs | 84 ++++----------------------- google-home/src/fulfillment.rs | 13 +++-- google-home/src/lib.rs | 1 - google-home/src/request/execute.rs | 20 ++----- google-home/src/response.rs | 13 ----- google-home/src/response/execute.rs | 15 +++-- google-home/src/response/query.rs | 15 +++-- google-home/src/response/sync.rs | 5 +- google-home/src/traits.rs | 88 ++++++++++------------------- src/devices/air_filter.rs | 23 ++++---- src/devices/audio_setup.rs | 6 +- src/devices/contact_sensor.rs | 2 +- src/devices/hue_group.rs | 2 +- src/devices/ikea_outlet.rs | 2 +- src/devices/kasa_outlet.rs | 2 +- src/devices/wake_on_lan.rs | 2 +- 18 files changed, 94 insertions(+), 229 deletions(-) delete mode 100644 google-home/src/attributes.rs diff --git a/automation_macro/src/lib.rs b/automation_macro/src/lib.rs index fa55c5e..da397ef 100644 --- a/automation_macro/src/lib.rs +++ b/automation_macro/src/lib.rs @@ -299,7 +299,7 @@ fn get_command_enum(traits: &Punctuated) -> proc_macro2::Token }); quote! { - #[derive(Debug, serde::Deserialize)] + #[derive(Debug, Clone, serde::Deserialize)] #[serde(tag = "command", content = "params", rename_all = "camelCase")] pub enum Command { #(#items,)* @@ -520,7 +520,7 @@ pub fn google_home_traits(item: TokenStream) -> TokenStream { Some(quote! { Command::#command_name {#(#parameters,)*} => { - if let Some(t) = self.cast() as Option<&dyn #ident> { + if let Some(t) = self.cast_mut() as Option<&mut dyn #ident> { t.#f_name(#(#parameters,)*) #asyncness #errors; serde_json::to_value(t.get_state().await?)? } else { @@ -547,7 +547,7 @@ pub fn google_home_traits(item: TokenStream) -> TokenStream { pub trait #fulfillment: Sync + Send { async fn sync(&self) -> Result<(Vec, serde_json::Value), Box>; async fn query(&self) -> Result>; - async fn execute(&self, command: Command) -> Result>; + async fn execute(&mut self, command: Command) -> Result>; } #(#structs)* @@ -575,7 +575,7 @@ pub fn google_home_traits(item: TokenStream) -> TokenStream { Ok(state) } - async fn execute(&self, command: Command) -> Result> { + async fn execute(&mut self, command: Command) -> Result> { let value = match command { #(#execute)* }; diff --git a/google-home/src/attributes.rs b/google-home/src/attributes.rs deleted file mode 100644 index 3463e14..0000000 --- a/google-home/src/attributes.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::Serialize; - -use crate::traits::AvailableSpeeds; - -#[derive(Debug, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Attributes { - #[serde(skip_serializing_if = "Option::is_none")] - pub command_only_on_off: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub query_only_on_off: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub scene_reversible: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reversible: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub command_only_fan_speed: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub available_fan_speeds: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub query_only_humidity_setting: Option, -} diff --git a/google-home/src/device.rs b/google-home/src/device.rs index b9334cb..f11157e 100644 --- a/google-home/src/device.rs +++ b/google-home/src/device.rs @@ -1,17 +1,13 @@ use async_trait::async_trait; -use automation_cast::Cast; use serde::Serialize; -use crate::errors::{DeviceError, ErrorCode}; -use crate::request::execute::CommandType; +use crate::errors::ErrorCode; use crate::response; -use crate::traits::{FanSpeed, HumiditySetting, OnOff, Scene, Trait}; +use crate::traits::{Command, GoogleHomeDeviceFulfillment}; use crate::types::Type; #[async_trait] -pub trait GoogleHomeDevice: - Sync + Send + Cast + Cast + Cast + Cast -{ +pub trait GoogleHomeDevice: GoogleHomeDeviceFulfillment { fn get_device_type(&self) -> Type; fn get_device_name(&self) -> Name; fn get_id(&self) -> String; @@ -41,35 +37,10 @@ pub trait GoogleHomeDevice: } device.device_info = self.get_device_info(); - let mut traits = Vec::new(); - - // OnOff - if let Some(on_off) = self.cast() as Option<&dyn OnOff> { - traits.push(Trait::OnOff); - device.attributes.command_only_on_off = on_off.is_command_only(); - device.attributes.query_only_on_off = on_off.is_query_only(); - } - - // Scene - if let Some(scene) = self.cast() as Option<&dyn Scene> { - traits.push(Trait::Scene); - device.attributes.scene_reversible = scene.is_scene_reversible(); - } - - // FanSpeed - if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> { - traits.push(Trait::FanSpeed); - device.attributes.command_only_fan_speed = fan_speed.command_only_fan_speed(); - device.attributes.available_fan_speeds = Some(fan_speed.available_speeds()); - } - - if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> { - traits.push(Trait::HumiditySetting); - device.attributes.query_only_humidity_setting = - humidity_setting.query_only_humidity_setting(); - } + let (traits, attributes) = GoogleHomeDeviceFulfillment::sync(self).await.unwrap(); device.traits = traits; + device.attributes = attributes; device } @@ -80,50 +51,15 @@ pub trait GoogleHomeDevice: device.set_offline(); } - // OnOff - if let Some(on_off) = self.cast() as Option<&dyn OnOff> { - device.state.on = on_off - .is_on() - .await - .map_err(|err| device.set_error(err)) - .ok(); - } - - // FanSpeed - if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> { - device.state.current_fan_speed_setting = Some(fan_speed.current_speed().await); - } - - if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> { - device.state.humidity_ambient_percent = - Some(humidity_setting.humidity_ambient_percent().await); - } + device.state = GoogleHomeDeviceFulfillment::query(self).await.unwrap(); device } - async fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> { - match command { - CommandType::OnOff { on } => { - if let Some(t) = self.cast_mut() as Option<&mut dyn OnOff> { - t.set_on(*on).await?; - } else { - return Err(DeviceError::ActionNotAvailable.into()); - } - } - CommandType::ActivateScene { deactivate } => { - if let Some(t) = self.cast_mut() as Option<&mut dyn Scene> { - t.set_active(!deactivate).await?; - } else { - return Err(DeviceError::ActionNotAvailable.into()); - } - } - CommandType::SetFanSpeed { fan_speed } => { - if let Some(t) = self.cast_mut() as Option<&mut dyn FanSpeed> { - t.set_speed(fan_speed).await?; - } - } - } + async fn execute(&mut self, command: Command) -> Result<(), ErrorCode> { + GoogleHomeDeviceFulfillment::execute(self, command.clone()) + .await + .unwrap(); Ok(()) } diff --git a/google-home/src/fulfillment.rs b/google-home/src/fulfillment.rs index cc89b07..82eb426 100644 --- a/google-home/src/fulfillment.rs +++ b/google-home/src/fulfillment.rs @@ -8,7 +8,7 @@ use tokio::sync::{Mutex, RwLock}; use crate::errors::{DeviceError, ErrorCode}; use crate::request::{self, Intent, Request}; -use crate::response::{self, execute, query, sync, Response, ResponsePayload, State}; +use crate::response::{self, execute, query, sync, Response, ResponsePayload}; use crate::GoogleHomeDevice; #[derive(Debug)] @@ -66,7 +66,7 @@ impl GoogleHome { let mut resp_payload = sync::Payload::new(&self.user_id); let f = devices.iter().map(|(_, device)| async move { if let Some(device) = device.read().await.as_ref().cast() { - Some(device.sync().await) + Some(GoogleHomeDevice::sync(device).await) } else { None } @@ -91,7 +91,7 @@ impl GoogleHome { let device = if let Some(device) = devices.get(id.as_str()) && let Some(device) = device.read().await.as_ref().cast() { - device.query().await + GoogleHomeDevice::query(device).await } else { let mut device = query::Device::new(); device.set_offline(); @@ -121,12 +121,12 @@ impl GoogleHome { let mut success = response::execute::Command::new(execute::Status::Success); success.states = Some(execute::States { online: true, - state: State::default(), + state: Default::default(), }); let mut offline = response::execute::Command::new(execute::Status::Offline); offline.states = Some(execute::States { online: false, - state: State::default(), + state: Default::default(), }); let mut errors: HashMap = HashMap::new(); @@ -147,7 +147,8 @@ impl GoogleHome { // NOTE: We can not use .map here because async =( let mut results = Vec::new(); for cmd in &execution { - results.push(device.execute(cmd).await); + results + .push(GoogleHomeDevice::execute(device, cmd.clone()).await); } // Convert vec of results to a result with a vec and the first diff --git a/google-home/src/lib.rs b/google-home/src/lib.rs index fac30fe..fab23bf 100644 --- a/google-home/src/lib.rs +++ b/google-home/src/lib.rs @@ -7,7 +7,6 @@ mod fulfillment; mod request; mod response; -mod attributes; pub mod errors; pub mod traits; pub mod types; diff --git a/google-home/src/request/execute.rs b/google-home/src/request/execute.rs index b3a640a..c7ae6ae 100644 --- a/google-home/src/request/execute.rs +++ b/google-home/src/request/execute.rs @@ -1,5 +1,7 @@ use serde::Deserialize; +use crate::traits; + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Payload { @@ -10,7 +12,7 @@ pub struct Payload { #[serde(rename_all = "camelCase")] pub struct Command { pub devices: Vec, - pub execution: Vec, + pub execution: Vec, } #[derive(Debug, Deserialize)] @@ -20,20 +22,6 @@ pub struct Device { // customData } -#[derive(Debug, Deserialize, Clone)] -#[serde(tag = "command", content = "params")] -pub enum CommandType { - #[serde(rename = "action.devices.commands.OnOff")] - OnOff { on: bool }, - #[serde(rename = "action.devices.commands.ActivateScene")] - ActivateScene { deactivate: bool }, - #[serde( - rename = "action.devices.commands.SetFanSpeed", - rename_all = "camelCase" - )] - SetFanSpeed { fan_speed: String }, -} - #[cfg(test)] mod tests { use super::*; @@ -74,7 +62,7 @@ mod tests { assert_eq!(payload.commands[0].devices.len(), 0); assert_eq!(payload.commands[0].execution.len(), 1); match &payload.commands[0].execution[0] { - CommandType::SetFanSpeed { fan_speed } => assert_eq!(fan_speed, "Test"), + traits::Command::SetFanSpeed { fan_speed } => assert_eq!(fan_speed, "Test"), _ => panic!("Expected SetFanSpeed"), } } diff --git a/google-home/src/response.rs b/google-home/src/response.rs index 448c094..f940056 100644 --- a/google-home/src/response.rs +++ b/google-home/src/response.rs @@ -27,16 +27,3 @@ pub enum ResponsePayload { Query(query::Payload), Execute(execute::Payload), } - -#[derive(Debug, Default, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct State { - #[serde(skip_serializing_if = "Option::is_none")] - pub on: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub current_fan_speed_setting: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub humidity_ambient_percent: Option, -} diff --git a/google-home/src/response/execute.rs b/google-home/src/response/execute.rs index 9e5cbe1..f193fba 100644 --- a/google-home/src/response/execute.rs +++ b/google-home/src/response/execute.rs @@ -1,7 +1,6 @@ use serde::Serialize; use crate::errors::ErrorCode; -use crate::response::State; #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] @@ -72,7 +71,7 @@ pub struct States { pub online: bool, #[serde(flatten)] - pub state: State, + pub state: serde_json::Value, } #[derive(Debug, Serialize, Clone)] @@ -87,19 +86,19 @@ pub enum Status { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; use crate::errors::DeviceError; - use crate::response::{Response, ResponsePayload, State}; + use crate::response::{Response, ResponsePayload}; #[test] fn serialize() { let mut execute_resp = Payload::new(); - let state = State { - on: Some(true), - current_fan_speed_setting: None, - humidity_ambient_percent: None, - }; + let state = json!({ + "on": true, + }); let mut command = Command::new(Status::Success); command.states = Some(States { online: true, diff --git a/google-home/src/response/query.rs b/google-home/src/response/query.rs index af0d789..8e92f1d 100644 --- a/google-home/src/response/query.rs +++ b/google-home/src/response/query.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use serde::Serialize; use crate::errors::ErrorCode; -use crate::response::State; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -53,7 +52,7 @@ pub struct Device { error_code: Option, #[serde(flatten)] - pub state: State, + pub state: serde_json::Value, } impl Device { @@ -62,7 +61,7 @@ impl Device { online: true, status: Status::Success, error_code: None, - state: State::default(), + state: Default::default(), } } @@ -88,6 +87,8 @@ impl Default for Device { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; use crate::response::{Response, ResponsePayload}; @@ -96,11 +97,15 @@ mod tests { let mut query_resp = Payload::new(); let mut device = Device::new(); - device.state.on = Some(true); + device.state = json!({ + "on": true, + }); query_resp.add_device("123", device); let mut device = Device::new(); - device.state.on = Some(false); + device.state = json!({ + "on": true, + }); query_resp.add_device("456", device); let resp = Response::new( diff --git a/google-home/src/response/sync.rs b/google-home/src/response/sync.rs index af9703a..c820314 100644 --- a/google-home/src/response/sync.rs +++ b/google-home/src/response/sync.rs @@ -1,6 +1,5 @@ use serde::Serialize; -use crate::attributes::Attributes; use crate::device; use crate::errors::ErrorCode; use crate::traits::Trait; @@ -47,7 +46,7 @@ pub struct Device { pub room_hint: Option, #[serde(skip_serializing_if = "Option::is_none")] pub device_info: Option, - pub attributes: Attributes, + pub attributes: serde_json::Value, } impl Device { @@ -61,7 +60,7 @@ impl Device { notification_supported_by_agent: None, room_hint: None, device_info: None, - attributes: Attributes::default(), + attributes: Default::default(), } } } diff --git a/google-home/src/traits.rs b/google-home/src/traits.rs index a92c656..40d0e29 100644 --- a/google-home/src/traits.rs +++ b/google-home/src/traits.rs @@ -1,42 +1,39 @@ -use async_trait::async_trait; +use automation_cast::Cast; +use automation_macro::google_home_traits; use serde::Serialize; use crate::errors::ErrorCode; +use crate::GoogleHomeDevice; -#[derive(Debug, Serialize)] -pub enum Trait { - #[serde(rename = "action.devices.traits.OnOff")] - OnOff, - #[serde(rename = "action.devices.traits.Scene")] - Scene, - #[serde(rename = "action.devices.traits.FanSpeed")] - FanSpeed, - #[serde(rename = "action.devices.traits.HumiditySetting")] - HumiditySetting, -} +google_home_traits! { + GoogleHomeDevice, + "action.devices.traits.OnOff" => trait OnOff { + command_only_on_off: Option, + query_only_on_off: Option, + async fn on(&self) -> Result, + "action.devices.commands.OnOff" => async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>, + }, + "action.devices.traits.Scene" => trait Scene { + scene_reversible: Option, -#[async_trait] -pub trait OnOff: Sync + Send { - fn is_command_only(&self) -> Option { - None + "action.devices.commands.ActivateScene" => async fn set_active(&mut self, activate: bool) -> Result<(), ErrorCode>, + }, + "action.devices.traits.FanSpeed" => trait FanSpeed { + reversible: Option, + command_only_fan_speed: Option, + available_fan_speeds: AvailableSpeeds, + + fn current_fan_speed_setting(&self) -> Result, + + // TODO: Figure out some syntax for optional command? + // Probably better to just force the user to always implement commands? + "action.devices.commands.SetFanSpeed" => async fn set_fan_speed(&mut self, fan_speed: String) -> Result<(), ErrorCode>, + }, + "action.devices.traits.HumiditySetting" => trait HumiditySetting { + query_only_humidity_setting: Option, + + fn humidity_ambient_percent(&self) -> Result, } - - fn is_query_only(&self) -> Option { - None - } - - // TODO: Implement correct error so we can handle them properly - async fn is_on(&self) -> Result; - async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>; -} - -#[async_trait] -pub trait Scene: Sync + Send { - fn is_scene_reversible(&self) -> Option { - None - } - - async fn set_active(&self, activate: bool) -> Result<(), ErrorCode>; } #[derive(Debug, Serialize)] @@ -56,28 +53,3 @@ pub struct AvailableSpeeds { pub speeds: Vec, pub ordered: bool, } - -#[async_trait] -pub trait FanSpeed: Sync + Send { - fn reversible(&self) -> Option { - None - } - - fn command_only_fan_speed(&self) -> Option { - None - } - - fn available_speeds(&self) -> AvailableSpeeds; - async fn current_speed(&self) -> String; - async fn set_speed(&self, speed: &str) -> Result<(), ErrorCode>; -} - -#[async_trait] -pub trait HumiditySetting: Sync + Send { - // TODO: This implementation is not complete, I have only implemented what I need right now - fn query_only_humidity_setting(&self) -> Option { - None - } - - async fn humidity_ambient_percent(&self) -> isize; -} diff --git a/src/devices/air_filter.rs b/src/devices/air_filter.rs index 0fad2d9..3aa1cf3 100644 --- a/src/devices/air_filter.rs +++ b/src/devices/air_filter.rs @@ -134,7 +134,7 @@ impl GoogleHomeDevice for AirFilter { #[async_trait] impl OnOff for AirFilter { - async fn is_on(&self) -> Result { + async fn on(&self) -> Result { Ok(self.last_known_state.state != AirFilterFanState::Off) } @@ -153,7 +153,7 @@ impl OnOff for AirFilter { #[async_trait] impl FanSpeed for AirFilter { - fn available_speeds(&self) -> AvailableSpeeds { + fn available_fan_speeds(&self) -> AvailableSpeeds { AvailableSpeeds { speeds: vec![ Speed { @@ -189,7 +189,7 @@ impl FanSpeed for AirFilter { } } - async fn current_speed(&self) -> String { + fn current_fan_speed_setting(&self) -> Result { let speed = match self.last_known_state.state { AirFilterFanState::Off => "off", AirFilterFanState::Low => "low", @@ -197,17 +197,18 @@ impl FanSpeed for AirFilter { AirFilterFanState::High => "high", }; - speed.into() + Ok(speed.into()) } - async fn set_speed(&self, speed: &str) -> Result<(), ErrorCode> { - let state = if speed == "off" { + async fn set_fan_speed(&mut self, fan_speed: String) -> Result<(), ErrorCode> { + let fan_speed = fan_speed.as_str(); + let state = if fan_speed == "off" { AirFilterFanState::Off - } else if speed == "low" { + } else if fan_speed == "low" { AirFilterFanState::Low - } else if speed == "medium" { + } else if fan_speed == "medium" { AirFilterFanState::Medium - } else if speed == "high" { + } else if fan_speed == "high" { AirFilterFanState::High } else { return Err(google_home::errors::DeviceError::TransientError.into()); @@ -225,7 +226,7 @@ impl HumiditySetting for AirFilter { Some(true) } - async fn humidity_ambient_percent(&self) -> isize { - self.last_known_state.humidity.round() as isize + fn humidity_ambient_percent(&self) -> Result { + Ok(self.last_known_state.humidity.round() as isize) } } diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index f6c6925..ff74a67 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -92,7 +92,7 @@ impl OnMqtt for AudioSetup { ) { match action { RemoteAction::On => { - if mixer.is_on().await.unwrap() { + if mixer.on().await.unwrap() { speakers.set_on(false).await.unwrap(); mixer.set_on(false).await.unwrap(); } else { @@ -101,9 +101,9 @@ impl OnMqtt for AudioSetup { } }, RemoteAction::BrightnessMoveUp => { - if !mixer.is_on().await.unwrap() { + if !mixer.on().await.unwrap() { mixer.set_on(true).await.unwrap(); - } else if speakers.is_on().await.unwrap() { + } else if speakers.on().await.unwrap() { speakers.set_on(false).await.unwrap(); } else { speakers.set_on(true).await.unwrap(); diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index a8e58c9..d2f5dbe 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -155,7 +155,7 @@ impl OnMqtt for ContactSensor { for (light, previous) in &mut trigger.devices { let mut light = light.write().await; if let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> { - *previous = light.is_on().await.unwrap(); + *previous = light.on().await.unwrap(); light.set_on(true).await.ok(); } } diff --git a/src/devices/hue_group.rs b/src/devices/hue_group.rs index fdb8517..ca96952 100644 --- a/src/devices/hue_group.rs +++ b/src/devices/hue_group.rs @@ -152,7 +152,7 @@ impl OnOff for HueGroup { Ok(()) } - async fn is_on(&self) -> Result { + async fn on(&self) -> Result { let res = reqwest::Client::new() .get(self.url_get_state()) .send() diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index 3330d9c..14b8d76 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -205,7 +205,7 @@ impl GoogleHomeDevice for IkeaOutlet { #[async_trait] impl traits::OnOff for IkeaOutlet { - async fn is_on(&self) -> Result { + async fn on(&self) -> Result { Ok(self.last_known_state) } diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index 4bb35da..cbd7cab 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -207,7 +207,7 @@ impl Response { #[async_trait] impl traits::OnOff for KasaOutlet { - async fn is_on(&self) -> Result { + async fn on(&self) -> Result { let mut stream = TcpStream::connect(self.config.addr) .await .or::(Err(DeviceError::DeviceOffline))?; diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index 51722e7..0454f70 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -103,7 +103,7 @@ impl GoogleHomeDevice for WakeOnLAN { #[async_trait] impl traits::Scene for WakeOnLAN { - async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> { + async fn set_active(&mut self, activate: bool) -> Result<(), ErrorCode> { if activate { debug!( id = Device::get_id(self),