use std::{
fs::OpenOptions,
io::Write,
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
time::Duration,
};
use clap::{Args, Parser, Subcommand};
use glimta::{Credentials, Gateway, Result};
#[derive(Parser)]
#[command(name = "glimta", about = "Talk to a classic IKEA TRADFRI gateway")]
struct Cli {
#[command(subcommand)]
command: CliCommand,
}
#[derive(Subcommand)]
enum CliCommand {
Discover,
Provision {
#[arg(long)]
gateway: Option<String>,
#[arg(long)]
credentials: PathBuf,
#[arg(long)]
identity: Option<String>,
},
Devices(ConnectionArgs),
Groups(ConnectionArgs),
LightOn {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
},
LightOff {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
},
LightBrightness {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
#[arg(long)]
value: u16,
},
Socket {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
#[arg(long)]
on: bool,
},
Blind {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
#[arg(long)]
position: u8,
},
PurifierAuto {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
},
PurifierOff {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
},
PurifierFan {
#[command(flatten)]
connection: ConnectionArgs,
#[arg(long)]
device: u32,
#[arg(long)]
speed: u8,
},
}
#[derive(Args)]
struct ConnectionArgs {
#[arg(long)]
gateway: Option<String>,
#[arg(long)]
credentials: PathBuf,
}
#[tokio::main]
async fn main() {
if let Err(error) = run(Cli::parse()).await {
eprintln!("Error: {error}");
std::process::exit(1);
}
}
async fn run(cli: Cli) -> Result<()> {
match cli.command {
CliCommand::Discover => discover().await?,
CliCommand::Provision {
gateway,
credentials,
identity,
} => provision(gateway.as_deref(), &credentials, identity.as_deref()).await?,
CliCommand::Devices(connection) => list_devices(&connection).await?,
CliCommand::Groups(connection) => list_groups(&connection).await?,
CliCommand::LightOn { connection, device } => {
client(&connection)
.await?
.set_light_state(device, true)
.await?;
}
CliCommand::LightOff { connection, device } => {
client(&connection)
.await?
.set_light_state(device, false)
.await?;
}
CliCommand::LightBrightness {
connection,
device,
value,
} => {
client(&connection)
.await?
.set_light_brightness(device, value, None)
.await?;
}
CliCommand::Socket {
connection,
device,
on,
} => {
client(&connection)
.await?
.set_socket_state(device, on)
.await?;
}
CliCommand::Blind {
connection,
device,
position,
} => {
client(&connection)
.await?
.set_blind_position(device, position)
.await?;
}
CliCommand::PurifierAuto { connection, device } => {
client(&connection)
.await?
.set_air_purifier_auto(device)
.await?;
}
CliCommand::PurifierOff { connection, device } => {
client(&connection)
.await?
.turn_air_purifier_off(device)
.await?;
}
CliCommand::PurifierFan {
connection,
device,
speed,
} => {
client(&connection)
.await?
.set_air_purifier_fan_speed(device, speed)
.await?;
}
}
Ok(())
}
async fn discover() -> Result<()> {
let gateway = Gateway::discover(Duration::from_secs(5)).await?;
println!("{}", gateway.address());
Ok(())
}
async fn provision(gateway: Option<&str>, path: &Path, identity: Option<&str>) -> Result<()> {
let gateway = resolve_gateway(gateway).await?;
let security_code = rpassword::prompt_password("Gateway security code: ")?;
let credentials = if let Some(identity) = identity {
gateway
.provision_with_identity(&security_code, identity)
.await?
} else {
gateway.provision(&security_code).await?
};
write_credentials(path, &credentials)?;
println!("Credentials written to {}", path.display());
Ok(())
}
async fn list_devices(connection: &ConnectionArgs) -> Result<()> {
let (gateway, credentials) = load_connection(connection).await?;
for device in gateway.connect(credentials).devices().await? {
println!(
"{}\t{}\t{:?}\treachable={:?}",
device.id,
device.name(),
device.capabilities(),
device.is_reachable()
);
}
Ok(())
}
async fn list_groups(connection: &ConnectionArgs) -> Result<()> {
let (gateway, credentials) = load_connection(connection).await?;
for group in gateway.connect(credentials).groups().await? {
println!(
"{}\t{}\ton={}\tmembers={:?}",
group.id,
group.name(),
group.is_on(),
group.member_ids()
);
}
Ok(())
}
async fn client(connection: &ConnectionArgs) -> Result<glimta::Client> {
let (gateway, credentials) = load_connection(connection).await?;
Ok(gateway.connect(credentials))
}
async fn load_connection(connection: &ConnectionArgs) -> Result<(Gateway, Credentials)> {
let gateway = resolve_gateway(connection.gateway.as_deref()).await?;
let credentials = read_credentials(&connection.credentials)?;
Ok((gateway, credentials))
}
async fn resolve_gateway(value: Option<&str>) -> Result<Gateway> {
match value {
Some(value) => Ok(parse_gateway(value)?),
None => Gateway::discover(Duration::from_secs(5)).await,
}
}
fn parse_gateway(value: &str) -> std::io::Result<Gateway> {
if let Ok(address) = value.parse::<SocketAddr>() {
return Ok(Gateway::from_socket_addr(address));
}
value.parse::<IpAddr>().map(Gateway::new).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid gateway address {value:?}: {error}"),
)
})
}
fn read_credentials(path: &Path) -> Result<Credentials> {
Ok(serde_json::from_slice(&std::fs::read(path)?)?)
}
fn write_credentials(path: &Path, credentials: &Credentials) -> Result<()> {
let data = serde_json::to_vec_pretty(credentials)?;
let mut options = OpenOptions::new();
options.create(true).truncate(true).write(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(path)?;
file.write_all(&data)?;
file.write_all(b"\n")?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
Ok(())
}