Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor fan in vesync #135744

Draft
wants to merge 18 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
8 changes: 7 additions & 1 deletion homeassistant/components/vesync/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from homeassistant.core import HomeAssistant

from .const import VeSyncHumidifierDevice
from .const import VeSyncFanDevice, VeSyncHumidifierDevice

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -58,6 +58,12 @@ def is_humidifier(device: VeSyncBaseDevice) -> bool:
return isinstance(device, VeSyncHumidifierDevice)


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

return isinstance(device, VeSyncFanDevice)


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

Expand Down
29 changes: 28 additions & 1 deletion homeassistant/components/vesync/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Constants for VeSync Component."""

from pyvesync.vesyncfan import VeSyncHumid200300S, VeSyncSuperior6000S
from pyvesync.vesyncfan import (
VeSyncAir131,
VeSyncAirBaseV2,
VeSyncAirBypass,
VeSyncHumid200300S,
VeSyncSuperior6000S,
)

DOMAIN = "vesync"
VS_DISCOVERY = "vesync_discovery_{}"
Expand Down Expand Up @@ -29,13 +35,34 @@
VS_HUMIDIFIER_MODE_MANUAL = "manual"
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"
VS_FAN_MODE_NORMAL = "normal"

# not a full list as manual is used as speed not present
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,
]
NIGHT_LIGHT_LEVEL_BRIGHT = "bright"
NIGHT_LIGHT_LEVEL_DIM = "dim"
NIGHT_LIGHT_LEVEL_OFF = "off"

VeSyncHumidifierDevice = VeSyncHumid200300S | VeSyncSuperior6000S
"""Humidifier device types"""

VeSyncFanDevice = VeSyncAirBypass | VeSyncAirBypass | VeSyncAirBaseV2 | VeSyncAir131
"""Fan device types"""


DEV_TYPE_TO_HA = {
"wifi-switch-1.3": "outlet",
"ESW03-USA": "outlet",
Expand Down
150 changes: 71 additions & 79 deletions homeassistant/components/vesync/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from homeassistant.components.fan import FanEntity, FanEntityFeature
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 AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
Expand All @@ -19,43 +20,27 @@
)
from homeassistant.util.scaling import int_states_in_range

from .common import is_fan
from .const import (
DEV_TYPE_TO_HA,
DOMAIN,
SKU_TO_BASE_DEVICE,
VS_COORDINATOR,
VS_DEVICES,
VS_DISCOVERY,
VS_FAN_MODE_ADVANCED_SLEEP,
VS_FAN_MODE_AUTO,
VS_FAN_MODE_MANUAL,
VS_FAN_MODE_NORMAL,
VS_FAN_MODE_PET,
VS_FAN_MODE_PRESET_LIST_HA,
VS_FAN_MODE_SLEEP,
VS_FAN_MODE_TURBO,
)
from .coordinator import VeSyncDataCoordinator
from .entity import VeSyncBaseEntity

_LOGGER = logging.getLogger(__name__)

FAN_MODE_AUTO = "auto"
FAN_MODE_SLEEP = "sleep"
FAN_MODE_PET = "pet"
FAN_MODE_TURBO = "turbo"
FAN_MODE_ADVANCED_SLEEP = "advancedSleep"
FAN_MODE_NORMAL = "normal"


PRESET_MODES = {
"LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
"Core200S": [FAN_MODE_SLEEP],
"Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
"Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
"Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP],
"EverestAir": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_TURBO],
"Vital200S": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_PET],
"Vital100S": [FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_PET],
"SmartTowerFan": [
FAN_MODE_ADVANCED_SLEEP,
FAN_MODE_AUTO,
FAN_MODE_TURBO,
FAN_MODE_NORMAL,
],
}
SPEED_RANGE = { # off is not included
"LV-PUR131S": (1, 3),
"Core200S": (1, 3),
Expand Down Expand Up @@ -97,13 +82,8 @@
coordinator: VeSyncDataCoordinator,
):
"""Check if device is fan and add entity."""
entities = [
VeSyncFanHA(dev, coordinator)
for dev in devices
if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type, "")) == "fan"
]

async_add_entities(entities, update_before_add=True)
async_add_entities(VeSyncFanHA(dev, coordinator) for dev in devices if is_fan(dev))


class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
Expand All @@ -118,13 +98,6 @@
_attr_name = None
_attr_translation_key = "vesync"

def __init__(
self, fan: VeSyncBaseDevice, coordinator: VeSyncDataCoordinator
) -> None:
"""Initialize the VeSync fan device."""
super().__init__(fan, coordinator)
self.smartfan = fan

@property
def is_on(self) -> bool:
"""Return True if device is on."""
Expand All @@ -134,8 +107,8 @@
def percentage(self) -> int | None:
"""Return the current speed."""
if (
self.smartfan.mode == "manual"
and (current_level := self.smartfan.fan_level) is not None
self.device.mode == VS_FAN_MODE_MANUAL
and (current_level := self.device.fan_level) is not None
):
return ranged_value_to_percentage(
SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], current_level
Expand All @@ -152,79 +125,95 @@
@property
def preset_modes(self) -> list[str]:
"""Get the list of available preset modes."""
return PRESET_MODES[SKU_TO_BASE_DEVICE[self.device.device_type]]
if hasattr(self.device, "modes"):
return sorted(
[
mode
for mode in self.device.modes
if mode in VS_FAN_MODE_PRESET_LIST_HA
]
)
return []

Check warning on line 136 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L136

Added line #L136 was not covered by tests

@property
def preset_mode(self) -> str | None:
"""Get the current preset mode."""
if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP, FAN_MODE_TURBO):
return self.smartfan.mode
if self.device.mode in VS_FAN_MODE_PRESET_LIST_HA:
return self.device.mode
return None

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the fan."""
attr = {}

if hasattr(self.smartfan, "active_time"):
attr["active_time"] = self.smartfan.active_time
if hasattr(self.device, "active_time"):
attr["active_time"] = self.device.active_time

Check warning on line 151 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L151

Added line #L151 was not covered by tests

if hasattr(self.smartfan, "screen_status"):
attr["screen_status"] = self.smartfan.screen_status
if hasattr(self.device, "screen_status"):
attr["screen_status"] = self.device.screen_status

if hasattr(self.smartfan, "child_lock"):
attr["child_lock"] = self.smartfan.child_lock
if hasattr(self.device, "child_lock"):
attr["child_lock"] = self.device.child_lock

if hasattr(self.smartfan, "night_light"):
attr["night_light"] = self.smartfan.night_light
if hasattr(self.device, "night_light"):
attr["night_light"] = self.device.night_light

if hasattr(self.smartfan, "mode"):
attr["mode"] = self.smartfan.mode
if hasattr(self.device, "mode"):
attr["mode"] = self.device.mode

return attr

def set_percentage(self, percentage: int) -> None:
"""Set the speed of the device."""
if percentage == 0:
self.smartfan.turn_off()
return

if not self.smartfan.is_on:
self.smartfan.turn_on()

self.smartfan.manual_mode()
self.smartfan.change_fan_speed(
success = self.device.turn_off()
if not success:
raise HomeAssistantError("An error occurred while turning off.")

Check warning on line 172 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L170-L172

Added lines #L170 - L172 were not covered by tests
elif not self.device.is_on:
success = self.device.turn_on()
if not success:
raise HomeAssistantError("An error occurred while turning on.")

success = self.device.manual_mode()
if not success:
raise HomeAssistantError("An error occurred while manual mode.")

Check warning on line 180 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L180

Added line #L180 was not covered by tests
success = self.device.change_fan_speed(
math.ceil(
percentage_to_ranged_value(
SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage
)
)
)
if not success:
raise HomeAssistantError("An error occurred while changing fan speed.")

Check warning on line 189 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L189

Added line #L189 was not covered by tests
self.schedule_update_ha_state()

def set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of device."""
if preset_mode not in self.preset_modes:
if preset_mode not in VS_FAN_MODE_PRESET_LIST_HA:
raise ValueError(
f"{preset_mode} is not one of the valid preset modes: "
f"{self.preset_modes}"
f"{VS_FAN_MODE_PRESET_LIST_HA}"
)

if not self.smartfan.is_on:
self.smartfan.turn_on()

if preset_mode == FAN_MODE_AUTO:
self.smartfan.auto_mode()
elif preset_mode == FAN_MODE_SLEEP:
self.smartfan.sleep_mode()
elif preset_mode == FAN_MODE_ADVANCED_SLEEP:
self.smartfan.advanced_sleep_mode()
elif preset_mode == FAN_MODE_PET:
self.smartfan.pet_mode()
elif preset_mode == FAN_MODE_TURBO:
self.smartfan.turbo_mode()
elif preset_mode == FAN_MODE_NORMAL:
self.smartfan.normal_mode()
if not self.device.is_on:
self.device.turn_on()

if preset_mode == VS_FAN_MODE_AUTO:
success = self.device.auto_mode()

Check warning on line 204 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L204

Added line #L204 was not covered by tests
elif preset_mode == VS_FAN_MODE_SLEEP:
success = self.device.sleep_mode()

Check warning on line 206 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L206

Added line #L206 was not covered by tests
elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP:
success = self.device.advanced_sleep_mode()

Check warning on line 208 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L208

Added line #L208 was not covered by tests
elif preset_mode == VS_FAN_MODE_PET:
success = self.device.pet_mode()

Check warning on line 210 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L210

Added line #L210 was not covered by tests
elif preset_mode == VS_FAN_MODE_TURBO:
success = self.device.turbo_mode()

Check warning on line 212 in homeassistant/components/vesync/fan.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/vesync/fan.py#L212

Added line #L212 was not covered by tests
elif preset_mode == VS_FAN_MODE_NORMAL:
success = self.device.normal_mode()
if not success:
raise HomeAssistantError("An error occurred while setting preset mode.")

self.schedule_update_ha_state()

Expand All @@ -244,4 +233,7 @@

def turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
self.device.turn_off()
success = self.device.turn_off()
if not success:
raise HomeAssistantError("An error occurred while turning off.")
self.schedule_update_ha_state()
2 changes: 2 additions & 0 deletions tests/components/vesync/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
ENTITY_HUMIDIFIER_HUMIDITY = "sensor.humidifier_200s_humidity"
ENTITY_HUMIDIFIER_300S_NIGHT_LIGHT_SELECT = "select.humidifier_300s_night_light_level"

ENTITY_FAN = "fan.SmartTowerFan"

ALL_DEVICES = load_json_object_fixture("vesync-devices.json", DOMAIN)
ALL_DEVICE_NAMES: list[str] = [
dev["deviceName"] for dev in ALL_DEVICES["result"]["list"]
Expand Down
20 changes: 20 additions & 0 deletions tests/components/vesync/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ async def install_humidifier_device(
await hass.async_block_till_done()


@pytest.fixture(name="fan_config_entry")
async def fan_config_entry(
hass: HomeAssistant, requests_mock: requests_mock.Mocker, config
) -> MockConfigEntry:
"""Create a mock VeSync config entry for `SmartTowerFan`."""
entry = MockConfigEntry(
title="VeSync",
domain=DOMAIN,
data=config[DOMAIN],
)
entry.add_to_hass(hass)

device_name = "SmartTowerFan"
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
Expand Down
6 changes: 3 additions & 3 deletions tests/components/vesync/snapshots/test_fan.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,8 @@
'preset_modes': list([
'advancedSleep',
'auto',
'turbo',
'normal',
'turbo',
]),
}),
'config_entry_id': <ANY>,
Expand Down Expand Up @@ -676,12 +676,12 @@
'night_light': 'off',
'percentage': None,
'percentage_step': 7.6923076923076925,
'preset_mode': None,
'preset_mode': 'normal',
'preset_modes': list([
'advancedSleep',
'auto',
'turbo',
'normal',
'turbo',
]),
'screen_status': False,
'supported_features': <FanEntityFeature: 57>,
Expand Down
Loading