Compare commits
2 Commits
master
...
d3f9feb96f
| Author | SHA1 | Date | |
|---|---|---|---|
|
d3f9feb96f
|
|||
|
4a83250258
|
@@ -36,6 +36,9 @@ pub struct Config {
|
|||||||
|
|
||||||
#[device_config(from_lua, default)]
|
#[device_config(from_lua, default)]
|
||||||
pub callback: ActionCallback<ContactSensor, bool>,
|
pub callback: ActionCallback<ContactSensor, bool>,
|
||||||
|
#[device_config(from_lua, default)]
|
||||||
|
pub battery_callback: ActionCallback<ContactSensor, f32>,
|
||||||
|
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub client: WrappedAsyncClient,
|
pub client: WrappedAsyncClient,
|
||||||
}
|
}
|
||||||
@@ -149,21 +152,27 @@ impl OnMqtt for ContactSensor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_closed = match ContactMessage::try_from(message) {
|
let message = match ContactMessage::try_from(message) {
|
||||||
Ok(state) => state.is_closed(),
|
Ok(message) => message,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = self.get_id(), "Failed to parse message: {err}");
|
error!(id = self.get_id(), "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_closed == self.state().await.is_closed {
|
if let Some(is_closed) = message.contact {
|
||||||
return;
|
if is_closed == self.state().await.is_closed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.config.callback.call(self, &!is_closed).await;
|
||||||
|
|
||||||
|
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
||||||
|
self.state_mut().await.is_closed = is_closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.config.callback.call(self, &!is_closed).await;
|
if let Some(battery) = message.battery {
|
||||||
|
self.config.battery_callback.call(self, &battery).await;
|
||||||
debug!(id = self.get_id(), "Updating state to {is_closed}");
|
}
|
||||||
self.state_mut().await.is_closed = is_closed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ impl AddAdditionalMethods for HueBridge {
|
|||||||
{
|
{
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"set_flag",
|
"set_flag",
|
||||||
|lua, this, (flag, value): (mlua::Value, bool)| async move {
|
async |lua, this, (flag, value): (mlua::Value, bool)| {
|
||||||
let flag: Flag = lua.from_value(flag)?;
|
let flag: Flag = lua.from_value(flag)?;
|
||||||
|
|
||||||
this.set_flag(flag, value).await;
|
this.set_flag(flag, value).await;
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ pub struct Config {
|
|||||||
|
|
||||||
#[device_config(from_lua, default)]
|
#[device_config(from_lua, default)]
|
||||||
pub right_hold_callback: ActionCallback<HueSwitch, ()>,
|
pub right_hold_callback: ActionCallback<HueSwitch, ()>,
|
||||||
|
|
||||||
|
#[device_config(from_lua, default)]
|
||||||
|
pub battery_callback: ActionCallback<HueSwitch, f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
enum Action {
|
enum Action {
|
||||||
LeftPress,
|
LeftPress,
|
||||||
@@ -48,7 +51,8 @@ enum Action {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
struct State {
|
struct State {
|
||||||
action: Action,
|
action: Option<Action>,
|
||||||
|
battery: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDevice)]
|
#[derive(Debug, Clone, LuaDevice)]
|
||||||
@@ -84,32 +88,43 @@ 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 device 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::<State>(&message.payload) {
|
let message = match serde_json::from_slice::<State>(&message.payload) {
|
||||||
Ok(message) => message.action,
|
Ok(message) => message,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
|
warn!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
|
||||||
|
|
||||||
match action {
|
if let Some(action) = message.action {
|
||||||
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await,
|
debug!(
|
||||||
Action::RightPressRelease => self.config.right_callback.call(self, &()).await,
|
id = Device::get_id(self),
|
||||||
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await,
|
?message.action,
|
||||||
Action::RightHold => self.config.right_hold_callback.call(self, &()).await,
|
"Action received",
|
||||||
// If there is no hold action, the switch will act like a normal release
|
);
|
||||||
Action::RightHoldRelease => {
|
|
||||||
if !self.config.right_hold_callback.is_set() {
|
match action {
|
||||||
self.config.right_callback.call(self, &()).await
|
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await,
|
||||||
|
Action::RightPressRelease => self.config.right_callback.call(self, &()).await,
|
||||||
|
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await,
|
||||||
|
Action::RightHold => self.config.right_hold_callback.call(self, &()).await,
|
||||||
|
// If there is no hold action, the switch will act like a normal release
|
||||||
|
Action::RightHoldRelease => {
|
||||||
|
if !self.config.right_hold_callback.is_set() {
|
||||||
|
self.config.right_callback.call(self, &()).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Action::LeftHoldRelease => {
|
||||||
Action::LeftHoldRelease => {
|
if !self.config.left_hold_callback.is_set() {
|
||||||
if !self.config.left_hold_callback.is_set() {
|
self.config.left_callback.call(self, &()).await
|
||||||
self.config.left_callback.call(self, &()).await
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
_ => {}
|
}
|
||||||
|
|
||||||
|
if let Some(battery) = message.battery {
|
||||||
|
self.config.battery_callback.call(self, &battery).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pub struct Config {
|
|||||||
|
|
||||||
#[device_config(from_lua)]
|
#[device_config(from_lua)]
|
||||||
pub callback: ActionCallback<IkeaRemote, bool>,
|
pub callback: ActionCallback<IkeaRemote, bool>,
|
||||||
|
#[device_config(from_lua, default)]
|
||||||
|
pub battery_callback: ActionCallback<IkeaRemote, f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, LuaDevice)]
|
#[derive(Debug, Clone, LuaDevice)]
|
||||||
@@ -60,31 +62,38 @@ impl OnMqtt for IkeaRemote {
|
|||||||
async fn on_mqtt(&self, message: Publish) {
|
async fn on_mqtt(&self, message: Publish) {
|
||||||
// Check if the message is from the deviec 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 RemoteMessage::try_from(message) {
|
let message = match RemoteMessage::try_from(message) {
|
||||||
Ok(message) => message.action(),
|
Ok(message) => message,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
|
||||||
|
|
||||||
let on = if self.config.single_button {
|
if let Some(action) = message.action {
|
||||||
match action {
|
debug!(id = Device::get_id(self), "Remote action = {:?}", action);
|
||||||
RemoteAction::On => Some(true),
|
|
||||||
RemoteAction::BrightnessMoveUp => Some(false),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match action {
|
|
||||||
RemoteAction::On => Some(true),
|
|
||||||
RemoteAction::Off => Some(false),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(on) = on {
|
let on = if self.config.single_button {
|
||||||
self.config.callback.call(self, &on).await;
|
match action {
|
||||||
|
RemoteAction::On => Some(true),
|
||||||
|
RemoteAction::BrightnessMoveUp => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match action {
|
||||||
|
RemoteAction::On => Some(true),
|
||||||
|
RemoteAction::Off => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(on) = on {
|
||||||
|
self.config.callback.call(self, &on).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(battery) = message.battery {
|
||||||
|
self.config.battery_callback.call(self, &battery).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ impl AddAdditionalMethods for Ntfy {
|
|||||||
{
|
{
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"send_notification",
|
"send_notification",
|
||||||
|lua, this, notification: mlua::Value| async move {
|
async |lua, this, notification: mlua::Value| {
|
||||||
let notification: Notification = lua.from_value(notification)?;
|
let notification: Notification = lua.from_value(notification)?;
|
||||||
|
|
||||||
this.send(notification).await;
|
this.send(notification).await;
|
||||||
|
|||||||
@@ -73,22 +73,19 @@ impl DeviceManager {
|
|||||||
match event {
|
match event {
|
||||||
Event::MqttMessage(message) => {
|
Event::MqttMessage(message) => {
|
||||||
let devices = self.devices.read().await;
|
let devices = self.devices.read().await;
|
||||||
let iter = devices.iter().map(|(id, device)| {
|
let iter = devices.iter().map(async |(id, device)| {
|
||||||
let message = message.clone();
|
let device: Option<&dyn OnMqtt> = device.cast();
|
||||||
async move {
|
if let Some(device) = device {
|
||||||
let device: Option<&dyn OnMqtt> = device.cast();
|
// let subscribed = device
|
||||||
if let Some(device) = device {
|
// .topics()
|
||||||
// let subscribed = device
|
// .iter()
|
||||||
// .topics()
|
// .any(|topic| matches(&message.topic, topic));
|
||||||
// .iter()
|
//
|
||||||
// .any(|topic| matches(&message.topic, topic));
|
// if subscribed {
|
||||||
//
|
trace!(id, "Handling");
|
||||||
// if subscribed {
|
device.on_mqtt(message.clone()).await;
|
||||||
trace!(id, "Handling");
|
trace!(id, "Done");
|
||||||
device.on_mqtt(message).await;
|
// }
|
||||||
trace!(id, "Done");
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,7 +97,7 @@ impl DeviceManager {
|
|||||||
|
|
||||||
impl mlua::UserData for DeviceManager {
|
impl mlua::UserData for DeviceManager {
|
||||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_async_method("add", |_lua, this, device: Box<dyn Device>| async move {
|
methods.add_async_method("add", async |_lua, this, device: Box<dyn Device>| {
|
||||||
this.add(device).await;
|
this.add(device).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -108,7 +105,7 @@ impl mlua::UserData for DeviceManager {
|
|||||||
|
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"schedule",
|
"schedule",
|
||||||
|lua, this, (schedule, f): (String, mlua::Function)| async move {
|
async |lua, this, (schedule, f): (String, mlua::Function)| {
|
||||||
debug!("schedule = {schedule}");
|
debug!("schedule = {schedule}");
|
||||||
// This creates a function, that returns the actual job we want to run
|
// This creates a function, that returns the actual job we want to run
|
||||||
let create_job = {
|
let create_job = {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl mlua::UserData for Timeout {
|
|||||||
|
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"start",
|
"start",
|
||||||
|_lua, this, (timeout, callback): (f32, ActionCallback<mlua::Value, bool>)| async move {
|
async |_lua, this, (timeout, callback): (f32, ActionCallback<mlua::Value, bool>)| {
|
||||||
if let Some(handle) = this.state.write().await.handle.take() {
|
if let Some(handle) = this.state.write().await.handle.take() {
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ impl mlua::UserData for Timeout {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
methods.add_async_method("cancel", |_lua, this, ()| async move {
|
methods.add_async_method("cancel", async |_lua, this, ()| {
|
||||||
debug!("Canceling timeout callback");
|
debug!("Canceling timeout callback");
|
||||||
|
|
||||||
if let Some(handle) = this.state.write().await.handle.take() {
|
if let Some(handle) = this.state.write().await.handle.take() {
|
||||||
@@ -60,7 +60,7 @@ impl mlua::UserData for Timeout {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_async_method("is_waiting", |_lua, this, ()| async move {
|
methods.add_async_method("is_waiting", async |_lua, this, ()| {
|
||||||
debug!("Canceling timeout callback");
|
debug!("Canceling timeout callback");
|
||||||
|
|
||||||
if let Some(handle) = this.state.read().await.handle.as_ref() {
|
if let Some(handle) = this.state.read().await.handle.as_ref() {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ pub trait OnOff {
|
|||||||
where
|
where
|
||||||
Self: Sized + google_home::traits::OnOff + 'static,
|
Self: Sized + google_home::traits::OnOff + 'static,
|
||||||
{
|
{
|
||||||
methods.add_async_method("set_on", |_lua, this, on: bool| async move {
|
methods.add_async_method("set_on", async |_lua, this, on: bool| {
|
||||||
this.deref().set_on(on).await.unwrap();
|
this.deref().set_on(on).await.unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_async_method("on", |_lua, this, ()| async move {
|
methods.add_async_method("on", async |_lua, this, ()| {
|
||||||
Ok(this.deref().on().await.unwrap())
|
Ok(this.deref().on().await.unwrap())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -25,13 +25,13 @@ pub trait Brightness {
|
|||||||
where
|
where
|
||||||
Self: Sized + google_home::traits::Brightness + 'static,
|
Self: Sized + google_home::traits::Brightness + 'static,
|
||||||
{
|
{
|
||||||
methods.add_async_method("set_brightness", |_lua, this, brightness: u8| async move {
|
methods.add_async_method("set_brightness", async |_lua, this, brightness: u8| {
|
||||||
this.set_brightness(brightness).await.unwrap();
|
this.set_brightness(brightness).await.unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_async_method("brightness", |_lua, this, _: ()| async move {
|
methods.add_async_method("brightness", async |_lua, this, _: ()| {
|
||||||
Ok(this.brightness().await.unwrap())
|
Ok(this.brightness().await.unwrap())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ pub trait ColorSetting {
|
|||||||
{
|
{
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"set_color_temperature",
|
"set_color_temperature",
|
||||||
|_lua, this, temperature: u32| async move {
|
async |_lua, this, temperature: u32| {
|
||||||
this.set_color(google_home::traits::Color { temperature })
|
this.set_color(google_home::traits::Color { temperature })
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -54,7 +54,7 @@ pub trait ColorSetting {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
methods.add_async_method("color_temperature", |_lua, this, ()| async move {
|
methods.add_async_method("color_temperature", async |_lua, this, ()| {
|
||||||
Ok(this.color().await.temperature)
|
Ok(this.color().await.temperature)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -66,16 +66,13 @@ pub trait OpenClose {
|
|||||||
where
|
where
|
||||||
Self: Sized + google_home::traits::OpenClose + 'static,
|
Self: Sized + google_home::traits::OpenClose + 'static,
|
||||||
{
|
{
|
||||||
methods.add_async_method(
|
methods.add_async_method("set_open_percent", async |_lua, this, open_percent: u8| {
|
||||||
"set_open_percent",
|
this.set_open_percent(open_percent).await.unwrap();
|
||||||
|_lua, this, open_percent: u8| async move {
|
|
||||||
this.set_open_percent(open_percent).await.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
methods.add_async_method("open_percent", |_lua, this, _: ()| async move {
|
methods.add_async_method("open_percent", async |_lua, this, _: ()| {
|
||||||
Ok(this.open_percent().await.unwrap())
|
Ok(this.open_percent().await.unwrap())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,13 +68,8 @@ pub enum RemoteAction {
|
|||||||
// Message used to report the action performed by a remote
|
// Message used to report the action performed by a remote
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RemoteMessage {
|
pub struct RemoteMessage {
|
||||||
action: RemoteAction,
|
pub action: Option<RemoteAction>,
|
||||||
}
|
pub battery: Option<f32>,
|
||||||
|
|
||||||
impl RemoteMessage {
|
|
||||||
pub fn action(&self) -> RemoteAction {
|
|
||||||
self.action
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Publish> for RemoteMessage {
|
impl TryFrom<Publish> for RemoteMessage {
|
||||||
@@ -144,13 +139,8 @@ impl TryFrom<Publish> for BrightnessMessage {
|
|||||||
// Message to report the state of a contact sensor
|
// Message to report the state of a contact sensor
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ContactMessage {
|
pub struct ContactMessage {
|
||||||
contact: bool,
|
pub contact: Option<bool>,
|
||||||
}
|
pub battery: Option<f32>,
|
||||||
|
|
||||||
impl ContactMessage {
|
|
||||||
pub fn is_closed(&self) -> bool {
|
|
||||||
self.contact
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Publish> for ContactMessage {
|
impl TryFrom<Publish> for ContactMessage {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl mlua::UserData for WrappedAsyncClient {
|
|||||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_async_method(
|
methods.add_async_method(
|
||||||
"send_message",
|
"send_message",
|
||||||
|_lua, this, (topic, message): (String, mlua::Value)| async move {
|
async |_lua, this, (topic, message): (String, mlua::Value)| {
|
||||||
let message = serde_json::to_string(&message).unwrap();
|
let message = serde_json::to_string(&message).unwrap();
|
||||||
|
|
||||||
debug!("message = {message}");
|
debug!("message = {message}");
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ impl Impl {
|
|||||||
quote! {
|
quote! {
|
||||||
impl mlua::UserData for #name #generics {
|
impl mlua::UserData for #name #generics {
|
||||||
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
|
||||||
methods.add_async_function("new", |_lua, config| async {
|
methods.add_async_function("new", async |_lua, config| {
|
||||||
let device: Self = LuaDeviceCreate::create(config)
|
let device: Self = LuaDeviceCreate::create(config)
|
||||||
.await
|
.await
|
||||||
.map_err(mlua::ExternalError::into_lua_err)?;
|
.map_err(mlua::ExternalError::into_lua_err)?;
|
||||||
@@ -58,7 +58,7 @@ impl Impl {
|
|||||||
Ok(b)
|
Ok(b)
|
||||||
});
|
});
|
||||||
|
|
||||||
methods.add_async_method("get_id", |_lua, this, _: ()| async move { Ok(this.get_id()) });
|
methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) });
|
||||||
|
|
||||||
#(
|
#(
|
||||||
#traits::add_methods(methods);
|
#traits::add_methods(methods);
|
||||||
|
|||||||
47
config.lua
47
config.lua
@@ -34,6 +34,37 @@ local ntfy = Ntfy.new({
|
|||||||
})
|
})
|
||||||
automation.device_manager:add(ntfy)
|
automation.device_manager:add(ntfy)
|
||||||
|
|
||||||
|
local low_battery = {}
|
||||||
|
local function check_battery(device, battery)
|
||||||
|
local id = device:get_id()
|
||||||
|
if battery < 15 then
|
||||||
|
print("Device '" .. id .. "' has low battery: " .. tostring(battery))
|
||||||
|
low_battery[id] = battery
|
||||||
|
else
|
||||||
|
low_battery[id] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
automation.device_manager:schedule("0 0 21 */1 * *", function()
|
||||||
|
-- Don't send notifications if there are now devices with low battery
|
||||||
|
if next(low_battery) == nil then
|
||||||
|
print("No devices with low battery")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local lines = {}
|
||||||
|
for name, battery in pairs(low_battery) do
|
||||||
|
table.insert(lines, name .. ": " .. tostring(battery) .. "%")
|
||||||
|
end
|
||||||
|
local message = table.concat(lines, "\n")
|
||||||
|
|
||||||
|
ntfy:send_notification({
|
||||||
|
title = "Low battery",
|
||||||
|
message = message,
|
||||||
|
tags = { "battery" },
|
||||||
|
priority = "default",
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
local on_presence = {
|
local on_presence = {
|
||||||
add = function(self, f)
|
add = function(self, f)
|
||||||
self[#self + 1] = f
|
self[#self + 1] = f
|
||||||
@@ -171,6 +202,7 @@ automation.device_manager:add(HueSwitch.new({
|
|||||||
right_hold_callback = function()
|
right_hold_callback = function()
|
||||||
living_lights_relax:set_on(true)
|
living_lights_relax:set_on(true)
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
automation.device_manager:add(WakeOnLAN.new({
|
automation.device_manager:add(WakeOnLAN.new({
|
||||||
@@ -222,6 +254,7 @@ automation.device_manager:add(IkeaRemote.new({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local function kettle_timeout()
|
local function kettle_timeout()
|
||||||
@@ -260,6 +293,7 @@ automation.device_manager:add(IkeaRemote.new({
|
|||||||
topic = mqtt_z2m("bedroom/remote"),
|
topic = mqtt_z2m("bedroom/remote"),
|
||||||
single_button = true,
|
single_button = true,
|
||||||
callback = set_kettle,
|
callback = set_kettle,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
automation.device_manager:add(IkeaRemote.new({
|
automation.device_manager:add(IkeaRemote.new({
|
||||||
@@ -269,6 +303,7 @@ automation.device_manager:add(IkeaRemote.new({
|
|||||||
topic = mqtt_z2m("kitchen/remote"),
|
topic = mqtt_z2m("kitchen/remote"),
|
||||||
single_button = true,
|
single_button = true,
|
||||||
callback = set_kettle,
|
callback = set_kettle,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local function off_timeout(duration)
|
local function off_timeout(duration)
|
||||||
@@ -356,6 +391,7 @@ automation.device_manager:add(IkeaRemote.new({
|
|||||||
workbench_light:set_on(false)
|
workbench_light:set_on(false)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local hallway_top_light = HueGroup.new({
|
local hallway_top_light = HueGroup.new({
|
||||||
@@ -373,6 +409,7 @@ automation.device_manager:add(HueSwitch.new({
|
|||||||
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:on())
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
automation.device_manager:add(HueSwitch.new({
|
automation.device_manager:add(HueSwitch.new({
|
||||||
name = "SwitchTop",
|
name = "SwitchTop",
|
||||||
@@ -382,6 +419,7 @@ automation.device_manager:add(HueSwitch.new({
|
|||||||
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:on())
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local hallway_light_automation = {
|
local hallway_light_automation = {
|
||||||
@@ -488,6 +526,7 @@ automation.device_manager:add(IkeaRemote.new({
|
|||||||
callback = function(_, on)
|
callback = function(_, on)
|
||||||
hallway_light_automation:switch_callback(on)
|
hallway_light_automation:switch_callback(on)
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
local hallway_frontdoor = ContactSensor.new({
|
local hallway_frontdoor = ContactSensor.new({
|
||||||
name = "Frontdoor",
|
name = "Frontdoor",
|
||||||
@@ -503,6 +542,7 @@ local hallway_frontdoor = ContactSensor.new({
|
|||||||
hallway_light_automation:door_callback(open)
|
hallway_light_automation:door_callback(open)
|
||||||
frontdoor_presence(open)
|
frontdoor_presence(open)
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
})
|
})
|
||||||
automation.device_manager:add(hallway_frontdoor)
|
automation.device_manager:add(hallway_frontdoor)
|
||||||
hallway_light_automation.door = hallway_frontdoor
|
hallway_light_automation.door = hallway_frontdoor
|
||||||
@@ -516,6 +556,7 @@ local hallway_trash = ContactSensor.new({
|
|||||||
callback = function(_, open)
|
callback = function(_, open)
|
||||||
hallway_light_automation:trash_callback(open)
|
hallway_light_automation:trash_callback(open)
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
})
|
})
|
||||||
automation.device_manager:add(hallway_trash)
|
automation.device_manager:add(hallway_trash)
|
||||||
hallway_light_automation.trash = hallway_trash
|
hallway_light_automation.trash = hallway_trash
|
||||||
@@ -564,6 +605,7 @@ automation.device_manager:add(HueSwitch.new({
|
|||||||
left_hold_callback = function()
|
left_hold_callback = function()
|
||||||
bedroom_lights_relax:set_on(true)
|
bedroom_lights_relax:set_on(true)
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
automation.device_manager:add(ContactSensor.new({
|
automation.device_manager:add(ContactSensor.new({
|
||||||
@@ -572,24 +614,28 @@ automation.device_manager:add(ContactSensor.new({
|
|||||||
sensor_type = "Door",
|
sensor_type = "Door",
|
||||||
topic = mqtt_z2m("living/balcony"),
|
topic = mqtt_z2m("living/balcony"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
automation.device_manager:add(ContactSensor.new({
|
automation.device_manager:add(ContactSensor.new({
|
||||||
name = "Window",
|
name = "Window",
|
||||||
room = "Living Room",
|
room = "Living Room",
|
||||||
topic = mqtt_z2m("living/window"),
|
topic = mqtt_z2m("living/window"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
automation.device_manager:add(ContactSensor.new({
|
automation.device_manager:add(ContactSensor.new({
|
||||||
name = "Window",
|
name = "Window",
|
||||||
room = "Bedroom",
|
room = "Bedroom",
|
||||||
topic = mqtt_z2m("bedroom/window"),
|
topic = mqtt_z2m("bedroom/window"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
automation.device_manager:add(ContactSensor.new({
|
automation.device_manager:add(ContactSensor.new({
|
||||||
name = "Window",
|
name = "Window",
|
||||||
room = "Guest Room",
|
room = "Guest Room",
|
||||||
topic = mqtt_z2m("guest/window"),
|
topic = mqtt_z2m("guest/window"),
|
||||||
client = mqtt_client,
|
client = mqtt_client,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
local storage_light = LightBrightness.new({
|
local storage_light = LightBrightness.new({
|
||||||
@@ -614,6 +660,7 @@ automation.device_manager:add(ContactSensor.new({
|
|||||||
storage_light:set_on(false)
|
storage_light:set_on(false)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
battery_callback = check_battery,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
automation.device_manager:schedule("0 0 19 * * *", function()
|
automation.device_manager:schedule("0 0 19 * * *", function()
|
||||||
|
|||||||
@@ -40,15 +40,13 @@ impl GoogleHome {
|
|||||||
let intent = request.inputs.into_iter().next();
|
let intent = request.inputs.into_iter().next();
|
||||||
|
|
||||||
let payload: OptionFuture<_> = intent
|
let payload: OptionFuture<_> = intent
|
||||||
.map(|intent| async move {
|
.map(async |intent| match intent {
|
||||||
match intent {
|
Intent::Sync => ResponsePayload::Sync(self.sync(devices).await),
|
||||||
Intent::Sync => ResponsePayload::Sync(self.sync(devices).await),
|
Intent::Query(payload) => {
|
||||||
Intent::Query(payload) => {
|
ResponsePayload::Query(self.query(payload, devices).await)
|
||||||
ResponsePayload::Query(self.query(payload, devices).await)
|
}
|
||||||
}
|
Intent::Execute(payload) => {
|
||||||
Intent::Execute(payload) => {
|
ResponsePayload::Execute(self.execute(payload, devices).await)
|
||||||
ResponsePayload::Execute(self.execute(payload, devices).await)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
@@ -64,7 +62,7 @@ impl GoogleHome {
|
|||||||
devices: &HashMap<String, Box<T>>,
|
devices: &HashMap<String, Box<T>>,
|
||||||
) -> sync::Payload {
|
) -> sync::Payload {
|
||||||
let mut resp_payload = sync::Payload::new(&self.user_id);
|
let mut resp_payload = sync::Payload::new(&self.user_id);
|
||||||
let f = devices.values().map(|device| async move {
|
let f = devices.values().map(async |device| {
|
||||||
if let Some(device) = device.as_ref().cast() {
|
if let Some(device) = device.as_ref().cast() {
|
||||||
Some(Device::sync(device).await)
|
Some(Device::sync(device).await)
|
||||||
} else {
|
} else {
|
||||||
@@ -86,7 +84,7 @@ impl GoogleHome {
|
|||||||
.devices
|
.devices
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|device| device.id)
|
.map(|device| device.id)
|
||||||
.map(|id| async move {
|
.map(async |id| {
|
||||||
// NOTE: Requires let_chains feature
|
// NOTE: Requires let_chains feature
|
||||||
let device = if let Some(device) = devices.get(id.as_str())
|
let device = if let Some(device) = devices.get(id.as_str())
|
||||||
&& let Some(device) = device.as_ref().cast()
|
&& let Some(device) = device.as_ref().cast()
|
||||||
@@ -115,84 +113,77 @@ impl GoogleHome {
|
|||||||
) -> execute::Payload {
|
) -> execute::Payload {
|
||||||
let resp_payload = Arc::new(Mutex::new(response::execute::Payload::new()));
|
let resp_payload = Arc::new(Mutex::new(response::execute::Payload::new()));
|
||||||
|
|
||||||
let f = payload.commands.into_iter().map(|command| {
|
let f = payload.commands.into_iter().map(async |command| {
|
||||||
let resp_payload = resp_payload.clone();
|
let mut success = response::execute::Command::new(execute::Status::Success);
|
||||||
async move {
|
success.states = Some(execute::States {
|
||||||
let mut success = response::execute::Command::new(execute::Status::Success);
|
online: true,
|
||||||
success.states = Some(execute::States {
|
state: Default::default(),
|
||||||
online: true,
|
});
|
||||||
state: Default::default(),
|
let mut offline = response::execute::Command::new(execute::Status::Offline);
|
||||||
});
|
offline.states = Some(execute::States {
|
||||||
let mut offline = response::execute::Command::new(execute::Status::Offline);
|
online: false,
|
||||||
offline.states = Some(execute::States {
|
state: Default::default(),
|
||||||
online: false,
|
});
|
||||||
state: Default::default(),
|
let mut errors: HashMap<ErrorCode, response::execute::Command> = HashMap::new();
|
||||||
});
|
|
||||||
let mut errors: HashMap<ErrorCode, response::execute::Command> = HashMap::new();
|
|
||||||
|
|
||||||
let f = command
|
let f = command
|
||||||
.devices
|
.devices
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|device| device.id)
|
.map(|device| device.id)
|
||||||
.map(|id| {
|
.map(async |id| {
|
||||||
let execution = command.execution.clone();
|
if let Some(device) = devices.get(id.as_str())
|
||||||
async move {
|
&& let Some(device) = device.as_ref().cast()
|
||||||
if let Some(device) = devices.get(id.as_str())
|
{
|
||||||
&& let Some(device) = device.as_ref().cast()
|
if !device.is_online().await {
|
||||||
{
|
return (id, Ok(false));
|
||||||
if !device.is_online().await {
|
|
||||||
return (id, Ok(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: We can not use .map here because async =(
|
|
||||||
let mut results = Vec::new();
|
|
||||||
for cmd in &execution {
|
|
||||||
results.push(Device::execute(device, cmd.clone()).await);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert vec of results to a result with a vec and the first
|
|
||||||
// encountered error
|
|
||||||
let results =
|
|
||||||
results.into_iter().collect::<Result<Vec<_>, ErrorCode>>();
|
|
||||||
|
|
||||||
// TODO: We only get one error not all errors
|
|
||||||
if let Err(err) = results {
|
|
||||||
(id, Err(err))
|
|
||||||
} else {
|
|
||||||
(id, Ok(true))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(id.clone(), Err(DeviceError::DeviceNotFound.into()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
let a = join_all(f).await;
|
// NOTE: We can not use .map here because async =(
|
||||||
a.into_iter().for_each(|(id, state)| {
|
let mut results = Vec::new();
|
||||||
match state {
|
for cmd in &command.execution {
|
||||||
Ok(true) => success.add_id(&id),
|
results.push(Device::execute(device, cmd.clone()).await);
|
||||||
Ok(false) => offline.add_id(&id),
|
}
|
||||||
Err(err) => errors
|
|
||||||
.entry(err)
|
// Convert vec of results to a result with a vec and the first
|
||||||
.or_insert_with(|| match &err {
|
// encountered error
|
||||||
ErrorCode::DeviceError(_) => {
|
let results = results.into_iter().collect::<Result<Vec<_>, ErrorCode>>();
|
||||||
response::execute::Command::new(execute::Status::Error)
|
|
||||||
}
|
// TODO: We only get one error not all errors
|
||||||
ErrorCode::DeviceException(_) => {
|
if let Err(err) = results {
|
||||||
response::execute::Command::new(execute::Status::Exceptions)
|
(id, Err(err))
|
||||||
}
|
} else {
|
||||||
})
|
(id, Ok(true))
|
||||||
.add_id(&id),
|
}
|
||||||
};
|
} else {
|
||||||
|
(id.clone(), Err(DeviceError::DeviceNotFound.into()))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut resp_payload = resp_payload.lock().await;
|
let a = join_all(f).await;
|
||||||
resp_payload.add_command(success);
|
a.into_iter().for_each(|(id, state)| {
|
||||||
resp_payload.add_command(offline);
|
match state {
|
||||||
for (error, mut cmd) in errors {
|
Ok(true) => success.add_id(&id),
|
||||||
cmd.error_code = Some(error);
|
Ok(false) => offline.add_id(&id),
|
||||||
resp_payload.add_command(cmd);
|
Err(err) => errors
|
||||||
}
|
.entry(err)
|
||||||
|
.or_insert_with(|| match &err {
|
||||||
|
ErrorCode::DeviceError(_) => {
|
||||||
|
response::execute::Command::new(execute::Status::Error)
|
||||||
|
}
|
||||||
|
ErrorCode::DeviceException(_) => {
|
||||||
|
response::execute::Command::new(execute::Status::Exceptions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add_id(&id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut resp_payload = resp_payload.lock().await;
|
||||||
|
resp_payload.add_command(success);
|
||||||
|
resp_payload.add_command(offline);
|
||||||
|
for (error, mut cmd) in errors {
|
||||||
|
cmd.error_code = Some(error);
|
||||||
|
resp_payload.add_command(cmd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user