From f7bc7d39115ed8cb223e706fee3bc9f3fd0a1481 Mon Sep 17 00:00:00 2001 From: cdnninja Date: Tue, 27 Jan 2026 08:18:37 -0700 Subject: [PATCH] Bump pyvesync to 3.4.1 (#160573) Co-authored-by: Joe Trabulsy --- homeassistant/components/vesync/const.py | 2 - homeassistant/components/vesync/fan.py | 101 +++++++++++------- homeassistant/components/vesync/humidifier.py | 46 +++++--- homeassistant/components/vesync/light.py | 61 +++++------ homeassistant/components/vesync/manifest.json | 2 +- homeassistant/components/vesync/number.py | 20 +++- homeassistant/components/vesync/select.py | 31 +++++- homeassistant/components/vesync/strings.json | 1 - homeassistant/components/vesync/switch.py | 68 +++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../vesync/snapshots/test_diagnostics.ambr | 3 + .../components/vesync/snapshots/test_fan.ambr | 6 +- tests/components/vesync/test_fan.py | 4 +- 14 files changed, 232 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index e392a01f293..c4a77514022 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -32,7 +32,6 @@ VS_HUMIDIFIER_MODE_SLEEP = "sleep" VS_FAN_MODE_AUTO = "auto" VS_FAN_MODE_SLEEP = "sleep" -VS_FAN_MODE_ADVANCED_SLEEP = "advancedSleep" VS_FAN_MODE_TURBO = "turbo" VS_FAN_MODE_PET = "pet" VS_FAN_MODE_MANUAL = "manual" @@ -42,7 +41,6 @@ VS_FAN_MODE_NORMAL = "normal" VS_FAN_MODE_PRESET_LIST_HA = [ VS_FAN_MODE_AUTO, VS_FAN_MODE_SLEEP, - VS_FAN_MODE_ADVANCED_SLEEP, VS_FAN_MODE_TURBO, VS_FAN_MODE_PET, VS_FAN_MODE_NORMAL, diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 75bfb203bda..062ef5a21d8 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -5,8 +5,7 @@ from __future__ import annotations import logging from typing import Any -from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice -from pyvesync.device_container import DeviceContainer +from pyvesync.base_devices import VeSyncFanBase, VeSyncPurifier from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant, callback @@ -22,7 +21,6 @@ from .common import is_fan, is_purifier, rgetattr from .const import ( VS_DEVICES, VS_DISCOVERY, - VS_FAN_MODE_ADVANCED_SLEEP, VS_FAN_MODE_AUTO, VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL, @@ -40,7 +38,6 @@ _LOGGER = logging.getLogger(__name__) VS_TO_HA_MODE_MAP = { VS_FAN_MODE_AUTO: VS_FAN_MODE_AUTO, VS_FAN_MODE_SLEEP: VS_FAN_MODE_SLEEP, - VS_FAN_MODE_ADVANCED_SLEEP: "advanced_sleep", VS_FAN_MODE_TURBO: VS_FAN_MODE_TURBO, VS_FAN_MODE_PET: VS_FAN_MODE_PET, VS_FAN_MODE_MANUAL: VS_FAN_MODE_MANUAL, @@ -60,26 +57,33 @@ async def async_setup_entry( coordinator = config_entry.runtime_data @callback - def discover(devices: list[VeSyncBaseDevice]) -> None: + def discover(devices: list[VeSyncFanBase | VeSyncPurifier]) -> None: """Add new devices to platform.""" - _setup_entities(devices, async_add_entities, coordinator) + _setup_entities( + [dev for dev in devices if is_fan(dev) or is_purifier(dev)], + async_add_entities, + coordinator, + ) config_entry.async_on_unload( async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_DEVICES), discover) ) _setup_entities( - config_entry.runtime_data.manager.devices, async_add_entities, coordinator + config_entry.runtime_data.manager.devices.air_purifiers + + config_entry.runtime_data.manager.devices.fans, + async_add_entities, + coordinator, ) @callback def _setup_entities( - devices: DeviceContainer | list[VeSyncBaseDevice], + devices: list[VeSyncFanBase | VeSyncPurifier], async_add_entities: AddConfigEntryEntitiesCallback, coordinator: VeSyncDataCoordinator, ) -> None: - """Check if device is fan and add entity.""" + """Check if device is fan or purifier and add entity.""" async_add_entities( VeSyncFanHA(dev, coordinator) @@ -95,7 +99,7 @@ def _get_ha_mode(vs_mode: str) -> str | None: return ha_mode -class VeSyncFanHA(VeSyncBaseEntity, FanEntity): +class VeSyncFanHA(VeSyncBaseEntity[VeSyncFanBase | VeSyncPurifier], FanEntity): """Representation of a VeSync fan.""" _attr_supported_features = ( @@ -109,7 +113,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): def __init__( self, - device: VeSyncBaseDevice, + device: VeSyncFanBase | VeSyncPurifier, coordinator: VeSyncDataCoordinator, ) -> None: """Initialize the fan.""" @@ -179,7 +183,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the fan.""" - attr = {} + attr: dict[str, Any] = {} if hasattr(self.device.state, "active_time"): attr["active_time"] = self.device.state.active_time @@ -219,37 +223,47 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): if percentage == 0: # Turning off is a special case: do not set speed or mode if not await self.device.turn_off(): - raise HomeAssistantError( - "An error occurred while turning off: " - + self.device.last_response.message - ) + if self.device.last_response: + raise HomeAssistantError( + "An error occurred while turning off: " + + self.device.last_response.message + ) + raise HomeAssistantError("Failed to turn off fan, no response found.") self.async_write_ha_state() return # If the fan is off, turn it on first if not self.device.is_on: if not await self.device.turn_on(): - raise HomeAssistantError( - "An error occurred while turning on: " - + self.device.last_response.message - ) + if self.device.last_response: + raise HomeAssistantError( + "An error occurred while turning on: " + + self.device.last_response.message + ) + raise HomeAssistantError("Failed to turn on fan, no response found.") # Switch to manual mode if not already set if self.device.state.mode not in (VS_FAN_MODE_MANUAL, VS_FAN_MODE_NORMAL): if not await self.device.set_manual_mode(): + if self.device.last_response: + raise HomeAssistantError( + "An error occurred while setting manual mode." + + self.device.last_response.message + ) raise HomeAssistantError( - "An error occurred while setting manual mode." - + self.device.last_response.message + "Failed to set manual mode, no response found." ) # Calculate the speed level and set it if not await self.device.set_fan_speed( percentage_to_ordered_list_item(self.device.fan_levels, percentage) ): - raise HomeAssistantError( - "An error occurred while changing fan speed: " - + self.device.last_response.message - ) + if self.device.last_response: + raise HomeAssistantError( + "An error occurred while changing fan speed: " + + self.device.last_response.message + ) + raise HomeAssistantError("Failed to set fan speed, no response found.") self.async_write_ha_state() @@ -270,17 +284,19 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): success = await self.device.set_auto_mode() elif vs_mode == VS_FAN_MODE_SLEEP: success = await self.device.set_sleep_mode() - elif vs_mode == VS_FAN_MODE_ADVANCED_SLEEP: - success = await self.device.set_advanced_sleep_mode() elif vs_mode == VS_FAN_MODE_PET: - success = await self.device.set_pet_mode() + if hasattr(self.device, "set_pet_mode"): + success = await self.device.set_pet_mode() elif vs_mode == VS_FAN_MODE_TURBO: success = await self.device.set_turbo_mode() elif vs_mode == VS_FAN_MODE_NORMAL: - success = await self.device.set_normal_mode() + if hasattr(self.device, "set_normal_mode"): + success = await self.device.set_normal_mode() if not success: - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to set preset mode, no response found.") self.async_write_ha_state() @@ -297,7 +313,9 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): if percentage is None: success = await self.device.turn_on() if not success: - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to turn on fan, no response found.") self.async_write_ha_state() else: await self.async_set_percentage(percentage) @@ -306,12 +324,21 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity): """Turn the device off.""" success = await self.device.turn_off() if not success: - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to turn off fan, no response found.") self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation.""" - success = await self.device.toggle_oscillation(oscillating) - if not success: - raise HomeAssistantError(self.device.last_response.message) - self.async_write_ha_state() + if hasattr(self.device, "toggle_oscillation"): + success = await self.device.toggle_oscillation(oscillating) + if not success: + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError( + "Failed to set oscillation, no response found." + ) + self.async_write_ha_state() + else: + raise HomeAssistantError("Oscillation not supported by this device.") diff --git a/homeassistant/components/vesync/humidifier.py b/homeassistant/components/vesync/humidifier.py index 372b55cf029..dafaefc2c9c 100644 --- a/homeassistant/components/vesync/humidifier.py +++ b/homeassistant/components/vesync/humidifier.py @@ -3,7 +3,7 @@ import logging from typing import TYPE_CHECKING, Any -from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice +from pyvesync.base_devices.humidifier_base import VeSyncHumidifier from homeassistant.components.humidifier import ( MODE_AUTO, @@ -17,6 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from .common import is_humidifier from .const import ( VS_DEVICES, VS_DISCOVERY, @@ -51,16 +52,24 @@ async def async_setup_entry( coordinator = config_entry.runtime_data @callback - def discover(devices: list[VeSyncBaseDevice]) -> None: + def discover(devices: list[VeSyncHumidifier]) -> None: """Add new devices to platform.""" - _setup_entities(devices, async_add_entities, coordinator) + _setup_entities( + [dev for dev in devices if is_humidifier(dev)], + async_add_entities, + coordinator, + ) config_entry.async_on_unload( async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_DEVICES), discover) ) _setup_entities( - config_entry.runtime_data.manager.devices.humidifiers, + [ + dev + for dev in config_entry.runtime_data.manager.devices.humidifiers + if is_humidifier(dev) + ], async_add_entities, coordinator, ) @@ -68,7 +77,7 @@ async def async_setup_entry( @callback def _setup_entities( - devices: list[VeSyncBaseDevice], + devices: list[VeSyncHumidifier], async_add_entities: AddConfigEntryEntitiesCallback, coordinator: VeSyncDataCoordinator, ) -> None: @@ -83,7 +92,7 @@ def _get_ha_mode(vs_mode: str) -> str | None: return ha_mode -class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): +class VeSyncHumidifierHA(VeSyncBaseEntity[VeSyncHumidifier], HumidifierEntity): """Representation of a VeSync humidifier.""" # The base VeSyncBaseEntity has _attr_has_entity_name and this is to follow the device name @@ -95,7 +104,7 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): def __init__( self, - device: VeSyncBaseDevice, + device: VeSyncHumidifier, coordinator: VeSyncDataCoordinator, ) -> None: """Initialize the VeSyncHumidifierHA device.""" @@ -119,8 +128,8 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): self._available_modes.sort() - def _get_vs_mode(self, ha_mode: str) -> str | None: - return self._ha_to_vs_mode_map.get(ha_mode) + def _get_vs_mode(self, ha_mode: str) -> str: + return self._ha_to_vs_mode_map[ha_mode] @property def available_modes(self) -> list[str]: @@ -153,7 +162,9 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): async def async_set_humidity(self, humidity: int) -> None: """Set the target humidity of the device.""" if not await self.device.set_humidity(humidity): - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to set humidity.") async def async_set_mode(self, mode: str) -> None: """Set the mode of the device.""" @@ -161,8 +172,13 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): raise HomeAssistantError( f"Invalid mode {mode}. Available modes: {self.available_modes}" ) + set_mode = self._get_vs_mode(mode) + if set_mode is None: + raise HomeAssistantError(f"Could not map mode {mode} to VeSync mode.") if not await self.device.set_mode(self._get_vs_mode(mode)): - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to set mode.") if mode == MODE_SLEEP: # We successfully changed the mode. Consider it a success even if display operation fails. @@ -174,7 +190,9 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): """Turn the device on.""" success = await self.device.turn_on() if not success: - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to turn on humidifier.") self.async_write_ha_state() @@ -182,7 +200,9 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity): """Turn the device off.""" success = await self.device.turn_off() if not success: - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Failed to turn off humidifier.") self.async_write_ha_state() diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 9b56274634a..e10dde26709 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -5,8 +5,6 @@ from typing import Any from pyvesync.base_devices.bulb_base import VeSyncBulb from pyvesync.base_devices.switch_base import VeSyncSwitch -from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice -from pyvesync.device_container import DeviceContainer from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -40,7 +38,7 @@ async def async_setup_entry( coordinator = config_entry.runtime_data @callback - def discover(devices: list[VeSyncBaseDevice]) -> None: + def discover(devices: list[VeSyncBulb | VeSyncSwitch]) -> None: """Add new devices to platform.""" _setup_entities(devices, async_add_entities, coordinator) @@ -49,13 +47,16 @@ async def async_setup_entry( ) _setup_entities( - config_entry.runtime_data.manager.devices, async_add_entities, coordinator + config_entry.runtime_data.manager.devices.bulbs + + config_entry.runtime_data.manager.devices.switches, + async_add_entities, + coordinator, ) @callback def _setup_entities( - devices: DeviceContainer | list[VeSyncBaseDevice], + devices: list[VeSyncBulb | VeSyncSwitch], async_add_entities: AddConfigEntryEntitiesCallback, coordinator: VeSyncDataCoordinator, ) -> None: @@ -73,9 +74,10 @@ def _setup_entities( async_add_entities(entities, update_before_add=True) -class VeSyncBaseLightHA(VeSyncBaseEntity, LightEntity): +class VeSyncBaseLightHA(VeSyncBaseEntity[VeSyncSwitch | VeSyncBulb], LightEntity): """Base class for VeSync Light Devices Representations.""" + device: VeSyncBulb | VeSyncSwitch _attr_name = None @property @@ -86,26 +88,24 @@ class VeSyncBaseLightHA(VeSyncBaseEntity, LightEntity): @property def brightness(self) -> int: """Get light brightness.""" - # get value from pyvesync library api, - result = self.device.state.brightness - try: - # check for validity of brightness value received - brightness_value = int(result) - except ValueError: - # deal if any unexpected/non numeric value + if self.device.state.brightness is None: _LOGGER.debug( - "VeSync - received unexpected 'brightness' value from pyvesync api: %s", - result, + "VeSync - received unexpected 'brightness' value from pyvesync api of None" ) return 0 + # convert percent brightness to ha expected range - return round((max(1, brightness_value) / 100) * 255) + return round((max(1, self.device.state.brightness) / 100) * 255) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" attribute_adjustment_only = False # set white temperature - if self.color_mode == ColorMode.COLOR_TEMP and ATTR_COLOR_TEMP_KELVIN in kwargs: + if ( + self.color_mode == ColorMode.COLOR_TEMP + and ATTR_COLOR_TEMP_KELVIN in kwargs + and hasattr(self.device, "set_color_temp") + ): # get white temperature from HA data color_temp = color_util.color_temperature_kelvin_to_mired( kwargs[ATTR_COLOR_TEMP_KELVIN] @@ -165,6 +165,7 @@ class VeSyncDimmableLightHA(VeSyncBaseLightHA, LightEntity): class VeSyncTunableWhiteLightHA(VeSyncBaseLightHA, LightEntity): """Representation of a VeSync Tunable White Light device.""" + device: VeSyncBulb _attr_color_mode = ColorMode.COLOR_TEMP _attr_min_color_temp_kelvin = 2700 # 370 Mireds _attr_max_color_temp_kelvin = 6500 # 153 Mireds @@ -173,24 +174,18 @@ class VeSyncTunableWhiteLightHA(VeSyncBaseLightHA, LightEntity): @property def color_temp_kelvin(self) -> int | None: """Return the color temperature value in Kelvin.""" - # get value from pyvesync library api - # pyvesync v3 provides BulbState.color_temp_kelvin() - possible to use that instead? - result = self.device.state.color_temp - try: - # check for validity of brightness value received - color_temp_value = int(result) - except ValueError: - # deal if any unexpected/non numeric value - _LOGGER.debug( - ( - "VeSync - received unexpected 'color_temp_pct' value from pyvesync" - " api: %s" - ), - result, - ) + if hasattr(self.device.state, "color_temp") is False: return None + + # pyvesync v3 provides BulbState.color_temp_kelvin() - possible to use that instead? + if self.device.state.color_temp is None: + _LOGGER.debug( + "VeSync - received unexpected 'color_temp' value from pyvesync api of None" + ) + return 0 + # flip cold/warm - color_temp_value = 100 - color_temp_value + color_temp_value = 100 - self.device.state.color_temp # ensure value between 0-100 color_temp_value = max(0, min(color_temp_value, 100)) # convert percent value to Mireds diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 82da2fff600..6730477b05f 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/vesync", "iot_class": "cloud_polling", "loggers": ["pyvesync"], - "requirements": ["pyvesync==3.3.3"] + "requirements": ["pyvesync==3.4.1"] } diff --git a/homeassistant/components/vesync/number.py b/homeassistant/components/vesync/number.py index 109774bb672..6793322e7b2 100644 --- a/homeassistant/components/vesync/number.py +++ b/homeassistant/components/vesync/number.py @@ -27,6 +27,20 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 +def _mist_levels(device: VeSyncBaseDevice) -> list[int]: + """Check if the device supports mist level adjustment.""" + if is_humidifier(device): + return device.mist_levels + raise HomeAssistantError("Device does not support mist level adjustment.") + + +def _set_mist_level(device: VeSyncBaseDevice, value: float) -> Awaitable[bool]: + """Set mist level on humidifier.""" + if is_humidifier(device): + return device.set_mist_level(int(value)) + raise HomeAssistantError("Device does not support mist level adjustment.") + + @dataclass(frozen=True, kw_only=True) class VeSyncNumberEntityDescription(NumberEntityDescription): """Class to describe a Vesync number entity.""" @@ -42,12 +56,12 @@ NUMBER_DESCRIPTIONS: list[VeSyncNumberEntityDescription] = [ VeSyncNumberEntityDescription( key="mist_level", translation_key="mist_level", - native_min_value_fn=lambda device: min(device.mist_levels), - native_max_value_fn=lambda device: max(device.mist_levels), + native_min_value_fn=lambda device: min(_mist_levels(device)), + native_max_value_fn=lambda device: max(_mist_levels(device)), native_step=1, mode=NumberMode.SLIDER, exists_fn=is_humidifier, - set_value_fn=lambda device, value: device.set_mist_level(value), + set_value_fn=_set_mist_level, value_fn=lambda device: device.state.mist_virtual_level, ) ] diff --git a/homeassistant/components/vesync/select.py b/homeassistant/components/vesync/select.py index 4da5ff98aba..00fa367001a 100644 --- a/homeassistant/components/vesync/select.py +++ b/homeassistant/components/vesync/select.py @@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging -from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice +from pyvesync.base_devices import VeSyncBaseDevice from pyvesync.device_container import DeviceContainer from homeassistant.components.select import SelectEntity, SelectEntityDescription @@ -45,6 +45,27 @@ HA_TO_VS_HUMIDIFIER_NIGHT_LIGHT_LEVEL_MAP = { PARALLEL_UPDATES = 1 +def _set_humidifier_nightlight(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle humidifier nightlight on.""" + if is_humidifier(device): + return device.set_nightlight_brightness(*args) + raise HomeAssistantError("Device does not support toggling nightlight.") + + +def _toggle_purifier_nightlight(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle air purifier nightlight on.""" + if is_purifier(device): + return device.set_nightlight_mode(*args) + raise HomeAssistantError("Device does not support toggling nightlight.") + + +def _toggle_outlet_nightlight(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle outlet nightlight on.""" + if is_outlet(device) and device.supports_nightlight: + return device.set_nightlight_state(*args) + raise HomeAssistantError("Device does not support toggling nightlight.") + + @dataclass(frozen=True, kw_only=True) class VeSyncSelectEntityDescription(SelectEntityDescription): """Class to describe a Vesync select entity.""" @@ -64,8 +85,8 @@ SELECT_DESCRIPTIONS: list[VeSyncSelectEntityDescription] = [ exists_fn=lambda device: is_humidifier(device) and device.supports_nightlight, # The select_option service framework ensures that only options specified are # accepted. ServiceValidationError gets raised for invalid value. - select_option_fn=lambda device, value: device.set_nightlight_brightness( - HA_TO_VS_HUMIDIFIER_NIGHT_LIGHT_LEVEL_MAP.get(value, 0) + select_option_fn=lambda device, value: _set_humidifier_nightlight( + device, HA_TO_VS_HUMIDIFIER_NIGHT_LIGHT_LEVEL_MAP.get(value, 0) ), # Reporting "off" as the choice for unhandled level. current_option_fn=lambda device: VS_TO_HA_HUMIDIFIER_NIGHT_LIGHT_LEVEL_MAP.get( @@ -84,7 +105,7 @@ SELECT_DESCRIPTIONS: list[VeSyncSelectEntityDescription] = [ ], icon="mdi:brightness-6", exists_fn=lambda device: is_purifier(device) and device.supports_nightlight, - select_option_fn=lambda device, value: device.set_nightlight_mode(value), + select_option_fn=_toggle_purifier_nightlight, current_option_fn=lambda device: device.state.nightlight_status, ), # night_light for outlets @@ -98,7 +119,7 @@ SELECT_DESCRIPTIONS: list[VeSyncSelectEntityDescription] = [ ], icon="mdi:brightness-6", exists_fn=lambda device: is_outlet(device) and device.supports_nightlight, - select_option_fn=lambda device, value: device.set_nightlight_state(value), + select_option_fn=_toggle_outlet_nightlight, current_option_fn=lambda device: device.state.nightlight_status, ), ] diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index c7b1fad29d9..115a760876a 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -50,7 +50,6 @@ "state_attributes": { "preset_mode": { "state": { - "advanced_sleep": "Advanced sleep", "auto": "[%key:common::state::auto%]", "normal": "[%key:common::state::normal%]", "pet": "Pet", diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 701b425cd99..043f656b7ff 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import logging from typing import Any, Final -from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice +from pyvesync.base_devices import VeSyncBaseDevice, VeSyncHumidifier from pyvesync.device_container import DeviceContainer from homeassistant.components.switch import ( @@ -28,6 +28,38 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 +def _toggle_switch(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle power on.""" + if args and args[0] is True and hasattr(device, "turn_on"): + return device.turn_on() + if args and args[0] is False and hasattr(device, "turn_off"): + return device.turn_off() + raise HomeAssistantError("Device does not support toggling power.") + + +def _toggle_display(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle display on.""" + if hasattr(device, "toggle_display"): + return device.toggle_display(*args) + raise HomeAssistantError("Device does not support toggling display.") + + +def _toggle_child_lock(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle child lock on.""" + if hasattr(device, "toggle_child_lock"): + return device.toggle_child_lock(*args) + raise HomeAssistantError("Device does not support toggling child lock.") + + +def _toggle_auto_stop(device: VeSyncBaseDevice, *args) -> Awaitable[bool]: + """Toggle automatic stop on.""" + match device: + case VeSyncHumidifier() as sw if hasattr(sw, "toggle_automatic_stop"): + return sw.toggle_automatic_stop(*args) + case _: + raise HomeAssistantError("Device does not support toggling automatic stop.") + + @dataclass(frozen=True, kw_only=True) class VeSyncSwitchEntityDescription(SwitchEntityDescription): """A class that describes custom switch entities.""" @@ -45,8 +77,8 @@ SENSOR_DESCRIPTIONS: Final[tuple[VeSyncSwitchEntityDescription, ...]] = ( # Other types of wall switches support dimming. Those use light.py platform. exists_fn=lambda device: is_wall_switch(device) or is_outlet(device), name=None, - on_fn=lambda device: device.turn_on(), - off_fn=lambda device: device.turn_off(), + on_fn=lambda device: _toggle_switch(device, True), + off_fn=lambda device: _toggle_switch(device, False), ), VeSyncSwitchEntityDescription( key="display", @@ -55,16 +87,16 @@ SENSOR_DESCRIPTIONS: Final[tuple[VeSyncSwitchEntityDescription, ...]] = ( lambda device: rgetattr(device, "state.display_set_status") is not None ), translation_key="display", - on_fn=lambda device: device.toggle_display(True), - off_fn=lambda device: device.toggle_display(False), + on_fn=lambda device: _toggle_display(device, True), + off_fn=lambda device: _toggle_display(device, False), ), VeSyncSwitchEntityDescription( key="child_lock", is_on=lambda device: device.state.child_lock, exists_fn=(lambda device: rgetattr(device, "state.child_lock") is not None), translation_key="child_lock", - on_fn=lambda device: device.toggle_child_lock(True), - off_fn=lambda device: device.toggle_child_lock(False), + on_fn=lambda device: _toggle_child_lock(device, True), + off_fn=lambda device: _toggle_child_lock(device, False), ), VeSyncSwitchEntityDescription( key="auto_off_config", @@ -73,8 +105,8 @@ SENSOR_DESCRIPTIONS: Final[tuple[VeSyncSwitchEntityDescription, ...]] = ( lambda device: rgetattr(device, "state.automatic_stop_config") is not None ), translation_key="auto_off_config", - on_fn=lambda device: device.toggle_automatic_stop(True), - off_fn=lambda device: device.toggle_automatic_stop(False), + on_fn=lambda device: _toggle_auto_stop(device, True), + off_fn=lambda device: _toggle_auto_stop(device, False), ), ) @@ -89,7 +121,7 @@ async def async_setup_entry( coordinator = config_entry.runtime_data @callback - def discover(devices: list[VeSyncBaseDevice]) -> None: + def discover(devices: DeviceContainer) -> None: """Add new devices to platform.""" _setup_entities(devices, async_add_entities, coordinator) @@ -98,13 +130,15 @@ async def async_setup_entry( ) _setup_entities( - config_entry.runtime_data.manager.devices, async_add_entities, coordinator + config_entry.runtime_data.manager.devices, + async_add_entities, + coordinator, ) @callback def _setup_entities( - devices: DeviceContainer | list[VeSyncBaseDevice], + devices: DeviceContainer, async_add_entities: AddConfigEntryEntitiesCallback, coordinator: VeSyncDataCoordinator, ) -> None: @@ -117,7 +151,7 @@ def _setup_entities( ) -class VeSyncSwitchEntity(SwitchEntity, VeSyncBaseEntity): +class VeSyncSwitchEntity(SwitchEntity, VeSyncBaseEntity[VeSyncBaseDevice]): """VeSync switch entity class.""" entity_description: VeSyncSwitchEntityDescription @@ -145,13 +179,17 @@ class VeSyncSwitchEntity(SwitchEntity, VeSyncBaseEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if not await self.entity_description.off_fn(self.device): - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Unknown error turning off device, no response.") self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" if not await self.entity_description.on_fn(self.device): - raise HomeAssistantError(self.device.last_response.message) + if self.device.last_response: + raise HomeAssistantError(self.device.last_response.message) + raise HomeAssistantError("Unknown error turning on device, no response.") self.async_write_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index eddbb2152e2..2c4d8faf592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2656,7 +2656,7 @@ pyvera==0.3.16 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==3.3.3 +pyvesync==3.4.1 # homeassistant.components.vizio pyvizio==0.1.61 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 11700a7358f..bf0adbeead4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2234,7 +2234,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.16 # homeassistant.components.vesync -pyvesync==3.3.3 +pyvesync==3.4.1 # homeassistant.components.vizio pyvizio==0.1.61 diff --git a/tests/components/vesync/snapshots/test_diagnostics.ambr b/tests/components/vesync/snapshots/test_diagnostics.ambr index 7062bf25764..28509cf632d 100644 --- a/tests/components/vesync/snapshots/test_diagnostics.ambr +++ b/tests/components/vesync/snapshots/test_diagnostics.ambr @@ -115,17 +115,20 @@ 'to_json': 'Method', 'to_jsonb': 'Method', 'toggle_automatic_stop': 'Method', + 'toggle_child_lock': 'Method', 'toggle_display': 'Method', 'toggle_drying_mode': 'Method', 'toggle_nightlight': 'Method', 'toggle_switch': 'Method', 'turn_off': 'Method', 'turn_off_automatic_stop': 'Method', + 'turn_off_child_lock': 'Method', 'turn_off_display': 'Method', 'turn_off_drying_mode': 'Method', 'turn_off_nightlight': 'Method', 'turn_on': 'Method', 'turn_on_automatic_stop': 'Method', + 'turn_on_child_lock': 'Method', 'turn_on_display': 'Method', 'turn_on_drying_mode': 'Method', 'turn_on_nightlight': 'Method', diff --git a/tests/components/vesync/snapshots/test_fan.ambr b/tests/components/vesync/snapshots/test_fan.ambr index 4a820ea7b57..30163fb34a1 100644 --- a/tests/components/vesync/snapshots/test_fan.ambr +++ b/tests/components/vesync/snapshots/test_fan.ambr @@ -740,9 +740,9 @@ 'area_id': None, 'capabilities': dict({ 'preset_modes': list([ - 'advanced_sleep', 'auto', 'normal', + 'sleep', 'turbo', ]), }), @@ -783,15 +783,15 @@ 'active_time': None, 'display_status': 'off', 'friendly_name': 'SmartTowerFan', - 'mode': 'normal', + 'mode': , 'oscillating': True, 'percentage': 0, 'percentage_step': 8.333333333333334, 'preset_mode': 'normal', 'preset_modes': list([ - 'advanced_sleep', 'auto', 'normal', + 'sleep', 'turbo', ]), 'supported_features': , diff --git a/tests/components/vesync/test_fan.py b/tests/components/vesync/test_fan.py index 731045ac10a..65505af4ee8 100644 --- a/tests/components/vesync/test_fan.py +++ b/tests/components/vesync/test_fan.py @@ -143,8 +143,8 @@ async def test_turn_on_off_raises_error( [ ("normal", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_normal_mode"), ( - "advanced_sleep", - "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_advanced_sleep_mode", + "sleep", + "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_sleep_mode", ), ("turbo", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_turbo_mode"), ("auto", "pyvesync.devices.vesyncfan.VeSyncTowerFan.set_auto_mode"),