import pytest from kasa import DeviceType, SmartBulb, SmartBulbPreset, SmartDeviceException from .conftest import ( bulb, bulb_iot, color_bulb, color_bulb_iot, dimmable, handle_turn_on, non_color_bulb, non_dimmable, non_variable_temp, turn_on, variable_temp, variable_temp_iot, ) from .newfakes import BULB_SCHEMA, LIGHT_STATE_SCHEMA @bulb async def test_bulb_sysinfo(dev: SmartBulb): assert dev.sys_info is not None BULB_SCHEMA(dev.sys_info) assert dev.model is not None # TODO: remove special handling for lightstrip if not dev.is_light_strip: assert dev.device_type == DeviceType.Bulb assert dev.is_bulb @bulb async def test_state_attributes(dev: SmartBulb): assert "Brightness" in dev.state_information assert dev.state_information["Brightness"] == dev.brightness assert "Is dimmable" in dev.state_information assert dev.state_information["Is dimmable"] == dev.is_dimmable @bulb_iot async def test_light_state_without_update(dev: SmartBulb, monkeypatch): with pytest.raises(SmartDeviceException): monkeypatch.setitem( dev._last_update["system"]["get_sysinfo"], "light_state", None ) print(dev.light_state) @bulb_iot async def test_get_light_state(dev: SmartBulb): LIGHT_STATE_SCHEMA(await dev.get_light_state()) @color_bulb @turn_on async def test_hsv(dev: SmartBulb, turn_on): await handle_turn_on(dev, turn_on) assert dev.is_color hue, saturation, brightness = dev.hsv assert 0 <= hue <= 360 assert 0 <= saturation <= 100 assert 0 <= brightness <= 100 await dev.set_hsv(hue=1, saturation=1, value=1) await dev.update() hue, saturation, brightness = dev.hsv assert hue == 1 assert saturation == 1 assert brightness == 1 @color_bulb_iot async def test_set_hsv_transition(dev: SmartBulb, mocker): set_light_state = mocker.patch("kasa.SmartBulb.set_light_state") await dev.set_hsv(10, 10, 100, transition=1000) set_light_state.assert_called_with( {"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0}, transition=1000, ) @color_bulb @turn_on async def test_invalid_hsv(dev: SmartBulb, turn_on): await handle_turn_on(dev, turn_on) assert dev.is_color for invalid_hue in [-1, 361, 0.5]: with pytest.raises(ValueError): await dev.set_hsv(invalid_hue, 0, 0) for invalid_saturation in [-1, 101, 0.5]: with pytest.raises(ValueError): await dev.set_hsv(0, invalid_saturation, 0) for invalid_brightness in [-1, 101, 0.5]: with pytest.raises(ValueError): await dev.set_hsv(0, 0, invalid_brightness) @color_bulb async def test_color_state_information(dev: SmartBulb): assert "HSV" in dev.state_information assert dev.state_information["HSV"] == dev.hsv @non_color_bulb async def test_hsv_on_non_color(dev: SmartBulb): assert not dev.is_color with pytest.raises(SmartDeviceException): await dev.set_hsv(0, 0, 0) with pytest.raises(SmartDeviceException): print(dev.hsv) @variable_temp async def test_variable_temp_state_information(dev: SmartBulb): assert "Color temperature" in dev.state_information assert dev.state_information["Color temperature"] == dev.color_temp assert "Valid temperature range" in dev.state_information assert ( dev.state_information["Valid temperature range"] == dev.valid_temperature_range ) @variable_temp @turn_on async def test_try_set_colortemp(dev: SmartBulb, turn_on): await handle_turn_on(dev, turn_on) await dev.set_color_temp(2700) await dev.update() assert dev.color_temp == 2700 @variable_temp_iot async def test_set_color_temp_transition(dev: SmartBulb, mocker): set_light_state = mocker.patch("kasa.SmartBulb.set_light_state") await dev.set_color_temp(2700, transition=100) set_light_state.assert_called_with({"color_temp": 2700}, transition=100) @variable_temp_iot async def test_unknown_temp_range(dev: SmartBulb, monkeypatch, caplog): monkeypatch.setitem(dev._sys_info, "model", "unknown bulb") assert dev.valid_temperature_range == (2700, 5000) assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text @variable_temp async def test_out_of_range_temperature(dev: SmartBulb): with pytest.raises(ValueError): await dev.set_color_temp(1000) with pytest.raises(ValueError): await dev.set_color_temp(10000) @non_variable_temp async def test_non_variable_temp(dev: SmartBulb): with pytest.raises(SmartDeviceException): await dev.set_color_temp(2700) with pytest.raises(SmartDeviceException): print(dev.valid_temperature_range) with pytest.raises(SmartDeviceException): print(dev.color_temp) @dimmable @turn_on async def test_dimmable_brightness(dev: SmartBulb, turn_on): await handle_turn_on(dev, turn_on) assert dev.is_dimmable await dev.set_brightness(50) await dev.update() assert dev.brightness == 50 await dev.set_brightness(10) await dev.update() assert dev.brightness == 10 with pytest.raises(ValueError): await dev.set_brightness("foo") @bulb_iot async def test_turn_on_transition(dev: SmartBulb, mocker): set_light_state = mocker.patch("kasa.SmartBulb.set_light_state") await dev.turn_on(transition=1000) set_light_state.assert_called_with({"on_off": 1}, transition=1000) await dev.turn_off(transition=100) set_light_state.assert_called_with({"on_off": 0}, transition=100) @bulb_iot async def test_dimmable_brightness_transition(dev: SmartBulb, mocker): set_light_state = mocker.patch("kasa.SmartBulb.set_light_state") await dev.set_brightness(10, transition=1000) set_light_state.assert_called_with({"brightness": 10}, transition=1000) @dimmable async def test_invalid_brightness(dev: SmartBulb): assert dev.is_dimmable with pytest.raises(ValueError): await dev.set_brightness(110) with pytest.raises(ValueError): await dev.set_brightness(-100) @non_dimmable async def test_non_dimmable(dev: SmartBulb): assert not dev.is_dimmable with pytest.raises(SmartDeviceException): assert dev.brightness == 0 with pytest.raises(SmartDeviceException): await dev.set_brightness(100) @bulb_iot async def test_ignore_default_not_set_without_color_mode_change_turn_on( dev: SmartBulb, mocker ): query_helper = mocker.patch("kasa.SmartBulb._query_helper") # When turning back without settings, ignore default to restore the state await dev.turn_on() args, kwargs = query_helper.call_args_list[0] assert args[2] == {"on_off": 1, "ignore_default": 0} await dev.turn_off() args, kwargs = query_helper.call_args_list[1] assert args[2] == {"on_off": 0, "ignore_default": 1} @bulb_iot async def test_list_presets(dev: SmartBulb): presets = dev.presets assert len(presets) == len(dev.sys_info["preferred_state"]) for preset, raw in zip(presets, dev.sys_info["preferred_state"]): assert preset.index == raw["index"] assert preset.hue == raw["hue"] assert preset.brightness == raw["brightness"] assert preset.saturation == raw["saturation"] assert preset.color_temp == raw["color_temp"] @bulb_iot async def test_modify_preset(dev: SmartBulb, mocker): """Verify that modifying preset calls the and exceptions are raised properly.""" if not dev.presets: pytest.skip("Some strips do not support presets") data = { "index": 0, "brightness": 10, "hue": 0, "saturation": 0, "color_temp": 0, } preset = SmartBulbPreset(**data) assert preset.index == 0 assert preset.brightness == 10 assert preset.hue == 0 assert preset.saturation == 0 assert preset.color_temp == 0 await dev.save_preset(preset) assert dev.presets[0].brightness == 10 with pytest.raises(SmartDeviceException): await dev.save_preset( SmartBulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) ) @bulb_iot @pytest.mark.parametrize( ("preset", "payload"), [ ( SmartBulbPreset(index=0, hue=0, brightness=1, saturation=0), {"index": 0, "hue": 0, "brightness": 1, "saturation": 0}, ), ( SmartBulbPreset(index=0, brightness=1, id="testid", mode=2, custom=0), {"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0}, ), ], ) async def test_modify_preset_payloads(dev: SmartBulb, preset, payload, mocker): """Test that modify preset payloads ignore none values.""" if not dev.presets: pytest.skip("Some strips do not support presets") query_helper = mocker.patch("kasa.SmartBulb._query_helper") await dev.save_preset(preset) query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)