Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f969dd0a10 | |||
| 4419442101 | |||
| 31e0664aa6 | |||
| 6d5340e9ca | |||
| 642e02949f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/target
|
||||
/mqttAutomation-*
|
||||
|
||||
214
Cargo.lock
generated
214
Cargo.lock
generated
@ -17,6 +17,21 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@ -44,6 +59,12 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.9.0"
|
||||
@ -65,6 +86,20 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@ -208,6 +243,30 @@ version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.7.1"
|
||||
@ -224,6 +283,16 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json"
|
||||
version = "0.12.4"
|
||||
@ -280,8 +349,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mqtt-client"
|
||||
version = "3.0.0"
|
||||
source = "git+https://gitea.finnvanreenen.nl/LailaTheElf/mqttClient.git?tag=v3.0.0#f186e437331b0c8680ac9917d7b88f17bb5f176b"
|
||||
version = "4.0.0"
|
||||
source = "git+https://gitea.finnvanreenen.nl/LailaTheElf/mqttClient.git?tag=v4.0.0#49e8adf2eb768fcd147f0d6508a2f32eed86a641"
|
||||
dependencies = [
|
||||
"crossbeam",
|
||||
"rumqttc",
|
||||
@ -289,8 +358,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mqttAutomation"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam",
|
||||
"json",
|
||||
"mqtt-client",
|
||||
@ -299,6 +369,15 @@ dependencies = [
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
@ -308,6 +387,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
@ -436,6 +521,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@ -646,6 +737,123 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mqttAutomation"
|
||||
version = "1.3.0"
|
||||
version = "1.3.2"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@ -9,4 +9,5 @@ json = "0.12.4"
|
||||
rumqttc = "0.24.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_yaml = "0.9.34"
|
||||
mqtt-client = { tag = "v3.0.0", git = "https://gitea.finnvanreenen.nl/LailaTheElf/mqttClient.git" }
|
||||
mqtt-client = { tag = "v4.0.0", git = "https://gitea.finnvanreenen.nl/LailaTheElf/mqttClient.git" }
|
||||
chrono = "0.4.41"
|
||||
|
||||
9
build.sh
Normal file
9
build.sh
Normal file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cross build --target aarch64-unknown-linux-gnu --release
|
||||
cargo build --release
|
||||
|
||||
cp target/aarch64-unknown-linux-gnu/release/mqttAutomation mqttAutomation-aarch64
|
||||
cp target/release/mqttAutomation mqttAutomation-x86_64
|
||||
@ -8,3 +8,18 @@ automation:
|
||||
base_topic: "/kees/automation/"
|
||||
alarm_hour: 7
|
||||
alarm_minute: 0
|
||||
|
||||
# auto:
|
||||
# - trigger:
|
||||
# !Time
|
||||
# date:
|
||||
# weekday: "0,1,2,3,4"
|
||||
# time:
|
||||
# hour: "7"
|
||||
# minute: "0"
|
||||
# seconds: "0"
|
||||
# actions:
|
||||
# - !Publish
|
||||
# type: publish
|
||||
# topic: "/cool/devices/lamp-01/set"
|
||||
# pauload: "ON"
|
||||
|
||||
66
src/automation/auto/mod.rs
Normal file
66
src/automation/auto/mod.rs
Normal file
@ -0,0 +1,66 @@
|
||||
mod triggers;
|
||||
|
||||
pub mod automation {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use crate::automation::auto::triggers::triggers;
|
||||
|
||||
mod actions {
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Publish {
|
||||
topic: String,
|
||||
payload: Option<String>,
|
||||
retain: Option<bool>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum Action {
|
||||
Publish(Publish),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Automation {
|
||||
pub trigger: triggers::Trigger,
|
||||
pub actions: Vec<actions::Action>
|
||||
}
|
||||
|
||||
pub struct Generator {
|
||||
mqtt_triggers: HashMap<String, triggers::Mqtt>,
|
||||
time_triggers: HashMap<String, triggers::Datetime>,
|
||||
}
|
||||
impl Generator {
|
||||
|
||||
pub fn new() -> Generator {
|
||||
Generator {
|
||||
mqtt_triggers: HashMap::new(),
|
||||
time_triggers: HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_automations(&mut self, automations: Vec<Automation>) {
|
||||
for auto in automations {
|
||||
self.read_automation_single(auto);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_automation_single(&mut self, auto: Automation) {
|
||||
// let auto_cloned = auto.clone();
|
||||
match auto.trigger {
|
||||
triggers::Trigger::Mqtt(mqtt) => {
|
||||
self.mqtt_triggers.insert(mqtt.topic.clone(), mqtt.clone());
|
||||
},
|
||||
triggers::Trigger::Time(_datetime) => {
|
||||
// self.time_triggers.insert(mqtt.topic, auto_cloned);
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
39
src/automation/auto/triggers.rs
Normal file
39
src/automation/auto/triggers.rs
Normal file
@ -0,0 +1,39 @@
|
||||
mod datetime;
|
||||
|
||||
pub mod triggers {
|
||||
use serde::Deserialize;
|
||||
|
||||
pub trait TriggerBase {
|
||||
fn get_next_trigger(&self) -> String;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
enum StringOrU16 {
|
||||
String(String),
|
||||
U16(u16)
|
||||
}
|
||||
|
||||
// ===== mqtt
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Mqtt {
|
||||
pub topic: String,
|
||||
pub payload: Option<String>
|
||||
}
|
||||
impl TriggerBase for Mqtt {
|
||||
fn get_next_trigger(&self) -> String {
|
||||
self.topic.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== datetime
|
||||
|
||||
pub use crate::automation::auto::triggers::datetime::trigger_datetime::Datetime;
|
||||
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub enum Trigger {
|
||||
Mqtt(Mqtt),
|
||||
Time(Datetime),
|
||||
}
|
||||
}
|
||||
316
src/automation/auto/triggers/datetime.rs
Normal file
316
src/automation/auto/triggers/datetime.rs
Normal file
@ -0,0 +1,316 @@
|
||||
|
||||
|
||||
pub mod trigger_datetime {
|
||||
use serde::Deserialize;
|
||||
use crate::automation::auto::triggers::triggers::TriggerBase;
|
||||
use chrono::{DateTime, Datelike, Local, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
fn get_u16_from_u32(value: u32) -> Option<u16> {
|
||||
match u16::try_from(value) {
|
||||
Ok(n) => Some(n),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
fn get_u16_from_i32(value: i32) -> Option<u16> {
|
||||
match u16::try_from(value) {
|
||||
Ok(n) => Some(n),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CompareError {
|
||||
InvalidValue,
|
||||
DivNaN
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Date {
|
||||
year: Option<String>,
|
||||
month: Option<String>,
|
||||
date: Option<String>,
|
||||
weekday: Option<String>
|
||||
}
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct Time {
|
||||
hour: Option<String>,
|
||||
minute: Option<String>,
|
||||
second: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct Datetime {
|
||||
date: Date,
|
||||
time: Time
|
||||
}
|
||||
impl Datetime {
|
||||
pub fn first_match(condition: String, start: u16, end: u16) -> Result<Option<u16>, CompareError> {
|
||||
if condition == "*" {
|
||||
return Ok(Some(start));
|
||||
}
|
||||
if condition.starts_with("*/") {
|
||||
let div = condition[2..].to_string();
|
||||
let mut options: Vec<u16> = (start..end).collect();
|
||||
match div.parse::<u16>() {
|
||||
Ok(n) => {
|
||||
options.retain(|x| x % n == 0);
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(CompareError::DivNaN);
|
||||
},
|
||||
}
|
||||
let min = options.iter().min();
|
||||
match min {
|
||||
Some(min) => {
|
||||
return Ok(Some(min.clone()));
|
||||
},
|
||||
None => {
|
||||
return Ok(None);
|
||||
},
|
||||
}
|
||||
}
|
||||
if condition.split(',').count() > 0 {
|
||||
let parts = condition.split(',');
|
||||
let mut options: Vec<u16> = [].to_vec();
|
||||
for part in parts {
|
||||
match part.parse::<u16>() {
|
||||
Ok(n) => {
|
||||
options.push(n);
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(CompareError::DivNaN);
|
||||
},
|
||||
}
|
||||
}
|
||||
options.retain(|x| *x >= start && *x <= end);
|
||||
let min = options.iter().min();
|
||||
match min {
|
||||
Some(min) => {
|
||||
return Ok(Some(min.clone()));
|
||||
},
|
||||
None => {
|
||||
return Ok(None);
|
||||
},
|
||||
}
|
||||
}
|
||||
match condition.parse::<u16>() {
|
||||
Ok(n) => {
|
||||
if n >= start && n <= end {
|
||||
return Ok(Some(n))
|
||||
}
|
||||
else {
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
return Err(CompareError::InvalidValue);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn check_thing(&self, option: String, start: u16, end: u16) -> Option<u16> {
|
||||
match self::Datetime::first_match(option, start, end) {
|
||||
Ok(r) => {
|
||||
return r
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: trigger.datetime.find_next: failed to parse month: {:?}", e);
|
||||
return None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_days_in_month(date: &DateTime<Local>) -> u16 {
|
||||
let year = date.year();
|
||||
let month = date.month();
|
||||
let date = match NaiveDate::from_ymd_opt(year, month + 1, 1) {
|
||||
Some(d) => d,
|
||||
None => NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
|
||||
};
|
||||
let d = date.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap());
|
||||
u16::try_from(d.num_days()).unwrap()
|
||||
}
|
||||
|
||||
fn find_next_trigger(&self, from: DateTime<Local>) -> Option<DateTime<Local>> {
|
||||
let mut next = from.clone();
|
||||
|
||||
// year
|
||||
match &self.date.year {
|
||||
Some(y) => {
|
||||
match self::Datetime::first_match(y.to_string(), get_u16_from_i32(next.year()).unwrap(), u16::MAX) {
|
||||
Ok(r) => {
|
||||
match r {
|
||||
Some(y) => {
|
||||
if get_u16_from_i32(next.year()).unwrap() != y {
|
||||
next = match Local::with_ymd_and_hms(&Local, i32::from(y), 1, 1, 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => return None,
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("ERROR: trigger.datetime.find_next: failed to parse year: {:?}", e);
|
||||
return None
|
||||
},
|
||||
}
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
|
||||
// month
|
||||
match &self.date.month {
|
||||
Some(opt) => {
|
||||
let now = get_u16_from_u32(next.month()).unwrap();
|
||||
match self.check_thing(opt.to_string(), now, 12) {
|
||||
Some(r) => {
|
||||
if r > now {
|
||||
next = match Local::with_ymd_and_hms(&Local, next.year(), u32::from(r), 1, 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
next = match Local::with_ymd_and_hms(&Local, next.year()+1, 1, 1, 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None
|
||||
};
|
||||
if (next - Local::now()).num_days() < 2*365 {
|
||||
return self.find_next_trigger(next);
|
||||
}
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
|
||||
// date
|
||||
match &self.date.date {
|
||||
Some(opt) => {
|
||||
let now = get_u16_from_u32(next.day()).unwrap();
|
||||
match self.check_thing(opt.to_string(), now, Datetime::get_days_in_month(&next)) {
|
||||
Some(r) => {
|
||||
if r > now {
|
||||
next = match Local::with_ymd_and_hms(&Local, next.year(), next.month(), u32::from(r), 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let m = match next.month() {
|
||||
12 => 1,
|
||||
munth => munth
|
||||
};
|
||||
let y = match m {
|
||||
1 => next.year()+1,
|
||||
_ => next.year()
|
||||
};
|
||||
next = match Local::with_ymd_and_hms(&Local, y, m, 1, 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None
|
||||
};
|
||||
if (next - Local::now()).num_days() < 2*365 {
|
||||
return self.find_next_trigger(next);
|
||||
}
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
|
||||
// hour
|
||||
match &self.time.hour {
|
||||
Some(opt) => {
|
||||
let now = get_u16_from_u32(next.hour()).unwrap();
|
||||
match self.check_thing(opt.to_string(), now, 23) {
|
||||
Some(r) => {
|
||||
if r > now {
|
||||
next = match Local::with_ymd_and_hms(&Local, next.year(), next.month(), next.hour(), u32::from(r), 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None,
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let mut d = next.day() + 1;
|
||||
if d == u32::from(Datetime::get_days_in_month(&next)) {
|
||||
d = 1;
|
||||
}
|
||||
let m = match d {
|
||||
1 => next.month()+1,
|
||||
_ => next.month()
|
||||
};
|
||||
let y = match m {
|
||||
1 => next.year()+1,
|
||||
_ => next.year()
|
||||
};
|
||||
next = match Local::with_ymd_and_hms(&Local, y, m, d, 0, 0, 0) {
|
||||
chrono::offset::LocalResult::Single(d) => d,
|
||||
chrono::offset::LocalResult::Ambiguous(d, _) => d,
|
||||
chrono::offset::LocalResult::None => return None
|
||||
};
|
||||
if (next - Local::now()).num_days() < 2*365 {
|
||||
return self.find_next_trigger(next);
|
||||
}
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
|
||||
Some(next)
|
||||
}
|
||||
}
|
||||
impl TriggerBase for Datetime {
|
||||
fn get_next_trigger(&self) -> String {
|
||||
let next_trigger: Option<DateTime<Local>> = self.find_next_trigger(Local::now());
|
||||
match next_trigger {
|
||||
Some(next) => {
|
||||
let str = next.format("%S").to_string();
|
||||
print!("datetime string: {str}");
|
||||
return str
|
||||
},
|
||||
None => {
|
||||
print!("ERROR: trigger.datetime.get_next: No next trigger found");
|
||||
return "None".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn datetime_first_match() {
|
||||
assert_eq!(Datetime::first_match("*".to_string(), 123, 128).unwrap(), Some(123));
|
||||
assert_eq!(Datetime::first_match("*/4".to_string(), 123, 128).unwrap(), Some(124));
|
||||
assert_eq!(Datetime::first_match("*/10".to_string(), 123, 128).unwrap(), None);
|
||||
assert_eq!(Datetime::first_match("125".to_string(), 123, 128).unwrap(), Some(125));
|
||||
assert_eq!(Datetime::first_match("122,126".to_string(), 123, 128).unwrap(), Some(126));
|
||||
assert_eq!(Datetime::first_match("122,129".to_string(), 123, 128).unwrap(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,23 @@
|
||||
mod json;
|
||||
//AUTO mod auto;
|
||||
|
||||
use std::{thread, time::Duration};
|
||||
use serde::Deserialize;
|
||||
|
||||
use mqtt_client::{MqttMessage, Sender, Receiver, QoS};
|
||||
use mqtt_client::{MqttMessage, MqttEvent, Sender, Receiver, QoS};
|
||||
use mqtt_client::mqtt_client;
|
||||
|
||||
use crate::automation::json::json_parser;
|
||||
//AUTO use crate::automation::auto::automation::{Automation as AutoConfig, Generator};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SettingsConf {
|
||||
base_topic: String,
|
||||
|
||||
alarm_hour: u8,
|
||||
alarm_minute: u8
|
||||
alarm_minute: u8,
|
||||
|
||||
//AUTO auto: Vec<AutoConfig>
|
||||
}
|
||||
|
||||
struct PingStats {
|
||||
@ -40,7 +44,8 @@ fn ping_stat_update(stats: &mut PingStats, payload: String) -> Option<f32> {
|
||||
}
|
||||
|
||||
pub struct Automation {
|
||||
tx: Sender<mqtt_client::MqttMessage>,
|
||||
tx: Sender<MqttMessage>,
|
||||
client: mqtt_client::Client,
|
||||
|
||||
clock_dow: u8,
|
||||
clock_hour: u8,
|
||||
@ -52,12 +57,14 @@ pub struct Automation {
|
||||
|
||||
ping_elfdesktop: PingStats,
|
||||
|
||||
config: SettingsConf
|
||||
config: SettingsConf,
|
||||
|
||||
//AUTO generator: Generator
|
||||
}
|
||||
|
||||
|
||||
impl Automation {
|
||||
fn tx(&self, message: mqtt_client::MqttMessage) {
|
||||
fn tx(&self, message: MqttMessage) {
|
||||
match self.tx.send(message) {
|
||||
Err(n) => println!("ERROR: faild to send publish ({:?})", n),
|
||||
Ok(_n) => {}
|
||||
@ -71,7 +78,7 @@ impl Automation {
|
||||
} else {
|
||||
payload = String::from("OFF");
|
||||
}
|
||||
self.tx({ mqtt_client::MqttMessage {
|
||||
self.tx({ MqttMessage {
|
||||
topic: topic,
|
||||
payload: payload,
|
||||
retain: false,
|
||||
@ -79,42 +86,83 @@ impl Automation {
|
||||
}});
|
||||
}
|
||||
fn lamp01_set(&self, state: bool) {
|
||||
self.tx_set(String::from("/cool/devices/lamp-01/set"), state);
|
||||
self.tx_set(String::from("/kees/devices/lamp-01/set"), state);
|
||||
}
|
||||
fn pc_sw_set(&self, state: bool) {
|
||||
self.tx_set(String::from("/cool/devices/sw-01/set"), state);
|
||||
self.tx_set(String::from("/kees/devices/sw-01/set"), state);
|
||||
}
|
||||
|
||||
fn get_current_time(&self) -> u32 {
|
||||
u32::from(self.clock_dow) * 24 + u32::from(self.clock_hour) * 60 + u32::from(self.clock_min)
|
||||
self.get_time(None, None, None, None)
|
||||
}
|
||||
fn get_time(&self, dow: Option<u8>, hour: Option<u8>, min: Option<u8>, sec: Option<u8>) -> u32 {
|
||||
let d = match dow {
|
||||
Some(n) => n,
|
||||
None => self.clock_dow
|
||||
};
|
||||
let h = match hour {
|
||||
Some(n) => n,
|
||||
None => self.clock_hour
|
||||
};
|
||||
let m = match min {
|
||||
Some(n) => n,
|
||||
None => self.clock_min
|
||||
};
|
||||
let s = match sec {
|
||||
Some(n) => n,
|
||||
None => self.clock_sec
|
||||
};
|
||||
((u32::from(d) * 24 + u32::from(h)) * 60 + u32::from(m)) * 60 + u32::from(s)
|
||||
}
|
||||
|
||||
fn alarm(&self) {
|
||||
fn alarm_trigger(&self) {
|
||||
self.lamp01_set(true);
|
||||
}
|
||||
fn alarm_send_config(&self) {
|
||||
self.tx({ MqttMessage {
|
||||
topic: format!("{}alarm", self.config.base_topic),
|
||||
payload: format!("{}:{}", self.config.alarm_hour, self.config.alarm_minute),
|
||||
retain: true,
|
||||
qos: mqtt_client::QoS::AtMostOnce,
|
||||
}});
|
||||
}
|
||||
|
||||
fn config_message_in(&mut self, message: mqtt_client::MqttMessage, topic: String) {
|
||||
if topic.starts_with("alarm/hour") {
|
||||
fn config_message_in(&mut self, message: MqttMessage, topic: String) {
|
||||
if topic.starts_with("alarm/set") {
|
||||
|
||||
match message.payload.parse::<u8>() {
|
||||
Err(e) =>
|
||||
println!("ERROR: config_message_in: alarm/hour has invalid payload ({:?})", e),
|
||||
Ok(n) => self.config.alarm_hour = n
|
||||
let mut time = message.payload.split(':');
|
||||
if time.clone().count() != 2 && time.clone().count() != 3 {
|
||||
println!("ERROR: config_message_in: alarm/set has invalid payload. incorect number of slices ({})", message.payload)
|
||||
}
|
||||
else {
|
||||
let hour_str = time.next().unwrap();
|
||||
let min_str = time.next().unwrap();
|
||||
let mut hour: Option<u8> = None;
|
||||
let mut min: Option<u8> = None;
|
||||
|
||||
}
|
||||
else if topic.eq("alarm/minute") {
|
||||
match hour_str.parse::<u8>() {
|
||||
Err(_) =>
|
||||
println!("ERROR: config_message_in: alarm/set has invalid payload. hour is NaN ({})", message.payload),
|
||||
Ok(n) => hour = Some(n)
|
||||
}
|
||||
|
||||
match message.payload.parse::<u8>() {
|
||||
Err(e) =>
|
||||
println!("ERROR: config_message_in: alarm/minute has invalid payload ({:?})", e),
|
||||
Ok(n) => self.config.alarm_minute = n
|
||||
match min_str.parse::<u8>() {
|
||||
Err(_) =>
|
||||
println!("ERROR: config_message_in: alarm/set has invalid payload. min is NaN ({})", message.payload),
|
||||
Ok(n) => min = Some(n)
|
||||
}
|
||||
|
||||
if hour != None && min != None {
|
||||
self.config.alarm_hour = hour.unwrap();
|
||||
self.config.alarm_minute = min.unwrap();
|
||||
}
|
||||
}
|
||||
self.alarm_send_config();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn clock_message_in(&mut self, message: mqtt_client::MqttMessage) {
|
||||
fn clock_message_in(&mut self, message: MqttMessage) {
|
||||
if message.topic.eq("clock/time/second") {
|
||||
|
||||
match message.payload.parse::<u8>() {
|
||||
@ -129,7 +177,7 @@ impl Automation {
|
||||
self.ping_elfdesktop.avg,
|
||||
1.0 - (f32::from(self.ping_elfdesktop.fails) / f32::from(self.ping_elfdesktop.total))
|
||||
);
|
||||
self.tx({ mqtt_client::MqttMessage {
|
||||
self.tx({ MqttMessage {
|
||||
topic: String::from("/kees/db/insert/ping"),
|
||||
payload: json,
|
||||
retain: false,
|
||||
@ -146,15 +194,16 @@ impl Automation {
|
||||
else if message.topic.eq("clock/time/minute") {
|
||||
|
||||
match message.payload.parse::<u8>() {
|
||||
Err(e) =>
|
||||
Err(e) =>
|
||||
println!("ERROR: clock_message_in: clock/time/minute has invalid payload ({:?})", e),
|
||||
Ok(n) => {
|
||||
Ok(n) => {
|
||||
self.clock_min = n;
|
||||
|
||||
if n == self.config.alarm_minute
|
||||
&& self.clock_hour == self.config.alarm_hour
|
||||
if n == self.config.alarm_minute
|
||||
&& n != 0
|
||||
&& self.clock_hour == self.config.alarm_hour
|
||||
&& self.clock_dow < 5 {
|
||||
self.alarm();
|
||||
self.alarm_trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,13 +212,15 @@ impl Automation {
|
||||
else if message.topic.eq("clock/time/hour") {
|
||||
|
||||
match message.payload.parse::<u8>() {
|
||||
Err(e) =>
|
||||
Err(e) =>
|
||||
println!("ERROR: clock_message_in: clock/time/hour has invalid payload ({:?})", e),
|
||||
Ok(n) => {
|
||||
self.clock_hour = n;
|
||||
|
||||
self.alarm_send_config();
|
||||
|
||||
if self.config.alarm_minute == 0 && n == self.config.alarm_hour && self.clock_dow < 5 {
|
||||
self.alarm();
|
||||
self.alarm_trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -187,7 +238,7 @@ impl Automation {
|
||||
// println!("DEBUG: clock_message_in: current time: {}:{}:{}", self.clock_hour, self.clock_min, self.clock_sec);
|
||||
}
|
||||
|
||||
fn message_in(&mut self, message: mqtt_client::MqttMessage) {
|
||||
fn message_in(&mut self, message: MqttMessage) {
|
||||
// println!("DEBUG : mqtt_automation: {}: {}", message.topic, message.payload);
|
||||
if message.topic.starts_with(&self.config.base_topic) {
|
||||
let topic = message.topic[self.config.base_topic.len()..].to_string();
|
||||
@ -196,14 +247,21 @@ impl Automation {
|
||||
else if message.topic.starts_with("clock/") {
|
||||
self.clock_message_in(message);
|
||||
}
|
||||
else if message.topic.eq("/cool/devices/KNMITemp/values") {
|
||||
else if message.topic.eq("/kees/devices/KNMITemp/values") {
|
||||
|
||||
let payload_json = json_parser::Json::Text(message.payload);
|
||||
let path = Vec::from([String::from("gr")]);
|
||||
match json_parser::get_u32(payload_json, path) {
|
||||
Ok(gr) => {
|
||||
if gr > 30 {
|
||||
self.lamp01_set(false);
|
||||
let alarm_time =
|
||||
self.get_time(None, Some(self.config.alarm_hour), Some(self.config.alarm_minute), Some(0));
|
||||
let time_30min =
|
||||
self.get_time(Some(0), Some(0), Some(29), Some(0));
|
||||
let time_diff: i64 = i64::from(self.get_current_time()) - i64::from(alarm_time);
|
||||
if self.clock_dow >= 5 || time_diff < 0 || time_diff > i64::from(time_30min) {
|
||||
self.lamp01_set(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) =>
|
||||
@ -236,40 +294,46 @@ impl Automation {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl mqtt_client::MqttTool<SettingsConf> for Automation {
|
||||
fn new(client: rumqttc::Client, tx: Sender<mqtt_client::MqttMessage>, config: SettingsConf) -> Automation {
|
||||
|
||||
match client.subscribe("clock/time/#", QoS::AtMostOnce) {
|
||||
fn init(&self) {
|
||||
println!("DEBUG: init");
|
||||
match self.client.subscribe("clock/time/#", QoS::AtMostOnce) {
|
||||
Err(e) =>
|
||||
println!("ERROR: main: faild to subscribe to clock/time/hour ({})", e),
|
||||
Ok(_) => {}
|
||||
}
|
||||
match client.subscribe("clock/date/dow", QoS::AtMostOnce) {
|
||||
match self.client.subscribe("clock/date/dow", QoS::AtMostOnce) {
|
||||
Err(e) =>
|
||||
println!("ERROR: main: faild to subscribe to clock/date/dow ({})", e),
|
||||
Ok(_) => {}
|
||||
}
|
||||
match client.subscribe("/kees/automation/#", QoS::AtMostOnce) {
|
||||
match self.client.subscribe("/kees/automation/#", QoS::AtMostOnce) {
|
||||
Err(e) =>
|
||||
println!("ERROR: main: faild to subscribe to automation/alarm/# ({})", e),
|
||||
Ok(_) => {}
|
||||
}
|
||||
match client.subscribe("/cool/devices/KNMITemp/values", QoS::AtMostOnce) {
|
||||
match self.client.subscribe("/kees/devices/KNMITemp/values", QoS::AtMostOnce) {
|
||||
Err(e) =>
|
||||
println!("ERROR: main: faild to subscribe to KNMITemp/values ({})", e),
|
||||
Ok(_) => {}
|
||||
}
|
||||
match client.subscribe("/kees/ping/#", QoS::AtMostOnce) {
|
||||
match self.client.subscribe("/kees/ping/#", QoS::AtMostOnce) {
|
||||
Err(e) =>
|
||||
println!("ERROR: main: faild to subscribe to KNMITemp/values ({})", e),
|
||||
Ok(_) => {}
|
||||
}
|
||||
|
||||
self.alarm_send_config();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl mqtt_client::MqttTool<SettingsConf> for Automation {
|
||||
fn new(tx: Sender<MqttMessage>, config: SettingsConf, client: mqtt_client::Client) -> Automation {
|
||||
Automation {
|
||||
tx,
|
||||
client,
|
||||
|
||||
clock_dow: u8::MAX,
|
||||
clock_hour: u8::MAX,
|
||||
@ -282,10 +346,12 @@ impl mqtt_client::MqttTool<SettingsConf> for Automation {
|
||||
ping_elfdesktop: { PingStats { total: 0, fails: 0, avg: 0.0 } },
|
||||
|
||||
config: config,
|
||||
|
||||
//AUTO generator: Generator::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(&mut self, rx: Receiver<MqttMessage>) {
|
||||
fn run(&mut self, rx: Receiver<MqttEvent>) {
|
||||
loop {
|
||||
let message = rx.recv();
|
||||
match message {
|
||||
@ -293,8 +359,16 @@ impl mqtt_client::MqttTool<SettingsConf> for Automation {
|
||||
println!("ERROR: mqttAutomation: failed to receve an message ({})", e);
|
||||
thread::sleep(Duration::from_millis(500));
|
||||
},
|
||||
Ok(message) => {
|
||||
self.message_in(message);
|
||||
Ok(event) => {
|
||||
match event {
|
||||
MqttEvent::Connected => {
|
||||
self.init();
|
||||
},
|
||||
MqttEvent::Disconnected => {},
|
||||
MqttEvent::Message(message) => {
|
||||
self.message_in(message);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user