denki 0.1.2

CLI for controlling TP-Link smart home devices — denki (電気) means electricity in Japanese
Documentation
use crate::bulb::{LightState, LightingEffectState};
use colored::{ColoredString, Colorize};

pub(crate) fn short_fw(fw: &str) -> String {
    crate::output::terminal_text(fw.split_whitespace().next().unwrap_or(fw))
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn wh_from(entry: &serde_json::Value) -> u64 {
    entry
        .get("energy_wh")
        .and_then(serde_json::Value::as_u64)
        .or_else(|| {
            entry
                .get("energy")
                .and_then(serde_json::Value::as_f64)
                .map(|kwh| (kwh * 1000.0).round() as u64)
        })
        .unwrap_or(0)
}

#[allow(
    clippy::many_single_char_names,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss
)]
pub(crate) fn hsv_to_rgb(h: u16, s: u8, v: u8) -> (u8, u8, u8) {
    let s = f32::from(s) / 100.0;
    let v = f32::from(v) / 100.0;
    let h = f32::from(h);
    let c = v * s;
    let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
    let m = v - c;
    let (r1, g1, b1) = match (h as u16) / 60 {
        0 => (c, x, 0.0),
        1 => (x, c, 0.0),
        2 => (0.0, c, x),
        3 => (0.0, x, c),
        4 => (x, 0.0, c),
        _ => (c, 0.0, x),
    };
    (
        ((r1 + m) * 255.0) as u8,
        ((g1 + m) * 255.0) as u8,
        ((b1 + m) * 255.0) as u8,
    )
}

pub(crate) fn on_state(is_on: bool) -> ColoredString {
    if is_on {
        "on".green().bold()
    } else {
        "off".dimmed()
    }
}

pub(crate) fn on_state_detail(is_on: bool) -> ColoredString {
    if is_on {
        "ON".green().bold()
    } else {
        "OFF".red()
    }
}

pub(crate) fn header(name: &str) -> ColoredString {
    format!("== {} ==", crate::output::terminal_text(name)).bold()
}

pub(crate) fn signal_label(rssi: i32) -> colored::ColoredString {
    if rssi >= -50 {
        "excellent".green()
    } else if rssi >= -65 {
        "good".yellow()
    } else {
        "weak".red()
    }
}

pub(crate) fn signal_summary(rssi: i32) -> String {
    format!("signal:{}", signal_label(rssi))
}

pub(crate) fn tapo_signal_label(level: u8) -> colored::ColoredString {
    match level {
        3 => "excellent".green(),
        2 => "good".yellow(),
        1 => "weak".red(),
        _ => "no signal".red(),
    }
}

pub(crate) fn print_light_color(ls: &LightState, indent: &str) {
    if ls.color_temp() > 0 {
        crate::output::println!(
            "{indent}Brightness: {}%  Warmth: {}K",
            ls.brightness(),
            ls.color_temp()
        );
    } else {
        let (r, g, b) = hsv_to_rgb(ls.hue(), ls.saturation(), ls.brightness());
        let swatch = "██".truecolor(r, g, b);
        crate::output::println!(
            "{indent}Brightness: {}%  Color: {} {}° hue  {} sat",
            ls.brightness(),
            swatch,
            ls.hue(),
            ls.saturation(),
        );
    }
}

pub(crate) fn print_light_state_detail(ls: &LightState) {
    crate::output::println!("  Brightness: {}%", ls.brightness());
    if ls.color_temp() > 0 {
        crate::output::println!("  Warmth:     {}K", ls.color_temp());
    } else {
        let (r, g, b) = hsv_to_rgb(ls.hue(), ls.saturation(), ls.brightness());
        crate::output::println!(
            "  Color:      {} {}° hue  {} sat",
            "██".truecolor(r, g, b),
            ls.hue(),
            ls.saturation()
        );
    }
}

pub(crate) fn print_light_effect_detail(effect: Option<&LightingEffectState>) {
    if let Some(effect) = effect {
        let name = if effect.enable == 1 {
            effect.name.as_str()
        } else {
            "Off"
        };
        crate::output::println!("  Effect:     {}", crate::output::terminal_text(name));
    }
}

pub(crate) fn format_energy_lines(d: &serde_json::Value) -> Vec<String> {
    let mut lines = Vec::new();
    if d.get("power_mw").is_some() {
        if let Some(mw) = d.get("power_mw").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Power:   {:.2} W", mw / 1000.0));
        }
        if let Some(mv) = d.get("voltage_mv").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Voltage: {:.1} V", mv / 1000.0));
        }
        if let Some(ma) = d.get("current_ma").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Current: {:.3} A", ma / 1000.0));
        }
        if let Some(wh) = d.get("total_wh").and_then(serde_json::Value::as_u64) {
            lines.push(format!("Total:   {wh} Wh"));
        }
    } else {
        if let Some(w) = d.get("power").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Power:   {w:.2} W"));
        }
        if let Some(v) = d.get("voltage").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Voltage: {v:.1} V"));
        }
        if let Some(a) = d.get("current").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Current: {a:.3} A"));
        }
        if let Some(kwh) = d.get("total").and_then(serde_json::Value::as_f64) {
            lines.push(format!("Total:   {kwh:.3} kWh"));
        }
    }
    lines
}

pub(crate) fn format_wday(wday: Option<&Vec<serde_json::Value>>) -> String {
    const LABELS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
    let Some(days) = wday else {
        return "every day".to_string();
    };
    if days.len() != LABELS.len() || days.iter().any(|v| !matches!(v.as_u64(), Some(0 | 1))) {
        return "invalid weekday data".to_string();
    }
    let active: Vec<&str> = LABELS
        .into_iter()
        .zip(days)
        .filter(|(_, v)| v.as_u64() == Some(1))
        .map(|(label, _)| label)
        .collect();
    match active.len() {
        0 => "no days".to_string(),
        7 => "every day".to_string(),
        _ => active.join(" "),
    }
}

pub(crate) fn sort_energy_entries(list: &[serde_json::Value], key: &str) -> Vec<(u64, u64)> {
    let mut entries: Vec<(u64, u64)> = list
        .iter()
        .map(|e| (e[key].as_u64().unwrap_or(0), wh_from(e)))
        .collect();
    entries.sort_by_key(|(k, _)| *k);
    entries
}