diff --git a/.coveragerc b/.coveragerc index e22611edf51..a5d7eec9115 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1115,9 +1115,9 @@ omit = homeassistant/components/transmission/errors.py homeassistant/components/travisci/sensor.py homeassistant/components/tuya/__init__.py + homeassistant/components/tuya/base.py homeassistant/components/tuya/climate.py homeassistant/components/tuya/const.py - homeassistant/components/tuya/cover.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/light.py homeassistant/components/tuya/scene.py diff --git a/CODEOWNERS b/CODEOWNERS index 84b359182dd..515aec64774 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -544,7 +544,7 @@ homeassistant/components/trafikverket_train/* @endor-force homeassistant/components/trafikverket_weatherstation/* @endor-force homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli -homeassistant/components/tuya/* @ollo69 +homeassistant/components/tuya/* @Tuya homeassistant/components/twentemilieu/* @frenck homeassistant/components/twinkly/* @dr1rrb homeassistant/components/ubus/* @noltari diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 7a639665948..c59e29ba348 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,376 +1,208 @@ +#!/usr/bin/env python3 """Support for Tuya Smart devices.""" -from datetime import timedelta + +import itertools import logging -from tuyaha import TuyaApi -from tuyaha.tuyaapi import ( - TuyaAPIException, - TuyaAPIRateLimitException, - TuyaFrequentlyInvokeException, - TuyaNetException, - TuyaServerException, +from tuya_iot import ( + ProjectType, + TuyaDevice, + TuyaDeviceListener, + TuyaDeviceManager, + TuyaHomeManager, + TuyaOpenAPI, + TuyaOpenMQ, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_PLATFORM, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( - CONF_COUNTRYCODE, - CONF_DISCOVERY_INTERVAL, - CONF_QUERY_DEVICE, - CONF_QUERY_INTERVAL, - DEFAULT_DISCOVERY_INTERVAL, - DEFAULT_QUERY_INTERVAL, + CONF_ACCESS_ID, + CONF_ACCESS_SECRET, + CONF_APP_TYPE, + CONF_COUNTRY_CODE, + CONF_ENDPOINT, + CONF_PASSWORD, + CONF_PROJECT_TYPE, + CONF_USERNAME, DOMAIN, - SIGNAL_CONFIG_ENTITY, - SIGNAL_DELETE_ENTITY, - SIGNAL_UPDATE_ENTITY, - TUYA_DATA, - TUYA_DEVICES_CONF, + PLATFORMS, + TUYA_DEVICE_MANAGER, TUYA_DISCOVERY_NEW, - TUYA_PLATFORMS, - TUYA_TYPE_NOT_QUERY, + TUYA_HA_DEVICES, + TUYA_HA_SIGNAL_UPDATE_ENTITY, + TUYA_HA_TUYA_MAP, + TUYA_HOME_MANAGER, + TUYA_MQTT_LISTENER, ) _LOGGER = logging.getLogger(__name__) -ATTR_TUYA_DEV_ID = "tuya_device_id" -ENTRY_IS_SETUP = "tuya_entry_is_setup" - -SERVICE_FORCE_UPDATE = "force_update" -SERVICE_PULL_DEVICES = "pull_devices" - -TUYA_TYPE_TO_HA = { - "climate": "climate", - "cover": "cover", - "fan": "fan", - "light": "light", - "scene": "scene", - "switch": "switch", -} - -TUYA_TRACKER = "tuya_tracker" -STOP_CANCEL = "stop_event_cancel" - -CONFIG_SCHEMA = cv.deprecated(DOMAIN) - - -def _update_discovery_interval(hass, interval): - tuya = hass.data[DOMAIN].get(TUYA_DATA) - if not tuya: - return - - try: - tuya.discovery_interval = interval - _LOGGER.info("Tuya discovery device poll interval set to %s seconds", interval) - except ValueError as ex: - _LOGGER.warning(ex) - - -def _update_query_interval(hass, interval): - tuya = hass.data[DOMAIN].get(TUYA_DATA) - if not tuya: - return - - try: - tuya.query_interval = interval - _LOGGER.info("Tuya query device poll interval set to %s seconds", interval) - except ValueError as ex: - _LOGGER.warning(ex) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Tuya platform.""" + """Async setup hass config entry.""" - tuya = TuyaApi() - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - country_code = entry.data[CONF_COUNTRYCODE] - platform = entry.data[CONF_PLATFORM] + _LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data) - try: - await hass.async_add_executor_job( - tuya.init, username, password, country_code, platform - ) - except ( - TuyaNetException, - TuyaServerException, - TuyaFrequentlyInvokeException, - ) as exc: - raise ConfigEntryNotReady() from exc + hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} - except TuyaAPIRateLimitException as exc: - raise ConfigEntryNotReady("Tuya login rate limited") from exc - - except TuyaAPIException as exc: - _LOGGER.error( - "Connection error during integration setup. Error: %s", - exc, - ) + success = await _init_tuya_sdk(hass, entry) + if not success: return False - domain_data = hass.data[DOMAIN] = { - TUYA_DATA: tuya, - TUYA_DEVICES_CONF: entry.options.copy(), - TUYA_TRACKER: None, - ENTRY_IS_SETUP: set(), - "entities": {}, - "pending": {}, - "listener": entry.add_update_listener(update_listener), - } - - _update_discovery_interval( - hass, entry.options.get(CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL) - ) - - _update_query_interval( - hass, entry.options.get(CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL) - ) - - async def async_load_devices(device_list): - """Load new devices by device_list.""" - device_type_list = {} - for device in device_list: - dev_type = device.device_type() - if ( - dev_type in TUYA_TYPE_TO_HA - and device.object_id() not in domain_data["entities"] - ): - ha_type = TUYA_TYPE_TO_HA[dev_type] - if ha_type not in device_type_list: - device_type_list[ha_type] = [] - device_type_list[ha_type].append(device.object_id()) - domain_data["entities"][device.object_id()] = None - - for ha_type, dev_ids in device_type_list.items(): - config_entries_key = f"{ha_type}.tuya" - if config_entries_key not in domain_data[ENTRY_IS_SETUP]: - domain_data["pending"][ha_type] = dev_ids - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, ha_type) - ) - domain_data[ENTRY_IS_SETUP].add(config_entries_key) - else: - async_dispatcher_send(hass, TUYA_DISCOVERY_NEW.format(ha_type), dev_ids) - - await async_load_devices(tuya.get_all_devices()) - - def _get_updated_devices(): - try: - tuya.poll_devices_update() - except TuyaFrequentlyInvokeException as exc: - _LOGGER.error(exc) - return tuya.get_all_devices() - - async def async_poll_devices_update(event_time): - """Check if accesstoken is expired and pull device list from server.""" - _LOGGER.debug("Pull devices from Tuya") - # Add new discover device. - device_list = await hass.async_add_executor_job(_get_updated_devices) - await async_load_devices(device_list) - # Delete not exist device. - newlist_ids = [] - for device in device_list: - newlist_ids.append(device.object_id()) - for dev_id in list(domain_data["entities"]): - if dev_id not in newlist_ids: - async_dispatcher_send(hass, SIGNAL_DELETE_ENTITY, dev_id) - domain_data["entities"].pop(dev_id) - - domain_data[TUYA_TRACKER] = async_track_time_interval( - hass, async_poll_devices_update, timedelta(minutes=2) - ) - - @callback - def _async_cancel_tuya_tracker(event): - domain_data[TUYA_TRACKER]() # pylint: disable=not-callable - - domain_data[STOP_CANCEL] = hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, _async_cancel_tuya_tracker - ) - - hass.services.async_register( - DOMAIN, SERVICE_PULL_DEVICES, async_poll_devices_update - ) - - async def async_force_update(call): - """Force all devices to pull data.""" - async_dispatcher_send(hass, SIGNAL_UPDATE_ENTITY) - - hass.services.async_register(DOMAIN, SERVICE_FORCE_UPDATE, async_force_update) - return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: + project_type = ProjectType(entry.data[CONF_PROJECT_TYPE]) + api = TuyaOpenAPI( + entry.data[CONF_ENDPOINT], + entry.data[CONF_ACCESS_ID], + entry.data[CONF_ACCESS_SECRET], + project_type, + ) + + api.set_dev_channel("hass") + + if project_type == ProjectType.INDUSTY_SOLUTIONS: + response = await hass.async_add_executor_job( + api.login, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) + else: + response = await hass.async_add_executor_job( + api.login, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_COUNTRY_CODE], + entry.data[CONF_APP_TYPE], + ) + + if response.get("success", False) is False: + _LOGGER.error("Tuya login error response: %s", response) + return False + + tuya_mq = TuyaOpenMQ(api) + tuya_mq.start() + + device_manager = TuyaDeviceManager(api, tuya_mq) + + # Get device list + home_manager = TuyaHomeManager(api, tuya_mq, device_manager) + await hass.async_add_executor_job(home_manager.update_device_cache) + hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] = home_manager + + listener = DeviceListener(hass, entry) + hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] = listener + device_manager.add_device_listener(listener) + hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] = device_manager + + # Clean up device entities + await cleanup_device_registry(hass, entry) + + _LOGGER.debug("init support type->%s", PLATFORMS) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def cleanup_device_registry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove deleted device registry entry if there are no remaining entities.""" + + device_registry_object = device_registry.async_get(hass) + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] + + for dev_id, device_entry in list(device_registry_object.devices.items()): + for item in device_entry.identifiers: + if DOMAIN == item[0] and item[1] not in device_manager.device_map: + device_registry_object.async_remove_device(dev_id) + break + + +@callback +def async_remove_hass_device(hass: HomeAssistant, device_id: str) -> None: + """Remove device from hass cache.""" + device_registry_object = device_registry.async_get(hass) + for device_entry in list(device_registry_object.devices.values()): + if device_id in list(device_entry.identifiers)[0]: + device_registry_object.async_remove_device(device_entry.id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" - domain_data = hass.data[DOMAIN] - platforms = [platform.split(".", 1)[0] for platform in domain_data[ENTRY_IS_SETUP]] - unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms) - if unload_ok: - domain_data["listener"]() - domain_data[STOP_CANCEL]() - domain_data[TUYA_TRACKER]() - hass.services.async_remove(DOMAIN, SERVICE_FORCE_UPDATE) - hass.services.async_remove(DOMAIN, SERVICE_PULL_DEVICES) + _LOGGER.debug("integration unload") + unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload: + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] + device_manager.mq.stop() + device_manager.remove_device_listener( + hass.data[DOMAIN][entry.entry_id][TUYA_MQTT_LISTENER] + ) + hass.data.pop(DOMAIN) - return unload_ok + return unload -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): - """Update when config_entry options update.""" - hass.data[DOMAIN][TUYA_DEVICES_CONF] = entry.options.copy() - _update_discovery_interval( - hass, entry.options.get(CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL) - ) - _update_query_interval( - hass, entry.options.get(CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL) - ) - async_dispatcher_send(hass, SIGNAL_CONFIG_ENTITY) +class DeviceListener(TuyaDeviceListener): + """Device Update Listener.""" + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Init DeviceListener.""" -async def cleanup_device_registry(hass: HomeAssistant, device_id): - """Remove device registry entry if there are no remaining entities.""" + self.hass = hass + self.entry = entry - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() - if device_id and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True - ): - device_registry.async_remove_device(device_id) - - -class TuyaDevice(Entity): - """Tuya base device.""" - - _dev_can_query_count = 0 - - def __init__(self, tuya, platform): - """Init Tuya devices.""" - self._tuya = tuya - self._tuya_platform = platform - - def _device_can_query(self): - """Check if device can also use query method.""" - dev_type = self._tuya.device_type() - return dev_type not in TUYA_TYPE_NOT_QUERY - - def _inc_device_count(self): - """Increment static variable device count.""" - if not self._device_can_query(): - return - TuyaDevice._dev_can_query_count += 1 - - def _dec_device_count(self): - """Decrement static variable device count.""" - if not self._device_can_query(): - return - TuyaDevice._dev_can_query_count -= 1 - - def _get_device_config(self): - """Get updated device options.""" - devices_config = self.hass.data[DOMAIN].get(TUYA_DEVICES_CONF) - if not devices_config: - return {} - dev_conf = devices_config.get(self.object_id, {}) - if dev_conf: + def update_device(self, device: TuyaDevice) -> None: + """Update device status.""" + if device.id in self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES]: _LOGGER.debug( - "Configuration for deviceID %s: %s", self.object_id, str(dev_conf) + "_update-->%s;->>%s", + self, + device.id, ) - return dev_conf + dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}") - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self.hass.data[DOMAIN]["entities"][self.object_id] = self.entity_id - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_DELETE_ENTITY, self._delete_callback - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_ENTITY, self._update_callback - ) - ) - self._inc_device_count() + def add_device(self, device: TuyaDevice) -> None: + """Add device added listener.""" + device_add = False - async def async_will_remove_from_hass(self): - """Call when entity is removed from hass.""" - self._dec_device_count() + if device.category in itertools.chain( + *self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP].values() + ): + ha_tuya_map = self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_TUYA_MAP] + self.hass.add_job(async_remove_hass_device, self.hass, device.id) - @property - def object_id(self): - """Return Tuya device id.""" - return self._tuya.object_id() + for domain, tuya_list in ha_tuya_map.items(): + if device.category in tuya_list: + device_add = True + _LOGGER.debug( + "Add device category->%s; domain-> %s", + device.category, + domain, + ) + self.hass.data[DOMAIN][self.entry.entry_id][TUYA_HA_DEVICES].add( + device.id + ) + dispatcher_send( + self.hass, TUYA_DISCOVERY_NEW.format(domain), [device.id] + ) - @property - def unique_id(self): - """Return a unique ID.""" - return f"tuya.{self._tuya.object_id()}" + if device_add: + device_manager = self.hass.data[DOMAIN][self.entry.entry_id][ + TUYA_DEVICE_MANAGER + ] + device_manager.mq.stop() + tuya_mq = TuyaOpenMQ(device_manager.api) + tuya_mq.start() - @property - def name(self): - """Return Tuya device name.""" - return self._tuya.name() + device_manager.mq = tuya_mq + tuya_mq.add_message_listener(device_manager.on_message) - @property - def available(self): - """Return if the device is available.""" - return self._tuya.available() - - @property - def device_info(self): - """Return a device description for device registry.""" - _device_info = { - "identifiers": {(DOMAIN, f"{self.unique_id}")}, - "manufacturer": TUYA_PLATFORMS.get( - self._tuya_platform, self._tuya_platform - ), - "name": self.name, - "model": self._tuya.object_type(), - } - return _device_info - - def update(self): - """Refresh Tuya device data.""" - query_dev = self.hass.data[DOMAIN][TUYA_DEVICES_CONF].get(CONF_QUERY_DEVICE, "") - use_discovery = ( - TuyaDevice._dev_can_query_count > 1 and self.object_id != query_dev - ) - try: - self._tuya.update(use_discovery=use_discovery) - except TuyaFrequentlyInvokeException as exc: - _LOGGER.error(exc) - - async def _delete_callback(self, dev_id): - """Remove this entity.""" - if dev_id == self.object_id: - entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) - if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) - entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) - else: - await self.async_remove(force_remove=True) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) + def remove_device(self, device_id: str) -> None: + """Add device removed listener.""" + _LOGGER.debug("tuya remove device:%s", device_id) + self.hass.add_job(async_remove_hass_device, self.hass, device_id) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py new file mode 100644 index 00000000000..572c452a920 --- /dev/null +++ b/homeassistant/components/tuya/base.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Tuya Home Assistant Base Device Model.""" +from __future__ import annotations + +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY + + +class TuyaHaEntity(Entity): + """Tuya base device.""" + + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + """Init TuyaHaEntity.""" + super().__init__() + + self.tuya_device = device + self.tuya_device_manager = device_manager + + @staticmethod + def remap(old_value, old_min, old_max, new_min, new_max): + """Remap old_value to new_value.""" + new_value = ((old_value - old_min) / (old_max - old_min)) * ( + new_max - new_min + ) + new_min + return new_value + + @property + def should_poll(self) -> bool: + """Hass should not poll.""" + return False + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"tuya.{self.tuya_device.id}" + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + return self.tuya_device.name + + @property + def device_info(self): + """Return a device description for device registry.""" + _device_info = { + "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, + "manufacturer": "Tuya", + "name": self.tuya_device.name, + "model": self.tuya_device.product_name, + } + return _device_info + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.tuya_device.online + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{self.tuya_device.id}", + self.async_write_ha_state, + ) + ) + + def _send_command(self, commands: list[dict[str, Any]]) -> None: + """Send command to the device.""" + self.tuya_device_manager.send_commands(self.tuya_device.id, commands) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 73ba69da797..368a65b8499 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,261 +1,484 @@ -"""Support for the Tuya climate devices.""" -from datetime import timedelta +"""Support for Tuya Climate.""" -from homeassistant.components.climate import ( - DOMAIN as SENSOR_DOMAIN, - ENTITY_ID_FORMAT, - ClimateEntity, -) +from __future__ import annotations + +import json +import logging +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.climate import DOMAIN as DEVICE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( - FAN_HIGH, - FAN_LOW, - FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_PLATFORM, - CONF_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TuyaDevice +from .base import TuyaHaEntity from .const import ( - CONF_CURR_TEMP_DIVIDER, - CONF_MAX_TEMP, - CONF_MIN_TEMP, - CONF_SET_TEMP_DIVIDED, - CONF_TEMP_DIVIDER, - CONF_TEMP_STEP_OVERRIDE, DOMAIN, - SIGNAL_CONFIG_ENTITY, - TUYA_DATA, + TUYA_DEVICE_MANAGER, TUYA_DISCOVERY_NEW, + TUYA_HA_DEVICES, + TUYA_HA_TUYA_MAP, ) -DEVICE_TYPE = "climate" +_LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=15) -HA_STATE_TO_TUYA = { - HVAC_MODE_AUTO: "auto", - HVAC_MODE_COOL: "cold", - HVAC_MODE_FAN_ONLY: "wind", - HVAC_MODE_HEAT: "hot", +# Air Conditioner +# https://developer.tuya.com/en/docs/iot/f?id=K9gf46qujdmwb +DPCODE_SWITCH = "switch" +DPCODE_TEMP_SET = "temp_set" +DPCODE_TEMP_SET_F = "temp_set_f" +DPCODE_MODE = "mode" +DPCODE_HUMIDITY_SET = "humidity_set" +DPCODE_FAN_SPEED_ENUM = "fan_speed_enum" + +# Temperature unit +DPCODE_TEMP_UNIT_CONVERT = "temp_unit_convert" +DPCODE_C_F = "c_f" + +# swing flap switch +DPCODE_SWITCH_HORIZONTAL = "switch_horizontal" +DPCODE_SWITCH_VERTICAL = "switch_vertical" + +# status +DPCODE_TEMP_CURRENT = "temp_current" +DPCODE_TEMP_CURRENT_F = "temp_current_f" +DPCODE_HUMIDITY_CURRENT = "humidity_current" + +SWING_OFF = "swing_off" +SWING_VERTICAL = "swing_vertical" +SWING_HORIZONTAL = "swing_horizontal" +SWING_BOTH = "swing_both" + +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 + +TUYA_HVAC_TO_HA = { + "hot": HVAC_MODE_HEAT, + "cold": HVAC_MODE_COOL, + "wet": HVAC_MODE_DRY, + "wind": HVAC_MODE_FAN_ONLY, + "auto": HVAC_MODE_AUTO, } -TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} - -FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} +TUYA_SUPPORT_TYPE = { + "kt", # Air conditioner + "qn", # Heater + "wk", # Thermostat +} -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tuya sensors dynamically through tuya discovery.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up tuya climate dynamically through tuya discovery.""" + _LOGGER.debug("climate init") - platform = config_entry.data[CONF_PLATFORM] + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_sensor(dev_ids): - """Discover and add a discovered tuya sensor.""" + @callback + def async_discover_device(dev_ids: list[str]) -> None: + """Discover and add a discovered tuya climate.""" + _LOGGER.debug("climate add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) + 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, dev_ids, platform): - """Set up Tuya Climate device.""" - tuya = hass.data[DOMAIN][TUYA_DATA] - entities = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: + """Set up Tuya Climate.""" + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] + entities: list[Entity] = [] + for device_id in device_ids: + device = device_manager.device_map[device_id] if device is None: continue - entities.append(TuyaClimateEntity(device, platform)) + entities.append(TuyaHaClimate(device, device_manager)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaClimateEntity(TuyaDevice, ClimateEntity): - """Tuya climate devices,include air conditioner,heater.""" +class TuyaHaClimate(TuyaHaEntity, ClimateEntity): + """Tuya Switch Device.""" - def __init__(self, tuya, platform): - """Init climate device.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self.operations = [HVAC_MODE_OFF] - self._has_operation = False - self._def_hvac_mode = HVAC_MODE_AUTO - self._set_temp_divided = True - self._temp_step_override = None - self._min_temp = None - self._max_temp = None - - @callback - def _process_config(self): - """Set device config parameter.""" - config = self._get_device_config() - if not config: - return - unit = config.get(CONF_UNIT_OF_MEASUREMENT) - if unit: - self._tuya.set_unit("FAHRENHEIT" if unit == TEMP_FAHRENHEIT else "CELSIUS") - self._tuya.temp_divider = config.get(CONF_TEMP_DIVIDER, 0) - self._tuya.curr_temp_divider = config.get(CONF_CURR_TEMP_DIVIDER, 0) - self._set_temp_divided = config.get(CONF_SET_TEMP_DIVIDED, True) - self._temp_step_override = config.get(CONF_TEMP_STEP_OVERRIDE) - min_temp = config.get(CONF_MIN_TEMP, 0) - max_temp = config.get(CONF_MAX_TEMP, 0) - if min_temp >= max_temp: - self._min_temp = self._max_temp = None + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + """Init Tuya Ha Climate.""" + super().__init__(device, device_manager) + if DPCODE_C_F in self.tuya_device.status: + self.dp_temp_unit = DPCODE_C_F else: - self._min_temp = min_temp - self._max_temp = max_temp + self.dp_temp_unit = DPCODE_TEMP_UNIT_CONVERT - async def async_added_to_hass(self): - """Create operation list when add to hass.""" - await super().async_added_to_hass() - self._process_config() - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_CONFIG_ENTITY, self._process_config - ) - ) - - modes = self._tuya.operation_list() - if modes is None: - if self._def_hvac_mode not in self.operations: - self.operations.append(self._def_hvac_mode) - return - - for mode in modes: - if mode not in TUYA_STATE_TO_HA: - continue - ha_mode = TUYA_STATE_TO_HA[mode] - if ha_mode not in self.operations: - self.operations.append(ha_mode) - self._has_operation = True - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - unit = self._tuya.temperature_unit() - if unit == "FAHRENHEIT": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - @property - def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" - if not self._tuya.state(): - return HVAC_MODE_OFF - - if not self._has_operation: - return self._def_hvac_mode - - mode = self._tuya.current_operation() - if mode is None: + def get_temp_set_scale(self) -> int | None: + """Get temperature set scale.""" + dp_temp_set = DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + temp_set_value_range_item = self.tuya_device.status_range.get(dp_temp_set) + if not temp_set_value_range_item: return None - return TUYA_STATE_TO_HA.get(mode) - @property - def hvac_modes(self): - """Return the list of available operation modes.""" - return self.operations + temp_set_value_range = json.loads(temp_set_value_range_item.values) + return temp_set_value_range.get("scale") - @property - def current_temperature(self): - """Return the current temperature.""" - return self._tuya.current_temperature() + def get_temp_current_scale(self) -> int | None: + """Get temperature current scale.""" + dp_temp_current = ( + DPCODE_TEMP_CURRENT if self.is_celsius() else DPCODE_TEMP_CURRENT_F + ) + temp_current_value_range_item = self.tuya_device.status_range.get( + dp_temp_current + ) + if not temp_current_value_range_item: + return None - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._tuya.target_temperature() + temp_current_value_range = json.loads(temp_current_value_range_item.values) + return temp_current_value_range.get("scale") - @property - def target_temperature_step(self): - """Return the supported step of target temperature.""" - if self._temp_step_override: - return self._temp_step_override - return self._tuya.target_temperature_step() + # Functions - @property - def fan_mode(self): - """Return the fan setting.""" - return self._tuya.current_fan_mode() - - @property - def fan_modes(self): - """Return the list of available fan modes.""" - return self._tuya.fan_list() - - def set_temperature(self, **kwargs): - """Set new target temperature.""" - if ATTR_TEMPERATURE in kwargs: - self._tuya.set_temperature(kwargs[ATTR_TEMPERATURE], self._set_temp_divided) - - def set_fan_mode(self, fan_mode): - """Set new target fan mode.""" - self._tuya.set_fan_mode(fan_mode) - - def set_hvac_mode(self, hvac_mode): - """Set new target operation mode.""" + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + commands = [] if hvac_mode == HVAC_MODE_OFF: - self._tuya.turn_off() + commands.append({"code": DPCODE_SWITCH, "value": False}) + else: + commands.append({"code": DPCODE_SWITCH, "value": True}) + + for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): + if ha_mode == hvac_mode: + commands.append({"code": DPCODE_MODE, "value": tuya_mode}) + break + + self._send_command(commands) + + def set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + self._send_command([{"code": DPCODE_FAN_SPEED_ENUM, "value": fan_mode}]) + + def set_humidity(self, humidity: float) -> None: + """Set new target humidity.""" + self._send_command([{"code": DPCODE_HUMIDITY_SET, "value": int(humidity)}]) + + def set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + if swing_mode == SWING_BOTH: + commands = [ + {"code": DPCODE_SWITCH_VERTICAL, "value": True}, + {"code": DPCODE_SWITCH_HORIZONTAL, "value": True}, + ] + elif swing_mode == SWING_HORIZONTAL: + commands = [ + {"code": DPCODE_SWITCH_VERTICAL, "value": False}, + {"code": DPCODE_SWITCH_HORIZONTAL, "value": True}, + ] + elif swing_mode == SWING_VERTICAL: + commands = [ + {"code": DPCODE_SWITCH_VERTICAL, "value": True}, + {"code": DPCODE_SWITCH_HORIZONTAL, "value": False}, + ] + else: + commands = [ + {"code": DPCODE_SWITCH_VERTICAL, "value": False}, + {"code": DPCODE_SWITCH_HORIZONTAL, "value": False}, + ] + + self._send_command(commands) + + def set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + _LOGGER.debug("climate temp-> %s", kwargs) + code = DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + temp_set_scale = self.get_temp_set_scale() + if not temp_set_scale: return - if not self._tuya.state(): - self._tuya.turn_on() - - if self._has_operation: - self._tuya.set_operation_mode(HA_STATE_TO_TUYA.get(hvac_mode)) - - @property - def supported_features(self): - """Return the list of supported features.""" - supports = 0 - if self._tuya.support_target_temperature(): - supports = supports | SUPPORT_TARGET_TEMPERATURE - if self._tuya.support_wind_speed(): - supports = supports | SUPPORT_FAN_MODE - return supports - - @property - def min_temp(self): - """Return the minimum temperature.""" - min_temp = ( - self._min_temp if self._min_temp is not None else self._tuya.min_temp() + self._send_command( + [ + { + "code": code, + "value": int(kwargs["temperature"] * (10 ** temp_set_scale)), + } + ] ) - if min_temp is not None: - return min_temp - return super().min_temp + + def is_celsius(self) -> bool: + """Return True if device reports in Celsius.""" + if ( + self.dp_temp_unit in self.tuya_device.status + and self.tuya_device.status.get(self.dp_temp_unit).lower() == "c" + ): + return True + if ( + DPCODE_TEMP_SET in self.tuya_device.status + or DPCODE_TEMP_CURRENT in self.tuya_device.status + ): + return True + return False @property - def max_temp(self): + def temperature_unit(self) -> str: + """Return true if fan is on.""" + if self.is_celsius(): + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if ( + DPCODE_TEMP_CURRENT not in self.tuya_device.status + and DPCODE_TEMP_CURRENT_F not in self.tuya_device.status + ): + return None + + temp_current_scale = self.get_temp_current_scale() + if not temp_current_scale: + return None + + if self.is_celsius(): + temperature = self.tuya_device.status.get(DPCODE_TEMP_CURRENT) + if not temperature: + return None + return temperature * 1.0 / (10 ** temp_current_scale) + + temperature = self.tuya_device.status.get(DPCODE_TEMP_CURRENT_F) + if not temperature: + return None + return temperature * 1.0 / (10 ** temp_current_scale) + + @property + def current_humidity(self) -> int: + """Return the current humidity.""" + return int(self.tuya_device.status.get(DPCODE_HUMIDITY_CURRENT, 0)) + + @property + def target_temperature(self) -> float | None: + """Return the temperature currently set to be reached.""" + temp_set_scale = self.get_temp_set_scale() + if temp_set_scale is None: + return None + + dpcode_temp_set = self.tuya_device.status.get(DPCODE_TEMP_SET) + if dpcode_temp_set is None: + return None + + return dpcode_temp_set * 1.0 / (10 ** temp_set_scale) + + @property + def max_temp(self) -> float: """Return the maximum temperature.""" - max_temp = ( - self._max_temp if self._max_temp is not None else self._tuya.max_temp() + scale = self.get_temp_set_scale() + if scale is None: + return DEFAULT_MAX_TEMP + + if self.is_celsius(): + if DPCODE_TEMP_SET not in self.tuya_device.function: + return DEFAULT_MAX_TEMP + + function_item = self.tuya_device.function.get(DPCODE_TEMP_SET) + if function_item is None: + return DEFAULT_MAX_TEMP + + temp_value = json.loads(function_item.values) + + temp_max = temp_value.get("max") + if temp_max is None: + return DEFAULT_MAX_TEMP + return temp_max * 1.0 / (10 ** scale) + if DPCODE_TEMP_SET_F not in self.tuya_device.function: + return DEFAULT_MAX_TEMP + + function_item_f = self.tuya_device.function.get(DPCODE_TEMP_SET_F) + if function_item_f is None: + return DEFAULT_MAX_TEMP + + temp_value_f = json.loads(function_item_f.values) + + temp_max_f = temp_value_f.get("max") + if temp_max_f is None: + return DEFAULT_MAX_TEMP + return temp_max_f * 1.0 / (10 ** scale) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + temp_set_scal = self.get_temp_set_scale() + if temp_set_scal is None: + return DEFAULT_MIN_TEMP + + if self.is_celsius(): + if DPCODE_TEMP_SET not in self.tuya_device.function: + return DEFAULT_MIN_TEMP + + function_temp_item = self.tuya_device.function.get(DPCODE_TEMP_SET) + if function_temp_item is None: + return DEFAULT_MIN_TEMP + temp_value = json.loads(function_temp_item.values) + temp_min = temp_value.get("min") + if temp_min is None: + return DEFAULT_MIN_TEMP + return temp_min * 1.0 / (10 ** temp_set_scal) + + if DPCODE_TEMP_SET_F not in self.tuya_device.function: + return DEFAULT_MIN_TEMP + + temp_value_temp_f = self.tuya_device.function.get(DPCODE_TEMP_SET_F) + if temp_value_temp_f is None: + return DEFAULT_MIN_TEMP + temp_value_f = json.loads(temp_value_temp_f.values) + + temp_min_f = temp_value_f.get("min") + if temp_min_f is None: + return DEFAULT_MIN_TEMP + + return temp_min_f * 1.0 / (10 ** temp_set_scal) + + @property + def target_temperature_step(self) -> float | None: + """Return target temperature setp.""" + if ( + DPCODE_TEMP_SET not in self.tuya_device.status_range + and DPCODE_TEMP_SET_F not in self.tuya_device.status_range + ): + return 1.0 + temp_set_value_range = json.loads( + self.tuya_device.status_range.get( + DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F + ).values ) - if max_temp is not None: - return max_temp - return super().max_temp + step = temp_set_value_range.get("step") + if step is None: + return None + + temp_set_scale = self.get_temp_set_scale() + if temp_set_scale is None: + return None + + return step * 1.0 / (10 ** temp_set_scale) + + @property + def target_humidity(self) -> int: + """Return target humidity.""" + return int(self.tuya_device.status.get(DPCODE_HUMIDITY_SET, 0)) + + @property + def hvac_mode(self) -> str: + """Return hvac mode.""" + if not self.tuya_device.status.get(DPCODE_SWITCH, False): + return HVAC_MODE_OFF + if DPCODE_MODE not in self.tuya_device.status: + return HVAC_MODE_OFF + if self.tuya_device.status.get(DPCODE_MODE) is not None: + return TUYA_HVAC_TO_HA[self.tuya_device.status[DPCODE_MODE]] + return HVAC_MODE_OFF + + @property + def hvac_modes(self) -> list[str]: + """Return hvac modes for select.""" + if DPCODE_MODE not in self.tuya_device.function: + return [] + modes = json.loads(self.tuya_device.function.get(DPCODE_MODE, {}).values).get( + "range" + ) + + hvac_modes = [HVAC_MODE_OFF] + for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): + if tuya_mode in modes: + hvac_modes.append(ha_mode) + + return hvac_modes + + @property + def fan_mode(self) -> str | None: + """Return fan mode.""" + return self.tuya_device.status.get(DPCODE_FAN_SPEED_ENUM) + + @property + def fan_modes(self) -> list[str]: + """Return fan modes for select.""" + data = json.loads( + self.tuya_device.function.get(DPCODE_FAN_SPEED_ENUM, {}).values + ).get("range") + return data + + @property + def swing_mode(self) -> str: + """Return swing mode.""" + mode = 0 + if ( + DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status + and self.tuya_device.status.get(DPCODE_SWITCH_HORIZONTAL) + ): + mode += 1 + if ( + DPCODE_SWITCH_VERTICAL in self.tuya_device.status + and self.tuya_device.status.get(DPCODE_SWITCH_VERTICAL) + ): + mode += 2 + + if mode == 3: + return SWING_BOTH + if mode == 2: + return SWING_VERTICAL + if mode == 1: + return SWING_HORIZONTAL + return SWING_OFF + + @property + def swing_modes(self) -> list[str]: + """Return swing mode for select.""" + return [SWING_OFF, SWING_HORIZONTAL, SWING_VERTICAL, SWING_BOTH] + + @property + def supported_features(self) -> int: + """Flag supported features.""" + supports = 0 + if ( + DPCODE_TEMP_SET in self.tuya_device.status + or DPCODE_TEMP_SET_F in self.tuya_device.status + ): + supports |= SUPPORT_TARGET_TEMPERATURE + if DPCODE_FAN_SPEED_ENUM in self.tuya_device.status: + supports |= SUPPORT_FAN_MODE + if DPCODE_HUMIDITY_SET in self.tuya_device.status: + supports |= SUPPORT_TARGET_HUMIDITY + if ( + DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status + or DPCODE_SWITCH_VERTICAL in self.tuya_device.status + ): + supports |= SUPPORT_SWING_MODE + return supports diff --git a/homeassistant/components/tuya/config_flow.py b/homeassistant/components/tuya/config_flow.py index 476a2295fc4..9761b1b6c96 100644 --- a/homeassistant/components/tuya/config_flow.py +++ b/homeassistant/components/tuya/config_flow.py @@ -1,409 +1,140 @@ +#!/usr/bin/env python3 """Config flow for Tuya.""" -from __future__ import annotations import logging -from typing import Any -from tuyaha import TuyaApi -from tuyaha.tuyaapi import ( - TuyaAPIException, - TuyaAPIRateLimitException, - TuyaNetException, - TuyaServerException, -) +from tuya_iot import ProjectType, TuyaOpenAPI import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_PASSWORD, - CONF_PLATFORM, - CONF_UNIT_OF_MEASUREMENT, - CONF_USERNAME, - ENTITY_MATCH_NONE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from .const import ( - CONF_BRIGHTNESS_RANGE_MODE, - CONF_COUNTRYCODE, - CONF_CURR_TEMP_DIVIDER, - CONF_DISCOVERY_INTERVAL, - CONF_MAX_KELVIN, - CONF_MAX_TEMP, - CONF_MIN_KELVIN, - CONF_MIN_TEMP, - CONF_QUERY_DEVICE, - CONF_QUERY_INTERVAL, - CONF_SET_TEMP_DIVIDED, - CONF_SUPPORT_COLOR, - CONF_TEMP_DIVIDER, - CONF_TEMP_STEP_OVERRIDE, - CONF_TUYA_MAX_COLTEMP, - DEFAULT_DISCOVERY_INTERVAL, - DEFAULT_QUERY_INTERVAL, - DEFAULT_TUYA_MAX_COLTEMP, + CONF_ACCESS_ID, + CONF_ACCESS_SECRET, + CONF_APP_TYPE, + CONF_COUNTRY_CODE, + CONF_ENDPOINT, + CONF_PASSWORD, + CONF_PROJECT_TYPE, + CONF_USERNAME, DOMAIN, - TUYA_DATA, - TUYA_PLATFORMS, - TUYA_TYPE_NOT_QUERY, + TUYA_APP_TYPE, + TUYA_ENDPOINT, + TUYA_PROJECT_TYPE, ) +RESULT_SINGLE_INSTANCE = "single_instance_allowed" +RESULT_AUTH_FAILED = "invalid_auth" +TUYA_ENDPOINT_BASE = "https://openapi.tuyacn.com" + _LOGGER = logging.getLogger(__name__) -CONF_LIST_DEVICES = "list_devices" +# Project Type +DATA_SCHEMA_PROJECT_TYPE = vol.Schema( + {vol.Required(CONF_PROJECT_TYPE, default=0): vol.In(TUYA_PROJECT_TYPE)} +) -DATA_SCHEMA_USER = vol.Schema( +# INDUSTY_SOLUTIONS Schema +DATA_SCHEMA_INDUSTRY_SOLUTIONS = vol.Schema( { + vol.Required(CONF_ENDPOINT): vol.In(TUYA_ENDPOINT), + vol.Required(CONF_ACCESS_ID): str, + vol.Required(CONF_ACCESS_SECRET): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_COUNTRYCODE): vol.Coerce(int), - vol.Required(CONF_PLATFORM): vol.In(TUYA_PLATFORMS), } ) -ERROR_DEV_MULTI_TYPE = "dev_multi_type" -ERROR_DEV_NOT_CONFIG = "dev_not_config" -ERROR_DEV_NOT_FOUND = "dev_not_found" - -RESULT_AUTH_FAILED = "invalid_auth" -RESULT_CONN_ERROR = "cannot_connect" -RESULT_SINGLE_INSTANCE = "single_instance_allowed" -RESULT_SUCCESS = "success" - -RESULT_LOG_MESSAGE = { - RESULT_AUTH_FAILED: "Invalid credential", - RESULT_CONN_ERROR: "Connection error", -} - -TUYA_TYPE_CONFIG = ["climate", "light"] +# SMART_HOME Schema +DATA_SCHEMA_SMART_HOME = vol.Schema( + { + vol.Required(CONF_ACCESS_ID): str, + vol.Required(CONF_ACCESS_SECRET): str, + vol.Required(CONF_APP_TYPE): vol.In(TUYA_APP_TYPE), + vol.Required(CONF_COUNTRY_CODE): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a tuya config flow.""" - - VERSION = 1 + """Tuya Config Flow.""" def __init__(self) -> None: - """Initialize flow.""" - self._country_code = None - self._password = None - self._platform = None - self._username = None + """Init tuya config flow.""" + super().__init__() + self.conf_project_type = None - def _save_entry(self): - return self.async_create_entry( - title=self._username, - data={ - CONF_COUNTRYCODE: self._country_code, - CONF_PASSWORD: self._password, - CONF_PLATFORM: self._platform, - CONF_USERNAME: self._username, - }, + @staticmethod + def _try_login(user_input): + project_type = ProjectType(user_input[CONF_PROJECT_TYPE]) + api = TuyaOpenAPI( + user_input[CONF_ENDPOINT] + if project_type == ProjectType.INDUSTY_SOLUTIONS + else "", + user_input[CONF_ACCESS_ID], + user_input[CONF_ACCESS_SECRET], + project_type, ) + api.set_dev_channel("hass") - def _try_connect(self): - """Try to connect and check auth.""" - tuya = TuyaApi() - try: - tuya.init( - self._username, self._password, self._country_code, self._platform + if project_type == ProjectType.INDUSTY_SOLUTIONS: + response = api.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + else: + api.endpoint = TUYA_ENDPOINT_BASE + response = api.login( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + user_input[CONF_COUNTRY_CODE], + user_input[CONF_APP_TYPE], ) - except (TuyaAPIRateLimitException, TuyaNetException, TuyaServerException): - return RESULT_CONN_ERROR - except TuyaAPIException: - return RESULT_AUTH_FAILED + if response.get("success", False) and isinstance( + api.token_info.platform_url, str + ): + api.endpoint = api.token_info.platform_url + user_input[CONF_ENDPOINT] = api.token_info.platform_url - return RESULT_SUCCESS + _LOGGER.debug("TuyaConfigFlow._try_login finish, response:, %s", response) + return response async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason=RESULT_SINGLE_INSTANCE) + """Step user.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_PROJECT_TYPE + ) + self.conf_project_type = user_input[CONF_PROJECT_TYPE] + + return await self.async_step_login() + + async def async_step_login(self, user_input=None): + """Step login.""" errors = {} - if user_input is not None: + assert self.conf_project_type is not None + user_input[CONF_PROJECT_TYPE] = self.conf_project_type - self._country_code = str(user_input[CONF_COUNTRYCODE]) - self._password = user_input[CONF_PASSWORD] - self._platform = user_input[CONF_PLATFORM] - self._username = user_input[CONF_USERNAME] - - result = await self.hass.async_add_executor_job(self._try_connect) - - if result == RESULT_SUCCESS: - return self._save_entry() - if result != RESULT_AUTH_FAILED: - return self.async_abort(reason=result) - errors["base"] = result - - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors - ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for Tuya.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - self._conf_devs_id = None - self._conf_devs_option: dict[str, Any] = {} - self._form_error = None - - def _get_form_error(self): - """Set the error to be shown in the options form.""" - errors = {} - if self._form_error: - errors["base"] = self._form_error - self._form_error = None - return errors - - def _get_tuya_devices_filtered(self, types, exclude_mode=False, type_prefix=True): - """Get the list of Tuya device to filtered by types.""" - config_list = {} - types_filter = set(types) - tuya = self.hass.data[DOMAIN][TUYA_DATA] - devices_list = tuya.get_all_devices() - for device in devices_list: - dev_type = device.device_type() - exclude = ( - dev_type in types_filter - if exclude_mode - else dev_type not in types_filter - ) - if exclude: - continue - dev_id = device.object_id() - if type_prefix: - dev_id = f"{dev_type}-{dev_id}" - config_list[dev_id] = f"{device.name()} ({dev_type})" - - return config_list - - def _get_device(self, dev_id): - """Get specific device from tuya library.""" - tuya = self.hass.data[DOMAIN][TUYA_DATA] - return tuya.get_device_by_id(dev_id) - - def _save_config(self, data): - """Save the updated options.""" - curr_conf = self.config_entry.options.copy() - curr_conf.update(data) - curr_conf.update(self._conf_devs_option) - - return self.async_create_entry(title="", data=curr_conf) - - async def _async_device_form(self, devs_id): - """Return configuration form for devices.""" - conf_devs_id = [] - for count, dev_id in enumerate(devs_id): - device_info = dev_id.split("-") - if count == 0: - device_type = device_info[0] - device_id = device_info[1] - elif device_type != device_info[0]: - self._form_error = ERROR_DEV_MULTI_TYPE - return await self.async_step_init() - conf_devs_id.append(device_info[1]) - - device = self._get_device(device_id) - if not device: - self._form_error = ERROR_DEV_NOT_FOUND - return await self.async_step_init() - - curr_conf = self._conf_devs_option.get( - device_id, self.config_entry.options.get(device_id, {}) - ) - - config_schema = self._get_device_schema(device_type, curr_conf, device) - if not config_schema: - self._form_error = ERROR_DEV_NOT_CONFIG - return await self.async_step_init() - - self._conf_devs_id = conf_devs_id - device_name = ( - "(multiple devices selected)" if len(conf_devs_id) > 1 else device.name() - ) - - return self.async_show_form( - step_id="device", - data_schema=config_schema, - description_placeholders={ - "device_type": device_type, - "device_name": device_name, - }, - ) - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - - if self.config_entry.state is not config_entries.ConfigEntryState.LOADED: - _LOGGER.error("Tuya integration not yet loaded") - return self.async_abort(reason=RESULT_CONN_ERROR) - - if user_input is not None: - dev_ids = user_input.get(CONF_LIST_DEVICES) - if dev_ids: - return await self.async_step_device(None, dev_ids) - - user_input.pop(CONF_LIST_DEVICES, []) - return self._save_config(data=user_input) - - data_schema = vol.Schema( - { - vol.Optional( - CONF_DISCOVERY_INTERVAL, - default=self.config_entry.options.get( - CONF_DISCOVERY_INTERVAL, DEFAULT_DISCOVERY_INTERVAL - ), - ): vol.All(vol.Coerce(int), vol.Clamp(min=30, max=900)), - } - ) - - query_devices = self._get_tuya_devices_filtered( - TUYA_TYPE_NOT_QUERY, True, False - ) - if query_devices: - devices = {ENTITY_MATCH_NONE: "Default"} - devices.update(query_devices) - def_val = self.config_entry.options.get(CONF_QUERY_DEVICE) - if not def_val or not query_devices.get(def_val): - def_val = ENTITY_MATCH_NONE - data_schema = data_schema.extend( - { - vol.Optional( - CONF_QUERY_INTERVAL, - default=self.config_entry.options.get( - CONF_QUERY_INTERVAL, DEFAULT_QUERY_INTERVAL - ), - ): vol.All(vol.Coerce(int), vol.Clamp(min=30, max=240)), - vol.Optional(CONF_QUERY_DEVICE, default=def_val): vol.In(devices), - } + response = await self.hass.async_add_executor_job( + self._try_login, user_input ) - config_devices = self._get_tuya_devices_filtered(TUYA_TYPE_CONFIG, False, True) - if config_devices: - data_schema = data_schema.extend( - {vol.Optional(CONF_LIST_DEVICES): cv.multi_select(config_devices)} + if response.get("success", False): + _LOGGER.debug("TuyaConfigFlow.async_step_user login success") + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + ) + errors["base"] = RESULT_AUTH_FAILED + + if ProjectType(self.conf_project_type) == ProjectType.SMART_HOME: + return self.async_show_form( + step_id="login", data_schema=DATA_SCHEMA_SMART_HOME, errors=errors ) return self.async_show_form( - step_id="init", - data_schema=data_schema, - errors=self._get_form_error(), + step_id="login", + data_schema=DATA_SCHEMA_INDUSTRY_SOLUTIONS, + errors=errors, ) - - async def async_step_device(self, user_input=None, dev_ids=None): - """Handle options flow for device.""" - if dev_ids is not None: - return await self._async_device_form(dev_ids) - if user_input is not None: - for device_id in self._conf_devs_id: - self._conf_devs_option[device_id] = user_input - - return await self.async_step_init() - - def _get_device_schema(self, device_type, curr_conf, device): - """Return option schema for device.""" - if device_type != device.device_type(): - return None - schema = None - if device_type == "light": - schema = self._get_light_schema(curr_conf, device) - elif device_type == "climate": - schema = self._get_climate_schema(curr_conf, device) - return schema - - @staticmethod - def _get_light_schema(curr_conf, device): - """Create option schema for light device.""" - min_kelvin = device.max_color_temp() - max_kelvin = device.min_color_temp() - - config_schema = vol.Schema( - { - vol.Optional( - CONF_SUPPORT_COLOR, - default=curr_conf.get(CONF_SUPPORT_COLOR, False), - ): bool, - vol.Optional( - CONF_BRIGHTNESS_RANGE_MODE, - default=curr_conf.get(CONF_BRIGHTNESS_RANGE_MODE, 0), - ): vol.In({0: "Range 1-255", 1: "Range 10-1000"}), - vol.Optional( - CONF_MIN_KELVIN, - default=curr_conf.get(CONF_MIN_KELVIN, min_kelvin), - ): vol.All(vol.Coerce(int), vol.Clamp(min=min_kelvin, max=max_kelvin)), - vol.Optional( - CONF_MAX_KELVIN, - default=curr_conf.get(CONF_MAX_KELVIN, max_kelvin), - ): vol.All(vol.Coerce(int), vol.Clamp(min=min_kelvin, max=max_kelvin)), - vol.Optional( - CONF_TUYA_MAX_COLTEMP, - default=curr_conf.get( - CONF_TUYA_MAX_COLTEMP, DEFAULT_TUYA_MAX_COLTEMP - ), - ): vol.All( - vol.Coerce(int), - vol.Clamp( - min=DEFAULT_TUYA_MAX_COLTEMP, max=DEFAULT_TUYA_MAX_COLTEMP * 10 - ), - ), - } - ) - - return config_schema - - @staticmethod - def _get_climate_schema(curr_conf, device): - """Create option schema for climate device.""" - unit = device.temperature_unit() - def_unit = TEMP_FAHRENHEIT if unit == "FAHRENHEIT" else TEMP_CELSIUS - supported_steps = device.supported_temperature_steps() - default_step = device.target_temperature_step() - - config_schema = vol.Schema( - { - vol.Optional( - CONF_UNIT_OF_MEASUREMENT, - default=curr_conf.get(CONF_UNIT_OF_MEASUREMENT, def_unit), - ): vol.In({TEMP_CELSIUS: "Celsius", TEMP_FAHRENHEIT: "Fahrenheit"}), - vol.Optional( - CONF_TEMP_DIVIDER, - default=curr_conf.get(CONF_TEMP_DIVIDER, 0), - ): vol.All(vol.Coerce(int), vol.Clamp(min=0)), - vol.Optional( - CONF_CURR_TEMP_DIVIDER, - default=curr_conf.get(CONF_CURR_TEMP_DIVIDER, 0), - ): vol.All(vol.Coerce(int), vol.Clamp(min=0)), - vol.Optional( - CONF_SET_TEMP_DIVIDED, - default=curr_conf.get(CONF_SET_TEMP_DIVIDED, True), - ): bool, - vol.Optional( - CONF_TEMP_STEP_OVERRIDE, - default=curr_conf.get(CONF_TEMP_STEP_OVERRIDE, default_step), - ): vol.In(supported_steps), - vol.Optional( - CONF_MIN_TEMP, - default=curr_conf.get(CONF_MIN_TEMP, 0), - ): int, - vol.Optional( - CONF_MAX_TEMP, - default=curr_conf.get(CONF_MAX_TEMP, 0), - ): int, - } - ) - - return config_schema diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 646bcc077cf..e259dd9190b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -1,39 +1,37 @@ +#!/usr/bin/env python3 """Constants for the Tuya integration.""" -CONF_BRIGHTNESS_RANGE_MODE = "brightness_range_mode" -CONF_COUNTRYCODE = "country_code" -CONF_CURR_TEMP_DIVIDER = "curr_temp_divider" -CONF_DISCOVERY_INTERVAL = "discovery_interval" -CONF_MAX_KELVIN = "max_kelvin" -CONF_MAX_TEMP = "max_temp" -CONF_MIN_KELVIN = "min_kelvin" -CONF_MIN_TEMP = "min_temp" -CONF_QUERY_DEVICE = "query_device" -CONF_QUERY_INTERVAL = "query_interval" -CONF_SET_TEMP_DIVIDED = "set_temp_divided" -CONF_SUPPORT_COLOR = "support_color" -CONF_TEMP_DIVIDER = "temp_divider" -CONF_TEMP_STEP_OVERRIDE = "temp_step_override" -CONF_TUYA_MAX_COLTEMP = "tuya_max_coltemp" - -DEFAULT_DISCOVERY_INTERVAL = 605 -DEFAULT_QUERY_INTERVAL = 120 -DEFAULT_TUYA_MAX_COLTEMP = 10000 - DOMAIN = "tuya" -SIGNAL_CONFIG_ENTITY = "tuya_config" -SIGNAL_DELETE_ENTITY = "tuya_delete" -SIGNAL_UPDATE_ENTITY = "tuya_update" +CONF_PROJECT_TYPE = "tuya_project_type" +CONF_ENDPOINT = "endpoint" +CONF_ACCESS_ID = "access_id" +CONF_ACCESS_SECRET = "access_secret" +CONF_USERNAME = "username" +CONF_PASSWORD = "password" +CONF_COUNTRY_CODE = "country_code" +CONF_APP_TYPE = "tuya_app_type" -TUYA_DATA = "tuya_data" -TUYA_DEVICES_CONF = "devices_config" TUYA_DISCOVERY_NEW = "tuya_discovery_new_{}" +TUYA_DEVICE_MANAGER = "tuya_device_manager" +TUYA_HOME_MANAGER = "tuya_home_manager" +TUYA_MQTT_LISTENER = "tuya_mqtt_listener" +TUYA_HA_TUYA_MAP = "tuya_ha_tuya_map" +TUYA_HA_DEVICES = "tuya_ha_devices" -TUYA_PLATFORMS = { - "tuya": "Tuya", - "smart_life": "Smart Life", - "jinvoo_smart": "Jinvoo Smart", +TUYA_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" + +TUYA_ENDPOINT = { + "https://openapi.tuyaus.com": "America", + "https://openapi.tuyacn.com": "China", + "https://openapi.tuyaeu.com": "Europe", + "https://openapi.tuyain.com": "India", + "https://openapi-ueaz.tuyaus.com": "EasternAmerica", + "https://openapi-weaz.tuyaeu.com": "WesternEurope", } -TUYA_TYPE_NOT_QUERY = ["scene", "switch"] +TUYA_PROJECT_TYPE = {1: "Custom Development", 0: "Smart Home PaaS"} + +TUYA_APP_TYPE = {"tuyaSmart": "TuyaSmart", "smartlife": "Smart Life"} + +PLATFORMS = ["climate", "fan", "light", "scene", "switch"] diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py deleted file mode 100644 index 08f1d92aca5..00000000000 --- a/homeassistant/components/tuya/cover.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Support for Tuya covers.""" -from datetime import timedelta - -from homeassistant.components.cover import ( - DOMAIN as SENSOR_DOMAIN, - ENTITY_ID_FORMAT, - SUPPORT_CLOSE, - SUPPORT_OPEN, - SUPPORT_STOP, - CoverEntity, -) -from homeassistant.const import CONF_PLATFORM -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import TuyaDevice -from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW - -SCAN_INTERVAL = timedelta(seconds=15) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tuya sensors dynamically through tuya discovery.""" - - platform = config_entry.data[CONF_PLATFORM] - - async def async_discover_sensor(dev_ids): - """Discover and add a discovered tuya sensor.""" - if not dev_ids: - return - entities = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) - async_add_entities(entities) - - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor - ) - - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) - - -def _setup_entities(hass, dev_ids, platform): - """Set up Tuya Cover device.""" - tuya = hass.data[DOMAIN][TUYA_DATA] - entities = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - entities.append(TuyaCover(device, platform)) - return entities - - -class TuyaCover(TuyaDevice, CoverEntity): - """Tuya cover devices.""" - - def __init__(self, tuya, platform): - """Init tuya cover device.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self._was_closing = False - self._was_opening = False - - @property - def supported_features(self): - """Flag supported features.""" - if self._tuya.support_stop(): - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP - return SUPPORT_OPEN | SUPPORT_CLOSE - - @property - def is_opening(self): - """Return if the cover is opening or not.""" - state = self._tuya.state() - if state == 1: - self._was_opening = True - self._was_closing = False - return True - return False - - @property - def is_closing(self): - """Return if the cover is closing or not.""" - state = self._tuya.state() - if state == 2: - self._was_opening = False - self._was_closing = True - return True - return False - - @property - def is_closed(self): - """Return if the cover is closed or not.""" - state = self._tuya.state() - if state != 2 and self._was_closing: - return True - if state != 1 and self._was_opening: - return False - return None - - def open_cover(self, **kwargs): - """Open the cover.""" - self._tuya.open_cover() - - def close_cover(self, **kwargs): - """Close cover.""" - self._tuya.close_cover() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - if self.is_closed is None: - self._was_opening = False - self._was_closing = False - self._tuya.stop_cover() diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index ab361c6ac31..dcfde0ded0f 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -1,141 +1,259 @@ -"""Support for Tuya fans.""" +"""Support for Tuya Fan.""" from __future__ import annotations -from datetime import timedelta +import json +import logging +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.fan import ( - DOMAIN as SENSOR_DOMAIN, - ENTITY_ID_FORMAT, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN as DEVICE_DOMAIN, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_PRESET_MODE, SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import CONF_PLATFORM, STATE_OFF +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 . import TuyaDevice -from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW +from .base import TuyaHaEntity +from .const import ( + DOMAIN, + TUYA_DEVICE_MANAGER, + TUYA_DISCOVERY_NEW, + TUYA_HA_DEVICES, + TUYA_HA_TUYA_MAP, +) -SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tuya sensors dynamically through tuya discovery.""" +# 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" - platform = config_entry.data[CONF_PLATFORM] +# 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" - async def async_discover_sensor(dev_ids): - """Discover and add a discovered tuya sensor.""" +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 = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) + 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, dev_ids, platform): - """Set up Tuya Fan device.""" - tuya = hass.data[DOMAIN][TUYA_DATA] +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 dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + for device_id in device_ids: + device = device_manager.device_map[device_id] if device is None: continue - entities.append(TuyaFanDevice(device, platform)) + entities.append(TuyaHaFan(device, device_manager)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaFanDevice(TuyaDevice, FanEntity): - """Tuya fan devices.""" +class TuyaHaFan(TuyaHaEntity, FanEntity): + """Tuya Fan Device.""" - def __init__(self, tuya, platform): - """Init Tuya fan device.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self.speeds = [] + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + """Init Tuya Fan Device.""" + super().__init__(device, device_manager) - async def async_added_to_hass(self): - """Create fan list when add to hass.""" - await super().async_added_to_hass() - self.speeds.extend(self._tuya.speed_list()) + 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 percentage of the fan.""" - if percentage == 0: - self.turn_off() + """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: - tuya_speed = percentage_to_ordered_list_item(self.speeds, percentage) - self._tuya.set_speed(tuya_speed) + 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, + **kwargs: Any, ) -> None: """Turn on the fan.""" - if percentage is not None: - self.set_percentage(percentage) - else: - self._tuya.turn_on() + self._send_command([{"code": DPCODE_SWITCH, "value": True}]) - def turn_off(self, **kwargs) -> None: - """Turn the entity off.""" - self._tuya.turn_off() - - def oscillate(self, oscillating) -> None: + def oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" - self._tuya.oscillate(oscillating) + 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: + """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], + ) + + return self.tuya_device.status[DPCODE_FAN_SPEED] @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - if self.speeds is None: - return super().speed_count - return len(self.speeds) + if self.tuya_device.category == "kj": + return self.air_purifier_speed_range_len + return super().speed_count @property - def oscillating(self): - """Return current oscillating status.""" - if self.supported_features & SUPPORT_OSCILLATE == 0: - return None - if self.speed == STATE_OFF: - return False - return self._tuya.oscillating() - - @property - def is_on(self): - """Return true if the entity is on.""" - return self._tuya.state() - - @property - def percentage(self) -> int | None: - """Return the current speed.""" - if not self.is_on: - return 0 - if self.speeds is None: - return None - return ordered_list_item_to_percentage(self.speeds, self._tuya.speed()) - - @property - def supported_features(self) -> int: + def supported_features(self): """Flag supported features.""" - if self._tuya.support_oscillate(): - return SUPPORT_SET_SPEED | SUPPORT_OSCILLATE - return SUPPORT_SET_SPEED + 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 diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 4602e65a4d5..180e3a68450 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -1,198 +1,387 @@ """Support for the Tuya lights.""" -from datetime import timedelta +from __future__ import annotations + +import json +import logging +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - DOMAIN as SENSOR_DOMAIN, - ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + DOMAIN as DEVICE_DOMAIN, LightEntity, ) -from homeassistant.const import CONF_PLATFORM -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import color as colorutil +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TuyaDevice +from .base import TuyaHaEntity from .const import ( - CONF_BRIGHTNESS_RANGE_MODE, - CONF_MAX_KELVIN, - CONF_MIN_KELVIN, - CONF_SUPPORT_COLOR, - CONF_TUYA_MAX_COLTEMP, - DEFAULT_TUYA_MAX_COLTEMP, DOMAIN, - SIGNAL_CONFIG_ENTITY, - TUYA_DATA, + TUYA_DEVICE_MANAGER, TUYA_DISCOVERY_NEW, + TUYA_HA_DEVICES, + TUYA_HA_TUYA_MAP, ) -SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger(__name__) -TUYA_BRIGHTNESS_RANGE0 = (1, 255) -TUYA_BRIGHTNESS_RANGE1 = (10, 1000) -BRIGHTNESS_MODES = { - 0: TUYA_BRIGHTNESS_RANGE0, - 1: TUYA_BRIGHTNESS_RANGE1, +# Light(dj) +# https://developer.tuya.com/en/docs/iot/f?id=K9i5ql3v98hn3 +DPCODE_SWITCH = "switch_led" +DPCODE_WORK_MODE = "work_mode" +DPCODE_BRIGHT_VALUE = "bright_value" +DPCODE_TEMP_VALUE = "temp_value" +DPCODE_COLOUR_DATA = "colour_data" +DPCODE_COLOUR_DATA_V2 = "colour_data_v2" +DPCODE_LIGHT = "light" + +MIREDS_MAX = 500 +MIREDS_MIN = 153 + +HSV_HA_HUE_MIN = 0 +HSV_HA_HUE_MAX = 360 +HSV_HA_SATURATION_MIN = 0 +HSV_HA_SATURATION_MAX = 100 + +WORK_MODE_WHITE = "white" +WORK_MODE_COLOUR = "colour" + +TUYA_SUPPORT_TYPE = { + "dj", # Light + "dd", # Light strip + "fwl", # Ambient light + "dc", # Light string + "jsq", # Humidifier's light + "xdd", # Ceiling Light + "xxj", # Diffuser's light + "fs", # Fan +} + +DEFAULT_HSV = { + "h": {"min": 1, "scale": 0, "unit": "", "max": 360, "step": 1}, + "s": {"min": 1, "scale": 0, "unit": "", "max": 255, "step": 1}, + "v": {"min": 1, "scale": 0, "unit": "", "max": 255, "step": 1}, +} + +DEFAULT_HSV_V2 = { + "h": {"min": 1, "scale": 0, "unit": "", "max": 360, "step": 1}, + "s": {"min": 1, "scale": 0, "unit": "", "max": 1000, "step": 1}, + "v": {"min": 1, "scale": 0, "unit": "", "max": 1000, "step": 1}, } -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tuya sensors dynamically through tuya discovery.""" +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up tuya light dynamically through tuya discovery.""" + _LOGGER.debug("light init") - platform = config_entry.data[CONF_PLATFORM] + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_sensor(dev_ids): - """Discover and add a discovered tuya sensor.""" + @callback + def async_discover_device(dev_ids: list[str]): + """Discover and add a discovered tuya light.""" + _LOGGER.debug("light add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) + 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, dev_ids, platform): +def _setup_entities( + hass, entry: ConfigEntry, device_ids: list[str] +) -> list[TuyaHaLight]: """Set up Tuya Light device.""" - tuya = hass.data[DOMAIN][TUYA_DATA] + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] entities = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + for device_id in device_ids: + device = device_manager.device_map[device_id] if device is None: continue - entities.append(TuyaLight(device, platform)) + + tuya_ha_light = TuyaHaLight(device, device_manager) + entities.append(tuya_ha_light) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add( + tuya_ha_light.tuya_device.id + ) + return entities -class TuyaLight(TuyaDevice, LightEntity): +class TuyaHaLight(TuyaHaEntity, LightEntity): """Tuya light device.""" - def __init__(self, tuya, platform): - """Init Tuya light device.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self._min_kelvin = tuya.max_color_temp() - self._max_kelvin = tuya.min_color_temp() + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: + """Init TuyaHaLight.""" + self.dp_code_bright = DPCODE_BRIGHT_VALUE + self.dp_code_temp = DPCODE_TEMP_VALUE + self.dp_code_colour = DPCODE_COLOUR_DATA - @callback - def _process_config(self): - """Set device config parameter.""" - config = self._get_device_config() - if not config: - return + for key in device.function: + if key.startswith(DPCODE_BRIGHT_VALUE): + self.dp_code_bright = key + elif key.startswith(DPCODE_TEMP_VALUE): + self.dp_code_temp = key + elif key.startswith(DPCODE_COLOUR_DATA): + self.dp_code_colour = key - # support color config - supp_color = config.get(CONF_SUPPORT_COLOR, False) - if supp_color: - self._tuya.force_support_color() - # brightness range config - self._tuya.brightness_white_range = BRIGHTNESS_MODES.get( - config.get(CONF_BRIGHTNESS_RANGE_MODE, 0), - TUYA_BRIGHTNESS_RANGE0, - ) - # color set temp range - min_tuya = self._tuya.max_color_temp() - min_kelvin = config.get(CONF_MIN_KELVIN, min_tuya) - max_tuya = self._tuya.min_color_temp() - max_kelvin = config.get(CONF_MAX_KELVIN, max_tuya) - self._min_kelvin = min(max(min_kelvin, min_tuya), max_tuya) - self._max_kelvin = min(max(max_kelvin, self._min_kelvin), max_tuya) - # color shown temp range - max_color_temp = max( - config.get(CONF_TUYA_MAX_COLTEMP, DEFAULT_TUYA_MAX_COLTEMP), - DEFAULT_TUYA_MAX_COLTEMP, - ) - self._tuya.color_temp_range = (1000, max_color_temp) - - async def async_added_to_hass(self): - """Set config parameter when add to hass.""" - await super().async_added_to_hass() - self._process_config() - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_CONFIG_ENTITY, self._process_config - ) - ) - return + super().__init__(device, device_manager) @property - def brightness(self): - """Return the brightness of the light.""" - if self._tuya.brightness() is None: - return None - return int(self._tuya.brightness()) - - @property - def hs_color(self): - """Return the hs_color of the light.""" - return tuple(map(int, self._tuya.hs_color())) - - @property - def color_temp(self): - """Return the color_temp of the light.""" - color_temp = int(self._tuya.color_temp()) - if color_temp is None: - return None - return colorutil.color_temperature_kelvin_to_mired(color_temp) - - @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self._tuya.state() + return self.tuya_device.status.get(DPCODE_SWITCH, False) - @property - def min_mireds(self): - """Return color temperature min mireds.""" - return colorutil.color_temperature_kelvin_to_mired(self._max_kelvin) - - @property - def max_mireds(self): - """Return color temperature max mireds.""" - return colorutil.color_temperature_kelvin_to_mired(self._min_kelvin) - - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" - if ( - ATTR_BRIGHTNESS not in kwargs - and ATTR_HS_COLOR not in kwargs - and ATTR_COLOR_TEMP not in kwargs - ): - self._tuya.turn_on() - if ATTR_BRIGHTNESS in kwargs: - self._tuya.set_brightness(kwargs[ATTR_BRIGHTNESS]) - if ATTR_HS_COLOR in kwargs: - self._tuya.set_color(kwargs[ATTR_HS_COLOR]) - if ATTR_COLOR_TEMP in kwargs: - color_temp = colorutil.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP] - ) - self._tuya.set_color_temp(color_temp) + commands = [] + _LOGGER.debug("light kwargs-> %s", kwargs) - def turn_off(self, **kwargs): + if ( + DPCODE_LIGHT in self.tuya_device.status + and DPCODE_SWITCH not in self.tuya_device.status + ): + commands += [{"code": DPCODE_LIGHT, "value": True}] + else: + commands += [{"code": DPCODE_SWITCH, "value": True}] + + if ATTR_BRIGHTNESS in kwargs: + if self._work_mode().startswith(WORK_MODE_COLOUR): + colour_data = self._get_hsv() + v_range = self._tuya_hsv_v_range() + colour_data["v"] = int( + self.remap(kwargs[ATTR_BRIGHTNESS], 0, 255, v_range[0], v_range[1]) + ) + commands += [ + {"code": self.dp_code_colour, "value": json.dumps(colour_data)} + ] + else: + new_range = self._tuya_brightness_range() + tuya_brightness = int( + self.remap( + kwargs[ATTR_BRIGHTNESS], 0, 255, new_range[0], new_range[1] + ) + ) + commands += [{"code": self.dp_code_bright, "value": tuya_brightness}] + + if ATTR_HS_COLOR in kwargs: + colour_data = self._get_hsv() + # hsv h + colour_data["h"] = int(kwargs[ATTR_HS_COLOR][0]) + # hsv s + ha_s = kwargs[ATTR_HS_COLOR][1] + s_range = self._tuya_hsv_s_range() + colour_data["s"] = int( + self.remap( + ha_s, + HSV_HA_SATURATION_MIN, + HSV_HA_SATURATION_MAX, + s_range[0], + s_range[1], + ) + ) + # hsv v + ha_v = self.brightness + v_range = self._tuya_hsv_v_range() + colour_data["v"] = int(self.remap(ha_v, 0, 255, v_range[0], v_range[1])) + + commands += [ + {"code": self.dp_code_colour, "value": json.dumps(colour_data)} + ] + if self.tuya_device.status[DPCODE_WORK_MODE] != "colour": + commands += [{"code": DPCODE_WORK_MODE, "value": "colour"}] + + if ATTR_COLOR_TEMP in kwargs: + # temp color + new_range = self._tuya_temp_range() + color_temp = self.remap( + self.max_mireds - kwargs[ATTR_COLOR_TEMP] + self.min_mireds, + self.min_mireds, + self.max_mireds, + new_range[0], + new_range[1], + ) + commands += [{"code": self.dp_code_temp, "value": int(color_temp)}] + + # brightness + ha_brightness = self.brightness + new_range = self._tuya_brightness_range() + tuya_brightness = self.remap( + ha_brightness, 0, 255, new_range[0], new_range[1] + ) + commands += [{"code": self.dp_code_bright, "value": int(tuya_brightness)}] + + if self.tuya_device.status[DPCODE_WORK_MODE] != "white": + commands += [{"code": DPCODE_WORK_MODE, "value": "white"}] + + self._send_command(commands) + + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - self._tuya.turn_off() + if ( + DPCODE_LIGHT in self.tuya_device.status + and DPCODE_SWITCH not in self.tuya_device.status + ): + commands = [{"code": DPCODE_LIGHT, "value": False}] + else: + commands = [{"code": DPCODE_SWITCH, "value": False}] + self._send_command(commands) @property - def supported_features(self): - """Flag supported features.""" - supports = SUPPORT_BRIGHTNESS - if self._tuya.support_color(): - supports = supports | SUPPORT_COLOR - if self._tuya.support_color_temp(): - supports = supports | SUPPORT_COLOR_TEMP - return supports + def brightness(self) -> int | None: + """Return the brightness of the light.""" + old_range = self._tuya_brightness_range() + brightness = self.tuya_device.status.get(self.dp_code_bright, 0) + + if self._work_mode().startswith(WORK_MODE_COLOUR): + colour_json = self.tuya_device.status.get(self.dp_code_colour) + if not colour_json: + return None + colour_data = json.loads(colour_json) + v_range = self._tuya_hsv_v_range() + hsv_v = colour_data.get("v", 0) + return int(self.remap(hsv_v, v_range[0], v_range[1], 0, 255)) + + return int(self.remap(brightness, old_range[0], old_range[1], 0, 255)) + + def _tuya_brightness_range(self) -> tuple[int, int]: + if self.dp_code_bright not in self.tuya_device.status: + return 0, 255 + bright_item = self.tuya_device.function.get(self.dp_code_bright) + if not bright_item: + return 0, 255 + bright_value = json.loads(bright_item.values) + return bright_value.get("min", 0), bright_value.get("max", 255) + + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hs_color of the light.""" + colour_json = self.tuya_device.status.get(self.dp_code_colour) + if not colour_json: + return None + colour_data = json.loads(colour_json) + s_range = self._tuya_hsv_s_range() + return colour_data.get("h", 0), self.remap( + colour_data.get("s", 0), + s_range[0], + s_range[1], + HSV_HA_SATURATION_MIN, + HSV_HA_SATURATION_MAX, + ) + + @property + def color_temp(self) -> int: + """Return the color_temp of the light.""" + new_range = self._tuya_temp_range() + tuya_color_temp = self.tuya_device.status.get(self.dp_code_temp, 0) + ha_color_temp = ( + self.max_mireds + - self.remap( + tuya_color_temp, + new_range[0], + new_range[1], + self.min_mireds, + self.max_mireds, + ) + + self.min_mireds + ) + return ha_color_temp + + @property + def min_mireds(self) -> int: + """Return color temperature min mireds.""" + return MIREDS_MIN + + @property + def max_mireds(self) -> int: + """Return color temperature max mireds.""" + return MIREDS_MAX + + def _tuya_temp_range(self) -> tuple[int, int]: + temp_item = self.tuya_device.function.get(self.dp_code_temp) + if not temp_item: + return 0, 255 + temp_value = json.loads(temp_item.values) + return temp_value.get("min", 0), temp_value.get("max", 255) + + def _tuya_hsv_s_range(self) -> tuple[int, int]: + hsv_data_range = self._tuya_hsv_function() + if hsv_data_range is not None: + hsv_s = hsv_data_range.get("s", {"min": 0, "max": 255}) + return hsv_s.get("min", 0), hsv_s.get("max", 255) + return 0, 255 + + def _tuya_hsv_v_range(self) -> tuple[int, int]: + hsv_data_range = self._tuya_hsv_function() + if hsv_data_range is not None: + hsv_v = hsv_data_range.get("v", {"min": 0, "max": 255}) + return hsv_v.get("min", 0), hsv_v.get("max", 255) + + return 0, 255 + + def _tuya_hsv_function(self) -> dict[str, dict] | None: + hsv_item = self.tuya_device.function.get(self.dp_code_colour) + if not hsv_item: + return None + hsv_data = json.loads(hsv_item.values) + if hsv_data: + return hsv_data + colour_json = self.tuya_device.status.get(self.dp_code_colour) + if not colour_json: + return None + colour_data = json.loads(colour_json) + if ( + self.dp_code_colour == DPCODE_COLOUR_DATA_V2 + or colour_data.get("v", 0) > 255 + or colour_data.get("s", 0) > 255 + ): + return DEFAULT_HSV_V2 + return DEFAULT_HSV + + def _work_mode(self) -> str: + return self.tuya_device.status.get(DPCODE_WORK_MODE, "") + + def _get_hsv(self) -> dict[str, int]: + return json.loads(self.tuya_device.status[self.dp_code_colour]) + + @property + def supported_color_modes(self) -> set[str] | None: + """Flag supported color modes.""" + color_modes = [COLOR_MODE_ONOFF] + if self.dp_code_bright in self.tuya_device.status: + color_modes.append(COLOR_MODE_BRIGHTNESS) + + if self.dp_code_temp in self.tuya_device.status: + color_modes.append(COLOR_MODE_COLOR_TEMP) + + if ( + self.dp_code_colour in self.tuya_device.status + and len(self.tuya_device.status[self.dp_code_colour]) > 0 + ): + color_modes.append(COLOR_MODE_HS) + return set(color_modes) diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 5dae8e6a101..85370bdfcac 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,17 +1,13 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": ["tuyaha==0.0.10"], - "codeowners": ["@ollo69"], + "documentation": "https://github.com/tuya/tuya-home-assistant", + "requirements": [ + "tuya-iot-py-sdk==0.4.1" + ], + "codeowners": [ + "@Tuya" + ], "config_flow": true, - "iot_class": "cloud_polling", - "dhcp": [ - {"macaddress": "508A06*"}, - {"macaddress": "7CF666*"}, - {"macaddress": "10D561*"}, - {"macaddress": "D4A651*"}, - {"macaddress": "68572D*"}, - {"macaddress": "1869D8*"} - ] -} + "iot_class": "cloud_push" +} \ No newline at end of file diff --git a/homeassistant/components/tuya/scene.py b/homeassistant/components/tuya/scene.py index 430b2bc7e27..c6010f9ef87 100644 --- a/homeassistant/components/tuya/scene.py +++ b/homeassistant/components/tuya/scene.py @@ -1,61 +1,74 @@ -"""Support for the Tuya scenes.""" +"""Support for Tuya scenes.""" +from __future__ import annotations + +import logging from typing import Any -from homeassistant.components.scene import DOMAIN as SENSOR_DOMAIN, Scene -from homeassistant.const import CONF_PLATFORM -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from tuya_iot import TuyaHomeManager, TuyaScene -from . import TuyaDevice -from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW +from homeassistant.components.scene import Scene +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback -ENTITY_ID_FORMAT = SENSOR_DOMAIN + ".{}" +from .const import DOMAIN, TUYA_HOME_MANAGER + +_LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up tuya sensors dynamically through tuya discovery.""" - - platform = config_entry.data[CONF_PLATFORM] - - async def async_discover_sensor(dev_ids): - """Discover and add a discovered tuya sensor.""" - if not dev_ids: - return - entities = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) - async_add_entities(entities) - - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor - ) - - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) - - -def _setup_entities(hass, dev_ids, platform): - """Set up Tuya Scene.""" - tuya = hass.data[DOMAIN][TUYA_DATA] +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up tuya scenes.""" entities = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) - if device is None: - continue - entities.append(TuyaScene(device, platform)) - return entities + + home_manager = hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] + scenes = await hass.async_add_executor_job(home_manager.query_scenes) + for scene in scenes: + entities.append(TuyaHAScene(home_manager, scene)) + + async_add_entities(entities) -class TuyaScene(TuyaDevice, Scene): - """Tuya Scene.""" +class TuyaHAScene(Scene): + """Tuya Scene Remote.""" - def __init__(self, tuya, platform): - """Init Tuya scene.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + def __init__(self, home_manager: TuyaHomeManager, scene: TuyaScene) -> None: + """Init Tuya Scene.""" + super().__init__() + self.home_manager = home_manager + self.scene = scene + + @property + def should_poll(self) -> bool: + """Hass should not poll.""" + return False + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"tys{self.scene.scene_id}" + + @property + def name(self) -> str | None: + """Return Tuya scene name.""" + return self.scene.name + + @property + def device_info(self): + """Return a device description for device registry.""" + return { + "identifiers": {(DOMAIN, f"{self.unique_id}")}, + "manufacturer": "tuya", + "name": self.scene.name, + "model": "Tuya Scene", + } + + @property + def available(self) -> bool: + """Return if the scene is enabled.""" + return self.scene.enabled def activate(self, **kwargs: Any) -> None: """Activate the scene.""" - self._tuya.activate() + self.home_manager.trigger_scene(self.scene.home_id, self.scene.scene_id) diff --git a/homeassistant/components/tuya/services.yaml b/homeassistant/components/tuya/services.yaml deleted file mode 100644 index 42fba3ad37b..00000000000 --- a/homeassistant/components/tuya/services.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Describes the format for available Tuya services - -pull_devices: - name: Pull devices - description: Pull device list from Tuya server. - -force_update: - name: Force update - description: Force all Tuya devices to pull data. diff --git a/homeassistant/components/tuya/strings.json b/homeassistant/components/tuya/strings.json index 61ea46c6a9f..91ca045e1f5 100644 --- a/homeassistant/components/tuya/strings.json +++ b/homeassistant/components/tuya/strings.json @@ -1,64 +1,29 @@ { "config": { + "flow_title": "Tuya configuration", "step": { - "user": { + "user":{ + "title":"Tuya Integration", + "data":{ + "tuya_project_type": "Tuya cloud project type" + } + }, + "login": { "title": "Tuya", - "description": "Enter your Tuya credentials.", + "description": "Enter your Tuya credential", "data": { - "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", - "password": "[%key:common::config_flow::data::password%]", - "platform": "The app where your account is registered", - "username": "[%key:common::config_flow::data::username%]" + "endpoint": "Availability Zone", + "access_id": "Access ID", + "access_secret": "Access Secret", + "tuya_app_type": "Mobile App", + "country_code": "Country Code", + "username": "Account", + "password": "Password" } } }, - "abort": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" - }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } - }, - "options": { - "abort": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - }, - "step": { - "init": { - "title": "Configure Tuya Options", - "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", - "data": { - "discovery_interval": "Discovery device polling interval in seconds", - "query_device": "Select device that will use query method for faster status update", - "query_interval": "Query device polling interval in seconds", - "list_devices": "Select the devices to configure or leave empty to save configuration" - } - }, - "device": { - "title": "Configure Tuya Device", - "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", - "data": { - "support_color": "Force color support", - "brightness_range_mode": "Brightness range used by device", - "min_kelvin": "Min color temperature supported in kelvin", - "max_kelvin": "Max color temperature supported in kelvin", - "tuya_max_coltemp": "Max color temperature reported by device", - "unit_of_measurement": "Temperature unit used by device", - "temp_divider": "Temperature values divider (0 = use default)", - "curr_temp_divider": "Current Temperature value divider (0 = use default)", - "set_temp_divided": "Use divided Temperature value for set temperature command", - "temp_step_override": "Target Temperature step", - "min_temp": "Min target temperature (use min and max = 0 for default)", - "max_temp": "Max target temperature (use min and max = 0 for default)" - } - } - }, - "error": { - "dev_multi_type": "Multiple selected devices to configure must be of the same type", - "dev_not_config": "Device type not configurable", - "dev_not_found": "Device not found" - } } -} +} \ No newline at end of file diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 3f5ff6db163..ab34ebbdfc0 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -1,74 +1,177 @@ +#!/usr/bin/env python3 """Support for Tuya switches.""" -from datetime import timedelta +from __future__ import annotations -from homeassistant.components.switch import ( - DOMAIN as SENSOR_DOMAIN, - ENTITY_ID_FORMAT, - SwitchEntity, -) -from homeassistant.const import CONF_PLATFORM +import logging +from typing import Any + +from tuya_iot import TuyaDevice, TuyaDeviceManager + +from homeassistant.components.switch import DOMAIN as DEVICE_DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import TuyaDevice -from .const import DOMAIN, TUYA_DATA, TUYA_DISCOVERY_NEW +from .base import TuyaHaEntity +from .const import ( + DOMAIN, + TUYA_DEVICE_MANAGER, + TUYA_DISCOVERY_NEW, + TUYA_HA_DEVICES, + TUYA_HA_TUYA_MAP, +) -SCAN_INTERVAL = timedelta(seconds=15) +_LOGGER = logging.getLogger(__name__) + +TUYA_SUPPORT_TYPE = { + "kg", # Switch + "cz", # Socket + "pc", # Power Strip + "bh", # Smart Kettle + "dlq", # Breaker + "cwysj", # Pet Water Feeder + "kj", # Air Purifier + "xxj", # Diffuser +} + +# Switch(kg), Socket(cz), Power Strip(pc) +# https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y +DPCODE_SWITCH = "switch" + +# Air Purifier +# https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7 +# Pet Water Feeder +# https://developer.tuya.com/en/docs/iot/f?id=K9gf46aewxem5 +DPCODE_ANION = "anion" # Air Purifier - Ionizer unit +# Air Purifier - Filter cartridge resetting; Pet Water Feeder - Filter cartridge resetting +DPCODE_FRESET = "filter_reset" +DPCODE_LIGHT = "light" # Air Purifier - Light +DPCODE_LOCK = "lock" # Air Purifier - Child lock +# Air Purifier - UV sterilization; Pet Water Feeder - UV sterilization +DPCODE_UV = "uv" +DPCODE_WET = "wet" # Air Purifier - Humidification unit +DPCODE_PRESET = "pump_reset" # Pet Water Feeder - Water pump resetting +DPCODE_WRESET = "water_reset" # Pet Water Feeder - Resetting of water usage days -async def async_setup_entry(hass, config_entry, async_add_entities): +DPCODE_START = "start" + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya sensors dynamically through tuya discovery.""" + _LOGGER.debug("switch init") - platform = config_entry.data[CONF_PLATFORM] + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_sensor(dev_ids): + async def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" + _LOGGER.debug("switch add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job( - _setup_entities, - hass, - dev_ids, - platform, - ) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(SENSOR_DOMAIN), async_discover_sensor + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - devices_ids = hass.data[DOMAIN]["pending"].pop(SENSOR_DOMAIN) - await async_discover_sensor(devices_ids) + 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) + await async_discover_device(device_ids) -def _setup_entities(hass, dev_ids, platform): +def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Entity]: """Set up Tuya Switch device.""" - tuya = hass.data[DOMAIN][TUYA_DATA] - entities = [] - for dev_id in dev_ids: - device = tuya.get_device_by_id(dev_id) + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] + entities: list[Entity] = [] + for device_id in device_ids: + device = device_manager.device_map[device_id] if device is None: continue - entities.append(TuyaSwitch(device, platform)) + + for function in device.function: + tuya_ha_switch = None + if device.category == "kj": + if function in [ + DPCODE_ANION, + DPCODE_FRESET, + DPCODE_LIGHT, + DPCODE_LOCK, + DPCODE_UV, + DPCODE_WET, + ]: + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + # Main device switch is handled by the Fan object + elif device.category == "cwysj": + if function in [DPCODE_FRESET, DPCODE_UV, DPCODE_PRESET, DPCODE_WRESET]: + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + + if function.startswith(DPCODE_SWITCH): + # Main device switch + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + else: + if function.startswith(DPCODE_START): + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + if function.startswith(DPCODE_SWITCH): + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + + if tuya_ha_switch is not None: + entities.append(tuya_ha_switch) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add( + tuya_ha_switch.tuya_device.id + ) return entities -class TuyaSwitch(TuyaDevice, SwitchEntity): +class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): """Tuya Switch Device.""" - def __init__(self, tuya, platform): - """Init Tuya switch device.""" - super().__init__(tuya, platform) - self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + dp_code_switch = DPCODE_SWITCH + dp_code_start = DPCODE_START + + def __init__( + self, device: TuyaDevice, device_manager: TuyaDeviceManager, dp_code: str = "" + ) -> None: + """Init TuyaHaSwitch.""" + super().__init__(device, device_manager) + + self.dp_code = dp_code + self.channel = ( + dp_code.replace(DPCODE_SWITCH, "") + if dp_code.startswith(DPCODE_SWITCH) + else dp_code + ) @property - def is_on(self): + def unique_id(self) -> str | None: + """Return a unique ID.""" + return f"{super().unique_id}{self.channel}" + + @property + def name(self) -> str | None: + """Return Tuya device name.""" + return f"{self.tuya_device.name}{self.channel}" + + @property + def is_on(self) -> bool: """Return true if switch is on.""" - return self._tuya.state() + return self.tuya_device.status.get(self.dp_code, False) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - self._tuya.turn_on() + self._send_command([{"code": self.dp_code, "value": True}]) - def turn_off(self, **kwargs): - """Turn the device off.""" - self._tuya.turn_off() + def turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + self._send_command([{"code": self.dp_code, "value": False}]) diff --git a/homeassistant/components/tuya/translations/af.json b/homeassistant/components/tuya/translations/af.json deleted file mode 100644 index 71ac741b6b8..00000000000 --- a/homeassistant/components/tuya/translations/af.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "options": { - "error": { - "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", - "dev_not_found": "Ger\u00e4t nicht gefunden" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json deleted file mode 100644 index 62fad2ad47f..00000000000 --- a/homeassistant/components/tuya/translations/ca.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "error": { - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" - }, - "flow_title": "Configuraci\u00f3 de Tuya", - "step": { - "user": { - "data": { - "country_code": "El teu codi de pa\u00eds (per exemple, 1 per l'EUA o 86 per la Xina)", - "password": "Contrasenya", - "platform": "L'aplicaci\u00f3 on es registra el teu compte", - "username": "Nom d'usuari" - }, - "description": "Introdueix les teves credencial de Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Ha fallat la connexi\u00f3" - }, - "error": { - "dev_multi_type": "Per configurar una selecci\u00f3 de m\u00faltiples dispositius, aquests han de ser del mateix tipus", - "dev_not_config": "El tipus d'aquest dispositiu no \u00e9s configurable", - "dev_not_found": "No s'ha trobat el dispositiu." - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Rang de brillantor utilitzat pel dispositiu", - "curr_temp_divider": "Divisor del valor de temperatura actual (0 = predeterminat)", - "max_kelvin": "Temperatura del color m\u00e0xima suportada, en Kelvin", - "max_temp": "Temperatura desitjada m\u00e0xima (utilitza min i max = 0 per defecte)", - "min_kelvin": "Temperatura del color m\u00ednima suportada, en Kelvin", - "min_temp": "Temperatura desitjada m\u00ednima (utilitza min i max = 0 per defecte)", - "set_temp_divided": "Utilitza el valor de temperatura dividit per a ordres de configuraci\u00f3 de temperatura", - "support_color": "For\u00e7a el suport de color", - "temp_divider": "Divisor del valor de temperatura (0 = predeterminat)", - "temp_step_override": "Pas de temperatura objectiu", - "tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu", - "unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu" - }, - "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada pel dispositiu {device_type} `{device_name}`", - "title": "Configuraci\u00f3 de dispositiu Tuya" - }, - "init": { - "data": { - "discovery_interval": "Interval de sondeig del dispositiu de descoberta, en segons", - "list_devices": "Selecciona els dispositius a configurar o deixa-ho buit per desar la configuraci\u00f3", - "query_device": "Selecciona el dispositiu que utilitzar\u00e0 m\u00e8tode de consulta, per actualitzacions d'estat m\u00e9s freq\u00fcents", - "query_interval": "Interval de sondeig de consultes del dispositiu, en segons" - }, - "description": "No estableixis valors d'interval de sondeig massa baixos ja que les crides fallaran i generaran missatges d'error al registre", - "title": "Configuraci\u00f3 d'opcions de Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json deleted file mode 100644 index 1dda4ea6df7..00000000000 --- a/homeassistant/components/tuya/translations/cs.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "error": { - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" - }, - "flow_title": "Konfigurace Tuya", - "step": { - "user": { - "data": { - "country_code": "K\u00f3d zem\u011b va\u0161eho \u00fa\u010dtu (nap\u0159. 1 pro USA nebo 86 pro \u010c\u00ednu)", - "password": "Heslo", - "platform": "Aplikace, ve kter\u00e9 m\u00e1te zaregistrovan\u00fd \u00fa\u010det", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje k Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" - }, - "error": { - "dev_multi_type": "V\u00edce vybran\u00fdch za\u0159\u00edzen\u00ed k nastaven\u00ed mus\u00ed b\u00fdt stejn\u00e9ho typu", - "dev_not_config": "Typ za\u0159\u00edzen\u00ed nelze nastavit", - "dev_not_found": "Za\u0159\u00edzen\u00ed nenalezeno" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Rozsah jasu pou\u017e\u00edvan\u00fd za\u0159\u00edzen\u00edm", - "max_kelvin": "Maxim\u00e1ln\u00ed podporovan\u00e1 teplota barev v kelvinech", - "max_temp": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 teplota (pou\u017eijte min a max = 0 jako v\u00fdchoz\u00ed)", - "min_kelvin": "Maxim\u00e1ln\u00ed podporovan\u00e1 teplota barev v kelvinech", - "min_temp": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 teplota (pou\u017eijte min a max = 0 jako v\u00fdchoz\u00ed)", - "support_color": "Vynutit podporu barev", - "tuya_max_coltemp": "Maxim\u00e1ln\u00ed teplota barev nahl\u00e1\u0161en\u00e1 za\u0159\u00edzen\u00edm", - "unit_of_measurement": "Jednotka teploty pou\u017e\u00edvan\u00e1 za\u0159\u00edzen\u00edm" - }, - "title": "Nastavte za\u0159\u00edzen\u00ed Tuya" - }, - "init": { - "data": { - "discovery_interval": "Interval objevov\u00e1n\u00ed za\u0159\u00edzen\u00ed v sekund\u00e1ch", - "list_devices": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e1 chcete nastavit, nebo ponechte pr\u00e1zdn\u00e9, abyste konfiguraci ulo\u017eili", - "query_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 bude pou\u017e\u00edvat metodu dotaz\u016f pro rychlej\u0161\u00ed aktualizaci stavu", - "query_interval": "Interval dotazov\u00e1n\u00ed za\u0159\u00edzen\u00ed v sekund\u00e1ch" - }, - "description": "Nenastavujte intervalu dotazov\u00e1n\u00ed p\u0159\u00edli\u0161 n\u00edzk\u00e9 hodnoty, jinak se dotazov\u00e1n\u00ed nezda\u0159\u00ed a bude generovat chybov\u00e9 zpr\u00e1vy do logu", - "title": "Nastavte mo\u017enosti Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json deleted file mode 100644 index 54fd3de7cbf..00000000000 --- a/homeassistant/components/tuya/translations/de.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "error": { - "invalid_auth": "Ung\u00fcltige Authentifizierung" - }, - "flow_title": "Tuya Konfiguration", - "step": { - "user": { - "data": { - "country_code": "L\u00e4ndercode deines Kontos (z. B. 1 f\u00fcr USA oder 86 f\u00fcr China)", - "password": "Passwort", - "platform": "Die App, in der dein Konto registriert ist", - "username": "Benutzername" - }, - "description": "Gib deine Tuya-Anmeldeinformationen ein.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Verbindung fehlgeschlagen" - }, - "error": { - "dev_multi_type": "Mehrere ausgew\u00e4hlte Ger\u00e4te zur Konfiguration m\u00fcssen vom gleichen Typ sein", - "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", - "dev_not_found": "Ger\u00e4t nicht gefunden" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Vom Ger\u00e4t genutzter Helligkeitsbereich", - "curr_temp_divider": "Aktueller Temperaturwert-Teiler (0 = Standard verwenden)", - "max_kelvin": "Maximal unterst\u00fctzte Farbtemperatur in Kelvin", - "max_temp": "Maximale Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", - "min_kelvin": "Minimale unterst\u00fctzte Farbtemperatur in Kelvin", - "min_temp": "Minimal Solltemperatur (f\u00fcr Voreinstellung min und max = 0 verwenden)", - "set_temp_divided": "Geteilten Temperaturwert f\u00fcr Solltemperaturbefehl verwenden", - "support_color": "Farbunterst\u00fctzung erzwingen", - "temp_divider": "Teiler f\u00fcr Temperaturwerte (0 = Standard verwenden)", - "temp_step_override": "Zieltemperaturschritt", - "tuya_max_coltemp": "Vom Ger\u00e4t gemeldete maximale Farbtemperatur", - "unit_of_measurement": "Vom Ger\u00e4t verwendete Temperatureinheit" - }, - "description": "Optionen zur Anpassung der angezeigten Informationen f\u00fcr das Ger\u00e4t `{device_name}` vom Typ: {device_type}konfigurieren", - "title": "Tuya-Ger\u00e4t konfigurieren" - }, - "init": { - "data": { - "discovery_interval": "Abfrageintervall f\u00fcr Ger\u00e4teabruf in Sekunden", - "list_devices": "W\u00e4hle die zu konfigurierenden Ger\u00e4te aus oder lasse sie leer, um die Konfiguration zu speichern", - "query_device": "W\u00e4hle ein Ger\u00e4t aus, das die Abfragemethode f\u00fcr eine schnellere Statusaktualisierung verwendet.", - "query_interval": "Ger\u00e4teabrufintervall in Sekunden" - }, - "description": "Stelle das Abfrageintervall nicht zu niedrig ein, sonst schlagen die Aufrufe fehl und erzeugen eine Fehlermeldung im Protokoll", - "title": "Tuya-Optionen konfigurieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index ee304ff30cd..631f0b7172f 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -1,65 +1,29 @@ { "config": { - "abort": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, "error": { "invalid_auth": "Invalid authentication" }, "flow_title": "Tuya configuration", "step": { - "user": { + "user":{ + "title":"Tuya Integration", + "data":{ + "tuya_project_type": "Tuya cloud project type" + } + }, + "login": { "data": { - "country_code": "Your account country code (e.g., 1 for USA or 86 for China)", - "password": "Password", - "platform": "The app where your account is registered", - "username": "Username" + "endpoint": "Availability Zone", + "access_id": "Access ID", + "access_secret": "Access Secret", + "tuya_app_type": "Mobile App", + "country_code": "Country Code", + "username": "Account", + "password": "Password" }, - "description": "Enter your Tuya credentials.", + "description": "Enter your Tuya credential.", "title": "Tuya" } } - }, - "options": { - "abort": { - "cannot_connect": "Failed to connect" - }, - "error": { - "dev_multi_type": "Multiple selected devices to configure must be of the same type", - "dev_not_config": "Device type not configurable", - "dev_not_found": "Device not found" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Brightness range used by device", - "curr_temp_divider": "Current Temperature value divider (0 = use default)", - "max_kelvin": "Max color temperature supported in kelvin", - "max_temp": "Max target temperature (use min and max = 0 for default)", - "min_kelvin": "Min color temperature supported in kelvin", - "min_temp": "Min target temperature (use min and max = 0 for default)", - "set_temp_divided": "Use divided Temperature value for set temperature command", - "support_color": "Force color support", - "temp_divider": "Temperature values divider (0 = use default)", - "temp_step_override": "Target Temperature step", - "tuya_max_coltemp": "Max color temperature reported by device", - "unit_of_measurement": "Temperature unit used by device" - }, - "description": "Configure options to adjust displayed information for {device_type} device `{device_name}`", - "title": "Configure Tuya Device" - }, - "init": { - "data": { - "discovery_interval": "Discovery device polling interval in seconds", - "list_devices": "Select the devices to configure or leave empty to save configuration", - "query_device": "Select device that will use query method for faster status update", - "query_interval": "Query device polling interval in seconds" - }, - "description": "Do not set pollings interval values too low or the calls will fail generating error message in the log", - "title": "Configure Tuya Options" - } - } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json deleted file mode 100644 index 9c57a216888..00000000000 --- a/homeassistant/components/tuya/translations/es.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "error": { - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" - }, - "flow_title": "Configuraci\u00f3n Tuya", - "step": { - "user": { - "data": { - "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", - "password": "Contrase\u00f1a", - "platform": "La aplicaci\u00f3n en la cual registraste tu cuenta", - "username": "Usuario" - }, - "description": "Introduce tu credencial Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "No se pudo conectar" - }, - "error": { - "dev_multi_type": "Los m\u00faltiples dispositivos seleccionados para configurar deben ser del mismo tipo", - "dev_not_config": "Tipo de dispositivo no configurable", - "dev_not_found": "Dispositivo no encontrado" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Rango de brillo utilizado por el dispositivo", - "curr_temp_divider": "Divisor del valor de la temperatura actual (0 = usar valor por defecto)", - "max_kelvin": "Temperatura de color m\u00e1xima admitida en kelvin", - "max_temp": "Temperatura objetivo m\u00e1xima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", - "min_kelvin": "Temperatura de color m\u00ednima soportada en kelvin", - "min_temp": "Temperatura objetivo m\u00ednima (usa m\u00edn. y m\u00e1x. = 0 por defecto)", - "set_temp_divided": "Use el valor de temperatura dividido para el comando de temperatura establecida", - "support_color": "Forzar soporte de color", - "temp_divider": "Divisor de los valores de temperatura (0 = usar valor por defecto)", - "temp_step_override": "Temperatura deseada", - "tuya_max_coltemp": "Temperatura de color m\u00e1xima notificada por dispositivo", - "unit_of_measurement": "Unidad de temperatura utilizada por el dispositivo" - }, - "description": "Configura las opciones para ajustar la informaci\u00f3n mostrada para {device_type} dispositivo `{device_name}`", - "title": "Configurar dispositivo Tuya" - }, - "init": { - "data": { - "discovery_interval": "Intervalo de sondeo del descubrimiento al dispositivo en segundos", - "list_devices": "Selecciona los dispositivos a configurar o d\u00e9jalos en blanco para guardar la configuraci\u00f3n", - "query_device": "Selecciona el dispositivo que utilizar\u00e1 el m\u00e9todo de consulta para una actualizaci\u00f3n de estado m\u00e1s r\u00e1pida", - "query_interval": "Intervalo de sondeo de la consulta al dispositivo en segundos" - }, - "description": "No establezcas valores de intervalo de sondeo demasiado bajos o las llamadas fallar\u00e1n generando un mensaje de error en el registro", - "title": "Configurar opciones de Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json deleted file mode 100644 index 48161f552b8..00000000000 --- a/homeassistant/components/tuya/translations/et.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamise viga", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." - }, - "error": { - "invalid_auth": "Tuvastamise viga" - }, - "flow_title": "Tuya seaded", - "step": { - "user": { - "data": { - "country_code": "Konto riigikood (nt 1 USA v\u00f5i 372 Eesti)", - "password": "Salas\u00f5na", - "platform": "\u00c4pp kus konto registreeriti", - "username": "Kasutajanimi" - }, - "description": "Sisesta oma Tuya konto andmed.", - "title": "" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u00dchendamine nurjus" - }, - "error": { - "dev_multi_type": "Mitu h\u00e4\u00e4lestatavat seadet peavad olema sama t\u00fc\u00fcpi", - "dev_not_config": "Seda t\u00fc\u00fcpi seade pole seadistatav", - "dev_not_found": "Seadet ei leitud" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Seadme kasutatav heledusvahemik", - "curr_temp_divider": "Praeguse temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)", - "max_kelvin": "Maksimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)", - "max_temp": "Maksimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", - "min_kelvin": "Minimaalne v\u00f5imalik v\u00e4rvitemperatuur (Kelvinites)", - "min_temp": "Minimaalne sihttemperatuur (vaikimisi kasuta min ja max = 0)", - "set_temp_divided": "M\u00e4\u00e4ratud temperatuuri k\u00e4su jaoks kasuta jagatud temperatuuri v\u00e4\u00e4rtust", - "support_color": "Luba v\u00e4rvuse juhtimine", - "temp_divider": "Temperatuuri v\u00e4\u00e4rtuse eraldaja (0 = kasuta vaikev\u00e4\u00e4rtust)", - "temp_step_override": "Sihttemperatuuri samm", - "tuya_max_coltemp": "Seadme teatatud maksimaalne v\u00e4rvitemperatuur", - "unit_of_measurement": "Seadme temperatuuri\u00fchik" - }, - "description": "Suvandid \u00fcksuse {device_type} {device_name} kuvatava teabe muutmiseks", - "title": "H\u00e4\u00e4lesta Tuya seade" - }, - "init": { - "data": { - "discovery_interval": "Seadme leidmisp\u00e4ringute intervall (sekundites)", - "list_devices": "Vali seadistatavad seadmed v\u00f5i j\u00e4ta s\u00e4tete salvestamiseks t\u00fchjaks", - "query_device": "Vali seade, mis kasutab oleku kiiremaks v\u00e4rskendamiseks p\u00e4ringumeetodit", - "query_interval": "P\u00e4ringute intervall (sekundites)" - }, - "description": "\u00c4ra m\u00e4\u00e4ra k\u00fcsitlusintervalli v\u00e4\u00e4rtusi liiga madalaks, vastasel korral v\u00f5ivad p\u00e4ringud logis t\u00f5rketeate genereerida", - "title": "Tuya suvandite seadistamine" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fi.json b/homeassistant/components/tuya/translations/fi.json deleted file mode 100644 index 3c74a9b8eeb..00000000000 --- a/homeassistant/components/tuya/translations/fi.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "flow_title": "Tuya-asetukset", - "step": { - "user": { - "data": { - "country_code": "Tilisi maakoodi (esim. 1 Yhdysvalloissa, 358 Suomessa)", - "password": "Salasana", - "platform": "Sovellus, johon tili rekister\u00f6id\u00e4\u00e4n", - "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - }, - "description": "Anna Tuya-tunnistetietosi.", - "title": "Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json deleted file mode 100644 index b741d3f9377..00000000000 --- a/homeassistant/components/tuya/translations/fr.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "error": { - "invalid_auth": "Authentification invalide" - }, - "flow_title": "Configuration Tuya", - "step": { - "user": { - "data": { - "country_code": "Le code de pays de votre compte (par exemple, 1 pour les \u00c9tats-Unis ou 86 pour la Chine)", - "password": "Mot de passe", - "platform": "L'application dans laquelle votre compte est enregistr\u00e9", - "username": "Nom d'utilisateur" - }, - "description": "Saisissez vos informations d'identification Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u00c9chec de connexion" - }, - "error": { - "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", - "dev_not_config": "Type d'appareil non configurable", - "dev_not_found": "Appareil non trouv\u00e9" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Plage de luminosit\u00e9 utilis\u00e9e par l'appareil", - "curr_temp_divider": "Diviseur de valeur de temp\u00e9rature actuelle (0 = utiliser la valeur par d\u00e9faut)", - "max_kelvin": "Temp\u00e9rature de couleur maximale prise en charge en Kelvin", - "max_temp": "Temp\u00e9rature cible maximale (utilisez min et max = 0 par d\u00e9faut)", - "min_kelvin": "Temp\u00e9rature de couleur minimale prise en charge en kelvin", - "min_temp": "Temp\u00e9rature cible minimale (utilisez min et max = 0 par d\u00e9faut)", - "set_temp_divided": "Utilisez la valeur de temp\u00e9rature divis\u00e9e pour la commande de temp\u00e9rature d\u00e9finie", - "support_color": "Forcer la prise en charge des couleurs", - "temp_divider": "Diviseur de valeurs de temp\u00e9rature (0 = utiliser la valeur par d\u00e9faut)", - "temp_step_override": "Pas de temp\u00e9rature cible", - "tuya_max_coltemp": "Temp\u00e9rature de couleur maximale rapport\u00e9e par l'appareil", - "unit_of_measurement": "Unit\u00e9 de temp\u00e9rature utilis\u00e9e par l'appareil" - }, - "description": "Configurer les options pour ajuster les informations affich\u00e9es pour l'appareil {device_type} ` {device_name} `", - "title": "Configurer l'appareil Tuya" - }, - "init": { - "data": { - "discovery_interval": "Intervalle de d\u00e9couverte de l'appareil en secondes", - "list_devices": "S\u00e9lectionnez les appareils \u00e0 configurer ou laissez vide pour enregistrer la configuration", - "query_device": "S\u00e9lectionnez l'appareil qui utilisera la m\u00e9thode de requ\u00eate pour une mise \u00e0 jour plus rapide de l'\u00e9tat", - "query_interval": "Intervalle d'interrogation de l'appareil en secondes" - }, - "description": "Ne d\u00e9finissez pas des valeurs d'intervalle d'interrogation trop faibles ou les appels \u00e9choueront \u00e0 g\u00e9n\u00e9rer un message d'erreur dans le journal", - "title": "Configurer les options de Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json deleted file mode 100644 index 44a7699e511..00000000000 --- a/homeassistant/components/tuya/translations/he.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "error": { - "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" - }, - "flow_title": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d8\u05d5\u05d9\u05d4", - "step": { - "user": { - "data": { - "country_code": "\u05e7\u05d5\u05d3 \u05de\u05d3\u05d9\u05e0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da (\u05dc\u05de\u05e9\u05dc, 1 \u05dc\u05d0\u05e8\u05d4\"\u05d1 \u05d0\u05d5 972 \u05dc\u05d9\u05e9\u05e8\u05d0\u05dc)", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "platform": "\u05d4\u05d9\u05d9\u05e9\u05d5\u05dd \u05d1\u05d5 \u05e8\u05e9\u05d5\u05dd \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - }, - "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d8\u05d5\u05d9\u05d4 \u05e9\u05dc\u05da.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json deleted file mode 100644 index 054e6443d2a..00000000000 --- a/homeassistant/components/tuya/translations/hu.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "error": { - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" - }, - "flow_title": "Tuya konfigur\u00e1ci\u00f3", - "step": { - "user": { - "data": { - "country_code": "A fi\u00f3k orsz\u00e1gk\u00f3dja (pl. 1 USA, 36 Magyarorsz\u00e1g, vagy 86 K\u00edna)", - "password": "Jelsz\u00f3", - "platform": "Az alkalmaz\u00e1s, ahol a fi\u00f3k regisztr\u00e1lt", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt" - }, - "error": { - "dev_multi_type": "T\u00f6bb kiv\u00e1lasztott konfigur\u00e1land\u00f3 eszk\u00f6znek azonos t\u00edpus\u00fanak kell lennie", - "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", - "dev_not_found": "Eszk\u00f6z nem tal\u00e1lhat\u00f3" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt f\u00e9nyer\u0151 tartom\u00e1ny", - "curr_temp_divider": "Aktu\u00e1lis h\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9k oszt\u00f3 (0 = alap\u00e9rtelmezetten)", - "max_kelvin": "Maxim\u00e1lis t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", - "max_temp": "Maxim\u00e1lis c\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (haszn\u00e1lja a min-t \u00e9s a max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", - "min_kelvin": "Minimum t\u00e1mogatott sz\u00ednh\u0151m\u00e9rs\u00e9klet kelvinben", - "min_temp": "Min. C\u00e9l-sz\u00ednh\u0151m\u00e9rs\u00e9klet (alap\u00e9rtelmez\u00e9s szerint haszn\u00e1ljon min-t \u00e9s max-ot = 0-t az alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1shoz)", - "set_temp_divided": "A h\u0151m\u00e9rs\u00e9klet be\u00e1ll\u00edt\u00e1s\u00e1hoz osztott h\u0151m\u00e9rs\u00e9kleti \u00e9rt\u00e9ket haszn\u00e1ljon", - "support_color": "Sz\u00ednt\u00e1mogat\u00e1s k\u00e9nyszer\u00edt\u00e9se", - "temp_divider": "Sz\u00ednh\u0151m\u00e9rs\u00e9klet-\u00e9rt\u00e9kek oszt\u00f3ja (0 = alap\u00e9rtelmezett)", - "temp_step_override": "C\u00e9lh\u0151m\u00e9rs\u00e9klet l\u00e9pcs\u0151", - "tuya_max_coltemp": "Az eszk\u00f6z \u00e1ltal megadott maxim\u00e1lis sz\u00ednh\u0151m\u00e9rs\u00e9klet", - "unit_of_measurement": "Az eszk\u00f6z \u00e1ltal haszn\u00e1lt h\u0151m\u00e9rs\u00e9kleti egys\u00e9g" - }, - "description": "Konfigur\u00e1l\u00e1si lehet\u0151s\u00e9gek a(z) {device_type} t\u00edpus\u00fa `{device_name}` eszk\u00f6z megjelen\u00edtett inform\u00e1ci\u00f3inak be\u00e1ll\u00edt\u00e1s\u00e1hoz", - "title": "Tuya eszk\u00f6z konfigur\u00e1l\u00e1sa" - }, - "init": { - "data": { - "discovery_interval": "Felfedez\u0151 eszk\u00f6z lek\u00e9rdez\u00e9si intervalluma m\u00e1sodpercben", - "list_devices": "V\u00e1laszd ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6z\u00f6ket, vagy hagyd \u00fcresen a konfigur\u00e1ci\u00f3 ment\u00e9s\u00e9hez", - "query_device": "V\u00e1laszd ki azt az eszk\u00f6zt, amely a lek\u00e9rdez\u00e9si m\u00f3dszert haszn\u00e1lja a gyorsabb \u00e1llapotfriss\u00edt\u00e9shez", - "query_interval": "Eszk\u00f6z lek\u00e9rdez\u00e9si id\u0151k\u00f6ze m\u00e1sodpercben" - }, - "description": "Ne \u00e1ll\u00edtsd t\u00fal alacsonyra a lek\u00e9rdez\u00e9si intervallum \u00e9rt\u00e9keit, k\u00fcl\u00f6nben a h\u00edv\u00e1sok nem fognak hiba\u00fczenetet gener\u00e1lni a napl\u00f3ban", - "title": "Tuya be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json deleted file mode 100644 index bb338e12752..00000000000 --- a/homeassistant/components/tuya/translations/id.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "error": { - "invalid_auth": "Autentikasi tidak valid" - }, - "flow_title": "Konfigurasi Tuya", - "step": { - "user": { - "data": { - "country_code": "Kode negara akun Anda (mis., 1 untuk AS atau 86 untuk China)", - "password": "Kata Sandi", - "platform": "Aplikasi tempat akun Anda mendaftar", - "username": "Nama Pengguna" - }, - "description": "Masukkan kredensial Tuya Anda.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Gagal terhubung" - }, - "error": { - "dev_multi_type": "Untuk konfigurasi sekaligus, beberapa perangkat yang dipilih harus berjenis sama", - "dev_not_config": "Jenis perangkat tidak dapat dikonfigurasi", - "dev_not_found": "Perangkat tidak ditemukan" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Rentang kecerahan yang digunakan oleh perangkat", - "curr_temp_divider": "Pembagi nilai suhu saat ini (0 = gunakan bawaan)", - "max_kelvin": "Suhu warna maksimal yang didukung dalam Kelvin", - "max_temp": "Suhu target maksimal (gunakan min dan maks = 0 untuk bawaan)", - "min_kelvin": "Suhu warna minimal yang didukung dalam Kelvin", - "min_temp": "Suhu target minimal (gunakan min dan maks = 0 untuk bawaan)", - "set_temp_divided": "Gunakan nilai suhu terbagi untuk mengirimkan perintah mengatur suhu", - "support_color": "Paksa dukungan warna", - "temp_divider": "Pembagi nilai suhu (0 = gunakan bawaan)", - "temp_step_override": "Langkah Suhu Target", - "tuya_max_coltemp": "Suhu warna maksimal yang dilaporkan oleh perangkat", - "unit_of_measurement": "Satuan suhu yang digunakan oleh perangkat" - }, - "description": "Konfigurasikan opsi untuk menyesuaikan informasi yang ditampilkan untuk perangkat {device_type} `{device_name}`", - "title": "Konfigurasi Perangkat Tuya" - }, - "init": { - "data": { - "discovery_interval": "Interval polling penemuan perangkat dalam detik", - "list_devices": "Pilih perangkat yang akan dikonfigurasi atau biarkan kosong untuk menyimpan konfigurasi", - "query_device": "Pilih perangkat yang akan menggunakan metode kueri untuk pembaruan status lebih cepat", - "query_interval": "Interval polling perangkat kueri dalam detik" - }, - "description": "Jangan atur nilai interval polling terlalu rendah karena panggilan akan gagal menghasilkan pesan kesalahan dalam log", - "title": "Konfigurasikan Opsi Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json deleted file mode 100644 index a2a8dc87473..00000000000 --- a/homeassistant/components/tuya/translations/it.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "error": { - "invalid_auth": "Autenticazione non valida" - }, - "flow_title": "Configurazione di Tuya", - "step": { - "user": { - "data": { - "country_code": "Prefisso internazionale del tuo account (ad es. 1 per gli Stati Uniti o 86 per la Cina)", - "password": "Password", - "platform": "L'app in cui \u00e8 registrato il tuo account", - "username": "Nome utente" - }, - "description": "Inserisci le tue credenziali Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Impossibile connettersi" - }, - "error": { - "dev_multi_type": "Pi\u00f9 dispositivi selezionati da configurare devono essere dello stesso tipo", - "dev_not_config": "Tipo di dispositivo non configurabile", - "dev_not_found": "Dispositivo non trovato" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Intervallo di luminosit\u00e0 utilizzato dal dispositivo", - "curr_temp_divider": "Divisore del valore della temperatura corrente (0 = usa il valore predefinito)", - "max_kelvin": "Temperatura colore massima supportata in kelvin", - "max_temp": "Temperatura di destinazione massima (utilizzare min e max = 0 per impostazione predefinita)", - "min_kelvin": "Temperatura colore minima supportata in kelvin", - "min_temp": "Temperatura di destinazione minima (utilizzare min e max = 0 per impostazione predefinita)", - "set_temp_divided": "Utilizzare il valore temperatura diviso per impostare il comando temperatura", - "support_color": "Forza il supporto del colore", - "temp_divider": "Divisore dei valori di temperatura (0 = utilizzare il valore predefinito)", - "temp_step_override": "Passo della temperatura da raggiungere", - "tuya_max_coltemp": "Temperatura di colore massima riportata dal dispositivo", - "unit_of_measurement": "Unit\u00e0 di temperatura utilizzata dal dispositivo" - }, - "description": "Configura le opzioni per regolare le informazioni visualizzate per il dispositivo {device_type} `{device_name}`", - "title": "Configura il dispositivo Tuya" - }, - "init": { - "data": { - "discovery_interval": "Intervallo di scansione di rilevamento dispositivo in secondi", - "list_devices": "Selezionare i dispositivi da configurare o lasciare vuoto per salvare la configurazione", - "query_device": "Selezionare il dispositivo che utilizzer\u00e0 il metodo di interrogazione per un pi\u00f9 rapido aggiornamento dello stato", - "query_interval": "Intervallo di scansione di interrogazione dispositivo in secondi" - }, - "description": "Non impostare valori dell'intervallo di scansione troppo bassi o le chiamate non riusciranno a generare un messaggio di errore nel registro", - "title": "Configura le opzioni Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json deleted file mode 100644 index 7c80ef1ffba..00000000000 --- a/homeassistant/components/tuya/translations/ka.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "options": { - "error": { - "dev_multi_type": "\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8 \u10db\u10e0\u10d0\u10d5\u10da\u10dd\u10d1\u10d8\u10d7\u10d8 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10dc\u10d3\u10d0 \u10d8\u10e7\u10dd\u10e1 \u10d4\u10e0\u10d7\u10dc\u10d0\u10d8\u10e0\u10d8 \u10e2\u10d8\u10de\u10d8\u10e1", - "dev_not_config": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0\u10d3\u10d8", - "dev_not_found": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e1\u10d8\u10d9\u10d0\u10e8\u10d9\u10d0\u10e8\u10d8\u10e1 \u10d3\u10d8\u10d0\u10de\u10d0\u10d6\u10dd\u10dc\u10d8", - "curr_temp_divider": "\u10db\u10d8\u10db\u10d3\u10d8\u10dc\u10d0\u10e0\u10d4 \u10e2\u10d4\u10db\u10d4\u10de\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 - \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", - "max_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", - "max_temp": "\u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", - "min_kelvin": "\u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d8\u10da\u10d8 \u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d8\u10e1 \u10e4\u10d4\u10e0\u10d8 \u10d9\u10d4\u10da\u10d5\u10d8\u10dc\u10d4\u10d1\u10e8\u10d8", - "min_temp": "\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10db\u10d8\u10d6\u10dc\u10dd\u10d1\u10e0\u10d8\u10d5\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0 (\u10db\u10d8\u10dc\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10d3\u10d0 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8\u10e1\u10d0\u10d7\u10d5\u10d8\u10e1 \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8\u10d0 0)", - "support_color": "\u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10ee\u10d0\u10e0\u10d3\u10d0\u10ed\u10d4\u10e0\u10d0 \u10d8\u10eb\u10e3\u10da\u10d4\u10d1\u10d8\u10d7", - "temp_divider": "\u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10d8\u10e1 \u10d2\u10d0\u10db\u10e7\u10dd\u10e4\u10d8 (0 = \u10dc\u10d0\u10d2\u10e3\u10da\u10d8\u10e1\u10ee\u10db\u10d4\u10d5\u10d8)", - "tuya_max_coltemp": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10db\u10dd\u10ec\u10dd\u10d3\u10d4\u10d1\u10e3\u10da\u10d8 \u10e4\u10d4\u10e0\u10d8\u10e1 \u10db\u10d0\u10e5\u10e1\u10d8\u10db\u10d0\u10da\u10e3\u10e0\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10d0", - "unit_of_measurement": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10db\u10d8\u10d4\u10e0 \u10d2\u10d0\u10db\u10dd\u10e7\u10d4\u10dc\u10d4\u10d1\u10e3\u10da\u10d8 \u10e2\u10d4\u10db\u10de\u10d4\u10e0\u10d0\u10e2\u10e3\u10e0\u10e3\u10da\u10d8 \u10d4\u10e0\u10d7\u10d4\u10e3\u10da\u10d8" - }, - "description": "\u10d3\u10d0\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d3 {device_type} `{device_name}` \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10d4\u10e0\u10d1\u10d8 \u10d8\u10dc\u10e4\u10dd\u10e0\u10db\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e9\u10d5\u10d4\u10dc\u10d4\u10d1\u10d8\u10e1 \u10db\u10dd\u10e1\u10d0\u10e0\u10d2\u10d4\u10d1\u10d0\u10d3", - "title": "Tuya-\u10e1 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" - }, - "init": { - "data": { - "discovery_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d0\u10e6\u10db\u10dd\u10e9\u10d4\u10dc\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8", - "list_devices": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d0\u10dc \u10d3\u10d0\u10e2\u10dd\u10d5\u10d4\u10d7 \u10ea\u10d0\u10e0\u10d8\u10d4\u10da\u10d8 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d0\u10ea\u10d8\u10d8\u10e1 \u10e8\u10d4\u10e1\u10d0\u10dc\u10d0\u10ee\u10d0\u10d3", - "query_device": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0, \u10e0\u10dd\u10db\u10d4\u10da\u10d8\u10ea \u10d2\u10d0\u10db\u10dd\u10d8\u10e7\u10d4\u10dc\u10d4\u10d1\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10db\u10d4\u10d7\u10dd\u10d3\u10e1 \u10e1\u10e2\u10d0\u10e2\u10e3\u10e1\u10d8\u10e1 \u10e1\u10ec\u10e0\u10d0\u10e4\u10d8 \u10d2\u10d0\u10dc\u10d0\u10ee\u10da\u10d4\u10d1\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1", - "query_interval": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10db\u10dd\u10d7\u10ee\u10dd\u10d5\u10dc\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8 \u10ec\u10d0\u10db\u10d4\u10d1\u10e8\u10d8" - }, - "description": "\u10d0\u10e0 \u10d3\u10d0\u10d0\u10e7\u10d4\u10dc\u10dd\u10d7 \u10d2\u10d0\u10db\u10dd\u10d9\u10d8\u10d7\u10ee\u10d5\u10d8\u10e1 \u10d8\u10dc\u10e2\u10d4\u10e0\u10d5\u10d0\u10da\u10d8\u10e1 \u10db\u10dc\u10d8\u10e8\u10d5\u10dc\u10d4\u10da\u10dd\u10d1\u10d4\u10d1\u10d8 \u10eb\u10d0\u10da\u10d8\u10d0\u10dc \u10db\u10ea\u10d8\u10e0\u10d4 \u10d7\u10dd\u10e0\u10d4\u10d1 \u10d2\u10d0\u10db\u10dd\u10eb\u10d0\u10ee\u10d4\u10d1\u10d4\u10d1\u10d8 \u10d3\u10d0\u10d0\u10d2\u10d4\u10dc\u10d4\u10e0\u10d8\u10e0\u10d4\u10d1\u10d4\u10dc \u10e8\u10d4\u10ea\u10d3\u10dd\u10db\u10d4\u10d1\u10e1 \u10da\u10dd\u10d2\u10e8\u10d8", - "title": "Tuya-\u10e1 \u10de\u10d0\u10e0\u10d0\u10db\u10d4\u10e2\u10e0\u10d4\u10d1\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ko.json b/homeassistant/components/tuya/translations/ko.json deleted file mode 100644 index afa2541e7b9..00000000000 --- a/homeassistant/components/tuya/translations/ko.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "flow_title": "Tuya \uad6c\uc131\ud558\uae30", - "step": { - "user": { - "data": { - "country_code": "\uacc4\uc815 \uad6d\uac00 \ucf54\ub4dc (\uc608 : \ubbf8\uad6d\uc758 \uacbd\uc6b0 1, \uc911\uad6d\uc758 \uacbd\uc6b0 86)", - "password": "\ube44\ubc00\ubc88\ud638", - "platform": "\uacc4\uc815\uc774 \ub4f1\ub85d\ub41c \uc571", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "description": "Tuya \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" - }, - "error": { - "dev_multi_type": "\uc120\ud0dd\ud55c \uc5ec\ub7ec \uae30\uae30\ub97c \uad6c\uc131\ud558\ub824\uba74 \uc720\ud615\uc774 \ub3d9\uc77c\ud574\uc57c \ud569\ub2c8\ub2e4", - "dev_not_config": "\uae30\uae30 \uc720\ud615\uc744 \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "dev_not_found": "\uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \ubc1d\uae30 \ubc94\uc704", - "curr_temp_divider": "\ud604\uc7ac \uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", - "max_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\ub300 \uc0c9\uc628\ub3c4", - "max_temp": "\ucd5c\ub300 \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", - "min_kelvin": "\uce98\ube48 \ub2e8\uc704\uc758 \ucd5c\uc18c \uc0c9\uc628\ub3c4", - "min_temp": "\ucd5c\uc18c \ubaa9\ud45c \uc628\ub3c4 (\uae30\ubcf8\uac12\uc758 \uacbd\uc6b0 \ucd5c\uc19f\uac12 \ubc0f \ucd5c\ub313\uac12 = 0)", - "set_temp_divided": "\uc124\uc815 \uc628\ub3c4 \uba85\ub839\uc5d0 \ubd84\ud560\ub41c \uc628\ub3c4 \uac12 \uc0ac\uc6a9\ud558\uae30", - "support_color": "\uc0c9\uc0c1 \uc9c0\uc6d0 \uac15\uc81c \uc801\uc6a9\ud558\uae30", - "temp_divider": "\uc628\ub3c4 \uac12 \ubd84\ud560 (0 = \uae30\ubcf8\uac12 \uc0ac\uc6a9)", - "temp_step_override": "\ud76c\ub9dd \uc628\ub3c4 \ub2e8\uacc4", - "tuya_max_coltemp": "\uae30\uae30\uc5d0\uc11c \ubcf4\uace0\ud55c \ucd5c\ub300 \uc0c9\uc628\ub3c4", - "unit_of_measurement": "\uae30\uae30\uc5d0\uc11c \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704" - }, - "description": "{device_type} `{device_name}` \uae30\uae30\uc5d0 \ub300\ud574 \ud45c\uc2dc\ub418\ub294 \uc815\ubcf4\ub97c \uc870\uc815\ud558\ub294 \uc635\uc158 \uad6c\uc131\ud558\uae30", - "title": "Tuya \uae30\uae30 \uad6c\uc131\ud558\uae30" - }, - "init": { - "data": { - "discovery_interval": "\uae30\uae30 \uac80\uc0c9 \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)", - "list_devices": "\uad6c\uc131\uc744 \uc800\uc7a5\ud558\ub824\uba74 \uad6c\uc131\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uac70\ub098 \ube44\uc6cc \ub450\uc138\uc694", - "query_device": "\ube60\ub978 \uc0c1\ud0dc \uc5c5\ub370\uc774\ud2b8\ub97c \uc704\ud574 \ucffc\ub9ac \ubc29\ubc95\uc744 \uc0ac\uc6a9\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "query_interval": "\uae30\uae30 \ucffc\ub9ac \ud3f4\ub9c1 \uac04\uaca9 (\ucd08)" - }, - "description": "\ud3f4\ub9c1 \uac04\uaca9 \uac12\uc744 \ub108\ubb34 \ub0ae\uac8c \uc124\uc815\ud558\uc9c0 \ub9d0\uc544 \uc8fc\uc138\uc694. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ud638\ucd9c\uc5d0 \uc2e4\ud328\ud558\uace0 \ub85c\uadf8\uc5d0 \uc624\ub958 \uba54\uc2dc\uc9c0\uac00 \uc0dd\uc131\ub429\ub2c8\ub2e4.", - "title": "Tuya \uc635\uc158 \uad6c\uc131\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json deleted file mode 100644 index 0000f9ef6e6..00000000000 --- a/homeassistant/components/tuya/translations/lb.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Feeler beim verbannen", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." - }, - "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun" - }, - "flow_title": "Tuya Konfiguratioun", - "step": { - "user": { - "data": { - "country_code": "De L\u00e4nner Code fir d\u00e4i Kont (beispill 1 fir USA oder 86 fir China)", - "password": "Passwuert", - "platform": "d'App wou den Kont registr\u00e9iert ass", - "username": "Benotzernumm" - }, - "description": "F\u00ebll deng Tuya Umeldungs Informatiounen aus.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Feeler beim verbannen" - }, - "error": { - "dev_multi_type": "Multiple ausgewielte Ger\u00e4ter fir ze konfigur\u00e9ieren musse vum selwechten Typ sinn", - "dev_not_config": "Typ vun Apparat net konfigur\u00e9ierbar", - "dev_not_found": "Apparat net fonnt" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Hellegkeetsber\u00e4ich vum Apparat", - "curr_temp_divider": "Aktuell Temperatur W\u00e4erter Deeler (0= benotz Standard)", - "max_kelvin": "Maximal Faarftemperatur \u00ebnnerst\u00ebtzt a Kelvin", - "max_temp": "Maximal Zil Temperatur (benotz min a max = 0 fir standard)", - "min_kelvin": "Minimal Faarftemperatur \u00ebnnerst\u00ebtzt a Kelvin", - "min_temp": "Minimal Zil Temperatur (benotz min a max = 0 fir standard)", - "support_color": "Forc\u00e9ier Faarf \u00cbnnerst\u00ebtzung", - "temp_divider": "Temperatur W\u00e4erter Deeler (0= benotz Standard)", - "tuya_max_coltemp": "Max Faarftemperatur vum Apparat gemellt", - "unit_of_measurement": "Temperatur Eenheet vum Apparat" - }, - "description": "Konfigur\u00e9ier Optioune fir ugewisen Informatioune fir {device_type} Apparat `{device_name}` unzepassen", - "title": "Tuya Apparat ariichten" - }, - "init": { - "data": { - "list_devices": "Wiel d'Apparater fir ze konfigur\u00e9ieren aus oder loss se eidel fir d'Konfiguratioun ze sp\u00e4icheren" - }, - "title": "Tuya Optioune konfigur\u00e9ieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json deleted file mode 100644 index 56b2ae8236f..00000000000 --- a/homeassistant/components/tuya/translations/nl.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie", - "single_instance_allowed": "Al geconfigureerd. Er is maar een configuratie mogelijk." - }, - "error": { - "invalid_auth": "Ongeldige authenticatie" - }, - "flow_title": "Tuya-configuratie", - "step": { - "user": { - "data": { - "country_code": "De landcode van uw account (bijvoorbeeld 1 voor de VS of 86 voor China)", - "password": "Wachtwoord", - "platform": "De app waar uw account is geregistreerd", - "username": "Gebruikersnaam" - }, - "description": "Voer uw Tuya-inloggegevens in.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Kan geen verbinding maken" - }, - "error": { - "dev_multi_type": "Meerdere geselecteerde apparaten om te configureren moeten van hetzelfde type zijn", - "dev_not_config": "Apparaattype kan niet worden geconfigureerd", - "dev_not_found": "Apparaat niet gevonden" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Helderheidsbereik gebruikt door apparaat", - "curr_temp_divider": "Huidige temperatuurwaarde deler (0 = standaardwaarde)", - "max_kelvin": "Max kleurtemperatuur in kelvin", - "max_temp": "Maximale doeltemperatuur (gebruik min en max = 0 voor standaardwaarde)", - "min_kelvin": "Minimaal ondersteunde kleurtemperatuur in kelvin", - "min_temp": "Min. gewenste temperatuur (gebruik min en max = 0 voor standaard)", - "set_temp_divided": "Gedeelde temperatuurwaarde gebruiken voor ingestelde temperatuuropdracht", - "support_color": "Forceer kleurenondersteuning", - "temp_divider": "Temperatuurwaarde deler (0 = standaardwaarde)", - "temp_step_override": "Doeltemperatuur stap", - "tuya_max_coltemp": "Max. kleurtemperatuur gerapporteerd door apparaat", - "unit_of_measurement": "Temperatuureenheid gebruikt door apparaat" - }, - "description": "Configureer opties om weergegeven informatie aan te passen voor {device_type} apparaat `{device_name}`", - "title": "Configureer Tuya Apparaat" - }, - "init": { - "data": { - "discovery_interval": "Polling-interval van nieuwe apparaten in seconden", - "list_devices": "Selecteer de te configureren apparaten of laat leeg om de configuratie op te slaan", - "query_device": "Selecteer apparaat dat query-methode zal gebruiken voor snellere statusupdate", - "query_interval": "Peilinginterval van het apparaat in seconden" - }, - "description": "Stel de waarden voor het pollinginterval niet te laag in, anders zullen de oproepen geen foutmelding in het logboek genereren", - "title": "Configureer Tuya opties" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json deleted file mode 100644 index eedf24be696..00000000000 --- a/homeassistant/components/tuya/translations/no.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "error": { - "invalid_auth": "Ugyldig godkjenning" - }, - "flow_title": "Tuya konfigurasjon", - "step": { - "user": { - "data": { - "country_code": "Din landskode for kontoen din (f.eks. 1 for USA eller 86 for Kina)", - "password": "Passord", - "platform": "Appen der kontoen din er registrert", - "username": "Brukernavn" - }, - "description": "Angi Tuya-legitimasjonen din.", - "title": "" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Tilkobling mislyktes" - }, - "error": { - "dev_multi_type": "Flere valgte enheter som skal konfigureres, m\u00e5 v\u00e6re av samme type", - "dev_not_config": "Enhetstype kan ikke konfigureres", - "dev_not_found": "Finner ikke enheten" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Lysstyrkeomr\u00e5de som brukes av enheten", - "curr_temp_divider": "N\u00e5v\u00e6rende temperaturverdi (0 = bruk standard)", - "max_kelvin": "Maks fargetemperatur st\u00f8ttet i kelvin", - "max_temp": "Maks m\u00e5ltemperatur (bruk min og maks = 0 for standard)", - "min_kelvin": "Min fargetemperatur st\u00f8ttet i kelvin", - "min_temp": "Min m\u00e5ltemperatur (bruk min og maks = 0 for standard)", - "set_temp_divided": "Bruk delt temperaturverdi for innstilt temperaturkommando", - "support_color": "Tving fargest\u00f8tte", - "temp_divider": "Deler temperaturverdier (0 = bruk standard)", - "temp_step_override": "Trinn for m\u00e5ltemperatur", - "tuya_max_coltemp": "Maks fargetemperatur rapportert av enheten", - "unit_of_measurement": "Temperaturenhet som brukes av enheten" - }, - "description": "Konfigurer alternativer for \u00e5 justere vist informasjon for {device_type} device ` {device_name} `", - "title": "Konfigurere Tuya-enhet" - }, - "init": { - "data": { - "discovery_interval": "Avsp\u00f8rringsintervall for discovery-enheten i l\u00f8pet av sekunder", - "list_devices": "Velg enhetene du vil konfigurere, eller la de v\u00e6re tomme for \u00e5 lagre konfigurasjonen", - "query_device": "Velg enhet som skal bruke sp\u00f8rringsmetode for raskere statusoppdatering", - "query_interval": "Sp\u00f8rringsintervall for intervall i sekunder" - }, - "description": "Ikke angi pollingsintervallverdiene for lave, ellers vil ikke anropene generere feilmelding i loggen", - "title": "Konfigurer Tuya-alternativer" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json deleted file mode 100644 index 92ced00e733..00000000000 --- a/homeassistant/components/tuya/translations/pl.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "error": { - "invalid_auth": "Niepoprawne uwierzytelnienie" - }, - "flow_title": "Konfiguracja integracji Tuya", - "step": { - "user": { - "data": { - "country_code": "Kod kraju twojego konta (np. 1 dla USA lub 86 dla Chin)", - "password": "Has\u0142o", - "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" - }, - "error": { - "dev_multi_type": "Wybrane urz\u0105dzenia do skonfigurowania musz\u0105 by\u0107 tego samego typu", - "dev_not_config": "Typ urz\u0105dzenia nie jest konfigurowalny", - "dev_not_found": "Nie znaleziono urz\u0105dzenia" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Zakres jasno\u015bci u\u017cywany przez urz\u0105dzenie", - "curr_temp_divider": "Dzielnik aktualnej warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", - "max_kelvin": "Maksymalna obs\u0142ugiwana temperatura barwy w kelwinach", - "max_temp": "Maksymalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", - "min_kelvin": "Minimalna obs\u0142ugiwana temperatura barwy w kelwinach", - "min_temp": "Minimalna temperatura docelowa (u\u017cyj min i max = 0 dla warto\u015bci domy\u015blnej)", - "set_temp_divided": "U\u017cyj podzielonej warto\u015bci temperatury dla polecenia ustawienia temperatury", - "support_color": "Wymu\u015b obs\u0142ug\u0119 kolor\u00f3w", - "temp_divider": "Dzielnik warto\u015bci temperatury (0 = u\u017cyj warto\u015bci domy\u015blnej)", - "temp_step_override": "Krok docelowej temperatury", - "tuya_max_coltemp": "Maksymalna temperatura barwy raportowana przez urz\u0105dzenie", - "unit_of_measurement": "Jednostka temperatury u\u017cywana przez urz\u0105dzenie" - }, - "description": "Skonfiguruj opcje, aby dostosowa\u0107 wy\u015bwietlane informacje dla urz\u0105dzenia {device_type} `{device_name}'", - "title": "Konfiguracja urz\u0105dzenia Tuya" - }, - "init": { - "data": { - "discovery_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania nowych urz\u0105dze\u0144 (w sekundach)", - "list_devices": "Wybierz urz\u0105dzenia do skonfigurowania lub pozostaw puste, aby zapisa\u0107 konfiguracj\u0119", - "query_device": "Wybierz urz\u0105dzenie, kt\u00f3re b\u0119dzie u\u017cywa\u0107 metody odpytywania w celu szybszej aktualizacji statusu", - "query_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania odpytywanego urz\u0105dzenia w sekundach" - }, - "description": "Nie ustawiaj zbyt niskich warto\u015bci skanowania, bo zako\u0144cz\u0105 si\u0119 niepowodzeniem, generuj\u0105c komunikat o b\u0142\u0119dzie w logu", - "title": "Konfiguracja opcji Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json deleted file mode 100644 index 8dc537e7549..00000000000 --- a/homeassistant/components/tuya/translations/pt-BR.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "flow_title": "Configura\u00e7\u00e3o Tuya", - "step": { - "user": { - "data": { - "country_code": "O c\u00f3digo do pa\u00eds da sua conta (por exemplo, 1 para os EUA ou 86 para a China)", - "password": "Senha", - "platform": "O aplicativo onde sua conta \u00e9 registrada", - "username": "Nome de usu\u00e1rio" - }, - "description": "Digite sua credencial Tuya.", - "title": "Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json deleted file mode 100644 index 566746538c0..00000000000 --- a/homeassistant/components/tuya/translations/pt.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" - }, - "step": { - "user": { - "data": { - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } - } - } - }, - "options": { - "abort": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json deleted file mode 100644 index 7b46689bc50..00000000000 --- a/homeassistant/components/tuya/translations/ru.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." - }, - "error": { - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." - }, - "flow_title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tuya", - "step": { - "user": { - "data": { - "country_code": "\u041a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044f)", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "platform": "\u041f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." - }, - "error": { - "dev_multi_type": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", - "dev_not_config": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "dev_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e." - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u0414\u0438\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0440\u043a\u043e\u0441\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", - "curr_temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", - "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)", - "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", - "min_kelvin": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0438\u043d\u0430\u0445)", - "min_temp": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 min \u0438 max = 0)", - "set_temp_divided": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", - "support_color": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u0446\u0432\u0435\u0442\u0430", - "temp_divider": "\u0414\u0435\u043b\u0438\u0442\u0435\u043b\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b (0 = \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", - "temp_step_override": "\u0428\u0430\u0433 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", - "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0446\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u0441\u043e\u043e\u0431\u0449\u0430\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", - "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c" - }, - "description": "\u041e\u043f\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0434\u043b\u044f {device_type} \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 `{device_name}`", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Tuya" - }, - "init": { - "data": { - "discovery_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "list_devices": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", - "query_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0434\u043b\u044f \u0431\u043e\u043b\u0435\u0435 \u0431\u044b\u0441\u0442\u0440\u043e\u0433\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0430", - "query_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" - }, - "description": "\u041d\u0435 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043d\u0438\u0437\u043a\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430 \u043e\u043f\u0440\u043e\u0441\u0430, \u0438\u043d\u0430\u0447\u0435 \u0432\u044b\u0437\u043e\u0432\u044b \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0435 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json deleted file mode 100644 index b07ad70adac..00000000000 --- a/homeassistant/components/tuya/translations/sl.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "options": { - "abort": { - "cannot_connect": "Povezovanje ni uspelo." - }, - "error": { - "dev_not_config": "Vrsta naprave ni nastavljiva", - "dev_not_found": "Naprave ni mogo\u010de najti" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json deleted file mode 100644 index 85cc9c57fd3..00000000000 --- a/homeassistant/components/tuya/translations/sv.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "flow_title": "Tuya-konfiguration", - "step": { - "user": { - "data": { - "country_code": "Landskod f\u00f6r ditt konto (t.ex. 1 f\u00f6r USA eller 86 f\u00f6r Kina)", - "password": "L\u00f6senord", - "platform": "Appen d\u00e4r ditt konto registreras", - "username": "Anv\u00e4ndarnamn" - }, - "description": "Ange dina Tuya anv\u00e4ndaruppgifter.", - "title": "Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json deleted file mode 100644 index 2edf3276b6c..00000000000 --- a/homeassistant/components/tuya/translations/tr.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "error": { - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" - }, - "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", - "step": { - "user": { - "data": { - "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", - "password": "Parola", - "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "description": "Tuya kimlik bilgilerinizi girin.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Ba\u011flanma hatas\u0131" - }, - "error": { - "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz", - "dev_not_found": "Cihaz bulunamad\u0131" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", - "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", - "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", - "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", - "support_color": "Vurgu rengi", - "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", - "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", - "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" - }, - "description": "{device_type} ayg\u0131t\u0131 '{device_name}' i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", - "title": "Tuya Cihaz\u0131n\u0131 Yap\u0131land\u0131r\u0131n" - }, - "init": { - "data": { - "discovery_interval": "Cihaz\u0131 yoklama aral\u0131\u011f\u0131 saniye cinsinden", - "list_devices": "Yap\u0131land\u0131rmay\u0131 kaydetmek i\u00e7in yap\u0131land\u0131r\u0131lacak veya bo\u015f b\u0131rak\u0131lacak cihazlar\u0131 se\u00e7in", - "query_device": "Daha h\u0131zl\u0131 durum g\u00fcncellemesi i\u00e7in sorgu y\u00f6ntemini kullanacak cihaz\u0131 se\u00e7in", - "query_interval": "Ayg\u0131t yoklama aral\u0131\u011f\u0131 saniye cinsinden" - }, - "description": "Yoklama aral\u0131\u011f\u0131 de\u011ferlerini \u00e7ok d\u00fc\u015f\u00fck ayarlamay\u0131n, aksi takdirde \u00e7a\u011fr\u0131lar g\u00fcnl\u00fckte hata mesaj\u0131 olu\u015fturarak ba\u015far\u0131s\u0131z olur", - "title": "Tuya Se\u00e7eneklerini Konfig\u00fcre Et" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json deleted file mode 100644 index 1d2709d260a..00000000000 --- a/homeassistant/components/tuya/translations/uk.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." - }, - "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." - }, - "flow_title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya", - "step": { - "user": { - "data": { - "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044e)", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "platform": "\u0414\u043e\u0434\u0430\u0442\u043e\u043a, \u0432 \u044f\u043a\u043e\u043c\u0443 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tuya.", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" - }, - "error": { - "dev_multi_type": "\u041a\u0456\u043b\u044c\u043a\u0430 \u043e\u0431\u0440\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0443.", - "dev_not_config": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", - "dev_not_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u0414\u0456\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", - "curr_temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", - "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", - "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", - "min_kelvin": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", - "min_temp": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", - "support_color": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 \u043a\u043e\u043b\u044c\u043e\u0440\u0443", - "temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u0437\u043d\u0430\u0447\u0435\u043d\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", - "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u044f\u043a\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", - "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438, \u044f\u043a\u0430 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c" - }, - "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u0434\u043b\u044f {device_type} \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e '{device_name}'", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Tuya" - }, - "init": { - "data": { - "discovery_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", - "list_devices": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457", - "query_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0438\u0442\u0443 \u0434\u043b\u044f \u0431\u0456\u043b\u044c\u0448 \u0448\u0432\u0438\u0434\u043a\u043e\u0433\u043e \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0443", - "query_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" - }, - "description": "\u041d\u0435 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0439\u0442\u0435 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u043d\u0438\u0437\u044c\u043a\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0456\u043d\u0430\u043a\u0448\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e \u043f\u043e\u043c\u0438\u043b\u043a\u0443 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0456.", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index ff3887c840d..e1acb5453aa 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -1,60 +1,29 @@ { "config": { - "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", - "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" - }, "error": { - "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + "invalid_auth": "身份认证无效" }, - "flow_title": "\u6d82\u9e26\u914d\u7f6e", + "flow_title": "涂鸦配置", "step": { - "user": { - "data": { - "country_code": "\u60a8\u7684\u5e10\u6237\u56fd\u5bb6(\u5730\u533a)\u4ee3\u7801\uff08\u4f8b\u5982\u4e2d\u56fd\u4e3a 86\uff0c\u7f8e\u56fd\u4e3a 1\uff09", - "password": "\u5bc6\u7801", - "platform": "\u60a8\u6ce8\u518c\u5e10\u6237\u7684\u5e94\u7528", - "username": "\u7528\u6237\u540d" - }, - "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", - "title": "\u6d82\u9e26" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" - }, - "error": { - "dev_multi_type": "\u591a\u4e2a\u8981\u914d\u7f6e\u7684\u8bbe\u5907\u5fc5\u987b\u5177\u6709\u76f8\u540c\u7684\u7c7b\u578b", - "dev_not_config": "\u8bbe\u5907\u7c7b\u578b\u4e0d\u53ef\u914d\u7f6e", - "dev_not_found": "\u672a\u627e\u5230\u8bbe\u5907" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u8bbe\u5907\u4f7f\u7528\u7684\u4eae\u5ea6\u8303\u56f4", - "max_kelvin": "\u6700\u9ad8\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", - "max_temp": "\u6700\u9ad8\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", - "min_kelvin": "\u6700\u4f4e\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", - "min_temp": "\u6700\u4f4e\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", - "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", - "tuya_max_coltemp": "\u8bbe\u5907\u62a5\u544a\u7684\u6700\u9ad8\u8272\u6e29", - "unit_of_measurement": "\u8bbe\u5907\u4f7f\u7528\u7684\u6e29\u5ea6\u5355\u4f4d" - }, - "title": "\u914d\u7f6e\u6d82\u9e26\u8bbe\u5907" + "user":{ + "title":"Tuya插件", + "data":{ + "tuya_project_type": "涂鸦云项目类型" + } }, - "init": { + "login": { "data": { - "discovery_interval": "\u53d1\u73b0\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09", - "list_devices": "\u8bf7\u9009\u62e9\u8981\u914d\u7f6e\u7684\u8bbe\u5907\uff0c\u6216\u7559\u7a7a\u4ee5\u4fdd\u5b58\u914d\u7f6e", - "query_device": "\u8bf7\u9009\u62e9\u4f7f\u7528\u67e5\u8be2\u65b9\u6cd5\u7684\u8bbe\u5907\uff0c\u4ee5\u4fbf\u66f4\u5feb\u5730\u66f4\u65b0\u72b6\u6001", - "query_interval": "\u67e5\u8be2\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09" + "endpoint": "可用区域", + "access_id": "Access ID", + "access_secret": "Access Secret", + "tuya_app_type": "移动应用", + "country_code": "国家码", + "username": "账号", + "password": "密码" }, - "description": "\u8bf7\u4e0d\u8981\u5c06\u8f6e\u8be2\u95f4\u9694\u8bbe\u7f6e\u5f97\u592a\u4f4e\uff0c\u5426\u5219\u5c06\u8c03\u7528\u5931\u8d25\u5e76\u5728\u65e5\u5fd7\u751f\u6210\u9519\u8bef\u6d88\u606f", - "title": "\u914d\u7f6e\u6d82\u9e26\u9009\u9879" + "description": "请输入涂鸦账户信息。", + "title": "涂鸦" } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json deleted file mode 100644 index 7221c86eb63..00000000000 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "error": { - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" - }, - "flow_title": "Tuya \u8a2d\u5b9a", - "step": { - "user": { - "data": { - "country_code": "\u5e33\u865f\u570b\u5bb6\u4ee3\u78bc\uff08\u4f8b\u5982\uff1a\u7f8e\u570b 1 \u6216\u4e2d\u570b 86\uff09", - "password": "\u5bc6\u78bc", - "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u8f38\u5165 Tuya \u6191\u8b49\u3002", - "title": "Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" - }, - "error": { - "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", - "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", - "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", - "curr_temp_divider": "\u76ee\u524d\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", - "max_kelvin": "Kelvin \u652f\u63f4\u6700\u9ad8\u8272\u6eab", - "max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", - "min_kelvin": "Kelvin \u652f\u63f4\u6700\u4f4e\u8272\u6eab", - "min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", - "set_temp_divided": "\u4f7f\u7528\u5206\u9694\u865f\u6eab\u5ea6\u503c\u4ee5\u57f7\u884c\u8a2d\u5b9a\u6eab\u5ea6\u6307\u4ee4", - "support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4", - "temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", - "temp_step_override": "\u76ee\u6a19\u6eab\u5ea6\u8a2d\u5b9a", - "tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab", - "unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" - }, - "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u88dd\u7f6e `{device_name}` \u986f\u793a\u8cc7\u8a0a", - "title": "\u8a2d\u5b9a Tuya \u88dd\u7f6e" - }, - "init": { - "data": { - "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", - "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", - "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" - }, - "description": "\u66f4\u65b0\u9593\u8ddd\u4e0d\u8981\u8a2d\u5b9a\u7684\u904e\u4f4e\u3001\u53ef\u80fd\u6703\u5c0e\u81f4\u65bc\u65e5\u8a8c\u4e2d\u7522\u751f\u932f\u8aa4\u8a0a\u606f", - "title": "\u8a2d\u5b9a Tuya \u9078\u9805" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 710f4d84d2c..370a87e2575 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -359,30 +359,6 @@ DHCP = [ "hostname": "lb*", "macaddress": "B09575*" }, - { - "domain": "tuya", - "macaddress": "508A06*" - }, - { - "domain": "tuya", - "macaddress": "7CF666*" - }, - { - "domain": "tuya", - "macaddress": "10D561*" - }, - { - "domain": "tuya", - "macaddress": "D4A651*" - }, - { - "domain": "tuya", - "macaddress": "68572D*" - }, - { - "domain": "tuya", - "macaddress": "1869D8*" - }, { "domain": "verisure", "macaddress": "0023C1*" diff --git a/requirements_all.txt b/requirements_all.txt index 91143cac731..dd27ed60898 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2332,7 +2332,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.10 +tuya-iot-py-sdk==0.4.1 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b040845dbd..961cea61a75 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1315,7 +1315,7 @@ total_connect_client==0.57 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.10 +tuya-iot-py-sdk==0.4.1 # homeassistant.components.twentemilieu twentemilieu==0.3.0 diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 6ea28cf8d2b..a15cfcc0fdf 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,68 +1,81 @@ """Tests for the Tuya config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest -from tuyaha.devices.climate import STEP_HALVES -from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException from homeassistant import config_entries, data_entry_flow -from homeassistant.components.tuya.config_flow import ( - CONF_LIST_DEVICES, - ERROR_DEV_MULTI_TYPE, - ERROR_DEV_NOT_CONFIG, - ERROR_DEV_NOT_FOUND, - RESULT_AUTH_FAILED, - RESULT_CONN_ERROR, - RESULT_SINGLE_INSTANCE, -) +from homeassistant.components.tuya.config_flow import RESULT_AUTH_FAILED from homeassistant.components.tuya.const import ( - CONF_BRIGHTNESS_RANGE_MODE, - CONF_COUNTRYCODE, - CONF_CURR_TEMP_DIVIDER, - CONF_DISCOVERY_INTERVAL, - CONF_MAX_KELVIN, - CONF_MAX_TEMP, - CONF_MIN_KELVIN, - CONF_MIN_TEMP, - CONF_QUERY_DEVICE, - CONF_QUERY_INTERVAL, - CONF_SET_TEMP_DIVIDED, - CONF_SUPPORT_COLOR, - CONF_TEMP_DIVIDER, - CONF_TEMP_STEP_OVERRIDE, - CONF_TUYA_MAX_COLTEMP, - DOMAIN, - TUYA_DATA, -) -from homeassistant.const import ( + CONF_ACCESS_ID, + CONF_ACCESS_SECRET, + CONF_APP_TYPE, + CONF_COUNTRY_CODE, + CONF_ENDPOINT, CONF_PASSWORD, - CONF_PLATFORM, - CONF_UNIT_OF_MEASUREMENT, + CONF_PROJECT_TYPE, CONF_USERNAME, - TEMP_CELSIUS, + DOMAIN, ) -from .common import CLIMATE_ID, LIGHT_ID, LIGHT_ID_FAKE1, LIGHT_ID_FAKE2, MockTuya +MOCK_SMART_HOME_PROJECT_TYPE = 0 +MOCK_INDUSTRY_PROJECT_TYPE = 1 -from tests.common import MockConfigEntry +MOCK_ACCESS_ID = "myAccessId" +MOCK_ACCESS_SECRET = "myAccessSecret" +MOCK_USERNAME = "myUsername" +MOCK_PASSWORD = "myPassword" +MOCK_COUNTRY_CODE = "1" +MOCK_APP_TYPE = "smartlife" +MOCK_ENDPOINT = "https://openapi-ueaz.tuyaus.com" -USERNAME = "myUsername" -PASSWORD = "myPassword" -COUNTRY_CODE = "1" -TUYA_PLATFORM = "tuya" +TUYA_SMART_HOME_PROJECT_DATA = { + CONF_PROJECT_TYPE: MOCK_SMART_HOME_PROJECT_TYPE, +} +TUYA_INDUSTRY_PROJECT_DATA = { + CONF_PROJECT_TYPE: MOCK_INDUSTRY_PROJECT_TYPE, +} -TUYA_USER_DATA = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_COUNTRYCODE: COUNTRY_CODE, - CONF_PLATFORM: TUYA_PLATFORM, +TUYA_INPUT_SMART_HOME_DATA = { + CONF_ACCESS_ID: MOCK_ACCESS_ID, + CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE, + CONF_APP_TYPE: MOCK_APP_TYPE, +} + +TUYA_INPUT_INDUSTRY_DATA = { + CONF_ENDPOINT: MOCK_ENDPOINT, + CONF_ACCESS_ID: MOCK_ACCESS_ID, + CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, +} + +TUYA_IMPORT_SMART_HOME_DATA = { + CONF_ACCESS_ID: MOCK_ACCESS_ID, + CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_COUNTRY_CODE: MOCK_COUNTRY_CODE, + CONF_APP_TYPE: MOCK_APP_TYPE, +} + + +TUYA_IMPORT_INDUSTRY_DATA = { + CONF_PROJECT_TYPE: MOCK_SMART_HOME_PROJECT_TYPE, + CONF_ENDPOINT: MOCK_ENDPOINT, + CONF_ACCESS_ID: MOCK_ACCESS_ID, + CONF_ACCESS_SECRET: MOCK_ACCESS_SECRET, + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, } @pytest.fixture(name="tuya") def tuya_fixture() -> Mock: """Patch libraries.""" - with patch("homeassistant.components.tuya.config_flow.TuyaApi") as tuya: + with patch("homeassistant.components.tuya.config_flow.TuyaOpenAPI") as tuya: yield tuya @@ -73,8 +86,8 @@ def tuya_setup_fixture(): yield -async def test_user(hass, tuya): - """Test user config.""" +async def test_industry_user(hass, tuya): + """Test industry user config.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -83,192 +96,92 @@ async def test_user(hass, tuya): assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=TUYA_USER_DATA + result["flow_id"], user_input=TUYA_INDUSTRY_PROJECT_DATA + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "login" + + tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_INPUT_INDUSTRY_DATA ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE - assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM + assert result["title"] == MOCK_USERNAME + assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID + assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD assert not result["result"].unique_id -async def test_abort_if_already_setup(hass, tuya): - """Test we abort if Tuya is already setup.""" - MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass) - - # Should fail, config exist (import) +async def test_smart_home_user(hass, tuya): + """Test smart home user config.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_SINGLE_INSTANCE - - -async def test_abort_on_invalid_credentials(hass, tuya): - """Test when we have invalid credentials.""" - tuya().init.side_effect = TuyaAPIException("Boom") - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": RESULT_AUTH_FAILED} + assert result["step_id"] == "user" - -async def test_abort_on_connection_error(hass, tuya): - """Test when we have a network error.""" - tuya().init.side_effect = TuyaNetException("Boom") - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=TUYA_USER_DATA + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_SMART_HOME_PROJECT_DATA ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_CONN_ERROR - - -async def test_options_flow(hass): - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=TUYA_USER_DATA, - ) - config_entry.add_to_hass(hass) - - # Set up the integration to make sure the config flow module is loaded. - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - # Unload the integration to prepare for the test. - with patch("homeassistant.components.tuya.async_unload_entry", return_value=True): - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "login" - # Test check for integration not loaded - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_CONN_ERROR - - # Load integration and enter options - await hass.config_entries.async_setup(config_entry.entry_id) + tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA + ) await hass.async_block_till_done() - hass.data[DOMAIN] = {TUYA_DATA: MockTuya()} - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + assert result["errors"]["base"] == RESULT_AUTH_FAILED - # Test dev not found error - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE1}"]}, + tuya().login = MagicMock(return_value={"success": True, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_IMPORT_SMART_HOME_DATA ) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": ERROR_DEV_NOT_FOUND} - - # Test dev type error - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID_FAKE2}"]}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": ERROR_DEV_NOT_CONFIG} - - # Test multi dev error - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}", f"light-{LIGHT_ID}"]}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": ERROR_DEV_MULTI_TYPE} - - # Test climate options form - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_LIST_DEVICES: [f"climate-{CLIMATE_ID}"]} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "device" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - CONF_TEMP_DIVIDER: 10, - CONF_CURR_TEMP_DIVIDER: 5, - CONF_SET_TEMP_DIVIDED: False, - CONF_TEMP_STEP_OVERRIDE: STEP_HALVES, - CONF_MIN_TEMP: 12, - CONF_MAX_TEMP: 22, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - # Test light options form - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_LIST_DEVICES: [f"light-{LIGHT_ID}"]} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "device" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_SUPPORT_COLOR: True, - CONF_BRIGHTNESS_RANGE_MODE: 1, - CONF_MIN_KELVIN: 4000, - CONF_MAX_KELVIN: 5000, - CONF_TUYA_MAX_COLTEMP: 12000, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - # Test common options - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_DISCOVERY_INTERVAL: 100, - CONF_QUERY_INTERVAL: 50, - CONF_QUERY_DEVICE: LIGHT_ID, - }, - ) - - # Verify results assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_USERNAME + assert result["data"][CONF_ACCESS_ID] == MOCK_ACCESS_ID + assert result["data"][CONF_ACCESS_SECRET] == MOCK_ACCESS_SECRET + assert result["data"][CONF_USERNAME] == MOCK_USERNAME + assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD + assert result["data"][CONF_COUNTRY_CODE] == MOCK_COUNTRY_CODE + assert result["data"][CONF_APP_TYPE] == MOCK_APP_TYPE + assert not result["result"].unique_id - climate_options = config_entry.options[CLIMATE_ID] - assert climate_options[CONF_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert climate_options[CONF_TEMP_DIVIDER] == 10 - assert climate_options[CONF_CURR_TEMP_DIVIDER] == 5 - assert climate_options[CONF_SET_TEMP_DIVIDED] is False - assert climate_options[CONF_TEMP_STEP_OVERRIDE] == STEP_HALVES - assert climate_options[CONF_MIN_TEMP] == 12 - assert climate_options[CONF_MAX_TEMP] == 22 - light_options = config_entry.options[LIGHT_ID] - assert light_options[CONF_SUPPORT_COLOR] is True - assert light_options[CONF_BRIGHTNESS_RANGE_MODE] == 1 - assert light_options[CONF_MIN_KELVIN] == 4000 - assert light_options[CONF_MAX_KELVIN] == 5000 - assert light_options[CONF_TUYA_MAX_COLTEMP] == 12000 +async def test_error_on_invalid_credentials(hass, tuya): + """Test when we have invalid credentials.""" - assert config_entry.options[CONF_DISCOVERY_INTERVAL] == 100 - assert config_entry.options[CONF_QUERY_INTERVAL] == 50 - assert config_entry.options[CONF_QUERY_DEVICE] == LIGHT_ID + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_INDUSTRY_PROJECT_DATA + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "login" + + tuya().login = MagicMock(return_value={"success": False, "errorCode": 1024}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=TUYA_INPUT_INDUSTRY_DATA + ) + await hass.async_block_till_done() + + assert result["errors"]["base"] == RESULT_AUTH_FAILED