diff --git a/Cargo.lock b/Cargo.lock index 433f582..0723cf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ version = "0.1.0" dependencies = [ "dotenv", "google-home", + "impl_cast", "paste", "rumqttc", "serde", @@ -321,9 +322,11 @@ name = "google-home" version = "0.1.0" dependencies = [ "anyhow", + "impl_cast", "serde", "serde_json", "serde_with", + "thiserror", "uuid", ] @@ -369,6 +372,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "impl_cast" +version = "0.1.0" +dependencies = [ + "paste", +] + [[package]] name = "indexmap" version = "1.9.2" diff --git a/Cargo.toml b/Cargo.toml index 13c17b3..d6b320f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ rumqttc = "0.18" serde = { version ="1.0.149", features = ["derive"] } serde_json = "1.0.89" dotenv = "0.15.0" +impl_cast = {path = "./impl_cast"} google-home = {path = "./google-home"} paste = "1.0.10" diff --git a/google-home/Cargo.lock b/google-home/Cargo.lock index 387e207..fb18af8 100644 --- a/google-home/Cargo.lock +++ b/google-home/Cargo.lock @@ -177,9 +177,11 @@ name = "google-home" version = "0.1.0" dependencies = [ "anyhow", + "impl_cast", "serde", "serde_json", "serde_with", + "thiserror", "uuid", ] @@ -225,6 +227,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "impl_cast" +version = "0.1.0" +dependencies = [ + "paste", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -300,6 +309,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +[[package]] +name = "paste" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1c2c742266c2f1041c914ba65355a83ae8747b05f208319784083583494b4b" + [[package]] name = "proc-macro2" version = "1.0.47" @@ -415,6 +430,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.17" diff --git a/google-home/Cargo.toml b/google-home/Cargo.toml index 873c359..eeec480 100644 --- a/google-home/Cargo.toml +++ b/google-home/Cargo.toml @@ -6,10 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0" +anyhow = "1.0.66" +impl_cast = { path = "../impl_cast" } serde = { version ="1.0.149", features = ["derive"] } serde_json = "1.0.89" serde_with = "2.1.0" +thiserror = "1.0.37" [dependencies.uuid] version = "1.2.2" diff --git a/google-home/src/device.rs b/google-home/src/device.rs index ab5f98d..87985b9 100644 --- a/google-home/src/device.rs +++ b/google-home/src/device.rs @@ -1,19 +1,19 @@ use serde::Serialize; use serde_with::skip_serializing_none; -use crate::{response, types::Type, traits::{AsOnOff, Trait, AsScene}}; +use crate::{response, types::Type, traits::{AsOnOff, Trait, AsScene}, errors::{DeviceError, ErrorCode}, request::execute::CommandType}; -pub trait GoogleHomeDevice<'a>: AsOnOff + AsScene { +pub trait GoogleHomeDevice: AsOnOff + AsScene { fn get_device_type(&self) -> Type; fn get_device_name(&self) -> Name; - fn get_id(&self) -> &'a str; + fn get_id(&self) -> String; fn is_online(&self) -> bool; // Default values that can optionally be overriden fn will_report_state(&self) -> bool { false } - fn get_room_hint(&self) -> Option<&'a str> { + fn get_room_hint(&self) -> Option { None } fn get_device_info(&self) -> Option { @@ -22,7 +22,7 @@ pub trait GoogleHomeDevice<'a>: AsOnOff + AsScene { } // This trait exists just to hide the sync, query and execute function from the user -pub trait Fullfillment<'a>: GoogleHomeDevice<'a> { +pub trait Fullfillment: GoogleHomeDevice { fn sync(&self) -> response::sync::Device { let name = self.get_device_name(); let mut device = response::sync::Device::new(&self.get_id(), &name.name, self.get_device_type()); @@ -37,20 +37,16 @@ pub trait Fullfillment<'a>: GoogleHomeDevice<'a> { let mut traits = Vec::new(); // OnOff - { - if let Some(d) = AsOnOff::cast(self) { - traits.push(Trait::OnOff); - device.attributes.command_only_on_off = d.is_command_only(); - device.attributes.query_only_on_off = d.is_query_only(); - } + if let Some(d) = AsOnOff::cast(self) { + traits.push(Trait::OnOff); + device.attributes.command_only_on_off = d.is_command_only(); + device.attributes.query_only_on_off = d.is_query_only(); } // Scene - { - if let Some(d) = AsScene::cast(self) { - traits.push(Trait::Scene); - device.attributes.scene_reversible = d.is_scene_reversible(); - } + if let Some(d) = AsScene::cast(self) { + traits.push(Trait::Scene); + device.attributes.scene_reversible = d.is_scene_reversible(); } device.traits = traits; @@ -59,29 +55,45 @@ pub trait Fullfillment<'a>: GoogleHomeDevice<'a> { } fn query(&self) -> response::query::Device { - let status; - let online = self.is_online(); - if online { - status = response::query::Status::Success; - } else { - status = response::query::Status::Offline; + let mut device = response::query::Device::new(); + if !self.is_online() { + device.set_offline(); } - let mut device = response::query::Device::new(online, status); - // OnOff - { - if let Some(d) = AsOnOff::cast(self) { - // @TODO Handle errors - device.state.on = Some(d.is_on().unwrap()); + if let Some(d) = AsOnOff::cast(self) { + match d.is_on() { + Ok(state) => device.state.on = Some(state), + Err(err) => device.set_error(err), } } return device; } + + fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> { + match command { + CommandType::OnOff { on } => { + if let Some(d) = AsOnOff::cast_mut(self) { + d.set_on(*on)?; + } else { + return Err(DeviceError::ActionNotAvailable.into()); + } + }, + CommandType::ActivateScene { deactivate } => { + if let Some(d) = AsScene::cast_mut(self) { + d.set_active(!deactivate)?; + } else { + return Err(DeviceError::ActionNotAvailable.into()); + } + }, + } + + return Ok(()); + } } -impl<'a, T: GoogleHomeDevice<'a>> Fullfillment<'a> for T {} +impl Fullfillment for T {} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/google-home/src/errors.rs b/google-home/src/errors.rs index fa1abff..a710899 100644 --- a/google-home/src/errors.rs +++ b/google-home/src/errors.rs @@ -1,7 +1,37 @@ +use thiserror::Error; use serde::Serialize; -#[derive(Debug, Serialize)] +#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, Serialize, Error)] #[serde(rename_all = "camelCase")] -pub enum Errors { - DeviceNotFound +pub enum DeviceError { + #[error("deviceNotFound")] + DeviceNotFound, + #[error("actionNotAvailable")] + ActionNotAvailable, +} + +#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, Serialize, Error)] +#[serde(rename_all = "camelCase")] +pub enum DeviceException { +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, Serialize, Error)] +#[serde(untagged)] +pub enum ErrorCode { + #[error("{0}")] + DeviceError(DeviceError), + #[error("{0}")] + DeviceException(DeviceException), +} + +impl From for ErrorCode { + fn from(value: DeviceError) -> Self { + Self::DeviceError(value) + } +} + +impl From for ErrorCode { + fn from(value: DeviceException) -> Self { + Self::DeviceException(value) + } } diff --git a/google-home/src/fullfillment.rs b/google-home/src/fullfillment.rs index 6f68a94..6b674f0 100644 --- a/google-home/src/fullfillment.rs +++ b/google-home/src/fullfillment.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use crate::{request::{Request, Intent, self}, device::Fullfillment, response::{sync, ResponsePayload, query, execute, Response}, errors::Errors}; +use crate::{request::{Request, Intent, self}, device::Fullfillment, response::{sync, ResponsePayload, query, execute, Response, self, State}, errors::{DeviceError, ErrorCode}}; pub struct GoogleHome { user_id: String, @@ -12,7 +12,7 @@ impl GoogleHome { Self { user_id: user_id.into() } } - pub fn handle_request(&self, request: Request, devices: &HashMap) -> Result { + pub fn handle_request(&self, request: Request, mut devices: &mut HashMap) -> Result { // @TODO What do we do if we actually get more then one thing in the input array, right now // we only respond to the first thing let payload = request @@ -21,7 +21,7 @@ impl GoogleHome { .map(|input| match input { Intent::Sync => ResponsePayload::Sync(self.sync(&devices)), Intent::Query(payload) => ResponsePayload::Query(self.query(payload, &devices)), - Intent::Execute(payload) => ResponsePayload::Execute(self.execute(payload, &devices)), + Intent::Execute(payload) => ResponsePayload::Execute(self.execute(payload, &mut devices)), }).next(); match payload { @@ -32,49 +32,113 @@ impl GoogleHome { fn sync(&self, devices: &HashMap) -> sync::Payload { let mut resp_payload = sync::Payload::new(&self.user_id); - resp_payload.devices = devices.iter().map(|(_, device)| device.sync()).collect::>(); + resp_payload.devices = devices + .iter() + .map(|(_, device)| device.sync()) + .collect::>(); return resp_payload; } fn query(&self, payload: request::query::Payload, devices: &HashMap) -> query::Payload { let mut resp_payload = query::Payload::new(); - for request::query::Device{id} in payload.devices { - let mut d: query::Device; - if let Some(device) = devices.get(&id) { - d = device.query(); - } else { - d = query::Device::new(false, query::Status::Error); - d.error_code = Some(Errors::DeviceNotFound); - } - resp_payload.add_device(&id, d) - } + resp_payload.devices = payload.devices + .into_iter() + .map(|device| device.id) + .map(|id| { + let mut d: query::Device; + if let Some(device) = devices.get(id.as_str()) { + d = device.query(); + } else { + d = query::Device::new(); + d.set_offline(); + d.set_error(DeviceError::DeviceNotFound.into()); + } + + return (id, d); + }).collect(); return resp_payload; } - fn execute(&self, payload: request::execute::Payload, devices: &HashMap) -> execute::Payload { - return execute::Payload::new(); + fn execute(&self, payload: request::execute::Payload, devices: &mut HashMap) -> execute::Payload { + let mut resp_payload = response::execute::Payload::new(); + + payload.commands + .into_iter() + .for_each(|command| { + let mut success = response::execute::Command::new(execute::Status::Success); + success.states = Some(execute::States { online: true, state: State::default() }); + let mut offline = response::execute::Command::new(execute::Status::Offline); + offline.states = Some(execute::States { online: false, state: State::default() }); + let mut errors: HashMap = HashMap::new(); + + command.devices + .into_iter() + .map(|device| device.id) + .map(|id| { + if let Some(device) = devices.get_mut(id.as_str()) { + if !device.is_online() { + return (id, Ok(false)); + } + let results = command.execution.iter().map(|cmd| { + // @TODO We should also return the state after update in the state + // struct, however that will make things WAY more complicated + device.execute(cmd) + }).collect::, ErrorCode>>(); + + // @TODO We only get one error not all errors + if let Err(err) = results { + return (id, Err(err)); + } else { + return (id, Ok(true)); + } + } else { + return (id, Err(DeviceError::DeviceNotFound.into())); + } + }).for_each(|(id, state)| { + match state { + Ok(true) => success.add_id(&id), + Ok(false) => offline.add_id(&id), + Err(err) => errors.entry(err).or_insert_with(|| { + match &err { + ErrorCode::DeviceError(_) => response::execute::Command::new(execute::Status::Error), + ErrorCode::DeviceException(_) => response::execute::Command::new(execute::Status::Exceptions), + } + }).add_id(&id), + }; + }); + + resp_payload.add_command(success); + resp_payload.add_command(offline); + for (error, mut cmd) in errors { + cmd.error_code = Some(error); + resp_payload.add_command(cmd); + } + }); + + return resp_payload; } } #[cfg(test)] mod tests { use super::*; - use crate::{request::Request, device::{GoogleHomeDevice, self}, types, traits}; + use crate::{request::Request, device::{GoogleHomeDevice, self}, types, traits, errors::ErrorCode}; struct TestOutlet { + name: String, on: bool, } impl TestOutlet { - fn new() -> Self { - Self { on: false } + fn new(name: &str) -> Self { + Self { name: name.into(), on: false } } } - impl<'a> GoogleHomeDevice<'a> for TestOutlet { + impl GoogleHomeDevice for TestOutlet { fn get_device_type(&self) -> types::Type { types::Type::Outlet } @@ -87,16 +151,16 @@ mod tests { return name; } - fn get_id(&self) -> &'a str { - return "bedroom/nightstand"; + fn get_id(&self) -> String { + return self.name.clone(); } fn is_online(&self) -> bool { true } - fn get_room_hint(&self) -> Option<&'a str> { - Some("Bedroom") + fn get_room_hint(&self) -> Option { + Some("Bedroom".into()) } fn get_device_info(&self) -> Option { @@ -110,19 +174,16 @@ mod tests { } impl traits::OnOff for TestOutlet { - fn is_on(&self) -> Result { + fn is_on(&self) -> Result { Ok(self.on) } - fn set_on(&mut self, on: bool) -> Result<(), anyhow::Error> { + fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { self.on = on; Ok(()) } } - impl traits::AsScene for TestOutlet {} - - struct TestScene {} impl TestScene { @@ -131,7 +192,7 @@ mod tests { } } - impl<'a> GoogleHomeDevice<'a> for TestScene { + impl GoogleHomeDevice for TestScene { fn get_device_type(&self) -> types::Type { types::Type::Scene } @@ -140,30 +201,26 @@ mod tests { device::Name::new("Party") } - fn get_id(&self) -> &'a str { - return "living/party_mode"; + fn get_id(&self) -> String { + return "living/party_mode".into(); } fn is_online(&self) -> bool { true } - fn get_room_hint(&self) -> Option<&'a str> { - Some("Living room") + fn get_room_hint(&self) -> Option { + Some("Living room".into()) } } impl traits::Scene for TestScene { - fn set_active(&self, _activate: bool) -> Result<(), anyhow::Error> { + fn set_active(&self, _activate: bool) -> Result<(), ErrorCode> { println!("Activating the party scene"); Ok(()) } } - impl traits::AsOnOff for TestScene {} - - - #[test] fn handle_sync() { let json = r#"{ @@ -180,13 +237,15 @@ mod tests { user_id: "Dreaded_X".into(), }; - let mut device = TestOutlet::new(); + let mut nightstand = TestOutlet::new("bedroom/nightstand"); + let mut lamp = TestOutlet::new("living/lamp"); let mut scene = TestScene::new(); let mut devices: HashMap = HashMap::new(); - devices.insert(device.get_id().into(), &mut device); - devices.insert(scene.get_id().into(), &mut scene); + devices.insert(nightstand.get_id(), &mut nightstand); + devices.insert(lamp.get_id(), &mut lamp); + devices.insert(scene.get_id(), &mut scene); - let resp = gh.handle_request(req, &devices).unwrap(); + let resp = gh.handle_request(req, &mut devices).unwrap(); let json = serde_json::to_string(&resp).unwrap(); println!("{}", json) @@ -218,13 +277,67 @@ mod tests { user_id: "Dreaded_X".into(), }; - let mut device = TestOutlet::new(); + let mut nightstand = TestOutlet::new("bedroom/nightstand"); + let mut lamp = TestOutlet::new("living/lamp"); let mut scene = TestScene::new(); let mut devices: HashMap = HashMap::new(); - devices.insert(device.get_id().into(), &mut device); - devices.insert(scene.get_id().into(), &mut scene); + devices.insert(nightstand.get_id(), &mut nightstand); + devices.insert(lamp.get_id(), &mut lamp); + devices.insert(scene.get_id(), &mut scene); - let resp = gh.handle_request(req, &devices).unwrap(); + let resp = gh.handle_request(req, &mut devices).unwrap(); + + let json = serde_json::to_string(&resp).unwrap(); + println!("{}", json) + } + + #[test] + fn handle_execute() { + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + { + "id": "bedroom/nightstand" + }, + { + "id": "living/lamp" + } + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": { + "on": true + } + } + ] + } + ] + } + } + ] +}"#; + let req: Request = serde_json::from_str(json).unwrap(); + + let gh = GoogleHome { + user_id: "Dreaded_X".into(), + }; + + let mut nightstand = TestOutlet::new("bedroom/nightstand"); + let mut lamp = TestOutlet::new("living/lamp"); + let mut scene = TestScene::new(); + let mut devices: HashMap = HashMap::new(); + devices.insert(nightstand.get_id(), &mut nightstand); + devices.insert(lamp.get_id(), &mut lamp); + devices.insert(scene.get_id(), &mut scene); + + let resp = gh.handle_request(req, &mut devices).unwrap(); let json = serde_json::to_string(&resp).unwrap(); println!("{}", json) diff --git a/google-home/src/lib.rs b/google-home/src/lib.rs index ee597a0..6f22ec3 100644 --- a/google-home/src/lib.rs +++ b/google-home/src/lib.rs @@ -1,10 +1,15 @@ -pub mod fullfillment; +#![feature(specialization)] +mod fullfillment; pub mod device; -pub mod request; -pub mod response; +mod request; +mod response; pub mod types; pub mod traits; -pub mod attributes; pub mod errors; +mod attributes; + +pub use fullfillment::GoogleHome; +pub use device::Fullfillment; +pub use device::GoogleHomeDevice; diff --git a/google-home/src/request/execute.rs b/google-home/src/request/execute.rs index c37b697..5c5f77c 100644 --- a/google-home/src/request/execute.rs +++ b/google-home/src/request/execute.rs @@ -3,20 +3,20 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Payload { - commands: Vec, + pub commands: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Command { - devices: Vec, - execution: Vec + pub devices: Vec, + pub execution: Vec } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Device { - id: String, + pub id: String, // customData } diff --git a/google-home/src/response/execute.rs b/google-home/src/response/execute.rs index 4ef8157..b497fa5 100644 --- a/google-home/src/response/execute.rs +++ b/google-home/src/response/execute.rs @@ -1,13 +1,13 @@ use serde::Serialize; use serde_with::skip_serializing_none; -use crate::{response::State, errors::Errors}; +use crate::{response::State, errors::ErrorCode}; #[skip_serializing_none] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Payload { - pub error_code: Option, + pub error_code: Option, pub debug_string: Option, commands: Vec, } @@ -18,7 +18,9 @@ impl Payload { } pub fn add_command(&mut self, command: Command) { - self.commands.push(command); + if !command.is_empty() { + self.commands.push(command); + } } } @@ -26,7 +28,7 @@ impl Payload { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Command { - pub error_code: Option, + pub error_code: Option, ids: Vec, status: Status, @@ -41,6 +43,10 @@ impl Command { pub fn add_id(&mut self, id: &str) { self.ids.push(id.into()); } + + pub fn is_empty(&self) -> bool { + self.ids.is_empty() + } } #[derive(Debug, Serialize)] @@ -67,7 +73,7 @@ mod tests { use std::str::FromStr; use uuid::Uuid; use super::*; - use crate::response::{Response, ResponsePayload, State}; + use crate::{response::{Response, ResponsePayload, State}, errors::DeviceError}; #[test] fn serialize() { @@ -84,7 +90,7 @@ mod tests { execute_resp.add_command(command); let mut command = Command::new(Status::Error); - command.error_code = Some(Errors::DeviceNotFound); + command.error_code = Some(DeviceError::DeviceNotFound.into()); command.ids.push("456".into()); execute_resp.add_command(command); diff --git a/google-home/src/response/query.rs b/google-home/src/response/query.rs index f587083..ab56a8e 100644 --- a/google-home/src/response/query.rs +++ b/google-home/src/response/query.rs @@ -3,15 +3,15 @@ use std::collections::HashMap; use serde::Serialize; use serde_with::skip_serializing_none; -use crate::{response::State, errors::Errors}; +use crate::{response::State, errors::ErrorCode}; #[skip_serializing_none] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Payload { - pub error_code: Option, + pub error_code: Option, pub debug_string: Option, - devices: HashMap, + pub devices: HashMap, } impl Payload { @@ -39,15 +39,28 @@ pub enum Status { pub struct Device { online: bool, status: Status, - pub error_code: Option, + error_code: Option, #[serde(flatten)] pub state: State, } impl Device { - pub fn new(online: bool, status: Status) -> Self { - Self { online, status, error_code: None, state: State::default() } + pub fn new() -> Self { + Self { online: true, status: Status::Success, error_code: None, state: State::default() } + } + + pub fn set_offline(&mut self) { + self.online = false; + self.status = Status::Offline; + } + + pub fn set_error(&mut self, err: ErrorCode) { + self.status = match err { + ErrorCode::DeviceError(_) => Status::Error, + ErrorCode::DeviceException(_) => Status::Exceptions, + }; + self.error_code = Some(err); } } @@ -56,19 +69,17 @@ mod tests { use std::str::FromStr; use uuid::Uuid; use super::*; - use crate::response::{Response, ResponsePayload, State}; + use crate::response::{Response, ResponsePayload}; #[test] fn serialize() { let mut query_resp = Payload::new(); - let state = State::default(); - let mut device = Device::new(true, Status::Success); + let mut device = Device::new(); device.state.on = Some(true); query_resp.add_device("123", device); - let state = State::default(); - let mut device = Device::new(true, Status::Success); + let mut device = Device::new(); device.state.on = Some(false); query_resp.add_device("456", device); diff --git a/google-home/src/response/sync.rs b/google-home/src/response/sync.rs index 2fbd466..d3fee82 100644 --- a/google-home/src/response/sync.rs +++ b/google-home/src/response/sync.rs @@ -3,7 +3,7 @@ use serde_with::skip_serializing_none; use crate::attributes::Attributes; use crate::device; -use crate::errors::Errors; +use crate::errors::ErrorCode; use crate::types::Type; use crate::traits::Trait; @@ -12,7 +12,7 @@ use crate::traits::Trait; #[serde(rename_all = "camelCase")] pub struct Payload { agent_user_id: String, - pub error_code: Option, + pub error_code: Option, pub debug_string: Option, pub devices: Vec, } diff --git a/google-home/src/traits.rs b/google-home/src/traits.rs index b9186cf..df5c538 100644 --- a/google-home/src/traits.rs +++ b/google-home/src/traits.rs @@ -1,6 +1,6 @@ use serde::Serialize; -use crate::device::GoogleHomeDevice; +use crate::{device::GoogleHomeDevice, errors::ErrorCode}; #[derive(Debug, Serialize)] pub enum Trait { @@ -20,47 +20,16 @@ pub trait OnOff { } // @TODO Implement correct error so we can handle them properly - fn is_on(&self) -> Result; - fn set_on(&mut self, on: bool) -> Result<(), anyhow::Error>; + fn is_on(&self) -> Result; + fn set_on(&mut self, on: bool) -> Result<(), ErrorCode>; } -pub trait AsOnOff { - fn cast(&self) -> Option<&dyn OnOff> { - None - } - fn cast_mut(&mut self) -> Option<&mut dyn OnOff> { - None - } -} -impl<'a, T: GoogleHomeDevice<'a> + OnOff> AsOnOff for T { - fn cast(&self) -> Option<&dyn OnOff> { - Some(self) - } - fn cast_mut(&mut self) -> Option<&mut dyn OnOff> { - Some(self) - } -} - +impl_cast::impl_cast!(GoogleHomeDevice, OnOff); pub trait Scene { fn is_scene_reversible(&self) -> Option { None } - fn set_active(&self, activate: bool) -> Result<(), anyhow::Error>; -} -pub trait AsScene { - fn cast(&self) -> Option<&dyn Scene> { - None - } - fn cast_mut(&mut self) -> Option<&mut dyn Scene> { - None - } -} -impl<'a, T: GoogleHomeDevice<'a> + Scene> AsScene for T { - fn cast(&self) -> Option<&dyn Scene> { - Some(self) - } - fn cast_mut(&mut self) -> Option<&mut dyn Scene> { - Some(self) - } + fn set_active(&self, activate: bool) -> Result<(), ErrorCode>; } +impl_cast::impl_cast!(GoogleHomeDevice, Scene); diff --git a/impl_cast/.gitignore b/impl_cast/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/impl_cast/.gitignore @@ -0,0 +1 @@ +/target diff --git a/impl_cast/Cargo.lock b/impl_cast/Cargo.lock new file mode 100644 index 0000000..457b9db --- /dev/null +++ b/impl_cast/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "impl_cast" +version = "0.1.0" +dependencies = [ + "paste", +] + +[[package]] +name = "paste" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1c2c742266c2f1041c914ba65355a83ae8747b05f208319784083583494b4b" diff --git a/impl_cast/Cargo.toml b/impl_cast/Cargo.toml new file mode 100644 index 0000000..bce1db2 --- /dev/null +++ b/impl_cast/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "impl_cast" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +paste = "1.0.10" diff --git a/impl_cast/src/lib.rs b/impl_cast/src/lib.rs new file mode 100644 index 0000000..c5c8235 --- /dev/null +++ b/impl_cast/src/lib.rs @@ -0,0 +1,31 @@ +pub extern crate paste; + +#[macro_export] +macro_rules! impl_cast { + ($base:ident, $trait:ident) => { + $crate::paste::paste! { + pub trait [< As $trait>] { + fn cast(&self) -> Option<&dyn $trait>; + fn cast_mut(&mut self) -> Option<&mut dyn $trait>; + } + + impl [< As $trait>] for T { + default fn cast(&self) -> Option<&dyn $trait> { + None + } + default fn cast_mut(&mut self) -> Option<&mut dyn $trait> { + None + } + } + + impl [< As $trait>] for T { + fn cast(&self) -> Option<&dyn $trait> { + Some(self) + } + fn cast_mut(&mut self) -> Option<&mut dyn $trait> { + Some(self) + } + } + } + }; +} diff --git a/src/devices.rs b/src/devices.rs index b85e705..6e9e8cc 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -1,64 +1,60 @@ mod ikea_outlet; +pub use self::ikea_outlet::IkeaOutlet; + +mod test_outlet; +pub use self::test_outlet::TestOutlet; use std::collections::HashMap; -use crate::{mqtt::Listener, state::StateOnOff}; +use google_home::{Fullfillment, traits::OnOff}; -pub use self::ikea_outlet::IkeaOutlet; +use crate::mqtt::Listener; -macro_rules! add_cast_for { - ($i:ident) => { - paste::paste! { - pub trait [< As $i>] { - fn cast(&self) -> Option<&dyn $i> { - None - } - fn cast_mut(&mut self) -> Option<&mut dyn $i> { - None - } - } - impl [< As $i>] for T { - fn cast(&self) -> Option<&dyn $i> { - Some(self) - } - fn cast_mut(&mut self) -> Option<&mut dyn $i> { - Some(self) - } - } - } - }; -} +impl_cast::impl_cast!(Device, Listener); +impl_cast::impl_cast!(Device, Fullfillment); +impl_cast::impl_cast!(Device, OnOff); -add_cast_for!(Listener); -add_cast_for!(StateOnOff); - -pub trait Device: AsListener + AsStateOnOff { - fn get_identifier(&self) -> &str; +pub trait Device: AsFullfillment + AsListener + AsOnOff { + fn get_id(&self) -> String; } pub struct Devices { devices: HashMap>, } +macro_rules! get_cast { + ($trait:ident) => { + paste::paste! { + pub fn [< as_ $trait:snake s >](&mut self) -> HashMap { + self.devices + .iter_mut() + .filter_map(|(id, device)| { + if let Some(listener) = [< As $trait >]::cast_mut(device.as_mut()) { + return Some((id.clone(), listener)); + }; + return None; + }).collect() + } + } + }; +} + impl Devices { pub fn new() -> Self { Self { devices: HashMap::new() } } pub fn add_device(&mut self, device: T) { - self.devices.insert(device.get_identifier().to_owned(), Box::new(device)); + self.devices.insert(device.get_id(), Box::new(device)); } - pub fn get_listeners(&mut self) -> HashMap<&str, &mut dyn Listener> { - self.devices - .iter_mut() - .filter_map(|(id, device)| { - if let Some(listener) = AsListener::cast_mut(device.as_mut()) { - return Some((id.as_str(), listener)); - }; - return None; - }).collect() - } + get_cast!(Listener); + get_cast!(Fullfillment); + get_cast!(OnOff); + + // pub fn get_google_devices(&mut self) -> HashMap<&str, &mut dyn GoogleHomeDevice> { + // self.devices + // } pub fn get_device(&mut self, name: &str) -> Option<&mut dyn Device> { if let Some(device) = self.devices.get_mut(name) { @@ -70,7 +66,7 @@ impl Devices { impl Listener for Devices { fn notify(&mut self, message: &rumqttc::Publish) { - self.get_listeners().iter_mut().for_each(|(_, listener)| { + self.as_listeners().iter_mut().for_each(|(_, listener)| { listener.notify(message); }) } diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index dc1ccfb..c4690f6 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -1,27 +1,29 @@ +use google_home::errors::ErrorCode; +use google_home::{GoogleHomeDevice, device, types::Type, traits}; use rumqttc::{Client, Publish}; use serde::{Deserialize, Serialize}; use crate::devices::Device; use crate::mqtt::Listener; -use crate::state::StateOnOff; use crate::zigbee::Zigbee; pub struct IkeaOutlet { + name: String, zigbee: Zigbee, client: Client, last_known_state: bool, } impl IkeaOutlet { - pub fn new(zigbee: Zigbee, mut client: Client) -> Self { + pub fn new(name: String, zigbee: Zigbee, mut client: Client) -> Self { client.subscribe(zigbee.get_topic(), rumqttc::QoS::AtLeastOnce).unwrap(); - Self{ zigbee, client, last_known_state: false } + Self{ name, zigbee, client, last_known_state: false } } } impl Device for IkeaOutlet { - fn get_identifier(& self) -> &str { - &self.zigbee.get_friendly_name() + fn get_id(&self) -> String { + self.zigbee.get_friendly_name().into() } } @@ -56,23 +58,42 @@ impl Listener for IkeaOutlet { } } -impl StateOnOff for IkeaOutlet { - // This will send a message over mqtt to update change the state of the device - // It does not change the internal state, that gets updated when the device responds - fn set_state(&mut self, state: bool) { +impl GoogleHomeDevice for IkeaOutlet { + fn get_device_type(&self) -> Type { + Type::Outlet + } + + fn get_device_name(&self) -> device::Name { + device::Name::new(&self.name) + } + + fn get_id(&self) -> String { + Device::get_id(self) + } + + fn is_online(&self) -> bool { + true + } +} + +impl traits::OnOff for IkeaOutlet { + fn is_on(&self) -> Result { + Ok(self.last_known_state) + } + + fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { let topic = self.zigbee.get_topic().to_owned(); let message = StateMessage{ - state: if state { + state: if on { "ON".to_owned() } else { "OFF".to_owned() } }; + // @TODO Handle potential error here self.client.publish(topic + "/set", rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap()).unwrap(); - } - fn get_state(&self) -> bool { - self.last_known_state + Ok(()) } } diff --git a/src/devices/test_outlet.rs b/src/devices/test_outlet.rs new file mode 100644 index 0000000..6d41840 --- /dev/null +++ b/src/devices/test_outlet.rs @@ -0,0 +1,31 @@ +use google_home::{errors::ErrorCode, traits}; + +use super::Device; + +pub struct TestOutlet { + on: bool +} + +impl TestOutlet { + pub fn new() -> Self { + Self { on: false } + } +} + +impl Device for TestOutlet { + fn get_id(&self) -> String { + "test_device".into() + } +} + +impl traits::OnOff for TestOutlet { + fn is_on(&self) -> Result { + Ok(self.on) + } + + fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { + println!("Setting on: {on}"); + self.on = on; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7df2d10..59dd5fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ +#![feature(specialization)] pub mod devices; pub mod zigbee; -mod state; pub mod mqtt; diff --git a/src/main.rs b/src/main.rs index d9b8122..ca93832 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,8 @@ use std::{time::Duration, rc::Rc, cell::RefCell, process::exit}; use dotenv::dotenv; -use automation::{devices::{Devices, IkeaOutlet, AsStateOnOff}, zigbee::Zigbee, mqtt::Notifier}; +use automation::{devices::{Devices, IkeaOutlet, TestOutlet}, zigbee::Zigbee, mqtt::Notifier}; +use google_home::GoogleHome; use rumqttc::{MqttOptions, Transport, Client}; fn get_required_env(name: &str) -> String { @@ -30,13 +31,60 @@ fn main() { let devices = Rc::new(RefCell::new(Devices::new())); // Create a new device and add it to the holder - devices.borrow_mut().add_device(IkeaOutlet::new(Zigbee::new("kitchen/kettle", "zigbee2mqtt/kitchen/kettle"), client.clone())); + devices.borrow_mut().add_device(IkeaOutlet::new("Kettle".into(), Zigbee::new("kitchen/kettle", "zigbee2mqtt/kitchen/kettle"), client.clone())); + + devices.borrow_mut().add_device(TestOutlet::new()); + + { + for (_, d) in devices.borrow_mut().as_on_offs().iter_mut() { + d.set_on(true).unwrap(); + } + } + + let gc = GoogleHome::new("Dreaded_X"); + + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + { + "id": "kitchen/kettle" + }, + { + "id": "test_device" + } + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": { + "on": false + } + } + ] + } + ] + } + } + ] +}"#; + let request = serde_json::from_str(json).unwrap(); + { + let mut binding = devices.borrow_mut(); + let mut ghd = binding.as_fullfillments(); + + let response = gc.handle_request(request, &mut ghd).unwrap(); + + println!("{response:?}"); + } let mut notifier = Notifier::new(); - // Update the state of the kettle - AsStateOnOff::cast_mut(devices.borrow_mut().get_device("kitchen/kettle").unwrap()).unwrap().set_state(false); - notifier.add_listener(Rc::downgrade(&devices)); notifier.start(connection); diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 478ee74..0000000 --- a/src/state.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub trait StateOnOff { - fn set_state(&mut self, state: bool); - fn get_state(&self) -> bool; -}