Added ntfy.sh integration
This commit is contained in:
parent
458c5e25a3
commit
3c0f4bf3b3
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -50,6 +50,7 @@ dependencies = [
|
|||
"rumqttc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"toml",
|
||||
]
|
||||
|
@ -1080,6 +1081,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
|
|
|
@ -24,6 +24,7 @@ dotenv = "0.15.0"
|
|||
anyhow = "1.0.68"
|
||||
reqwest = "0.11.13"
|
||||
axum = "0.6.1"
|
||||
serde_repr = "0.1.10"
|
||||
|
||||
[profile.release]
|
||||
lto=true
|
||||
|
|
|
@ -4,11 +4,10 @@ port=8883
|
|||
username="mqtt"
|
||||
|
||||
[fullfillment]
|
||||
port=7878
|
||||
username="Dreaded_X"
|
||||
|
||||
[presence]
|
||||
topic = "automation/presence"
|
||||
topic = "automation_dev/presence"
|
||||
|
||||
[devices.kitchen_kettle]
|
||||
type = "IkeaOutlet"
|
||||
|
|
|
@ -6,10 +6,14 @@ use serde::Deserialize;
|
|||
|
||||
use crate::devices::{DeviceBox, IkeaOutlet, WakeOnLAN};
|
||||
|
||||
// @TODO Configure more defaults
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub mqtt: MqttConfig,
|
||||
pub fullfillment: FullfillmentConfig,
|
||||
#[serde(default)]
|
||||
pub ntfy: NtfyConfig,
|
||||
pub presence: MqttDeviceConfig,
|
||||
#[serde(default)]
|
||||
pub devices: HashMap<String, Device>
|
||||
|
@ -25,10 +29,32 @@ pub struct MqttConfig {
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FullfillmentConfig {
|
||||
#[serde(default = "default_fullfillment_port")]
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
fn default_fullfillment_port() -> u16 {
|
||||
7878
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NtfyConfig {
|
||||
#[serde(default = "default_ntfy_url")]
|
||||
pub url: String,
|
||||
pub topic: Option<String>,
|
||||
}
|
||||
|
||||
fn default_ntfy_url() -> String {
|
||||
"https://ntfy.sh".into()
|
||||
}
|
||||
|
||||
impl Default for NtfyConfig {
|
||||
fn default() -> Self {
|
||||
Self { url: default_ntfy_url(), topic: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InfoConfig {
|
||||
pub name: String,
|
||||
|
@ -66,7 +92,8 @@ impl Config {
|
|||
let file = fs::read_to_string(filename)?;
|
||||
let mut config: Self = toml::from_str(&file)?;
|
||||
|
||||
config.mqtt.password = Some(std::env::var("MQTT_PASSWORD").or(config.mqtt.password.ok_or("MQTT password needs to be set in either config or the environment!"))?);
|
||||
config.mqtt.password = Some(std::env::var("MQTT_PASSWORD").or(config.mqtt.password.ok_or("MQTT password needs to be set in either config [mqtt.password] or the environment [MQTT_PASSWORD]"))?);
|
||||
config.ntfy.topic = Some(std::env::var("NTFY_TOPIC").or(config.ntfy.topic.ok_or("ntfy.sh topic needs to be set in either config [ntfy.url] or the environment [NTFY_TOPIC]!"))?);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@ pub mod devices;
|
|||
pub mod mqtt;
|
||||
pub mod config;
|
||||
pub mod presence;
|
||||
pub mod ntfy;
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::{time::Duration, sync::{Arc, RwLock}, process, net::SocketAddr};
|
|||
|
||||
use axum::{Router, Json, routing::post, http::StatusCode};
|
||||
|
||||
use automation::{config::Config, presence::Presence};
|
||||
use automation::{config::Config, presence::Presence, ntfy::Ntfy};
|
||||
use dotenv::dotenv;
|
||||
use rumqttc::{MqttOptions, Transport, AsyncClient};
|
||||
use env_logger::Builder;
|
||||
|
@ -49,10 +49,14 @@ async fn main() {
|
|||
// Register devices as presence listener
|
||||
presence.add_listener(Arc::downgrade(&devices));
|
||||
|
||||
let ntfy = Arc::new(RwLock::new(Ntfy::new(config.ntfy)));
|
||||
presence.add_listener(Arc::downgrade(&ntfy));
|
||||
|
||||
// Register presence as mqtt listener
|
||||
let presence = Arc::new(RwLock::new(presence));
|
||||
mqtt.add_listener(Arc::downgrade(&presence));
|
||||
|
||||
|
||||
// Start mqtt, this spawns a seperate async task
|
||||
mqtt.start();
|
||||
|
||||
|
|
137
src/ntfy.rs
Normal file
137
src/ntfy.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use log::{warn, error};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
use serde_repr::*;
|
||||
|
||||
use crate::{presence::OnPresence, config::NtfyConfig};
|
||||
|
||||
pub struct Ntfy {
|
||||
base_url: String,
|
||||
topic: String
|
||||
}
|
||||
|
||||
#[derive(Serialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum Priority {
|
||||
Min = 1,
|
||||
Low,
|
||||
Default,
|
||||
High,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "action")]
|
||||
enum ActionType {
|
||||
Broadcast {
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
extras: HashMap<String, String>
|
||||
},
|
||||
// View,
|
||||
// Http
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Action {
|
||||
#[serde(flatten)]
|
||||
action: ActionType,
|
||||
label: String,
|
||||
clear: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Notification {
|
||||
topic: String,
|
||||
#[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")]
|
||||
tags: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
priority: Option<Priority>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
actions: Vec<Action>,
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
fn new(topic: &str) -> Self {
|
||||
Self { topic: topic.to_owned(), title: None, message: None, tags: Vec::new(), priority: None, actions: Vec::new() }
|
||||
}
|
||||
|
||||
fn set_title(mut self, title: &str) -> Self {
|
||||
self.title = Some(title.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
fn set_message(mut self, message: &str) -> Self {
|
||||
self.message = Some(message.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
fn add_tag(mut self, tag: &str) -> Self {
|
||||
self.tags.push(tag.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
fn set_priority(mut self, priority: Priority) -> Self {
|
||||
self.priority = Some(priority);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_action(mut self, action: Action) -> Self {
|
||||
self.actions.push(action);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Ntfy {
|
||||
pub fn new(config: NtfyConfig) -> Self {
|
||||
Self { base_url: config.url, topic: config.topic.unwrap() }
|
||||
}
|
||||
}
|
||||
|
||||
impl OnPresence for Ntfy {
|
||||
fn on_presence(&mut self, presence: bool) {
|
||||
// Setup extras for the broadcast
|
||||
let extras = HashMap::from([
|
||||
("cmd".into(), "presence".into()),
|
||||
("state".into(), if presence { "0" } else { "1" }.into()),
|
||||
]);
|
||||
|
||||
// Create broadcast action
|
||||
let action = Action {
|
||||
action: ActionType::Broadcast { extras },
|
||||
label: if presence { "Set away" } else { "Set home" }.to_owned(),
|
||||
clear: Some(true)
|
||||
};
|
||||
|
||||
// Create the notification
|
||||
let notification = Notification::new(&self.topic)
|
||||
.set_title("Presence")
|
||||
.set_message(if presence { "Home" } else { "Away" })
|
||||
.add_tag("house")
|
||||
.add_action(action)
|
||||
.set_priority(Priority::Low);
|
||||
|
||||
// Create the request
|
||||
let req = reqwest::Client::new()
|
||||
.post(self.base_url.clone())
|
||||
.body(serde_json::to_string(¬ification).unwrap());
|
||||
|
||||
// Send the notification
|
||||
tokio::spawn(async move {
|
||||
let res = req.send().await;
|
||||
if let Err(err) = res {
|
||||
error!("Something went wrong while sending the notifcation: {err}");
|
||||
} else if let Ok(res) = res {
|
||||
let status = res.status();
|
||||
if status != StatusCode::OK {
|
||||
warn!("Received status {status} when sending notification");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user