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

This commit is contained in:
Dreaded_X 2023-01-18 20:03:33 +01:00
parent d36a6eb518
commit b6bf8a82a2
Signed by: Dreaded_X
GPG Key ID: 76BDEC4E165D8AD9
8 changed files with 110 additions and 103 deletions

View File

@ -35,16 +35,16 @@ pub trait GoogleHomeDevice: AsOnOff + AsScene {
let mut traits = Vec::new(); let mut traits = Vec::new();
// OnOff // OnOff
if let Some(d) = AsOnOff::cast(self) { if let Some(on_off) = AsOnOff::cast(self) {
traits.push(Trait::OnOff); traits.push(Trait::OnOff);
device.attributes.command_only_on_off = d.is_command_only(); device.attributes.command_only_on_off = on_off.is_command_only();
device.attributes.query_only_on_off = d.is_query_only(); device.attributes.query_only_on_off = on_off.is_query_only();
} }
// Scene // Scene
if let Some(d) = AsScene::cast(self) { if let Some(scene) = AsScene::cast(self) {
traits.push(Trait::Scene); traits.push(Trait::Scene);
device.attributes.scene_reversible = d.is_scene_reversible(); device.attributes.scene_reversible = scene.is_scene_reversible();
} }
device.traits = traits; device.traits = traits;
@ -59,11 +59,10 @@ pub trait GoogleHomeDevice: AsOnOff + AsScene {
} }
// OnOff // OnOff
if let Some(d) = AsOnOff::cast(self) { if let Some(on_off) = AsOnOff::cast(self) {
match d.is_on() { device.state.on = on_off.is_on()
Ok(state) => device.state.on = Some(state), .map_err(|err| device.set_error(err))
Err(err) => device.set_error(err), .ok();
}
} }
return device; return device;
@ -72,18 +71,16 @@ pub trait GoogleHomeDevice: AsOnOff + AsScene {
fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> { fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> {
match command { match command {
CommandType::OnOff { on } => { CommandType::OnOff { on } => {
if let Some(d) = AsOnOff::cast_mut(self) { let on_off = AsOnOff::cast_mut(self)
d.set_on(*on)?; .ok_or::<ErrorCode>(DeviceError::ActionNotAvailable.into())?;
} else {
return Err(DeviceError::ActionNotAvailable.into()); on_off.set_on(*on)?;
}
}, },
CommandType::ActivateScene { deactivate } => { CommandType::ActivateScene { deactivate } => {
if let Some(d) = AsScene::cast_mut(self) { let scene = AsScene::cast_mut(self)
d.set_active(!deactivate)?; .ok_or::<ErrorCode>(DeviceError::ActionNotAvailable.into())?;
} else {
return Err(DeviceError::ActionNotAvailable.into()); scene.set_active(!deactivate)?;
}
}, },
} }

View File

@ -47,16 +47,16 @@ impl GoogleHome {
.into_iter() .into_iter()
.map(|device| device.id) .map(|device| device.id)
.map(|id| { .map(|id| {
let mut d: query::Device; let device = devices.get(id.as_str())
if let Some(device) = devices.get(id.as_str()) { .map_or_else(|| {
d = device.query(); let mut device = query::Device::new();
} else { device.set_offline();
d = query::Device::new(); device.set_error(DeviceError::DeviceNotFound.into());
d.set_offline();
d.set_error(DeviceError::DeviceNotFound.into());
}
return (id, d); device
}, |device| device.query());
return (id, device);
}).collect(); }).collect();
return resp_payload; return resp_payload;
@ -79,25 +79,25 @@ impl GoogleHome {
.into_iter() .into_iter()
.map(|device| device.id) .map(|device| device.id)
.map(|id| { .map(|id| {
if let Some(device) = devices.get_mut(id.as_str()) { devices.get_mut(id.as_str())
if !device.is_online() { .map_or((id.clone(), Err(DeviceError::DeviceNotFound.into())), |device| {
return (id, Ok(false)); if !device.is_online() {
} return (id, Ok(false));
let results = command.execution.iter().map(|cmd| { }
// @TODO We should also return the state after update in the state
// struct, however that will make things WAY more complicated
device.execute(cmd)
}).collect::<Result<Vec<_>, ErrorCode>>();
// @TODO We only get one error not all errors let results = command.execution.iter().map(|cmd| {
if let Err(err) = results { // @TODO We should also return the state after update in the state
return (id, Err(err)); // struct, however that will make things WAY more complicated
} else { device.execute(cmd)
return (id, Ok(true)); }).collect::<Result<Vec<_>, ErrorCode>>();
}
} else { // @TODO We only get one error not all errors
return (id, Err(DeviceError::DeviceNotFound.into())); if let Err(err) = results {
} return (id, Err(err));
} else {
return (id, Ok(true));
}
})
}).for_each(|(id, state)| { }).for_each(|(id, state)| {
match state { match state {
Ok(true) => success.add_id(&id), Ok(true) => success.add_id(&id),

View File

@ -7,7 +7,7 @@ use rumqttc::{AsyncClient, has_wildcards};
use serde::Deserialize; use serde::Deserialize;
use eui48::MacAddress; use eui48::MacAddress;
use crate::{devices::{DeviceBox, IkeaOutlet, WakeOnLAN, AudioSetup, ContactSensor, KasaOutlet}, error::{FailedToParseConfig, MissingEnv, MissingWildcard, Error, FailedToCreateDevice}}; use crate::{devices::{DeviceBox, IkeaOutlet, WakeOnLAN, AudioSetup, ContactSensor, KasaOutlet, self}, error::{FailedToParseConfig, MissingEnv, MissingWildcard, Error, FailedToCreateDevice}};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
@ -132,7 +132,7 @@ pub struct PresenceDeviceConfig {
impl PresenceDeviceConfig { impl PresenceDeviceConfig {
/// Set the mqtt topic to an appropriate value if it is not already set /// Set the mqtt topic to an appropriate value if it is not already set
fn generate_topic(&mut self, class: &str, identifier: &str, config: &Config) -> Result<(), MissingWildcard> { fn generate_topic(mut self, class: &str, identifier: &str, config: &Config) -> Result<PresenceDeviceConfig, MissingWildcard> {
if self.mqtt.is_none() { if self.mqtt.is_none() {
if !has_wildcards(&config.presence.topic) { if !has_wildcards(&config.presence.topic) {
return Err(MissingWildcard::new(&config.presence.topic).into()); return Err(MissingWildcard::new(&config.presence.topic).into());
@ -145,7 +145,7 @@ impl PresenceDeviceConfig {
self.mqtt = Some(MqttDeviceConfig { topic }); self.mqtt = Some(MqttDeviceConfig { topic });
} }
Ok(()) Ok(self)
} }
} }
@ -213,23 +213,27 @@ impl Config {
} }
} }
// Quick helper function to box up the devices,
// passing in Box::new would be ideal, however the return type is incorrect
// Maybe there is a better way to solve this?
fn device_box<T: devices::Device + 'static>(device: T) -> DeviceBox {
let a: DeviceBox = Box::new(device);
a
}
impl Device { impl Device {
#[async_recursion] #[async_recursion]
pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, FailedToCreateDevice> { pub async fn create(self, identifier: &str, config: &Config, client: AsyncClient) -> Result<DeviceBox, FailedToCreateDevice> {
let device: Result<DeviceBox, Error> = match self { let device: Result<DeviceBox, Error> = match self {
Device::IkeaOutlet { info, mqtt, kettle } => { Device::IkeaOutlet { info, mqtt, kettle } => {
trace!(id = identifier, "IkeaOutlet [{} in {:?}]", info.name, info.room); trace!(id = identifier, "IkeaOutlet [{} in {:?}]", info.name, info.room);
match IkeaOutlet::build(&identifier, info, mqtt, kettle, client).await { IkeaOutlet::build(&identifier, info, mqtt, kettle, client).await
Ok(device) => Ok(Box::new(device)), .map(device_box)
Err(err) => Err(err),
}
}, },
Device::WakeOnLAN { info, mqtt, mac_address } => { Device::WakeOnLAN { info, mqtt, mac_address } => {
trace!(id = identifier, "WakeOnLan [{} in {:?}]", info.name, info.room); trace!(id = identifier, "WakeOnLan [{} in {:?}]", info.name, info.room);
match WakeOnLAN::build(&identifier, info, mqtt, mac_address, client).await { WakeOnLAN::build(&identifier, info, mqtt, mac_address, client).await
Ok(device) => Ok(Box::new(device)), .map(device_box)
Err(err) => Err(err),
}
}, },
Device::KasaOutlet { ip } => { Device::KasaOutlet { ip } => {
trace!(id = identifier, "KasaOutlet [{}]", identifier); trace!(id = identifier, "KasaOutlet [{}]", identifier);
@ -243,22 +247,18 @@ impl Device {
let speakers_id = identifier.to_owned() + ".speakers"; let speakers_id = identifier.to_owned() + ".speakers";
let speakers = (*speakers).create(&speakers_id, config, client.clone()).await?; let speakers = (*speakers).create(&speakers_id, config, client.clone()).await?;
match AudioSetup::build(&identifier, mqtt, mixer, speakers, client).await { AudioSetup::build(&identifier, mqtt, mixer, speakers, client).await
Ok(device) => Ok(Box::new(device)), .map(device_box)
Err(err) => Err(err),
}
}, },
Device::ContactSensor { mqtt, mut presence } => { Device::ContactSensor { mqtt, presence } => {
trace!(id = identifier, "ContactSensor [{}]", identifier); trace!(id = identifier, "ContactSensor [{}]", identifier);
if let Some(presence) = &mut presence { let presence = presence
presence.generate_topic("contact", &identifier, &config) .map(|p| p.generate_topic("contact", &identifier, &config))
.map_err(|err| FailedToCreateDevice::new(&identifier, err.into()))?; .transpose()
} .map_err(|err| FailedToCreateDevice::new(&identifier, err.into()))?;
match ContactSensor::build(&identifier, mqtt, presence, client).await { ContactSensor::build(&identifier, mqtt, presence, client).await
Ok(device) => Ok(Box::new(device)), .map(device_box)
Err(err) => Err(err),
}
}, },
}; };

View File

@ -10,13 +10,13 @@ struct DebugBridge {
} }
impl DebugBridge { impl DebugBridge {
pub fn new(topic: String, client: AsyncClient) -> Self { pub fn new(topic: &str, client: AsyncClient) -> Self {
Self { topic, client } Self { topic: topic.to_owned(), client }
} }
} }
pub fn start(mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver, config: DebugBridgeConfig, client: AsyncClient) { pub fn start(mut presence_rx: presence::Receiver, mut light_sensor_rx: light_sensor::Receiver, config: DebugBridgeConfig, client: AsyncClient) {
let mut debug_bridge = DebugBridge::new(config.topic, client); let mut debug_bridge = DebugBridge::new(&config.topic, client);
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {

View File

@ -26,7 +26,7 @@ impl_cast::impl_cast!(Device, OnDarkness);
impl_cast::impl_cast!(Device, GoogleHomeDevice); impl_cast::impl_cast!(Device, GoogleHomeDevice);
impl_cast::impl_cast!(Device, OnOff); impl_cast::impl_cast!(Device, OnOff);
pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnDarkness + AsOnOff + std::fmt::Debug { pub trait Device: AsGoogleHomeDevice + AsOnMqtt + AsOnPresence + AsOnDarkness + AsOnOff + std::fmt::Debug + Sync + Send {
fn get_id(&self) -> &str; fn get_id(&self) -> &str;
} }
@ -43,10 +43,8 @@ macro_rules! get_cast {
self.devices self.devices
.iter_mut() .iter_mut()
.filter_map(|(id, device)| { .filter_map(|(id, device)| {
if let Some(listener) = [< As $trait >]::cast_mut(device.as_mut()) { [< As $trait >]::cast_mut(device.as_mut())
return Some((id.as_str(), listener)); .map(|listener| (id.as_str(), listener))
};
return None;
}).collect() }).collect()
} }
} }
@ -66,7 +64,7 @@ enum Command {
} }
} }
pub type DeviceBox = Box<dyn Device + Sync + Send>; pub type DeviceBox = Box<dyn Device>;
#[derive(Clone)] #[derive(Clone)]
pub struct DeviceHandle { pub struct DeviceHandle {

View File

@ -1,7 +1,7 @@
use std::{net::{SocketAddr, Ipv4Addr, TcpStream}, io::{Write, Read}}; use std::{net::{SocketAddr, Ipv4Addr, TcpStream}, io::{Write, Read}};
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::{traits, errors::{ErrorCode, DeviceError}}; use google_home::{traits, errors::{self, DeviceError}};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use super::Device; use super::Device;
@ -84,13 +84,30 @@ impl Request {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ResponseSetRelayState { struct ErrorCode {
err_code: isize, err_code: isize,
} }
impl ErrorCode {
fn ok(&self) -> Result<(), anyhow::Error> {
if self.err_code != 0 {
Err(anyhow::anyhow!("Error code: {}", self.err_code))
} else {
Ok(())
}
}
}
#[derive(Debug, Deserialize)]
struct ResponseSetRelayState {
#[serde(flatten)]
err_code: ErrorCode,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ResponseGetSysinfo { struct ResponseGetSysinfo {
err_code: isize, #[serde(flatten)]
err_code: ErrorCode,
relay_state: isize, relay_state: isize,
} }
@ -108,10 +125,8 @@ struct Response {
impl Response { impl Response {
fn get_current_relay_state(&self) -> Result<bool, anyhow::Error> { fn get_current_relay_state(&self) -> Result<bool, anyhow::Error> {
if let Some(sysinfo) = &self.system.get_sysinfo { if let Some(sysinfo) = &self.system.get_sysinfo {
if sysinfo.err_code != 0 { return sysinfo.err_code.ok()
return Err(anyhow::anyhow!("Error code: {}", sysinfo.err_code)); .map(|_| sysinfo.relay_state == 1);
}
return Ok(sysinfo.relay_state == 1);
} }
return Err(anyhow::anyhow!("No sysinfo found in response")); return Err(anyhow::anyhow!("No sysinfo found in response"));
@ -119,10 +134,7 @@ impl Response {
fn check_set_relay_success(&self) -> Result<(), anyhow::Error> { fn check_set_relay_success(&self) -> Result<(), anyhow::Error> {
if let Some(set_relay_state) = &self.system.set_relay_state { if let Some(set_relay_state) = &self.system.set_relay_state {
if set_relay_state.err_code != 0 { return set_relay_state.err_code.ok();
return Err(anyhow::anyhow!("Error code: {}", set_relay_state.err_code));
}
return Ok(());
} }
return Err(anyhow::anyhow!("No relay_state found in response")); return Err(anyhow::anyhow!("No relay_state found in response"));
@ -148,7 +160,7 @@ impl Response {
} }
impl traits::OnOff for KasaOutlet { impl traits::OnOff for KasaOutlet {
fn is_on(&self) -> Result<bool, ErrorCode> { fn is_on(&self) -> Result<bool, errors::ErrorCode> {
let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline.into()))?; let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline.into()))?;
let body = Request::get_sysinfo().encrypt(); let body = Request::get_sysinfo().encrypt();
@ -157,7 +169,7 @@ impl traits::OnOff for KasaOutlet {
let mut received = Vec::new(); let mut received = Vec::new();
let mut rx_bytes = [0; 1024]; let mut rx_bytes = [0; 1024];
loop { loop {
let read = stream.read(&mut rx_bytes).or::<ErrorCode>(Err(DeviceError::TransientError.into()))?; let read = stream.read(&mut rx_bytes).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
received.extend_from_slice(&rx_bytes[..read]); received.extend_from_slice(&rx_bytes[..read]);
@ -166,12 +178,12 @@ impl traits::OnOff for KasaOutlet {
} }
} }
let resp = Response::decrypt(received.into()).or::<ErrorCode>(Err(DeviceError::TransientError.into()))?; let resp = Response::decrypt(received.into()).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
resp.get_current_relay_state().or(Err(DeviceError::TransientError.into())) resp.get_current_relay_state().or(Err(DeviceError::TransientError.into()))
} }
fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { fn set_on(&mut self, on: bool) -> Result<(), errors::ErrorCode> {
let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline.into()))?; let mut stream = TcpStream::connect(self.addr).or::<DeviceError>(Err(DeviceError::DeviceOffline.into()))?;
let body = Request::set_relay_state(on).encrypt(); let body = Request::set_relay_state(on).encrypt();
@ -192,7 +204,7 @@ impl traits::OnOff for KasaOutlet {
} }
} }
let resp = Response::decrypt(received.into()).or::<ErrorCode>(Err(DeviceError::TransientError.into()))?; let resp = Response::decrypt(received.into()).or::<errors::ErrorCode>(Err(DeviceError::TransientError.into()))?;
resp.check_set_relay_success().or(Err(DeviceError::TransientError.into())) resp.check_set_relay_success().or(Err(DeviceError::TransientError.into()))
} }

View File

@ -91,18 +91,18 @@ async fn app() -> Result<(), Box<dyn std::error::Error>> {
).await.into_iter().collect::<Result<_, _>>()?; ).await.into_iter().collect::<Result<_, _>>()?;
// Start the ntfy service if it is configured // Start the ntfy service if it is configured
if let Some(ntfy_config) = config.ntfy { if let Some(config) = config.ntfy {
ntfy::start(presence.clone(), &ntfy_config); ntfy::start(presence.clone(), config);
} }
// Start the hue bridge if it is configured // Start the hue bridge if it is configured
if let Some(hue_bridge_config) = config.hue_bridge { if let Some(config) = config.hue_bridge {
hue_bridge::start(presence.clone(), light_sensor.clone(), hue_bridge_config); hue_bridge::start(presence.clone(), light_sensor.clone(), config);
} }
// Start the debug bridge if it is configured // Start the debug bridge if it is configured
if let Some(debug_bridge_config) = config.debug_bridge { if let Some(config) = config.debug_bridge {
debug_bridge::start(presence.clone(), light_sensor.clone(), debug_bridge_config, client.clone()); debug_bridge::start(presence.clone(), light_sensor.clone(), config, client.clone());
} }
// Actually start listening for mqtt message, // Actually start listening for mqtt message,

View File

@ -93,7 +93,7 @@ impl Ntfy {
} }
} }
pub fn start(mut rx: presence::Receiver, config: &NtfyConfig) { pub fn start(mut rx: presence::Receiver, config: NtfyConfig) {
let mut ntfy = Ntfy::new(&config.url, &config.topic); let mut ntfy = Ntfy::new(&config.url, &config.topic);
tokio::spawn(async move { tokio::spawn(async move {