This introduces a configurable debouncing delay before which no update is sent to matrix, which avoids flooding the matrix room with updates when the door is opened and closed in quick succession. It also allows to quickly open and close the door without sending notifications. The debouncing window can be configured in config.yml, see example.
173 lines
5.6 KiB
Rust
173 lines
5.6 KiB
Rust
/*!
|
|
* matrix-lolopener v0.1.0
|
|
*
|
|
* Copyright (C) 2026 The 1312 Media Collective
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#![recursion_limit = "256"]
|
|
|
|
mod config;
|
|
mod matrix;
|
|
mod mqtt;
|
|
|
|
use tokio::{
|
|
sync::{Notify, broadcast, mpsc},
|
|
time::{sleep, Duration},
|
|
task::JoinHandle,
|
|
};
|
|
|
|
use crate::{
|
|
config::Config,
|
|
matrix::login_and_sync,
|
|
mqtt::{MQTT, StatusMessage},
|
|
};
|
|
|
|
use std::sync::{ Arc, Mutex };
|
|
use tracing::{ info, debug };
|
|
|
|
use clap::Parser;
|
|
|
|
/// Matrix bot posting when the front door is opened or closed
|
|
#[derive(Parser, Debug)]
|
|
#[command(version, about, long_about = None)]
|
|
struct Args {
|
|
/// Configuration file path
|
|
#[arg(short, long, default_value_t = "config.yml".into())]
|
|
config: String,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
|
enum DoorSensorStatus {
|
|
Open,
|
|
Closed,
|
|
}
|
|
#[derive(Debug)]
|
|
struct LolOpenerState {
|
|
sensor_status: Option<DoorSensorStatus>,
|
|
pending: Option<(DoorSensorStatus, String)>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
let args = Args::parse();
|
|
|
|
let config = Config::load(&args.config).unwrap_or_else(|e| {
|
|
panic!("Failed to load config: {e:?}")
|
|
});
|
|
let config = Arc::new(config);
|
|
|
|
let matrix_ready = Arc::new(Notify::new());
|
|
|
|
// This channel is used for sending messages to the matrix module,
|
|
// it holds a tuple with an HTML message and a list of rooms to post to
|
|
let (bcast_tx, bcast_rx) = broadcast::channel::<(String, Vec<String>)>(1024);
|
|
|
|
let handle: JoinHandle<_> = {
|
|
let matrix_ready = Arc::clone(&matrix_ready);
|
|
let config = Arc::clone(&config);
|
|
|
|
tokio::spawn(async move {
|
|
debug!("Waiting until matrix is ready");
|
|
matrix_ready.notified().await;
|
|
info!("Matrix is ready");
|
|
|
|
let current_state = Arc::new(Mutex::new(LolOpenerState {
|
|
sensor_status: None,
|
|
pending: None
|
|
}));
|
|
let current_state = Arc::clone(¤t_state);
|
|
|
|
let (debounce_tx, mut debounce_rx) = mpsc::unbounded_channel::<(DoorSensorStatus, String)>();
|
|
let debounce_delay = config.debounce_delay;
|
|
let rooms = config.rooms.clone();
|
|
let handle = tokio::spawn(async move {
|
|
|
|
while let Some((new_status, msg)) = debounce_rx.recv().await {
|
|
let should_wait = {
|
|
//XXX: Panic if the lock is poisoned
|
|
let mut state = current_state.lock().unwrap();
|
|
let should_wait = state.pending.is_none();
|
|
|
|
state.pending = Some((new_status, msg));
|
|
should_wait
|
|
};
|
|
|
|
if should_wait {
|
|
let tx = bcast_tx.clone();
|
|
let rooms = rooms.clone();
|
|
let state = current_state.clone();
|
|
|
|
tokio::spawn(async move {
|
|
sleep(Duration::from_secs(debounce_delay)).await;
|
|
|
|
let msg = {
|
|
//XXX: Panic if the lock is poisoned
|
|
let mut state = state.lock().unwrap();
|
|
let pending = state.pending.take();
|
|
|
|
match pending {
|
|
Some((status, msg))
|
|
if state.sensor_status.as_ref() != Some(&status) => {
|
|
state.sensor_status = Some(status);
|
|
Some(msg)
|
|
}
|
|
|
|
_ => None,
|
|
}
|
|
};
|
|
|
|
if let Some(msg) = msg {
|
|
debug!("Sending msg {}", msg);
|
|
//XXX: Panic if the matrix module can't get the message
|
|
tx.send((msg, rooms)).unwrap();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
let mut mqtt = MQTT::new(config.mqtt.clone()).await;
|
|
mqtt.subscribe(move |status: StatusMessage| {
|
|
let (new_status, msg) = match status {
|
|
StatusMessage { contact: true, .. } => (DoorSensorStatus::Closed, "🔴 Le LOL est fermé :("),
|
|
StatusMessage { contact: false, .. } => (DoorSensorStatus::Open, "🟢 Le LOL est ouvert :)"),
|
|
};
|
|
|
|
//XXX: Panic if the channel is closed
|
|
debounce_tx.send((new_status, String::from(msg))).unwrap();
|
|
|
|
}).await.unwrap(); //XXX: Panic if the mqtt broker fails
|
|
|
|
handle.abort();
|
|
handle.await.unwrap();
|
|
})
|
|
};
|
|
|
|
login_and_sync(
|
|
config.homeserver_uri.clone(),
|
|
&config.auth,
|
|
matrix_ready,
|
|
bcast_rx
|
|
).await?;
|
|
|
|
handle.abort();
|
|
handle.await?;
|
|
|
|
Ok(())
|
|
}
|