More refactoring

This commit is contained in:
Dreaded_X 2023-08-18 03:07:16 +02:00
parent 3134891751
commit 044c38ba86
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
20 changed files with 209 additions and 105 deletions

5
Cargo.lock generated
View File

@ -43,9 +43,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.72" version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
@ -554,6 +554,7 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
name = "google-home" name = "google-home"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"async-trait", "async-trait",
"futures", "futures",
"impl_cast", "impl_cast",

View File

@ -13,3 +13,4 @@ thiserror = "1.0.37"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
async-trait = "0.1.61" async-trait = "0.1.61"
futures = "0.3.25" futures = "0.3.25"
anyhow = "1.0.75"

View File

@ -195,9 +195,8 @@ impl GoogleHome {
join_all(f).await; join_all(f).await;
// We await all the futures that use resp_payload so try_unwrap should never fail
std::sync::Arc::<tokio::sync::Mutex<response::execute::Payload>>::try_unwrap(resp_payload) std::sync::Arc::<tokio::sync::Mutex<response::execute::Payload>>::try_unwrap(resp_payload)
.unwrap() .expect("All futures are done, so there should only be one strong reference")
.into_inner() .into_inner()
} }
} }
@ -331,11 +330,11 @@ impl GoogleHome {
// let mut lamp = TestOutlet::new("living/lamp"); // let mut lamp = TestOutlet::new("living/lamp");
// let mut scene = TestScene::new(); // let mut scene = TestScene::new();
// let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new(); // let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new();
// let id = nightstand.get_id().to_owned(); // let id = nightstand.get_id().into();
// devices.insert(&id, &mut nightstand); // devices.insert(&id, &mut nightstand);
// let id = lamp.get_id().to_owned(); // let id = lamp.get_id().into();
// devices.insert(&id, &mut lamp); // devices.insert(&id, &mut lamp);
// let id = scene.get_id().to_owned(); // let id = scene.get_id().into();
// devices.insert(&id, &mut scene); // devices.insert(&id, &mut scene);
// //
// let resp = gh.handle_request(req, &mut devices).unwrap(); // let resp = gh.handle_request(req, &mut devices).unwrap();
@ -374,11 +373,11 @@ impl GoogleHome {
// let mut lamp = TestOutlet::new("living/lamp"); // let mut lamp = TestOutlet::new("living/lamp");
// let mut scene = TestScene::new(); // let mut scene = TestScene::new();
// let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new(); // let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new();
// let id = nightstand.get_id().to_owned(); // let id = nightstand.get_id().into();
// devices.insert(&id, &mut nightstand); // devices.insert(&id, &mut nightstand);
// let id = lamp.get_id().to_owned(); // let id = lamp.get_id().into();
// devices.insert(&id, &mut lamp); // devices.insert(&id, &mut lamp);
// let id = scene.get_id().to_owned(); // let id = scene.get_id().into();
// devices.insert(&id, &mut scene); // devices.insert(&id, &mut scene);
// //
// let resp = gh.handle_request(req, &mut devices).unwrap(); // let resp = gh.handle_request(req, &mut devices).unwrap();
@ -429,11 +428,11 @@ impl GoogleHome {
// let mut lamp = TestOutlet::new("living/lamp"); // let mut lamp = TestOutlet::new("living/lamp");
// let mut scene = TestScene::new(); // let mut scene = TestScene::new();
// let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new(); // let mut devices: HashMap<&str, &mut dyn GoogleHomeDevice> = HashMap::new();
// let id = nightstand.get_id().to_owned(); // let id = nightstand.get_id().into();
// devices.insert(&id, &mut nightstand); // devices.insert(&id, &mut nightstand);
// let id = lamp.get_id().to_owned(); // let id = lamp.get_id().into();
// devices.insert(&id, &mut lamp); // devices.insert(&id, &mut lamp);
// let id = scene.get_id().to_owned(); // let id = scene.get_id().into();
// devices.insert(&id, &mut scene); // devices.insert(&id, &mut scene);
// //
// let resp = gh.handle_request(req, &mut devices).unwrap(); // let resp = gh.handle_request(req, &mut devices).unwrap();

View File

@ -83,7 +83,7 @@ mod tests {
assert_eq!( assert_eq!(
req.request_id, req.request_id,
"ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_owned() "ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_string()
); );
assert_eq!(req.inputs.len(), 1); assert_eq!(req.inputs.len(), 1);
match &req.inputs[0] { match &req.inputs[0] {

View File

@ -54,7 +54,7 @@ mod tests {
assert_eq!( assert_eq!(
req.request_id, req.request_id,
"ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_owned() "ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_string()
); );
assert_eq!(req.inputs.len(), 1); assert_eq!(req.inputs.len(), 1);
match &req.inputs[0] { match &req.inputs[0] {

View File

@ -19,7 +19,7 @@ mod tests {
assert_eq!( assert_eq!(
req.request_id, req.request_id,
"ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_owned() "ff36a3cc-ec34-11e6-b1a0-64510650abcf".to_string()
); );
assert_eq!(req.inputs.len(), 1); assert_eq!(req.inputs.len(), 1);
match req.inputs[0] { match req.inputs[0] {

View File

@ -14,7 +14,7 @@ pub struct Response {
impl Response { impl Response {
pub fn new(request_id: &str, payload: ResponsePayload) -> Self { pub fn new(request_id: &str, payload: ResponsePayload) -> Self {
Self { Self {
request_id: request_id.to_owned(), request_id: request_id.into(),
payload, payload,
} }
} }

View File

@ -86,10 +86,10 @@ mod tests {
device.room_hint = Some("kitchen".into()); device.room_hint = Some("kitchen".into());
device.device_info = Some(device::Info { device.device_info = Some(device::Info {
manufacturer: Some("lights-out-inc".to_string()), manufacturer: Some("lights-out-inc".into()),
model: Some("hs1234".to_string()), model: Some("hs1234".into()),
hw_version: Some("3.2".to_string()), hw_version: Some("3.2".into()),
sw_version: Some("11.4".to_string()), sw_version: Some("11.4".into()),
}); });
sync_resp.add_device(device); sync_resp.add_device(device);

View File

@ -122,16 +122,16 @@ impl Config {
let file = fs::read_to_string(filename)?; let file = fs::read_to_string(filename)?;
// Substitute in environment variables // Substitute in environment variables
let re = Regex::new(r"\$\{(.*)\}").unwrap(); let re = Regex::new(r"\$\{(.*)\}").expect("Regex should be valid");
let mut missing = MissingEnv::new(); let mut missing = MissingEnv::new();
let file = re.replace_all(&file, |caps: &Captures| { let file = re.replace_all(&file, |caps: &Captures| {
let key = caps.get(1).unwrap().as_str(); let key = caps.get(1).expect("Capture group should exist").as_str();
debug!("Substituting '{key}' in config"); debug!("Substituting '{key}' in config");
match std::env::var(key) { match std::env::var(key) {
Ok(value) => value, Ok(value) => value,
Err(_) => { Err(_) => {
missing.add_missing(key); missing.add_missing(key);
"".to_string() "".into()
} }
} }
}); });

View File

@ -91,7 +91,7 @@ impl DeviceManager {
} }
pub async fn add(&self, device: Box<dyn Device>) { pub async fn add(&self, device: Box<dyn Device>) {
let id = device.get_id().to_owned(); let id = device.get_id().into();
debug!(id, "Adding device"); debug!(id, "Adding device");

View File

@ -178,7 +178,7 @@ impl OnMqtt for ContactSensor {
if trigger.timeout.is_zero() && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) { if trigger.timeout.is_zero() && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
light.set_on(false).await.ok(); light.set_on(false).await.ok();
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) { } else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) {
light.start_timeout(trigger.timeout).await; light.start_timeout(trigger.timeout).await.unwrap();
} }
// TODO: Put a warning/error on creation if either of this has to option to fail // TODO: Put a warning/error on creation if either of this has to option to fail
} }

View File

@ -60,7 +60,7 @@ impl OnPresence for DebugBridge {
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
true, true,
serde_json::to_string(&message).unwrap(), serde_json::to_string(&message).expect("Serialization should not fail"),
) )
.await .await
.map_err(|err| { .map_err(|err| {

View File

@ -3,11 +3,11 @@ use std::{
time::Duration, time::Duration,
}; };
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use google_home::{errors::ErrorCode, traits::OnOff}; use google_home::{errors::ErrorCode, traits::OnOff};
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use tracing::{error, warn};
use tracing::{debug, error, warn};
use crate::{ use crate::{
device_manager::{ConfigExternal, DeviceConfig}, device_manager::{ConfigExternal, DeviceConfig},
@ -53,6 +53,25 @@ struct HueLight {
pub timer_id: isize, pub timer_id: isize,
} }
// Couple of helper function to get the correct urls
impl HueLight {
fn url_base(&self) -> String {
format!("http://{}/api/{}", self.addr, self.login)
}
fn url_set_schedule(&self) -> String {
format!("{}/schedules/{}", self.url_base(), self.timer_id)
}
fn url_set_state(&self) -> String {
format!("{}/lights/{}/state", self.url_base(), self.light_id)
}
fn url_get_state(&self) -> String {
format!("{}/lights/{}", self.url_base(), self.light_id)
}
}
impl Device for HueLight { impl Device for HueLight {
fn get_id(&self) -> &str { fn get_id(&self) -> &str {
&self.identifier &self.identifier
@ -63,16 +82,12 @@ impl Device for HueLight {
impl OnOff for HueLight { impl OnOff for HueLight {
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await; self.stop_timeout().await.unwrap();
let url = format!(
"http://{}/api/{}/lights/{}/state",
self.addr, self.login, self.light_id
);
let message = message::State::new(on);
let res = reqwest::Client::new() let res = reqwest::Client::new()
.put(url) .put(self.url_set_state())
.body(format!(r#"{{"on": {}}}"#, on)) .json(&message)
.send() .send()
.await; .await;
@ -90,12 +105,10 @@ impl OnOff for HueLight {
} }
async fn is_on(&self) -> Result<bool, ErrorCode> { async fn is_on(&self) -> Result<bool, ErrorCode> {
let url = format!( let res = reqwest::Client::new()
"http://{}/api/{}/lights/{}", .get(self.url_get_state())
self.addr, self.login, self.light_id .send()
); .await;
let res = reqwest::Client::new().get(url).send().await;
match res { match res {
Ok(res) => { Ok(res) => {
@ -104,9 +117,16 @@ impl OnOff for HueLight {
warn!(id = self.identifier, "Status code is not success: {status}"); warn!(id = self.identifier, "Status code is not success: {status}");
} }
let v: Value = serde_json::from_slice(res.bytes().await.unwrap().as_ref()).unwrap(); let on = match res.json::<message::Info>().await {
// TODO: This is not very nice Ok(info) => info.is_on(),
return Ok(v["state"]["on"].as_bool().unwrap()); Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}");
// TODO: Error code
return Ok(false);
}
};
return Ok(on);
} }
Err(err) => error!(id = self.identifier, "Error: {err}"), Err(err) => error!(id = self.identifier, "Error: {err}"),
} }
@ -117,59 +137,109 @@ impl OnOff for HueLight {
#[async_trait] #[async_trait]
impl Timeout for HueLight { impl Timeout for HueLight {
async fn start_timeout(&mut self, timeout: Duration) { async fn start_timeout(&mut self, timeout: Duration) -> Result<()> {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await; self.stop_timeout().await?;
let url = format!(
"http://{}/api/{}/schedules/{}",
self.addr, self.login, self.timer_id
);
let seconds = timeout.as_secs() % 60;
let minutes = (timeout.as_secs() / 60) % 60;
let hours = timeout.as_secs() / 3600;
let time = format!("PT{hours:<02}:{minutes:<02}:{seconds:<02}");
debug!(id = self.identifier, "Starting timeout ({time})...");
let message = message::Timeout::new(Some(timeout));
let res = reqwest::Client::new() let res = reqwest::Client::new()
.put(url) .put(self.url_set_schedule())
.body(format!(r#"{{"status": "enabled", "localtime": "{time}"}}"#)) .json(&message)
.send() .send()
.await; .await
.context("Failed to start timeout")?;
match res { let status = res.status();
Ok(res) => { if !status.is_success() {
let status = res.status(); return Err(anyhow!(
if !status.is_success() { "Hue bridge returned unsuccessful status '{status}'"
warn!(id = self.identifier, "Status code is not success: {status}"); ));
} }
}
Err(err) => error!(id = self.identifier, "Error: {err}"), Ok(())
}
async fn stop_timeout(&mut self) -> Result<()> {
let message = message::Timeout::new(None);
let res = reqwest::Client::new()
.put(self.url_set_schedule())
.json(&message)
.send()
.await
.context("Failed to stop timeout")?;
let status = res.status();
if !status.is_success() {
return Err(anyhow!(
"Hue bridge returned unsuccessful status '{status}'"
));
}
Ok(())
}
}
mod message {
use std::time::Duration;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct State {
on: bool,
}
impl State {
pub fn new(on: bool) -> Self {
Self { on }
} }
} }
async fn stop_timeout(&mut self) { #[derive(Debug, Serialize, Deserialize)]
let url = format!( pub struct Info {
"http://{}/api/{}/schedules/{}", state: State,
self.addr, self.login, self.timer_id }
);
let res = reqwest::Client::new() impl Info {
.put(url) pub fn is_on(&self) -> bool {
.body(format!(r#"{{"status": "disabled"}}"#)) self.state.on
.send() }
.await; }
match res { #[derive(Debug)]
Ok(res) => { pub struct Timeout {
let status = res.status(); timeout: Option<Duration>,
if !status.is_success() { }
warn!(id = self.identifier, "Status code is not success: {status}");
} impl Timeout {
pub fn new(timeout: Option<Duration>) -> Self {
Self { timeout }
}
}
impl Serialize for Timeout {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let len = if self.timeout.is_some() { 2 } else { 1 };
let mut state = serializer.serialize_struct("TimerMessage", len)?;
if self.timeout.is_some() {
state.serialize_field("status", "enabled")?;
} else {
state.serialize_field("status", "disabled")?;
} }
Err(err) => error!(id = self.identifier, "Error: {err}"),
if let Some(timeout) = self.timeout {
let seconds = timeout.as_secs() % 60;
let minutes = (timeout.as_secs() / 60) % 60;
let hours = timeout.as_secs() / 3600;
let time = format!("PT{hours:<02}:{minutes:<02}:{seconds:<02}");
state.serialize_field("localtime", &time)?;
};
state.end()
} }
} }
} }

View File

@ -1,3 +1,4 @@
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::{ use google_home::{
@ -135,14 +136,14 @@ impl OnMqtt for IkeaOutlet {
} }
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await; self.stop_timeout().await.unwrap();
debug!(id = self.identifier, "Updating state to {state}"); debug!(id = self.identifier, "Updating state to {state}");
self.last_known_state = state; self.last_known_state = state;
// If this is a kettle start a timeout for turning it of again // If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.timeout { if state && let Some(timeout) = self.timeout {
self.start_timeout(timeout).await; self.start_timeout(timeout).await.unwrap();
} }
} }
} }
@ -205,9 +206,9 @@ impl traits::OnOff for IkeaOutlet {
#[async_trait] #[async_trait]
impl crate::traits::Timeout for IkeaOutlet { impl crate::traits::Timeout for IkeaOutlet {
async fn start_timeout(&mut self, timeout: Duration) { async fn start_timeout(&mut self, timeout: Duration) -> Result<()> {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await; self.stop_timeout().await?;
// Turn the kettle of after the specified timeout // Turn the kettle of after the specified timeout
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet // TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
@ -224,11 +225,15 @@ impl crate::traits::Timeout for IkeaOutlet {
// I don't think we can really get around calling outside function // I don't think we can really get around calling outside function
set_on(client, &topic, false).await; set_on(client, &topic, false).await;
})); }));
Ok(())
} }
async fn stop_timeout(&mut self) { async fn stop_timeout(&mut self) -> Result<()> {
if let Some(handle) = self.handle.take() { if let Some(handle) = self.handle.take() {
handle.abort(); handle.abort();
} }
Ok(())
} }
} }

View File

@ -81,17 +81,17 @@ impl Notification {
} }
pub fn set_title(mut self, title: &str) -> Self { pub fn set_title(mut self, title: &str) -> Self {
self.title = Some(title.to_owned()); self.title = Some(title.into());
self self
} }
pub fn set_message(mut self, message: &str) -> Self { pub fn set_message(mut self, message: &str) -> Self {
self.message = Some(message.to_owned()); self.message = Some(message.into());
self self
} }
pub fn add_tag(mut self, tag: &str) -> Self { pub fn add_tag(mut self, tag: &str) -> Self {
self.tags.push(tag.to_owned()); self.tags.push(tag.into());
self self
} }
@ -107,7 +107,7 @@ impl Notification {
fn finalize(self, topic: &str) -> NotificationFinal { fn finalize(self, topic: &str) -> NotificationFinal {
NotificationFinal { NotificationFinal {
topic: topic.to_owned(), topic: topic.into(),
inner: self, inner: self,
} }
} }
@ -168,7 +168,7 @@ impl OnPresence for Ntfy {
// Create broadcast action // Create broadcast action
let action = Action { let action = Action {
action: ActionType::Broadcast { extras }, action: ActionType::Broadcast { extras },
label: if presence { "Set away" } else { "Set home" }.to_owned(), label: if presence { "Set away" } else { "Set home" }.into(),
clear: Some(true), clear: Some(true),
}; };

View File

@ -59,7 +59,7 @@ impl OnMqtt for Presence {
.find('+') .find('+')
.or(self.mqtt.topic.find('#')) .or(self.mqtt.topic.find('#'))
.expect("Presence::create fails if it does not contain wildcards"); .expect("Presence::create fails if it does not contain wildcards");
let device_name = message.topic[offset..].to_owned(); let device_name = message.topic[offset..].into();
if message.payload.is_empty() { if message.payload.is_empty() {
// Remove the device from the map // Remove the device from the map

View File

@ -18,7 +18,7 @@ impl MissingEnv {
} }
pub fn add_missing(&mut self, key: &str) { pub fn add_missing(&mut self, key: &str) {
self.keys.push(key.to_owned()); self.keys.push(key.into());
} }
pub fn has_missing(self) -> result::Result<(), Self> { pub fn has_missing(self) -> result::Result<(), Self> {
@ -84,7 +84,7 @@ pub struct MissingWildcard {
impl MissingWildcard { impl MissingWildcard {
pub fn new(topic: &str) -> Self { pub fn new(topic: &str) -> Self {
Self { Self {
topic: topic.to_owned(), topic: topic.into(),
} }
} }
} }
@ -145,7 +145,8 @@ impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
( (
self.status_code, self.status_code,
serde_json::to_string::<ApiErrorJson>(&self.into()).unwrap(), serde_json::to_string::<ApiErrorJson>(&self.into())
.expect("Serialization should not fail"),
) )
.into_response() .into_response()
} }

View File

@ -50,7 +50,7 @@ async fn app() -> anyhow::Result<()> {
info!("Starting automation_rs..."); info!("Starting automation_rs...");
let config_filename = let config_filename =
std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.toml".to_owned()); std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.toml".into());
let config = Config::parse_file(&config_filename)?; let config = Config::parse_file(&config_filename)?;
// Create a mqtt client // Create a mqtt client

View File

@ -1,5 +1,6 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use bytes::Bytes;
use rumqttc::Publish; use rumqttc::Publish;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -215,3 +216,28 @@ impl TryFrom<Publish> for PowerMessage {
.or(Err(ParseError::InvalidPayload(message.payload.clone()))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
// Message used to report the power state of a hue light
#[derive(Debug, Deserialize)]
pub struct HueState {
on: bool,
}
#[derive(Debug, Deserialize)]
pub struct HueMessage {
state: HueState,
}
impl HueMessage {
pub fn is_on(&self) -> bool {
self.state.on
}
}
impl TryFrom<Bytes> for HueMessage {
type Error = ParseError;
fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
serde_json::from_slice(&bytes).or(Err(ParseError::InvalidPayload(bytes.clone())))
}
}

View File

@ -1,11 +1,12 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait; use impl_cast::device_trait;
#[async_trait] #[async_trait]
#[device_trait] #[device_trait]
pub trait Timeout { pub trait Timeout {
async fn start_timeout(&mut self, _timeout: Duration); async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>;
async fn stop_timeout(&mut self); async fn stop_timeout(&mut self) -> Result<()>;
} }