denki 0.1.1

CLI for controlling TP-Link smart home devices — denki (電気) means electricity in Japanese
Documentation
use anyhow::Result;
use std::time::Duration;

use crate::bulb;
use crate::devices::{self, DeviceKind};
use crate::dimmer;
use crate::ops;

use super::shared::KasaContext;

async fn ensure_light_on(ctx: &KasaContext) -> Result<()> {
    if bulb::parse(ctx.json()).is_some_and(|b| !b.light_state.is_on()) {
        match ctx.kind() {
            DeviceKind::Bulb => ops::bulb_on(ctx.ip()).await?,
            DeviceKind::LightStrip => ops::lightstrip_set_power(ctx.ip(), true).await?,
            _ => {}
        }
    }
    Ok(())
}

pub async fn handle_dim(host: &str, level: u8) -> Result<()> {
    let ctx = KasaContext::load(host, "dim").await?;
    devices::can_dim(ctx.kind())?;
    match ctx.kind() {
        DeviceKind::Bulb => {
            ensure_light_on(&ctx).await?;
            ops::bulb_set_brightness(ctx.ip(), level).await?;
        }
        DeviceKind::LightStrip => ops::lightstrip_set_brightness(ctx.ip(), level).await?,
        DeviceKind::Dimmer => {
            if level > 0 && dimmer::parse(ctx.json()).is_some_and(|d| !d.is_on()) {
                ops::relay_on(ctx.ip()).await?;
            }
            ops::dimmer_set_brightness(ctx.ip(), level).await?;
        }
        other => anyhow::bail!("`dim` is only supported on bulbs and dimmers, not {other}"),
    }
    crate::output::record(serde_json::json!({"brightness_pct":level}));
    crate::output::println!("Brightness -> {level}%");
    Ok(())
}

pub async fn handle_color_temp(host: &str, kelvin: u16) -> Result<()> {
    let ctx = KasaContext::load(host, "color-temp").await?;
    devices::can_set_color_temp(ctx.kind())?;
    match ctx.kind() {
        DeviceKind::Bulb => {
            ensure_light_on(&ctx).await?;
            ops::bulb_set_color_temp(ctx.ip(), kelvin).await?;
        }
        DeviceKind::LightStrip => ops::lightstrip_set_color_temp(ctx.ip(), kelvin).await?,
        _ => unreachable!("capability guard accepted a non-light device"),
    }
    crate::output::record(serde_json::json!({"color_temp_k":kelvin}));
    crate::output::println!("Color temperature -> {kelvin}K");
    Ok(())
}

pub async fn handle_color(host: &str, hue: u16, saturation: u8, value: u8) -> Result<()> {
    let ctx = KasaContext::load(host, "color").await?;
    devices::can_set_color(ctx.kind())?;
    match ctx.kind() {
        DeviceKind::Bulb => {
            ensure_light_on(&ctx).await?;
            ops::bulb_set_color(ctx.ip(), hue, saturation, value).await?;
        }
        DeviceKind::LightStrip => {
            ops::lightstrip_set_color(ctx.ip(), hue, saturation, value).await?
        }
        _ => unreachable!("capability guard accepted a non-light device"),
    }
    crate::output::record(
        serde_json::json!({"hue_deg":hue,"saturation_pct":saturation,"brightness_pct":value}),
    );
    crate::output::println!("Color -> hue:{hue} sat:{saturation} val:{value}");
    Ok(())
}

pub async fn handle_rainbow(host: &str, step_delay_ms: u64) -> Result<()> {
    let ctx = KasaContext::load(host, "rainbow").await?;
    devices::can_set_color(ctx.kind())?;
    let state = bulb::parse(ctx.json())
        .ok_or_else(|| anyhow::anyhow!("Could not read current light state"))?;
    let mut hue = state.light_state.hue() % 360;
    let saturation = state.light_state.saturation();
    let brightness = state.light_state.brightness();

    match ctx.kind() {
        DeviceKind::Bulb => ensure_light_on(&ctx).await?,
        DeviceKind::LightStrip => {}
        other => anyhow::bail!("`rainbow` is only supported on color lights, not {other}"),
    }

    crate::output::println!("Cycling rainbow colors; press Ctrl-C to stop.");
    let stop = tokio::signal::ctrl_c();
    tokio::pin!(stop);
    loop {
        tokio::select! {
            result = &mut stop => {
                result?;
                crate::output::println!("Rainbow stopped at {hue}° hue.");
                return Ok(());
            }
            _ = tokio::time::sleep(Duration::from_millis(step_delay_ms)) => {}
        }

        hue = (hue + 10) % 360;
        match ctx.kind() {
            DeviceKind::Bulb => {
                ops::bulb_set_color(ctx.ip(), hue, saturation, brightness).await?;
            }
            DeviceKind::LightStrip => {
                ops::lightstrip_set_color(ctx.ip(), hue, saturation, brightness).await?;
            }
            _ => unreachable!("capability guard accepted a non-color light"),
        }
    }
}