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}