Compare commits

..

No commits in common. "175056416e5d1f2b8c25437691131b039a5b4f1d" and "14aabe202d2a647e9ae495011f8f4e5e06d035aa" have entirely different histories.

14 changed files with 77 additions and 463 deletions

View File

@ -32,7 +32,7 @@ pub struct Config {
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub presence: Option<PresenceDeviceConfig>, pub presence: Option<PresenceDeviceConfig>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<ContactSensor, bool>, pub callback: ActionCallback<bool>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
@ -116,7 +116,7 @@ impl OnMqtt for ContactSensor {
return; return;
} }
self.config.callback.call(self, &!is_closed).await; self.config.callback.call(!is_closed).await;
debug!(id = self.get_id(), "Updating state to {is_closed}"); debug!(id = self.get_id(), "Updating state to {is_closed}");
self.state_mut().await.is_closed = is_closed; self.state_mut().await.is_closed = is_closed;

View File

@ -21,10 +21,10 @@ pub struct Config {
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub left_callback: ActionCallback<HueSwitch, ()>, pub left_callback: ActionCallback<()>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub right_callback: ActionCallback<HueSwitch, ()>, pub right_callback: ActionCallback<()>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -58,7 +58,7 @@ impl LuaDeviceCreate for HueSwitch {
#[async_trait] #[async_trait]
impl OnMqtt for HueSwitch { impl OnMqtt for HueSwitch {
async fn on_mqtt(&self, message: Publish) { async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the device itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
let action = match serde_json::from_slice::<Zigbee929003017102>(&message.payload) { let action = match serde_json::from_slice::<Zigbee929003017102>(&message.payload) {
Ok(message) => message.action, Ok(message) => message.action,
@ -70,12 +70,8 @@ impl OnMqtt for HueSwitch {
debug!(id = Device::get_id(self), "Remote action = {:?}", action); debug!(id = Device::get_id(self), "Remote action = {:?}", action);
match action { match action {
Zigbee929003017102Action::LeftPress => { Zigbee929003017102Action::LeftPress => self.config.left_callback.call(()).await,
self.config.left_callback.call(self, &()).await Zigbee929003017102Action::RightPress => self.config.right_callback.call(()).await,
}
Zigbee929003017102Action::RightPress => {
self.config.right_callback.call(self, &()).await
}
_ => {} _ => {}
} }
} }

View File

@ -23,6 +23,7 @@ pub enum OutletType {
Outlet, Outlet,
Kettle, Kettle,
Charger, Charger,
Light,
} }
#[derive(Debug, Clone, LuaDeviceConfig)] #[derive(Debug, Clone, LuaDeviceConfig)]
@ -35,7 +36,7 @@ pub struct Config {
pub outlet_type: OutletType, pub outlet_type: OutletType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<IkeaOutlet, bool>, pub callback: ActionCallback<(IkeaOutlet, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@ -108,7 +109,7 @@ impl OnMqtt for IkeaOutlet {
return; return;
} }
self.config.callback.call(self, &state).await; self.config.callback.call((self.clone(), state)).await;
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;
@ -132,6 +133,7 @@ impl google_home::Device for IkeaOutlet {
match self.config.outlet_type { match self.config.outlet_type {
OutletType::Outlet => Type::Outlet, OutletType::Outlet => Type::Outlet,
OutletType::Kettle => Type::Kettle, OutletType::Kettle => Type::Kettle,
OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work
OutletType::Charger => Type::Outlet, // Find a better device type for this, ideally would like to use charger, but that needs more work OutletType::Charger => Type::Outlet, // Find a better device type for this, ideally would like to use charger, but that needs more work
} }
} }

View File

@ -24,7 +24,7 @@ pub struct Config {
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua)] #[device_config(from_lua)]
pub callback: ActionCallback<IkeaRemote, bool>, pub callback: ActionCallback<bool>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -84,7 +84,7 @@ impl OnMqtt for IkeaRemote {
}; };
if let Some(on) = on { if let Some(on) = on {
self.config.callback.call(self, &on).await; self.config.callback.call(on).await;
} }
} }
} }

View File

@ -10,13 +10,11 @@ mod kasa_outlet;
mod light_sensor; mod light_sensor;
mod wake_on_lan; mod wake_on_lan;
mod washer; mod washer;
mod zigbee;
use std::ops::Deref; use std::ops::Deref;
use automation_cast::Cast; use automation_cast::Cast;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use zigbee::light::{LightBrightness, LightOnOff};
pub use self::air_filter::AirFilter; pub use self::air_filter::AirFilter;
pub use self::contact_sensor::ContactSensor; pub use self::contact_sensor::ContactSensor;
@ -68,7 +66,7 @@ macro_rules! impl_device {
Ok(()) Ok(())
}); });
methods.add_async_method("on", |_lua, this, _: ()| async move { methods.add_async_method("is_on", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn google_home::traits::OnOff>) Ok((this.deref().cast() as Option<&dyn google_home::traits::OnOff>)
.expect("Cast should be valid") .expect("Cast should be valid")
.on() .on()
@ -76,33 +74,11 @@ macro_rules! impl_device {
.unwrap()) .unwrap())
}); });
} }
if impls::impls!($device: google_home::traits::Brightness) {
methods.add_async_method("set_brightness", |_lua, this, brightness: u8| async move {
(this.deref().cast() as Option<&dyn google_home::traits::Brightness>)
.expect("Cast should be valid")
.set_brightness(brightness)
.await
.unwrap();
Ok(())
});
methods.add_async_method("brightness", |_lua, this, _: ()| async move {
Ok((this.deref().cast() as Option<&dyn google_home::traits::Brightness>)
.expect("Cast should be valid")
.brightness()
.await
.unwrap())
});
}
} }
} }
}; };
} }
impl_device!(LightOnOff);
impl_device!(LightBrightness);
impl_device!(AirFilter); impl_device!(AirFilter);
impl_device!(ContactSensor); impl_device!(ContactSensor);
impl_device!(DebugBridge); impl_device!(DebugBridge);
@ -117,8 +93,6 @@ impl_device!(WakeOnLAN);
impl_device!(Washer); impl_device!(Washer);
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
register_device!(lua, LightOnOff);
register_device!(lua, LightBrightness);
register_device!(lua, AirFilter); register_device!(lua, AirFilter);
register_device!(lua, ContactSensor); register_device!(lua, ContactSensor);
register_device!(lua, DebugBridge); register_device!(lua, DebugBridge);

View File

@ -1,298 +0,0 @@
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::Arc;
use anyhow::Result;
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::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig;
use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, OnOff};
use google_home::types::Type;
use rumqttc::{matches, Publish};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
pub trait LightState:
Debug + Clone + Default + Sync + Send + Serialize + Into<StateOnOff> + 'static
{
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config<T: LightState> {
#[device_config(flatten)]
pub info: InfoConfig,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)]
pub callback: ActionCallback<Light<T>, T>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
}
impl LightState for StateOnOff {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StateBrightness {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
brightness: f64,
}
impl LightState for StateBrightness {}
impl From<StateBrightness> for StateOnOff {
fn from(state: StateBrightness) -> Self {
StateOnOff { state: state.state }
}
}
#[derive(Debug, Clone)]
pub struct Light<T: LightState> {
config: Config<T>,
state: Arc<RwLock<T>>,
}
pub type LightOnOff = Light<StateOnOff>;
pub type LightBrightness = Light<StateBrightness>;
impl<T: LightState> Light<T> {
async fn state(&self) -> RwLockReadGuard<T> {
self.state.read().await
}
async fn state_mut(&self) -> RwLockWriteGuard<T> {
self.state.write().await
}
}
#[async_trait]
impl<T: LightState> LuaDeviceCreate for Light<T> {
type Config = Config<T>;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
state: Default::default(),
})
}
}
impl<T: LightState> Device for Light<T> {
fn get_id(&self) -> String {
self.config.info.identifier()
}
}
#[async_trait]
impl OnMqtt for Light<StateOnOff> {
async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the device itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) {
let state = match serde_json::from_slice::<StateOnOff>(&message.payload) {
Ok(state) => state,
Err(err) => {
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
// No need to do anything if the state has not changed
if state.state == self.state().await.state {
return;
}
self.state_mut().await.state = state.state;
debug!(
id = Device::get_id(self),
"Updating state to {:?}",
self.state().await
);
self.config
.callback
.call(self, self.state().await.deref())
.await;
}
}
}
#[async_trait]
impl OnMqtt for Light<StateBrightness> {
async fn on_mqtt(&self, message: Publish) {
// Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.config.mqtt.topic) {
let state = match serde_json::from_slice::<StateBrightness>(&message.payload) {
Ok(state) => state,
Err(err) => {
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
return;
}
};
{
let current_state = self.state().await;
// No need to do anything if the state has not changed
if state.state == current_state.state
&& state.brightness == current_state.brightness
{
return;
}
}
self.state_mut().await.state = state.state;
self.state_mut().await.brightness = state.brightness;
debug!(
id = Device::get_id(self),
"Updating state to {:?}",
self.state().await
);
self.config
.callback
.call(self, self.state().await.deref())
.await;
}
}
}
#[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();
}
}
}
impl<T: LightState> google_home::Device for Light<T> {
fn get_device_type(&self) -> Type {
Type::Light
}
fn get_device_name(&self) -> device::Name {
device::Name::new(&self.config.info.name)
}
fn get_id(&self) -> String {
Device::get_id(self)
}
fn is_online(&self) -> bool {
true
}
fn get_room_hint(&self) -> Option<&str> {
self.config.info.room.as_deref()
}
fn will_report_state(&self) -> bool {
// TODO: Implement state reporting
false
}
}
#[async_trait]
impl<T> OnOff for Light<T>
where
T: LightState,
{
async fn on(&self) -> Result<bool, ErrorCode> {
let state = self.state().await;
let state: StateOnOff = state.deref().clone().into();
Ok(state.state)
}
async fn set_on(&self, on: bool) -> Result<(), ErrorCode> {
let message = json!({
"state": if on { "ON" } else { "OFF"}
});
debug!(id = Device::get_id(self), "{message}");
let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here
self.config
.client
.publish(
&topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&message).unwrap(),
)
.await
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
.ok();
Ok(())
}
}
const FACTOR: f64 = 30.0;
#[async_trait]
impl<T> Brightness for Light<T>
where
T: LightState,
T: Into<StateBrightness>,
{
async fn brightness(&self) -> Result<u8, ErrorCode> {
let state = self.state().await;
let state: StateBrightness = state.deref().clone().into();
let brightness =
100.0 * f64::log10(state.brightness / FACTOR + 1.0) / f64::log10(254.0 / FACTOR + 1.0);
Ok(brightness.clamp(0.0, 100.0).round() as u8)
}
async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode> {
let brightness =
FACTOR * ((FACTOR / (FACTOR + 254.0)).powf(-(brightness as f64) / 100.0) - 1.0);
let message = json!({
"brightness": brightness.clamp(0.0, 254.0).round() as u8
});
let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here
self.config
.client
.publish(
&topic,
rumqttc::QoS::AtLeastOnce,
false,
serde_json::to_string(&message).unwrap(),
)
.await
.map_err(|err| warn!("Failed to update state on {topic}: {err}"))
.ok();
Ok(())
}
}

View File

@ -1 +0,0 @@
pub mod light;

View File

@ -1,7 +1,6 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use mlua::{FromLua, IntoLua, LuaSerdeExt}; use mlua::{FromLua, IntoLuaMulti};
use serde::Serialize;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Internal { struct Internal {
@ -10,23 +9,21 @@ struct Internal {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ActionCallback<T, S> { pub struct ActionCallback<T> {
internal: Option<Internal>, internal: Option<Internal>,
_this: PhantomData<T>, phantom: PhantomData<T>,
_state: PhantomData<S>,
} }
impl<T, S> Default for ActionCallback<T, S> { impl<T> Default for ActionCallback<T> {
fn default() -> Self { fn default() -> Self {
Self { Self {
internal: None, internal: None,
_this: PhantomData::<T>, phantom: PhantomData::<T>,
_state: PhantomData::<S>,
} }
} }
} }
impl<T, S> FromLua for ActionCallback<T, S> { impl<T> FromLua for ActionCallback<T> {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> { fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
let uuid = uuid::Uuid::new_v4(); let uuid = uuid::Uuid::new_v4();
lua.set_named_registry_value(&uuid.to_string(), value)?; lua.set_named_registry_value(&uuid.to_string(), value)?;
@ -36,31 +33,27 @@ impl<T, S> FromLua for ActionCallback<T, S> {
uuid, uuid,
lua: lua.clone(), lua: lua.clone(),
}), }),
_this: PhantomData::<T>, phantom: PhantomData::<T>,
_state: PhantomData::<S>,
}) })
} }
} }
// TODO: Return proper error here // TODO: Return proper error here
impl<T, S> ActionCallback<T, S> impl<T> ActionCallback<T>
where where
T: IntoLua + Sync + Send + Clone + 'static, T: IntoLuaMulti + Sync + Send + Clone + 'static,
S: Serialize,
{ {
pub async fn call(&self, this: &T, state: &S) { pub async fn call(&self, state: T) {
let Some(internal) = self.internal.as_ref() else { let Some(internal) = self.internal.as_ref() else {
return; return;
}; };
let state = internal.lua.to_value(state).unwrap();
let callback: mlua::Value = internal let callback: mlua::Value = internal
.lua .lua
.named_registry_value(&internal.uuid.to_string()) .named_registry_value(&internal.uuid.to_string())
.unwrap(); .unwrap();
match callback { match callback {
mlua::Value::Function(f) => f.call_async::<()>((this.clone(), state)).await.unwrap(), mlua::Value::Function(f) => f.call_async::<()>(state).await.unwrap(),
_ => todo!("Only functions are currently supported"), _ => todo!("Only functions are currently supported"),
} }
} }

View File

@ -1,4 +1,3 @@
pub mod serialization;
mod timeout; mod timeout;
pub use timeout::Timeout; pub use timeout::Timeout;

View File

@ -1,16 +0,0 @@
use serde::de::{self, Unexpected};
use serde::{Deserialize, Deserializer};
pub fn state_deserializer<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
match String::deserialize(deserializer)?.as_ref() {
"ON" => Ok(true),
"OFF" => Ok(false),
other => Err(de::Error::invalid_value(
Unexpected::Str(other),
&"Value expected was either ON or OFF",
)),
}
}

View File

@ -29,7 +29,7 @@ impl mlua::UserData for Timeout {
methods.add_async_method( methods.add_async_method(
"start", "start",
|_lua, this, (timeout, callback): (u64, ActionCallback<mlua::Value, bool>)| async move { |_lua, this, (timeout, callback): (u64, ActionCallback<bool>)| async move {
if let Some(handle) = this.state.write().await.handle.take() { if let Some(handle) = this.state.write().await.handle.take() {
handle.abort(); handle.abort();
} }
@ -42,7 +42,7 @@ impl mlua::UserData for Timeout {
async move { async move {
tokio::time::sleep(timeout).await; tokio::time::sleep(timeout).await;
callback.call(&mlua::Nil, &false).await; callback.call(false).await;
} }
})); }));

View File

@ -260,9 +260,8 @@ pub fn impl_lua_device_config_macro(ast: &DeriveInput) -> TokenStream {
}) })
.collect(); .collect();
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let impl_from_lua = quote! { let impl_from_lua = quote! {
impl #impl_generics mlua::FromLua for #name #type_generics #where_clause { impl mlua::FromLua for #name {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> { fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
if !value.is_table() { if !value.is_table() {
panic!("Expected table"); panic!("Expected table");

View File

@ -90,9 +90,9 @@ automation.device_manager:add(IkeaRemote.new({
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("living/remote"), topic = mqtt_z2m("living/remote"),
single_button = true, single_button = true,
callback = function(_, on) callback = function(on)
if on then if on then
if living_mixer:on() then if living_mixer:is_on() then
living_mixer:set_on(false) living_mixer:set_on(false)
living_speakers:set_on(false) living_speakers:set_on(false)
else else
@ -100,10 +100,10 @@ automation.device_manager:add(IkeaRemote.new({
living_speakers:set_on(true) living_speakers:set_on(true)
end end
else else
if not living_mixer:on() then if not living_mixer:is_on() then
living_mixer:set_on(true) living_mixer:set_on(true)
else else
living_speakers:set_on(not living_speakers:on()) living_speakers:set_on(not living_speakers:is_on())
end end
end end
end, end,
@ -133,7 +133,7 @@ local kettle = IkeaOutlet.new({
}) })
automation.device_manager:add(kettle) automation.device_manager:add(kettle)
local function set_kettle(_, on) local function set_kettle(on)
kettle:set_on(on) kettle:set_on(on)
end end
@ -155,7 +155,8 @@ automation.device_manager:add(IkeaRemote.new({
callback = set_kettle, callback = set_kettle,
})) }))
automation.device_manager:add(LightOnOff.new({ automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Light",
name = "Light", name = "Light",
room = "Bathroom", room = "Bathroom",
topic = mqtt_z2m("bathroom/light"), topic = mqtt_z2m("bathroom/light"),
@ -201,7 +202,7 @@ automation.device_manager:add(HueSwitch.new({
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("hallway/switchbottom"), topic = mqtt_z2m("hallway/switchbottom"),
left_callback = function() left_callback = function()
hallway_top_light:set_on(not hallway_top_light:on()) hallway_top_light:set_on(not hallway_top_light:is_on())
end, end,
})) }))
automation.device_manager:add(HueSwitch.new({ automation.device_manager:add(HueSwitch.new({
@ -210,69 +211,10 @@ automation.device_manager:add(HueSwitch.new({
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("hallway/switchtop"), topic = mqtt_z2m("hallway/switchtop"),
left_callback = function() left_callback = function()
hallway_top_light:set_on(not hallway_top_light:on()) hallway_top_light:set_on(not hallway_top_light:is_on())
end, end,
})) }))
local hallway_light_automation = {
timeout = Timeout.new(),
state = {
door_open = false,
trash_open = false,
forced = false,
},
switch_callback = function(self, on)
self.timeout:cancel()
self.group.set_on(on)
self.state.forced = on
end,
door_callback = function(self, open)
self.state.door_open = open
if open then
self.timeout:cancel()
self.group.set_on(true)
elseif not self.state.forced then
self.timeout:start(debug and 10 or 60, function()
if not self.state.trash_open then
self.group.set_on(false)
end
end)
end
end,
trash_callback = function(self, open)
self.state.trash_open = open
if open then
self.group.set_on(true)
else
if not self.timeout:is_waiting() and not self.state.door_open and not self.state.forced then
self.group.set_on(false)
end
end
end,
light_callback = function(self, on)
if on and not self.state.trash_open and not self.state.door_open then
-- If the door and trash are not open, that means the light got turned on manually
self.timeout:cancel()
self.state.forced = true
elseif not on then
-- The light is never forced when it is off
self.state.forced = false
end
end,
}
local hallway_storage = LightBrightness.new({
name = "Storage",
room = "Hallway",
topic = mqtt_z2m("hallway/storage"),
client = mqtt_client,
callback = function(_, state)
hallway_light_automation:light_callback(state.state)
end,
})
automation.device_manager:add(hallway_storage)
local hallway_bottom_lights = HueGroup.new({ local hallway_bottom_lights = HueGroup.new({
identifier = "hallway_bottom_lights", identifier = "hallway_bottom_lights",
ip = hue_ip, ip = hue_ip,
@ -283,14 +225,42 @@ local hallway_bottom_lights = HueGroup.new({
}) })
automation.device_manager:add(hallway_bottom_lights) automation.device_manager:add(hallway_bottom_lights)
hallway_light_automation.group = { local hallway_light_automation = {
set_on = function(on) group = hallway_bottom_lights,
if on then timeout = Timeout.new(),
hallway_storage:set_brightness(80) state = {
else door_open = false,
hallway_storage:set_on(false) trash_open = false,
forced = false,
},
switch_callback = function(self, on)
self.timeout:cancel()
self.group:set_on(on)
self.state.forced = on
end,
door_callback = function(self, open)
self.state.door_open = open
if open then
self.timeout:cancel()
self.group:set_on(true)
elseif not self.state.forced then
self.timeout:start(debug and 10 or 60, function()
if not self.state.trash_open then
self.group:set_on(false)
end
end)
end
end,
trash_callback = function(self, open)
self.state.trash_open = open
if open then
self.group:set_on(true)
else
if not self.timeout:is_waiting() and not self.state.door_open and not self.state.forced then
self.group:set_on(false)
end
end end
hallway_bottom_lights:set_on(on)
end, end,
} }
@ -299,7 +269,7 @@ automation.device_manager:add(IkeaRemote.new({
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("hallway/remote"), topic = mqtt_z2m("hallway/remote"),
callback = function(_, on) callback = function(on)
hallway_light_automation:switch_callback(on) hallway_light_automation:switch_callback(on)
end, end,
})) }))
@ -311,7 +281,7 @@ automation.device_manager:add(ContactSensor.new({
topic = mqtt_automation("presence/contact/frontdoor"), topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60, timeout = debug and 10 or 15 * 60,
}, },
callback = function(_, open) callback = function(open)
hallway_light_automation:door_callback(open) hallway_light_automation:door_callback(open)
end, end,
})) }))
@ -319,12 +289,13 @@ automation.device_manager:add(ContactSensor.new({
identifier = "hallway_trash", identifier = "hallway_trash",
topic = mqtt_z2m("hallway/trash"), topic = mqtt_z2m("hallway/trash"),
client = mqtt_client, client = mqtt_client,
callback = function(_, open) callback = function(open)
hallway_light_automation:trash_callback(open) hallway_light_automation:trash_callback(open)
end, end,
})) }))
automation.device_manager:add(LightOnOff.new({ automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Light",
name = "Light", name = "Light",
room = "Guest", room = "Guest",
topic = mqtt_z2m("guest/light"), topic = mqtt_z2m("guest/light"),

View File

@ -14,11 +14,6 @@ traits! {
async fn on(&self) -> Result<bool, ErrorCode>, async fn on(&self) -> Result<bool, ErrorCode>,
"action.devices.commands.OnOff" => async fn set_on(&self, on: bool) -> Result<(), ErrorCode>, "action.devices.commands.OnOff" => async fn set_on(&self, on: bool) -> Result<(), ErrorCode>,
}, },
"action.devices.traits.Brightness" => trait Brightness {
command_only_brightness: Option<bool>,
async fn brightness(&self) -> Result<u8, ErrorCode>,
"action.devices.commands.BrightnessAbsolute" => async fn set_brightness(&self, brightness: u8) -> Result<(), ErrorCode>,
},
"action.devices.traits.Scene" => trait Scene { "action.devices.traits.Scene" => trait Scene {
scene_reversible: Option<bool>, scene_reversible: Option<bool>,