Improved error handling
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing

This commit is contained in:
Dreaded_X 2023-01-18 22:37:57 +01:00
parent b6bf8a82a2
commit a0cefa8302
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
17 changed files with 189 additions and 184 deletions

10
Cargo.lock generated
View File

@ -67,6 +67,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"thiserror",
"tokio", "tokio",
"toml", "toml",
"tracing", "tracing",
@ -334,7 +335,6 @@ dependencies = [
name = "google-home" name = "google-home"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"impl_cast", "impl_cast",
"serde", "serde",
"serde_json", "serde_json",
@ -1017,18 +1017,18 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.37" version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -19,7 +19,6 @@ paste = "1.0.10"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
toml = "0.5.10" toml = "0.5.10"
dotenvy = "0.15.0" dotenvy = "0.15.0"
anyhow = "1.0.68"
reqwest = { version = "0.11.13", features = ["json", "rustls-tls"], default-features = false } # Use rustls, since the other packages also use rustls reqwest = { version = "0.11.13", features = ["json", "rustls-tls"], default-features = false } # Use rustls, since the other packages also use rustls
axum = "0.6.1" axum = "0.6.1"
serde_repr = "0.1.10" serde_repr = "0.1.10"
@ -32,6 +31,8 @@ async-trait = "0.1.61"
async-recursion = "1.0.0" async-recursion = "1.0.0"
futures = "0.3.25" futures = "0.3.25"
eui48 = { version = "1.1.0", default-features = false, features = ["disp_hexstring", "serde"] } eui48 = { version = "1.1.0", default-features = false, features = ["disp_hexstring", "serde"] }
thiserror = "1.0.38"
anyhow = "1.0.68"
[profile.release] [profile.release]
lto=true lto=true

View File

@ -6,7 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
anyhow = "1.0.66"
impl_cast = { path = "../impl_cast" } impl_cast = { path = "../impl_cast" }
serde = { version ="1.0.149", features = ["derive"] } serde = { version ="1.0.149", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"

View File

@ -1,5 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use thiserror::Error;
use crate::{request::{Request, Intent, self}, device::GoogleHomeDevice, response::{sync, ResponsePayload, query, execute, Response, self, State}, errors::{DeviceError, ErrorCode}}; use crate::{request::{Request, Intent, self}, device::GoogleHomeDevice, response::{sync, ResponsePayload, query, execute, Response, self, State}, errors::{DeviceError, ErrorCode}};
#[derive(Debug)] #[derive(Debug)]
@ -8,12 +10,18 @@ pub struct GoogleHome {
// Add credentials so we can notify google home of actions // Add credentials so we can notify google home of actions
} }
#[derive(Debug, Error)]
pub enum FullfillmentError {
#[error("Expected at least one ResponsePayload")]
ExpectedOnePayload
}
impl GoogleHome { impl GoogleHome {
pub fn new(user_id: &str) -> Self { pub fn new(user_id: &str) -> Self {
Self { user_id: user_id.into() } Self { user_id: user_id.into() }
} }
pub fn handle_request(&self, request: Request, mut devices: &mut HashMap<&str, &mut dyn GoogleHomeDevice>) -> Result<Response, anyhow::Error> { pub fn handle_request(&self, request: Request, mut devices: &mut HashMap<&str, &mut dyn GoogleHomeDevice>) -> Result<Response, FullfillmentError> {
// @TODO What do we do if we actually get more then one thing in the input array, right now // @TODO What do we do if we actually get more then one thing in the input array, right now
// we only respond to the first thing // we only respond to the first thing
let payload = request let payload = request
@ -25,10 +33,9 @@ impl GoogleHome {
Intent::Execute(payload) => ResponsePayload::Execute(self.execute(payload, &mut devices)), Intent::Execute(payload) => ResponsePayload::Execute(self.execute(payload, &mut devices)),
}).next(); }).next();
match payload { payload
Some(payload) => Ok(Response::new(&request.request_id, payload)), .ok_or(FullfillmentError::ExpectedOnePayload)
_ => Err(anyhow::anyhow!("Expected at least one ResponsePayload")), .map(|payload| Response::new(&request.request_id, payload))
}
} }
fn sync(&self, devices: &HashMap<&str, &mut dyn GoogleHomeDevice>) -> sync::Payload { fn sync(&self, devices: &HashMap<&str, &mut dyn GoogleHomeDevice>) -> sync::Payload {

View File

@ -11,6 +11,7 @@ pub mod errors;
mod attributes; mod attributes;
pub use fullfillment::GoogleHome; pub use fullfillment::GoogleHome;
pub use fullfillment::FullfillmentError;
pub use request::Request; pub use request::Request;
pub use response::Response; pub use response::Response;
pub use device::GoogleHomeDevice; pub use device::GoogleHomeDevice;

View File

@ -7,7 +7,7 @@ use rumqttc::{AsyncClient, has_wildcards};
use serde::Deserialize; use serde::Deserialize;
use eui48::MacAddress; use eui48::MacAddress;
use crate::{devices::{DeviceBox, IkeaOutlet, WakeOnLAN, AudioSetup, ContactSensor, KasaOutlet, self}, error::{FailedToParseConfig, MissingEnv, MissingWildcard, Error, FailedToCreateDevice}}; use crate::{devices::{DeviceBox, IkeaOutlet, WakeOnLAN, AudioSetup, ContactSensor, KasaOutlet, self}, error::{MissingEnv, MissingWildcard, ConfigParseError, DeviceCreationError}};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
@ -183,10 +183,9 @@ pub enum Device {
} }
impl Config { impl Config {
pub fn parse_file(filename: &str) -> Result<Self, FailedToParseConfig> { pub fn parse_file(filename: &str) -> Result<Self, ConfigParseError> {
debug!("Loading config: {filename}"); debug!("Loading config: {filename}");
let file = fs::read_to_string(filename) let file = fs::read_to_string(filename)?;
.map_err(|err| FailedToParseConfig::new(filename, err.into()))?;
// Substitute in environment variables // Substitute in environment variables
let re = Regex::new(r"\$\{(.*)\}").unwrap(); let re = Regex::new(r"\$\{(.*)\}").unwrap();
@ -203,11 +202,9 @@ impl Config {
} }
}); });
missing.has_missing() missing.has_missing()?;
.map_err(|err| FailedToParseConfig::new(filename, err.into()))?;
let config: Config = toml::from_str(&file) let config: Config = toml::from_str(&file)?;
.map_err(|err| FailedToParseConfig::new(filename, err.into()))?;
Ok(config) Ok(config)
} }
@ -223,21 +220,21 @@ fn device_box<T: devices::Device + 'static>(device: T) -> DeviceBox {
impl Device { impl Device {
#[async_recursion] #[async_recursion]
pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, FailedToCreateDevice> { pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, DeviceCreationError> {
let device: Result<DeviceBox, Error> = match self { let device = match self {
Device::IkeaOutlet { info, mqtt, kettle } => { Device::IkeaOutlet { info, mqtt, kettle } => {
trace!(id = identifier, "IkeaOutlet [{} in {:?}]", info.name, info.room); trace!(id = identifier, "IkeaOutlet [{} in {:?}]", info.name, info.room);
IkeaOutlet::build(&identifier, info, mqtt, kettle, client).await IkeaOutlet::build(&identifier, info, mqtt, kettle, client).await
.map(device_box) .map(device_box)?
}, },
Device::WakeOnLAN { info, mqtt, mac_address } => { Device::WakeOnLAN { info, mqtt, mac_address } => {
trace!(id = identifier, "WakeOnLan [{} in {:?}]", info.name, info.room); trace!(id = identifier, "WakeOnLan [{} in {:?}]", info.name, info.room);
WakeOnLAN::build(&identifier, info, mqtt, mac_address, client).await WakeOnLAN::build(&identifier, info, mqtt, mac_address, client).await
.map(device_box) .map(device_box)?
}, },
Device::KasaOutlet { ip } => { Device::KasaOutlet { ip } => {
trace!(id = identifier, "KasaOutlet [{}]", identifier); trace!(id = identifier, "KasaOutlet [{}]", identifier);
Ok(Box::new(KasaOutlet::new(&identifier, ip))) device_box(KasaOutlet::new(&identifier, ip))
} }
Device::AudioSetup { mqtt, mixer, speakers } => { Device::AudioSetup { mqtt, mixer, speakers } => {
trace!(id = identifier, "AudioSetup [{}]", identifier); trace!(id = identifier, "AudioSetup [{}]", identifier);
@ -248,20 +245,19 @@ impl Device {
let speakers = (*speakers).create(&speakers_id, config, client.clone()).await?; let speakers = (*speakers).create(&speakers_id, config, client.clone()).await?;
AudioSetup::build(&identifier, mqtt, mixer, speakers, client).await AudioSetup::build(&identifier, mqtt, mixer, speakers, client).await
.map(device_box) .map(device_box)?
}, },
Device::ContactSensor { mqtt, presence } => { Device::ContactSensor { mqtt, presence } => {
trace!(id = identifier, "ContactSensor [{}]", identifier); trace!(id = identifier, "ContactSensor [{}]", identifier);
let presence = presence let presence = presence
.map(|p| p.generate_topic("contact", &identifier, &config)) .map(|p| p.generate_topic("contact", &identifier, &config))
.transpose() .transpose()?;
.map_err(|err| FailedToCreateDevice::new(&identifier, err.into()))?;
ContactSensor::build(&identifier, mqtt, presence, client).await ContactSensor::build(&identifier, mqtt, presence, client).await
.map(device_box) .map(device_box)?
}, },
}; };
return device.map_err(|err| FailedToCreateDevice::new(&identifier, err)); Ok(device)
} }
} }

View File

@ -12,13 +12,14 @@ pub use self::contact_sensor::ContactSensor;
use std::collections::HashMap; use std::collections::HashMap;
use thiserror::Error;
use async_trait::async_trait; use async_trait::async_trait;
use google_home::{GoogleHomeDevice, traits::OnOff, GoogleHome}; use google_home::{GoogleHomeDevice, traits::OnOff, GoogleHome, FullfillmentError, };
use pollster::FutureExt; use pollster::FutureExt;
use tokio::sync::{oneshot, mpsc}; use tokio::sync::{oneshot, mpsc};
use tracing::{trace, debug, span, Level}; use tracing::{trace, debug, span, Level};
use crate::{mqtt::{OnMqtt, self}, presence::{OnPresence, self}, light_sensor::{OnDarkness, self}, error}; use crate::{mqtt::{OnMqtt, self}, presence::{OnPresence, self}, light_sensor::{OnDarkness, self}};
impl_cast::impl_cast!(Device, OnMqtt); impl_cast::impl_cast!(Device, OnMqtt);
impl_cast::impl_cast!(Device, OnPresence); impl_cast::impl_cast!(Device, OnPresence);
@ -52,11 +53,11 @@ macro_rules! get_cast {
} }
#[derive(Debug)] #[derive(Debug)]
enum Command { pub enum Command {
Fullfillment { Fullfillment {
google_home: GoogleHome, google_home: GoogleHome,
payload: google_home::Request, payload: google_home::Request,
tx: oneshot::Sender<anyhow::Result<google_home::Response>>, tx: oneshot::Sender<Result<google_home::Response, FullfillmentError>>,
}, },
AddDevice { AddDevice {
device: DeviceBox, device: DeviceBox,
@ -67,26 +68,37 @@ enum Command {
pub type DeviceBox = Box<dyn Device>; pub type DeviceBox = Box<dyn Device>;
#[derive(Clone)] #[derive(Clone)]
pub struct DeviceHandle { pub struct DevicesHandle {
tx: mpsc::Sender<Command> tx: mpsc::Sender<Command>
} }
impl DeviceHandle { #[derive(Debug, Error)]
pub enum DevicesError {
#[error(transparent)]
FullfillmentError(#[from] FullfillmentError),
#[error(transparent)]
SendError(#[from] tokio::sync::mpsc::error::SendError<Command>),
#[error(transparent)]
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
}
impl DevicesHandle {
// @TODO Improve error type // @TODO Improve error type
pub async fn fullfillment(&self, google_home: GoogleHome, payload: google_home::Request) -> anyhow::Result<google_home::Response> { pub async fn fullfillment(&self, google_home: GoogleHome, payload: google_home::Request) -> Result<google_home::Response, DevicesError> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.tx.send(Command::Fullfillment { google_home, payload, tx }).await?; self.tx.send(Command::Fullfillment { google_home, payload, tx }).await?;
rx.await? Ok(rx.await??)
} }
pub async fn add_device(&self, device: DeviceBox) -> error::Result<()> { pub async fn add_device(&self, device: DeviceBox) -> Result<(), DevicesError> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.tx.send(Command::AddDevice { device, tx }).await?; self.tx.send(Command::AddDevice { device, tx }).await?;
Ok(rx.await?) Ok(rx.await?)
} }
} }
pub fn start(mut mqtt_rx: mqtt::Receiver, mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver) -> DeviceHandle { pub fn start(mut mqtt_rx: mqtt::Receiver, mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver) -> DevicesHandle {
let mut devices = Devices { devices: HashMap::new() }; let mut devices = Devices { devices: HashMap::new() };
@ -114,7 +126,7 @@ pub fn start(mut mqtt_rx: mqtt::Receiver, mut presence_rx: presence::Receiver, m
} }
}); });
return DeviceHandle { tx }; return DevicesHandle { tx };
} }
impl Devices { impl Devices {

View File

@ -4,7 +4,7 @@ use rumqttc::{AsyncClient, matches};
use tracing::{error, warn, debug}; use tracing::{error, warn, debug};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::error; use crate::error::DeviceError;
use crate::mqtt::{OnMqtt, RemoteMessage, RemoteAction}; use crate::mqtt::{OnMqtt, RemoteMessage, RemoteAction};
use crate::presence::OnPresence; use crate::presence::OnPresence;
@ -21,16 +21,13 @@ pub struct AudioSetup {
} }
impl AudioSetup { impl AudioSetup {
pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, mixer: DeviceBox, speakers: DeviceBox, client: AsyncClient) -> error::Result<Self> { 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 // We expect the children devices to implement the OnOff trait
let mixer = match AsOnOff::consume(mixer) { let mixer = AsOnOff::consume(mixer)
Some(mixer) => mixer, .ok_or_else(|| DeviceError::OnOffExpected(identifier.to_owned() + ".mixer"))?;
None => Err(error::ExpectedOnOff::new(&(identifier.to_owned() + ".mixer")))?,
}; let speakers = AsOnOff::consume(speakers)
let speakers = match AsOnOff::consume(speakers) { .ok_or_else(|| DeviceError::OnOffExpected(identifier.to_owned() + ".speakers"))?;
Some(speakers) => speakers,
None => Err(error::ExpectedOnOff::new(&(identifier.to_owned() + ".speakers")))?,
};
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;

View File

@ -5,7 +5,7 @@ use rumqttc::{AsyncClient, matches};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{error, debug, warn}; use tracing::{error, debug, warn};
use crate::{config::{MqttDeviceConfig, PresenceDeviceConfig}, mqtt::{OnMqtt, ContactMessage, PresenceMessage}, presence::OnPresence, error}; use crate::{config::{MqttDeviceConfig, PresenceDeviceConfig}, mqtt::{OnMqtt, ContactMessage, PresenceMessage}, presence::OnPresence, error::DeviceError};
use super::Device; use super::Device;
@ -22,7 +22,7 @@ pub struct ContactSensor {
} }
impl ContactSensor { impl ContactSensor {
pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, presence: Option<PresenceDeviceConfig>, client: AsyncClient) -> error::Result<Self> { 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?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
Ok(Self { Ok(Self {

View File

@ -9,7 +9,7 @@ use pollster::FutureExt as _;
use crate::config::{KettleConfig, InfoConfig, MqttDeviceConfig}; use crate::config::{KettleConfig, InfoConfig, MqttDeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error; use crate::error::DeviceError;
use crate::mqtt::{OnMqtt, OnOffMessage}; use crate::mqtt::{OnMqtt, OnOffMessage};
use crate::presence::OnPresence; use crate::presence::OnPresence;
@ -26,7 +26,7 @@ pub struct IkeaOutlet {
} }
impl IkeaOutlet { impl IkeaOutlet {
pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option<KettleConfig>, client: AsyncClient) -> error::Result<Self> { pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option<KettleConfig>, client: AsyncClient) -> Result<Self, DeviceError> {
// @TODO Handle potential errors here // @TODO Handle potential errors here
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;

View File

@ -1,5 +1,6 @@
use std::{net::{SocketAddr, Ipv4Addr, TcpStream}, io::{Write, Read}}; use std::{net::{SocketAddr, Ipv4Addr, TcpStream}, io::{Write, Read}, str::Utf8Error};
use thiserror::Error;
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::{traits, errors::{self, DeviceError}}; use google_home::{traits, errors::{self, DeviceError}};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -89,9 +90,9 @@ struct ErrorCode {
} }
impl ErrorCode { impl ErrorCode {
fn ok(&self) -> Result<(), anyhow::Error> { fn ok(&self) -> Result<(), ResponseError> {
if self.err_code != 0 { if self.err_code != 0 {
Err(anyhow::anyhow!("Error code: {}", self.err_code)) Err(ResponseError::ErrorCode(self.err_code))
} else { } else {
Ok(()) Ok(())
} }
@ -122,28 +123,55 @@ struct Response {
system: ResponseSystem, system: ResponseSystem,
} }
// @TODO Improve this error
#[derive(Debug, Error)]
enum ResponseError {
#[error("Expected a minimum data length of 4")]
ToShort,
#[error("No sysinfo found in response")]
SysinfoNotFound,
#[error("No relay_state not found in response")]
RelayStateNotFound,
#[error("Error code: {0}")]
ErrorCode(isize),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error>),
}
impl From<Utf8Error> for ResponseError {
fn from(err: Utf8Error) -> Self {
ResponseError::Other(err.into())
}
}
impl From<serde_json::Error> for ResponseError {
fn from(err: serde_json::Error) -> Self {
ResponseError::Other(err.into())
}
}
impl Response { impl Response {
fn get_current_relay_state(&self) -> Result<bool, anyhow::Error> { fn get_current_relay_state(&self) -> Result<bool, ResponseError> {
if let Some(sysinfo) = &self.system.get_sysinfo { if let Some(sysinfo) = &self.system.get_sysinfo {
return sysinfo.err_code.ok() return sysinfo.err_code.ok()
.map(|_| sysinfo.relay_state == 1); .map(|_| sysinfo.relay_state == 1);
} }
return Err(anyhow::anyhow!("No sysinfo found in response")); return Err(ResponseError::SysinfoNotFound);
} }
fn check_set_relay_success(&self) -> Result<(), anyhow::Error> { fn check_set_relay_success(&self) -> Result<(), ResponseError> {
if let Some(set_relay_state) = &self.system.set_relay_state { if let Some(set_relay_state) = &self.system.set_relay_state {
return set_relay_state.err_code.ok(); return set_relay_state.err_code.ok();
} }
return Err(anyhow::anyhow!("No relay_state found in response")); return Err(ResponseError::RelayStateNotFound);
} }
fn decrypt(mut data: bytes::Bytes) -> Result<Self, anyhow::Error> { fn decrypt(mut data: bytes::Bytes) -> Result<Self, ResponseError> {
let mut key: u8 = 171; let mut key: u8 = 171;
if data.len() < 4 { if data.len() < 4 {
return Err(anyhow::anyhow!("Expected a minimun data length of 4")); return Err(ResponseError::ToShort.into());
} }
let length = data.get_u32(); let length = data.get_u32();

View File

@ -1,11 +1,11 @@
use async_trait::async_trait; use async_trait::async_trait;
use google_home::{GoogleHomeDevice, types::Type, device, traits::{self, Scene}, errors::{ErrorCode, DeviceError}}; use google_home::{GoogleHomeDevice, types::Type, device, traits::{self, Scene}, errors::ErrorCode};
use tracing::{debug, error}; use tracing::{debug, error};
use rumqttc::{AsyncClient, Publish, matches}; use rumqttc::{AsyncClient, Publish, matches};
use pollster::FutureExt as _; use pollster::FutureExt as _;
use eui48::MacAddress; use eui48::MacAddress;
use crate::{config::{InfoConfig, MqttDeviceConfig}, mqtt::{OnMqtt, ActivateMessage}, error}; use crate::{config::{InfoConfig, MqttDeviceConfig}, mqtt::{OnMqtt, ActivateMessage}, error::DeviceError};
use super::Device; use super::Device;
@ -18,7 +18,7 @@ pub struct WakeOnLAN {
} }
impl WakeOnLAN { impl WakeOnLAN {
pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, client: AsyncClient) -> error::Result<Self> { pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, client: AsyncClient) -> Result<Self, DeviceError> {
// @TODO Handle potential errors here // @TODO Handle potential errors here
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
@ -89,7 +89,7 @@ impl traits::Scene for WakeOnLAN {
Ok(res) => res, Ok(res) => res,
Err(err) => { Err(err) => {
error!(id, "Failed to call webhook: {err}"); error!(id, "Failed to call webhook: {err}");
return Err(DeviceError::TransientError.into()); return Err(google_home::errors::DeviceError::TransientError.into());
} }
}; };
@ -102,7 +102,7 @@ impl traits::Scene for WakeOnLAN {
} else { } 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 // We do not support deactivating this scene
Err(ErrorCode::DeviceError(DeviceError::ActionNotAvailable)) Err(ErrorCode::DeviceError(google_home::errors::DeviceError::ActionNotAvailable))
} }
} }
} }

View File

@ -1,11 +1,10 @@
use std::{fmt, error, result}; use std::{fmt, error, result};
use rumqttc::ClientError;
use thiserror::Error;
use axum::{response::IntoResponse, http::status::InvalidStatusCode}; use axum::{response::IntoResponse, http::status::InvalidStatusCode};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
pub type Error = Box<dyn error::Error>;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MissingEnv { pub struct MissingEnv {
keys: Vec<String> keys: Vec<String>
@ -51,9 +50,19 @@ impl fmt::Display for MissingEnv {
impl error::Error for MissingEnv {} impl error::Error for MissingEnv {}
#[derive(Debug, Error)]
pub enum ConfigParseError {
#[error(transparent)]
MissingEnv(#[from] MissingEnv),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
DeserializeError(#[from] toml::de::Error)
}
// @TODO Would be nice to somehow get the line number of the expected wildcard topic // @TODO Would be nice to somehow get the line number of the expected wildcard topic
#[derive(Debug, Clone)] #[derive(Debug, Error)]
#[error("Topic '{topic}' is expected to be a wildcard topic")]
pub struct MissingWildcard { pub struct MissingWildcard {
topic: String topic: String
} }
@ -64,118 +73,64 @@ impl MissingWildcard {
} }
} }
impl fmt::Display for MissingWildcard { #[derive(Debug, Error)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { pub enum DeviceError {
write!(f, "Topic '{}' is exptected to be a wildcard topic", self.topic) #[error(transparent)]
} SubscribeError(#[from] ClientError),
#[error("Expected device '{0}' to implement OnOff trait")]
OnOffExpected(String)
} }
impl error::Error for MissingWildcard {} #[derive(Debug, Error)]
pub enum DeviceCreationError {
#[error(transparent)]
#[derive(Debug)] DeviceError(#[from] DeviceError),
pub struct FailedToParseConfig { #[error(transparent)]
config: String, MissingWildcard(#[from] MissingWildcard),
cause: Error,
} }
impl FailedToParseConfig { #[derive(Debug, Error)]
pub fn new(config: &str, cause: Error) -> Self { pub enum PresenceError {
Self { config: config.to_owned(), cause } #[error(transparent)]
} SubscribeError(#[from] ClientError),
#[error(transparent)]
MissingWildcard(#[from] MissingWildcard),
} }
impl fmt::Display for FailedToParseConfig { #[derive(Debug, Error)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { pub enum LightSensorError {
write!(f, "Failed to parse config '{}'", self.config) #[error(transparent)]
} SubscribeError(#[from] ClientError),
} }
impl error::Error for FailedToParseConfig { #[derive(Debug, Error)]
fn source(&self) -> Option<&(dyn error::Error + 'static)> { #[error("{source}")]
Some(self.cause.as_ref())
}
}
#[derive(Debug)]
pub struct FailedToCreateDevice {
device: String,
cause: Error,
}
impl FailedToCreateDevice {
pub fn new(device: &str, cause: Error) -> Self {
Self { device: device.to_owned(), cause }
}
}
impl fmt::Display for FailedToCreateDevice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Failed to create device '{}'", self.device)
}
}
impl error::Error for FailedToCreateDevice {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
Some(self.cause.as_ref())
}
}
#[derive(Debug, Clone)]
pub struct ExpectedOnOff {
device: String
}
impl ExpectedOnOff {
pub fn new(device: &str) -> Self {
Self { device: device.to_owned() }
}
}
impl fmt::Display for ExpectedOnOff {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Expected device '{}' to implement OnOff trait", self.device)
}
}
impl error::Error for ExpectedOnOff {}
#[derive(Debug)]
pub struct ApiError { pub struct ApiError {
status_code: axum::http::StatusCode, status_code: axum::http::StatusCode,
error: Error, source: Box<dyn std::error::Error>,
} }
impl ApiError { impl ApiError {
pub fn new(status_code: axum::http::StatusCode, error: Error) -> Self { pub fn new(status_code: axum::http::StatusCode, source: Box<dyn std::error::Error>) -> Self {
Self { status_code, error } Self { status_code, source }
} }
}
pub fn prepare_for_json(&self) -> ApiErrorJson { impl From<ApiError> for ApiErrorJson {
fn from(value: ApiError) -> Self {
let error = ApiErrorJsonError { let error = ApiErrorJsonError {
code: self.status_code.as_u16(), code: value.status_code.as_u16(),
status: self.status_code.to_string(), status: value.status_code.to_string(),
reason: self.error.to_string(), reason: value.source.to_string(),
}; };
ApiErrorJson { error } Self { error }
} }
} }
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.error.fmt(f)
}
}
impl error::Error for ApiError {}
impl IntoResponse for ApiError { impl IntoResponse for ApiError {
fn into_response(self) -> axum::response::Response { fn into_response(self) -> axum::response::Response {
(self.status_code, serde_json::to_string(&self.prepare_for_json()).unwrap()).into_response() (self.status_code, serde_json::to_string::<ApiErrorJson>(&self.into()).unwrap()).into_response()
} }
} }
@ -196,8 +151,8 @@ impl TryFrom<ApiErrorJson> for ApiError {
fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> { fn try_from(value: ApiErrorJson) -> result::Result<Self, Self::Error> {
let status_code = axum::http::StatusCode::from_u16(value.error.code)?; let status_code = axum::http::StatusCode::from_u16(value.error.code)?;
let error = value.error.reason.into(); let source = value.error.reason.into();
Ok(Self { status_code, error }) Ok(Self { status_code, source })
} }
} }

View File

@ -3,7 +3,7 @@ use rumqttc::{matches, AsyncClient};
use tokio::sync::watch; use tokio::sync::watch;
use tracing::{error, trace, debug}; use tracing::{error, trace, debug};
use crate::{config::{MqttDeviceConfig, LightSensorConfig}, mqtt::{self, OnMqtt, BrightnessMessage}, error}; use crate::{config::{MqttDeviceConfig, LightSensorConfig}, mqtt::{self, OnMqtt, BrightnessMessage}, error::{LightSensorError}};
#[async_trait] #[async_trait]
pub trait OnDarkness { pub trait OnDarkness {
@ -28,7 +28,7 @@ impl LightSensor {
} }
} }
pub async fn start(mut mqtt_rx: mqtt::Receiver, config: LightSensorConfig, client: AsyncClient) -> error::Result<Receiver> { pub async fn start(mut mqtt_rx: mqtt::Receiver, config: LightSensorConfig, client: AsyncClient) -> Result<Receiver, LightSensorError> {
client.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; client.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;
let mut light_sensor = LightSensor::new(config.mqtt, config.min, config.max); let mut light_sensor = LightSensor::new(config.mqtt, config.min, config.max);

View File

@ -45,7 +45,7 @@ async fn main() {
} }
async fn app() -> Result<(), Box<dyn std::error::Error>> { async fn app() -> anyhow::Result<()> {
dotenv().ok(); dotenv().ok();
let filter = EnvFilter::builder() let filter = EnvFilter::builder()
@ -86,7 +86,8 @@ async fn app() -> Result<(), Box<dyn std::error::Error>> {
let identifier = identifier; let identifier = identifier;
let device = device_config.create(&identifier, &config, client.clone()).await?; let device = device_config.create(&identifier, &config, client.clone()).await?;
devices.add_device(device).await?; devices.add_device(device).await?;
Ok::<(), Box<dyn std::error::Error>>(()) // We don't need a seperate error type in main
anyhow::Ok(())
}) })
).await.into_iter().collect::<Result<_, _>>()?; ).await.into_iter().collect::<Result<_, _>>()?;

View File

@ -1,5 +1,7 @@
use std::time::{UNIX_EPOCH, SystemTime}; use std::time::{UNIX_EPOCH, SystemTime};
use bytes::Bytes;
use thiserror::Error;
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -20,6 +22,12 @@ pub struct Mqtt {
eventloop: EventLoop, eventloop: EventLoop,
} }
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Invalid message payload received: {0:?}")]
InvalidPayload(Bytes),
}
impl Mqtt { impl Mqtt {
pub fn new(eventloop: EventLoop) -> Self { pub fn new(eventloop: EventLoop) -> Self {
let (tx, _rx) = broadcast::channel(100); let (tx, _rx) = broadcast::channel(100);
@ -67,11 +75,11 @@ impl OnOffMessage {
} }
impl TryFrom<&Publish> for OnOffMessage { impl TryFrom<&Publish> for OnOffMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -87,11 +95,11 @@ impl ActivateMessage {
} }
impl TryFrom<&Publish> for ActivateMessage { impl TryFrom<&Publish> for ActivateMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -117,11 +125,11 @@ impl RemoteMessage {
} }
impl TryFrom<&Publish> for RemoteMessage { impl TryFrom<&Publish> for RemoteMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -142,11 +150,11 @@ impl PresenceMessage {
} }
impl TryFrom<&Publish> for PresenceMessage { impl TryFrom<&Publish> for PresenceMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -162,11 +170,11 @@ impl BrightnessMessage {
} }
impl TryFrom<&Publish> for BrightnessMessage { impl TryFrom<&Publish> for BrightnessMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -182,11 +190,11 @@ impl ContactMessage {
} }
impl TryFrom<&Publish> for ContactMessage { impl TryFrom<&Publish> for ContactMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }
@ -207,11 +215,11 @@ impl DarknessMessage {
} }
impl TryFrom<&Publish> for DarknessMessage { impl TryFrom<&Publish> for DarknessMessage {
type Error = anyhow::Error; type Error = ParseError;
fn try_from(message: &Publish) -> Result<Self, Self::Error> { fn try_from(message: &Publish) -> Result<Self, Self::Error> {
serde_json::from_slice(&message.payload) serde_json::from_slice(&message.payload)
.or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) .or(Err(ParseError::InvalidPayload(message.payload.clone())))
} }
} }

View File

@ -5,7 +5,7 @@ use tokio::sync::watch;
use tracing::{debug, error}; use tracing::{debug, error};
use rumqttc::{AsyncClient, matches, has_wildcards}; use rumqttc::{AsyncClient, matches, has_wildcards};
use crate::{mqtt::{OnMqtt, PresenceMessage, self}, config::MqttDeviceConfig, error::{self, MissingWildcard}}; use crate::{mqtt::{OnMqtt, PresenceMessage, self}, config::MqttDeviceConfig, error::{MissingWildcard, PresenceError}};
#[async_trait] #[async_trait]
pub trait OnPresence { pub trait OnPresence {
@ -33,7 +33,7 @@ impl Presence {
} }
} }
pub async fn start(mqtt: MqttDeviceConfig, mut mqtt_rx: mqtt::Receiver, client: AsyncClient) -> error::Result<Receiver> { pub async fn start(mqtt: MqttDeviceConfig, mut mqtt_rx: mqtt::Receiver, client: AsyncClient) -> Result<Receiver, PresenceError> {
// Subscribe to the relevant topics on mqtt // Subscribe to the relevant topics on mqtt
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;