diff --git a/google-home/src/request.rs b/google-home/src/request.rs index 32d955a..5959e74 100644 --- a/google-home/src/request.rs +++ b/google-home/src/request.rs @@ -1,43 +1,24 @@ +pub mod sync; +pub mod query; +pub mod execute; + use serde::Deserialize; use uuid::Uuid; -#[derive(Debug, PartialEq, Eq, Deserialize)] -#[serde(tag = "intent")] +#[derive(Debug, Deserialize)] +#[serde(tag = "intent", content = "payload")] enum Intent { #[serde(rename = "action.devices.SYNC")] Sync, + #[serde(rename = "action.devices.QUERY")] + Query(query::Payload), + #[serde(rename = "action.devices.EXECUTE")] + Execute(execute::Payload), } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct Request { - request_id: Uuid, - inputs: Vec, + pub request_id: Uuid, + pub inputs: Vec, } - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - - #[test] - fn deserialize_sync_request() { - - let json = r#"{ - "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", - "inputs": [ - { - "intent": "action.devices.SYNC" - } - ] - }"#; - - let req: Request = serde_json::from_str(json).unwrap(); - - assert_eq!(req.request_id, Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap()); - assert_eq!(req.inputs.len(), 1); - assert_eq!(req.inputs[0], Intent::Sync); - } -} - diff --git a/google-home/src/request/execute.rs b/google-home/src/request/execute.rs new file mode 100644 index 0000000..a6cbe6c --- /dev/null +++ b/google-home/src/request/execute.rs @@ -0,0 +1,104 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload { + commands: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Command { + devices: Vec, + execution: Vec +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Device { + id: String, + // customData +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "command", content = "params")] +pub enum CommandType { + #[serde(rename = "action.devices.commands.OnOff")] + OnOff { + on: bool + }, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use uuid::Uuid; + use super::*; + use crate::request::{Request, Intent}; + + #[test] + fn deserialize() { + + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [ + { + "id": "123", + "customData": { + "fooValue": 74, + "barValue": true, + "bazValue": "sheepdip" + } + }, + { + "id": "456", + "customData": { + "fooValue": 36, + "barValue": false, + "bazValue": "moarsheep" + } + } + ], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": { + "on": true + } + } + ] + } + ] + } + } + ] +}"#; + + let req: Request = serde_json::from_str(json).unwrap(); + + println!("{:?}", req); + + assert_eq!(req.request_id, Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap()); + assert_eq!(req.inputs.len(), 1); + match &req.inputs[0] { + Intent::Execute(payload) => { + assert_eq!(payload.commands.len(), 1); + assert_eq!(payload.commands[0].devices.len(), 2); + assert_eq!(payload.commands[0].devices[0].id, "123"); + assert_eq!(payload.commands[0].devices[1].id, "456"); + assert_eq!(payload.commands[0].execution.len(), 1); + match payload.commands[0].execution[0] { + CommandType::OnOff{on} => assert_eq!(on, true), + // _ => panic!("Expected OnOff") + } + }, + _ => panic!("Expected Execute intent") + }; + } +} diff --git a/google-home/src/request/query.rs b/google-home/src/request/query.rs new file mode 100644 index 0000000..da79bdb --- /dev/null +++ b/google-home/src/request/query.rs @@ -0,0 +1,71 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload { + pub devices: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Device { + pub id: String, + // customData +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use uuid::Uuid; + + use crate::request::{Request, Intent}; + + #[test] + fn deserialize() { + + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.QUERY", + "payload": { + "devices": [ + { + "id": "123", + "customData": { + "fooValue": 74, + "barValue": true, + "bazValue": "foo" + } + }, + { + "id": "456", + "customData": { + "fooValue": 12, + "barValue": false, + "bazValue": "bar" + } + } + ] + } + } + ] +}"#; + + let req: Request = serde_json::from_str(json).unwrap(); + + println!("{:?}", req); + + assert_eq!(req.request_id, Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap()); + assert_eq!(req.inputs.len(), 1); + match &req.inputs[0] { + Intent::Query(payload) => { + assert_eq!(payload.devices.len(), 2); + assert_eq!(payload.devices[0].id, "123"); + assert_eq!(payload.devices[1].id, "456"); + }, + _ => panic!("Expected Query intent") + }; + } +} diff --git a/google-home/src/request/sync.rs b/google-home/src/request/sync.rs new file mode 100644 index 0000000..e19c7d6 --- /dev/null +++ b/google-home/src/request/sync.rs @@ -0,0 +1,33 @@ +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use uuid::Uuid; + + use crate::request::{Request, Intent}; + + #[test] + fn deserialize() { + + let json = r#"{ + "requestId": "ff36a3cc-ec34-11e6-b1a0-64510650abcf", + "inputs": [ + { + "intent": "action.devices.SYNC" + } + ] + }"#; + + let req: Request = serde_json::from_str(json).unwrap(); + + println!("{:?}", req); + + assert_eq!(req.request_id, Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap()); + assert_eq!(req.inputs.len(), 1); + match req.inputs[0] { + Intent::Sync => {}, + _ => panic!("Expected Sync intent") + } + } +} + diff --git a/google-home/src/response.rs b/google-home/src/response.rs index 4b95f9c..0d56ca9 100644 --- a/google-home/src/response.rs +++ b/google-home/src/response.rs @@ -1,4 +1,6 @@ pub mod sync; +pub mod query; +pub mod execute; use serde::Serialize; use uuid::Uuid; @@ -10,35 +12,31 @@ pub struct Response { payload: ResponsePayload, } +impl Response { + fn new(request_id: Uuid, payload: ResponsePayload) -> Self { + Self { request_id, payload } + } +} + #[derive(Debug, Serialize)] #[serde(untagged)] pub enum ResponsePayload { - Sync(sync::Payload) + Sync(sync::Payload), + Query(query::Payload), + Execute(execute::Payload), } -#[cfg(test)] -mod tests { - use std::str::FromStr; +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct State { + #[serde(skip_serializing_if = "Option::is_none")] + on: Option, +} - use crate::{response::sync::Device, types::Type, traits::Trait}; - - use super::*; - - #[test] - fn serialize_sync_response() { - let mut sync_resp = sync::Payload::new("Dreaded_X"); - - let mut device = Device::new("kitchen/kettle", "Kettle", Type::Kettle); - device.traits.push(Trait::OnOff); - device.room_hint = "Kitchen".into(); - sync_resp.add_device(device); - - let resp = Response{ request_id: Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap(), payload: ResponsePayload::Sync(sync_resp) }; - - println!("{:?}", resp); - - let json = serde_json::to_string(&resp).unwrap(); - - println!("{}", json); +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 new file mode 100644 index 0000000..7d8c054 --- /dev/null +++ b/google-home/src/response/execute.rs @@ -0,0 +1,98 @@ +use serde::Serialize; + +use crate::response::State; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload { + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub debug_string: Option, + commands: Vec, +} + +impl Payload { + pub fn new() -> Self { + Self { error_code: None, debug_string: None, commands: Vec::new() } + } + + pub fn add_command(&mut self, command: Command) { + self.commands.push(command); + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Command { + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + + ids: Vec, + status: Status, + pub states: Option, +} + +impl Command { + pub fn new(status: Status) -> Self { + Self { error_code: None, ids: Vec::new(), status, states: None } + } + + pub fn add_id(&mut self, id: &str) { + self.ids.push(id.into()); + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct States { + pub online: bool, + + #[serde(flatten)] + pub state: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Status { + Success, + Pending, + Offline, + Exceptions, + Error, +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use uuid::Uuid; + use super::*; + use crate::response::{Response, ResponsePayload, State}; + + #[test] + fn serialize() { + let mut execute_resp = Payload::new(); + + let state = State::default().on(true); + let mut command = Command::new(Status::Success); + command.states = Some(States { + online: true, + state: Some(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.ids.push("456".into()); + execute_resp.add_command(command); + + let resp = Response::new(Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap(), ResponsePayload::Execute(execute_resp)); + + let json = serde_json::to_string(&resp).unwrap(); + + println!("{}", json); + + // @TODO Add a known correct output to test against + } +} diff --git a/google-home/src/response/query.rs b/google-home/src/response/query.rs new file mode 100644 index 0000000..8fd0425 --- /dev/null +++ b/google-home/src/response/query.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use serde::Serialize; + +use crate::response::State; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Payload { + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub debug_string: Option, + devices: HashMap, +} + +impl Payload { + pub fn new() -> Self { + Self { error_code: None, debug_string: None, devices: HashMap::new() } + } + + pub fn add_device(&mut self, id: &str, device: Device) { + self.devices.insert(id.into(), device); + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Status { + Success, + Offline, + Exceptions, + Error, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Device { + online: bool, + status: Status, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + + #[serde(flatten)] + pub state: Option, +} + +impl Device { + pub fn new(online: bool, status: Status) -> Self { + Self { online, status, error_code: None, state: None } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use uuid::Uuid; + use super::*; + use crate::response::{Response, ResponsePayload, State}; + + #[test] + fn serialize() { + let mut query_resp = Payload::new(); + + let state = State::default().on(true); + let mut device = Device::new(true, Status::Success); + device.state = Some(state); + query_resp.add_device("123", device); + + let state = State::default().on(false); + let mut device = Device::new(true, Status::Success); + device.state = Some(state); + query_resp.add_device("456", device); + + let resp = Response::new(Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap(), ResponsePayload::Query(query_resp)); + + let json = serde_json::to_string(&resp).unwrap(); + + println!("{}", json); + + // @TODO Add a known correct output to test against + } +} diff --git a/google-home/src/response/sync.rs b/google-home/src/response/sync.rs index 2d059a0..18221ff 100644 --- a/google-home/src/response/sync.rs +++ b/google-home/src/response/sync.rs @@ -6,17 +6,17 @@ use crate::traits::Trait; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Payload { - user_agent_id: String, + agent_user_id: String, #[serde(skip_serializing_if = "Option::is_none")] - error_code: Option, + pub error_code: Option, #[serde(skip_serializing_if = "Option::is_none")] - debug_string: Option, - devices: Vec, + pub debug_string: Option, + pub devices: Vec, } impl Payload { - pub fn new(user_agent_id: &str) -> Self { - Self { user_agent_id: user_agent_id.into(), error_code: None, debug_string: None, devices: Vec::new() } + pub fn new(agent_user_id: &str) -> Self { + Self { agent_user_id: agent_user_id.into(), error_code: None, debug_string: None, devices: Vec::new() } } pub fn add_device(&mut self, device: Device) { @@ -27,9 +27,9 @@ impl Payload { #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct Device { - pub id: String, + id: String, #[serde(rename = "type")] - pub device_type: Type, + device_type: Type, pub traits: Vec, pub name: DeviceName, pub will_report_state: bool, @@ -37,6 +37,10 @@ pub struct Device { pub notification_supported_by_agent: Option, #[serde(skip_serializing_if = "String::is_empty")] pub room_hint: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub device_info: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes: Option, } impl Device { @@ -46,12 +50,14 @@ impl Device { device_type, traits: Vec::new(), name: DeviceName { - default_name: Vec::new(), + default_names: Vec::new(), name: name.into(), nicknames: Vec::new() }, - will_report_state: true, + will_report_state: false, notification_supported_by_agent: None, room_hint: "".into(), + device_info: None, + attributes: None, } } } @@ -60,8 +66,66 @@ impl Device { #[serde(rename_all = "camelCase")] pub struct DeviceName { #[serde(skip_serializing_if = "Vec::is_empty")] - pub default_name: Vec, - pub name: String, + pub default_names: Vec, + name: String, #[serde(skip_serializing_if = "Vec::is_empty")] pub nicknames: Vec, } + +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeviceInfo { + #[serde(skip_serializing_if = "String::is_empty")] + pub manufacturer: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub model: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub hw_version: String, + #[serde(skip_serializing_if = "String::is_empty")] + pub sw_version: String, + // attributes + // customData + // otherDeviceIds +} + +#[derive(Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Attributes { + +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use uuid::Uuid; + use super::*; + use crate::{response::{Response, ResponsePayload}, types::Type, traits::Trait}; + + #[test] + fn serialize() { + let mut sync_resp = Payload::new("1836.15267389"); + + let mut device = Device::new("123", "Night light", Type::Kettle); + device.traits.push(Trait::OnOff); + device.name.default_names.push("My Outlet 1234".to_string()); + device.name.nicknames.push("wall plug".to_string()); + + device.room_hint = "kitchen".into(); + device.device_info = Some(DeviceInfo { + manufacturer: "lights-out-inc".to_string(), + model: "hs1234".to_string(), + hw_version: "3.2".to_string(), + sw_version: "11.4".to_string(), + }); + + sync_resp.add_device(device); + + let resp = Response::new(Uuid::from_str("ff36a3cc-ec34-11e6-b1a0-64510650abcf").unwrap(), ResponsePayload::Sync(sync_resp)); + + let json = serde_json::to_string(&resp).unwrap(); + + 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"}}]}}"#) + } +}