/*!
* 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 .
*/
#![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,
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)>(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(())
}