/*! * 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(()) }