Started actually using the google home trait macro

This commit is contained in:
Dreaded_X 2024-07-06 00:34:15 +02:00
parent d84ff8ec8e
commit 9aa16e3ef8
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
18 changed files with 94 additions and 229 deletions

View File

@ -299,7 +299,7 @@ fn get_command_enum(traits: &Punctuated<Trait, Token![,]>) -> 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<Trait>, serde_json::Value), Box<dyn ::std::error::Error>>;
async fn query(&self) -> Result<serde_json::Value, Box<dyn ::std::error::Error>>;
async fn execute(&self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
async fn execute(&mut self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>>;
}
#(#structs)*
@ -575,7 +575,7 @@ pub fn google_home_traits(item: TokenStream) -> TokenStream {
Ok(state)
}
async fn execute(&self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
async fn execute(&mut self, command: Command) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let value = match command {
#(#execute)*
};

View File

@ -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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_only_on_off: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scene_reversible: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reversible: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command_only_fan_speed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub available_fan_speeds: Option<AvailableSpeeds>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_only_humidity_setting: Option<bool>,
}

View File

@ -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<dyn OnOff> + Cast<dyn Scene> + Cast<dyn FanSpeed> + Cast<dyn HumiditySetting>
{
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(())
}

View File

@ -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<ErrorCode, response::execute::Command> = 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

View File

@ -7,7 +7,6 @@ mod fulfillment;
mod request;
mod response;
mod attributes;
pub mod errors;
pub mod traits;
pub mod types;

View File

@ -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<Device>,
pub execution: Vec<CommandType>,
pub execution: Vec<traits::Command>,
}
#[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"),
}
}

View File

@ -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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_fan_speed_setting: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub humidity_ambient_percent: Option<isize>,
}

View File

@ -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,

View File

@ -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<ErrorCode>,
#[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(

View File

@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_info: Option<device::Info>,
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(),
}
}
}

View File

@ -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<bool>,
query_only_on_off: Option<bool>,
async fn on(&self) -> Result<bool, ErrorCode>,
"action.devices.commands.OnOff" => async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>,
},
"action.devices.traits.Scene" => trait Scene {
scene_reversible: Option<bool>,
#[async_trait]
pub trait OnOff: Sync + Send {
fn is_command_only(&self) -> Option<bool> {
None
"action.devices.commands.ActivateScene" => async fn set_active(&mut self, activate: bool) -> Result<(), ErrorCode>,
},
"action.devices.traits.FanSpeed" => trait FanSpeed {
reversible: Option<bool>,
command_only_fan_speed: Option<bool>,
available_fan_speeds: AvailableSpeeds,
fn current_fan_speed_setting(&self) -> Result<String, ErrorCode>,
// 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<bool>,
fn humidity_ambient_percent(&self) -> Result<isize, ErrorCode>,
}
fn is_query_only(&self) -> Option<bool> {
None
}
// TODO: Implement correct error so we can handle them properly
async fn is_on(&self) -> Result<bool, ErrorCode>;
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>;
}
#[async_trait]
pub trait Scene: Sync + Send {
fn is_scene_reversible(&self) -> Option<bool> {
None
}
async fn set_active(&self, activate: bool) -> Result<(), ErrorCode>;
}
#[derive(Debug, Serialize)]
@ -56,28 +53,3 @@ pub struct AvailableSpeeds {
pub speeds: Vec<Speed>,
pub ordered: bool,
}
#[async_trait]
pub trait FanSpeed: Sync + Send {
fn reversible(&self) -> Option<bool> {
None
}
fn command_only_fan_speed(&self) -> Option<bool> {
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<bool> {
None
}
async fn humidity_ambient_percent(&self) -> isize;
}

View File

@ -134,7 +134,7 @@ impl GoogleHomeDevice for AirFilter {
#[async_trait]
impl OnOff for AirFilter {
async fn is_on(&self) -> Result<bool, ErrorCode> {
async fn on(&self) -> Result<bool, ErrorCode> {
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<String, ErrorCode> {
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<isize, ErrorCode> {
Ok(self.last_known_state.humidity.round() as isize)
}
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -152,7 +152,7 @@ impl OnOff for HueGroup {
Ok(())
}
async fn is_on(&self) -> Result<bool, ErrorCode> {
async fn on(&self) -> Result<bool, ErrorCode> {
let res = reqwest::Client::new()
.get(self.url_get_state())
.send()

View File

@ -205,7 +205,7 @@ impl GoogleHomeDevice for IkeaOutlet {
#[async_trait]
impl traits::OnOff for IkeaOutlet {
async fn is_on(&self) -> Result<bool, ErrorCode> {
async fn on(&self) -> Result<bool, ErrorCode> {
Ok(self.last_known_state)
}

View File

@ -207,7 +207,7 @@ impl Response {
#[async_trait]
impl traits::OnOff for KasaOutlet {
async fn is_on(&self) -> Result<bool, errors::ErrorCode> {
async fn on(&self) -> Result<bool, errors::ErrorCode> {
let mut stream = TcpStream::connect(self.config.addr)
.await
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?;

View File

@ -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),