Use IkeaRemote to control devices and completely replace AudioSetup
All checks were successful
Build and deploy / Build application (push) Successful in 3m24s
Build and deploy / Build container (push) Successful in 43s
Build and deploy / Deploy container (push) Successful in 18s

This commit is contained in:
Dreaded_X 2024-11-30 06:06:30 +01:00
parent a353ba3d08
commit 4bb49a381b
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
6 changed files with 71 additions and 229 deletions

View File

@ -84,25 +84,60 @@ automation.device_manager:add(living_mixer)
local living_speakers = KasaOutlet.new({ identifier = "living_speakers", ip = "10.0.0.127" })
automation.device_manager:add(living_speakers)
automation.device_manager:add(AudioSetup.new({
identifier = "living_audio",
topic = mqtt_z2m("living/remote"),
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Living",
client = mqtt_client,
mixer = living_mixer,
speakers = living_speakers,
topic = mqtt_z2m("living/remote"),
single_button = true,
callback = function(on)
if on then
if living_mixer:is_on() then
living_mixer:set_on(false)
living_speakers:set_on(false)
else
living_mixer:set_on(true)
living_speakers:set_on(true)
end
else
if not living_mixer:is_on() then
living_mixer:set_on(true)
else
living_speakers:set_on(not living_speakers:is_on())
end
end
end,
}))
automation.device_manager:add(IkeaOutlet.new({
local kettle = IkeaOutlet.new({
outlet_type = "Kettle",
name = "Kettle",
room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"),
client = mqtt_client,
timeout = debug and 5 or 300,
remotes = {
{ topic = mqtt_z2m("bedroom/remote") },
{ topic = mqtt_z2m("kitchen/remote") },
},
})
automation.device_manager:add(kettle)
function set_kettle(on)
kettle:set_on(on)
end
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Bedroom",
client = mqtt_client,
topic = mqtt_z2m("bedroom/remote"),
single_button = true,
callback = set_kettle,
}))
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Kitchen",
client = mqtt_client,
topic = mqtt_z2m("kitchen/remote"),
single_button = true,
callback = set_kettle,
}))
automation.device_manager:add(IkeaOutlet.new({
@ -145,12 +180,18 @@ local hallway_lights = HueGroup.new({
group_id = 81,
scene_id = "3qWKxGVadXFFG4o",
timer_id = 1,
remotes = {
{ topic = mqtt_z2m("hallway/remote") },
},
client = mqtt_client,
})
automation.device_manager:add(hallway_lights)
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Hallway",
client = mqtt_client,
topic = mqtt_z2m("hallway/remote"),
callback = function(on)
hallway_lights:set_on(on)
end,
}))
automation.device_manager:add(ContactSensor.new({
identifier = "hallway_frontdoor",

View File

@ -1,126 +0,0 @@
use async_trait::async_trait;
use automation_macro::LuaDeviceConfig;
use google_home::traits::OnOff;
use tracing::{debug, error, trace, warn};
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config {
pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub mixer: Box<dyn Device>,
#[device_config(from_lua)]
pub speakers: Box<dyn Device>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, Clone)]
pub struct AudioSetup {
config: Config,
}
#[async_trait]
impl LuaDeviceCreate for AudioSetup {
type Config = Config;
type Error = DeviceConfigError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup");
{
let mixer_id = config.mixer.get_id().to_owned();
if (config.mixer.cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
}
let speakers_id = config.speakers.get_id().to_owned();
if (config.speakers.cast() as Option<&dyn OnOff>).is_none() {
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
}
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(AudioSetup { config })
}
}
impl Device for AudioSetup {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
#[async_trait]
impl OnMqtt for AudioSetup {
async fn on_mqtt(&self, message: rumqttc::Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = self.get_id(), "Failed to parse message: {err}");
return;
}
};
if let (Some(mixer), Some(speakers)) = (
self.config.mixer.cast() as Option<&dyn OnOff>,
self.config.speakers.cast() as Option<&dyn OnOff>,
) {
match action {
RemoteAction::On => {
if mixer.on().await.unwrap() {
speakers.set_on(false).await.unwrap();
mixer.set_on(false).await.unwrap();
} else {
speakers.set_on(true).await.unwrap();
mixer.set_on(true).await.unwrap();
}
},
RemoteAction::BrightnessMoveUp => {
if !mixer.on().await.unwrap() {
mixer.set_on(true).await.unwrap();
} else if speakers.on().await.unwrap() {
speakers.set_on(false).await.unwrap();
} else {
speakers.set_on(true).await.unwrap();
}
},
RemoteAction::BrightnessStop => { /* Ignore this action */ },
_ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}")
}
}
}
}
#[async_trait]
impl OnPresence for AudioSetup {
async fn on_presence(&self, presence: bool) {
if let (Some(mixer), Some(speakers)) = (
self.config.mixer.cast() as Option<&dyn OnOff>,
self.config.speakers.cast() as Option<&dyn OnOff>,
) {
// Turn off the audio setup when we leave the house
if !presence {
debug!(id = self.get_id(), "Turning devices off");
speakers.set_on(false).await.unwrap();
mixer.set_on(false).await.unwrap();
}
}
}
}

View File

@ -6,13 +6,9 @@ use async_trait::async_trait;
use automation_macro::LuaDeviceConfig;
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
use rumqttc::{Publish, SubscribeFilter};
use tracing::{debug, error, trace, warn};
use tracing::{error, trace, warn};
use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig;
use crate::event::OnMqtt;
use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
@ -25,8 +21,6 @@ pub struct Config {
pub group_id: isize,
pub timer_id: isize,
pub scene_id: String,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
@ -45,16 +39,6 @@ impl LuaDeviceCreate for HueGroup {
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
Ok(Self { config })
}
}
@ -83,38 +67,6 @@ impl Device for HueGroup {
}
}
#[async_trait]
impl OnMqtt for HueGroup {
async fn on_mqtt(&self, message: Publish) {
if !self
.config
.remotes
.iter()
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
return;
}
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = self.get_id(), "Failed to parse message: {err}");
return;
}
};
debug!("Action: {action:#?}");
match action {
RemoteAction::On | RemoteAction::BrightnessMoveUp => self.set_on(true).await.unwrap(),
RemoteAction::Off | RemoteAction::BrightnessMoveDown => {
self.set_on(false).await.unwrap()
}
RemoteAction::BrightnessStop => { /* Ignore this action */ }
};
}
}
#[async_trait]
impl OnOff for HueGroup {
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {

View File

@ -8,7 +8,7 @@ use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff};
use google_home::types::Type;
use rumqttc::{matches, Publish, SubscribeFilter};
use rumqttc::{matches, Publish};
use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
@ -18,7 +18,7 @@ use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device;
use crate::event::{OnMqtt, OnPresence};
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::messages::OnOffMessage;
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
@ -40,8 +40,6 @@ pub struct Config {
pub outlet_type: OutletType,
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>,
#[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
@ -78,16 +76,6 @@ impl LuaDeviceCreate for IkeaOutlet {
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
@ -138,26 +126,6 @@ impl OnMqtt for IkeaOutlet {
if state && let Some(timeout) = self.config.timeout {
self.start_timeout(timeout).await.unwrap();
}
} else if self
.config
.remotes
.iter()
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(),
Err(err) => {
error!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
match action {
RemoteAction::On => self.set_on(true).await.unwrap(),
RemoteAction::BrightnessMoveUp => self.set_on(false).await.unwrap(),
RemoteAction::BrightnessStop => { /* Ignore this action */ },
_ => warn!("Expected ikea shortcut button which only supports 'on' and 'brightness_move_up', got: {action:?}")
}
}
}
}

View File

@ -6,14 +6,15 @@ use async_trait::async_trait;
use automation_macro::LuaDeviceConfig;
use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError};
use google_home::traits;
use google_home::traits::OnOff;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::trace;
use tracing::{debug, trace};
use super::{Device, LuaDeviceCreate};
use crate::event::OnPresence;
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config {
@ -206,7 +207,7 @@ impl Response {
}
#[async_trait]
impl traits::OnOff for KasaOutlet {
impl OnOff for KasaOutlet {
async fn on(&self) -> Result<bool, errors::ErrorCode> {
let mut stream = TcpStream::connect(self.config.addr)
.await
@ -275,3 +276,13 @@ impl traits::OnOff for KasaOutlet {
.or(Err(DeviceError::TransientError.into()))
}
}
#[async_trait]
impl OnPresence for KasaOutlet {
async fn on_presence(&self, presence: bool) {
if !presence {
debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok();
}
}
}

View File

@ -1,5 +1,4 @@
mod air_filter;
mod audio_setup;
mod contact_sensor;
mod debug_bridge;
mod hue_bridge;
@ -23,7 +22,6 @@ use google_home::traits::OnOff;
use mlua::ObjectLike;
pub use self::air_filter::AirFilter;
pub use self::audio_setup::AudioSetup;
pub use self::contact_sensor::ContactSensor;
pub use self::debug_bridge::DebugBridge;
pub use self::hue_bridge::HueBridge;
@ -100,7 +98,6 @@ macro_rules! impl_device {
}
impl_device!(AirFilter);
impl_device!(AudioSetup);
impl_device!(ContactSensor);
impl_device!(DebugBridge);
impl_device!(HueBridge);
@ -116,7 +113,6 @@ impl_device!(Washer);
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
register_device!(lua, AirFilter);
register_device!(lua, AudioSetup);
register_device!(lua, ContactSensor);
register_device!(lua, DebugBridge);
register_device!(lua, HueBridge);