AudioSetup now takes the name of two already created devices and stores a reference to the devices instead of creating and owning the devices directly

This commit is contained in:
Dreaded_X 2023-08-14 01:51:45 +02:00
parent 76b75b0cfb
commit e38c5eed31
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
12 changed files with 151 additions and 77 deletions

View File

@ -2,12 +2,12 @@
base_url = "https://login.huizinga.dev/api/oidc" base_url = "https://login.huizinga.dev/api/oidc"
[mqtt] [mqtt]
host="olympus.vpn.huizinga.dev" host = "olympus.vpn.huizinga.dev"
port=8883 port = 8883
client_name="automation-ares" client_name = "automation-ares"
username="mqtt" username = "mqtt"
password="${MQTT_PASSWORD}" password = "${MQTT_PASSWORD}"
tls=true tls = true
[ntfy] [ntfy]
topic = "${NTFY_TOPIC}" topic = "${NTFY_TOPIC}"

View File

@ -65,15 +65,19 @@ topic = "automation/appliance/living_room/zeus"
mac_address = "30:9c:23:60:9c:13" mac_address = "30:9c:23:60:9c:13"
broadcast_ip = "10.0.0.255" broadcast_ip = "10.0.0.255"
[devices.living_mixer]
type = "KasaOutlet"
ip = "10.0.0.49"
[devices.living_speakers]
type = "KasaOutlet"
ip = "10.0.0.182"
[devices.living_audio] [devices.living_audio]
type = "AudioSetup" type = "AudioSetup"
topic = "zigbee2mqtt/living/remote" topic = "zigbee2mqtt/living/remote"
[devices.living_audio.mixer] mixer = "living_mixer"
type = "KasaOutlet" speakers = "living_speakers"
ip = "10.0.0.49"
[devices.living_audio.speakers]
type = "KasaOutlet"
ip = "10.0.0.182"
[devices.hallway_frontdoor] [devices.hallway_frontdoor]
type = "ContactSensor" type = "ContactSensor"

View File

@ -65,15 +65,19 @@ room = "Living Room"
topic = "automation/appliance/living_room/zeus" topic = "automation/appliance/living_room/zeus"
mac_address = "30:9c:23:60:9c:13" mac_address = "30:9c:23:60:9c:13"
[devices.living_mixer]
type = "KasaOutlet"
ip = "10.0.0.49"
[devices.living_speakers]
type = "KasaOutlet"
ip = "10.0.0.182"
[devices.living_audio] [devices.living_audio]
type = "AudioSetup" type = "AudioSetup"
topic = "zigbee2mqtt/living/remote" topic = "zigbee2mqtt/living/remote"
[devices.living_audio.mixer] mixer = "light_sensor"
type = "KasaOutlet" speakers = "living_speakers"
ip = "10.0.0.49"
[devices.living_audio.speakers]
type = "KasaOutlet"
ip = "10.0.0.182"
[devices.hallway_frontdoor] [devices.hallway_frontdoor]
type = "ContactSensor" type = "ContactSensor"

View File

@ -4,6 +4,7 @@ use std::{
time::Duration, time::Duration,
}; };
use async_trait::async_trait;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use rumqttc::{AsyncClient, MqttOptions, Transport}; use rumqttc::{AsyncClient, MqttOptions, Transport};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
@ -11,6 +12,7 @@ use tracing::debug;
use crate::{ use crate::{
auth::OpenIDConfig, auth::OpenIDConfig,
device_manager::DeviceManager,
devices::{ devices::{
AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet, AudioSetup, ContactSensor, DebugBridgeConfig, Device, HueBridgeConfig, IkeaOutlet,
KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN, KasaOutlet, LightSensorConfig, PresenceConfig, WakeOnLAN,
@ -158,39 +160,42 @@ impl Config {
} }
} }
#[async_trait]
pub trait CreateDevice { pub trait CreateDevice {
type Config; type Config;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
event_channel: &EventChannel, event_channel: &EventChannel,
client: &AsyncClient, client: &AsyncClient,
// TODO: Not a big fan of passing in the global config // TODO: Not a big fan of passing in the global config
presence_topic: &str, presence_topic: &str,
devices: &DeviceManager,
) -> Result<Self, CreateDeviceError> ) -> Result<Self, CreateDeviceError>
where where
Self: Sized; Self: Sized;
} }
macro_rules! create { macro_rules! create {
(($self:ident, $id:ident, $event_channel:ident, $client:ident, $presence_topic:ident), [ $( $Variant:ident ),* ]) => { (($self:ident, $id:ident, $event_channel:ident, $client:ident, $presence_topic:ident, $device_manager:ident), [ $( $Variant:ident ),* ]) => {
match $self { match $self {
$(DeviceConfig::$Variant(c) => Box::new($Variant::create($id, c, $event_channel, $client, $presence_topic)?),)* $(DeviceConfig::$Variant(c) => Box::new($Variant::create($id, c, $event_channel, $client, $presence_topic, $device_manager).await?),)*
} }
}; };
} }
impl DeviceConfig { impl DeviceConfig {
pub fn create( pub async fn create(
self, self,
id: &str, id: &str,
event_channel: &EventChannel, event_channel: &EventChannel,
client: &AsyncClient, client: &AsyncClient,
presence_topic: &str, presence_topic: &str,
device_manager: &DeviceManager,
) -> Result<Box<dyn Device>, CreateDeviceError> { ) -> Result<Box<dyn Device>, CreateDeviceError> {
Ok(create! { Ok(create! {
(self, id, event_channel, client, presence_topic), [ (self, id, event_channel, client, presence_topic, device_manager), [
AudioSetup, AudioSetup,
ContactSensor, ContactSensor,
IkeaOutlet, IkeaOutlet,

View File

@ -14,7 +14,8 @@ use crate::{
event::{Event, EventChannel, OnMqtt}, event::{Event, EventChannel, OnMqtt},
}; };
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>; pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>;
pub type DeviceMap = HashMap<String, WrappedDevice>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DeviceManager { pub struct DeviceManager {
@ -70,6 +71,10 @@ impl DeviceManager {
self.devices.write().await.insert(id, device); self.devices.write().await.insert(id, device);
} }
pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
self.devices.read().await.get(name).cloned()
}
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> { pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
self.devices.read().await self.devices.read().await
} }

View File

@ -1,11 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use google_home::traits; use google_home::traits::OnOff;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use crate::{ use crate::{
config::{self, CreateDevice, MqttDeviceConfig}, config::{CreateDevice, MqttDeviceConfig},
device_manager::{DeviceManager, WrappedDevice},
devices::As,
error::CreateDeviceError, error::CreateDeviceError,
event::EventChannel, event::EventChannel,
event::OnMqtt, event::OnMqtt,
@ -13,14 +15,14 @@ use crate::{
messages::{RemoteAction, RemoteMessage}, messages::{RemoteAction, RemoteMessage},
}; };
use super::{As, Device}; use super::Device;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct AudioSetupConfig { pub struct AudioSetupConfig {
#[serde(flatten)] #[serde(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
mixer: Box<config::DeviceConfig>, mixer: String,
speakers: Box<config::DeviceConfig>, speakers: String,
} }
// TODO: We need a better way to store the children devices // TODO: We need a better way to store the children devices
@ -28,32 +30,48 @@ pub struct AudioSetupConfig {
pub struct AudioSetup { pub struct AudioSetup {
identifier: String, identifier: String,
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
mixer: Box<dyn traits::OnOff>, mixer: WrappedDevice,
speakers: Box<dyn traits::OnOff>, speakers: WrappedDevice,
} }
#[async_trait]
impl CreateDevice for AudioSetup { impl CreateDevice for AudioSetup {
type Config = AudioSetupConfig; type Config = AudioSetupConfig;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
event_channel: &EventChannel, _event_channel: &EventChannel,
client: &AsyncClient, _client: &AsyncClient,
presence_topic: &str, _presence_topic: &str,
device_manager: &DeviceManager,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up AudioSetup"); trace!(id = identifier, "Setting up AudioSetup");
// Create the child devices // TODO: Make sure they implement OnOff?
let mixer_id = format!("{}.mixer", identifier); let mixer = device_manager
let mixer = (*config.mixer).create(&mixer_id, event_channel, client, presence_topic)?; .get(&config.mixer)
let mixer = As::consume(mixer).ok_or(CreateDeviceError::OnOffExpected(mixer_id))?; .await
// NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen...
.ok_or(CreateDeviceError::DeviceDoesNotExist(config.mixer.clone()))?;
let speakers_id = format!("{}.speakers", identifier); {
let speakers = let mixer = mixer.read().await;
(*config.speakers).create(&speakers_id, event_channel, client, presence_topic)?; if As::<dyn OnOff>::cast(mixer.as_ref()).is_none() {
let speakers = return Err(CreateDeviceError::OnOffExpected(config.mixer));
As::consume(speakers).ok_or(CreateDeviceError::OnOffExpected(speakers_id))?; }
}
let speakers = device_manager.get(&config.speakers).await.ok_or(
CreateDeviceError::DeviceDoesNotExist(config.speakers.clone()),
)?;
{
let speakers = speakers.read().await;
if As::<dyn OnOff>::cast(speakers.as_ref()).is_none() {
return Err(CreateDeviceError::OnOffExpected(config.speakers));
}
}
Ok(Self { Ok(Self {
identifier: identifier.to_owned(), identifier: identifier.to_owned(),
@ -85,39 +103,54 @@ impl OnMqtt for AudioSetup {
} }
}; };
let mut mixer = self.mixer.write().await;
let mut speakers = self.speakers.write().await;
if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
) {
match action { match action {
RemoteAction::On => { RemoteAction::On => {
if self.mixer.is_on().await.unwrap() { if mixer.is_on().await.unwrap() {
self.speakers.set_on(false).await.unwrap(); speakers.set_on(false).await.unwrap();
self.mixer.set_on(false).await.unwrap(); mixer.set_on(false).await.unwrap();
} else { } else {
self.speakers.set_on(true).await.unwrap(); speakers.set_on(true).await.unwrap();
self.mixer.set_on(true).await.unwrap(); mixer.set_on(true).await.unwrap();
} }
}, },
RemoteAction::BrightnessMoveUp => { RemoteAction::BrightnessMoveUp => {
if !self.mixer.is_on().await.unwrap() { if !mixer.is_on().await.unwrap() {
self.mixer.set_on(true).await.unwrap(); mixer.set_on(true).await.unwrap();
} else if self.speakers.is_on().await.unwrap() { } else if speakers.is_on().await.unwrap() {
self.speakers.set_on(false).await.unwrap(); speakers.set_on(false).await.unwrap();
} else { } else {
self.speakers.set_on(true).await.unwrap(); speakers.set_on(true).await.unwrap();
} }
}, },
RemoteAction::BrightnessStop => { /* Ignore this action */ }, RemoteAction::BrightnessStop => { /* Ignore this action */ },
_ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}") _ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}")
} }
} }
}
} }
#[async_trait] #[async_trait]
impl OnPresence for AudioSetup { impl OnPresence for AudioSetup {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
let mut mixer = self.mixer.write().await;
let mut speakers = self.speakers.write().await;
if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
) {
// Turn off the audio setup when we leave the house // Turn off the audio setup when we leave the house
if !presence { if !presence {
debug!(id = self.identifier, "Turning devices off"); debug!(id = self.identifier, "Turning devices off");
self.speakers.set_on(false).await.unwrap(); speakers.set_on(false).await.unwrap();
self.mixer.set_on(false).await.unwrap(); mixer.set_on(false).await.unwrap();
}
} }
} }
} }

View File

@ -8,6 +8,7 @@ use tracing::{debug, error, trace, warn};
use crate::{ use crate::{
config::{CreateDevice, MqttDeviceConfig}, config::{CreateDevice, MqttDeviceConfig},
device_manager::DeviceManager,
devices::DEFAULT_PRESENCE, devices::DEFAULT_PRESENCE,
error::{CreateDeviceError, MissingWildcard}, error::{CreateDeviceError, MissingWildcard},
event::EventChannel, event::EventChannel,
@ -72,15 +73,17 @@ pub struct ContactSensor {
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
#[async_trait]
impl CreateDevice for ContactSensor { impl CreateDevice for ContactSensor {
type Config = ContactSensorConfig; type Config = ContactSensorConfig;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_event_channel: &EventChannel, _event_channel: &EventChannel,
client: &AsyncClient, client: &AsyncClient,
presence_topic: &str, presence_topic: &str,
_device_manager: &DeviceManager,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up ContactSensor"); trace!(id = identifier, "Setting up ContactSensor");

View File

@ -13,6 +13,7 @@ use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig}; use crate::config::{CreateDevice, InfoConfig, MqttDeviceConfig};
use crate::device_manager::DeviceManager;
use crate::devices::Device; use crate::devices::Device;
use crate::error::CreateDeviceError; use crate::error::CreateDeviceError;
use crate::event::EventChannel; use crate::event::EventChannel;
@ -56,15 +57,17 @@ pub struct IkeaOutlet {
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
#[async_trait]
impl CreateDevice for IkeaOutlet { impl CreateDevice for IkeaOutlet {
type Config = IkeaOutletConfig; type Config = IkeaOutletConfig;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_event_channel: &EventChannel, _event_channel: &EventChannel,
client: &AsyncClient, client: &AsyncClient,
_presence_topic: &str, _presence_topic: &str,
_device_manager: &DeviceManager,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!( trace!(
id = identifier, id = identifier,

View File

@ -18,7 +18,10 @@ use tokio::{
}; };
use tracing::trace; use tracing::trace;
use crate::{config::CreateDevice, error::CreateDeviceError, event::EventChannel}; use crate::{
config::CreateDevice, device_manager::DeviceManager, error::CreateDeviceError,
event::EventChannel,
};
use super::Device; use super::Device;
@ -33,15 +36,17 @@ pub struct KasaOutlet {
addr: SocketAddr, addr: SocketAddr,
} }
#[async_trait]
impl CreateDevice for KasaOutlet { impl CreateDevice for KasaOutlet {
type Config = KasaOutletConfig; type Config = KasaOutletConfig;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_event_channel: &EventChannel, _event_channel: &EventChannel,
_client: &AsyncClient, _client: &AsyncClient,
_presence_topic: &str, _presence_topic: &str,
_device_manager: &DeviceManager,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!(id = identifier, "Setting up KasaOutlet"); trace!(id = identifier, "Setting up KasaOutlet");

View File

@ -15,6 +15,7 @@ use tracing::{debug, error, trace};
use crate::{ use crate::{
config::{CreateDevice, InfoConfig, MqttDeviceConfig}, config::{CreateDevice, InfoConfig, MqttDeviceConfig},
device_manager::DeviceManager,
error::CreateDeviceError, error::CreateDeviceError,
event::EventChannel, event::EventChannel,
event::OnMqtt, event::OnMqtt,
@ -47,15 +48,17 @@ pub struct WakeOnLAN {
broadcast_ip: Ipv4Addr, broadcast_ip: Ipv4Addr,
} }
#[async_trait]
impl CreateDevice for WakeOnLAN { impl CreateDevice for WakeOnLAN {
type Config = WakeOnLANConfig; type Config = WakeOnLANConfig;
fn create( async fn create(
identifier: &str, identifier: &str,
config: Self::Config, config: Self::Config,
_event_channel: &EventChannel, _event_channel: &EventChannel,
_client: &AsyncClient, _client: &AsyncClient,
_presence_topic: &str, _presence_topic: &str,
_device_manager: &DeviceManager,
) -> Result<Self, CreateDeviceError> { ) -> Result<Self, CreateDeviceError> {
trace!( trace!(
id = identifier, id = identifier,

View File

@ -91,6 +91,8 @@ impl MissingWildcard {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum CreateDeviceError { pub enum CreateDeviceError {
#[error("Child device '{0}' does not exist (yet?)")]
DeviceDoesNotExist(String),
#[error("Expected device '{0}' to implement OnOff trait")] #[error("Expected device '{0}' to implement OnOff trait")]
OnOffExpected(String), OnOffExpected(String),
#[error(transparent)] #[error(transparent)]

View File

@ -62,8 +62,15 @@ async fn app() -> anyhow::Result<()> {
// Create all the devices specified in the config // Create all the devices specified in the config
for (id, device_config) in config.devices { for (id, device_config) in config.devices {
let device = let device = device_config
device_config.create(&id, &event_channel, &client, &config.presence.mqtt.topic)?; .create(
&id,
&event_channel,
&client,
&config.presence.mqtt.topic,
&device_manager,
)
.await?;
device_manager.add(device).await; device_manager.add(device).await;
} }