From a0cefa83027bf62a6380d7aee1701e4fa8a8cace Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Wed, 18 Jan 2023 22:37:57 +0100 Subject: [PATCH] Improved error handling --- Cargo.lock | 10 +-- Cargo.toml | 3 +- google-home/Cargo.toml | 1 - google-home/src/fullfillment.rs | 17 ++-- google-home/src/lib.rs | 1 + src/config.rs | 32 +++---- src/devices.rs | 34 +++++--- src/devices/audio_setup.rs | 17 ++-- src/devices/contact_sensor.rs | 4 +- src/devices/ikea_outlet.rs | 4 +- src/devices/kasa_outlet.rs | 46 ++++++++-- src/devices/wake_on_lan.rs | 10 +-- src/error.rs | 145 +++++++++++--------------------- src/light_sensor.rs | 4 +- src/main.rs | 5 +- src/mqtt.rs | 36 +++++--- src/presence.rs | 4 +- 17 files changed, 189 insertions(+), 184 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0aa82ba..483407b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", + "thiserror", "tokio", "toml", "tracing", @@ -334,7 +335,6 @@ dependencies = [ name = "google-home" version = "0.1.0" dependencies = [ - "anyhow", "impl_cast", "serde", "serde_json", @@ -1017,18 +1017,18 @@ checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 54b9196..b9d7a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ paste = "1.0.10" tokio = { version = "1", features = ["rt-multi-thread"] } toml = "0.5.10" 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 axum = "0.6.1" serde_repr = "0.1.10" @@ -32,6 +31,8 @@ async-trait = "0.1.61" async-recursion = "1.0.0" futures = "0.3.25" eui48 = { version = "1.1.0", default-features = false, features = ["disp_hexstring", "serde"] } +thiserror = "1.0.38" +anyhow = "1.0.68" [profile.release] lto=true diff --git a/google-home/Cargo.toml b/google-home/Cargo.toml index cd89f18..db9aaa0 100644 --- a/google-home/Cargo.toml +++ b/google-home/Cargo.toml @@ -6,7 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.66" impl_cast = { path = "../impl_cast" } serde = { version ="1.0.149", features = ["derive"] } serde_json = "1.0.89" diff --git a/google-home/src/fullfillment.rs b/google-home/src/fullfillment.rs index bb525b6..6f32ef1 100644 --- a/google-home/src/fullfillment.rs +++ b/google-home/src/fullfillment.rs @@ -1,5 +1,7 @@ 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}}; #[derive(Debug)] @@ -8,12 +10,18 @@ pub struct GoogleHome { // 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 { pub fn new(user_id: &str) -> Self { Self { user_id: user_id.into() } } - pub fn handle_request(&self, request: Request, mut devices: &mut HashMap<&str, &mut dyn GoogleHomeDevice>) -> Result { + pub fn handle_request(&self, request: Request, mut devices: &mut HashMap<&str, &mut dyn GoogleHomeDevice>) -> Result { // @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 let payload = request @@ -25,10 +33,9 @@ impl GoogleHome { Intent::Execute(payload) => ResponsePayload::Execute(self.execute(payload, &mut devices)), }).next(); - match payload { - Some(payload) => Ok(Response::new(&request.request_id, payload)), - _ => Err(anyhow::anyhow!("Expected at least one ResponsePayload")), - } + payload + .ok_or(FullfillmentError::ExpectedOnePayload) + .map(|payload| Response::new(&request.request_id, payload)) } fn sync(&self, devices: &HashMap<&str, &mut dyn GoogleHomeDevice>) -> sync::Payload { diff --git a/google-home/src/lib.rs b/google-home/src/lib.rs index fd218c4..df6f3d7 100644 --- a/google-home/src/lib.rs +++ b/google-home/src/lib.rs @@ -11,6 +11,7 @@ pub mod errors; mod attributes; pub use fullfillment::GoogleHome; +pub use fullfillment::FullfillmentError; pub use request::Request; pub use response::Response; pub use device::GoogleHomeDevice; diff --git a/src/config.rs b/src/config.rs index 674017d..2fa96e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use rumqttc::{AsyncClient, has_wildcards}; use serde::Deserialize; 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)] pub struct Config { @@ -183,10 +183,9 @@ pub enum Device { } impl Config { - pub fn parse_file(filename: &str) -> Result { + pub fn parse_file(filename: &str) -> Result { debug!("Loading config: {filename}"); - let file = fs::read_to_string(filename) - .map_err(|err| FailedToParseConfig::new(filename, err.into()))?; + let file = fs::read_to_string(filename)?; // Substitute in environment variables let re = Regex::new(r"\$\{(.*)\}").unwrap(); @@ -203,11 +202,9 @@ impl Config { } }); - missing.has_missing() - .map_err(|err| FailedToParseConfig::new(filename, err.into()))?; + missing.has_missing()?; - let config: Config = toml::from_str(&file) - .map_err(|err| FailedToParseConfig::new(filename, err.into()))?; + let config: Config = toml::from_str(&file)?; Ok(config) } @@ -223,21 +220,21 @@ fn device_box(device: T) -> DeviceBox { impl Device { #[async_recursion] - pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result { - let device: Result = match self { + pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result { + let device = match self { Device::IkeaOutlet { info, mqtt, kettle } => { trace!(id = identifier, "IkeaOutlet [{} in {:?}]", info.name, info.room); IkeaOutlet::build(&identifier, info, mqtt, kettle, client).await - .map(device_box) + .map(device_box)? }, Device::WakeOnLAN { info, mqtt, mac_address } => { trace!(id = identifier, "WakeOnLan [{} in {:?}]", info.name, info.room); WakeOnLAN::build(&identifier, info, mqtt, mac_address, client).await - .map(device_box) + .map(device_box)? }, Device::KasaOutlet { ip } => { trace!(id = identifier, "KasaOutlet [{}]", identifier); - Ok(Box::new(KasaOutlet::new(&identifier, ip))) + device_box(KasaOutlet::new(&identifier, ip)) } Device::AudioSetup { mqtt, mixer, speakers } => { trace!(id = identifier, "AudioSetup [{}]", identifier); @@ -248,20 +245,19 @@ impl Device { let speakers = (*speakers).create(&speakers_id, config, client.clone()).await?; AudioSetup::build(&identifier, mqtt, mixer, speakers, client).await - .map(device_box) + .map(device_box)? }, Device::ContactSensor { mqtt, presence } => { trace!(id = identifier, "ContactSensor [{}]", identifier); let presence = presence .map(|p| p.generate_topic("contact", &identifier, &config)) - .transpose() - .map_err(|err| FailedToCreateDevice::new(&identifier, err.into()))?; + .transpose()?; ContactSensor::build(&identifier, mqtt, presence, client).await - .map(device_box) + .map(device_box)? }, }; - return device.map_err(|err| FailedToCreateDevice::new(&identifier, err)); + Ok(device) } } diff --git a/src/devices.rs b/src/devices.rs index 92eb511..aa1f7ba 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -12,13 +12,14 @@ pub use self::contact_sensor::ContactSensor; use std::collections::HashMap; +use thiserror::Error; use async_trait::async_trait; -use google_home::{GoogleHomeDevice, traits::OnOff, GoogleHome}; +use google_home::{GoogleHomeDevice, traits::OnOff, GoogleHome, FullfillmentError, }; use pollster::FutureExt; use tokio::sync::{oneshot, mpsc}; 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, OnPresence); @@ -52,11 +53,11 @@ macro_rules! get_cast { } #[derive(Debug)] -enum Command { +pub enum Command { Fullfillment { google_home: GoogleHome, payload: google_home::Request, - tx: oneshot::Sender>, + tx: oneshot::Sender>, }, AddDevice { device: DeviceBox, @@ -67,26 +68,37 @@ enum Command { pub type DeviceBox = Box; #[derive(Clone)] -pub struct DeviceHandle { +pub struct DevicesHandle { tx: mpsc::Sender } -impl DeviceHandle { +#[derive(Debug, Error)] +pub enum DevicesError { + #[error(transparent)] + FullfillmentError(#[from] FullfillmentError), + #[error(transparent)] + SendError(#[from] tokio::sync::mpsc::error::SendError), + #[error(transparent)] + RecvError(#[from] tokio::sync::oneshot::error::RecvError), +} + + +impl DevicesHandle { // @TODO Improve error type - pub async fn fullfillment(&self, google_home: GoogleHome, payload: google_home::Request) -> anyhow::Result { + pub async fn fullfillment(&self, google_home: GoogleHome, payload: google_home::Request) -> Result { let (tx, rx) = oneshot::channel(); 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(); self.tx.send(Command::AddDevice { device, tx }).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() }; @@ -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 { diff --git a/src/devices/audio_setup.rs b/src/devices/audio_setup.rs index 5245c08..0242e6e 100644 --- a/src/devices/audio_setup.rs +++ b/src/devices/audio_setup.rs @@ -4,7 +4,7 @@ use rumqttc::{AsyncClient, matches}; use tracing::{error, warn, debug}; use crate::config::MqttDeviceConfig; -use crate::error; +use crate::error::DeviceError; use crate::mqtt::{OnMqtt, RemoteMessage, RemoteAction}; use crate::presence::OnPresence; @@ -21,16 +21,13 @@ pub struct AudioSetup { } impl AudioSetup { - pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, mixer: DeviceBox, speakers: DeviceBox, client: AsyncClient) -> error::Result { + pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, mixer: DeviceBox, speakers: DeviceBox, client: AsyncClient) -> Result { // We expect the children devices to implement the OnOff trait - let mixer = match AsOnOff::consume(mixer) { - Some(mixer) => mixer, - None => Err(error::ExpectedOnOff::new(&(identifier.to_owned() + ".mixer")))?, - }; - let speakers = match AsOnOff::consume(speakers) { - Some(speakers) => speakers, - None => Err(error::ExpectedOnOff::new(&(identifier.to_owned() + ".speakers")))?, - }; + let mixer = AsOnOff::consume(mixer) + .ok_or_else(|| DeviceError::OnOffExpected(identifier.to_owned() + ".mixer"))?; + + let speakers = AsOnOff::consume(speakers) + .ok_or_else(|| DeviceError::OnOffExpected(identifier.to_owned() + ".speakers"))?; client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index bc34638..a4f41e3 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -5,7 +5,7 @@ use rumqttc::{AsyncClient, matches}; use tokio::task::JoinHandle; 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; @@ -22,7 +22,7 @@ pub struct ContactSensor { } impl ContactSensor { - pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, presence: Option, client: AsyncClient) -> error::Result { + pub async fn build(identifier: &str, mqtt: MqttDeviceConfig, presence: Option, client: AsyncClient) -> Result { client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; Ok(Self { diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index 63c45cf..32f2b24 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -9,7 +9,7 @@ use pollster::FutureExt as _; use crate::config::{KettleConfig, InfoConfig, MqttDeviceConfig}; use crate::devices::Device; -use crate::error; +use crate::error::DeviceError; use crate::mqtt::{OnMqtt, OnOffMessage}; use crate::presence::OnPresence; @@ -26,7 +26,7 @@ pub struct IkeaOutlet { } impl IkeaOutlet { - pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option, client: AsyncClient) -> error::Result { + pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, kettle: Option, client: AsyncClient) -> Result { // @TODO Handle potential errors here client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; diff --git a/src/devices/kasa_outlet.rs b/src/devices/kasa_outlet.rs index 5e847e4..93a449e 100644 --- a/src/devices/kasa_outlet.rs +++ b/src/devices/kasa_outlet.rs @@ -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 google_home::{traits, errors::{self, DeviceError}}; use serde::{Serialize, Deserialize}; @@ -89,9 +90,9 @@ struct ErrorCode { } impl ErrorCode { - fn ok(&self) -> Result<(), anyhow::Error> { + fn ok(&self) -> Result<(), ResponseError> { if self.err_code != 0 { - Err(anyhow::anyhow!("Error code: {}", self.err_code)) + Err(ResponseError::ErrorCode(self.err_code)) } else { Ok(()) } @@ -122,28 +123,55 @@ struct Response { 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), +} + +impl From for ResponseError { + fn from(err: Utf8Error) -> Self { + ResponseError::Other(err.into()) + } +} + +impl From for ResponseError { + fn from(err: serde_json::Error) -> Self { + ResponseError::Other(err.into()) + } +} + impl Response { - fn get_current_relay_state(&self) -> Result { + fn get_current_relay_state(&self) -> Result { if let Some(sysinfo) = &self.system.get_sysinfo { return sysinfo.err_code.ok() .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 { 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 { + fn decrypt(mut data: bytes::Bytes) -> Result { let mut key: u8 = 171; 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(); diff --git a/src/devices/wake_on_lan.rs b/src/devices/wake_on_lan.rs index 1175fb6..3bef4ce 100644 --- a/src/devices/wake_on_lan.rs +++ b/src/devices/wake_on_lan.rs @@ -1,11 +1,11 @@ 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 rumqttc::{AsyncClient, Publish, matches}; use pollster::FutureExt as _; 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; @@ -18,7 +18,7 @@ pub struct WakeOnLAN { } impl WakeOnLAN { - pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, client: AsyncClient) -> error::Result { + pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, client: AsyncClient) -> Result { // @TODO Handle potential errors here client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; @@ -89,7 +89,7 @@ impl traits::Scene for WakeOnLAN { Ok(res) => res, Err(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 { debug!(id = self.identifier, "Trying to deactive computer, this is not currently supported"); // We do not support deactivating this scene - Err(ErrorCode::DeviceError(DeviceError::ActionNotAvailable)) + Err(ErrorCode::DeviceError(google_home::errors::DeviceError::ActionNotAvailable)) } } } diff --git a/src/error.rs b/src/error.rs index a7290b9..f16cdfd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,10 @@ use std::{fmt, error, result}; +use rumqttc::ClientError; +use thiserror::Error; use axum::{response::IntoResponse, http::status::InvalidStatusCode}; use serde::{Serialize, Deserialize}; -pub type Error = Box; -pub type Result = result::Result; - #[derive(Debug, Clone)] pub struct MissingEnv { keys: Vec @@ -51,9 +50,19 @@ impl fmt::Display 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 -#[derive(Debug, Clone)] +#[derive(Debug, Error)] +#[error("Topic '{topic}' is expected to be a wildcard topic")] pub struct MissingWildcard { topic: String } @@ -64,118 +73,64 @@ impl MissingWildcard { } } -impl fmt::Display for MissingWildcard { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Topic '{}' is exptected to be a wildcard topic", self.topic) - } +#[derive(Debug, Error)] +pub enum DeviceError { + #[error(transparent)] + SubscribeError(#[from] ClientError), + #[error("Expected device '{0}' to implement OnOff trait")] + OnOffExpected(String) } -impl error::Error for MissingWildcard {} - - -#[derive(Debug)] -pub struct FailedToParseConfig { - config: String, - cause: Error, +#[derive(Debug, Error)] +pub enum DeviceCreationError { + #[error(transparent)] + DeviceError(#[from] DeviceError), + #[error(transparent)] + MissingWildcard(#[from] MissingWildcard), } -impl FailedToParseConfig { - pub fn new(config: &str, cause: Error) -> Self { - Self { config: config.to_owned(), cause } - } +#[derive(Debug, Error)] +pub enum PresenceError { + #[error(transparent)] + SubscribeError(#[from] ClientError), + #[error(transparent)] + MissingWildcard(#[from] MissingWildcard), } -impl fmt::Display for FailedToParseConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Failed to parse config '{}'", self.config) - } +#[derive(Debug, Error)] +pub enum LightSensorError { + #[error(transparent)] + SubscribeError(#[from] ClientError), } -impl error::Error for FailedToParseConfig { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - 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)] +#[derive(Debug, Error)] +#[error("{source}")] pub struct ApiError { status_code: axum::http::StatusCode, - error: Error, + source: Box, } impl ApiError { - pub fn new(status_code: axum::http::StatusCode, error: Error) -> Self { - Self { status_code, error } + pub fn new(status_code: axum::http::StatusCode, source: Box) -> Self { + Self { status_code, source } } +} - pub fn prepare_for_json(&self) -> ApiErrorJson { +impl From for ApiErrorJson { + fn from(value: ApiError) -> Self { let error = ApiErrorJsonError { - code: self.status_code.as_u16(), - status: self.status_code.to_string(), - reason: self.error.to_string(), + code: value.status_code.as_u16(), + status: value.status_code.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 { 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::(&self.into()).unwrap()).into_response() } } @@ -196,8 +151,8 @@ impl TryFrom for ApiError { fn try_from(value: ApiErrorJson) -> result::Result { 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 }) } } diff --git a/src/light_sensor.rs b/src/light_sensor.rs index 75ea3cf..7eb5dfb 100644 --- a/src/light_sensor.rs +++ b/src/light_sensor.rs @@ -3,7 +3,7 @@ use rumqttc::{matches, AsyncClient}; use tokio::sync::watch; 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] pub trait OnDarkness { @@ -28,7 +28,7 @@ impl LightSensor { } } -pub async fn start(mut mqtt_rx: mqtt::Receiver, config: LightSensorConfig, client: AsyncClient) -> error::Result { +pub async fn start(mut mqtt_rx: mqtt::Receiver, config: LightSensorConfig, client: AsyncClient) -> Result { client.subscribe(config.mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?; let mut light_sensor = LightSensor::new(config.mqtt, config.min, config.max); diff --git a/src/main.rs b/src/main.rs index 261bb0f..fc8ba80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,7 @@ async fn main() { } -async fn app() -> Result<(), Box> { +async fn app() -> anyhow::Result<()> { dotenv().ok(); let filter = EnvFilter::builder() @@ -86,7 +86,8 @@ async fn app() -> Result<(), Box> { let identifier = identifier; let device = device_config.create(&identifier, &config, client.clone()).await?; devices.add_device(device).await?; - Ok::<(), Box>(()) + // We don't need a seperate error type in main + anyhow::Ok(()) }) ).await.into_iter().collect::>()?; diff --git a/src/mqtt.rs b/src/mqtt.rs index bba28fa..557b5a0 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -1,5 +1,7 @@ use std::time::{UNIX_EPOCH, SystemTime}; +use bytes::Bytes; +use thiserror::Error; use async_trait::async_trait; use serde::{Serialize, Deserialize}; use tracing::{debug, warn}; @@ -20,6 +22,12 @@ pub struct Mqtt { eventloop: EventLoop, } +#[derive(Debug, Error)] +pub enum ParseError { + #[error("Invalid message payload received: {0:?}")] + InvalidPayload(Bytes), +} + impl Mqtt { pub fn new(eventloop: EventLoop) -> Self { let (tx, _rx) = broadcast::channel(100); @@ -67,11 +75,11 @@ impl OnOffMessage { } impl TryFrom<&Publish> for OnOffMessage { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { 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 { - type Error = anyhow::Error; + type Error = ParseError; fn try_from(message: &Publish) -> Result { serde_json::from_slice(&message.payload) - .or(Err(anyhow::anyhow!("Invalid message payload received: {:?}", message.payload))) + .or(Err(ParseError::InvalidPayload(message.payload.clone()))) } } diff --git a/src/presence.rs b/src/presence.rs index a5e0b30..d4de5fb 100644 --- a/src/presence.rs +++ b/src/presence.rs @@ -5,7 +5,7 @@ use tokio::sync::watch; use tracing::{debug, error}; 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] pub trait OnPresence { @@ -33,7 +33,7 @@ impl Presence { } } -pub async fn start(mqtt: MqttDeviceConfig, mut mqtt_rx: mqtt::Receiver, client: AsyncClient) -> error::Result { +pub async fn start(mqtt: MqttDeviceConfig, mut mqtt_rx: mqtt::Receiver, client: AsyncClient) -> Result { // Subscribe to the relevant topics on mqtt client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;