Finished basic google home implementation with some slight refactors along the way
This commit is contained in:
@@ -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<T: $i> [< 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<String, Box<dyn Device>>,
|
||||
}
|
||||
|
||||
macro_rules! get_cast {
|
||||
($trait:ident) => {
|
||||
paste::paste! {
|
||||
pub fn [< as_ $trait:snake s >](&mut self) -> HashMap<String, &mut dyn $trait> {
|
||||
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<T: Device + 'static>(&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);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<bool, ErrorCode> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
31
src/devices/test_outlet.rs
Normal file
31
src/devices/test_outlet.rs
Normal file
@@ -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<bool, ErrorCode> {
|
||||
Ok(self.on)
|
||||
}
|
||||
|
||||
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
|
||||
println!("Setting on: {on}");
|
||||
self.on = on;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#![feature(specialization)]
|
||||
pub mod devices;
|
||||
pub mod zigbee;
|
||||
mod state;
|
||||
pub mod mqtt;
|
||||
|
||||
58
src/main.rs
58
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);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
pub trait StateOnOff {
|
||||
fn set_state(&mut self, state: bool);
|
||||
fn get_state(&self) -> bool;
|
||||
}
|
||||
Reference in New Issue
Block a user