Move ntfy and presence to automation_devices

This commit is contained in:
2025-08-31 04:57:31 +02:00
parent 03dcd44e0e
commit eb36d41f17
9 changed files with 25 additions and 31 deletions

View File

@@ -10,6 +10,4 @@ pub mod helpers;
pub mod lua;
pub mod messages;
pub mod mqtt;
pub mod ntfy;
pub mod presence;
pub mod schedule;

View File

@@ -1,186 +0,0 @@
use std::collections::HashMap;
use std::convert::Infallible;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize};
use serde_repr::*;
use tracing::{error, trace, warn};
use crate::device::{Device, LuaDeviceCreate};
use crate::event::{self, EventChannel};
use crate::lua::traits::AddAdditionalMethods;
#[derive(Debug, Serialize_repr, Deserialize, Clone, Copy)]
#[repr(u8)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Min = 1,
Low,
Default,
High,
Max,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "snake_case", tag = "action")]
pub enum ActionType {
Broadcast {
#[serde(skip_serializing_if = "HashMap::is_empty")]
extras: HashMap<String, String>,
},
// View,
// Http
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Action {
#[serde(flatten)]
pub action: ActionType,
pub label: String,
pub clear: Option<bool>,
}
#[derive(Serialize, Deserialize)]
struct NotificationFinal {
topic: String,
#[serde(flatten)]
inner: Notification,
}
#[derive(Debug, Serialize, Clone, Deserialize)]
pub struct Notification {
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
priority: Option<Priority>,
#[serde(skip_serializing_if = "Vec::is_empty", default = "Default::default")]
actions: Vec<Action>,
}
impl Notification {
pub fn new() -> Self {
Self {
title: None,
message: None,
tags: Vec::new(),
priority: None,
actions: Vec::new(),
}
}
pub fn set_title(mut self, title: &str) -> Self {
self.title = Some(title.into());
self
}
pub fn set_message(mut self, message: &str) -> Self {
self.message = Some(message.into());
self
}
pub fn add_tag(mut self, tag: &str) -> Self {
self.tags.push(tag.into());
self
}
pub fn set_priority(mut self, priority: Priority) -> Self {
self.priority = Some(priority);
self
}
pub fn add_action(mut self, action: Action) -> Self {
self.actions.push(action);
self
}
fn finalize(self, topic: &str) -> NotificationFinal {
NotificationFinal {
topic: topic.into(),
inner: self,
}
}
}
impl Default for Notification {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct Config {
#[device_config(default("https://ntfy.sh".into()))]
pub url: String,
pub topic: String,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
}
#[derive(Debug, Clone, LuaDevice)]
#[traits(crate::lua::traits::AddAdditionalMethods)]
pub struct Ntfy {
config: Config,
}
#[async_trait]
impl LuaDeviceCreate for Ntfy {
type Config = Config;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "ntfy", "Setting up Ntfy");
Ok(Self { config })
}
}
impl Device for Ntfy {
fn get_id(&self) -> String {
"ntfy".to_string()
}
}
impl Ntfy {
async fn send(&self, notification: Notification) {
let notification = notification.finalize(&self.config.topic);
// Create the request
let res = reqwest::Client::new()
.post(self.config.url.clone())
.json(&notification)
.send()
.await;
if let Err(err) = res {
error!("Something went wrong while sending the notification: {err}");
} else if let Ok(res) = res {
let status = res.status();
if !status.is_success() {
warn!("Received status {status} when sending notification");
}
}
}
}
impl AddAdditionalMethods for Ntfy {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static,
{
methods.add_async_method(
"send_notification",
|lua, this, notification: mlua::Value| async move {
let notification: Notification = lua.from_value(notification)?;
this.send(notification).await;
Ok(())
},
);
}
}

View File

@@ -1,135 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn};
use crate::action_callback::ActionCallback;
use crate::config::MqttDeviceConfig;
use crate::device::{Device, LuaDeviceCreate};
use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PresenceMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, LuaDeviceConfig)]
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>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
pub const DEFAULT_PRESENCE: bool = false;
#[derive(Debug)]
pub struct State {
devices: HashMap<String, bool>,
current_overall_presence: bool,
}
#[derive(Debug, Clone, LuaDevice)]
pub struct Presence {
config: Config,
state: Arc<RwLock<State>>,
}
impl Presence {
async fn state(&self) -> RwLockReadGuard<'_, State> {
self.state.read().await
}
async fn state_mut(&self) -> RwLockWriteGuard<'_, State> {
self.state.write().await
}
}
#[async_trait]
impl LuaDeviceCreate for Presence {
type Config = Config;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "presence", "Setting up Presence");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
let state = State {
devices: HashMap::new(),
current_overall_presence: DEFAULT_PRESENCE,
};
let state = Arc::new(RwLock::new(state));
Ok(Self { config, state })
}
}
impl Device for Presence {
fn get_id(&self) -> String {
"presence".to_string()
}
}
#[async_trait]
impl OnMqtt for Presence {
async fn on_mqtt(&self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let offset = self
.config
.mqtt
.topic
.find('+')
.or(self.config.mqtt.topic.find('#'))
.expect("Presence::create fails if it does not contain wildcards");
let device_name = message.topic[offset..].into();
if message.payload.is_empty() {
// Remove the device from the map
debug!("State of device [{device_name}] has been removed");
self.state_mut().await.devices.remove(&device_name);
} else {
let present = match PresenceMessage::try_from(message) {
Ok(state) => state.presence(),
Err(err) => {
warn!("Failed to parse message: {err}");
return;
}
};
debug!("State of device [{device_name}] has changed: {}", present);
self.state_mut().await.devices.insert(device_name, present);
}
let overall_presence = self.state().await.devices.iter().any(|(_, v)| *v);
if overall_presence != self.state().await.current_overall_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;
}
}
}