denki 0.1.2

CLI for controlling TP-Link smart home devices — denki (電気) means electricity in Japanese
Documentation
use crate::devices::{
    DeviceKind, can_control_led, can_control_power, can_dim, can_get_clock, can_get_schedules,
    can_set_color, can_set_color_temp, require_energy,
};
use rstest::rstest;
use serde_json::json;

#[test]
fn can_control_power_accepts_supported_device_kinds() {
    assert!(can_control_power(&DeviceKind::Bulb).is_ok());
    assert!(can_control_power(&DeviceKind::Plug).is_ok());
    assert!(can_control_power(&DeviceKind::Dimmer).is_ok());
    assert!(can_control_power(&DeviceKind::Strip).is_ok());
    assert!(can_control_power(&DeviceKind::LightStrip).is_ok());
    assert!(can_control_power(&DeviceKind::Unknown("IOT.X".into())).is_err());
}

#[test]
fn can_dim_accepts_lights_and_dimmer() {
    assert!(can_dim(&DeviceKind::Bulb).is_ok());
    assert!(can_dim(&DeviceKind::Dimmer).is_ok());
    assert!(can_dim(&DeviceKind::LightStrip).is_ok());
    assert!(can_dim(&DeviceKind::Plug).is_err());
    assert!(can_dim(&DeviceKind::Strip).is_err());
    assert!(can_dim(&DeviceKind::Unknown("IOT.X".into())).is_err());
}

#[test]
fn can_dim_error_names_the_command() {
    let err = can_dim(&DeviceKind::Plug).unwrap_err();
    assert!(err.to_string().contains("`dim`"), "{err}");
}

#[rstest]
#[case(crate::devices::can_get_specs as fn(&DeviceKind) -> anyhow::Result<()>, "`specs`")]
#[case(crate::devices::can_get_presets as fn(&DeviceKind) -> anyhow::Result<()>, "`presets`")]
fn bulb_only_guards_accept_bulb_reject_others(
    #[case] guard: fn(&DeviceKind) -> anyhow::Result<()>,
    #[case] cmd: &str,
) {
    assert!(
        guard(&DeviceKind::Bulb).is_ok(),
        "{cmd}: should accept bulb"
    );
    for kind in [
        DeviceKind::Dimmer,
        DeviceKind::Plug,
        DeviceKind::Strip,
        DeviceKind::LightStrip,
    ] {
        let err = guard(&kind).unwrap_err();
        assert!(
            err.to_string().contains(cmd),
            "{cmd}: error should name the command for {kind}: {err}"
        );
    }
}

#[test]
fn color_guards_accept_bulbs_and_lightstrips() {
    for guard in [
        can_set_color_temp as fn(&DeviceKind) -> anyhow::Result<()>,
        can_set_color as fn(&DeviceKind) -> anyhow::Result<()>,
    ] {
        assert!(guard(&DeviceKind::Bulb).is_ok());
        assert!(guard(&DeviceKind::LightStrip).is_ok());
        assert!(guard(&DeviceKind::Plug).is_err());
    }
}

#[rstest]
#[case(can_get_schedules as fn(&DeviceKind) -> anyhow::Result<()>, "`schedules`")]
#[case(can_control_led as fn(&DeviceKind) -> anyhow::Result<()>, "`led`")]
#[case(can_get_clock as fn(&DeviceKind) -> anyhow::Result<()>, "`clock`")]
fn relay_device_guards_accept_plug_dimmer_strip(
    #[case] guard: fn(&DeviceKind) -> anyhow::Result<()>,
    #[case] cmd: &str,
) {
    assert!(guard(&DeviceKind::Plug).is_ok(), "{cmd}: plug");
    assert!(guard(&DeviceKind::Dimmer).is_ok(), "{cmd}: dimmer");
    assert!(guard(&DeviceKind::Strip).is_ok(), "{cmd}: strip");
    assert!(
        guard(&DeviceKind::LightStrip).is_err(),
        "{cmd}: light strip"
    );
    let err = guard(&DeviceKind::Bulb).unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains(cmd), "{cmd}: error should name command: {msg}");
    assert!(
        msg.contains("KP115") || msg.contains("HS220") || msg.contains("HS300"),
        "{cmd}: error should mention supported models: {msg}"
    );
}

#[rstest]
#[case(DeviceKind::Plug)]
#[case(DeviceKind::Strip)]
fn require_energy_bails_when_parse_fails(#[case] kind: DeviceKind) {
    assert!(require_energy(&json!({}), &kind).is_err());
}

#[test]
fn require_energy_plug_with_ene_feature_succeeds() {
    let json = json!({
        "system": { "get_sysinfo": {
            "mic_type": "IOT.SMARTPLUGSWITCH",
            "alias": "Desk Plug", "model": "KP115(US)",
            "hw_ver": "1.0", "sw_ver": "1.1.1",
            "rssi": -48, "relay_state": 1, "feature": "TIM:ENE"
        }}
    });
    assert!(require_energy(&json, &DeviceKind::Plug).is_ok());
}

#[test]
fn require_energy_plug_without_ene_fails_with_model_name() {
    let json = json!({
        "system": { "get_sysinfo": {
            "mic_type": "IOT.SMARTPLUGSWITCH",
            "alias": "Simple Plug", "model": "HS105(US)",
            "hw_ver": "4.0", "sw_ver": "1.0.0",
            "rssi": -55, "relay_state": 0, "feature": "TIM"
        }}
    });
    let err = require_energy(&json, &DeviceKind::Plug).unwrap_err();
    let msg = err.to_string();
    assert!(msg.contains("HS105"), "error should name the model: {msg}");
    assert!(msg.contains("energy"), "{msg}");
}

#[test]
fn require_energy_bulb_and_lightstrip_always_ok() {
    assert!(require_energy(&json!({}), &DeviceKind::Bulb).is_ok());
    assert!(require_energy(&json!({}), &DeviceKind::LightStrip).is_ok());
}

#[test]
fn require_energy_unknown_kind_fails() {
    assert!(require_energy(&json!({}), &DeviceKind::Dimmer).is_err());
    assert!(require_energy(&json!({}), &DeviceKind::Unknown("X".into())).is_err());
}