Improved error handling
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
145
src/error.rs
145
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<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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<_, _>>()?;
|
||||
|
||||
|
||||
36
src/mqtt.rs
36
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<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())))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user