homelander/
lib.rs

1//! # Homelander
2//! Homelander is a Google Home integration framework. It provides serialization and deserialization for fulfillment requests.
3//! It also handles translation between Google Home traits and Rust traits. Furthermore it provides error handling and translating between
4//! Rust errors and errors accepted by Google Home.
5//!
6//! Homelander does *not* provide an OAuth2 server or a web server.
7//!
8//! ## Getting started
9//! To get started, you'll first have to create your own OAuth2 server or use an existing implementation.
10//! Refer to [the Google documentation](https://developers.google.com/assistant/smarthome/concepts/account-linking) for details.
11//!
12//! After you've done this, you've presumably also configured your web server. You can then easily get started with Homelander.
13//! Create a Device like so:
14//! ```
15//! use std::sync::{Arc, Mutex};
16//! use homelander::{Device, DeviceType, Homelander};
17//! use homelander::traits::{CombinedDeviceError, DeviceInfo, DeviceName, GoogleHomeDevice};
18//! use homelander::traits::on_off::OnOff;
19//!
20//! #[derive(Debug)]
21//! struct MyDevice(bool);
22//!
23//! // Implement the basic GoogleHomeDevice trait,
24//! // This gives the basic information required for every device
25//! impl GoogleHomeDevice for MyDevice {
26//!     fn get_device_info(&self) -> DeviceInfo {
27//!         DeviceInfo {
28//!             model: "mydevice".to_string(),
29//!             manufacturer: "mydevice company".to_string(),
30//!             hw: "0.1.0".to_string(),
31//!             sw: "0.1.0".to_string(),
32//!         }
33//!     }
34//!
35//!     fn will_report_state(&self) -> bool {
36//!         // Will this Device be reporting state to Google?
37//!         // Note that as of August 6 2022, this isn't implemented in Homelander yet,
38//!         // Until it is, this should *always* be false.
39//!         false
40//!     }
41//!
42//!     fn get_device_name(&self) -> DeviceName {
43//!         DeviceName {
44//!             name: "MyDevice".to_string(),
45//!             default_names: Vec::new(),
46//!             nicknames: Vec::new(),
47//!         }
48//!     }
49//!
50//!     fn is_online(&self) -> bool {
51//!         true
52//!     }
53//!
54//!     fn disconnect(&mut self) {
55//!         // Handle your disconnect here
56//!     }
57//! }
58//!
59//! // Implement a device specific trait. E.g. OnOff
60//! impl OnOff for MyDevice {
61//!     fn is_on(&self) -> Result<bool, CombinedDeviceError> {
62//!         Ok(self.0)
63//!     }
64//!
65//!     fn set_on(&mut self, on: bool) -> Result<(), CombinedDeviceError> {
66//!         self.0 = on;
67//!         Ok(())
68//!     }
69//! }
70//!
71//! // Create the device
72//! let mut device = Device::new(MyDevice(false), DeviceType::Outlet, "my_id".to_string());
73//! // Register the OnOff traitr
74//! device.set_on_off();
75//!
76//! // Create the Homelander struct
77//! let mut homelander = Homelander::new("my_user_id".to_string());
78//! homelander.add_device(device);
79//! ```
80//! This will create a basic setup. You can now register a fulfillment route with your webserver.
81//! This route should take a JSON payload: [Request]. This request can then be passed to Homelander:
82//! ```
83//! # use std::sync::{Arc, Mutex};
84//! # use homelander::{Device, DeviceTraits, DeviceType, Homelander, Request};
85//! # use homelander::fulfillment::request::Input;
86//! # use homelander::traits::{CombinedDeviceError, DeviceInfo, DeviceName, GoogleHomeDevice};
87//! # use homelander::traits::on_off::OnOff;
88//! #
89//! # fn get_homelander(_: String) -> Homelander {
90//! #    let mut homelander = Homelander::new("my_user_id".to_string());
91//! #    let mut device = Device::new(MyDevice(false), DeviceType::Outlet, "my_id".to_string());
92//! #    device.set_on_off();
93//! #    homelander.add_device(device);
94//! #    homelander
95//! # }
96//! #
97//! # #[derive(Debug)]
98//! # struct MyDevice(bool);
99//! #
100//! # impl GoogleHomeDevice for MyDevice {
101//! #    fn get_device_info(&self) -> DeviceInfo {
102//! #        DeviceInfo {
103//! #            model: "mydevice".to_string(),
104//! #            manufacturer: "mydevice company".to_string(),
105//! #            hw: "0.1.0".to_string(),
106//! #            sw: "0.1.0".to_string(),
107//! #        }
108//! #    }
109//! #
110//! #    fn will_report_state(&self) -> bool {
111//! #        false
112//! #    }
113//! #
114//! #    fn get_device_name(&self) -> DeviceName {
115//! #        DeviceName {
116//! #            name: "MyDevice".to_string(),
117//! #            default_names: Vec::new(),
118//! #            nicknames: Vec::new(),
119//! #        }
120//! #    }
121//! #
122//! #    fn is_online(&self) -> bool {
123//! #        true
124//! #    }
125//! #
126//! #    fn disconnect(&mut self) {
127//! #        todo!()
128//! #    }
129//! # }
130//! #
131//! # impl OnOff for MyDevice {
132//! #    fn is_on(&self) -> Result<bool, CombinedDeviceError> {
133//! #        Ok(self.0)
134//! #    }
135//! #
136//! #    fn set_on(&mut self, on: bool) -> Result<(), CombinedDeviceError> {
137//! #        self.0 = on;
138//! #        Ok(())
139//! #    }
140//! # }
141//! #
142//! # fn get_incoming_request() -> Request {
143//! #    Request {
144//! #        request_id: String::default(),
145//! #        inputs: vec![
146//! #            Input::Sync
147//! #        ]
148//! #    }
149//! # }
150//!
151//! // Retrieve the Homelander for the user,
152//! // The user can be identified through the OAuth2 token provided by Google
153//! let mut homelander = get_homelander("my_user_id".to_string());
154//! // Let homelander handle the request and create a response
155//! // The response can then be returned to Google as JSON
156//! let the_request = get_incoming_request(); // Usually you'd get this from your web framework
157//! let response = homelander.handle_request(the_request);
158//! ```
159//!
160
161use crate::fulfillment::request::execute::CommandType;
162use crate::fulfillment::request::Input;
163use crate::fulfillment::response::execute::CommandStatus;
164use crate::traits::arm_disarm::ArmDisarm;
165use crate::traits::brightness::Brightness;
166use crate::traits::color_setting::ColorSetting;
167use crate::traits::{CombinedDeviceError, GoogleHomeDevice};
168use std::collections::HashMap;
169use std::error::Error;
170use std::fmt::Debug;
171use tracing::{instrument, trace};
172
173mod device;
174mod device_trait;
175mod device_type;
176mod execute_error;
177#[doc(hidden)]
178pub mod fulfillment;
179mod serializable_error;
180pub mod traits;
181
182pub use device::Device;
183pub use device_type::DeviceType;
184pub use fulfillment::request::Request;
185pub use fulfillment::response::Response;
186pub use serializable_error::*;
187
188/// The output of an EXECUTE command
189struct CommandOutput {
190    id: String,
191    status: CommandStatus,
192    state: Option<fulfillment::response::execute::CommandState>,
193    error: Option<SerializableError>,
194    debug_string: Option<String>,
195}
196
197pub trait DeviceTraits: GoogleHomeDevice + Send + Sync + Debug + 'static {}
198
199impl<T: GoogleHomeDevice + Send + Debug + Sync + 'static> DeviceTraits for T {}
200
201/// Keeps track of all devices owned by a specific user.
202#[derive(Debug)]
203pub struct Homelander {
204    agent_user_id: String,
205    devices: Vec<Device<dyn crate::DeviceTraits>>,
206}
207
208impl Homelander {
209    pub fn new(user_id: String) -> Self {
210        Self {
211            agent_user_id: user_id,
212            devices: Vec::new(),
213        }
214    }
215
216    /// Add a device
217    pub fn add_device<T: DeviceTraits>(&mut self, device: Device<T>) {
218        self.devices.push(device.unsize());
219    }
220
221    /// Remove a device with ID `id`
222    pub fn remove_device<S: AsRef<str>>(&mut self, id: S) {
223        self.devices.retain(|f| f.id.ne(id.as_ref()));
224    }
225
226    /// Handle an incomming fulfillment request from Google and create a response for it
227    #[instrument]
228    pub fn handle_request(&mut self, request: fulfillment::request::Request) -> fulfillment::response::Response {
229        let payload = request
230            .inputs
231            .into_iter()
232            .map(|input| match input {
233                Input::Execute(execute) => {
234                    let commands = execute
235                        .commands
236                        .into_iter()
237                        .map(|command| {
238                            command
239                                .devices
240                                .into_iter()
241                                .map(|device| device.id)
242                                .map(|device_id| {
243                                    command
244                                        .execution
245                                        .iter()
246                                        .map(|command_type| self.execute(&device_id, command_type.clone()))
247                                        .filter_map(|command_output| command_output)
248                                        .collect::<Vec<_>>()
249                                })
250                                .flatten()
251                                .collect::<Vec<_>>()
252                        })
253                        .flatten()
254                        .collect::<Vec<_>>()
255                        .into_iter()
256                        .map(|output| match output.status {
257                            CommandStatus::Success | CommandStatus::Exceptions => fulfillment::response::execute::Command {
258                                ids: vec![output.id],
259                                status: output.status,
260                                states: output.state,
261                                error_code: None,
262                                debug_string: output.debug_string,
263                            },
264                            CommandStatus::Error => fulfillment::response::execute::Command {
265                                ids: vec![output.id],
266                                status: CommandStatus::Error,
267                                states: None,
268                                error_code: output.error,
269                                debug_string: output.debug_string,
270                            },
271                            CommandStatus::Offline | CommandStatus::Pending => fulfillment::response::execute::Command {
272                                ids: vec![output.id],
273                                status: output.status,
274                                states: None,
275                                error_code: None,
276                                debug_string: output.debug_string,
277                            },
278                        })
279                        .collect::<Vec<_>>();
280
281                    fulfillment::response::ResponsePayload::Execute(fulfillment::response::execute::Payload { commands })
282                }
283                Input::Sync => fulfillment::response::ResponsePayload::Sync(self.sync()),
284                Input::Query(payload) => fulfillment::response::ResponsePayload::Query(self.query(payload)),
285                Input::Disconnect => {
286                    self.devices.iter_mut().for_each(|x| x.disconnect());
287                    fulfillment::response::ResponsePayload::Disconnect
288                }
289            })
290            .collect::<Vec<_>>()
291            .remove(0);
292
293        fulfillment::response::Response {
294            request_id: request.request_id,
295            payload,
296        }
297    }
298
299    /// QUERY all devices specified in `payload`
300    #[instrument]
301    fn query(&self, payload: fulfillment::request::query::Payload) -> fulfillment::response::query::Payload {
302        trace!("Running QUERY operation");
303
304        let device_states = payload
305            .devices
306            .into_iter()
307            .map(|device| device.id)
308            .map(|device_id| {
309                (
310                    device_id.clone(),
311                    self.devices
312                        .iter()
313                        .filter(|device| device.id.eq(&device_id))
314                        .map(|device| device.query())
315                        .collect::<Vec<_>>(),
316                )
317            })
318            .filter(|(_, device_states)| !device_states.is_empty())
319            .map(|(id, mut device_state)| (id, device_state.remove(0)))
320            .collect::<HashMap<_, _>>();
321
322        fulfillment::response::query::Payload {
323            devices: device_states,
324            error_code: None,
325            debug_string: None,
326        }
327    }
328
329    /// SYNC all devices
330    #[instrument]
331    fn sync(&self) -> fulfillment::response::sync::Payload {
332        trace!("Running SYNC operation");
333        let devices = self.devices.iter().map(|x| x.sync()).collect::<Result<Vec<_>, Box<dyn Error>>>();
334
335        struct PayloadContent {
336            devices: Vec<fulfillment::response::sync::Device>,
337            error_code: Option<String>,
338            debug_string: Option<String>,
339        }
340
341        let content = match devices {
342            Ok(d) => PayloadContent {
343                devices: d,
344                error_code: None,
345                debug_string: None,
346            },
347            Err(e) => PayloadContent {
348                devices: Vec::with_capacity(0),
349                error_code: Some("deviceOffline".to_string()),
350                debug_string: Some(e.to_string()),
351            },
352        };
353
354        fulfillment::response::sync::Payload {
355            agent_user_id: self.agent_user_id.clone(),
356            devices: content.devices,
357            error_code: content.error_code,
358            debug_string: content.debug_string,
359        }
360    }
361
362    /// EXECUTE `command` on `device_id`
363    #[instrument]
364    fn execute(&mut self, device_id: &str, command: CommandType) -> Option<CommandOutput> {
365        trace!("Running EXECUTE intent");
366        let mut output = self
367            .devices
368            .iter_mut()
369            .filter(|x| x.id.eq(device_id))
370            .map(|device| device.execute(command.clone()))
371            .collect::<Vec<_>>();
372
373        if output.is_empty() {
374            None
375        } else {
376            Some(output.remove(0))
377        }
378    }
379}
380
381#[cfg(test)]
382mod test {
383    use crate::device_type::DeviceType;
384    use crate::traits::arm_disarm::{ArmDisarmError, ArmLevel};
385    use crate::traits::{DeviceInfo, DeviceName, GoogleHomeDevice};
386    use crate::{ArmDisarm, CommandType, Device, Homelander};
387
388    #[derive(Clone, Debug)]
389    struct Foo;
390
391    impl GoogleHomeDevice for Foo {
392        fn get_device_info(&self) -> DeviceInfo {
393            DeviceInfo {
394                manufacturer: String::default(),
395                model: String::default(),
396                hw: String::default(),
397                sw: String::default(),
398            }
399        }
400
401        fn will_report_state(&self) -> bool {
402            false
403        }
404
405        fn get_device_name(&self) -> DeviceName {
406            DeviceName {
407                nicknames: Vec::new(),
408                default_names: Vec::new(),
409                name: String::default(),
410            }
411        }
412
413        fn is_online(&self) -> bool {
414            true
415        }
416
417        fn disconnect(&mut self) {}
418    }
419
420    impl ArmDisarm for Foo {
421        fn get_available_arm_levels(&self) -> Result<Option<Vec<ArmLevel>>, ArmDisarmError> {
422            Ok(None)
423        }
424
425        fn is_ordered(&self) -> Result<bool, ArmDisarmError> {
426            Ok(true)
427        }
428
429        fn is_armed(&self) -> Result<bool, ArmDisarmError> {
430            Ok(true)
431        }
432
433        fn current_arm_level(&self) -> Result<String, ArmDisarmError> {
434            Ok(String::default())
435        }
436
437        fn exit_allowance(&self) -> Result<i32, ArmDisarmError> {
438            Ok(0)
439        }
440
441        fn arm(&mut self, _arm: bool) -> Result<(), ArmDisarmError> {
442            Ok(())
443        }
444
445        fn cancel_arm(&mut self) -> Result<(), ArmDisarmError> {
446            Ok(())
447        }
448
449        fn arm_with_level(&mut self, _arm: bool, _level: String) -> Result<(), ArmDisarmError> {
450            Ok(())
451        }
452    }
453
454    #[test]
455    fn add_device() {
456        let mut device = Device::new(Foo, DeviceType::AcUnit, String::default());
457        device.set_arm_disarm();
458
459        let mut homelander = Homelander::new(String::default());
460        homelander.add_device(device);
461    }
462
463    #[test]
464    fn test_dynamic_traits() {
465        let mut device = Device::new(Foo, DeviceType::AcUnit, String::default());
466        device.set_arm_disarm();
467        device.execute(CommandType::ArmDisarm {
468            arm: true,
469            follow_up_token: None,
470            cancel: None,
471            arm_level: None,
472        });
473    }
474}