1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Bump pyvesync to 3.4.1 (#160573)

Co-authored-by: Joe Trabulsy <jtrabulsy@gmail.com>
This commit is contained in:
cdnninja
2026-01-27 08:18:37 -07:00
committed by GitHub
parent d880d305f4
commit f7bc7d3911
14 changed files with 232 additions and 117 deletions

View File

@@ -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,

View File

@@ -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.")

View File

@@ -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()

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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,
)
]

View File

@@ -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,
),
]

View File

@@ -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",

View File

@@ -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()

2
requirements_all.txt generated
View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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': <FanModes.NORMAL: 'normal'>,
'oscillating': True,
'percentage': 0,
'percentage_step': 8.333333333333334,
'preset_mode': 'normal',
'preset_modes': list([
'advanced_sleep',
'auto',
'normal',
'sleep',
'turbo',
]),
'supported_features': <FanEntityFeature: 59>,

View File

@@ -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"),