From fdc6d9e004d4fcded30063960971b4efb967708a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 18:50:51 +0200 Subject: [PATCH] Add select platform to Tuya (#57674) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 6 +- homeassistant/components/tuya/select.py | 130 ++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/tuya/select.py diff --git a/.coveragerc b/.coveragerc index cb65ae85c2e..1a5380dd0c9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1117,6 +1117,7 @@ omit = homeassistant/components/tuya/fan.py homeassistant/components/tuya/light.py homeassistant/components/tuya/scene.py + homeassistant/components/tuya/select.py homeassistant/components/tuya/switch.py homeassistant/components/twentemilieu/const.py homeassistant/components/twentemilieu/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 0efdb10b6a2..51d92b399e3 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -40,6 +40,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "kg", # Switch "kj", # Air Purifier "kj", # Air Purifier + "kfj", # Coffee maker "kt", # Air conditioner "mcs", # Door Window Sensor "pc", # Power Strip @@ -53,7 +54,7 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( TUYA_SMART_APP = "tuyaSmart" SMARTLIFE_APP = "smartlife" -PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "switch"] +PLATFORMS = ["binary_sensor", "climate", "fan", "light", "scene", "select", "switch"] class DPCode(str, Enum): @@ -68,6 +69,8 @@ class DPCode(str, Enum): CHILD_LOCK = "child_lock" # Child lock COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode + CONCENTRATION_SET = "concentration_set" # Concentration setting + CUP_NUMBER = "cup_number" # NUmber of cups DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor FAN_DIRECTION = "fan_direction" # Fan direction FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode @@ -77,6 +80,7 @@ class DPCode(str, Enum): HUMIDITY_SET = "humidity_set" # Humidity setting LIGHT = "light" # Light LOCK = "lock" # Lock / Child lock + MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py new file mode 100644 index 00000000000..c3b81787086 --- /dev/null +++ b/homeassistant/components/tuya/select.py @@ -0,0 +1,130 @@ +"""Support for Tuya select.""" +from __future__ import annotations + +from typing import cast + +from tuya_iot import TuyaDevice, TuyaDeviceManager +from tuya_iot.device import TuyaDeviceStatusRange + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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 . import HomeAssistantTuyaData +from .base import EnumTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Enum data types in the +# default instructions set of each category end up being a select. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { + # Coffee maker + # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f + "kfj": ( + SelectEntityDescription( + key=DPCode.CUP_NUMBER, + name="Cups", + icon="mdi:numeric", + ), + SelectEntityDescription( + key=DPCode.CONCENTRATION_SET, + name="Concentration", + icon="mdi:altimeter", + ), + SelectEntityDescription( + key=DPCode.MATERIAL, + name="Material", + ), + SelectEntityDescription( + key=DPCode.MODE, + name="Mode", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya select dynamically through Tuya discovery.""" + hass_data: HomeAssistantTuyaData = hass.data[DOMAIN][entry.entry_id] + + @callback + def async_discover_device(device_ids: list[str]) -> None: + """Discover and add a discovered Tuya select.""" + entities: list[TuyaSelectEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := SELECTS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaSelectEntity( + device, hass_data.device_manager, description + ) + ) + + async_add_entities(entities) + + async_discover_device([*hass_data.device_manager.device_map]) + + entry.async_on_unload( + async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device) + ) + + +class TuyaSelectEntity(TuyaEntity, SelectEntity): + """Tuya Select Entity.""" + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: SelectEntityDescription, + ) -> None: + """Init Tuya sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + self._attr_opions: list[str] = [] + if status_range := device.status_range.get(description.key): + self._status_range = cast(TuyaDeviceStatusRange, status_range) + + # Extract type data from enum status range, + if self._status_range.type == "Enum": + type_data = EnumTypeData.from_json(self._status_range.values) + self._attr_options = type_data.range + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + if self.entity_description.name is not None: + return f"{self.tuya_device.name} {self.entity_description.name}" + return self.tuya_device.name + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + # Raw value + value = self.tuya_device.status.get(self.entity_description.key) + if value is None or value not in self._attr_options: + return None + + return value + + def select_option(self, option: str) -> None: + """Change the selected option.""" + self._send_command( + [ + { + "code": self.entity_description.key, + "value": option, + } + ] + )