This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use google_home::traits;
|
||||
use rumqttc::{AsyncClient, matches};
|
||||
use tracing::{error, warn, debug};
|
||||
use rumqttc::{matches, AsyncClient};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::error::DeviceError;
|
||||
use crate::mqtt::{OnMqtt, RemoteMessage, RemoteAction};
|
||||
use crate::mqtt::{OnMqtt, RemoteAction, RemoteMessage};
|
||||
use crate::presence::OnPresence;
|
||||
|
||||
use super::{Device, DeviceBox, AsOnOff};
|
||||
use super::{AsOnOff, Device, DeviceBox};
|
||||
|
||||
// TODO: Ideally we store am Arc to the childern devices,
|
||||
// that way they hook into everything just like all other devices
|
||||
@@ -21,19 +21,31 @@ pub struct AudioSetup {
|
||||
}
|
||||
|
||||
impl AudioSetup {
|
||||
pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, mixer: DeviceBox, speakers: DeviceBox, client: AsyncClient) -> Result<Self, DeviceError> {
|
||||
pub async fn build(
|
||||
identifier: &str,
|
||||
mqtt: MqttDeviceConfig,
|
||||
mixer: DeviceBox,
|
||||
speakers: DeviceBox,
|
||||
client: AsyncClient,
|
||||
) -> Result<Self, DeviceError> {
|
||||
// We expect the children devices to implement the OnOff trait
|
||||
let mixer_id = mixer.get_id().to_owned();
|
||||
let mixer = AsOnOff::consume(mixer)
|
||||
.ok_or_else(|| DeviceError::OnOffExpected(mixer_id))?;
|
||||
let mixer = AsOnOff::consume(mixer).ok_or_else(|| DeviceError::OnOffExpected(mixer_id))?;
|
||||
|
||||
let speakers_id = speakers.get_id().to_owned();
|
||||
let speakers = AsOnOff::consume(speakers)
|
||||
.ok_or_else(|| DeviceError::OnOffExpected(speakers_id))?;
|
||||
let speakers =
|
||||
AsOnOff::consume(speakers).ok_or_else(|| DeviceError::OnOffExpected(speakers_id))?;
|
||||
|
||||
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
|
||||
client
|
||||
.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { identifier: identifier.to_owned(), mqtt, mixer, speakers })
|
||||
Ok(Self {
|
||||
identifier: identifier.to_owned(),
|
||||
mqtt,
|
||||
mixer,
|
||||
speakers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rumqttc::{AsyncClient, matches};
|
||||
use rumqttc::{matches, AsyncClient};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{error, debug, warn};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::{config::{MqttDeviceConfig, PresenceDeviceConfig}, mqtt::{OnMqtt, ContactMessage, PresenceMessage}, presence::OnPresence, error::DeviceError};
|
||||
use crate::{
|
||||
config::{MqttDeviceConfig, PresenceDeviceConfig},
|
||||
error::DeviceError,
|
||||
mqtt::{ContactMessage, OnMqtt, PresenceMessage},
|
||||
presence::OnPresence,
|
||||
};
|
||||
|
||||
use super::Device;
|
||||
|
||||
@@ -22,8 +27,15 @@ pub struct ContactSensor {
|
||||
}
|
||||
|
||||
impl ContactSensor {
|
||||
pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, presence: Option<PresenceDeviceConfig>, client: AsyncClient) -> Result<Self, DeviceError> {
|
||||
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
|
||||
pub async fn build(
|
||||
identifier: &str,
|
||||
mqtt: MqttDeviceConfig,
|
||||
presence: Option<PresenceDeviceConfig>,
|
||||
client: AsyncClient,
|
||||
) -> Result<Self, DeviceError> {
|
||||
client
|
||||
.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
identifier: identifier.to_owned(),
|
||||
@@ -62,7 +74,7 @@ impl OnMqtt for ContactSensor {
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
return;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if is_closed == self.is_closed {
|
||||
@@ -97,7 +109,13 @@ impl OnMqtt for ContactSensor {
|
||||
// This is to prevent the house from being marked as present for however long the
|
||||
// timeout is set when leaving the house
|
||||
if !self.overall_presence {
|
||||
self.client.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&PresenceMessage::new(true)).unwrap())
|
||||
self.client
|
||||
.publish(
|
||||
topic.clone(),
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||
.ok();
|
||||
@@ -107,17 +125,16 @@ impl OnMqtt for ContactSensor {
|
||||
let client = self.client.clone();
|
||||
let id = self.identifier.clone();
|
||||
let timeout = Duration::from_secs(presence.timeout);
|
||||
self.handle = Some(
|
||||
tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?}) for contact sensor...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Removing door device!");
|
||||
client.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||
.ok();
|
||||
})
|
||||
);
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?}) for contact sensor...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Removing door device!");
|
||||
client
|
||||
.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::time::Duration;
|
||||
use async_trait::async_trait;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::{GoogleHomeDevice, device, types::Type, traits::{self, OnOff}};
|
||||
use rumqttc::{AsyncClient, Publish, matches};
|
||||
use tracing::{debug, error, warn};
|
||||
use tokio::task::JoinHandle;
|
||||
use google_home::{
|
||||
device,
|
||||
traits::{self, OnOff},
|
||||
types::Type,
|
||||
GoogleHomeDevice,
|
||||
};
|
||||
use pollster::FutureExt as _;
|
||||
use rumqttc::{matches, AsyncClient, Publish};
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig, OutletType};
|
||||
use crate::devices::Device;
|
||||
@@ -27,11 +32,29 @@ pub struct IkeaOutlet {
|
||||
}
|
||||
|
||||
impl IkeaOutlet {
|
||||
pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, outlet_type: OutletType, timeout: Option<u64>, client: AsyncClient) -> Result<Self, DeviceError> {
|
||||
pub async fn build(
|
||||
identifier: &str,
|
||||
info: InfoConfig,
|
||||
mqtt: MqttDeviceConfig,
|
||||
outlet_type: OutletType,
|
||||
timeout: Option<u64>,
|
||||
client: AsyncClient,
|
||||
) -> Result<Self, DeviceError> {
|
||||
// TODO: Handle potential errors here
|
||||
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
|
||||
client
|
||||
.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self{ identifier: identifier.to_owned(), info, mqtt, outlet_type, timeout, client, last_known_state: false, handle: None })
|
||||
Ok(Self {
|
||||
identifier: identifier.to_owned(),
|
||||
info,
|
||||
mqtt,
|
||||
outlet_type,
|
||||
timeout,
|
||||
client,
|
||||
last_known_state: false,
|
||||
handle: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +63,13 @@ async fn set_on(client: AsyncClient, topic: &str, on: bool) {
|
||||
|
||||
let topic = format!("{}/set", topic);
|
||||
// TODO: Handle potential errors here
|
||||
client.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, serde_json::to_string(&message).unwrap())
|
||||
client
|
||||
.publish(
|
||||
topic.clone(),
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
|
||||
.ok();
|
||||
@@ -94,17 +123,15 @@ impl OnMqtt for IkeaOutlet {
|
||||
let client = self.client.clone();
|
||||
let topic = self.mqtt.topic.clone();
|
||||
let id = self.identifier.clone();
|
||||
self.handle = Some(
|
||||
tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?}) for kettle...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Turning kettle off!");
|
||||
// TODO: Idealy we would call self.set_on(false), however since we want to do
|
||||
// it after a timeout we have to put it in a seperate task.
|
||||
// I don't think we can really get around calling outside function
|
||||
set_on(client, &topic, false).await;
|
||||
})
|
||||
);
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?}) for kettle...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Turning kettle off!");
|
||||
// TODO: Idealy we would call self.set_on(false), however since we want to do
|
||||
// it after a timeout we have to put it in a seperate task.
|
||||
// I don't think we can really get around calling outside function
|
||||
set_on(client, &topic, false).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::{net::{SocketAddr, Ipv4Addr, TcpStream}, io::{Write, Read}, str::Utf8Error};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::{Ipv4Addr, SocketAddr, TcpStream},
|
||||
str::Utf8Error,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
use bytes::{Buf, BufMut};
|
||||
use google_home::{traits, errors::{self, DeviceError}};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use google_home::{
|
||||
errors::{self, DeviceError},
|
||||
traits,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::Device;
|
||||
|
||||
@@ -15,7 +22,10 @@ pub struct KasaOutlet {
|
||||
|
||||
impl KasaOutlet {
|
||||
pub fn new(identifier: &str, ip: Ipv4Addr) -> Self {
|
||||
Self { identifier: identifier.to_owned(), addr: (ip, 9999).into() }
|
||||
Self {
|
||||
identifier: identifier.to_owned(),
|
||||
addr: (ip, 9999).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +60,9 @@ impl Request {
|
||||
fn get_sysinfo() -> Self {
|
||||
Self {
|
||||
system: RequestSystem {
|
||||
get_sysinfo: Some(RequestSysinfo{}),
|
||||
set_relay_state: None
|
||||
}
|
||||
get_sysinfo: Some(RequestSysinfo {}),
|
||||
set_relay_state: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,9 +71,9 @@ impl Request {
|
||||
system: RequestSystem {
|
||||
get_sysinfo: None,
|
||||
set_relay_state: Some(RequestRelayState {
|
||||
state: if on { 1 } else { 0 }
|
||||
})
|
||||
}
|
||||
state: if on { 1 } else { 0 },
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +163,7 @@ impl From<serde_json::Error> for ResponseError {
|
||||
impl Response {
|
||||
fn get_current_relay_state(&self) -> Result<bool, ResponseError> {
|
||||
if let Some(sysinfo) = &self.system.get_sysinfo {
|
||||
return sysinfo.err_code.ok()
|
||||
.map(|_| sysinfo.relay_state == 1);
|
||||
return sysinfo.err_code.ok().map(|_| sysinfo.relay_state == 1);
|
||||
}
|
||||
|
||||
Err(ResponseError::SysinfoNotFound)
|
||||
@@ -189,15 +198,21 @@ impl Response {
|
||||
|
||||
impl traits::OnOff for KasaOutlet {
|
||||
fn is_on(&self) -> Result<bool, errors::ErrorCode> {
|
||||
let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
let mut stream =
|
||||
TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
||||
let body = Request::get_sysinfo().encrypt();
|
||||
stream.write_all(&body).and(stream.flush()).or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
stream
|
||||
.write_all(&body)
|
||||
.and(stream.flush())
|
||||
.or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
|
||||
let mut received = Vec::new();
|
||||
let mut rx_bytes = [0; 1024];
|
||||
loop {
|
||||
let read = stream.read(&mut rx_bytes).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
let read = stream
|
||||
.read(&mut rx_bytes)
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
received.extend_from_slice(&rx_bytes[..read]);
|
||||
|
||||
@@ -206,16 +221,22 @@ impl traits::OnOff for KasaOutlet {
|
||||
}
|
||||
}
|
||||
|
||||
let resp = Response::decrypt(received.into()).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
let resp = Response::decrypt(received.into())
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
resp.get_current_relay_state().or(Err(DeviceError::TransientError.into()))
|
||||
resp.get_current_relay_state()
|
||||
.or(Err(DeviceError::TransientError.into()))
|
||||
}
|
||||
|
||||
fn set_on(&mut self, on: bool) -> Result<(), errors::ErrorCode> {
|
||||
let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
let mut stream =
|
||||
TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
||||
let body = Request::set_relay_state(on).encrypt();
|
||||
stream.write_all(&body).and(stream.flush()).or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
stream
|
||||
.write_all(&body)
|
||||
.and(stream.flush())
|
||||
.or::<DeviceError>(Err(DeviceError::TransientError))?;
|
||||
|
||||
let mut received = Vec::new();
|
||||
let mut rx_bytes = [0; 1024];
|
||||
@@ -232,9 +253,10 @@ impl traits::OnOff for KasaOutlet {
|
||||
}
|
||||
}
|
||||
|
||||
let resp = Response::decrypt(received.into()).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
let resp = Response::decrypt(received.into())
|
||||
.or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
|
||||
|
||||
resp.check_set_relay_success().or(Err(DeviceError::TransientError.into()))
|
||||
resp.check_set_relay_success()
|
||||
.or(Err(DeviceError::TransientError.into()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use google_home::{GoogleHomeDevice, types::Type, device, traits::{self, Scene}, errors::ErrorCode};
|
||||
use tracing::{debug, error};
|
||||
use rumqttc::{AsyncClient, Publish, matches};
|
||||
use eui48::MacAddress;
|
||||
use google_home::{
|
||||
device,
|
||||
errors::ErrorCode,
|
||||
traits::{self, Scene},
|
||||
types::Type,
|
||||
GoogleHomeDevice,
|
||||
};
|
||||
use rumqttc::{matches, AsyncClient, Publish};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{config::{InfoConfig, MqttDeviceConfig}, mqtt::{OnMqtt, ActivateMessage}, error::DeviceError};
|
||||
use crate::{
|
||||
config::{InfoConfig, MqttDeviceConfig},
|
||||
error::DeviceError,
|
||||
mqtt::{ActivateMessage, OnMqtt},
|
||||
};
|
||||
|
||||
use super::Device;
|
||||
|
||||
@@ -20,11 +30,26 @@ pub struct WakeOnLAN {
|
||||
}
|
||||
|
||||
impl WakeOnLAN {
|
||||
pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, broadcast_ip: Ipv4Addr, client: AsyncClient) -> Result<Self, DeviceError> {
|
||||
pub async fn build(
|
||||
identifier: &str,
|
||||
info: InfoConfig,
|
||||
mqtt: MqttDeviceConfig,
|
||||
mac_address: MacAddress,
|
||||
broadcast_ip: Ipv4Addr,
|
||||
client: AsyncClient,
|
||||
) -> Result<Self, DeviceError> {
|
||||
// TODO: Handle potential errors here
|
||||
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
|
||||
client
|
||||
.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
Ok(Self { identifier: identifier.to_owned(), info, mqtt, mac_address, broadcast_ip })
|
||||
Ok(Self {
|
||||
identifier: identifier.to_owned(),
|
||||
info,
|
||||
mqtt,
|
||||
mac_address,
|
||||
broadcast_ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,20 +106,31 @@ impl GoogleHomeDevice for WakeOnLAN {
|
||||
impl traits::Scene for WakeOnLAN {
|
||||
fn set_active(&self, activate: bool) -> Result<(), ErrorCode> {
|
||||
if activate {
|
||||
debug!(id = self.identifier, "Activating Computer: {} (Sending to {})", self.mac_address, self.broadcast_ip);
|
||||
let wol = wakey::WolPacket::from_bytes(&self.mac_address.to_array()).map_err(|err| {
|
||||
error!(id = self.identifier, "invalid mac address: {err}");
|
||||
google_home::errors::DeviceError::TransientError
|
||||
})?;
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
"Activating Computer: {} (Sending to {})", self.mac_address, self.broadcast_ip
|
||||
);
|
||||
let wol =
|
||||
wakey::WolPacket::from_bytes(&self.mac_address.to_array()).map_err(|err| {
|
||||
error!(id = self.identifier, "invalid mac address: {err}");
|
||||
google_home::errors::DeviceError::TransientError
|
||||
})?;
|
||||
|
||||
wol.send_magic_to((Ipv4Addr::new(0, 0, 0, 0), 0), (self.broadcast_ip, 9)).map_err(|err| {
|
||||
error!(id = self.identifier, "Failed to activate computer: {err}");
|
||||
google_home::errors::DeviceError::TransientError.into()
|
||||
}).map(|_| debug!(id = self.identifier, "Success!"))
|
||||
wol.send_magic_to((Ipv4Addr::new(0, 0, 0, 0), 0), (self.broadcast_ip, 9))
|
||||
.map_err(|err| {
|
||||
error!(id = self.identifier, "Failed to activate computer: {err}");
|
||||
google_home::errors::DeviceError::TransientError.into()
|
||||
})
|
||||
.map(|_| debug!(id = self.identifier, "Success!"))
|
||||
} else {
|
||||
debug!(id = self.identifier, "Trying to deactive computer, this is not currently supported");
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
"Trying to deactive computer, this is not currently supported"
|
||||
);
|
||||
// We do not support deactivating this scene
|
||||
Err(ErrorCode::DeviceError(google_home::errors::DeviceError::ActionNotAvailable))
|
||||
Err(ErrorCode::DeviceError(
|
||||
google_home::errors::DeviceError::ActionNotAvailable,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user