Improved error handling
This commit is contained in:
parent
b6bf8a82a2
commit
a0cefa8302
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user