mirror of
				https://github.com/python-kasa/python-kasa.git
				synced 2025-11-04 06:32:07 +00:00 
			
		
		
		
	Update cli to use common modules and remove iot specific cli testing (#913)
This commit is contained in:
		
							
								
								
									
										74
									
								
								kasa/cli.py
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								kasa/cli.py
									
									
									
									
									
								
							@@ -27,7 +27,7 @@ from kasa import (
 | 
			
		||||
    EncryptType,
 | 
			
		||||
    Feature,
 | 
			
		||||
    KasaException,
 | 
			
		||||
    Light,
 | 
			
		||||
    Module,
 | 
			
		||||
    UnsupportedDeviceError,
 | 
			
		||||
)
 | 
			
		||||
from kasa.discover import DiscoveryResult
 | 
			
		||||
@@ -859,18 +859,18 @@ async def usage(dev: Device, year, month, erase):
 | 
			
		||||
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
 | 
			
		||||
@click.option("--transition", type=int, required=False)
 | 
			
		||||
@pass_dev
 | 
			
		||||
async def brightness(dev: Light, brightness: int, transition: int):
 | 
			
		||||
async def brightness(dev: Device, brightness: int, transition: int):
 | 
			
		||||
    """Get or set brightness."""
 | 
			
		||||
    if not dev.is_dimmable:
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
 | 
			
		||||
        echo("This device does not support brightness.")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if brightness is None:
 | 
			
		||||
        echo(f"Brightness: {dev.brightness}")
 | 
			
		||||
        return dev.brightness
 | 
			
		||||
        echo(f"Brightness: {light.brightness}")
 | 
			
		||||
        return light.brightness
 | 
			
		||||
    else:
 | 
			
		||||
        echo(f"Setting brightness to {brightness}")
 | 
			
		||||
        return await dev.set_brightness(brightness, transition=transition)
 | 
			
		||||
        return await light.set_brightness(brightness, transition=transition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@@ -879,15 +879,15 @@ async def brightness(dev: Light, brightness: int, transition: int):
 | 
			
		||||
)
 | 
			
		||||
@click.option("--transition", type=int, required=False)
 | 
			
		||||
@pass_dev
 | 
			
		||||
async def temperature(dev: Light, temperature: int, transition: int):
 | 
			
		||||
async def temperature(dev: Device, temperature: int, transition: int):
 | 
			
		||||
    """Get or set color temperature."""
 | 
			
		||||
    if not dev.is_variable_color_temp:
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
 | 
			
		||||
        echo("Device does not support color temperature")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if temperature is None:
 | 
			
		||||
        echo(f"Color temperature: {dev.color_temp}")
 | 
			
		||||
        valid_temperature_range = dev.valid_temperature_range
 | 
			
		||||
        echo(f"Color temperature: {light.color_temp}")
 | 
			
		||||
        valid_temperature_range = light.valid_temperature_range
 | 
			
		||||
        if valid_temperature_range != (0, 0):
 | 
			
		||||
            echo("(min: {}, max: {})".format(*valid_temperature_range))
 | 
			
		||||
        else:
 | 
			
		||||
@@ -895,31 +895,34 @@ async def temperature(dev: Light, temperature: int, transition: int):
 | 
			
		||||
                "Temperature range unknown, please open a github issue"
 | 
			
		||||
                f" or a pull request for model '{dev.model}'"
 | 
			
		||||
            )
 | 
			
		||||
        return dev.valid_temperature_range
 | 
			
		||||
        return light.valid_temperature_range
 | 
			
		||||
    else:
 | 
			
		||||
        echo(f"Setting color temperature to {temperature}")
 | 
			
		||||
        return await dev.set_color_temp(temperature, transition=transition)
 | 
			
		||||
        return await light.set_color_temp(temperature, transition=transition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@click.argument("effect", type=click.STRING, default=None, required=False)
 | 
			
		||||
@click.pass_context
 | 
			
		||||
@pass_dev
 | 
			
		||||
async def effect(dev, ctx, effect):
 | 
			
		||||
async def effect(dev: Device, ctx, effect):
 | 
			
		||||
    """Set an effect."""
 | 
			
		||||
    if not dev.has_effects:
 | 
			
		||||
    if not (light_effect := dev.modules.get(Module.LightEffect)):
 | 
			
		||||
        echo("Device does not support effects")
 | 
			
		||||
        return
 | 
			
		||||
    if effect is None:
 | 
			
		||||
        raise click.BadArgumentUsage(
 | 
			
		||||
            f"Setting an effect requires a named built-in effect: {dev.effect_list}",
 | 
			
		||||
            "Setting an effect requires a named built-in effect: "
 | 
			
		||||
            + f"{light_effect.effect_list}",
 | 
			
		||||
            ctx,
 | 
			
		||||
        )
 | 
			
		||||
    if effect not in dev.effect_list:
 | 
			
		||||
        raise click.BadArgumentUsage(f"Effect must be one of: {dev.effect_list}", ctx)
 | 
			
		||||
    if effect not in light_effect.effect_list:
 | 
			
		||||
        raise click.BadArgumentUsage(
 | 
			
		||||
            f"Effect must be one of: {light_effect.effect_list}", ctx
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    echo(f"Setting Effect: {effect}")
 | 
			
		||||
    return await dev.set_effect(effect)
 | 
			
		||||
    return await light_effect.set_effect(effect)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@@ -929,33 +932,36 @@ async def effect(dev, ctx, effect):
 | 
			
		||||
@click.option("--transition", type=int, required=False)
 | 
			
		||||
@click.pass_context
 | 
			
		||||
@pass_dev
 | 
			
		||||
async def hsv(dev, ctx, h, s, v, transition):
 | 
			
		||||
async def hsv(dev: Device, ctx, h, s, v, transition):
 | 
			
		||||
    """Get or set color in HSV."""
 | 
			
		||||
    if not dev.is_color:
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_color:
 | 
			
		||||
        echo("Device does not support colors")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if h is None or s is None or v is None:
 | 
			
		||||
        echo(f"Current HSV: {dev.hsv}")
 | 
			
		||||
        return dev.hsv
 | 
			
		||||
    if h is None and s is None and v is None:
 | 
			
		||||
        echo(f"Current HSV: {light.hsv}")
 | 
			
		||||
        return light.hsv
 | 
			
		||||
    elif s is None or v is None:
 | 
			
		||||
        raise click.BadArgumentUsage("Setting a color requires 3 values.", ctx)
 | 
			
		||||
    else:
 | 
			
		||||
        echo(f"Setting HSV: {h} {s} {v}")
 | 
			
		||||
        return await dev.set_hsv(h, s, v, transition=transition)
 | 
			
		||||
        return await light.set_hsv(h, s, v, transition=transition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@click.argument("state", type=bool, required=False)
 | 
			
		||||
@pass_dev
 | 
			
		||||
async def led(dev, state):
 | 
			
		||||
async def led(dev: Device, state):
 | 
			
		||||
    """Get or set (Plug's) led state."""
 | 
			
		||||
    if not (led := dev.modules.get(Module.Led)):
 | 
			
		||||
        echo("Device does not support led.")
 | 
			
		||||
        return
 | 
			
		||||
    if state is not None:
 | 
			
		||||
        echo(f"Turning led to {state}")
 | 
			
		||||
        return await dev.set_led(state)
 | 
			
		||||
        return await led.set_led(state)
 | 
			
		||||
    else:
 | 
			
		||||
        echo(f"LED state: {dev.led}")
 | 
			
		||||
        return dev.led
 | 
			
		||||
        echo(f"LED state: {led.led}")
 | 
			
		||||
        return led.led
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.command()
 | 
			
		||||
@@ -975,8 +981,8 @@ async def time(dev):
 | 
			
		||||
async def on(dev: Device, index: int, name: str, transition: int):
 | 
			
		||||
    """Turn the device on."""
 | 
			
		||||
    if index is not None or name is not None:
 | 
			
		||||
        if not dev.is_strip:
 | 
			
		||||
            echo("Index and name are only for power strips!")
 | 
			
		||||
        if not dev.children:
 | 
			
		||||
            echo("Index and name are only for devices with children.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if index is not None:
 | 
			
		||||
@@ -996,8 +1002,8 @@ async def on(dev: Device, index: int, name: str, transition: int):
 | 
			
		||||
async def off(dev: Device, index: int, name: str, transition: int):
 | 
			
		||||
    """Turn the device off."""
 | 
			
		||||
    if index is not None or name is not None:
 | 
			
		||||
        if not dev.is_strip:
 | 
			
		||||
            echo("Index and name are only for power strips!")
 | 
			
		||||
        if not dev.children:
 | 
			
		||||
            echo("Index and name are only for devices with children.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if index is not None:
 | 
			
		||||
@@ -1017,8 +1023,8 @@ async def off(dev: Device, index: int, name: str, transition: int):
 | 
			
		||||
async def toggle(dev: Device, index: int, name: str, transition: int):
 | 
			
		||||
    """Toggle the device on/off."""
 | 
			
		||||
    if index is not None or name is not None:
 | 
			
		||||
        if not dev.is_strip:
 | 
			
		||||
            echo("Index and name are only for power strips!")
 | 
			
		||||
        if not dev.children:
 | 
			
		||||
            echo("Index and name are only for devices with children.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if index is not None:
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ from kasa import (
 | 
			
		||||
    DeviceError,
 | 
			
		||||
    EmeterStatus,
 | 
			
		||||
    KasaException,
 | 
			
		||||
    Module,
 | 
			
		||||
    UnsupportedDeviceError,
 | 
			
		||||
)
 | 
			
		||||
from kasa.cli import (
 | 
			
		||||
@@ -21,11 +22,15 @@ from kasa.cli import (
 | 
			
		||||
    brightness,
 | 
			
		||||
    cli,
 | 
			
		||||
    cmd_command,
 | 
			
		||||
    effect,
 | 
			
		||||
    emeter,
 | 
			
		||||
    hsv,
 | 
			
		||||
    led,
 | 
			
		||||
    raw_command,
 | 
			
		||||
    reboot,
 | 
			
		||||
    state,
 | 
			
		||||
    sysinfo,
 | 
			
		||||
    temperature,
 | 
			
		||||
    toggle,
 | 
			
		||||
    update_credentials,
 | 
			
		||||
    wifi,
 | 
			
		||||
@@ -34,7 +39,6 @@ from kasa.discover import Discover, DiscoveryResult
 | 
			
		||||
from kasa.iot import IotDevice
 | 
			
		||||
 | 
			
		||||
from .conftest import (
 | 
			
		||||
    device_iot,
 | 
			
		||||
    device_smart,
 | 
			
		||||
    get_device_for_fixture_protocol,
 | 
			
		||||
    handle_turn_on,
 | 
			
		||||
@@ -78,11 +82,10 @@ async def test_update_called_by_cli(dev, mocker, runner):
 | 
			
		||||
    update.assert_called()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@device_iot
 | 
			
		||||
async def test_sysinfo(dev, runner):
 | 
			
		||||
async def test_sysinfo(dev: Device, runner):
 | 
			
		||||
    res = await runner.invoke(sysinfo, obj=dev)
 | 
			
		||||
    assert "System info" in res.output
 | 
			
		||||
    assert dev.alias in res.output
 | 
			
		||||
    assert dev.model in res.output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@turn_on
 | 
			
		||||
@@ -108,7 +111,6 @@ async def test_toggle(dev, turn_on, runner):
 | 
			
		||||
    assert dev.is_on != turn_on
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@device_iot
 | 
			
		||||
async def test_alias(dev, runner):
 | 
			
		||||
    res = await runner.invoke(alias, obj=dev)
 | 
			
		||||
    assert f"Alias: {dev.alias}" in res.output
 | 
			
		||||
@@ -308,15 +310,14 @@ async def test_emeter(dev: Device, mocker, runner):
 | 
			
		||||
    daily.assert_called_with(year=1900, month=12)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@device_iot
 | 
			
		||||
async def test_brightness(dev, runner):
 | 
			
		||||
async def test_brightness(dev: Device, runner):
 | 
			
		||||
    res = await runner.invoke(brightness, obj=dev)
 | 
			
		||||
    if not dev.is_dimmable:
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
 | 
			
		||||
        assert "This device does not support brightness." in res.output
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(brightness, obj=dev)
 | 
			
		||||
    assert f"Brightness: {dev.brightness}" in res.output
 | 
			
		||||
    assert f"Brightness: {light.brightness}" in res.output
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(brightness, ["12"], obj=dev)
 | 
			
		||||
    assert "Setting brightness" in res.output
 | 
			
		||||
@@ -326,7 +327,110 @@ async def test_brightness(dev, runner):
 | 
			
		||||
    assert "Brightness: 12" in res.output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@device_iot
 | 
			
		||||
async def test_color_temperature(dev: Device, runner):
 | 
			
		||||
    res = await runner.invoke(temperature, obj=dev)
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
 | 
			
		||||
        assert "Device does not support color temperature" in res.output
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(temperature, obj=dev)
 | 
			
		||||
    assert f"Color temperature: {light.color_temp}" in res.output
 | 
			
		||||
    valid_range = light.valid_temperature_range
 | 
			
		||||
    assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
 | 
			
		||||
 | 
			
		||||
    val = int((valid_range.min + valid_range.max) / 2)
 | 
			
		||||
    res = await runner.invoke(temperature, [str(val)], obj=dev)
 | 
			
		||||
    assert "Setting color temperature to " in res.output
 | 
			
		||||
    await dev.update()
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(temperature, obj=dev)
 | 
			
		||||
    assert f"Color temperature: {val}" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
 | 
			
		||||
    invalid_max = valid_range.max + 100
 | 
			
		||||
    # Lights that support the maximum range will not get past the click cli range check
 | 
			
		||||
    # So can't be tested for the internal range check.
 | 
			
		||||
    if invalid_max < 9000:
 | 
			
		||||
        res = await runner.invoke(temperature, [str(invalid_max)], obj=dev)
 | 
			
		||||
        assert res.exit_code == 1
 | 
			
		||||
        assert isinstance(res.exception, ValueError)
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(temperature, [str(9100)], obj=dev)
 | 
			
		||||
    assert res.exit_code == 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_color_hsv(dev: Device, runner: CliRunner):
 | 
			
		||||
    res = await runner.invoke(hsv, obj=dev)
 | 
			
		||||
    if not (light := dev.modules.get(Module.Light)) or not light.is_color:
 | 
			
		||||
        assert "Device does not support colors" in res.output
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(hsv, obj=dev)
 | 
			
		||||
    assert f"Current HSV: {light.hsv}" in res.output
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(hsv, ["180", "50", "50"], obj=dev)
 | 
			
		||||
    assert "Setting HSV: 180 50 50" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
    await dev.update()
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(hsv, ["180", "50"], obj=dev)
 | 
			
		||||
    assert "Setting a color requires 3 values." in res.output
 | 
			
		||||
    assert res.exit_code == 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_light_effect(dev: Device, runner: CliRunner):
 | 
			
		||||
    res = await runner.invoke(effect, obj=dev)
 | 
			
		||||
    if not (light_effect := dev.modules.get(Module.LightEffect)):
 | 
			
		||||
        assert "Device does not support effects" in res.output
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Start off with a known state of off
 | 
			
		||||
    await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
 | 
			
		||||
    await dev.update()
 | 
			
		||||
    assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(effect, obj=dev)
 | 
			
		||||
    msg = (
 | 
			
		||||
        "Setting an effect requires a named built-in effect: "
 | 
			
		||||
        + f"{light_effect.effect_list}"
 | 
			
		||||
    )
 | 
			
		||||
    assert msg in res.output
 | 
			
		||||
    assert res.exit_code == 2
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(effect, [light_effect.effect_list[1]], obj=dev)
 | 
			
		||||
    assert f"Setting Effect: {light_effect.effect_list[1]}" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
    await dev.update()
 | 
			
		||||
    assert light_effect.effect == light_effect.effect_list[1]
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(effect, ["foobar"], obj=dev)
 | 
			
		||||
    assert f"Effect must be one of: {light_effect.effect_list}" in res.output
 | 
			
		||||
    assert res.exit_code == 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_led(dev: Device, runner: CliRunner):
 | 
			
		||||
    res = await runner.invoke(led, obj=dev)
 | 
			
		||||
    if not (led_module := dev.modules.get(Module.Led)):
 | 
			
		||||
        assert "Device does not support led" in res.output
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(led, obj=dev)
 | 
			
		||||
    assert f"LED state: {led_module.led}" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(led, ["on"], obj=dev)
 | 
			
		||||
    assert "Turning led to True" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
    await dev.update()
 | 
			
		||||
    assert led_module.led is True
 | 
			
		||||
 | 
			
		||||
    res = await runner.invoke(led, ["off"], obj=dev)
 | 
			
		||||
    assert "Turning led to False" in res.output
 | 
			
		||||
    assert res.exit_code == 0
 | 
			
		||||
    await dev.update()
 | 
			
		||||
    assert led_module.led is False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_json_output(dev: Device, mocker, runner):
 | 
			
		||||
    """Test that the json output produces correct output."""
 | 
			
		||||
    mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev})
 | 
			
		||||
@@ -375,7 +479,6 @@ async def test_credentials(discovery_mock, mocker, runner):
 | 
			
		||||
    assert "Username:foo Password:bar\n" in res.output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@device_iot
 | 
			
		||||
async def test_without_device_type(dev, mocker, runner):
 | 
			
		||||
    """Test connecting without the device type."""
 | 
			
		||||
    discovery_mock = mocker.patch(
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user