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_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",

View File

@ -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

View File

@ -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"

View File

@ -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<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
// 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 {

View File

@ -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;

View File

@ -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<Self, FailedToParseConfig> {
pub fn parse_file(filename: &str) -> Result<Self, ConfigParseError> {
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<T: devices::Device + 'static>(device: T) -> DeviceBox {
impl Device {
#[async_recursion]
pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, FailedToCreateDevice> {
let device: Result<DeviceBox, Error> = match self {
pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, DeviceCreationError> {
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)
}
}

View File

@ -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<anyhow::Result<google_home::Response>>,
tx: oneshot::Sender<Result<google_home::Response, FullfillmentError>>,
},
AddDevice {
device: DeviceBox,
@ -67,26 +68,37 @@ enum Command {
pub type DeviceBox = Box<dyn Device>;
#[derive(Clone)]
pub struct DeviceHandle {
pub struct DevicesHandle {
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
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();
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 {

View File

@ -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<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
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?;

View File

@ -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<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?;
Ok(Self {

View File

@ -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<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
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 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<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 {
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 {
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<Self, anyhow::Error> {
fn decrypt(mut data: bytes::Bytes) -> Result<Self, ResponseError> {
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();

View File

@ -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<Self> {
pub async fn build(identifier: &str, info: InfoConfig, mqtt: MqttDeviceConfig, mac_address: MacAddress, client: AsyncClient) -> Result<Self, DeviceError> {
// @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))
}
}
}

View File

@ -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<dyn error::Error>;
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, Clone)]
pub struct MissingEnv {
keys: Vec<String>
@ -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<dyn std::error::Error>,
}
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<dyn std::error::Error>) -> Self {
Self { status_code, source }
}
}
pub fn prepare_for_json(&self) -> ApiErrorJson {
impl From<ApiError> 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::<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> {
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 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<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?;
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();
let filter = EnvFilter::builder()
@ -86,7 +86,8 @@ async fn app() -> Result<(), Box<dyn std::error::Error>> {
let identifier = identifier;
let device = device_config.create(&identifier, &config, client.clone()).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<_, _>>()?;

View File

@ -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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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<Self, Self::Error> {
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 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<Receiver> {
pub async fn start(mqtt: MqttDeviceConfig, mut mqtt_rx: mqtt::Receiver, client: AsyncClient) -> Result<Receiver, PresenceError> {
// Subscribe to the relevant topics on mqtt
client.subscribe(mqtt.topic.clone(), rumqttc::QoS::AtLeastOnce).await?;