1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00
Files
core/homeassistant/components/tuya/fan.py
2026-04-01 14:35:12 +00:00

214 lines
7.4 KiB
Python

"""Support for Tuya Fan."""
from __future__ import annotations
from typing import Any
from tuya_device_handlers import TUYA_QUIRKS_REGISTRY
from tuya_device_handlers.definition.fan import (
FanQuirk,
TuyaFanDefinition,
get_default_definition,
)
from tuya_device_handlers.helpers.homeassistant import TuyaFanDirection
from tuya_sharing import CustomerDevice, Manager
from homeassistant.components.fan import (
DIRECTION_FORWARD,
DIRECTION_REVERSE,
FanEntity,
FanEntityDescription,
FanEntityFeature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DeviceCategory
from .entity import TuyaEntity
FANS: dict[DeviceCategory, FanEntityDescription] = {
DeviceCategory.CS: FanEntityDescription(key=""),
DeviceCategory.FS: FanEntityDescription(key=""),
DeviceCategory.FSD: FanEntityDescription(key=""),
DeviceCategory.FSKG: FanEntityDescription(key=""),
DeviceCategory.KJ: FanEntityDescription(key=""),
DeviceCategory.KS: FanEntityDescription(key=""),
}
_TUYA_TO_HA_DIRECTION_MAPPINGS = {
TuyaFanDirection.FORWARD: DIRECTION_FORWARD,
TuyaFanDirection.REVERSE: DIRECTION_REVERSE,
}
_HA_TO_TUYA_DIRECTION_MAPPINGS = {
v: k for k, v in _TUYA_TO_HA_DIRECTION_MAPPINGS.items()
}
def _get_quirk_entity_description(
entity_quirk: FanQuirk,
) -> FanEntityDescription:
return FanEntityDescription(key=entity_quirk.key)
def _get_quirk_entities(
manager: Manager, device: CustomerDevice
) -> list[TuyaFanEntity] | None:
if (quirk := TUYA_QUIRKS_REGISTRY.get_quirk_for_device(device)) is None or (
entity_quirks := quirk.fan_quirks
) is None:
return None
return [
TuyaFanEntity(
device,
manager,
_get_quirk_entity_description(entity_quirk),
definition,
)
for entity_quirk in entity_quirks
if (definition := entity_quirk.definition_fn(device))
]
async def async_setup_entry(
hass: HomeAssistant,
entry: TuyaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up tuya fan dynamically through tuya discovery."""
manager = entry.runtime_data.manager
@callback
def async_discover_device(device_ids: list[str]) -> None:
"""Discover and add a discovered tuya fan."""
entities: list[TuyaFanEntity] = []
for device_id in device_ids:
device = manager.device_map[device_id]
if (quirk_entities := _get_quirk_entities(manager, device)) is not None:
entities.extend(quirk_entities)
continue
if (description := FANS.get(device.category)) and (
definition := get_default_definition(device)
):
entities.append(TuyaFanEntity(device, manager, description, definition))
async_add_entities(entities)
async_discover_device([*manager.device_map])
entry.async_on_unload(
async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
)
class TuyaFanEntity(TuyaEntity, FanEntity):
"""Tuya Fan Device."""
_attr_name = None
def __init__(
self,
device: CustomerDevice,
device_manager: Manager,
description: FanEntityDescription,
definition: TuyaFanDefinition,
) -> None:
"""Init Tuya Fan Device."""
super().__init__(device, device_manager, description)
self._direction_wrapper = definition.direction_wrapper
self._mode_wrapper = definition.mode_wrapper
self._oscillate_wrapper = definition.oscillate_wrapper
self._speed_wrapper = definition.speed_wrapper
self._switch_wrapper = definition.switch_wrapper
if definition.mode_wrapper:
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
self._attr_preset_modes = definition.mode_wrapper.options
if definition.speed_wrapper:
self._attr_supported_features |= FanEntityFeature.SET_SPEED
# if speed is from an enum, set speed count from options
# else keep entity default 100
if hasattr(definition.speed_wrapper, "options"):
self._attr_speed_count = len(definition.speed_wrapper.options)
if definition.oscillate_wrapper:
self._attr_supported_features |= FanEntityFeature.OSCILLATE
if definition.direction_wrapper:
self._attr_supported_features |= FanEntityFeature.DIRECTION
if definition.switch_wrapper:
self._attr_supported_features |= (
FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode of the fan."""
await self._async_send_wrapper_updates(self._mode_wrapper, preset_mode)
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
if tuya_value := _HA_TO_TUYA_DIRECTION_MAPPINGS.get(direction):
await self._async_send_wrapper_updates(self._direction_wrapper, tuya_value)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
await self._async_send_wrapper_updates(self._speed_wrapper, percentage)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the fan off."""
await self._async_send_wrapper_updates(self._switch_wrapper, False)
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the fan."""
if self._switch_wrapper is None:
return
commands = self._switch_wrapper.get_update_commands(self.device, True)
if percentage is not None and self._speed_wrapper is not None:
commands.extend(
self._speed_wrapper.get_update_commands(self.device, percentage)
)
if preset_mode is not None and self._mode_wrapper:
commands.extend(
self._mode_wrapper.get_update_commands(self.device, preset_mode)
)
await self._async_send_commands(commands)
async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
await self._async_send_wrapper_updates(self._oscillate_wrapper, oscillating)
@property
def is_on(self) -> bool | None:
"""Return true if fan is on."""
return self._read_wrapper(self._switch_wrapper)
@property
def current_direction(self) -> str | None:
"""Return the current direction of the fan."""
tuya_value = self._read_wrapper(self._direction_wrapper)
return _TUYA_TO_HA_DIRECTION_MAPPINGS.get(tuya_value) if tuya_value else None
@property
def oscillating(self) -> bool | None:
"""Return true if the fan is oscillating."""
return self._read_wrapper(self._oscillate_wrapper)
@property
def preset_mode(self) -> str | None:
"""Return the current preset_mode."""
return self._read_wrapper(self._mode_wrapper)
@property
def percentage(self) -> int | None:
"""Return the current speed."""
return self._read_wrapper(self._speed_wrapper)