Implemented new timeout mechanism for ikea_outlet
All checks were successful
Build and deploy / Build application (push) Successful in 5m24s
Build and deploy / Build container (push) Successful in 1m8s
Build and deploy / Deploy container (push) Successful in 19s

This commit is contained in:
Dreaded_X 2024-12-04 03:03:53 +01:00
parent 03f1790627
commit 9d4b52b511
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
6 changed files with 105 additions and 52 deletions

View File

@ -109,16 +109,31 @@ automation.device_manager:add(IkeaRemote.new({
end, end,
})) }))
local function off_timeout(duration)
local timeout = Timeout.new()
return function(this, on)
if on then
timeout:start(duration, function()
this:set_on(false)
end)
else
timeout:cancel()
end
end
end
local kettle = IkeaOutlet.new({ local kettle = IkeaOutlet.new({
outlet_type = "Kettle", outlet_type = "Kettle",
name = "Kettle", name = "Kettle",
room = "Kitchen", room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"), topic = mqtt_z2m("kitchen/kettle"),
client = mqtt_client, client = mqtt_client,
timeout = debug and 5 or 300, callback = off_timeout(debug and 5 or 300),
}) })
automation.device_manager:add(kettle) automation.device_manager:add(kettle)
function set_kettle(on)
local function set_kettle(on)
kettle:set_on(on) kettle:set_on(on)
end end
@ -146,7 +161,7 @@ automation.device_manager:add(IkeaOutlet.new({
room = "Bathroom", room = "Bathroom",
topic = mqtt_z2m("bathroom/light"), topic = mqtt_z2m("bathroom/light"),
client = mqtt_client, client = mqtt_client,
timeout = debug and 60 or 45 * 60, callback = off_timeout(debug and 60 or 45 * 60),
})) }))
automation.device_manager:add(Washer.new({ automation.device_manager:add(Washer.new({
@ -163,7 +178,7 @@ automation.device_manager:add(IkeaOutlet.new({
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/charger"), topic = mqtt_z2m("workbench/charger"),
client = mqtt_client, client = mqtt_client,
timeout = debug and 5 or 20 * 3600, callback = off_timeout(debug and 5 or 20 * 3600),
})) }))
automation.device_manager:add(IkeaOutlet.new({ automation.device_manager:add(IkeaOutlet.new({

View File

@ -1,5 +1,4 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
@ -11,16 +10,15 @@ use google_home::types::Type;
use rumqttc::{matches, Publish}; use rumqttc::{matches, Publish};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate; use super::LuaDeviceCreate;
use crate::action_callback::ActionCallback;
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::OnOffMessage; use crate::messages::OnOffMessage;
use crate::mqtt::WrappedAsyncClient; use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
pub enum OutletType { pub enum OutletType {
@ -38,17 +36,17 @@ pub struct Config {
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(default(OutletType::Outlet))] #[device_config(default(OutletType::Outlet))]
pub outlet_type: OutletType, pub outlet_type: OutletType,
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>, #[device_config(from_lua, default)]
pub callback: ActionCallback<(IkeaOutlet, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
#[derive(Debug)] #[derive(Debug, Default)]
pub struct State { pub struct State {
last_known_state: bool, last_known_state: bool,
handle: Option<JoinHandle<()>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -81,13 +79,10 @@ impl LuaDeviceCreate for IkeaOutlet {
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce) .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?; .await?;
let state = State { Ok(Self {
last_known_state: false, config,
handle: None, state: Default::default(),
}; })
let state = Arc::new(RwLock::new(state));
Ok(Self { config, state })
} }
} }
@ -116,16 +111,10 @@ impl OnMqtt for IkeaOutlet {
return; return;
} }
// Abort any timer that is currently running self.config.callback.call((self.clone(), state)).await;
self.stop_timeout().await.unwrap();
debug!(id = Device::get_id(self), "Updating state to {state}"); debug!(id = Device::get_id(self), "Updating state to {state}");
self.state_mut().await.last_known_state = state; self.state_mut().await.last_known_state = state;
// If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.config.timeout {
self.start_timeout(timeout).await.unwrap();
}
} }
} }
} }
@ -199,29 +188,3 @@ impl traits::OnOff for IkeaOutlet {
Ok(()) Ok(())
} }
} }
#[async_trait]
impl crate::traits::Timeout for IkeaOutlet {
async fn start_timeout(&self, timeout: Duration) -> Result<()> {
// Abort any timer that is currently running
self.stop_timeout().await?;
let device = self.clone();
self.state_mut().await.handle = Some(tokio::spawn(async move {
debug!(id = device.get_id(), "Starting timeout ({timeout:?})...");
tokio::time::sleep(timeout).await;
debug!(id = device.get_id(), "Turning outlet off!");
device.set_on(false).await.unwrap();
}));
Ok(())
}
async fn stop_timeout(&self) -> Result<()> {
if let Some(handle) = self.state_mut().await.handle.take() {
handle.abort();
}
Ok(())
}
}

10
src/helpers/mod.rs Normal file
View File

@ -0,0 +1,10 @@
mod timeout;
pub use timeout::Timeout;
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals()
.set("Timeout", lua.create_proxy::<Timeout>()?)?;
Ok(())
}

63
src/helpers/timeout.rs Normal file
View File

@ -0,0 +1,63 @@
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tracing::debug;
use crate::action_callback::ActionCallback;
#[derive(Debug, Default)]
pub struct State {
handle: Option<JoinHandle<()>>,
}
#[derive(Debug, Clone)]
pub struct Timeout {
state: Arc<RwLock<State>>,
}
impl mlua::UserData for Timeout {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_function("new", |_lua, ()| {
let device = Self {
state: Default::default(),
};
Ok(device)
});
methods.add_async_method(
"start",
|_lua, this, (timeout, callback): (u64, ActionCallback<bool>)| async move {
if let Some(handle) = this.state.write().await.handle.take() {
handle.abort();
}
debug!("Running timeout callback after {timeout}s");
let timeout = Duration::from_secs(timeout);
this.state.write().await.handle = Some(tokio::spawn({
async move {
tokio::time::sleep(timeout).await;
callback.call(false).await;
}
}));
Ok(())
},
);
methods.add_async_method("cancel", |_lua, this, ()| async move {
debug!("Canceling timeout callback");
if let Some(handle) = this.state.write().await.handle.take() {
handle.abort();
}
Ok(())
});
}
}

View File

@ -9,6 +9,7 @@ pub mod device_manager;
pub mod devices; pub mod devices;
pub mod error; pub mod error;
pub mod event; pub mod event;
pub mod helpers;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule; pub mod schedule;

View File

@ -5,9 +5,9 @@ use anyhow::anyhow;
use automation::auth::User; use automation::auth::User;
use automation::config::{FulfillmentConfig, MqttConfig}; use automation::config::{FulfillmentConfig, MqttConfig};
use automation::device_manager::DeviceManager; use automation::device_manager::DeviceManager;
use automation::devices;
use automation::error::ApiError; use automation::error::ApiError;
use automation::mqtt::{self, WrappedAsyncClient}; use automation::mqtt::{self, WrappedAsyncClient};
use automation::{devices, helpers};
use axum::extract::{FromRef, State}; use axum::extract::{FromRef, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::post; use axum::routing::post;
@ -112,6 +112,7 @@ async fn app() -> anyhow::Result<()> {
lua.globals().set("automation", automation)?; lua.globals().set("automation", automation)?;
devices::register_with_lua(&lua)?; devices::register_with_lua(&lua)?;
helpers::register_with_lua(&lua)?;
// TODO: Make this not hardcoded // TODO: Make this not hardcoded
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into()); let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());