Refactor Tuya Fans (#64765)

This commit is contained in:
Franck Nijhof 2022-01-25 08:53:55 +01:00 committed by GitHub
parent ed924325e3
commit ac7450bfda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 147 additions and 94 deletions

View File

@ -202,8 +202,10 @@ class DPCode(StrEnum):
EDGE_BRUSH = "edge_brush" EDGE_BRUSH = "edge_brush"
ELECTRICITY_LEFT = "electricity_left" ELECTRICITY_LEFT = "electricity_left"
FAN_DIRECTION = "fan_direction" # Fan direction FAN_DIRECTION = "fan_direction" # Fan direction
FAN_SPEED = "fan_speed"
FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode
FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed
FAN_MODE = "fan_mode"
FAR_DETECTION = "far_detection" FAR_DETECTION = "far_detection"
FAULT = "fault" FAULT = "fault"
FEED_REPORT = "feed_report" FEED_REPORT = "feed_report"
@ -301,6 +303,7 @@ class DPCode(StrEnum):
SWITCH_CHARGE = "switch_charge" SWITCH_CHARGE = "switch_charge"
SWITCH_CONTROLLER = "switch_controller" SWITCH_CONTROLLER = "switch_controller"
SWITCH_DISTURB = "switch_disturb" SWITCH_DISTURB = "switch_disturb"
SWITCH_FAN = "switch_fan"
SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch SWITCH_HORIZONTAL = "switch_horizontal" # Horizontal swing flap switch
SWITCH_LED = "switch_led" # Switch SWITCH_LED = "switch_led" # Switch
SWITCH_LED_1 = "switch_led_1" SWITCH_LED_1 = "switch_led_1"

View File

@ -1,7 +1,6 @@
"""Support for Tuya Fan.""" """Support for Tuya Fan."""
from __future__ import annotations from __future__ import annotations
import json
from typing import Any from typing import Any
from tuya_iot import TuyaDevice, TuyaDeviceManager from tuya_iot import TuyaDevice, TuyaDeviceManager
@ -25,13 +24,14 @@ from homeassistant.util.percentage import (
) )
from . import HomeAssistantTuyaData from . import HomeAssistantTuyaData
from .base import TuyaEntity from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
TUYA_SUPPORT_TYPE = { TUYA_SUPPORT_TYPE = {
"fs", # Fan "fs", # Fan
"kj", # Air Purifier
"fsd", # Fan with Light "fsd", # Fan with Light
"fskg", # Fan wall switch
"kj", # Air Purifier
} }
@ -61,68 +61,103 @@ async def async_setup_entry(
class TuyaFanEntity(TuyaEntity, FanEntity): class TuyaFanEntity(TuyaEntity, FanEntity):
"""Tuya Fan Device.""" """Tuya Fan Device."""
def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: _direction: EnumTypeData | None = None
_oscillate: DPCode | None = None
_presets: EnumTypeData | None = None
_speed: IntegerTypeData | None = None
_speeds: EnumTypeData | None = None
_switch: DPCode | None = None
def __init__(
self,
device: TuyaDevice,
device_manager: TuyaDeviceManager,
) -> None:
"""Init Tuya Fan Device.""" """Init Tuya Fan Device."""
super().__init__(device, device_manager) super().__init__(device, device_manager)
self.ha_preset_modes = [] self._switch = self.find_dpcode(
if DPCode.MODE in self.device.function: (DPCode.SWITCH_FAN, DPCode.SWITCH), prefer_function=True
self.ha_preset_modes = json.loads( )
self.device.function[DPCode.MODE].values
).get("range", [])
# Air purifier fan can be controlled either via the ranged values or via the enum. self._attr_preset_modes = []
# We will always prefer the enumeration if available if enum_type := self.find_dpcode(
# Enum is used for e.g. MEES SmartHIMOX-H06 (DPCode.FAN_MODE, DPCode.MODE), dptype=DPType.ENUM, prefer_function=True
# Range is used for e.g. Concept CA3000
self.air_purifier_speed_range_len = 0
self.air_purifier_speed_range_enum = []
if self.device.category == "kj" and (
DPCode.FAN_SPEED_ENUM in self.device.function
or DPCode.SPEED in self.device.function
): ):
if DPCode.FAN_SPEED_ENUM in self.device.function: self._presets = enum_type
self.dp_code_speed_enum = DPCode.FAN_SPEED_ENUM self._attr_supported_features |= SUPPORT_PRESET_MODE
else: self._attr_preset_modes = enum_type.range
self.dp_code_speed_enum = DPCode.SPEED
data = json.loads(self.device.function[self.dp_code_speed_enum].values).get( # Find speed controls, can be either percentage or a set of speeds
"range" dpcodes = (
) DPCode.FAN_SPEED_PERCENT,
if data: DPCode.FAN_SPEED,
self.air_purifier_speed_range_len = len(data) DPCode.SPEED,
self.air_purifier_speed_range_enum = data DPCode.FAN_SPEED_ENUM,
)
if int_type := self.find_dpcode(
dpcodes, dptype=DPType.INTEGER, prefer_function=True
):
self._attr_supported_features |= SUPPORT_SET_SPEED
self._speed = int_type
elif enum_type := self.find_dpcode(
dpcodes, dptype=DPType.ENUM, prefer_function=True
):
self._attr_supported_features |= SUPPORT_SET_SPEED
self._speeds = enum_type
if dpcode := self.find_dpcode(
(DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL), prefer_function=True
):
self._oscillate = dpcode
self._attr_supported_features |= SUPPORT_OSCILLATE
if enum_type := self.find_dpcode(
DPCode.FAN_DIRECTION, dptype=DPType.ENUM, prefer_function=True
):
self._direction = enum_type
self._attr_supported_features |= SUPPORT_DIRECTION
def set_preset_mode(self, preset_mode: str) -> None: def set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan.""" """Set the preset mode of the fan."""
self._send_command([{"code": DPCode.MODE, "value": preset_mode}]) if self._presets is None:
return
self._send_command([{"code": self._presets.dpcode, "value": preset_mode}])
def set_direction(self, direction: str) -> None: def set_direction(self, direction: str) -> None:
"""Set the direction of the fan.""" """Set the direction of the fan."""
self._send_command([{"code": DPCode.FAN_DIRECTION, "value": direction}]) if self._direction is None:
return
self._send_command([{"code": self._direction.dpcode, "value": direction}])
def set_percentage(self, percentage: int) -> None: def set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage.""" """Set the speed of the fan, as a percentage."""
if self.device.category == "kj": if self._speed is not None:
value_in_range = percentage_to_ordered_list_item(
self.air_purifier_speed_range_enum, percentage
)
self._send_command( self._send_command(
[ [
{ {
"code": self.dp_code_speed_enum, "code": self._speed.dpcode,
"value": value_in_range, "value": self._speed.scale_value_back(percentage),
} }
] ]
) )
else: return
if self._speeds is not None:
self._send_command( self._send_command(
[{"code": DPCode.FAN_SPEED_PERCENT, "value": percentage}] [
{
"code": self._speeds.dpcode,
"value": percentage_to_ordered_list_item(
self._speeds.range, percentage
),
}
]
) )
def turn_off(self, **kwargs: Any) -> None: def turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off.""" """Turn the fan off."""
self._send_command([{"code": DPCode.SWITCH, "value": False}]) self._send_command([{"code": self._switch, "value": False}])
def turn_on( def turn_on(
self, self,
@ -132,84 +167,99 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Turn on the fan.""" """Turn on the fan."""
self._send_command([{"code": DPCode.SWITCH, "value": True}]) if self._switch is None:
return
commands: list[dict[str, str | bool | int]] = [
{"code": self._switch, "value": True}
]
if percentage is not None and self._speed is not None:
commands.append(
{
"code": self._speed.dpcode,
"value": int(self._speed.remap_value_from(percentage, 0, 100)),
}
)
return
if percentage is not None and self._speeds is not None:
commands.append(
{
"code": self._speeds.dpcode,
"value": percentage_to_ordered_list_item(
self._speeds.range, percentage
),
}
)
if preset_mode is not None and self._presets is not None:
commands.append({"code": self._presets.dpcode, "value": preset_mode})
self._send_command(commands)
def oscillate(self, oscillating: bool) -> None: def oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan.""" """Oscillate the fan."""
self._send_command([{"code": DPCode.SWITCH_HORIZONTAL, "value": oscillating}]) if self._oscillate is None:
return
self._send_command([{"code": self._oscillate, "value": oscillating}])
@property @property
def is_on(self) -> bool: def is_on(self) -> bool | None:
"""Return true if fan is on.""" """Return true if fan is on."""
return self.device.status.get(DPCode.SWITCH, False) if self._switch is None:
return None
return self.device.status.get(self._switch)
@property @property
def current_direction(self) -> str: def current_direction(self) -> str | None:
"""Return the current direction of the fan.""" """Return the current direction of the fan."""
if self.device.status[DPCode.FAN_DIRECTION]: if (
self._direction is None
or (value := self.device.status.get(self._direction.dpcode)) is None
):
return None
if value.lower() == DIRECTION_FORWARD:
return DIRECTION_FORWARD return DIRECTION_FORWARD
return DIRECTION_REVERSE
if value.lower() == DIRECTION_REVERSE:
return DIRECTION_REVERSE
return None
@property @property
def oscillating(self) -> bool: def oscillating(self) -> bool | None:
"""Return true if the fan is oscillating.""" """Return true if the fan is oscillating."""
return self.device.status.get(DPCode.SWITCH_HORIZONTAL, False) if self._oscillate is None:
return None
@property return self.device.status.get(self._oscillate)
def preset_modes(self) -> list[str]:
"""Return the list of available preset_modes."""
return self.ha_preset_modes
@property @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:
"""Return the current preset_mode.""" """Return the current preset_mode."""
return self.device.status.get(DPCode.MODE) if self._presets is None:
return None
return self.device.status.get(self._presets.dpcode)
@property @property
def percentage(self) -> int | None: def percentage(self) -> int | None:
"""Return the current speed.""" """Return the current speed."""
if not self.is_on: if self._speed is not None:
return 0 if (value := self.device.status.get(self._speed.dpcode)) is None:
return None
return int(self._speed.remap_value_to(value, 0, 100))
if ( if self._speeds is not None:
self.device.category == "kj" if (value := self.device.status.get(self._speeds.dpcode)) is None:
and self.air_purifier_speed_range_len > 1 return None
and not self.air_purifier_speed_range_enum return ordered_list_item_to_percentage(self._speeds.range, value)
and DPCode.FAN_SPEED_ENUM in self.device.status
):
# if air-purifier speed enumeration is supported we will prefer it.
return ordered_list_item_to_percentage(
self.air_purifier_speed_range_enum,
self.device.status[DPCode.FAN_SPEED_ENUM],
)
# some type may not have the fan_speed_percent key return None
return self.device.status.get(DPCode.FAN_SPEED_PERCENT)
@property @property
def speed_count(self) -> int: def speed_count(self) -> int:
"""Return the number of speeds the fan supports.""" """Return the number of speeds the fan supports."""
if self.device.category == "kj": if self._speeds is not None:
return self.air_purifier_speed_range_len return len(self._speeds.range)
return super().speed_count return 100
@property
def supported_features(self):
"""Flag supported features."""
supports = 0
if DPCode.MODE in self.device.status:
supports |= SUPPORT_PRESET_MODE
if DPCode.FAN_SPEED_PERCENT in self.device.status:
supports |= SUPPORT_SET_SPEED
if DPCode.SWITCH_HORIZONTAL in self.device.status:
supports |= SUPPORT_OSCILLATE
if DPCode.FAN_DIRECTION in self.device.status:
supports |= SUPPORT_DIRECTION
# Air Purifier specific
if (
DPCode.SPEED in self.device.status
or DPCode.FAN_SPEED_ENUM in self.device.status
):
supports |= SUPPORT_SET_SPEED
return supports