From 6843d5e59bcf48e9cb89682efbba69df1c556ff2 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 19 Jan 2025 00:38:42 +0000 Subject: [PATCH 1/6] update state after setting --- homeassistant/components/vesync/fan.py | 1 + homeassistant/components/vesync/switch.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index ba1880f2492950..80c29ec90f7fcb 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -236,3 +236,4 @@ def turn_on( def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() + self.schedule_update_ha_state() diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index ef8e6c6051f2a5..3554235deb915e 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -64,6 +64,7 @@ class VeSyncBaseSwitch(VeSyncBaseEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self.device.turn_on() + self.schedule_update_ha_state() @property def is_on(self) -> bool: @@ -73,6 +74,7 @@ def is_on(self) -> bool: def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() + self.schedule_update_ha_state() class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): From 245c8d9c5128fa50f68142d288b0c8ee2c09eff8 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 19 Jan 2025 16:06:15 +0000 Subject: [PATCH 2/6] Same for light.py --- homeassistant/components/vesync/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 40f68986145070..83fa220f4da38d 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -138,10 +138,12 @@ def turn_on(self, **kwargs: Any) -> None: return # send turn_on command to pyvesync api self.device.turn_on() + self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() + self.schedule_update_ha_state() class VeSyncDimmableLightHA(VeSyncBaseLightHA, LightEntity): From 809f1239b09a1dfff1fad9326245a59b37390eb9 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Mon, 3 Feb 2025 19:00:20 +0000 Subject: [PATCH 3/6] Add if --- homeassistant/components/vesync/fan.py | 10 +++++----- homeassistant/components/vesync/light.py | 8 ++++---- homeassistant/components/vesync/switch.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 80c29ec90f7fcb..72b880309515ab 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -188,14 +188,14 @@ def set_percentage(self, percentage: int) -> None: self.smartfan.turn_on() self.smartfan.manual_mode() - self.smartfan.change_fan_speed( + if self.smartfan.change_fan_speed( math.ceil( percentage_to_ranged_value( SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage ) ) - ) - self.schedule_update_ha_state() + ): + self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of device.""" @@ -235,5 +235,5 @@ def turn_on( def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self.device.turn_off() - self.schedule_update_ha_state() + if self.device.turn_off(): + self.schedule_update_ha_state() diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 83fa220f4da38d..1ca4ab3a5c7c16 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -137,13 +137,13 @@ def turn_on(self, **kwargs: Any) -> None: if attribute_adjustment_only: return # send turn_on command to pyvesync api - self.device.turn_on() - self.schedule_update_ha_state() + if self.device.turn_on(): + self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self.device.turn_off() - self.schedule_update_ha_state() + if self.device.turn_off(): + self.schedule_update_ha_state() class VeSyncDimmableLightHA(VeSyncBaseLightHA, LightEntity): diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 3554235deb915e..0d2f7c398716f2 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -63,8 +63,8 @@ class VeSyncBaseSwitch(VeSyncBaseEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self.device.turn_on() - self.schedule_update_ha_state() + if self.device.turn_on(): + self.schedule_update_ha_state() @property def is_on(self) -> bool: @@ -73,8 +73,8 @@ def is_on(self) -> bool: def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self.device.turn_off() - self.schedule_update_ha_state() + if self.device.turn_off(): + self.schedule_update_ha_state() class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): From e3088af192a687a58cffce8e6e36321cb32c7087 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 23 Feb 2025 22:14:37 +0000 Subject: [PATCH 4/6] Remove Fan, add test, add exceptions --- homeassistant/components/vesync/fan.py | 8 +-- homeassistant/components/vesync/light.py | 11 ++-- tests/components/vesync/common.py | 2 + tests/components/vesync/conftest.py | 20 ++++++ tests/components/vesync/test_light.py | 81 +++++++++++++++++++++++- 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 8cb97f55b89ea0..139d5eccba0ffd 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -193,14 +193,13 @@ def set_percentage(self, percentage: int) -> None: self.smartfan.turn_on() self.smartfan.manual_mode() - if self.smartfan.change_fan_speed( + self.smartfan.change_fan_speed( math.ceil( percentage_to_ranged_value( SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage ) ) - ): - self.schedule_update_ha_state() + ) def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of device.""" @@ -244,5 +243,4 @@ def turn_on( def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - if self.device.turn_off(): - self.schedule_update_ha_state() + self.device.turn_off() diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 1ca4ab3a5c7c16..334969722df72c 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -13,6 +13,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import color as color_util @@ -137,13 +138,15 @@ def turn_on(self, **kwargs: Any) -> None: if attribute_adjustment_only: return # send turn_on command to pyvesync api - if self.device.turn_on(): - self.schedule_update_ha_state() + if not self.device.turn_on(): + raise HomeAssistantError("An error occurred while turning on.") + self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - if self.device.turn_off(): - self.schedule_update_ha_state() + if not self.device.turn_off(): + raise HomeAssistantError("An error occurred while turning off.") + self.schedule_update_ha_state() class VeSyncDimmableLightHA(VeSyncBaseLightHA, LightEntity): diff --git a/tests/components/vesync/common.py b/tests/components/vesync/common.py index ee9f9b94052ab5..c4ed9d25087c2c 100644 --- a/tests/components/vesync/common.py +++ b/tests/components/vesync/common.py @@ -14,6 +14,8 @@ ENTITY_HUMIDIFIER_MIST_LEVEL = "number.humidifier_200s_mist_level" ENTITY_HUMIDIFIER_HUMIDITY = "sensor.humidifier_200s_humidity" +ENTITY_LIGHT = "light.dimmer_switch" + ALL_DEVICES = load_json_object_fixture("vesync-devices.json", DOMAIN) ALL_DEVICE_NAMES: list[str] = [ dev["deviceName"] for dev in ALL_DEVICES["result"]["list"] diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 9ec7bd23fa59e2..2b0f9afbd2616b 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -155,6 +155,26 @@ async def humidifier_config_entry( return entry +@pytest.fixture(name="light_config_entry") +async def light_config_entry( + hass: HomeAssistant, requests_mock: requests_mock.Mocker, config +) -> MockConfigEntry: + """Create a mock VeSync config entry for `Dimmable Light`.""" + entry = MockConfigEntry( + title="VeSync", + domain=DOMAIN, + data=config[DOMAIN], + ) + entry.add_to_hass(hass) + + device_name = "Dimmer Switch" + mock_multiple_device_responses(requests_mock, [device_name]) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + @pytest.fixture(name="switch_old_id_config_entry") async def switch_old_id_config_entry( hass: HomeAssistant, requests_mock: requests_mock.Mocker, config diff --git a/tests/components/vesync/test_light.py b/tests/components/vesync/test_light.py index 866e6b295bf076..31cada42d7bcaa 100644 --- a/tests/components/vesync/test_light.py +++ b/tests/components/vesync/test_light.py @@ -1,17 +1,24 @@ """Tests for the light module.""" +from contextlib import nullcontext +from unittest.mock import patch + import pytest import requests_mock from syrupy import SnapshotAssertion from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from .common import ALL_DEVICE_NAMES, mock_devices_response +from .common import ALL_DEVICE_NAMES, ENTITY_LIGHT, mock_devices_response from tests.common import MockConfigEntry +NoException = nullcontext() + @pytest.mark.parametrize("device_name", ALL_DEVICE_NAMES) async def test_light_state( @@ -49,3 +56,75 @@ async def test_light_state( # Check states for entity in entities: assert hass.states.get(entity.entity_id) == snapshot(name=entity.entity_id) + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(False, pytest.raises(HomeAssistantError)), (True, NoException)], +) +async def test_turn_on( + hass: HomeAssistant, + light_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test turn_on method.""" + + # turn_on returns False indicating failure in which case light.turn_on + # raises HomeAssistantError. + with ( + expectation, + patch( + "pyvesync.vesyncswitch.VeSyncDimmerSwitch.turn_on", + return_value=api_response, + ) as method_mock, + ): + with patch( + "homeassistant.components.vesync.light.VeSyncBaseLightHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() + + +@pytest.mark.parametrize( + ("api_response", "expectation"), + [(False, pytest.raises(HomeAssistantError)), (True, NoException)], +) +async def test_turn_off( + hass: HomeAssistant, + light_config_entry: MockConfigEntry, + api_response: bool, + expectation, +) -> None: + """Test turn_off method.""" + + # turn_off returns False indicating failure in which case light.turn_off + # raises HomeAssistantError. + with ( + expectation, + patch( + "pyvesync.vesyncswitch.VeSyncDimmerSwitch.turn_off", + return_value=api_response, + ) as method_mock, + ): + with patch( + "homeassistant.components.vesync.light.VeSyncBaseLightHA.schedule_update_ha_state" + ) as update_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT}, + blocking=True, + ) + + await hass.async_block_till_done() + method_mock.assert_called_once() + update_mock.assert_called_once() From 1e83ec6870f765e8322838006c2043768b1f1204 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 23 Feb 2025 22:18:48 +0000 Subject: [PATCH 5/6] Revert change --- homeassistant/components/vesync/fan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 15c1ab5ea9a1c8..daf734d50a8e82 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -200,6 +200,7 @@ def set_percentage(self, percentage: int) -> None: ) ) ) + self.schedule_update_ha_state() def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of device.""" From 0203e0e2446219b983414c5d437edd172eaef303 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Sun, 23 Feb 2025 22:25:05 +0000 Subject: [PATCH 6/6] ruff --- tests/components/vesync/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/vesync/conftest.py b/tests/components/vesync/conftest.py index 3cd122932d5226..434375ff08831b 100644 --- a/tests/components/vesync/conftest.py +++ b/tests/components/vesync/conftest.py @@ -202,6 +202,7 @@ async def light_config_entry( return entry + @pytest.fixture async def install_humidifier_device( hass: HomeAssistant,