Compare commits

..

7 Commits

Author SHA1 Message Date
203c341863 Removed old presence system
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
2025-08-31 23:44:02 +02:00
91c35d56fd Move front door presence logic to lua 2025-08-31 23:40:35 +02:00
91971cb795 Handle turning off devices when away through lua 2025-08-31 23:40:35 +02:00
27fe726234 Removed DebugBridge as it no longer served a purpose 2025-08-31 23:40:35 +02:00
2ca5e53706 Moved presence debug mqtt message to lua 2025-08-31 23:40:35 +02:00
52c32e2438 Presence and light sensor call all function in array 2025-08-31 23:40:35 +02:00
4209fb38ce Removed old darkness system 2025-08-31 23:37:44 +02:00
13 changed files with 143 additions and 297 deletions

View File

@@ -1,13 +1,12 @@
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::error::DeviceConfigError;
use automation_lib::event::{OnMqtt, OnPresence};
use automation_lib::messages::{ContactMessage, PresenceMessage};
use automation_lib::event::OnMqtt;
use automation_lib::messages::ContactMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::device;
@@ -16,10 +15,7 @@ use google_home::traits::OpenClose;
use google_home::types::Type;
use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn};
use crate::presence::DEFAULT_PRESENCE;
use tracing::{debug, error, trace};
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
pub enum SensorType {
@@ -28,23 +24,12 @@ pub enum SensorType {
Window,
}
// NOTE: If we add more presence devices we might need to move this out of here
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct PresenceDeviceConfig {
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(with(Duration::from_secs))]
pub timeout: Duration,
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config {
#[device_config(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)]
pub presence: Option<PresenceDeviceConfig>,
#[device_config(default(SensorType::Window))]
pub sensor_type: SensorType,
@@ -57,9 +42,7 @@ pub struct Config {
#[derive(Debug)]
struct State {
overall_presence: bool,
is_closed: bool,
handle: Option<JoinHandle<()>>,
}
#[derive(Debug, Clone, LuaDevice)]
@@ -92,11 +75,7 @@ impl LuaDeviceCreate for ContactSensor {
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
let state = State {
overall_presence: DEFAULT_PRESENCE,
is_closed: true,
handle: None,
};
let state = State { is_closed: true };
let state = Arc::new(RwLock::new(state));
Ok(Self { config, state })
@@ -163,13 +142,6 @@ impl OpenClose for ContactSensor {
}
}
#[async_trait]
impl OnPresence for ContactSensor {
async fn on_presence(&self, presence: bool) {
self.state_mut().await.overall_presence = presence;
}
}
#[async_trait]
impl OnMqtt for ContactSensor {
async fn on_mqtt(&self, message: rumqttc::Publish) {
@@ -193,64 +165,5 @@ impl OnMqtt for ContactSensor {
debug!(id = self.get_id(), "Updating state to {is_closed}");
self.state_mut().await.is_closed = is_closed;
// Check if this contact sensor works as a presence device
// If not we are done here
let presence = match &self.config.presence {
Some(presence) => presence.clone(),
None => return,
};
if !is_closed {
// Activate presence and stop any timeout once we open the door
if let Some(handle) = self.state_mut().await.handle.take() {
handle.abort();
}
// Only use the door as an presence sensor if there the current presence is set false
// This is to prevent the house from being marked as present for however long the
// timeout is set when leaving the house
if !self.state().await.overall_presence {
self.config
.client
.publish(
&presence.mqtt.topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
)
.await
.map_err(|err| {
warn!(
"Failed to publish presence on {}: {err}",
presence.mqtt.topic
)
})
.ok();
}
} else {
// Once the door is closed again we start a timeout for removing the presence
let device = self.clone();
self.state_mut().await.handle = Some(tokio::spawn(async move {
debug!(
id = device.get_id(),
"Starting timeout ({:?}) for contact sensor...", presence.timeout
);
tokio::time::sleep(presence.timeout).await;
debug!(id = device.get_id(), "Removing door device!");
device
.config
.client
.publish(&presence.mqtt.topic, rumqttc::QoS::AtLeastOnce, false, "")
.await
.map_err(|err| {
warn!(
"Failed to publish presence on {}: {err}",
presence.mqtt.topic
)
})
.ok();
}));
}
}
}

View File

@@ -1,65 +0,0 @@
use std::convert::Infallible;
use async_trait::async_trait;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnPresence;
use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use tracing::{trace, warn};
#[derive(Debug, LuaDeviceConfig, Clone)]
pub struct Config {
pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, Clone, LuaDevice)]
pub struct DebugBridge {
config: Config,
}
#[async_trait]
impl LuaDeviceCreate for DebugBridge {
type Config = Config;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up DebugBridge");
Ok(Self { config })
}
}
impl Device for DebugBridge {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
#[async_trait]
impl OnPresence for DebugBridge {
async fn on_presence(&self, presence: bool) {
let message = PresenceMessage::new(presence);
let topic = format!("{}/presence", self.config.mqtt.topic);
self.config
.client
.publish(
topic,
rumqttc::QoS::AtLeastOnce,
true,
serde_json::to_string(&message).expect("Serialization should not fail"),
)
.await
.map_err(|err| {
warn!(
"Failed to update presence on {}/presence: {err}",
self.config.mqtt.topic
)
})
.ok();
}
}

View File

@@ -3,7 +3,6 @@ use std::net::SocketAddr;
use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnPresence;
use automation_lib::lua::traits::AddAdditionalMethods;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use mlua::LuaSerdeExt;
@@ -110,11 +109,3 @@ impl AddAdditionalMethods for HueBridge {
);
}
}
#[async_trait]
impl OnPresence for HueBridge {
async fn on_presence(&self, presence: bool) {
trace!("Bridging presence to hue");
self.set_flag(Flag::Presence, presence).await;
}
}

View File

@@ -4,7 +4,6 @@ use std::str::Utf8Error;
use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnPresence;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError};
@@ -13,7 +12,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tracing::{debug, trace};
use tracing::trace;
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config {
@@ -276,13 +275,3 @@ impl 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,6 +1,5 @@
mod air_filter;
mod contact_sensor;
mod debug_bridge;
mod hue_bridge;
mod hue_group;
mod hue_switch;
@@ -19,7 +18,6 @@ use zigbee::outlet::{OutletOnOff, OutletPower};
pub use self::air_filter::AirFilter;
pub use self::contact_sensor::ContactSensor;
pub use self::debug_bridge::DebugBridge;
pub use self::hue_bridge::HueBridge;
pub use self::hue_group::HueGroup;
pub use self::hue_switch::HueSwitch;
@@ -41,7 +39,6 @@ macro_rules! register_device {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
register_device!(lua, AirFilter);
register_device!(lua, ContactSensor);
register_device!(lua, DebugBridge);
register_device!(lua, HueBridge);
register_device!(lua, HueGroup);
register_device!(lua, HueSwitch);

View File

@@ -4,7 +4,7 @@ use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{self, EventChannel, OnMqtt};
use automation_lib::event::OnMqtt;
use automation_lib::messages::BrightnessMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
@@ -19,8 +19,6 @@ pub struct Config {
pub mqtt: MqttDeviceConfig,
pub min: isize,
pub max: isize,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua, default)]
pub callback: ActionCallback<LightSensor, bool>,

View File

@@ -5,7 +5,8 @@ use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{self, Event, EventChannel, OnMqtt};
use automation_lib::event::OnMqtt;
use automation_lib::lua::traits::AddAdditionalMethods;
use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
@@ -17,8 +18,6 @@ use tracing::{debug, trace, warn};
pub struct Config {
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, rename("event_channel"), with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua, default)]
pub callback: ActionCallback<Presence, bool>,
@@ -36,6 +35,7 @@ pub struct State {
}
#[derive(Debug, Clone, LuaDevice)]
#[traits(AddAdditionalMethods)]
pub struct Presence {
config: Config,
state: Arc<RwLock<State>>,
@@ -118,17 +118,18 @@ impl OnMqtt for Presence {
debug!("Overall presence updated: {overall_presence}");
self.state_mut().await.current_overall_presence = overall_presence;
if self
.config
.tx
.send(Event::Presence(overall_presence))
.await
.is_err()
{
warn!("There are no receivers on the event channel");
}
self.config.callback.call(self, &overall_presence).await;
}
}
}
impl AddAdditionalMethods for Presence {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static,
{
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
}

View File

@@ -7,7 +7,7 @@ use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{OnMqtt, OnPresence};
use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
@@ -251,16 +251,6 @@ impl OnMqtt for Light<StateColorTemperature> {
}
}
#[async_trait]
impl<T: LightState> OnPresence for Light<T> {
async fn on_presence(&self, presence: bool) {
if !presence {
debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok();
}
}
}
#[async_trait]
impl<T: LightState> google_home::Device for Light<T> {
fn get_device_type(&self) -> Type {

View File

@@ -7,7 +7,7 @@ use async_trait::async_trait;
use automation_lib::action_callback::ActionCallback;
use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::{OnMqtt, OnPresence};
use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
@@ -50,10 +50,6 @@ pub struct Config<T: OutletState> {
#[device_config(default(OutletType::Outlet))]
pub outlet_type: OutletType,
// TODO: One presence is reworked, this should be removed!
#[device_config(default(true))]
pub presence_auto_off: bool,
#[device_config(from_lua, default)]
pub callback: ActionCallback<Outlet<T>, T>,
@@ -202,16 +198,6 @@ impl OnMqtt for Outlet<StatePower> {
}
}
#[async_trait]
impl<T: OutletState> OnPresence for Outlet<T> {
async fn on_presence(&self, presence: bool) {
if self.config.presence_auto_off && !presence {
debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok();
}
}
}
#[async_trait]
impl<T: OutletState> google_home::Device for Outlet<T> {
fn get_device_type(&self) -> Type {

View File

@@ -5,7 +5,7 @@ use dyn_clone::DynClone;
use google_home::traits::OnOff;
use mlua::ObjectLike;
use crate::event::{OnMqtt, OnPresence};
use crate::event::OnMqtt;
#[async_trait::async_trait]
pub trait LuaDeviceCreate {
@@ -18,14 +18,7 @@ pub trait LuaDeviceCreate {
}
pub trait Device:
Debug
+ DynClone
+ Sync
+ Send
+ Cast<dyn google_home::Device>
+ Cast<dyn OnMqtt>
+ Cast<dyn OnPresence>
+ Cast<dyn OnOff>
Debug + DynClone + Sync + Send + Cast<dyn google_home::Device> + Cast<dyn OnMqtt> + Cast<dyn OnOff>
{
fn get_id(&self) -> String;
}

View File

@@ -9,7 +9,7 @@ use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, instrument, trace};
use crate::device::Device;
use crate::event::{Event, EventChannel, OnMqtt, OnPresence};
use crate::event::{Event, EventChannel, OnMqtt};
pub type DeviceMap = HashMap<String, Box<dyn Device>>;
@@ -92,19 +92,6 @@ impl DeviceManager {
}
});
join_all(iter).await;
}
Event::Presence(presence) => {
let devices = self.devices.read().await;
let iter = devices.iter().map(|(id, device)| async move {
let device: Option<&dyn OnPresence> = device.cast();
if let Some(device) = device {
trace!(id, "Handling");
device.on_presence(presence).await;
trace!(id, "Done");
}
});
join_all(iter).await;
}
}

View File

@@ -6,7 +6,6 @@ use tokio::sync::mpsc;
#[derive(Debug, Clone)]
pub enum Event {
MqttMessage(Publish),
Presence(bool),
}
pub type Sender = mpsc::Sender<Event>;
@@ -34,8 +33,3 @@ pub trait OnMqtt: Sync + Send {
// fn topics(&self) -> Vec<&str>;
async fn on_mqtt(&self, message: Publish);
}
#[async_trait]
pub trait OnPresence: Sync + Send {
async fn on_presence(&self, presence: bool);
}

View File

@@ -1,4 +1,4 @@
print("Hello from lua")
print(_VERSION)
local host = automation.util.get_hostname()
print("Running @" .. host)
@@ -34,36 +34,85 @@ local ntfy = Ntfy.new({
})
automation.device_manager:add(ntfy)
automation.device_manager:add(Presence.new({
local on_presence = {
add = function(self, f)
self[#self + 1] = f
end,
}
local presence_system = Presence.new({
topic = mqtt_automation("presence/+/#"),
client = mqtt_client,
event_channel = automation.device_manager:event_channel(),
callback = function(_, presence)
ntfy:send_notification({
title = "Presence",
message = presence and "Home" or "Away",
tags = { "house" },
priority = "low",
actions = {
{
action = "broadcast",
extras = {
cmd = "presence",
state = presence and "0" or "1",
},
label = presence and "Set away" or "Set home",
clear = true,
for _, f in ipairs(on_presence) do
if type(f) == "function" then
f(presence)
end
end
end,
})
automation.device_manager:add(presence_system)
on_presence:add(function(presence)
ntfy:send_notification({
title = "Presence",
message = presence and "Home" or "Away",
tags = { "house" },
priority = "low",
actions = {
{
action = "broadcast",
extras = {
cmd = "presence",
state = presence and "0" or "1",
},
label = presence and "Set away" or "Set home",
clear = true,
},
})
},
})
end)
on_presence:add(function(presence)
mqtt_client:send_message(mqtt_automation("debug") .. "/presence", {
state = presence,
updated = automation.util.get_epoch(),
})
end)
local function turn_off_when_away(device)
on_presence:add(function(presence)
if not presence then
device:set_on(false)
end
end)
end
local on_light = {
add = function(self, f)
self[#self + 1] = f
end,
}
automation.device_manager:add(LightSensor.new({
identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"),
client = mqtt_client,
min = 22000,
max = 23500,
event_channel = automation.device_manager:event_channel(),
callback = function(_, light)
for _, f in ipairs(on_light) do
if type(f) == "function" then
f(light)
end
end
end,
}))
automation.device_manager:add(DebugBridge.new({
identifier = "debug_bridge",
topic = mqtt_automation("debug"),
client = mqtt_client,
}))
on_light:add(function(light)
mqtt_client:send_message(mqtt_automation("debug") .. "/darkness", {
state = not light,
updated = automation.util.get_epoch(),
})
end)
local hue_ip = "10.0.0.102"
local hue_token = automation.util.get_env("HUE_TOKEN")
@@ -78,6 +127,12 @@ local hue_bridge = HueBridge.new({
},
})
automation.device_manager:add(hue_bridge)
on_light:add(function(light)
hue_bridge:set_flag("darkness", not light)
end)
on_presence:add(function(presence)
hue_bridge:set_flag("presence", presence)
end)
local kitchen_lights = HueGroup.new({
identifier = "kitchen_lights",
@@ -120,22 +175,6 @@ automation.device_manager:add(HueSwitch.new({
end,
}))
automation.device_manager:add(LightSensor.new({
identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"),
client = mqtt_client,
min = 22000,
max = 23500,
event_channel = automation.device_manager:event_channel(),
callback = function(_, light)
hue_bridge:set_flag("darkness", not light)
mqtt_client:send_message(mqtt_automation("debug") .. "/darkness", {
state = not light,
updated = automation.util.get_epoch(),
})
end,
}))
automation.device_manager:add(WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
@@ -151,6 +190,7 @@ local living_mixer = OutletOnOff.new({
topic = mqtt_z2m("living/mixer"),
client = mqtt_client,
})
turn_off_when_away(living_mixer)
automation.device_manager:add(living_mixer)
local living_speakers = OutletOnOff.new({
name = "Speakers",
@@ -158,6 +198,7 @@ local living_speakers = OutletOnOff.new({
topic = mqtt_z2m("living/speakers"),
client = mqtt_client,
})
turn_off_when_away(living_speakers)
automation.device_manager:add(living_speakers)
automation.device_manager:add(IkeaRemote.new({
@@ -207,6 +248,7 @@ local kettle = OutletPower.new({
client = mqtt_client,
callback = kettle_timeout(),
})
turn_off_when_away(kettle)
automation.device_manager:add(kettle)
local function set_kettle(_, on)
@@ -245,13 +287,14 @@ local function off_timeout(duration)
end
end
automation.device_manager:add(LightOnOff.new({
local bathroom_light = LightOnOff.new({
name = "Light",
room = "Bathroom",
topic = mqtt_z2m("bathroom/light"),
client = mqtt_client,
callback = off_timeout(debug and 60 or 45 * 60),
}))
})
automation.device_manager:add(bathroom_light)
automation.device_manager:add(Washer.new({
identifier = "bathroom_washer",
@@ -269,7 +312,6 @@ automation.device_manager:add(Washer.new({
}))
automation.device_manager:add(OutletOnOff.new({
presence_auto_off = false,
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
@@ -277,12 +319,14 @@ automation.device_manager:add(OutletOnOff.new({
callback = off_timeout(debug and 5 or 20 * 3600),
}))
automation.device_manager:add(OutletOnOff.new({
local workbench_outlet = OutletOnOff.new({
name = "Outlet",
room = "Workbench",
topic = mqtt_z2m("workbench/outlet"),
client = mqtt_client,
}))
})
turn_off_when_away(workbench_outlet)
automation.device_manager:add(workbench_outlet)
local workbench_light = LightColorTemperature.new({
name = "Light",
@@ -290,6 +334,7 @@ local workbench_light = LightColorTemperature.new({
topic = mqtt_z2m("workbench/light"),
client = mqtt_client,
})
turn_off_when_away(workbench_light)
automation.device_manager:add(workbench_light)
local delay_color_temp = Timeout.new()
@@ -392,6 +437,7 @@ local hallway_storage = LightBrightness.new({
hallway_light_automation:light_callback(state.state)
end,
})
turn_off_when_away(hallway_storage)
automation.device_manager:add(hallway_storage)
local hallway_bottom_lights = HueGroup.new({
@@ -414,6 +460,28 @@ hallway_light_automation.group = {
end,
}
local frontdoor_presence = {
timeout = Timeout.new(),
}
setmetatable(frontdoor_presence, {
__call = function(self, open)
if open then
self.timeout:cancel()
if not presence_system:overall_presence() then
mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), {
state = true,
updated = automation.util.get_epoch(),
})
end
else
self.timeout:start(debug and 10 or 15 * 60, function()
mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), {})
end)
end
end,
})
automation.device_manager:add(IkeaRemote.new({
name = "Remote",
room = "Hallway",
@@ -435,6 +503,7 @@ local hallway_frontdoor = ContactSensor.new({
},
callback = function(_, open)
hallway_light_automation:door_callback(open)
frontdoor_presence(open)
end,
})
automation.device_manager:add(hallway_frontdoor)
@@ -453,12 +522,14 @@ local hallway_trash = ContactSensor.new({
automation.device_manager:add(hallway_trash)
hallway_light_automation.trash = hallway_trash
automation.device_manager:add(LightOnOff.new({
local guest_light = LightOnOff.new({
name = "Light",
room = "Guest Room",
topic = mqtt_z2m("guest/light"),
client = mqtt_client,
}))
})
turn_off_when_away(guest_light)
automation.device_manager:add(guest_light)
local bedroom_air_filter = AirFilter.new({
name = "Air Filter",
@@ -529,6 +600,7 @@ local storage_light = LightBrightness.new({
topic = mqtt_z2m("storage/light"),
client = mqtt_client,
})
turn_off_when_away(storage_light)
automation.device_manager:add(storage_light)
automation.device_manager:add(ContactSensor.new({