261 lines
8.5 KiB
Python
261 lines
8.5 KiB
Python
"""Support for Tuya Fan."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any
|
|
|
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
|
|
|
from homeassistant.components.fan import (
|
|
DIRECTION_FORWARD,
|
|
DIRECTION_REVERSE,
|
|
DOMAIN as DEVICE_DOMAIN,
|
|
SUPPORT_DIRECTION,
|
|
SUPPORT_OSCILLATE,
|
|
SUPPORT_PRESET_MODE,
|
|
SUPPORT_SET_SPEED,
|
|
FanEntity,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.util.percentage import (
|
|
ordered_list_item_to_percentage,
|
|
percentage_to_ordered_list_item,
|
|
)
|
|
|
|
from .base import TuyaHaEntity
|
|
from .const import (
|
|
DOMAIN,
|
|
TUYA_DEVICE_MANAGER,
|
|
TUYA_DISCOVERY_NEW,
|
|
TUYA_HA_DEVICES,
|
|
TUYA_HA_TUYA_MAP,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
# Fan
|
|
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45vs7vkge
|
|
DPCODE_SWITCH = "switch"
|
|
DPCODE_FAN_SPEED = "fan_speed_percent"
|
|
DPCODE_MODE = "mode"
|
|
DPCODE_SWITCH_HORIZONTAL = "switch_horizontal"
|
|
DPCODE_FAN_DIRECTION = "fan_direction"
|
|
|
|
# Air Purifier
|
|
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r41mn81
|
|
DPCODE_AP_FAN_SPEED = "speed"
|
|
DPCODE_AP_FAN_SPEED_ENUM = "fan_speed_enum"
|
|
|
|
TUYA_SUPPORT_TYPE = {
|
|
"fs", # Fan
|
|
"kj", # Air Purifier
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
|
):
|
|
"""Set up tuya fan dynamically through tuya discovery."""
|
|
_LOGGER.debug("fan init")
|
|
|
|
hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][
|
|
DEVICE_DOMAIN
|
|
] = TUYA_SUPPORT_TYPE
|
|
|
|
@callback
|
|
def async_discover_device(dev_ids: list[str]) -> None:
|
|
"""Discover and add a discovered tuya fan."""
|
|
_LOGGER.debug("fan add-> %s", dev_ids)
|
|
if not dev_ids:
|
|
return
|
|
entities = _setup_entities(hass, entry, dev_ids)
|
|
async_add_entities(entities)
|
|
|
|
entry.async_on_unload(
|
|
async_dispatcher_connect(
|
|
hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device
|
|
)
|
|
)
|
|
|
|
device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER]
|
|
device_ids = []
|
|
for (device_id, device) in device_manager.device_map.items():
|
|
if device.category in TUYA_SUPPORT_TYPE:
|
|
device_ids.append(device_id)
|
|
async_discover_device(device_ids)
|
|
|
|
|
|
def _setup_entities(
|
|
hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str]
|
|
) -> list[TuyaHaFan]:
|
|
"""Set up Tuya Fan."""
|
|
device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER]
|
|
entities = []
|
|
for device_id in device_ids:
|
|
device = device_manager.device_map[device_id]
|
|
if device is None:
|
|
continue
|
|
entities.append(TuyaHaFan(device, device_manager))
|
|
hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id)
|
|
return entities
|
|
|
|
|
|
class TuyaHaFan(TuyaHaEntity, FanEntity):
|
|
"""Tuya Fan Device."""
|
|
|
|
def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
|
|
"""Init Tuya Fan Device."""
|
|
super().__init__(device, device_manager)
|
|
|
|
self.ha_preset_modes = []
|
|
if DPCODE_MODE in self.tuya_device.function:
|
|
self.ha_preset_modes = json.loads(
|
|
self.tuya_device.function[DPCODE_MODE].values
|
|
).get("range", [])
|
|
|
|
# Air purifier fan can be controlled either via the ranged values or via the enum.
|
|
# We will always prefer the enumeration if available
|
|
# Enum is used for e.g. MEES SmartHIMOX-H06
|
|
# Range is used for e.g. Concept CA3000
|
|
self.air_purifier_speed_range_len = 0
|
|
self.air_purifier_speed_range_enum = []
|
|
if self.tuya_device.category == "kj" and (
|
|
DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.function
|
|
or DPCODE_AP_FAN_SPEED in self.tuya_device.function
|
|
):
|
|
if DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.function:
|
|
self.dp_code_speed_enum = DPCODE_AP_FAN_SPEED_ENUM
|
|
else:
|
|
self.dp_code_speed_enum = DPCODE_AP_FAN_SPEED
|
|
|
|
data = json.loads(
|
|
self.tuya_device.function[self.dp_code_speed_enum].values
|
|
).get("range")
|
|
if data:
|
|
self.air_purifier_speed_range_len = len(data)
|
|
self.air_purifier_speed_range_enum = data
|
|
|
|
def set_preset_mode(self, preset_mode: str) -> None:
|
|
"""Set the preset mode of the fan."""
|
|
self._send_command([{"code": DPCODE_MODE, "value": preset_mode}])
|
|
|
|
def set_direction(self, direction: str) -> None:
|
|
"""Set the direction of the fan."""
|
|
self._send_command([{"code": DPCODE_FAN_DIRECTION, "value": direction}])
|
|
|
|
def set_percentage(self, percentage: int) -> None:
|
|
"""Set the speed of the fan, as a percentage."""
|
|
if self.tuya_device.category == "kj":
|
|
value_in_range = percentage_to_ordered_list_item(
|
|
self.air_purifier_speed_range_enum, percentage
|
|
)
|
|
self._send_command(
|
|
[
|
|
{
|
|
"code": self.dp_code_speed_enum,
|
|
"value": value_in_range,
|
|
}
|
|
]
|
|
)
|
|
else:
|
|
self._send_command([{"code": DPCODE_FAN_SPEED, "value": percentage}])
|
|
|
|
def turn_off(self, **kwargs: Any) -> None:
|
|
"""Turn the fan off."""
|
|
self._send_command([{"code": DPCODE_SWITCH, "value": False}])
|
|
|
|
def turn_on(
|
|
self,
|
|
speed: str = None,
|
|
percentage: int = None,
|
|
preset_mode: str = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Turn on the fan."""
|
|
self._send_command([{"code": DPCODE_SWITCH, "value": True}])
|
|
|
|
def oscillate(self, oscillating: bool) -> None:
|
|
"""Oscillate the fan."""
|
|
self._send_command([{"code": DPCODE_SWITCH_HORIZONTAL, "value": oscillating}])
|
|
|
|
@property
|
|
def is_on(self) -> bool:
|
|
"""Return true if fan is on."""
|
|
return self.tuya_device.status.get(DPCODE_SWITCH, False)
|
|
|
|
@property
|
|
def current_direction(self) -> str:
|
|
"""Return the current direction of the fan."""
|
|
if self.tuya_device.status[DPCODE_FAN_DIRECTION]:
|
|
return DIRECTION_FORWARD
|
|
return DIRECTION_REVERSE
|
|
|
|
@property
|
|
def oscillating(self) -> bool:
|
|
"""Return true if the fan is oscillating."""
|
|
return self.tuya_device.status.get(DPCODE_SWITCH_HORIZONTAL, False)
|
|
|
|
@property
|
|
def preset_modes(self) -> list[str]:
|
|
"""Return the list of available preset_modes."""
|
|
return self.ha_preset_modes
|
|
|
|
@property
|
|
def preset_mode(self) -> str:
|
|
"""Return the current preset_mode."""
|
|
return self.tuya_device.status[DPCODE_MODE]
|
|
|
|
@property
|
|
def percentage(self) -> int | None:
|
|
"""Return the current speed."""
|
|
if not self.is_on:
|
|
return 0
|
|
|
|
if (
|
|
self.tuya_device.category == "kj"
|
|
and self.air_purifier_speed_range_len > 1
|
|
and not self.air_purifier_speed_range_enum
|
|
and DPCODE_AP_FAN_SPEED_ENUM in self.tuya_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.tuya_device.status[DPCODE_AP_FAN_SPEED_ENUM],
|
|
)
|
|
|
|
# some type may not have the fan_speed_percent key
|
|
return self.tuya_device.status.get(DPCODE_FAN_SPEED)
|
|
|
|
@property
|
|
def speed_count(self) -> int:
|
|
"""Return the number of speeds the fan supports."""
|
|
if self.tuya_device.category == "kj":
|
|
return self.air_purifier_speed_range_len
|
|
return super().speed_count
|
|
|
|
@property
|
|
def supported_features(self):
|
|
"""Flag supported features."""
|
|
supports = 0
|
|
if DPCODE_MODE in self.tuya_device.status:
|
|
supports |= SUPPORT_PRESET_MODE
|
|
if DPCODE_FAN_SPEED in self.tuya_device.status:
|
|
supports |= SUPPORT_SET_SPEED
|
|
if DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status:
|
|
supports |= SUPPORT_OSCILLATE
|
|
if DPCODE_FAN_DIRECTION in self.tuya_device.status:
|
|
supports |= SUPPORT_DIRECTION
|
|
|
|
# Air Purifier specific
|
|
if (
|
|
DPCODE_AP_FAN_SPEED in self.tuya_device.status
|
|
or DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.status
|
|
):
|
|
supports |= SUPPORT_SET_SPEED
|
|
return supports
|