From 445ad1d592588fd91442ef45fe196ec3272dec44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Feb 2022 09:31:26 -0600 Subject: [PATCH] Add test coverage for WiZ lights and switches (#66387) --- .coveragerc | 6 - homeassistant/components/wiz/discovery.py | 12 +- homeassistant/components/wiz/light.py | 16 +- homeassistant/components/wiz/manifest.json | 1 + tests/components/wiz/__init__.py | 4 +- tests/components/wiz/test_config_flow.py | 21 +++ tests/components/wiz/test_init.py | 32 ++++ tests/components/wiz/test_light.py | 173 +++++++++++++++++++-- tests/components/wiz/test_switch.py | 66 ++++++++ 9 files changed, 288 insertions(+), 43 deletions(-) create mode 100644 tests/components/wiz/test_init.py create mode 100644 tests/components/wiz/test_switch.py diff --git a/.coveragerc b/.coveragerc index de48567eb6c..e85278586dd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1398,12 +1398,6 @@ omit = homeassistant/components/wiffi/sensor.py homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* - homeassistant/components/wiz/__init__.py - homeassistant/components/wiz/const.py - homeassistant/components/wiz/discovery.py - homeassistant/components/wiz/entity.py - homeassistant/components/wiz/light.py - homeassistant/components/wiz/switch.py homeassistant/components/wolflink/__init__.py homeassistant/components/wolflink/sensor.py homeassistant/components/wolflink/const.py diff --git a/homeassistant/components/wiz/discovery.py b/homeassistant/components/wiz/discovery.py index c7ee612c6a9..0b7015643ff 100644 --- a/homeassistant/components/wiz/discovery.py +++ b/homeassistant/components/wiz/discovery.py @@ -17,17 +17,11 @@ _LOGGER = logging.getLogger(__name__) async def async_discover_devices( - hass: HomeAssistant, timeout: int, address: str | None = None + hass: HomeAssistant, timeout: int ) -> list[DiscoveredBulb]: """Discover wiz devices.""" - if address: - targets = [address] - else: - targets = [ - str(address) - for address in await network.async_get_ipv4_broadcast_addresses(hass) - ] - + broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) + targets = [str(address) for address in broadcast_addrs] combined_discoveries: dict[str, DiscoveredBulb] = {} for idx, discovered in enumerate( await asyncio.gather( diff --git a/homeassistant/components/wiz/light.py b/homeassistant/components/wiz/light.py index ef60deea956..9b2d7e6fab4 100644 --- a/homeassistant/components/wiz/light.py +++ b/homeassistant/components/wiz/light.py @@ -32,6 +32,8 @@ from .const import DOMAIN from .entity import WizToggleEntity from .models import WizData +RGB_WHITE_CHANNELS_COLOR_MODE = {1: COLOR_MODE_RGBW, 2: COLOR_MODE_RGBWW} + def _async_pilot_builder(**kwargs: Any) -> PilotBuilder: """Create the PilotBuilder for turn on.""" @@ -79,10 +81,7 @@ class WizBulbEntity(WizToggleEntity, LightEntity): features: Features = bulb_type.features color_modes = set() if features.color: - if bulb_type.white_channels == 2: - color_modes.add(COLOR_MODE_RGBWW) - else: - color_modes.add(COLOR_MODE_RGBW) + color_modes.add(RGB_WHITE_CHANNELS_COLOR_MODE[bulb_type.white_channels]) if features.color_tmp: color_modes.add(COLOR_MODE_COLOR_TEMP) if not color_modes and features.brightness: @@ -90,12 +89,9 @@ class WizBulbEntity(WizToggleEntity, LightEntity): self._attr_supported_color_modes = color_modes self._attr_effect_list = wiz_data.scenes if bulb_type.bulb_type != BulbClass.DW: - self._attr_min_mireds = color_temperature_kelvin_to_mired( - bulb_type.kelvin_range.max - ) - self._attr_max_mireds = color_temperature_kelvin_to_mired( - bulb_type.kelvin_range.min - ) + kelvin = bulb_type.kelvin_range + self._attr_min_mireds = color_temperature_kelvin_to_mired(kelvin.max) + self._attr_max_mireds = color_temperature_kelvin_to_mired(kelvin.min) if bulb_type.features.effect: self._attr_supported_features = SUPPORT_EFFECT self._async_update_attrs() diff --git a/homeassistant/components/wiz/manifest.json b/homeassistant/components/wiz/manifest.json index 3aa137f2460..e333691d20c 100644 --- a/homeassistant/components/wiz/manifest.json +++ b/homeassistant/components/wiz/manifest.json @@ -7,6 +7,7 @@ {"hostname":"wiz_*"} ], "dependencies": ["network"], + "quality_scale": "platinum", "documentation": "https://www.home-assistant.io/integrations/wiz", "requirements": ["pywizlight==0.5.8"], "iot_class": "local_push", diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 931dd5ec18c..e553593bf2f 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -204,7 +204,7 @@ def _patch_discovery(): async def async_setup_integration( - hass, device=None, extended_white_range=None, bulb_type=None + hass, wizlight=None, device=None, extended_white_range=None, bulb_type=None ): """Set up the integration with a mock device.""" entry = MockConfigEntry( @@ -213,7 +213,7 @@ async def async_setup_integration( data={CONF_HOST: FAKE_IP}, ) entry.add_to_hass(hass) - bulb = _mocked_wizlight(device, extended_white_range, bulb_type) + bulb = wizlight or _mocked_wizlight(device, extended_white_range, bulb_type) with _patch_discovery(), _patch_wizlight(device=bulb): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index f87f1b75437..f8426ece56d 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -410,6 +410,27 @@ async def test_setup_via_discovery_cannot_connect(hass): assert result3["reason"] == "cannot_connect" +async def test_setup_via_discovery_exception_finds_nothing(hass): + """Test we do not find anything if discovery throws.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "user" + assert not result["errors"] + + with patch( + "homeassistant.components.wiz.discovery.find_wizlights", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "no_devices_found" + + async def test_discovery_with_firmware_update(hass): """Test we check the device again between first discovery and config entry creation.""" with _patch_wizlight( diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py new file mode 100644 index 00000000000..6411146d162 --- /dev/null +++ b/tests/components/wiz/test_init.py @@ -0,0 +1,32 @@ +"""Tests for wiz integration.""" +import datetime +from unittest.mock import AsyncMock + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from . import ( + FAKE_MAC, + FAKE_SOCKET, + _mocked_wizlight, + _patch_discovery, + _patch_wizlight, + async_setup_integration, +) + +from tests.common import async_fire_time_changed + + +async def test_setup_retry(hass: HomeAssistant) -> None: + """Test setup is retried on error.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.getMac = AsyncMock(side_effect=OSError) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.state == config_entries.ConfigEntryState.SETUP_RETRY + bulb.getMac = AsyncMock(return_value=FAKE_MAC) + + with _patch_discovery(), _patch_wizlight(device=bulb): + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=15)) + await hass.async_block_till_done() + assert entry.state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/wiz/test_light.py b/tests/components/wiz/test_light.py index 16d7c6a0a5d..c79cf74e130 100644 --- a/tests/components/wiz/test_light.py +++ b/tests/components/wiz/test_light.py @@ -1,30 +1,171 @@ """Tests for light platform.""" -from homeassistant.components import wiz -from homeassistant.const import CONF_HOST, STATE_ON +from pywizlight import PilotBuilder + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.setup import async_setup_component -from . import FAKE_IP, FAKE_MAC, _patch_discovery, _patch_wizlight - -from tests.common import MockConfigEntry +from . import ( + FAKE_MAC, + FAKE_RGBW_BULB, + FAKE_RGBWW_BULB, + FAKE_TURNABLE_BULB, + async_push_update, + async_setup_integration, +) async def test_light_unique_id(hass: HomeAssistant) -> None: """Test a light unique id.""" - entry = MockConfigEntry( - domain=wiz.DOMAIN, - unique_id=FAKE_MAC, - data={CONF_HOST: FAKE_IP}, - ) - entry.add_to_hass(hass) - with _patch_discovery(), _patch_wizlight(): - await async_setup_component(hass, wiz.DOMAIN, {wiz.DOMAIN: {}}) - await hass.async_block_till_done() - + await async_setup_integration(hass) entity_id = "light.mock_title" entity_registry = er.async_get(hass) assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC state = hass.states.get(entity_id) assert state.state == STATE_ON + + +async def test_light_operation(hass: HomeAssistant) -> None: + """Test a light operation.""" + bulb, _ = await async_setup_integration(hass) + entity_id = "light.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_off.assert_called_once() + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "state": False}) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + bulb.turn_on.assert_called_once() + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, "state": True}) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_rgbww_light(hass: HomeAssistant) -> None: + """Test a light operation with a rgbww light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_RGBWW_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (1, 2, 3, 4, 5)}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"b": 3, "c": 4, "g": 2, "r": 1, "state": True, "w": 5} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_RGBWW_COLOR] == (1, 2, 3, 4, 5) + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_TEMP] == 153 + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Ocean"}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"sceneId": 1, "state": True} + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_EFFECT] == "Ocean" + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "Rhythm"}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"state": True} + + +async def test_rgbw_light(hass: HomeAssistant) -> None: + """Test a light operation with a rgbww light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_RGBW_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (1, 2, 3, 4)}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"b": 3, "g": 2, "r": 1, "state": True, "w": 4} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_RGBW_COLOR] == (1, 2, 3, 4) + + bulb.turn_on.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + + +async def test_turnable_light(hass: HomeAssistant) -> None: + """Test a light operation with a turnable light.""" + bulb, _ = await async_setup_integration(hass, bulb_type=FAKE_TURNABLE_BULB) + entity_id = "light.mock_title" + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + pilot: PilotBuilder = bulb.turn_on.mock_calls[0][1][0] + assert pilot.pilot_params == {"dimming": 50, "temp": 6535, "state": True} + + await async_push_update(hass, bulb, {"mac": FAKE_MAC, **pilot.pilot_params}) + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_TEMP] == 153 diff --git a/tests/components/wiz/test_switch.py b/tests/components/wiz/test_switch.py new file mode 100644 index 00000000000..e728ff4a645 --- /dev/null +++ b/tests/components/wiz/test_switch.py @@ -0,0 +1,66 @@ +"""Tests for switch platform.""" + +import datetime + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import FAKE_MAC, FAKE_SOCKET, async_push_update, async_setup_integration + +from tests.common import async_fire_time_changed + + +async def test_switch_operation(hass: HomeAssistant) -> None: + """Test switch operation.""" + switch, _ = await async_setup_integration(hass, bulb_type=FAKE_SOCKET) + entity_id = "switch.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_off.assert_called_once() + + await async_push_update(hass, switch, {"mac": FAKE_MAC, "state": False}) + assert hass.states.get(entity_id).state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_on.assert_called_once() + + await async_push_update(hass, switch, {"mac": FAKE_MAC, "state": True}) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_update_fails(hass: HomeAssistant) -> None: + """Test switch update fails when push updates are not working.""" + switch, _ = await async_setup_integration(hass, bulb_type=FAKE_SOCKET) + entity_id = "switch.mock_title" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == FAKE_MAC + assert hass.states.get(entity_id).state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + switch.turn_off.assert_called_once() + + switch.updateState.side_effect = OSError + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(seconds=15)) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE