"""Tests for the Bulb API.""" from typing import AsyncGenerator from unittest.mock import patch import pytest from pywizlight import SCENES, PilotBuilder, wizlight from pywizlight.bulb import states_match from pywizlight.bulblibrary import BulbClass, BulbType, Features, KelvinRange from pywizlight.discovery import discover_lights from pywizlight.exceptions import WizLightConnectionError from pywizlight.tests.fake_bulb import startup_bulb @pytest.fixture() async def correct_bulb() -> AsyncGenerator[wizlight, None]: shutdown, port = await startup_bulb( module_name="ESP01_SHRGB_03", firmware_version="1.25.0" ) bulb = wizlight(ip="127.0.0.1", port=port) yield bulb await bulb.async_close() shutdown() @pytest.fixture() async def power_socket_old_firmware() -> AsyncGenerator[wizlight, None]: shutdown, port = await startup_bulb( module_name="ESP25_SOCKET_01", firmware_version="1.26.1" ) bulb = wizlight(ip="127.0.0.1", port=port) yield bulb await bulb.async_close() shutdown() @pytest.fixture() async def power_socket() -> AsyncGenerator[wizlight, None]: shutdown, port = await startup_bulb( module_name="ESP25_SOCKET_01", firmware_version="1.26.2" ) bulb = wizlight(ip="127.0.0.1", port=port) yield bulb await bulb.async_close() shutdown() @pytest.fixture() async def bad_bulb() -> AsyncGenerator[wizlight, None]: bulb = wizlight(ip="1.1.1.1") yield bulb await bulb.async_close() # Non-Error states - PilotBuilder - Turn On @pytest.mark.asyncio async def test_Bulb_Discovery(correct_bulb: wizlight) -> None: """Test discovery function.""" # Use a random available port since otherwise the # test may fail with patch("pywizlight.discovery.PORT", 0): bulbs = await discover_lights(broadcast_space="192.168.178.255", wait_time=0.02) for bulb in bulbs: with patch("pywizlight.bulb.FIRST_SEND_INTERVAL", 0.01), patch( "pywizlight.bulb.TIMEOUT", 0.01 ): state = await bulb.updateState() assert state and state.get_state() is False @pytest.mark.asyncio async def test_PilotBuilder_state(correct_bulb: wizlight) -> None: """Test State.""" state = await correct_bulb.updateState() assert state and state.get_state() is False @pytest.mark.asyncio async def test_PilotBuilder_colortemp(correct_bulb: wizlight) -> None: """Test Color Temp.""" await correct_bulb.turn_on(PilotBuilder(colortemp=2800)) state = await correct_bulb.updateState() assert state and state.get_colortemp() == 2800 @pytest.mark.asyncio async def test_PilotBuilder_brightness(correct_bulb: wizlight) -> None: """Test Brightness.""" await correct_bulb.turn_on(PilotBuilder(brightness=10)) state = await correct_bulb.updateState() # 10% == 26 in Hex assert state and state.get_brightness() == 26 @pytest.mark.asyncio async def test_PilotBuilder_warm_wite(correct_bulb: wizlight) -> None: """Test Warm White.""" await correct_bulb.turn_on(PilotBuilder(warm_white=255)) state = await correct_bulb.updateState() assert state and state.get_warm_white() == 255 @pytest.mark.asyncio async def test_PilotBuilder_cold_white(correct_bulb: wizlight) -> None: """Test Cold White.""" await correct_bulb.turn_on(PilotBuilder(cold_white=255)) state = await correct_bulb.updateState() assert state and state.get_cold_white() == 255 @pytest.mark.asyncio async def test_PilotBuilder_rgb(correct_bulb: wizlight) -> None: """Test RGB Value.""" await correct_bulb.turn_on(PilotBuilder(rgb=(0, 128, 255))) state = await correct_bulb.updateState() assert state and state.get_rgb() == (0, 127, 255) @pytest.mark.asyncio async def test_PilotBuilder_hucolor(correct_bulb: wizlight) -> None: """Test RGB Value via hucolor.""" await correct_bulb.turn_on(PilotBuilder(hucolor=(100, 50))) state = await correct_bulb.updateState() assert state and state.get_rgb() == (88.0, 255.0, 0.0) @pytest.mark.asyncio async def test_setting_rgbw(correct_bulb: wizlight) -> None: """Test setting rgbw.""" await correct_bulb.turn_on(PilotBuilder(rgbw=(1, 2, 3, 4))) state = await correct_bulb.updateState() assert state and state.get_rgbw() == (1, 2, 3, 4) @pytest.mark.asyncio async def test_PilotBuilder_scene(correct_bulb: wizlight) -> None: """Test scene.""" await correct_bulb.turn_on(PilotBuilder(scene=1)) state = await correct_bulb.updateState() assert state and state.get_scene() == SCENES[1] state.pilotResult["schdPsetId"] = True assert state.get_scene() == SCENES[1000] @pytest.mark.asyncio async def test_PilotBuilder_scene_empty(correct_bulb: wizlight) -> None: """Test scene with no scene set.""" state = await correct_bulb.updateState() assert state is not None if "sceneId" in state.pilotResult: del state.pilotResult["sceneId"] assert state and state.get_scene() is None @pytest.mark.asyncio async def test_PilotBuilder_speed(correct_bulb: wizlight) -> None: """Test speed.""" await correct_bulb.turn_on(PilotBuilder(scene=1, speed=50)) state = await correct_bulb.updateState() assert state and state.get_scene() == SCENES[1] assert state and state.get_speed() == 50 @pytest.mark.asyncio async def test_set_speed(correct_bulb: wizlight) -> None: """Set speed.""" await correct_bulb.set_speed(125) state = await correct_bulb.updateState() assert state and state.get_speed() == 125 @pytest.mark.asyncio async def test_get_source(correct_bulb: wizlight) -> None: """Test getting the source.""" state = await correct_bulb.updateState() assert state and state.get_source() == "udp" # ------ Error states ------------------------------------- @pytest.mark.asyncio async def test_error_PilotBuilder_brightness(correct_bulb: wizlight) -> None: """Error Brightness.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(brightness=500)) @pytest.mark.asyncio async def test_error_PilotBuilder_warm_wite(correct_bulb: wizlight) -> None: """Error Warm White.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(warm_white=300)) with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(warm_white=-1)) @pytest.mark.asyncio async def test_error_PilotBuilder_cold_white_upper(correct_bulb: wizlight) -> None: """Error Cold White.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(cold_white=300)) @pytest.mark.asyncio async def test_error_PilotBuilder_cold_white_lower(correct_bulb: wizlight) -> None: """Error Cold White.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(cold_white=-1)) @pytest.mark.asyncio async def test_error_PilotBuilder_r(correct_bulb: wizlight) -> None: """Error Red Value.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(rgb=(300, 0, 0))) @pytest.mark.asyncio async def test_error_PilotBuilder_green(correct_bulb: wizlight) -> None: """Error Green Value.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(rgb=(0, 300, 0))) @pytest.mark.asyncio async def test_error_PilotBuilder_blue(correct_bulb: wizlight) -> None: """Error Blue Value.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(rgb=(0, 0, 300))) @pytest.mark.asyncio async def test_error_PilotBuilder_cold_white(correct_bulb: wizlight) -> None: """Error Cold White Value.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(cold_white=9999)) @pytest.mark.asyncio async def test_error_PilotBuilder_scene(correct_bulb: wizlight) -> None: """Error scene.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(scene=532)) @pytest.mark.asyncio async def test_error_PilotBuilder_speed(correct_bulb: wizlight) -> None: """Error speed.""" with pytest.raises(ValueError): await correct_bulb.turn_on(PilotBuilder(speed=532)) @pytest.mark.asyncio async def test_error_set_speed(correct_bulb: wizlight) -> None: """Error speed.""" with pytest.raises(ValueError): await correct_bulb.set_speed(532) @pytest.mark.asyncio async def test_fw_version(correct_bulb: wizlight) -> None: """Test fetching the firmware version.""" bulb_type = await correct_bulb.get_bulbtype() assert bulb_type == BulbType( features=Features( color=True, color_tmp=True, effect=True, brightness=True, dual_head=False ), name="ESP01_SHRGB_03", kelvin_range=KelvinRange(max=6500, min=2200), bulb_type=BulbClass.RGB, fw_version="1.25.0", white_channels=1, white_to_color_ratio=30, ) assert correct_bulb.mac == "a8bb5006033d" @pytest.mark.asyncio async def test_get_mac(correct_bulb: wizlight) -> None: """Test getting the mac address.""" mac = await correct_bulb.getMac() assert mac == "a8bb5006033d" mac = await correct_bulb.getMac() assert mac == "a8bb5006033d" @pytest.mark.asyncio async def test_get_power(power_socket: wizlight) -> None: """Test getting power in watts.""" watts = await power_socket.get_power() assert watts == 1065.385 @pytest.mark.asyncio async def test_get_power_old_firmware(power_socket_old_firmware: wizlight) -> None: """Test getting power in watts.""" watts = await power_socket_old_firmware.get_power() assert watts is None @pytest.mark.asyncio async def test_get_power_unsupported_device(correct_bulb: wizlight) -> None: """Test getting power in watts.""" assert await correct_bulb.get_power() is None assert await correct_bulb.get_power() is None # Error states / Timout @pytest.mark.asyncio async def test_timeout(bad_bulb: wizlight) -> None: """Test the timeout exception after.""" with pytest.raises(WizLightConnectionError), patch( "pywizlight.bulb.FIRST_SEND_INTERVAL", 0.01 ), patch("pywizlight.bulb.TIMEOUT", 0.01): await bad_bulb.getBulbConfig() @pytest.mark.asyncio async def test_timeout_PilotBuilder(bad_bulb: wizlight) -> None: """Test Timeout for Result.""" # check if the bulb state it given as bool - mock ? with pytest.raises(WizLightConnectionError), patch( "pywizlight.bulb.FIRST_SEND_INTERVAL", 0.01 ), patch("pywizlight.bulb.TIMEOUT", 0.01): await bad_bulb.turn_on(PilotBuilder(brightness=255)) @pytest.mark.asyncio async def test_states_match_with_occupancy() -> None: """Test states match always sends pir updates but we ignore mqttCd, rssi, and ts.""" state_off_ios = { "mac": "a8bb50d46a1c", "rssi": -70, "src": "ios", "mqttCd": 0, "ts": 1644440635, "state": False, "sceneId": 0, } state_on_ios = { "mac": "a8bb50d46a1c", "rssi": -45, "src": "ios", "mqttCd": 0, "ts": 1644440662, "state": False, "sceneId": 27, "speed": 100, "dimming": 100, } state_on_hb = { "mac": "a8bb50d46a1c", "rssi": -45, "src": "hb", "mqttCd": 0, "ts": 1644440642, "state": False, "sceneId": 27, "speed": 100, "dimming": 100, } ios_scene27 = { "mac": "a8bb50d46a1c", "rssi": -48, "src": "ios", "state": True, "sceneId": 27, "speed": 100, "dimming": 100, } ios_off = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "ios", "state": False, "sceneId": 0, } occupancy_detected_scene27 = { "mac": "a8bb50d46a1c", "rssi": -48, "src": "pir", "state": True, "sceneId": 27, "speed": 100, "dimming": 100, } occupancy_not_detected = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "pir", "state": False, "sceneId": 0, } assert states_match(state_off_ios, state_off_ios) assert not states_match(state_off_ios, state_on_ios) assert states_match( state_on_ios, state_on_hb ) # source change does not matter unless its a PIR assert not states_match( ios_scene27, occupancy_detected_scene27 ) # source change matters since its a PIR assert states_match(occupancy_detected_scene27, occupancy_detected_scene27) assert not states_match( ios_off, occupancy_not_detected ) # source change matters since its a PIR @pytest.mark.asyncio async def test_states_match_with_button() -> None: """Test states match always sends button updates but we ignore mqttCd, rssi, and ts.""" button_on_press = { "mac": "a8bb50d46a1c", "rssi": -48, "src": "wfa1", "state": True, } button_off_press = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "wfa2", "state": False, "sceneId": 0, } button_1_press_state_false = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "wfa16", "state": False, "sceneId": 0, } button_1_press_state_true = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "wfa16", "state": True, "sceneId": 0, } ios_off = { "mac": "a8bb50d46a1c", "rssi": -69, "src": "ios", "state": False, "sceneId": 0, } assert not states_match(button_on_press, button_off_press) assert not states_match(button_1_press_state_false, button_1_press_state_true) assert not states_match( ios_off, button_1_press_state_false ) # source change matters since its a button