Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions homeassistant/components/vesync/common.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Common utilities for VeSync Component."""

import logging
from typing import TypeGuard

from pyvesync.base_devices import VeSyncHumidifier
from pyvesync.base_devices.fan_base import VeSyncFanBase
from pyvesync.base_devices.fryer_base import VeSyncFryer
from pyvesync.base_devices.outlet_base import VeSyncOutlet
from pyvesync.base_devices.purifier_base import VeSyncPurifier
from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice
from pyvesync.const import ProductTypes
from pyvesync.devices.vesyncswitch import VeSyncWallSwitch

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -35,37 +37,39 @@ def rgetattr(obj: object, attr: str) -> object | str | None:
return obj


def is_humidifier(device: VeSyncBaseDevice) -> bool:
def is_humidifier(device: VeSyncBaseDevice) -> TypeGuard[VeSyncHumidifier]:
"""Check if the device represents a humidifier."""

return isinstance(device, VeSyncHumidifier)
return device.product_type == ProductTypes.HUMIDIFIER


def is_fan(device: VeSyncBaseDevice) -> bool:
def is_fan(device: VeSyncBaseDevice) -> TypeGuard[VeSyncFanBase]:
"""Check if the device represents a fan."""

return isinstance(device, VeSyncFanBase)
return device.product_type == ProductTypes.FAN


def is_outlet(device: VeSyncBaseDevice) -> bool:
def is_outlet(device: VeSyncBaseDevice) -> TypeGuard[VeSyncOutlet]:
"""Check if the device represents an outlet."""

return isinstance(device, VeSyncOutlet)
return device.product_type == ProductTypes.OUTLET


def is_wall_switch(device: VeSyncBaseDevice) -> bool:
def is_wall_switch(device: VeSyncBaseDevice) -> TypeGuard[VeSyncWallSwitch]:
"""Check if the device represents a wall switch, note this doessn't include dimming switches."""
if device.product_type != ProductTypes.SWITCH:
return False

return isinstance(device, VeSyncWallSwitch)
return getattr(device, "supports_dimmable", False) is False


def is_purifier(device: VeSyncBaseDevice) -> bool:
def is_purifier(device: VeSyncBaseDevice) -> TypeGuard[VeSyncPurifier]:
"""Check if the device represents an air purifier."""

return isinstance(device, VeSyncPurifier)
return device.product_type == ProductTypes.PURIFIER


def is_air_fryer(device: VeSyncBaseDevice) -> bool:
def is_air_fryer(device: VeSyncBaseDevice) -> TypeGuard[VeSyncFryer]:
"""Check if the device represents an air fryer."""

return isinstance(device, VeSyncFryer)
return device.product_type == ProductTypes.AIR_FRYER
2 changes: 0 additions & 2 deletions homeassistant/components/vesync/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@

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"
Expand All @@ -42,7 +41,6 @@
VS_FAN_MODE_PRESET_LIST_HA = [
VS_FAN_MODE_AUTO,
VS_FAN_MODE_SLEEP,
VS_FAN_MODE_ADVANCED_SLEEP,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this removal intentional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Library converted that into sleep. It previously didn't work either way. It is fixed now.

VS_FAN_MODE_TURBO,
VS_FAN_MODE_PET,
VS_FAN_MODE_NORMAL,
Expand Down
10 changes: 6 additions & 4 deletions homeassistant/components/vesync/entity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Common entity for VeSync Component."""

from typing import Generic, TypeVar

from pyvesync.base_devices.vesyncbasedevice import VeSyncBaseDevice

from homeassistant.helpers.device_registry import DeviceInfo
Expand All @@ -8,15 +10,15 @@
from .const import DOMAIN
from .coordinator import VeSyncDataCoordinator

T = TypeVar("T", bound=VeSyncBaseDevice)


class VeSyncBaseEntity(CoordinatorEntity[VeSyncDataCoordinator]):
class VeSyncBaseEntity(CoordinatorEntity[VeSyncDataCoordinator], Generic[T]):
"""Base class for VeSync Entity Representations."""

_attr_has_entity_name = True

def __init__(
self, device: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
) -> None:
def __init__(self, device: T, coordinator: VeSyncDataCoordinator) -> None:
"""Initialize the VeSync device."""
super().__init__(coordinator)
self.device = device
Expand Down
106 changes: 68 additions & 38 deletions homeassistant/components/vesync/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
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
Expand All @@ -22,7 +21,6 @@
from .const import (
VS_DEVICES,
VS_DISCOVERY,
VS_FAN_MODE_ADVANCED_SLEEP,
VS_FAN_MODE_AUTO,
VS_FAN_MODE_MANUAL,
VS_FAN_MODE_NORMAL,
Expand All @@ -40,7 +38,6 @@
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,
Expand All @@ -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)
Expand All @@ -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 = (
Expand All @@ -109,7 +113,7 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):

def __init__(
self,
device: VeSyncBaseDevice,
device: VeSyncFanBase | VeSyncPurifier,
coordinator: VeSyncDataCoordinator,
) -> None:
"""Initialize the fan."""
Expand Down Expand Up @@ -196,7 +200,10 @@ def extra_state_attributes(self) -> dict[str, Any]:
hasattr(self.device.state, "child_lock")
and self.device.state.child_lock is not None
):
attr["child_lock"] = self.device.state.child_lock
if isinstance(self.device.state.child_lock, bool):
attr["child_lock"] = self.device.state.child_lock
else:
attr["child_lock"] = self.device.state.child_lock != "off"

if (
hasattr(self.device.state, "nightlight_status")
Expand All @@ -206,7 +213,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
self.device.state.nightlight_status, "value", None
)
if hasattr(self.device.state, "mode"):
attr["mode"] = self.device.state.mode
attr["mode"] = getattr(self.device.state.mode, "value", None)

return attr

Expand All @@ -219,37 +226,47 @@ async def async_set_percentage(self, percentage: int) -> None:
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()

Expand All @@ -270,17 +287,19 @@ async def async_set_preset_mode(self, preset_mode: str) -> None:
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()

Expand All @@ -297,7 +316,9 @@ async def async_turn_on(
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)
Expand All @@ -306,12 +327,21 @@ async def async_turn_off(self, **kwargs: Any) -> None:
"""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.")
Loading