From 0407a56fdfca0a36bda64d2c514e1dd9c97b28cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Oct 2021 22:15:41 +0200 Subject: [PATCH] Add number platform to Tuya (#57672) --- .coveragerc | 1 + homeassistant/components/tuya/const.py | 7 +- homeassistant/components/tuya/number.py | 151 ++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/tuya/number.py diff --git a/.coveragerc b/.coveragerc index 6274e8526fc..91f4d43c702 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1116,6 +1116,7 @@ omit = homeassistant/components/tuya/const.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/light.py + homeassistant/components/tuya/number.py homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py homeassistant/components/tuya/sensor.py diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index fa66d72da4f..56f5b584121 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -37,9 +37,9 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "fs", # Fan "fwl", # Ambient light "jsq", # Humidifier's light + "kfj", # Coffee Maker "kg", # Switch "kj", # Air Purifier - "kj", # Air Purifier "kfj", # Coffee maker "kt", # Air conditioner "mcs", # Door Window Sensor @@ -48,7 +48,6 @@ TUYA_SUPPORTED_PRODUCT_CATEGORIES = ( "wk", # Thermostat "xdd", # Ceiling Light "xxj", # Diffuser - "xxj", # Diffuser's light ) TUYA_SMART_APP = "tuyaSmart" @@ -59,6 +58,7 @@ PLATFORMS = [ "climate", "fan", "light", + "number", "scene", "select", "sensor", @@ -96,6 +96,7 @@ class DPCode(str, Enum): LOCK = "lock" # Lock / Child lock MATERIAL = "material" # Material MODE = "mode" # Working mode / Mode + POWDER_SET = "powder_set" # Powder PUMP_RESET = "pump_reset" # Water pump reset SHAKE = "shake" # Oscillating SPEED = "speed" # Speed level @@ -129,7 +130,9 @@ class DPCode(str, Enum): TEMPER_ALARM = "temper_alarm" # Tamper alarm UV = "uv" # UV sterilization WARM = "warm" # Heat preservation + WARM_TIME = "warm_time" # Heat preservation time WATER_RESET = "water_reset" # Resetting of water usage days + WATER_SET = "water_set" # Water level WET = "wet" # Humidification WORK_MODE = "work_mode" # Working mode diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py new file mode 100644 index 00000000000..977917f8b0c --- /dev/null +++ b/homeassistant/components/tuya/number.py @@ -0,0 +1,151 @@ +"""Support for Tuya number.""" +from __future__ import annotations + +from typing import cast + +from tuya_iot import TuyaDevice, TuyaDeviceManager +from tuya_iot.device import TuyaDeviceStatusRange + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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 IntegerTypeData, TuyaEntity +from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode + +# All descriptions can be found here. Mostly the Integer data types in the +# default instructions set of each category end up being a number. +# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq +NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { + # Coffee maker + # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f + "kfj": ( + NumberEntityDescription( + key=DPCode.WATER_SET, + name="Water Level", + icon="mdi:cup-water", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.TEMP_SET, + name="Temperature", + icon="mdi:thermometer", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.WARM_TIME, + name="Heat Preservation Time", + icon="mdi:timer", + entity_registry_enabled_default=False, + ), + NumberEntityDescription( + key=DPCode.POWDER_SET, + name="Powder", + entity_registry_enabled_default=False, + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Tuya number 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 number.""" + entities: list[TuyaNumberEntity] = [] + for device_id in device_ids: + device = hass_data.device_manager.device_map[device_id] + if descriptions := NUMBERS.get(device.category): + for description in descriptions: + if ( + description.key in device.function + or description.key in device.status + ): + entities.append( + TuyaNumberEntity( + 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 TuyaNumberEntity(TuyaEntity, NumberEntity): + """Tuya Number Entity.""" + + _status_range: TuyaDeviceStatusRange | None = None + _type_data: IntegerTypeData | None = None + + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + description: NumberEntityDescription, + ) -> None: + """Init Tuya sensor.""" + super().__init__(device, device_manager) + self.entity_description = description + self._attr_unique_id = f"{super().unique_id}{description.key}" + + if status_range := device.status_range.get(description.key): + self._status_range = cast(TuyaDeviceStatusRange, status_range) + + # Extract type data from integer status range, + # and determine unit of measurement + if self._status_range.type == "Integer": + self._type_data = IntegerTypeData.from_json(self._status_range.values) + self._attr_max_value = self._type_data.max + self._attr_min_value = self._type_data.min + self._attr_step = self._type_data.step + if description.unit_of_measurement is None: + self._attr_unit_of_measurement = self._type_data.unit + + @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 value(self) -> float | None: + """Return the entity value to represent the entity state.""" + # Unknown or unsupported data type + if self._status_range is None or self._status_range.type != "Integer": + return None + + # Raw value + value = self.tuya_device.status.get(self.entity_description.key) + + # Scale integer/float value + if value and isinstance(self._type_data, IntegerTypeData): + return self.scale(value, self._type_data.scale) + + return None + + def set_value(self, value: float) -> None: + """Set new value.""" + if self._type_data is None: + raise RuntimeError("Cannot set value, device doesn't provide type data") + + self._send_command( + [ + { + "code": self.entity_description.key, + "value": int(self.scale(value, self._type_data.scale)), + } + ] + )