"""Support for Tuya number.""" from __future__ import annotations from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.number import ( NumberDeviceClass, NumberEntity, NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import IntegerTypeData, TuyaEntity from .const import DEVICE_CLASS_UNITS, DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType # 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, ...]] = { # Multi-functional Sensor # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 "dgnbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, name="Time", entity_category=EntityCategory.CONFIG, ), ), # Smart Kettle # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 "bh": ( NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET_F, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), ), # Smart Pet Feeder # https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld "cwwsq": ( NumberEntityDescription( key=DPCode.MANUAL_FEED, name="Feed", icon="mdi:bowl", ), NumberEntityDescription( key=DPCode.VOICE_TIMES, name="Voice times", icon="mdi:microphone", ), ), # Human Presence Sensor # https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs "hps": ( NumberEntityDescription( key=DPCode.SENSITIVITY, name="Sensitivity", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, name="Near detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, name="Far detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), ), # 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_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.POWDER_SET, name="Powder", entity_category=EntityCategory.CONFIG, ), ), # Sous Vide Cooker # https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux "mzj": ( NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, name="Cook temperature", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, name="Cook time", icon="mdi:timer", native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLOUD_RECIPE_NUMBER, name="Cloud recipe", entity_category=EntityCategory.CONFIG, ), ), # Robot Vacuum # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo "sd": ( NumberEntityDescription( key=DPCode.VOLUME_SET, name="Volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( NumberEntityDescription( key=DPCode.ALARM_TIME, name="Time", entity_category=EntityCategory.CONFIG, ), ), # Smart Camera # https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12 "sp": ( NumberEntityDescription( key=DPCode.BASIC_DEVICE_VOLUME, name="Volume", icon="mdi:volume-high", entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgkg": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, name="Minimum brightness 3", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, name="Maximum brightness 3", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), ), # Dimmer Switch # https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o "tgq": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), ), # Vibration Sensor # https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno "zd": ( NumberEntityDescription( key=DPCode.SENSITIVITY, name="Sensitivity", entity_category=EntityCategory.CONFIG, ), ), # Fingerbot "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, name="Move down %", icon="mdi:arrow-down-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, name="Move up %", icon="mdi:arrow-up-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, name="Down delay", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), ), # Fan # https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c "fs": ( NumberEntityDescription( key=DPCode.TEMP, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), ), # Humidifier # https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b "jsq": ( NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), NumberEntityDescription( key=DPCode.TEMP_SET_F, name="Temperature", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), ), } 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.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.""" _number: 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 int_type := self.find_dpcode( description.key, dptype=DPType.INTEGER, prefer_function=True ): self._number = int_type self._attr_native_max_value = self._number.max_scaled self._attr_native_min_value = self._number.min_scaled self._attr_native_step = self._number.step_scaled # Logic to ensure the set device class and API received Unit Of Measurement # match Home Assistants requirements. if ( self.device_class is not None and not self.device_class.startswith(DOMAIN) and description.native_unit_of_measurement is None ): # We cannot have a device class, if the UOM isn't set or the # device class cannot be found in the validation mapping. if ( self.native_unit_of_measurement is None or self.device_class not in DEVICE_CLASS_UNITS ): self._attr_device_class = None return uoms = DEVICE_CLASS_UNITS[self.device_class] self._uom = uoms.get(self.native_unit_of_measurement) or uoms.get( self.native_unit_of_measurement.lower() ) # Unknown unit of measurement, device class should not be used. if self._uom is None: self._attr_device_class = None return # If we still have a device class, we should not use an icon if self.device_class: self._attr_icon = None # Found unit of measurement, use the standardized Unit # Use the target conversion unit (if set) self._attr_native_unit_of_measurement = ( self._uom.conversion_unit or self._uom.unit ) @property def native_value(self) -> float | None: """Return the entity value to represent the entity state.""" # Unknown or unsupported data type if self._number is None: return None # Raw value if not (value := self.device.status.get(self.entity_description.key)): return None return self._number.scale_value(value) def set_native_value(self, value: float) -> None: """Set new value.""" if self._number is None: raise RuntimeError("Cannot set value, device doesn't provide type data") self._send_command( [ { "code": self.entity_description.key, "value": self._number.scale_value_back(value), } ] )