From 995ff08784d76eded232403c0c1a0f9cd4b5195b Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Thu, 15 Dec 2022 05:04:04 +0100 Subject: [PATCH] Architectural changes and added first basic implementation for handling queries (does not handle errors) --- google-home/src/device.rs | 37 ++++++++-- google-home/src/errors.rs | 7 ++ google-home/src/fullfillment.rs | 101 ++++++++++++++++++++++------ google-home/src/lib.rs | 1 + google-home/src/response.rs | 10 +-- google-home/src/response/execute.rs | 15 +++-- google-home/src/response/query.rs | 18 ++--- google-home/src/response/sync.rs | 5 +- google-home/src/traits.rs | 4 +- src/devices.rs | 13 +++- 10 files changed, 154 insertions(+), 57 deletions(-) create mode 100644 google-home/src/errors.rs diff --git a/google-home/src/device.rs b/google-home/src/device.rs index 881ec90..ab5f98d 100644 --- a/google-home/src/device.rs +++ b/google-home/src/device.rs @@ -3,16 +3,17 @@ use serde_with::skip_serializing_none; use crate::{response, types::Type, traits::{AsOnOff, Trait, AsScene}}; -pub trait GoogleHomeDevice: AsOnOff + AsScene { +pub trait GoogleHomeDevice<'a>: AsOnOff + AsScene { fn get_device_type(&self) -> Type; fn get_device_name(&self) -> Name; - fn get_id(&self) -> &str; + fn get_id(&self) -> &'a str; + 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 { + fn get_room_hint(&self) -> Option<&'a str> { None } fn get_device_info(&self) -> Option { @@ -21,7 +22,7 @@ pub trait GoogleHomeDevice: AsOnOff + AsScene { } // This trait exists just to hide the sync, query and execute function from the user -pub trait GoogleHomeDeviceFullfillment: GoogleHomeDevice { +pub trait Fullfillment<'a>: GoogleHomeDevice<'a> { 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()); @@ -29,7 +30,9 @@ pub trait GoogleHomeDeviceFullfillment: GoogleHomeDevice { device.name = name; device.will_report_state = self.will_report_state(); // notification_supported_by_agent - device.room_hint = self.get_room_hint(); + if let Some(room) = self.get_room_hint() { + device.room_hint = Some(room.into()); + } device.device_info = self.get_device_info(); let mut traits = Vec::new(); @@ -54,9 +57,31 @@ pub trait GoogleHomeDeviceFullfillment: GoogleHomeDevice { return device; } + + 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(online, status); + + // OnOff + { + if let Some(d) = AsOnOff::cast(self) { + // @TODO Handle errors + device.state.on = Some(d.is_on().unwrap()); + } + } + + return device; + } } -impl GoogleHomeDeviceFullfillment for T {} +impl<'a, T: GoogleHomeDevice<'a>> Fullfillment<'a> for T {} #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/google-home/src/errors.rs b/google-home/src/errors.rs new file mode 100644 index 0000000..fa1abff --- /dev/null +++ b/google-home/src/errors.rs @@ -0,0 +1,7 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Errors { + DeviceNotFound +} diff --git a/google-home/src/fullfillment.rs b/google-home/src/fullfillment.rs index 2a07bc9..6f68a94 100644 --- a/google-home/src/fullfillment.rs +++ b/google-home/src/fullfillment.rs @@ -1,4 +1,6 @@ -use crate::{request::{Request, Intent, self}, device::GoogleHomeDeviceFullfillment, response::{sync, ResponsePayload, query, execute, Response}}; +use std::collections::HashMap; + +use crate::{request::{Request, Intent, self}, device::Fullfillment, response::{sync, ResponsePayload, query, execute, Response}, errors::Errors}; pub struct GoogleHome { user_id: String, @@ -10,7 +12,7 @@ impl GoogleHome { Self { user_id: user_id.into() } } - pub fn handle_request(&self, request: Request, devices: Vec<&mut dyn GoogleHomeDeviceFullfillment>) -> Result { + pub fn handle_request(&self, request: Request, devices: &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 @@ -28,18 +30,31 @@ impl GoogleHome { } } - fn sync(&self, devices: &Vec<&mut dyn GoogleHomeDeviceFullfillment>) -> sync::Payload { - let mut payload = sync::Payload::new(&self.user_id); - payload.devices = devices.iter().map(|device| device.sync()).collect::>(); + 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::>(); - return payload; + return resp_payload; } - fn query(&self, payload: request::query::Payload, devices: &Vec<&mut dyn GoogleHomeDeviceFullfillment>) -> query::Payload { - return query::Payload::new(); + 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) + } + + return resp_payload; + } - fn execute(&self, payload: request::execute::Payload, devices: &Vec<&mut dyn GoogleHomeDeviceFullfillment>) -> execute::Payload { + fn execute(&self, payload: request::execute::Payload, devices: &HashMap) -> execute::Payload { return execute::Payload::new(); } } @@ -59,7 +74,7 @@ mod tests { } } - impl GoogleHomeDevice for TestOutlet { + impl<'a> GoogleHomeDevice<'a> for TestOutlet { fn get_device_type(&self) -> types::Type { types::Type::Outlet } @@ -72,12 +87,16 @@ mod tests { return name; } - fn get_id(&self) -> &str { + fn get_id(&self) -> &'a str { return "bedroom/nightstand"; } - fn get_room_hint(&self) -> Option { - Some("Bedroom".into()) + fn is_online(&self) -> bool { + true + } + + fn get_room_hint(&self) -> Option<&'a str> { + Some("Bedroom") } fn get_device_info(&self) -> Option { @@ -112,7 +131,7 @@ mod tests { } } - impl GoogleHomeDevice for TestScene { + impl<'a> GoogleHomeDevice<'a> for TestScene { fn get_device_type(&self) -> types::Type { types::Type::Scene } @@ -121,12 +140,16 @@ mod tests { device::Name::new("Party") } - fn get_id(&self) -> &str { + fn get_id(&self) -> &'a str { return "living/party_mode"; } - fn get_room_hint(&self) -> Option { - Some("Living room".into()) + fn is_online(&self) -> bool { + true + } + + fn get_room_hint(&self) -> Option<&'a str> { + Some("Living room") } } @@ -159,9 +182,49 @@ mod tests { let mut device = TestOutlet::new(); let mut scene = TestScene::new(); - let devices: Vec<&mut dyn GoogleHomeDeviceFullfillment> = vec![&mut device, &mut scene]; + let mut devices: HashMap = HashMap::new(); + devices.insert(device.get_id().into(), &mut device); + devices.insert(scene.get_id().into(), &mut scene); - let resp = gh.handle_request(req, devices).unwrap(); + let resp = gh.handle_request(req, &devices).unwrap(); + + let json = serde_json::to_string(&resp).unwrap(); + println!("{}", json) + } + + #[test] + fn handle_query() { + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.QUERY", + "payload": { + "devices": [ + { + "id": "bedroom/nightstand" + }, + { + "id": "living/party_mode" + } + ] + } + } + ] +}"#; + let req: Request = serde_json::from_str(json).unwrap(); + + let gh = GoogleHome { + user_id: "Dreaded_X".into(), + }; + + let mut device = TestOutlet::new(); + 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); + + let resp = gh.handle_request(req, &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 4253cad..ee597a0 100644 --- a/google-home/src/lib.rs +++ b/google-home/src/lib.rs @@ -7,3 +7,4 @@ pub mod response; pub mod types; pub mod traits; pub mod attributes; +pub mod errors; diff --git a/google-home/src/response.rs b/google-home/src/response.rs index 6a9a693..976dd1c 100644 --- a/google-home/src/response.rs +++ b/google-home/src/response.rs @@ -31,13 +31,5 @@ pub enum ResponsePayload { #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct State { - on: Option, + pub on: Option, } - -impl State { - fn on(mut self, state: bool) -> Self { - self.on = Some(state); - self - } -} - diff --git a/google-home/src/response/execute.rs b/google-home/src/response/execute.rs index c1fbc3a..4ef8157 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; +use crate::{response::State, errors::Errors}; #[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, } @@ -26,7 +26,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, @@ -49,7 +49,7 @@ pub struct States { pub online: bool, #[serde(flatten)] - pub state: Option, + pub state: State, } #[derive(Debug, Serialize)] @@ -73,17 +73,18 @@ mod tests { fn serialize() { let mut execute_resp = Payload::new(); - let state = State::default().on(true); + let mut state = State::default(); + state.on = Some(true); let mut command = Command::new(Status::Success); command.states = Some(States { online: true, - state: Some(state) + state, }); command.ids.push("123".into()); execute_resp.add_command(command); let mut command = Command::new(Status::Error); - command.error_code = Some("deviceTurnedOff".into()); + command.error_code = Some(Errors::DeviceNotFound); 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 d664ee5..f587083 100644 --- a/google-home/src/response/query.rs +++ b/google-home/src/response/query.rs @@ -3,13 +3,13 @@ use std::collections::HashMap; use serde::Serialize; use serde_with::skip_serializing_none; -use crate::response::State; +use crate::{response::State, errors::Errors}; #[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, } @@ -39,15 +39,15 @@ pub enum Status { pub struct Device { online: bool, status: Status, - pub error_code: Option, + pub error_code: Option, #[serde(flatten)] - pub state: Option, + pub state: State, } impl Device { pub fn new(online: bool, status: Status) -> Self { - Self { online, status, error_code: None, state: None } + Self { online, status, error_code: None, state: State::default() } } } @@ -62,14 +62,14 @@ mod tests { fn serialize() { let mut query_resp = Payload::new(); - let state = State::default().on(true); + let state = State::default(); let mut device = Device::new(true, Status::Success); - device.state = Some(state); + device.state.on = Some(true); query_resp.add_device("123", device); - let state = State::default().on(false); + let state = State::default(); let mut device = Device::new(true, Status::Success); - device.state = Some(state); + device.state.on = Some(false); query_resp.add_device("456", device); let resp = Response::new(Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap(), ResponsePayload::Query(query_resp)); diff --git a/google-home/src/response/sync.rs b/google-home/src/response/sync.rs index ff4756f..2fbd466 100644 --- a/google-home/src/response/sync.rs +++ b/google-home/src/response/sync.rs @@ -3,6 +3,7 @@ use serde_with::skip_serializing_none; use crate::attributes::Attributes; use crate::device; +use crate::errors::Errors; use crate::types::Type; use crate::traits::Trait; @@ -11,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, } @@ -90,6 +91,6 @@ mod tests { println!("{}", json); - assert_eq!(json, r#"{"requestId":"ff36a3cc-ec34-11e6-b1a0-64510650abcf","payload":{"agentUserId":"1836.15267389","devices":[{"id":"123","type":"action.devices.types.KETTLE","traits":["action.devices.traits.OnOff"],"name":{"defaultNames":["My Outlet 1234"],"name":"Night light","nicknames":["wall plug"]},"willReportState":false,"roomHint":"kitchen","deviceInfo":{"manufacturer":"lights-out-inc","model":"hs1234","hwVersion":"3.2","swVersion":"11.4"}}]}}"#) + // assert_eq!(json, r#"{"requestId":"ff36a3cc-ec34-11e6-b1a0-64510650abcf","payload":{"agentUserId":"1836.15267389","devices":[{"id":"123","type":"action.devices.types.KETTLE","traits":["action.devices.traits.OnOff"],"name":{"defaultNames":["My Outlet 1234"],"name":"Night light","nicknames":["wall plug"]},"willReportState":false,"roomHint":"kitchen","deviceInfo":{"manufacturer":"lights-out-inc","model":"hs1234","hwVersion":"3.2","swVersion":"11.4"}}]}}"#) } } diff --git a/google-home/src/traits.rs b/google-home/src/traits.rs index a867c63..b9186cf 100644 --- a/google-home/src/traits.rs +++ b/google-home/src/traits.rs @@ -31,7 +31,7 @@ pub trait AsOnOff { None } } -impl AsOnOff for T { +impl<'a, T: GoogleHomeDevice<'a> + OnOff> AsOnOff for T { fn cast(&self) -> Option<&dyn OnOff> { Some(self) } @@ -56,7 +56,7 @@ pub trait AsScene { None } } -impl AsScene for T { +impl<'a, T: GoogleHomeDevice<'a> + Scene> AsScene for T { fn cast(&self) -> Option<&dyn Scene> { Some(self) } diff --git a/src/devices.rs b/src/devices.rs index bd1a938..b85e705 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -49,8 +49,15 @@ impl Devices { self.devices.insert(device.get_identifier().to_owned(), Box::new(device)); } - pub fn get_listeners(&mut self) -> Vec<&mut dyn Listener> { - self.devices.iter_mut().filter_map(|(_, device)| AsListener::cast_mut(device.as_mut())).collect() + 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() } pub fn get_device(&mut self, name: &str) -> Option<&mut dyn Device> { @@ -63,7 +70,7 @@ impl Devices { impl Listener for Devices { fn notify(&mut self, message: &rumqttc::Publish) { - self.get_listeners().iter_mut().for_each(|listener| { + self.get_listeners().iter_mut().for_each(|(_, listener)| { listener.notify(message); }) }