190 lines
6.9 KiB
Rust
190 lines
6.9 KiB
Rust
//! 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 <help | show-locks | lock <door> [wait|nowait] | unlock <door> [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 <lock> [wait|nowait] -> Locks the door identified with <lock>.");
|
|
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 <lock> [wait|nowait] -> Unlocks the lock identified with <lock>.");
|
|
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<ApiLock> {
|
|
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!(" !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
|
}
|
|
}
|