Started actually using the google home trait macro

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

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;
}