//! This is a small CLI intended to be run on a host that accepts dooris commands via SSH //! //! The SSH server should be configured to force execution of this command via the following server config snippet //! //! ```sshd_config //! Match User dooris //! AuthorizedKeysFile /path/to/api/state/authorized_keys //! ForceCommand /path/to/ssh-cli //! SetEnv DOORIS_SERVER_URL=https://dooris.ccchh.net/ DOORIS_API_TOKEN=foobar123 //! ``` mod api_client; mod api_models; mod cli; use std::{process::exit, time::Duration}; use eyre::eyre; use crate::{ api_client::ApiClient, api_models::{ApiLock, DesiredLockState, LockState}, }; #[tokio::main(flavor = "current_thread")] async fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); simple_eyre::install()?; let args = cli::CliArgs::parse()?; tracing::debug!(cli = ?args, "Parsed CLI parameters"); match args { cli::CliArgs::Help => { println!( "dooris-over-ssh: Your personal door assistant with an extra serving of chaos" ); println!( "Usage: ssh dooris@dooris.ccchh.net [wait|nowait] | unlock [wait|nowait]>" ); println!(); println!("Commands:"); println!(); println!(" help -> The help command prints this message. Hello 👋"); println!(""); println!( " show-locks -> Display a list of currently connected locks along with their status." ); println!(""); println!(" lock [wait|nowait] -> Locks the door identified with ."); println!( " Also takes an additional parameter of \"wait\" or \"nowait\"which dictates" ); println!(" whether the CLI exit as soon as the lock operation is queued or whether"); println!(" it waits until the lock status changes to closed."); println!(" Defaults to \"wait\" if not specified."); println!(); println!(" unlock [wait|nowait] -> Unlocks the lock identified with ."); println!( " Also takes an additional parameter of \"wait\" or \"nowait\"which dictates" ); println!(" whether the CLI exit as soon as the lock operation is queued or whether"); println!(" it waits until the lock status changes to open."); println!(" Defaults to \"wait\" if not specified."); exit(0); } cli::CliArgs::Lock { lock_name, wait_until_done, } => { let api = ApiClient::new_from_env()?; let lock = find_lock(&api, &lock_name).await?; println!("dooris at your service, locking {} now…", lock.name); print_lock_warnings(&lock); api.operate_lock(&lock.id, DesiredLockState::CLOSED).await?; if wait_until_done { wait_for_lock_state(&api, &lock_name, LockState::LOCKED).await?; println!("Lock is now closed. Enjoy <3"); } else { println!("Lock is now closing. Enjoy <3"); } exit(0); } cli::CliArgs::Unlock { lock_name, wait_until_done, } => { let api = ApiClient::new_from_env()?; let lock = find_lock(&api, &lock_name).await?; println!("dooris at your service, unlocking {} now…", lock.name); print_lock_warnings(&lock); api.operate_lock(&lock.id, DesiredLockState::OPEN).await?; if wait_until_done { wait_for_lock_state(&api, &lock_name, LockState::UNLOCKED).await?; println!("Lock is now opened. Enjoy <3"); } else { println!("Lock is now opening. Enjoy <3"); } exit(0); } cli::CliArgs::ShowLocks => { let api = ApiClient::new_from_env()?; let locks = api.fetch_locks().await?; for i_lock in locks.iter() { println!(">> {}", i_lock.name); print_lock_warnings(&lock); println!(" State: {:?}", i_lock.status.lock_state); println!(" Desired State: {:?}", i_lock.status.lock_target_level); println!(" Acitivty: {:?}", i_lock.status.activity_state); println!(); } exit(0); } }; } async fn find_lock(api: &ApiClient, name: &str) -> eyre::Result { let locks = api.fetch_locks().await?; tracing::trace!("Comparing locks from API for one with name {name:?}"); let lock = locks .into_iter() .find(|i_lock| i_lock.name.eq_ignore_ascii_case(&name)) .ok_or_else(|| { eyre!("No lock named {name:?} exists. See \"list-locks\" for a list of valid locks.") })?; tracing::trace!(lock = ?lock, "Identified lock with name matching {name:?}"); Ok(lock) } async fn wait_for_lock_state( api: &ApiClient, lock_name: &str, desired_state: LockState, ) -> eyre::Result<()> { tokio::time::sleep(Duration::from_millis(200)).await; let lock = find_lock(&api, &lock_name).await?; let mut interval = tokio::time::interval(Duration::from_millis(500)); let mut last_status = lock.status; while last_status.lock_state != desired_state { interval.tick().await; let lock = find_lock(&api, &lock_name).await?; if lock.status != last_status { println!( "Lock state changed to {:?} (activity={:?})", lock.status.lock_state, lock.status.activity_state ); last_status = lock.status; } } Ok(()) } /// Display big warnings if the lock is in an error state like jammed/disconnected/low battery fn print_lock_warnings(lock: &ApiLock) { let has_any_error = lock.status.is_unreachable || lock.status.is_low_battery || lock.status.is_error_jammed; if has_any_error { println!(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") } if lock.status.is_unreachable { println!(" !!! !!!"); println!(" !!! Lock is currently unreachable !!!"); println!(" !!! !!!"); } if lock.status.is_low_battery { println!(" !!! !!!"); println!(" !!! Lock has low battery !!!"); println!(" !!! !!!"); } if lock.status.is_error_jammed { println!(" !!! !!!"); println!(" !!! Lock is jammed !!!"); println!(" !!! !!!"); } if has_any_error { println!(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") } }