From 7434a0294c495763c63eed01df9112aaae433823 Mon Sep 17 00:00:00 2001 From: erchuan Date: Mon, 11 Oct 2021 17:37:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=E4=BB=A3=E7=A0=81=E5=90=88=E5=B9=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/tuya_v2/__init__.py | 324 +++++++---------- custom_components/tuya_v2/aes_cbc.py | 88 ----- .../tuya_v2/alarm_control_panel.py | 54 +-- custom_components/tuya_v2/base.py | 41 +-- custom_components/tuya_v2/binary_sensor.py | 254 +++----------- custom_components/tuya_v2/climate.py | 329 ++++++++++-------- custom_components/tuya_v2/config_flow.py | 127 +++---- custom_components/tuya_v2/const.py | 31 +- custom_components/tuya_v2/cover.py | 44 ++- custom_components/tuya_v2/fan.py | 148 ++++---- custom_components/tuya_v2/humidifier.py | 43 ++- custom_components/tuya_v2/light.py | 190 +++++----- custom_components/tuya_v2/number.py | 51 ++- custom_components/tuya_v2/scene.py | 28 +- custom_components/tuya_v2/select.py | 45 ++- custom_components/tuya_v2/sensor.py | 56 +-- custom_components/tuya_v2/strings.json | 9 +- custom_components/tuya_v2/switch.py | 105 +++--- .../tuya_v2/translations/de.json | 21 +- .../tuya_v2/translations/en.json | 21 +- .../tuya_v2/translations/es.json | 21 +- .../tuya_v2/translations/fr.json | 21 +- .../tuya_v2/translations/hi.json | 21 +- .../tuya_v2/translations/hu.json | 21 +- .../tuya_v2/translations/id.json | 21 +- .../tuya_v2/translations/it.json | 21 +- .../tuya_v2/translations/kn.json | 21 +- .../tuya_v2/translations/nl.json | 21 +- .../tuya_v2/translations/pt.json | 21 +- .../tuya_v2/translations/zh-Hans.json | 21 +- .../tuya_v2/translations/zh-Hant.json | 21 +- custom_components/tuya_v2/vacuum.py | 69 ++-- 32 files changed, 974 insertions(+), 1335 deletions(-) delete mode 100644 custom_components/tuya_v2/aes_cbc.py diff --git a/custom_components/tuya_v2/__init__.py b/custom_components/tuya_v2/__init__.py index 077edbc..e96e22b 100644 --- a/custom_components/tuya_v2/__init__.py +++ b/custom_components/tuya_v2/__init__.py @@ -1,16 +1,8 @@ """Support for Tuya Smart devices.""" import itertools -import json import logging -from typing import Any - -import homeassistant.helpers.config_validation as cv -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import UnknownFlow, UnknownStep -from homeassistant.helpers.dispatcher import async_dispatcher_send + from tuya_iot import ( ProjectType, TuyaDevice, @@ -19,11 +11,13 @@ TuyaHomeManager, TuyaOpenAPI, TuyaOpenMQ, - tuya_logger, ) -from .aes_cbc import AES_ACCOUNT_KEY, KEY_KEY, XOR_KEY -from .aes_cbc import AesCBC as Aes +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import dispatcher_send + from .const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, @@ -34,108 +28,59 @@ CONF_PROJECT_TYPE, CONF_USERNAME, DOMAIN, + PLATFORMS, TUYA_DEVICE_MANAGER, TUYA_DISCOVERY_NEW, TUYA_HA_DEVICES, + TUYA_HA_SIGNAL_UPDATE_ENTITY, TUYA_HA_TUYA_MAP, TUYA_HOME_MANAGER, TUYA_MQTT_LISTENER, - TUYA_SETUP_PLATFORM, - TUYA_SUPPORT_HA_TYPE, ) _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PROJECT_TYPE): int, - vol.Required(CONF_ENDPOINT): cv.string, - vol.Required(CONF_ACCESS_ID): cv.string, - vol.Required(CONF_ACCESS_SECRET): cv.string, - CONF_USERNAME: cv.string, - CONF_PASSWORD: cv.string, - CONF_COUNTRY_CODE: cv.string, - CONF_APP_TYPE: cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Async setup hass config entry.""" -def entry_decrypt(hass: HomeAssistant, entry: ConfigEntry, init_entry_data): - """Decript or encrypt entry info.""" - aes = Aes() - # decrypt the new account info - if XOR_KEY in init_entry_data: - _LOGGER.info("tuya.__init__.exist_xor_cache-->True") - key_iv = aes.xor_decrypt(init_entry_data[XOR_KEY], init_entry_data[KEY_KEY]) - cbc_key = key_iv[0:16] - cbc_iv = key_iv[16:32] - decrpyt_str = aes.cbc_decrypt(cbc_key, cbc_iv, init_entry_data[AES_ACCOUNT_KEY]) - # _LOGGER.info(f"tuya.__init__.exist_xor_cache:::decrpyt_str-->{decrpyt_str}") - entry_data = aes.json_to_dict(decrpyt_str) - else: - # if not exist xor cache, use old account info - _LOGGER.info("tuya.__init__.exist_xor_cache-->False") - entry_data = init_entry_data - cbc_key = aes.random_16() - cbc_iv = aes.random_16() - access_id = init_entry_data[CONF_ACCESS_ID] - access_id_entry = aes.cbc_encrypt(cbc_key, cbc_iv, access_id) - c = cbc_key + cbc_iv - c_xor_entry = aes.xor_encrypt(c, access_id_entry) - # account info encrypted with AES-CBC - user_input_encrpt = aes.cbc_encrypt( - cbc_key, cbc_iv, json.dumps(dict(init_entry_data)) - ) - # udpate old account info - hass.config_entries.async_update_entry( - entry, - data={ - AES_ACCOUNT_KEY: user_input_encrpt, - XOR_KEY: c_xor_entry, - KEY_KEY: access_id_entry, - }, - ) - return entry_data + _LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data) + + hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} + + success = await _init_tuya_sdk(hass, entry) + if not success: + return False + + return True async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Initialize the Tuya SDK.""" - init_entry_data = entry.data - # decrypt or encrypt entry info - entry_data = entry_decrypt(hass, entry, init_entry_data) - project_type = ProjectType(entry_data[CONF_PROJECT_TYPE]) + project_type = ProjectType(entry.data[CONF_PROJECT_TYPE]) api = TuyaOpenAPI( - entry_data[CONF_ENDPOINT], - entry_data[CONF_ACCESS_ID], - entry_data[CONF_ACCESS_SECRET], + entry.data[CONF_ENDPOINT], + entry.data[CONF_ACCESS_ID], + entry.data[CONF_ACCESS_SECRET], project_type, ) api.set_dev_channel("hass") - response = ( - await hass.async_add_executor_job( - api.login, entry_data[CONF_USERNAME], entry_data[CONF_PASSWORD] + if project_type == ProjectType.INDUSTY_SOLUTIONS: + response = await hass.async_add_executor_job( + api.login, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] ) - if project_type == ProjectType.INDUSTY_SOLUTIONS - else await hass.async_add_executor_job( + 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], + 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(f"Tuya login error response: {response}") + _LOGGER.error("Tuya login error response: %s", response) return False tuya_mq = TuyaOpenMQ(api) @@ -146,148 +91,117 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: # 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][TUYA_HOME_MANAGER] = home_manager - - class DeviceListener(TuyaDeviceListener): - """Device Update Listener.""" - - def update_device(self, device: TuyaDevice): - for ha_device in hass.data[DOMAIN][TUYA_HA_DEVICES]: - if ha_device.tuya_device.id == device.id: - _LOGGER.debug(f"_update-->{self};->>{ha_device.tuya_device.status}") - ha_device.schedule_update_ha_state() + hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] = home_manager - def add_device(self, device: TuyaDevice): - - device_add = False - - _LOGGER.info( - f"""add device category->{device.category}; keys->, - {hass.data[DOMAIN][TUYA_HA_TUYA_MAP].keys()}""" - ) - if device.category in itertools.chain( - *hass.data[DOMAIN][TUYA_HA_TUYA_MAP].values() - ): - ha_tuya_map = hass.data[DOMAIN][TUYA_HA_TUYA_MAP] - - remove_hass_device(hass, device.id) - - for key, tuya_list in ha_tuya_map.items(): - if device.category in tuya_list: - device_add = True - async_dispatcher_send( - hass, TUYA_DISCOVERY_NEW.format(key), [device.id] - ) - - if device_add: - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - device_manager.mq.stop() - tuya_mq = TuyaOpenMQ(device_manager.api) - tuya_mq.start() - - device_manager.mq = tuya_mq - tuya_mq.add_message_listener(device_manager.on_message) - - def remove_device(self, device_id: str): - _LOGGER.info(f"tuya remove device:{device_id}") - remove_hass_device(hass, device_id) - - __listener = DeviceListener() - hass.data[DOMAIN][TUYA_MQTT_LISTENER] = __listener - device_manager.add_device_listener(__listener) - hass.data[DOMAIN][TUYA_DEVICE_MANAGER] = device_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) + await cleanup_device_registry(hass, entry) - _LOGGER.info(f"init support type->{TUYA_SUPPORT_HA_TYPE}") + _LOGGER.debug("init support type->%s", PLATFORMS) - for platform in TUYA_SUPPORT_HA_TYPE: - _LOGGER.info(f"tuya async platform-->{platform}") - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - hass.data[DOMAIN][TUYA_SETUP_PLATFORM].add(platform) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def cleanup_device_registry(hass: HomeAssistant): +async def cleanup_device_registry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Remove deleted device registry entry if there are no remaining entities.""" - device_registry = hass.helpers.device_registry.async_get(hass) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + device_registry_object = device_registry.async_get(hass) + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] - for dev_id, device_entity in list(device_registry.devices.items()): - for item in device_entity.identifiers: - if DOMAIN == item[0] and item[1] not in device_manager.device_map.keys(): - device_registry.async_remove_device(dev_id) + 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 -def remove_hass_device(hass: HomeAssistant, device_id: str): +@callback +def async_remove_hass_device(hass: HomeAssistant, device_id: str) -> None: """Remove device from hass cache.""" - device_registry = hass.helpers.device_registry.async_get(hass) - entity_registry = hass.helpers.entity_registry.async_get(hass) - for entity in list(entity_registry.entities.values()): - if entity.unique_id.startswith(f"ty{device_id}"): - entity_registry.async_remove(entity.entity_id) - if device_registry.async_get(entity.device_id): - device_registry.async_remove_device(entity.device_id) - - -async def async_setup(hass: HomeAssistant, config): - """Set up the Tuya integration.""" - tuya_logger.setLevel(_LOGGER.level) - conf = config.get(DOMAIN) - - _LOGGER.info(f"Tuya async setup conf {conf}") - if conf is not None: - - async def flow_init() -> Any: - try: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - except UnknownFlow as flow: - _LOGGER.error(flow.args) - except UnknownStep as step: - _LOGGER.error(step.args) - except ValueError as err: - _LOGGER.error(err.args) - _LOGGER.info("Tuya async setup flow_init") - return result - - hass.async_create_task(flow_init()) - - return True + 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): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unloading the Tuya platforms.""" - _LOGGER.info("integration unload") - unload = await hass.config_entries.async_unload_platforms( - entry, hass.data[DOMAIN]["setup_platform"] - ) + _LOGGER.debug("integration unload") + unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload: - __device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - __device_manager.mq.stop() - __device_manager.remove_device_listener(hass.data[DOMAIN][TUYA_MQTT_LISTENER]) + 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 -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Async setup hass config entry.""" - _LOGGER.info(f"tuya.__init__.async_setup_entry-->{entry.data}") +class DeviceListener(TuyaDeviceListener): + """Device Update Listener.""" - hass.data[DOMAIN] = {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: []} - hass.data[DOMAIN][TUYA_SETUP_PLATFORM] = set() + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Init DeviceListener.""" - success = await _init_tuya_sdk(hass, entry) - if not success: - return False + self.hass = hass + self.entry = entry - return True + 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( + "_update-->%s;->>%s", + self, + device.id, + ) + dispatcher_send(self.hass, f"{TUYA_HA_SIGNAL_UPDATE_ENTITY}_{device.id}") + + def add_device(self, device: TuyaDevice) -> None: + """Add device added listener.""" + device_add = False + + 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) + + 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] + ) + + 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() + + device_manager.mq = tuya_mq + tuya_mq.add_message_listener(device_manager.on_message) + + 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/custom_components/tuya_v2/aes_cbc.py b/custom_components/tuya_v2/aes_cbc.py deleted file mode 100644 index 1738ff8..0000000 --- a/custom_components/tuya_v2/aes_cbc.py +++ /dev/null @@ -1,88 +0,0 @@ -"""AES-CBC encryption and decryption for account info.""" - -import base64 as b64 -import json -import random -from binascii import a2b_hex, b2a_hex - -from Crypto.Cipher import AES - -AES_ACCOUNT_KEY = "o0o0o0" -XOR_KEY = "00oo00" -KEY_KEY = "oo00oo" - - -class AesCBC: - - # random_16 - def random_16(self): - str = "" - return str.join( - random.choice("abcdefghijklmnopqrstuvwxyz!@#$%^&*1234567890") - for i in range(16) - ) - - # add_to_16 - def add_to_16(self, text): - if len(text.encode("utf-8")) % 16: - add = 16 - (len(text.encode("utf-8")) % 16) - else: - add = 0 - text = text + ("\0" * add) - return text.encode("utf-8") - - # cbc_encryption - def cbc_encrypt(self, key, iv, text): - key = key.encode("utf-8") - mode = AES.MODE_CBC - iv = bytes(iv, encoding="utf8") - text = self.add_to_16(text) - cryptos = AES.new(key, mode, iv) - cipher_text = cryptos.encrypt(text) - return str(b2a_hex(cipher_text), encoding="utf-8") - - # cbc_decryption - def cbc_decrypt(self, key, iv, text): - key = key.encode("utf-8") - iv = bytes(iv, encoding="utf8") - mode = AES.MODE_CBC - cryptos = AES.new(key, mode, iv) - plain_text = cryptos.decrypt(a2b_hex(text)) - return bytes.decode(plain_text).rstrip("\0") - - # xor_encrypt - def xor_encrypt(self, data, key): - lkey = len(key) - secret = [] - num = 0 - for each in data: - if num >= lkey: - num = num % lkey - secret.append(chr(ord(each) ^ ord(key[num]))) - num += 1 - return b64.b64encode("".join(secret).encode()).decode() - - # xor_decrypt - def xor_decrypt(self, secret, key): - tips = b64.b64decode(secret.encode()).decode() - lkey = len(key) - secret = [] - num = 0 - for each in tips: - if num >= lkey: - num = num % lkey - secret.append(chr(ord(each) ^ ord(key[num]))) - num += 1 - return "".join(secret) - - # json to dict - def json_to_dict(self, json_str): - return json.loads(json_str) - - # confuse str - def b64_encrypt(self, text): - return b64.b64encode(text.encode()).decode() - - # unconfuse str - def b64_decrypt(self, text): - return b64.b64decode(text).decode() diff --git a/custom_components/tuya_v2/alarm_control_panel.py b/custom_components/tuya_v2/alarm_control_panel.py index a848bb2..ce07fea 100644 --- a/custom_components/tuya_v2/alarm_control_panel.py +++ b/custom_components/tuya_v2/alarm_control_panel.py @@ -10,12 +10,13 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_TRIGGERED -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -41,38 +42,43 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya alarm dynamically through tuya discovery.""" - _LOGGER.info("alarm init") + _LOGGER.debug("alarm init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.info("alarm add->", dev_ids) + _LOGGER.debug("alarm add->", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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: @@ -90,6 +96,7 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): ), ) ) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_GAS_SENSOR_STATE in device.status: entities.append( TuyaHaAlarm( @@ -102,6 +109,7 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): ), ) ) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_PIR in device.stastus: entities.append( TuyaHaAlarm( @@ -114,19 +122,15 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): ), ) ) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaAlarm(TuyaHaDevice, AlarmControlPanelEntity): +class TuyaHaAlarm(TuyaHaEntity, AlarmControlPanelEntity): """Tuya Alarm Device.""" - def __init__( - self, - device: TuyaDevice, - device_manager: TuyaDeviceManager, - sensor_is_on: Callable[..., str], - ) -> None: + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager, sensor_is_on: Callable[..., str]) -> None: """Init TuyaHaAlarm.""" super().__init__(device, device_manager) self._is_on = sensor_is_on diff --git a/custom_components/tuya_v2/base.py b/custom_components/tuya_v2/base.py index f5c276c..86a508a 100644 --- a/custom_components/tuya_v2/base.py +++ b/custom_components/tuya_v2/base.py @@ -1,26 +1,25 @@ """Tuya Home Assistant Base Device Model.""" from __future__ import annotations -import asyncio +from typing import Any from tuya_iot import TuyaDevice, TuyaDeviceManager -from .const import DOMAIN +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from .const import DOMAIN, TUYA_HA_SIGNAL_UPDATE_ENTITY -class TuyaHaDevice: + +class TuyaHaEntity(Entity): """Tuya base device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: - """Init TuyaHaDevice.""" + """Init TuyaHaEntity.""" super().__init__() self.tuya_device = device self.tuya_device_manager = device_manager - self.entity_id = f"tuya_v2.ty{self.tuya_device.id}" - - self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(self.loop) @staticmethod def remap(old_value, old_min, old_max, new_min, new_max): @@ -38,7 +37,7 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str | None: """Return a unique ID.""" - return f"ty{self.tuya_device.id}" + return f"tuya.{self.tuya_device.id}" @property def name(self) -> str | None: @@ -50,25 +49,27 @@ def device_info(self): """Return a device description for device registry.""" _device_info = { "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, - "manufacturer": "tuya", + "manufacturer": "Tuya", "name": self.tuya_device.name, "model": self.tuya_device.product_name, } return _device_info - # @property - # def icon(self) -> Optional[str]: - # """Return Tuya device icon.""" - # cdn_url = 'https://images.tuyacn.com/' - # # customize cdn url - # return cdn_url + self.tuyaDevice.icon - @property def available(self) -> bool: """Return if the device is available.""" return self.tuya_device.online - def _send_command(self, commands) -> None: - self.loop.run_in_executor( - None, self.tuya_device_manager.send_commands, self.tuya_device.id, commands + 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/custom_components/tuya_v2/binary_sensor.py b/custom_components/tuya_v2/binary_sensor.py index 85b4703..9fe7127 100644 --- a/custom_components/tuya_v2/binary_sensor.py +++ b/custom_components/tuya_v2/binary_sensor.py @@ -1,29 +1,22 @@ """Support for Tuya Binary Sensor.""" -import json import logging -from threading import Timer from typing import Callable -from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SMOKE, +from homeassistant.components.alarm_control_panel import DOMAIN as DEVICE_DOMAIN +from homeassistant.components.alarm_control_panel import ( + SUPPORT_ALARM_TRIGGER, + AlarmControlPanelEntity, ) -from homeassistant.components.binary_sensor import DOMAIN as DEVICE_DOMAIN -from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_TRIGGERED +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -35,250 +28,119 @@ _LOGGER = logging.getLogger(__name__) TUYA_SUPPORT_TYPE = [ - "mcs", # Door Window Sensor "ywbj", # Smoke Detector "rqbj", # Gas Detector "pir", # PIR Detector - "sj", # Water Detector - "sos", # Emergency Button - "hps", # Human Presence Sensor - "ms", # Residential Lock - "ckmkzq", # Garage Door Opener ] -# Door Window Sensor -# https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m +# Smoke Detector +# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5i2iiy -DPCODE_SWITCH = "switch" - - -DPCODE_BATTERY_STATE = "battery_state" - -DPCODE_DOORCONTACT_STATE = "doorcontact_state" DPCODE_SMOKE_SENSOR_STATE = "smoke_sensor_state" -DPCODE_SMOKE_SENSOR_STATUS = "smoke_sensor_status" DPCODE_GAS_SENSOR_STATE = "gas_sensor_state" DPCODE_PIR = "pir" -DPCODE_WATER_SENSOR_STATE = "watersensor_state" -DPCODE_SOS_STATE = "sos_state" -DPCODE_PRESENCE_STATE = "presence_state" -DPCODE_TEMPER_ALRAM = "temper_alarm" -DPCODE_DOORLOCK_STATE = "closed_opened" async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up tuya binary sensors dynamically through tuya discovery.""" - _LOGGER.info("binary sensor init") + """Set up tuya alarm dynamically through tuya discovery.""" + _LOGGER.debug("alarm init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.info(f"binary sensor add->{dev_ids}") + _LOGGER.debug("alarm add->", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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 - if DPCODE_DOORLOCK_STATE in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_DOOR, - DPCODE_DOORLOCK_STATE, - (lambda d: d.status.get(DPCODE_DOORLOCK_STATE, "none") != "closed"), - ) - ) - if DPCODE_DOORCONTACT_STATE in device.status: - if device.category == "ckmkzq": - device_class_d = DEVICE_CLASS_GARAGE_DOOR - else: - device_class_d = DEVICE_CLASS_DOOR - entities.append( - TuyaHaBSensor( - device, - device_manager, - device_class_d, - DPCODE_DOORCONTACT_STATE, - (lambda d: d.status.get(DPCODE_DOORCONTACT_STATE, False)), - ) - ) - if DPCODE_SWITCH in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_DOOR, - DPCODE_SWITCH, - (lambda d: d.status.get(DPCODE_SWITCH, False)), - ) - ) if DPCODE_SMOKE_SENSOR_STATE in device.status: entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_SMOKE, - DPCODE_SMOKE_SENSOR_STATE, - (lambda d: d.status.get(DPCODE_SMOKE_SENSOR_STATE, 1) == "1"), - ) - ) - if DPCODE_SMOKE_SENSOR_STATUS in device.status: - entities.append( - TuyaHaBSensor( + TuyaHaAlarm( device, device_manager, - DEVICE_CLASS_SMOKE, - DPCODE_SMOKE_SENSOR_STATUS, ( - lambda d: d.status.get(DPCODE_SMOKE_SENSOR_STATUS, "normal") - == "alarm" + lambda d: STATE_ALARM_TRIGGERED + if d.status.get(DPCODE_SMOKE_SENSOR_STATE, 1) == "1" + else STATE_ALARM_ARMING ), ) ) - if DPCODE_BATTERY_STATE in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_BATTERY, - DPCODE_BATTERY_STATE, - (lambda d: d.status.get(DPCODE_BATTERY_STATE, "normal") == "low"), - ) - ) - if DPCODE_TEMPER_ALRAM in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_MOTION, - DPCODE_TEMPER_ALRAM, - (lambda d: d.status.get(DPCODE_TEMPER_ALRAM, False)), - ) - ) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_GAS_SENSOR_STATE in device.status: entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_GAS, - DPCODE_GAS_SENSOR_STATE, - (lambda d: d.status.get(DPCODE_GAS_SENSOR_STATE, 1) == "1"), - ) - ) - if DPCODE_PIR in device.status: - entities.append( - TuyaHaBSensor( + TuyaHaAlarm( device, device_manager, - DEVICE_CLASS_MOTION, - DPCODE_PIR, - (lambda d: d.status.get(DPCODE_PIR, "none") == "pir"), - ) - ) - if DPCODE_WATER_SENSOR_STATE in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_MOISTURE, - DPCODE_WATER_SENSOR_STATE, ( - lambda d: d.status.get(DPCODE_WATER_SENSOR_STATE, "normal") - == "alarm" + lambda d: STATE_ALARM_TRIGGERED + if d.status.get(DPCODE_GAS_SENSOR_STATE, 1) == "1" + else STATE_ALARM_ARMING ), ) ) - if DPCODE_SOS_STATE in device.status: - entities.append( - TuyaHaBSensor( - device, - device_manager, - DEVICE_CLASS_PROBLEM, - DPCODE_SOS_STATE, - (lambda d: d.status.get(DPCODE_SOS_STATE, False)), - ) - ) - if DPCODE_PRESENCE_STATE in device.status: + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) + if DPCODE_PIR in device.stastus: entities.append( - TuyaHaBSensor( + TuyaHaAlarm( device, device_manager, - DEVICE_CLASS_MOTION, - DPCODE_PRESENCE_STATE, ( - lambda d: d.status.get(DPCODE_PRESENCE_STATE, "none") - == "presence" + lambda d: STATE_ALARM_TRIGGERED + if d.status.get(DPCODE_GAS_SENSOR_STATE, "none") == "pir" + else STATE_ALARM_ARMING ), ) ) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaBSensor(TuyaHaDevice, BinarySensorEntity): - """Tuya Binary Sensor Device.""" +class TuyaHaAlarm(TuyaHaEntity, AlarmControlPanelEntity): + """Tuya Alarm Device.""" - def __init__( - self, - device: TuyaDevice, - device_manager: TuyaDeviceManager, - sensor_type: str, - sensor_code: str, - sensor_is_on: Callable[..., bool], - ) -> None: - """Init TuyaHaBSensor.""" - self._type = sensor_type - self._code = sensor_code - self._is_on = sensor_is_on - self._attr_unique_id = f"{super().unique_id}{self._code}" - self._attr_name = f"{self.tuya_device.name}_{self._code}" - self._attr_device_class = self._type - self._attr_available = True + def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager, sensor_is_on: Callable[..., str]) -> None: + """Init TuyaHaAlarm.""" super().__init__(device, device_manager) + self._is_on = sensor_is_on @property - def is_on(self): - """Return true if the binary sensor is on.""" + def state(self): + """Return is alarm on.""" return self._is_on(self.tuya_device) - def reset_pir(self): - self.tuya_device.status[DPCODE_PIR] = "none" - self.schedule_update_ha_state() - - def schedule_update_ha_state(self, force_refresh: bool = False) -> None: - - if self._code == DPCODE_PIR: - pir_range = json.loads( - self.tuya_device.status_range.get(DPCODE_PIR, {}).values - ).get("range") - if len(pir_range) == 1 and self.tuya_device.status[DPCODE_PIR] == "pir": - timer = Timer(10, lambda: self.reset_pir()) - timer.start() - - super().schedule_update_ha_state(force_refresh) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_TRIGGER diff --git a/custom_components/tuya_v2/climate.py b/custom_components/tuya_v2/climate.py index 6d7f2c6..810e8ad 100644 --- a/custom_components/tuya_v2/climate.py +++ b/custom_components/tuya_v2/climate.py @@ -1,16 +1,15 @@ """Support for Tuya Climate.""" +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 -from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate import DOMAIN as DEVICE_DOMAIN, ClimateEntity from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, - CURRENT_HVAC_FAN, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -24,12 +23,12 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant +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 tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -50,7 +49,7 @@ DPCODE_HUMIDITY_SET = "humidity_set" DPCODE_FAN_SPEED_ENUM = "fan_speed_enum" -# Temerature unit +# Temperature unit DPCODE_TEMP_UNIT_CONVERT = "temp_unit_convert" DPCODE_C_F = "c_f" @@ -68,6 +67,9 @@ 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, @@ -76,14 +78,6 @@ "auto": HVAC_MODE_AUTO, } -TUYA_ACTION_TO_HA = { - "off": CURRENT_HVAC_OFF, - "heating": CURRENT_HVAC_HEAT, - "cooling": CURRENT_HVAC_COOL, - "wind": CURRENT_HVAC_FAN, - "auto": CURRENT_HVAC_IDLE, -} - TUYA_SUPPORT_TYPE = { "kt", # Air conditioner "qn", # Heater @@ -92,50 +86,57 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya climate dynamically through tuya discovery.""" - _LOGGER.info("climate init") + _LOGGER.debug("climate init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids: list[str]) -> None: """Discover and add a discovered tuya climate.""" - _LOGGER.info(f"climate add->{dev_ids}") + _LOGGER.debug("climate add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Climate.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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(TuyaHaClimate(device, device_manager)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaClimate(TuyaHaDevice, ClimateEntity): +class TuyaHaClimate(TuyaHaEntity, ClimateEntity): """Tuya Switch Device.""" - def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager): + 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: @@ -143,27 +144,33 @@ def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager): else: self.dp_temp_unit = DPCODE_TEMP_UNIT_CONVERT - def get_temp_set_scale(self) -> int: + 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 = json.loads( - self.tuya_device.status_range.get(__dp_temp_set).values - ) - return __temp_set_value_range.get("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 - def get_temp_current_scale(self) -> int: + temp_set_value_range = json.loads(temp_set_value_range_item.values) + return temp_set_value_range.get("scale") + + 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 + dp_temp_current = ( + DPCODE_TEMP_CURRENT if self.is_celsius() else DPCODE_TEMP_CURRENT_F ) - __temp_current_value_range = json.loads( - self.tuya_device.status_range.get(__dp_temp_current).values + temp_current_value_range_item = self.tuya_device.status_range.get( + dp_temp_current ) - return __temp_current_value_range.get("scale") + if not temp_current_value_range_item: + return None + + temp_current_value_range = json.loads(temp_current_value_range_item.values) + return temp_current_value_range.get("scale") # Functions - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" commands = [] if hvac_mode == HVAC_MODE_OFF: @@ -174,20 +181,20 @@ def set_hvac_mode(self, hvac_mode): 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): + 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): + 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): + def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" - commands = [] if swing_mode == SWING_BOTH: commands = [ {"code": DPCODE_SWITCH_VERTICAL, "value": True}, @@ -211,28 +218,31 @@ def set_swing_mode(self, swing_mode): self._send_command(commands) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - _LOGGER.debug(f"climate temp->{kwargs}") - code = DPCODE_TEMP_SET if self.__is_celsius() else DPCODE_TEMP_SET_F + _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 + self._send_command( [ { "code": code, - "value": int( - kwargs["temperature"] * (10 ** self.get_temp_set_scale()) - ), + "value": int(kwargs["temperature"] * (10 ** temp_set_scale)), } ] ) - def __is_celsius(self) -> bool: + 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 - elif ( + if ( DPCODE_TEMP_SET in self.tuya_device.status or DPCODE_TEMP_CURRENT in self.tuya_device.status ): @@ -242,12 +252,12 @@ def __is_celsius(self) -> bool: @property def temperature_unit(self) -> str: """Return true if fan is on.""" - if self.__is_celsius(): + if self.is_celsius(): return TEMP_CELSIUS return TEMP_FAHRENHEIT @property - def current_temperature(self) -> float: + def current_temperature(self) -> float | None: """Return the current temperature.""" if ( DPCODE_TEMP_CURRENT not in self.tuya_device.status @@ -255,17 +265,20 @@ def current_temperature(self) -> float: ): return None - if self.__is_celsius(): - return ( - self.tuya_device.status.get(DPCODE_TEMP_CURRENT, 0) - * 1.0 - / (10 ** self.get_temp_current_scale()) - ) - return ( - self.tuya_device.status.get(DPCODE_TEMP_CURRENT_F, 0) - * 1.0 - / (10 ** self.get_temp_current_scale()) - ) + 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: @@ -273,79 +286,109 @@ def current_humidity(self) -> int: return int(self.tuya_device.status.get(DPCODE_HUMIDITY_CURRENT, 0)) @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return the temperature currently set to be reached.""" - return ( - self.tuya_device.status.get(DPCODE_TEMP_SET, 0) - * 1.0 - / (10 ** self.get_temp_set_scale()) - ) + 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.""" - return self.target_temperature_high + 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.""" - return self.target_temperature_low + temp_set_scal = self.get_temp_set_scale() + if temp_set_scal is None: + return DEFAULT_MIN_TEMP - @property - def target_temperature_high(self) -> float: - """Return the upper bound target temperature.""" - if self.__is_celsius(): + if self.is_celsius(): if DPCODE_TEMP_SET not in self.tuya_device.function: - return 0 - temp_value = json.loads( - self.tuya_device.function.get(DPCODE_TEMP_SET, {}).values - ) - return temp_value.get("max", 0) * 1.0 / (10 ** self.get_temp_set_scale()) - if DPCODE_TEMP_SET_F not in self.tuya_device.function: - return 0 - temp_value = json.loads( - self.tuya_device.function.get(DPCODE_TEMP_SET_F, {}).values - ) - return temp_value.get("max", 0) * 1.0 / (10 ** self.get_temp_set_scale()) + 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) - @property - def target_temperature_low(self) -> float: - """Return the lower bound target temperature.""" - if self.__is_celsius(): - if DPCODE_TEMP_SET not in self.tuya_device.function: - return 0 - temp_value = json.loads( - self.tuya_device.function.get(DPCODE_TEMP_SET, {}).values - ) - low_value = ( - temp_value.get("min", 0) * 1.0 / (10 ** self.get_temp_set_scale()) - ) - return low_value if DPCODE_TEMP_SET_F not in self.tuya_device.function: - return 0 - temp_value = json.loads( - self.tuya_device.function.get(DPCODE_TEMP_SET_F, {}).values - ) - return temp_value.get("min", 0) * 1.0 / (10 ** self.get_temp_set_scale()) + 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: + 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( + temp_set_value_range = json.loads( self.tuya_device.status_range.get( - DPCODE_TEMP_SET if self.__is_celsius() else DPCODE_TEMP_SET_F + DPCODE_TEMP_SET if self.is_celsius() else DPCODE_TEMP_SET_F ).values ) - return ( - __temp_set_value_range.get("step", 0) - * 1.0 - / (10 ** self.get_temp_set_scale()) - ) + 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: @@ -359,10 +402,12 @@ def hvac_mode(self) -> str: return HVAC_MODE_OFF if DPCODE_MODE not in self.tuya_device.status: return HVAC_MODE_OFF - return TUYA_HVAC_TO_HA[self.tuya_device.status.get(DPCODE_MODE)] + 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: + def hvac_modes(self) -> list[str]: """Return hvac modes for select.""" if DPCODE_MODE not in self.tuya_device.function: return [] @@ -370,7 +415,6 @@ def hvac_modes(self) -> list: "range" ) - _LOGGER.debug(f"hvac_modes->{modes}") hvac_modes = [HVAC_MODE_OFF] for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items(): if tuya_mode in modes: @@ -379,28 +423,17 @@ def hvac_modes(self) -> list: return hvac_modes @property - def preset_modes(self) -> list: - """Return available presets.""" - if DPCODE_MODE not in self.tuya_device.function: - return [] - modes = json.loads(self.tuya_device.function.get(DPCODE_MODE, {}).values).get( - "range" - ) - preset_modes = filter(lambda d: d not in TUYA_HVAC_TO_HA.keys(), modes) - return list(preset_modes) - - @property - def fan_mode(self) -> str: + 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 + fan_speed_device_function = self.tuya_device.function.get(DPCODE_FAN_SPEED_ENUM) + if not fan_speed_device_function: + return [] + return json.loads(fan_speed_device_function.values).get("range", []) @property def swing_mode(self) -> str: @@ -426,26 +459,26 @@ def swing_mode(self) -> str: return SWING_OFF @property - def swing_modes(self) -> list: + 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): + 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 = supports | SUPPORT_TARGET_TEMPERATURE + supports |= SUPPORT_TARGET_TEMPERATURE if DPCODE_FAN_SPEED_ENUM in self.tuya_device.status: - supports = supports | SUPPORT_FAN_MODE + supports |= SUPPORT_FAN_MODE if DPCODE_HUMIDITY_SET in self.tuya_device.status: - supports = supports | SUPPORT_TARGET_HUMIDITY + supports |= SUPPORT_TARGET_HUMIDITY if ( DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status or DPCODE_SWITCH_VERTICAL in self.tuya_device.status ): - supports = supports | SUPPORT_SWING_MODE + supports |= SUPPORT_SWING_MODE return supports diff --git a/custom_components/tuya_v2/config_flow.py b/custom_components/tuya_v2/config_flow.py index bc813db..0adc6c2 100644 --- a/custom_components/tuya_v2/config_flow.py +++ b/custom_components/tuya_v2/config_flow.py @@ -1,14 +1,12 @@ """Config flow for Tuya.""" -import json import logging +from tuya_iot import ProjectType, TuyaOpenAPI import voluptuous as vol + from homeassistant import config_entries -from tuya_iot import ProjectType, TuyaOpenAPI -from .aes_cbc import AES_ACCOUNT_KEY, KEY_KEY, XOR_KEY -from .aes_cbc import AesCBC as Aes from .const import ( CONF_ACCESS_ID, CONF_ACCESS_SECRET, @@ -19,25 +17,33 @@ CONF_PROJECT_TYPE, CONF_USERNAME, DOMAIN, - TUYA_APP_TYPE, - TUYA_ENDPOINT, - TUYA_PROJECT_TYPE, + TUYA_APP_TYPES, + TUYA_ENDPOINTS, + TUYA_PROJECT_TYPE_SMART_HOME, + TUYA_PROJECT_TYPES, ) RESULT_SINGLE_INSTANCE = "single_instance_allowed" RESULT_AUTH_FAILED = "invalid_auth" +TUYA_ENDPOINT_BASE = "https://openapi.tuyacn.com" +TUYA_ENDPOINT_OTHER = "https://openapi.tuyaus.com" +COUNTRY_CODE_CHINA = ["86", "+86", "China"] _LOGGER = logging.getLogger(__name__) # Project Type DATA_SCHEMA_PROJECT_TYPE = vol.Schema( - {vol.Required(CONF_PROJECT_TYPE, default=0): vol.In(TUYA_PROJECT_TYPE)} + { + vol.Required(CONF_PROJECT_TYPE, default=TUYA_PROJECT_TYPE_SMART_HOME): vol.In( + TUYA_PROJECT_TYPES.keys() + ) + } ) -# INDUSTRY_SOLUTIONS Schema +# INDUSTY_SOLUTIONS Schema DATA_SCHEMA_INDUSTRY_SOLUTIONS = vol.Schema( { - vol.Required(CONF_ENDPOINT): vol.In(TUYA_ENDPOINT), + vol.Required(CONF_ENDPOINT): vol.In(TUYA_ENDPOINTS.keys()), vol.Required(CONF_ACCESS_ID): str, vol.Required(CONF_ACCESS_SECRET): str, vol.Required(CONF_USERNAME): str, @@ -50,15 +56,13 @@ { 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_APP_TYPE): vol.In(TUYA_APP_TYPES.keys()), vol.Required(CONF_COUNTRY_CODE): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, } ) -COUNTRY_CODE_CHINA = ["86", "+86", "China"] - class TuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Tuya Config Flow.""" @@ -67,15 +71,12 @@ def __init__(self) -> None: """Init tuya config flow.""" super().__init__() self.conf_project_type = None - self.project_type = ProjectType.SMART_HOME - self.is_import = False - @classmethod - def _try_login(cls, user_input): - _LOGGER.info(f"TuyaConfigFlow._try_login start, user_input: {user_input}") + @staticmethod + def _try_login(user_input): project_type = ProjectType(user_input[CONF_PROJECT_TYPE]) api = TuyaOpenAPI( - user_input[CONF_ENDPOINT] + TUYA_ENDPOINTS[user_input[CONF_ENDPOINT]] if project_type == ProjectType.INDUSTY_SOLUTIONS else "", user_input[CONF_ACCESS_ID], @@ -88,98 +89,62 @@ def _try_login(cls, user_input): response = api.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) else: if user_input[CONF_COUNTRY_CODE] in COUNTRY_CODE_CHINA: - api.endpoint = "https://openapi.tuyacn.com" + api.endpoint = TUYA_ENDPOINT_BASE else: - api.endpoint = "https://openapi.tuyaus.com" + api.endpoint = TUYA_ENDPOINT_OTHER response = api.login( user_input[CONF_USERNAME], user_input[CONF_PASSWORD], user_input[CONF_COUNTRY_CODE], - user_input[CONF_APP_TYPE], + TUYA_APP_TYPES[user_input[CONF_APP_TYPE]], ) - if response.get("success", False): + 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 - _LOGGER.info(f"TuyaConfigFlow._try_login finish, response:, {response}") return response - async def async_step_import(self, user_input=None): - """Step import.""" - self.is_import = True - return await self.async_step_user(user_input) - - async def async_step_project_type(self, user_input=None): - """Step project type.""" - self.conf_project_type = user_input[CONF_PROJECT_TYPE] - self.project_type = ProjectType(self.conf_project_type) - return ( - self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_SMART_HOME) - if self.project_type == ProjectType.SMART_HOME - else self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_INDUSTRY_SOLUTIONS - ) - ) - async def async_step_user(self, user_input=None): """Step user.""" - _LOGGER.info( - f"TuyaConfigFlow.async_step_user start, is_import= {self.is_import}" - ) - _LOGGER.info(f"TuyaConfigFlow.async_step_user start, user_input= {user_input}") + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_PROJECT_TYPE + ) - if self._async_current_entries(): - return self.async_abort(reason=RESULT_SINGLE_INSTANCE) + 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: - if self.conf_project_type is not None: - user_input[CONF_PROJECT_TYPE] = self.conf_project_type + assert self.conf_project_type is not None + user_input[CONF_PROJECT_TYPE] = TUYA_PROJECT_TYPES[self.conf_project_type] response = await self.hass.async_add_executor_job( self._try_login, user_input ) if response.get("success", False): - aes = Aes() - _LOGGER.info("TuyaConfigFlow.async_step_user login success") - cbc_key = aes.random_16() - cbc_iv = aes.random_16() - access_id = user_input[CONF_ACCESS_ID] - access_id_entry = aes.cbc_encrypt(cbc_key, cbc_iv, access_id) - c = cbc_key + cbc_iv - c_xor_entry = aes.xor_encrypt(c, access_id_entry) - # account info encrypted with AES-CBC - user_input_encrpt = aes.cbc_encrypt( - cbc_key, cbc_iv, json.dumps(user_input) - ) - # account info encrypted add to cache + _LOGGER.debug("Login success: %s", response) return self.async_create_entry( title=user_input[CONF_USERNAME], - data={ - AES_ACCOUNT_KEY: user_input_encrpt, - XOR_KEY: c_xor_entry, - KEY_KEY: access_id_entry, - }, + data=user_input, ) - errors["base"] = RESULT_AUTH_FAILED - if self.is_import: - return self.async_abort(reason=errors["base"]) + _LOGGER.error("Login failed: %s", response) - return ( - self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA_SMART_HOME, errors=errors - ) - if self.project_type == ProjectType.SMART_HOME - else self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA_INDUSTRY_SOLUTIONS, - errors=errors, - ) + if (ProjectType(TUYA_PROJECT_TYPES[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="project_type", data_schema=DATA_SCHEMA_PROJECT_TYPE, errors=errors + step_id="login", + data_schema=DATA_SCHEMA_INDUSTRY_SOLUTIONS, + errors=errors, ) diff --git a/custom_components/tuya_v2/const.py b/custom_components/tuya_v2/const.py index 90d7cea..40a058f 100644 --- a/custom_components/tuya_v2/const.py +++ b/custom_components/tuya_v2/const.py @@ -17,22 +17,29 @@ TUYA_MQTT_LISTENER = "tuya_mqtt_listener" TUYA_HA_TUYA_MAP = "tuya_ha_tuya_map" TUYA_HA_DEVICES = "tuya_ha_devices" -TUYA_SETUP_PLATFORM = "setup_platform" - -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_HA_SIGNAL_UPDATE_ENTITY = "tuya_entry_update" + +TUYA_PROJECT_TYPE_INDUSTY_SOLUTIONS = "Custom Development" +TUYA_PROJECT_TYPE_SMART_HOME = "Smart Home PaaS" + +TUYA_PROJECT_TYPES = { + TUYA_PROJECT_TYPE_SMART_HOME: 0, + TUYA_PROJECT_TYPE_INDUSTY_SOLUTIONS: 1, } -TUYA_PROJECT_TYPE = {1: "Custom Development", 0: "Smart Home PaaS"} +TUYA_ENDPOINTS = { + "America": "https://openapi.tuyaus.com", + "China": "https://openapi.tuyacn.com", + "Europe": "https://openapi.tuyaeu.com", + "India": "https://openapi.tuyain.com", + "Eastern America": "https://openapi-ueaz.tuyaus.com", + "Western Europe": "https://openapi-weaz.tuyaeu.com", +} -TUYA_APP_TYPE = {"tuyaSmart": "TuyaSmart", "smartlife": "Smart Life"} +TUYA_APP_TYPES = {"TuyaSmart": "tuyaSmart", "Smart Life": "smartlife"} -TUYA_SUPPORT_HA_TYPE = [ +PLATFORMS = [ "binary_sensor", "climate", "cover", diff --git a/custom_components/tuya_v2/cover.py b/custom_components/tuya_v2/cover.py index 77a1dde..aec88a2 100644 --- a/custom_components/tuya_v2/cover.py +++ b/custom_components/tuya_v2/cover.py @@ -14,10 +14,11 @@ CoverEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -41,47 +42,54 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: """Set up tuya cover dynamically through tuya discovery.""" - _LOGGER.info("cover init") + _LOGGER.debug("cover init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya cover.""" - _LOGGER.info(f"cover add-> {dev_ids}") + _LOGGER.debug(f"cover add-> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Cover.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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(TuyaHaCover(device, device_manager)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaCover(TuyaHaDevice, CoverEntity): +class TuyaHaCover(TuyaHaEntity, CoverEntity): """Tuya Switch Device.""" _attr_device_class = DEVICE_CLASS_CURTAIN diff --git a/custom_components/tuya_v2/fan.py b/custom_components/tuya_v2/fan.py index cc98b51..15a8e55 100644 --- a/custom_components/tuya_v2/fan.py +++ b/custom_components/tuya_v2/fan.py @@ -5,9 +5,12 @@ import logging from typing import Any -from homeassistant.components.fan import DIRECTION_FORWARD, DIRECTION_REVERSE -from homeassistant.components.fan import DOMAIN as DEVICE_DOMAIN +from tuya_iot import TuyaDevice, TuyaDeviceManager + from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN as DEVICE_DOMAIN, SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_PRESET_MODE, @@ -15,16 +18,15 @@ FanEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -56,81 +58,87 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Set up tuya fan dynamically through tuya discovery.""" - _LOGGER.info("fan init") + _LOGGER.debug("fan init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids: list[str]) -> None: """Discover and add a discovered tuya fan.""" - _LOGGER.info(f"fan add-> {dev_ids}") + _LOGGER.debug("fan add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[TuyaHaFan]: """Set up Tuya Fan.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + device_manager = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE_MANAGER] entities = [] for device_id in device_ids: device = device_manager.device_map[device_id] if device is None: continue entities.append(TuyaHaFan(device, device_manager)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaFan(TuyaHaDevice, FanEntity): +class TuyaHaFan(TuyaHaEntity, FanEntity): """Tuya Fan Device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: """Init Tuya Fan Device.""" super().__init__(device, device_manager) + 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": - try: - if ( - DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.status - or DPCODE_AP_FAN_SPEED in self.tuya_device.status - ): - - self.dp_code_speed_enum = ( - DPCODE_AP_FAN_SPEED_ENUM - if DPCODE_AP_FAN_SPEED_ENUM in self.tuya_device.status - else DPCODE_AP_FAN_SPEED - ) - data = json.loads( - self.tuya_device.function.get( - 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 - except Exception: - _LOGGER.error("Cannot parse the air-purifier speed range") + 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.""" @@ -183,11 +191,9 @@ def is_on(self) -> bool: @property def current_direction(self) -> str: """Return the current direction of the fan.""" - return ( - DIRECTION_FORWARD - if self.tuya_device.status.get(DPCODE_FAN_DIRECTION) - else DIRECTION_REVERSE - ) + if self.tuya_device.status[DPCODE_FAN_DIRECTION]: + return DIRECTION_FORWARD + return DIRECTION_REVERSE @property def oscillating(self) -> bool: @@ -195,41 +201,35 @@ def oscillating(self) -> bool: return self.tuya_device.status.get(DPCODE_SWITCH_HORIZONTAL, False) @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Return the list of available preset_modes.""" - if DPCODE_MODE not in self.tuya_device.function: - return [] - - try: - data = json.loads( - self.tuya_device.function.get(DPCODE_MODE, {}).values - ).get("range") - return data - except Exception: - _LOGGER.error("Cannot parse the preset modes") - return [] + return self.ha_preset_modes @property def preset_mode(self) -> str: """Return the current preset_mode.""" - return self.tuya_device.status.get(DPCODE_MODE) + return self.tuya_device.status[DPCODE_MODE] @property - def percentage(self) -> int: + def percentage(self) -> int | None: """Return the current speed.""" if not self.is_on: return 0 - if self.tuya_device.category == "kj": - if self.air_purifier_speed_range_len > 1: - if not self.air_purifier_speed_range_enum: - # 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.get(DPCODE_AP_FAN_SPEED_ENUM, 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.get(DPCODE_FAN_SPEED, 0) + # some type may not have the fan_speed_percent key + return self.tuya_device.status.get(DPCODE_FAN_SPEED) @property def speed_count(self) -> int: @@ -243,18 +243,18 @@ def supported_features(self): """Flag supported features.""" supports = 0 if DPCODE_MODE in self.tuya_device.status: - supports = supports | SUPPORT_PRESET_MODE + supports |= SUPPORT_PRESET_MODE if DPCODE_FAN_SPEED in self.tuya_device.status: - supports = supports | SUPPORT_SET_SPEED + supports |= SUPPORT_SET_SPEED if DPCODE_SWITCH_HORIZONTAL in self.tuya_device.status: - supports = supports | SUPPORT_OSCILLATE + supports |= SUPPORT_OSCILLATE if DPCODE_FAN_DIRECTION in self.tuya_device.status: - supports = supports | SUPPORT_DIRECTION + 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 = supports | SUPPORT_SET_SPEED + supports |= SUPPORT_SET_SPEED return supports diff --git a/custom_components/tuya_v2/humidifier.py b/custom_components/tuya_v2/humidifier.py index 10b9ae5..4305eb9 100644 --- a/custom_components/tuya_v2/humidifier.py +++ b/custom_components/tuya_v2/humidifier.py @@ -8,12 +8,13 @@ from homeassistant.components.humidifier import DOMAIN as DEVICE_DOMAIN from homeassistant.components.humidifier import SUPPORT_MODES, HumidifierEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -39,49 +40,55 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): """Set up tuya sensors dynamically through tuya discovery.""" - _LOGGER.info("humidifier init") + _LOGGER.debug("humidifier init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.info(f"humidifier add-> {dev_ids}") + _LOGGER.debug(f"humidifier add-> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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(TuyaHaHumidifier(device, device_manager)) - + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaHumidifier(TuyaHaDevice, HumidifierEntity): +class TuyaHaHumidifier(TuyaHaEntity, HumidifierEntity): """Tuya Switch Device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: diff --git a/custom_components/tuya_v2/light.py b/custom_components/tuya_v2/light.py index 5a94062..7609091 100644 --- a/custom_components/tuya_v2/light.py +++ b/custom_components/tuya_v2/light.py @@ -1,28 +1,31 @@ """Support for the Tuya lights.""" +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, -) -from homeassistant.components.light import DOMAIN as DEVICE_DOMAIN -from homeassistant.components.light import ( - 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.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -80,47 +83,59 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya light dynamically through tuya discovery.""" - _LOGGER.info("light init") + _LOGGER.debug("light init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids: list[str]): """Discover and add a discovered tuya light.""" - _LOGGER.info(f"light add-> {dev_ids}") + _LOGGER.debug("light add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass, entry: ConfigEntry, device_ids: list[str] +) -> list[TuyaHaLight]: """Set up Tuya Light device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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(TuyaHaLight(device, device_manager)) + + 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 TuyaHaLight(TuyaHaDevice, LightEntity): +class TuyaHaLight(TuyaHaEntity, LightEntity): """Tuya light device.""" def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: @@ -147,12 +162,8 @@ def is_on(self) -> bool: def turn_on(self, **kwargs: Any) -> None: """Turn on or control the light.""" commands = [] - _LOGGER.debug(f"light kwargs-> {kwargs}") - # if ( - # ATTR_BRIGHTNESS not in kwargs - # and ATTR_HS_COLOR not in kwargs - # and ATTR_COLOR_TEMP not in kwargs - # ): + _LOGGER.debug("light kwargs-> %s", kwargs) + if ( DPCODE_LIGHT in self.tuya_device.status and DPCODE_SWITCH not in self.tuya_device.status @@ -165,7 +176,6 @@ def turn_on(self, **kwargs: Any) -> None: if self._work_mode().startswith(WORK_MODE_COLOUR): colour_data = self._get_hsv() v_range = self._tuya_hsv_v_range() - # hsv_v = colour_data.get('v', 0) colour_data["v"] = int( self.remap(kwargs[ATTR_BRIGHTNESS], 0, 255, v_range[0], v_range[1]) ) @@ -180,7 +190,6 @@ def turn_on(self, **kwargs: Any) -> None: ) ) commands += [{"code": self.dp_code_bright, "value": tuya_brightness}] - # commands += [{'code': DPCODE_WORK_MODE, 'value': self._work_mode()}] if ATTR_HS_COLOR in kwargs: colour_data = self._get_hsv() @@ -246,21 +255,16 @@ def turn_off(self, **kwargs: Any) -> None: self._send_command(commands) @property - def brightness(self): + 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) - _LOGGER.debug( - f"""brightness id-> {self.tuya_device.id}, - work_mode-> {self._work_mode()}, - check true-> {self._work_mode().startswith(WORK_MODE_COLOUR)}""" - ) - if self._work_mode().startswith(WORK_MODE_COLOUR): - colour_data = json.loads( - self.tuya_device.status.get(self.dp_code_colour, 0) - ) + 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)) @@ -270,16 +274,19 @@ def brightness(self): def _tuya_brightness_range(self) -> tuple[int, int]: if self.dp_code_bright not in self.tuya_device.status: return 0, 255 - - bright_value = json.loads( - self.tuya_device.function.get(self.dp_code_bright, {}).values - ) + 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): + def hs_color(self) -> tuple[float, float] | None: """Return the hs_color of the light.""" - colour_data = json.loads(self.tuya_device.status.get(self.dp_code_colour, 0)) + 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), @@ -290,11 +297,11 @@ def hs_color(self): ) @property - def color_temp(self): + 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 = ( + return ( self.max_mireds - self.remap( tuya_color_temp, @@ -305,66 +312,77 @@ def color_temp(self): ) + self.min_mireds ) - return ha_color_temp @property - def min_mireds(self): + def min_mireds(self) -> int: """Return color temperature min mireds.""" return MIREDS_MIN @property - def max_mireds(self): + def max_mireds(self) -> int: """Return color temperature max mireds.""" return MIREDS_MAX def _tuya_temp_range(self) -> tuple[int, int]: - temp_value = json.loads( - self.tuya_device.function.get(self.dp_code_temp, {}).values - ) + 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]: - colour_data = self._tuya_hsv_function() - hsv_s = colour_data.get("s") - return hsv_s.get("min", 0), hsv_s.get("max", 255) + 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]: - colour_data = self._tuya_hsv_function() - hsv_v = colour_data.get("v") - return hsv_v.get("min", 0), hsv_v.get("max", 255) - - def _tuya_hsv_function(self): - hsv_data = json.loads( - self.tuya_device.function.get(self.dp_code_colour, {}).values - ) - if hsv_data == {}: - return ( - DEFAULT_HSV_V2 - if self.dp_code_colour == DPCODE_COLOUR_DATA_V2 - else DEFAULT_HSV - ) - - return hsv_data + 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_features(self): - """Flag supported features.""" - supports = 0 + 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: - supports = supports | SUPPORT_BRIGHTNESS + 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 ): - supports = supports | SUPPORT_COLOR - if self.dp_code_temp in self.tuya_device.status: - supports = supports | SUPPORT_COLOR_TEMP - return supports + color_modes.append(COLOR_MODE_HS) + return set(color_modes) diff --git a/custom_components/tuya_v2/number.py b/custom_components/tuya_v2/number.py index b0e83db..e12b701 100644 --- a/custom_components/tuya_v2/number.py +++ b/custom_components/tuya_v2/number.py @@ -7,12 +7,13 @@ from homeassistant.components.number import DOMAIN as DEVICE_DOMAIN from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -42,38 +43,44 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya number dynamically through tuya discovery.""" - _LOGGER.info("number init") + _LOGGER.debug("number init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya number.""" - _LOGGER.info(f"number add-> {dev_ids}") + _LOGGER.debug(f"number add-> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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: @@ -81,26 +88,32 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): if DPCODE_SENSITIVITY in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_SENSITIVITY)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_TEMPSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_TEMPSET)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_WARMTIME in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_WARMTIME)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_WATERSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_WATERSET)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_POWDERSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_POWDERSET)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_CLOUDRECIPE in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_CLOUDRECIPE)) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaNumber(TuyaHaDevice, NumberEntity): +class TuyaHaNumber(TuyaHaEntity, NumberEntity): """Tuya Device Number.""" def __init__( @@ -122,7 +135,7 @@ def unique_id(self) -> str | None: @property def name(self) -> str | None: """Return Tuya device name.""" - return self.tuya_device.name + self._code + return f"{self.tuya_device.name}{self._code}" @property def value(self) -> float: diff --git a/custom_components/tuya_v2/scene.py b/custom_components/tuya_v2/scene.py index ed4773a..c90c679 100644 --- a/custom_components/tuya_v2/scene.py +++ b/custom_components/tuya_v2/scene.py @@ -4,11 +4,12 @@ import logging from typing import Any +from tuya_iot import TuyaHomeManager, TuyaScene + from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from tuya_iot import TuyaHomeManager, TuyaScene from .const import DOMAIN, TUYA_HOME_MANAGER @@ -16,19 +17,12 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya scenes.""" - _LOGGER.info("scenes init") - - entities = [] - - __home_manager = hass.data[DOMAIN][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) + home_manager = hass.data[DOMAIN][entry.entry_id][TUYA_HOME_MANAGER] + scenes = await hass.async_add_executor_job(home_manager.query_scenes) + async_add_entities(TuyaHAScene(home_manager, scene) for scene in scenes) class TuyaHAScene(Scene): @@ -58,24 +52,18 @@ def name(self) -> str | None: @property def device_info(self): """Return a device description for device registry.""" - _device_info = { + return { "identifiers": {(DOMAIN, f"{self.unique_id}")}, "manufacturer": "tuya", "name": self.scene.name, "model": "Tuya Scene", } - return _device_info @property def available(self) -> bool: """Return if the scene is enabled.""" return self.scene.enabled - @property - def current_activity(self) -> str | None: - """Active activity.""" - return self.scene.name - def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self.home_manager.trigger_scene(self.scene.home_id, self.scene.scene_id) diff --git a/custom_components/tuya_v2/select.py b/custom_components/tuya_v2/select.py index 57abd85..d1871e4 100644 --- a/custom_components/tuya_v2/select.py +++ b/custom_components/tuya_v2/select.py @@ -7,11 +7,12 @@ from homeassistant.components.select import DOMAIN as DEVICE_DOMAIN from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -52,30 +53,35 @@ async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +) -> None: + """Set up tuya select dynamically through tuya discovery.""" _LOGGER.info("select init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): _LOGGER.info(f"select add-> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) def get_auto_generate_data_points(status) -> list: @@ -87,9 +93,12 @@ def get_auto_generate_data_points(status) -> list: return dps -def _setup_entities(hass: HomeAssistant, device_ids: list): - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] +def _setup_entities( + hass, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: + """Set up Tuya Select.""" + 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: @@ -97,11 +106,13 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): for data_point in get_auto_generate_data_points(device.status): entities.append(TuyaHaSelect(device, device_manager, data_point)) - + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaSelect(TuyaHaDevice, SelectEntity): +class TuyaHaSelect(TuyaHaEntity, SelectEntity): + """Tuya Select Device.""" + def __init__( self, device: TuyaDevice, device_manager: TuyaDeviceManager, code: str = "" ): diff --git a/custom_components/tuya_v2/sensor.py b/custom_components/tuya_v2/sensor.py index 98cdafb..37c2321 100644 --- a/custom_components/tuya_v2/sensor.py +++ b/custom_components/tuya_v2/sensor.py @@ -1,4 +1,5 @@ """Support for Tuya sensors.""" +from __future__ import annotations import json import logging @@ -23,13 +24,14 @@ TIME_DAYS, TIME_MINUTES, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType +from homeassistant.helpers.entity import Entity from tuya_iot import TuyaDevice, TuyaDeviceManager -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -115,38 +117,47 @@ DPCODE_BATTERY_VALUE = "battery_value" async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up tuya sensors dynamically through tuya discovery.""" - _LOGGER.info("sensor init") + _LOGGER.debug("sensor init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids: list[str]): """Discover and add a discovered tuya sensor.""" - _LOGGER.info(f"sensor add-> {dev_ids}") + _LOGGER.debug(f"sensor add-> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) + for entity in entities: + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entity._attr_unique_id) + async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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: @@ -290,7 +301,6 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): PERCENTAGE, ) ) - if DPCODE_TEMPERATURE in device.status: entities.append( TuyaHaSensor( @@ -311,7 +321,6 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): TEMP_CELSIUS, ) ) - if DPCODE_HUMIDITY in device.status: entities.append( TuyaHaSensor( @@ -332,7 +341,6 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): PERCENTAGE, ) ) - if DPCODE_PM100_VALUE in device.status: entities.append( TuyaHaSensor( @@ -351,7 +359,6 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): device, device_manager, "PM1.0", DPCODE_PM10_VALUE, "ug/m³" ) ) - if DPCODE_CURRENT in device.status: entities.append( TuyaHaSensor( @@ -457,7 +464,7 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): return entities -class TuyaHaSensor(TuyaHaDevice, SensorEntity): +class TuyaHaSensor(TuyaHaEntity, SensorEntity): """Tuya Sensor Device.""" def __init__( @@ -469,13 +476,18 @@ def __init__( sensor_unit: str, ) -> None: """Init TuyaHaSensor.""" + super().__init__(device, device_manager) self._code = sensor_code self._attr_device_class = sensor_type self._attr_name = self.tuya_device.name + "_" + self._attr_device_class self._attr_unique_id = f"{super().unique_id}{self._code}" self._attr_unit_of_measurement = sensor_unit self._attr_available = True - super().__init__(device, device_manager) + + @property + def unique_id(self) -> str | None: + """Return a unique ID.""" + return self._attr_unique_id @property def state(self) -> StateType: diff --git a/custom_components/tuya_v2/strings.json b/custom_components/tuya_v2/strings.json index 59996e9..91ca045 100644 --- a/custom_components/tuya_v2/strings.json +++ b/custom_components/tuya_v2/strings.json @@ -2,13 +2,13 @@ "config": { "flow_title": "Tuya configuration", "step": { - "project_type":{ + "user":{ "title":"Tuya Integration", "data":{ "tuya_project_type": "Tuya cloud project type" } }, - "user": { + "login": { "title": "Tuya", "description": "Enter your Tuya credential", "data": { @@ -22,11 +22,6 @@ } } }, - "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%]" } diff --git a/custom_components/tuya_v2/switch.py b/custom_components/tuya_v2/switch.py index 2e91a8a..0c9b18b 100644 --- a/custom_components/tuya_v2/switch.py +++ b/custom_components/tuya_v2/switch.py @@ -5,14 +5,16 @@ import logging from typing import Any -from homeassistant.components.switch import DOMAIN as DEVICE_DOMAIN -from homeassistant.components.switch import SwitchEntity +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 tuya_iot import TuyaDevice, TuyaDeviceManager +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -29,15 +31,9 @@ "pc", # Power Strip "bh", # Smart Kettle "dlq", # Breaker - "tdq", # Breaker "cwysj", # Pet Water Feeder "kj", # Air Purifier "xxj", # Diffuser - "ckmkzq", # Garage Door Opener - "zndb", # Smart Electricity Meter - "fs", # Fan - "sd", # Vacuum - "zndb" "kfj", # Smart Electricity Meter # Coffee Maker } # Switch(kg), Socket(cz), Power Strip(pc) @@ -60,58 +56,51 @@ DPCODE_WRESET = "water_reset" # Pet Water Feeder - Resetting of water usage days DPCODE_START = "start" -# Coffee Maker -# https://developer.tuya.com/en/docs/iot/f?id=K9gf4701ox167 -DPCODE_PAUSE = "pause" -DPCODE_WARM = "warm" -DPCODE_CLEANING = "cleaning" -# Fan -# https://developer.tuya.com/en/docs/iot/f?id=K9gf45vs7vkge -DPCODE_FAN_LIGHT = "light" - -# Vacuum -#https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo -DPCODE_VOICE = "voice_switch" + async def async_setup_entry( - hass: HomeAssistant, _entry: ConfigEntry, async_add_entities -): + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up tuya sensors dynamically through tuya discovery.""" - _LOGGER.info("switch init") + _LOGGER.debug("switch init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.info(f"switch add-> {dev_ids}") + _LOGGER.debug("switch add-> %s", dev_ids) if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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 for function in device.function: + tuya_ha_switch = None if device.category == "kj": if function in [ DPCODE_ANION, @@ -120,47 +109,31 @@ def _setup_entities(hass: HomeAssistant, device_ids: list): DPCODE_LOCK, DPCODE_UV, DPCODE_WET, - DPCODE_FAN_LIGHT, ]: - entities.append(TuyaHaSwitch(device, device_manager, function)) + 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]: - entities.append(TuyaHaSwitch(device, device_manager, function)) + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) if function.startswith(DPCODE_SWITCH): # Main device switch - entities.append(TuyaHaSwitch(device, device_manager, function)) - continue - - elif device.category == "kfj": - if function in [ - DPCODE_SWITCH, - DPCODE_START, - DPCODE_PAUSE, - DPCODE_WARM, - DPCODE_CLEANING, - ]: - entities.append(TuyaHaSwitch(device, device_manager, function)) - continue - - elif device.category == "sd": - if function in [DPCODE_VOICE ]: - entities.append(TuyaHaSwitch( - device, device_manager, function)) - continue - + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) else: if function.startswith(DPCODE_START): - entities.append(TuyaHaSwitch(device, device_manager, function)) - continue + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) if function.startswith(DPCODE_SWITCH): - entities.append(TuyaHaSwitch(device, device_manager, function)) - continue + 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 TuyaHaSwitch(TuyaHaDevice, SwitchEntity): +class TuyaHaSwitch(TuyaHaEntity, SwitchEntity): """Tuya Switch Device.""" dp_code_switch = DPCODE_SWITCH @@ -187,7 +160,7 @@ def unique_id(self) -> str | None: @property def name(self) -> str | None: """Return Tuya device name.""" - return self.tuya_device.name + self.channel + return f"{self.tuya_device.name}{self.channel}" @property def is_on(self) -> bool: diff --git a/custom_components/tuya_v2/translations/de.json b/custom_components/tuya_v2/translations/de.json index fa591c2..5edd8d9 100644 --- a/custom_components/tuya_v2/translations/de.json +++ b/custom_components/tuya_v2/translations/de.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Verbinden fehlgeschlagen", - "invalid_auth": "Ungültige Authentifizierung", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration möglich." - }, - "error": { - "invalid_auth": "Ungültige Authentifizierung" - }, "flow_title": "Tuya Konfiguration", "step": { - "project_type":{ + "user":{ "title":"Tuya Integration", "data":{ "tuya_project_type": "Tuya Cloud Projekttyp" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Geben Sie Ihre Tuya Anmeldedaten ein.", "data": { "endpoint": "Verfügbarkeitszone", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "Länder Code", "username": "Benutzername", "password": "Passwort" - }, - "description": "Geben Sie Ihre Tuya Anmeldedaten ein.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Ungültige Authentifizierung" } } } diff --git a/custom_components/tuya_v2/translations/en.json b/custom_components/tuya_v2/translations/en.json index 42c2572..087ee84 100644 --- a/custom_components/tuya_v2/translations/en.json +++ b/custom_components/tuya_v2/translations/en.json @@ -1,22 +1,16 @@ { "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": { - "project_type":{ + "user":{ "title":"Tuya Integration", "data":{ "tuya_project_type": "Tuya cloud project type" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Enter your Tuya credential.", "data": { "endpoint": "Availability Zone", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "Country Code", "username": "Account", "password": "Password" - }, - "description": "Enter your Tuya credential.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Invalid authentication" } } } diff --git a/custom_components/tuya_v2/translations/es.json b/custom_components/tuya_v2/translations/es.json index e734293..4107e76 100644 --- a/custom_components/tuya_v2/translations/es.json +++ b/custom_components/tuya_v2/translations/es.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticación inválida", - "single_instance_allowed": "Ya esta configurado. Solo es posible una única configuración." - }, - "error": { - "invalid_auth": "Autenticación inválida" - }, "flow_title": "Configuración de Tuya", "step": { - "project_type":{ + "user":{ "title":"Integración Tuya", "data":{ "tuya_project_type": "Tipo de cloud Tuya" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Ingrese su credencial Tuya.", "data": { "endpoint": "Zona de disponibilidad", "access_id": "ID de acceso", @@ -25,10 +19,11 @@ "country_code": "Código de país", "username": "Cuenta", "password": "Contraseña" - }, - "description": "Ingrese su credencial Tuya.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Autenticación inválida" } } } diff --git a/custom_components/tuya_v2/translations/fr.json b/custom_components/tuya_v2/translations/fr.json index 0eae137..edb8a26 100644 --- a/custom_components/tuya_v2/translations/fr.json +++ b/custom_components/tuya_v2/translations/fr.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Impossible de se connecter", - "invalid_auth": "Authentification invalide", - "single_instance_allowed": "Configuration déjà effectuée. Une seule configuration est possible." - }, - "error": { - "invalid_auth": "Authentification invalide" - }, "flow_title": "Configuration de Tuya", "step": { - "project_type":{ + "user":{ "title":"Intégration Tuya", "data":{ "tuya_project_type": "Type de projet cloud Tuya" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Saisissez vos identifiants Tuya.", "data": { "endpoint": "Région", "access_id": "Access ID", @@ -25,9 +19,10 @@ "country_code": "Code Pays", "username": "Compte", "password": "Mot de passe" - }, - "description": "Saisissez vos identifiants Tuya.", - "title": "Tuya" + } + }, + "error": { + "invalid_auth": "Authentification invalide" } } } diff --git a/custom_components/tuya_v2/translations/hi.json b/custom_components/tuya_v2/translations/hi.json index 32f3c84..b91a455 100644 --- a/custom_components/tuya_v2/translations/hi.json +++ b/custom_components/tuya_v2/translations/hi.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "जुडने में विफल", - "invalid_auth": "अमान्य प्रमाणीकरण", - "single_instance_allowed": "पहले से ही कॉन्फ़िगर किया गया है। केवल एक ही विन्यास संभव है।" - }, - "error": { - "invalid_auth": "अमान्य प्रमाणीकरण" - }, "flow_title": "Tuya विन्यास", "step": { - "project_type":{ + "user":{ "title":"Tuya एकीकरण", "data":{ "tuya_project_type": "Tuya क्लाउड प्रोजेक्ट टाइप" } }, - "user": { + "login": { + "title": "Tuya", + "description": "अपना Tuya क्रेडेंशियल दर्ज करें", "data": { "endpoint": "उपलब्धता क्षेत्र", "access_id": "एक्सेस आईडी", @@ -25,10 +19,11 @@ "country_code": "कंट्री कोड", "username": "अकाउंट", "password": "पासवर्ड" - }, - "description": "अपना Tuya क्रेडेंशियल दर्ज करें", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "अमान्य प्रमाणीकरण" } } } diff --git a/custom_components/tuya_v2/translations/hu.json b/custom_components/tuya_v2/translations/hu.json index f4cf96e..d8b28d1 100644 --- a/custom_components/tuya_v2/translations/hu.json +++ b/custom_components/tuya_v2/translations/hu.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Sikertelen kapcsolódás", - "invalid_auth": "Érvénytelen hitelesítés", - "single_instance_allowed": "Már be van konfigurálva. Csak egyetlen konfiguráció lehetséges." - }, - "error": { - "invalid_auth": "Érvénytelen hitelesítés" - }, "flow_title": "Tuya konfiguráció", "step": { - "project_type":{ + "user":{ "title":"Tuya Integráció", "data":{ "tuya_project_type": "A Tuya cloud projekt típusa" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Adja meg a Tuya hitelesítő adatait.", "data": { "endpoint": "A rendelkezésre álló zóna", "access_id": "Hozzáférés azonosító", @@ -25,10 +19,11 @@ "country_code": "Országkód", "username": "Felhasználónév", "password": "Jelszó" - }, - "description": "Adja meg a Tuya hitelesítő adatait.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Érvénytelen hitelesítés" } } } \ No newline at end of file diff --git a/custom_components/tuya_v2/translations/id.json b/custom_components/tuya_v2/translations/id.json index 193a865..7ef0019 100644 --- a/custom_components/tuya_v2/translations/id.json +++ b/custom_components/tuya_v2/translations/id.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Koneksi Gagal", - "invalid_auth": "Otentikasi Salah", - "single_instance_allowed": "Sudah terkonfigurasi. Anda hanya bisa melakukan satu konfigurasi saja." - }, - "error": { - "invalid_auth": "Otentikasi Salah" - }, "flow_title": "Konfigurasi Tuya", "step": { - "project_type":{ + "user":{ "title":"Integrasi Tuya", "data":{ "tuya_project_type": "Tipe dari Projek Tuya cloud" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Masukkan kredensial dari Tuya Anda.", "data": { "endpoint": "Zona yang Tersedia", "access_id": "Akses ID", @@ -25,10 +19,11 @@ "country_code": "Kode Negara", "username": "Akun", "password": "Password" - }, - "description": "Masukkan kredensial dari Tuya Anda.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Otentikasi Salah" } } } diff --git a/custom_components/tuya_v2/translations/it.json b/custom_components/tuya_v2/translations/it.json index ab9e6bc..3a69557 100644 --- a/custom_components/tuya_v2/translations/it.json +++ b/custom_components/tuya_v2/translations/it.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Connessione fallita", - "invalid_auth": "Autenticazione fallita", - "single_instance_allowed": "Hai già configurato questa integrazione. Puoi farlo solo una volta." - }, - "error": { - "invalid_auth": "Autenticazione non valida" - }, "flow_title": "Configurazione Tuya", "step": { - "project_type":{ + "user":{ "title":"Integrazione Tuya", "data":{ "tuya_project_type": "Tipo di progetto cloud Tuya" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Inserisci le tue credenziali Tuya.", "data": { "endpoint": "Zona di disponibilità", "access_id": "ID di accesso", @@ -25,10 +19,11 @@ "country_code": "Codice Paese", "username": "Account", "password": "Password" - }, - "description": "Inserisci le tue credenziali Tuya.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Autenticazione non valida" } } } diff --git a/custom_components/tuya_v2/translations/kn.json b/custom_components/tuya_v2/translations/kn.json index dd9e371..81d5970 100644 --- a/custom_components/tuya_v2/translations/kn.json +++ b/custom_components/tuya_v2/translations/kn.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "ಸಂಪರ್ಕಿಸಲು ವಿಫಲವಾಗಿದೆ", - "invalid_auth": "ಅಮಾನ್ಯ ದದೃಢೀಕರಣ", - "single_instance_allowed": "ಈಗಾಗಲೇ ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾಗಿದೆ. ಒಂದೇ ಸಂರಚನೆ ಮಾತ್ರ ಸಾಧ್ಯ." - }, - "error": { - "invalid_auth": "ಅಮಾನ್ಯ ದದೃಢೀಕರಣ" - }, "flow_title": "Tuya ಸಂರಚನೆ", "step": { - "project_type":{ + "user":{ "title":"Tuya ಏಕೀಕರಣ", "data":{ "tuya_project_type": "Tuya ಕ್ಲೌಡ್ ಯೋಜನೆಯ ಪ್ರಕಾರ" } }, - "user": { + "login": { + "title": "Tuya", + "description": "ನಿಮ್ಮ Tuya ರುಜುವಾತು ನಮೂದಿಸಿ.", "data": { "endpoint": "ಲಭ್ಯತೆ ವಲಯ", "access_id": "ಆಕ್ಸೇಸ್ ಐಡಿ", @@ -25,10 +19,11 @@ "country_code": "ಕಂಟ್ರೀ ಕೋಡ್", "username": "ಅಕೌಂಟ್", "password": "ಪಾಸ್ವರ್ಡ್" - }, - "description": "ನಿಮ್ಮ Tuya ರುಜುವಾತು ನಮೂದಿಸಿ.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "ಅಮಾನ್ಯ ದದೃಢೀಕರಣ" } } } diff --git a/custom_components/tuya_v2/translations/nl.json b/custom_components/tuya_v2/translations/nl.json index 2e32bdb..60f8927 100644 --- a/custom_components/tuya_v2/translations/nl.json +++ b/custom_components/tuya_v2/translations/nl.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Verbindingsfout", - "invalid_auth": "Authenticatiefout", - "single_instance_allowed": "Reeds geconfigureerd. Slechts één configuratie is mogelijk." - }, - "error": { - "invalid_auth": "Authenticatiefout" - }, "flow_title": "Tuya Configuratie", "step": { - "project_type":{ + "user":{ "title":"Tuya Integratie", "data":{ "tuya_project_type": "Tuya cloud projecttype" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Voer je Tuya aanmeldgegevens in.", "data": { "endpoint": "Availability Zone", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "Landscode", "username": "Gebruikersnaam", "password": "Wachtwoord" - }, - "description": "Voer je Tuya aanmeldgegevens in.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Authenticatiefout" } } } diff --git a/custom_components/tuya_v2/translations/pt.json b/custom_components/tuya_v2/translations/pt.json index 9213d5c..d800c35 100644 --- a/custom_components/tuya_v2/translations/pt.json +++ b/custom_components/tuya_v2/translations/pt.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "Falha na conexão", - "invalid_auth": "Autenticação inválida", - "single_instance_allowed": "Já configurado. Só é possível uma configuração desta integração." - }, - "error": { - "invalid_auth": "Autenticação inválida" - }, "flow_title": "Configuração Tuya", "step": { - "project_type":{ + "user":{ "title":"Integração Tuya", "data":{ "tuya_project_type": "Tipo de projeto Tuya Cloud" } }, - "user": { + "login": { + "title": "Tuya", + "description": "Insira as credenciais Tuya.", "data": { "endpoint": "Região", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "Código do País", "username": "Conta", "password": "Senha" - }, - "description": "Insira as credenciais Tuya.", - "title": "Tuya" + } } + }, + "error": { + "invalid_auth": "Autenticação inválida" } } } diff --git a/custom_components/tuya_v2/translations/zh-Hans.json b/custom_components/tuya_v2/translations/zh-Hans.json index e682bbc..706a785 100644 --- a/custom_components/tuya_v2/translations/zh-Hans.json +++ b/custom_components/tuya_v2/translations/zh-Hans.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "连接失败", - "invalid_auth": "身份认证无效", - "single_instance_allowed": "已经配置过了,且只能配置一次。" - }, - "error": { - "invalid_auth": "身份认证无效" - }, "flow_title": "涂鸦配置", "step": { - "project_type":{ + "user":{ "title":"Tuya插件", "data":{ "tuya_project_type": "涂鸦云项目类型" } }, - "user": { + "login": { + "title": "涂鸦", + "description": "请输入涂鸦账户信息。", "data": { "endpoint": "可用区域", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "国家码", "username": "账号", "password": "密码" - }, - "description": "请输入涂鸦账户信息。", - "title": "涂鸦" + } } + }, + "error": { + "invalid_auth": "身份认证无效" } } } diff --git a/custom_components/tuya_v2/translations/zh-Hant.json b/custom_components/tuya_v2/translations/zh-Hant.json index 6c7a9e8..d3c59aa 100644 --- a/custom_components/tuya_v2/translations/zh-Hant.json +++ b/custom_components/tuya_v2/translations/zh-Hant.json @@ -1,22 +1,16 @@ { "config": { - "abort": { - "cannot_connect": "連線失敗", - "invalid_auth": "帳號資訊有誤", - "single_instance_allowed": "已設定,且無法重複設定" - }, - "error": { - "invalid_auth": "帳號資訊有誤" - }, "flow_title": "塗鴉設定", "step": { - "project_type":{ + "user":{ "title":"Tuya 套件", "data":{ "tuya_project_type": "塗鴉雲端專案類型" } }, - "user": { + "login": { + "title": "塗鴉", + "description": "請輸入塗鴉帳號資訊。", "data": { "endpoint": "區域", "access_id": "Access ID", @@ -25,10 +19,11 @@ "country_code": "國家代碼", "username": "帳號", "password": "密碼" - }, - "description": "請輸入塗鴉帳號資訊。", - "title": "塗鴉" + } } + }, + "error": { + "invalid_auth": "帳號資訊有誤" } } } diff --git a/custom_components/tuya_v2/vacuum.py b/custom_components/tuya_v2/vacuum.py index 3b8e518..2928ee1 100644 --- a/custom_components/tuya_v2/vacuum.py +++ b/custom_components/tuya_v2/vacuum.py @@ -22,11 +22,12 @@ StateVacuumEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +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 .base import TuyaHaDevice +from .base import TuyaHaEntity from .const import ( DOMAIN, TUYA_DEVICE_MANAGER, @@ -60,48 +61,54 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): +) -> None: """Set up tuya vacuum dynamically through tuya discovery.""" - _LOGGER.info("vacuum init") + _LOGGER.debug("vacuum init") - hass.data[DOMAIN][TUYA_HA_TUYA_MAP].update({DEVICE_DOMAIN: TUYA_SUPPORT_TYPE}) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ + DEVICE_DOMAIN + ] = TUYA_SUPPORT_TYPE - async def async_discover_device(dev_ids): + @callback + def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.info(f"vacuum add -> {dev_ids}") + _LOGGER.debug(f"vacuum add -> {dev_ids}") if not dev_ids: return - entities = await hass.async_add_executor_job(_setup_entities, hass, dev_ids) - hass.data[DOMAIN][TUYA_HA_DEVICES].extend(entities) + entities = _setup_entities(hass, entry, dev_ids) async_add_entities(entities) - async_dispatcher_connect( - hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + entry.async_on_unload( + async_dispatcher_connect( + hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device + ) ) - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] + 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) + async_discover_device(device_ids) -def _setup_entities(hass: HomeAssistant, device_ids: list): +def _setup_entities( + hass: HomeAssistant, entry: ConfigEntry, device_ids: list[str] +) -> list[Entity]: """Set up Tuya Switch device.""" - device_manager = hass.data[DOMAIN][TUYA_DEVICE_MANAGER] - entities = [] + 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(TuyaHaVacuum(device, device_manager)) - + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaVacuum(TuyaHaDevice, StateVacuumEntity): +class TuyaHaVacuum(TuyaHaEntity, StateVacuumEntity): """Tuya Vacuum Device.""" @property @@ -169,29 +176,12 @@ def supported_features(self): supports = supports | SUPPORT_BATTERY return supports - # def turn_on(self, **kwargs: Any) -> None: - # """Turn the device on.""" - # _LOGGER.debug(f"Turning on {self.name}") - - # self.tuya_device_manager.sendCommands( - # self.tuya_device.id, [{"code": DPCODE_POWER, "value": True}] - # ) - def start(self, **kwargs: Any) -> None: """Turn the device on.""" _LOGGER.debug(f"Starting {self.name}") self._send_command([{"code": DPCODE_POWER_GO, "value": True}]) - # Turn off/pause/stop all do the same thing - - # def turn_off(self, **kwargs: Any) -> None: - # """Turn the device off.""" - # _LOGGER.debug(f"Turning off {self.name}") - # self.tuya_device_manager.sendCommands( - # self.tuya_device.id, [{"code": DPCODE_POWER, "value": False}] - # ) - def stop(self, **kwargs: Any) -> None: """Turn the device off.""" _LOGGER.debug(f"Stopping {self.name}") @@ -202,15 +192,6 @@ def pause(self, **kwargs: Any) -> None: _LOGGER.debug(f"Pausing {self.name}") self._send_command([{"code": DPCODE_PAUSE, "value": True}]) - # def start_pause(self, **kwargs: Any) -> None: - # """Start/Pause the device""" - # _LOGGER.debug(f"Start/Pausing {self.name}") - # status = False - # status = self.tuya_device.status.get(DPCODE_PAUSE) - # self.tuya_device_manager.sendCommands( - # self.tuya_device.id, [{"code": DPCODE_PAUSE, "value": not status}] - # ) - def return_to_base(self, **kwargs: Any) -> None: """Return device to Dock""" _LOGGER.debug(f"Return to base device {self.name}") From 5ecc62ac48c984c31edb6d1198ccea6dc20039de Mon Sep 17 00:00:00 2001 From: erchuan Date: Mon, 11 Oct 2021 19:34:24 +0800 Subject: [PATCH 2/5] code style --- custom_components/tuya_v2/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tuya_v2/fan.py b/custom_components/tuya_v2/fan.py index 15a8e55..aaf2dd3 100644 --- a/custom_components/tuya_v2/fan.py +++ b/custom_components/tuya_v2/fan.py @@ -59,7 +59,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -): +) -> None: """Set up tuya fan dynamically through tuya discovery.""" _LOGGER.debug("fan init") From bae62b7c625d100b06fe06b53a93f2d0b2d30e03 Mon Sep 17 00:00:00 2001 From: erchuan Date: Tue, 12 Oct 2021 16:28:43 +0800 Subject: [PATCH 3/5] code review --- custom_components/tuya_v2/__init__.py | 60 +++++- custom_components/tuya_v2/aes_cbc.py | 88 ++++++++ custom_components/tuya_v2/binary_sensor.py | 227 +++++++++++++++++---- custom_components/tuya_v2/switch.py | 43 +++- 4 files changed, 369 insertions(+), 49 deletions(-) create mode 100644 custom_components/tuya_v2/aes_cbc.py diff --git a/custom_components/tuya_v2/__init__.py b/custom_components/tuya_v2/__init__.py index e96e22b..3fa4273 100644 --- a/custom_components/tuya_v2/__init__.py +++ b/custom_components/tuya_v2/__init__.py @@ -2,6 +2,7 @@ import itertools import logging +import json from tuya_iot import ( ProjectType, @@ -13,6 +14,9 @@ TuyaOpenMQ, ) +from .aes_cbc import AES_ACCOUNT_KEY, KEY_KEY, XOR_KEY +from .aes_cbc import AesCBC as Aes + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry @@ -54,13 +58,51 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +def entry_decrypt(hass: HomeAssistant, entry: ConfigEntry, init_entry_data): + """Decript or encrypt entry info.""" + aes = Aes() + # decrypt the new account info + if XOR_KEY in init_entry_data: + _LOGGER.info("tuya.__init__.exist_xor_cache-->True") + key_iv = aes.xor_decrypt(init_entry_data[XOR_KEY], init_entry_data[KEY_KEY]) + cbc_key = key_iv[0:16] + cbc_iv = key_iv[16:32] + decrpyt_str = aes.cbc_decrypt(cbc_key, cbc_iv, init_entry_data[AES_ACCOUNT_KEY]) + # _LOGGER.info(f"tuya.__init__.exist_xor_cache:::decrpyt_str-->{decrpyt_str}") + entry_data = aes.json_to_dict(decrpyt_str) + else: + # if not exist xor cache, use old account info + _LOGGER.info("tuya.__init__.exist_xor_cache-->False") + entry_data = init_entry_data + cbc_key = aes.random_16() + cbc_iv = aes.random_16() + access_id = init_entry_data[CONF_ACCESS_ID] + access_id_entry = aes.cbc_encrypt(cbc_key, cbc_iv, access_id) + c = cbc_key + cbc_iv + c_xor_entry = aes.xor_encrypt(c, access_id_entry) + # account info encrypted with AES-CBC + user_input_encrpt = aes.cbc_encrypt( + cbc_key, cbc_iv, json.dumps(dict(init_entry_data)) + ) + # udpate old account info + hass.config_entries.async_update_entry( + entry, + data={ + AES_ACCOUNT_KEY: user_input_encrpt, + XOR_KEY: c_xor_entry, + KEY_KEY: access_id_entry, + }, + ) + return entry_data + async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: - project_type = ProjectType(entry.data[CONF_PROJECT_TYPE]) + entry_data = entry_decrypt(hass, entry, entry.data) + project_type = ProjectType(entry_data[CONF_PROJECT_TYPE]) api = TuyaOpenAPI( - entry.data[CONF_ENDPOINT], - entry.data[CONF_ACCESS_ID], - entry.data[CONF_ACCESS_SECRET], + entry_data[CONF_ENDPOINT], + entry_data[CONF_ACCESS_ID], + entry_data[CONF_ACCESS_SECRET], project_type, ) @@ -68,15 +110,15 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: if project_type == ProjectType.INDUSTY_SOLUTIONS: response = await hass.async_add_executor_job( - api.login, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + 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], + 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: diff --git a/custom_components/tuya_v2/aes_cbc.py b/custom_components/tuya_v2/aes_cbc.py new file mode 100644 index 0000000..1738ff8 --- /dev/null +++ b/custom_components/tuya_v2/aes_cbc.py @@ -0,0 +1,88 @@ +"""AES-CBC encryption and decryption for account info.""" + +import base64 as b64 +import json +import random +from binascii import a2b_hex, b2a_hex + +from Crypto.Cipher import AES + +AES_ACCOUNT_KEY = "o0o0o0" +XOR_KEY = "00oo00" +KEY_KEY = "oo00oo" + + +class AesCBC: + + # random_16 + def random_16(self): + str = "" + return str.join( + random.choice("abcdefghijklmnopqrstuvwxyz!@#$%^&*1234567890") + for i in range(16) + ) + + # add_to_16 + def add_to_16(self, text): + if len(text.encode("utf-8")) % 16: + add = 16 - (len(text.encode("utf-8")) % 16) + else: + add = 0 + text = text + ("\0" * add) + return text.encode("utf-8") + + # cbc_encryption + def cbc_encrypt(self, key, iv, text): + key = key.encode("utf-8") + mode = AES.MODE_CBC + iv = bytes(iv, encoding="utf8") + text = self.add_to_16(text) + cryptos = AES.new(key, mode, iv) + cipher_text = cryptos.encrypt(text) + return str(b2a_hex(cipher_text), encoding="utf-8") + + # cbc_decryption + def cbc_decrypt(self, key, iv, text): + key = key.encode("utf-8") + iv = bytes(iv, encoding="utf8") + mode = AES.MODE_CBC + cryptos = AES.new(key, mode, iv) + plain_text = cryptos.decrypt(a2b_hex(text)) + return bytes.decode(plain_text).rstrip("\0") + + # xor_encrypt + def xor_encrypt(self, data, key): + lkey = len(key) + secret = [] + num = 0 + for each in data: + if num >= lkey: + num = num % lkey + secret.append(chr(ord(each) ^ ord(key[num]))) + num += 1 + return b64.b64encode("".join(secret).encode()).decode() + + # xor_decrypt + def xor_decrypt(self, secret, key): + tips = b64.b64decode(secret.encode()).decode() + lkey = len(key) + secret = [] + num = 0 + for each in tips: + if num >= lkey: + num = num % lkey + secret.append(chr(ord(each) ^ ord(key[num]))) + num += 1 + return "".join(secret) + + # json to dict + def json_to_dict(self, json_str): + return json.loads(json_str) + + # confuse str + def b64_encrypt(self, text): + return b64.b64encode(text.encode()).decode() + + # unconfuse str + def b64_decrypt(self, text): + return b64.b64decode(text).decode() diff --git a/custom_components/tuya_v2/binary_sensor.py b/custom_components/tuya_v2/binary_sensor.py index 9fe7127..81cda59 100644 --- a/custom_components/tuya_v2/binary_sensor.py +++ b/custom_components/tuya_v2/binary_sensor.py @@ -1,15 +1,24 @@ """Support for Tuya Binary Sensor.""" +from __future__ import annotations +import json import logging +from threading import Timer from typing import Callable -from homeassistant.components.alarm_control_panel import DOMAIN as DEVICE_DOMAIN -from homeassistant.components.alarm_control_panel import ( - SUPPORT_ALARM_TRIGGER, - AlarmControlPanelEntity, +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SMOKE, ) +from homeassistant.components.binary_sensor import DOMAIN as DEVICE_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ALARM_ARMING, STATE_ALARM_TRIGGERED from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,37 +37,58 @@ _LOGGER = logging.getLogger(__name__) TUYA_SUPPORT_TYPE = [ + "mcs", # Door Window Sensor "ywbj", # Smoke Detector "rqbj", # Gas Detector "pir", # PIR Detector + "sj", # Water Detector + "sos", # Emergency Button + "hps", # Human Presence Sensor + "ms", # Residential Lock + "ckmkzq", # Garage Door Opener ] -# Smoke Detector -# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5i2iiy +# Door Window Sensor +# https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m +DPCODE_SWITCH = "switch" + + +DPCODE_BATTERY_STATE = "battery_state" + +DPCODE_DOORCONTACT_STATE = "doorcontact_state" DPCODE_SMOKE_SENSOR_STATE = "smoke_sensor_state" +DPCODE_SMOKE_SENSOR_STATUS = "smoke_sensor_status" DPCODE_GAS_SENSOR_STATE = "gas_sensor_state" DPCODE_PIR = "pir" +DPCODE_WATER_SENSOR_STATE = "watersensor_state" +DPCODE_SOS_STATE = "sos_state" +DPCODE_PRESENCE_STATE = "presence_state" +DPCODE_TEMPER_ALRAM = "temper_alarm" +DPCODE_DOORLOCK_STATE = "closed_opened" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up tuya alarm dynamically through tuya discovery.""" - _LOGGER.debug("alarm init") + """Set up tuya binary sensors dynamically through tuya discovery.""" + _LOGGER.debug("binary sensor init") hass.data[DOMAIN][entry.entry_id][TUYA_HA_TUYA_MAP][ - DEVICE_DOMAIN + DEVICE_DOMAIN ] = TUYA_SUPPORT_TYPE @callback def async_discover_device(dev_ids): """Discover and add a discovered tuya sensor.""" - _LOGGER.debug("alarm add->", dev_ids) + _LOGGER.debug(f"binary sensor add->{dev_ids}") if not dev_ids: return - entities = entities = _setup_entities(hass, entry, dev_ids) + entities = _setup_entities(hass, entry, dev_ids) + for entity in entities: + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entity._attr_unique_id) async_add_entities(entities) + entry.async_on_unload( async_dispatcher_connect( hass, TUYA_DISCOVERY_NEW.format(DEVICE_DOMAIN), async_discover_device @@ -84,63 +114,186 @@ def _setup_entities( if device is None: continue + if DPCODE_DOORLOCK_STATE in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_DOOR, + DPCODE_DOORLOCK_STATE, + (lambda d: d.status.get(DPCODE_DOORLOCK_STATE, "none") != "closed"), + ) + ) + if DPCODE_DOORCONTACT_STATE in device.status: + if device.category == "ckmkzq": + device_class_d = DEVICE_CLASS_GARAGE_DOOR + else: + device_class_d = DEVICE_CLASS_DOOR + entities.append( + TuyaHaBSensor( + device, + device_manager, + device_class_d, + DPCODE_DOORCONTACT_STATE, + (lambda d: d.status.get(DPCODE_DOORCONTACT_STATE, False)), + ) + ) + if DPCODE_SWITCH in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_DOOR, + DPCODE_SWITCH, + (lambda d: d.status.get(DPCODE_SWITCH, False)), + ) + ) if DPCODE_SMOKE_SENSOR_STATE in device.status: entities.append( - TuyaHaAlarm( + TuyaHaBSensor( device, device_manager, + DEVICE_CLASS_SMOKE, + DPCODE_SMOKE_SENSOR_STATE, + (lambda d: d.status.get(DPCODE_SMOKE_SENSOR_STATE, 1) == "1"), + ) + ) + if DPCODE_SMOKE_SENSOR_STATUS in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_SMOKE, + DPCODE_SMOKE_SENSOR_STATUS, ( - lambda d: STATE_ALARM_TRIGGERED - if d.status.get(DPCODE_SMOKE_SENSOR_STATE, 1) == "1" - else STATE_ALARM_ARMING + lambda d: d.status.get(DPCODE_SMOKE_SENSOR_STATUS, "normal") + == "alarm" ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) + if DPCODE_BATTERY_STATE in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_BATTERY, + DPCODE_BATTERY_STATE, + (lambda d: d.status.get(DPCODE_BATTERY_STATE, "normal") == "low"), + ) + ) + if DPCODE_TEMPER_ALRAM in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_MOTION, + DPCODE_TEMPER_ALRAM, + (lambda d: d.status.get(DPCODE_TEMPER_ALRAM, False)), + ) + ) if DPCODE_GAS_SENSOR_STATE in device.status: entities.append( - TuyaHaAlarm( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_GAS, + DPCODE_GAS_SENSOR_STATE, + (lambda d: d.status.get(DPCODE_GAS_SENSOR_STATE, 1) == "1"), + ) + ) + if DPCODE_PIR in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_MOTION, + DPCODE_PIR, + (lambda d: d.status.get(DPCODE_PIR, "none") == "pir"), + ) + ) + if DPCODE_WATER_SENSOR_STATE in device.status: + entities.append( + TuyaHaBSensor( device, device_manager, + DEVICE_CLASS_MOISTURE, + DPCODE_WATER_SENSOR_STATE, ( - lambda d: STATE_ALARM_TRIGGERED - if d.status.get(DPCODE_GAS_SENSOR_STATE, 1) == "1" - else STATE_ALARM_ARMING + lambda d: d.status.get(DPCODE_WATER_SENSOR_STATE, "normal") + == "alarm" ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) - if DPCODE_PIR in device.stastus: + if DPCODE_SOS_STATE in device.status: entities.append( - TuyaHaAlarm( + TuyaHaBSensor( device, device_manager, + DEVICE_CLASS_PROBLEM, + DPCODE_SOS_STATE, + (lambda d: d.status.get(DPCODE_SOS_STATE, False)), + ) + ) + if DPCODE_PRESENCE_STATE in device.status: + entities.append( + TuyaHaBSensor( + device, + device_manager, + DEVICE_CLASS_MOTION, + DPCODE_PRESENCE_STATE, ( - lambda d: STATE_ALARM_TRIGGERED - if d.status.get(DPCODE_GAS_SENSOR_STATE, "none") == "pir" - else STATE_ALARM_ARMING + lambda d: d.status.get(DPCODE_PRESENCE_STATE, "none") + == "presence" ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities -class TuyaHaAlarm(TuyaHaEntity, AlarmControlPanelEntity): - """Tuya Alarm Device.""" +class TuyaHaBSensor(TuyaHaEntity, BinarySensorEntity): + """Tuya Binary Sensor Device.""" - def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager, sensor_is_on: Callable[..., str]) -> None: - """Init TuyaHaAlarm.""" + def __init__( + self, + device: TuyaDevice, + device_manager: TuyaDeviceManager, + sensor_type: str, + sensor_code: str, + sensor_is_on: Callable[..., bool], + ) -> None: + """Init TuyaHaBSensor.""" super().__init__(device, device_manager) + self._type = sensor_type + self._code = sensor_code self._is_on = sensor_is_on + self._attr_unique_id = f"{super().unique_id}{self._code}" + self._attr_name = f"{self.tuya_device.name}_{self._code}" + self._attr_device_class = self._type + self._attr_available = True @property - def state(self): - """Return is alarm on.""" + def is_on(self): + """Return true if the binary sensor is on.""" return self._is_on(self.tuya_device) @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_TRIGGER + def unique_id(self) -> str | None: + """Return a unique ID.""" + return self._attr_unique_id + + def reset_pir(self): + self.tuya_device.status[DPCODE_PIR] = "none" + self.schedule_update_ha_state() + + def schedule_update_ha_state(self, force_refresh: bool = False) -> None: + + if self._code == DPCODE_PIR: + pir_range = json.loads( + self.tuya_device.status_range.get(DPCODE_PIR, {}).values + ).get("range") + if len(pir_range) == 1 and self.tuya_device.status[DPCODE_PIR] == "pir": + timer = Timer(10, lambda: self.reset_pir()) + timer.start() + + super().schedule_update_ha_state(force_refresh) diff --git a/custom_components/tuya_v2/switch.py b/custom_components/tuya_v2/switch.py index 0c9b18b..64f7775 100644 --- a/custom_components/tuya_v2/switch.py +++ b/custom_components/tuya_v2/switch.py @@ -31,9 +31,15 @@ "pc", # Power Strip "bh", # Smart Kettle "dlq", # Breaker + "tdq", # Breaker "cwysj", # Pet Water Feeder "kj", # Air Purifier "xxj", # Diffuser + "ckmkzq", # Garage Door Opener + "zndb", # Smart Electricity Meter + "fs", # Fan + "sd", # Vacuum + "zndb" "kfj", # Smart Electricity Meter # Coffee Maker } # Switch(kg), Socket(cz), Power Strip(pc) @@ -56,6 +62,18 @@ DPCODE_WRESET = "water_reset" # Pet Water Feeder - Resetting of water usage days DPCODE_START = "start" +# Coffee Maker +# https://developer.tuya.com/en/docs/iot/f?id=K9gf4701ox167 +DPCODE_PAUSE = "pause" +DPCODE_WARM = "warm" +DPCODE_CLEANING = "cleaning" +# Fan +# https://developer.tuya.com/en/docs/iot/f?id=K9gf45vs7vkge +DPCODE_FAN_LIGHT = "light" + +# Vacuum +#https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo +DPCODE_VOICE = "voice_switch" async def async_setup_entry( @@ -109,6 +127,7 @@ def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Ent DPCODE_LOCK, DPCODE_UV, DPCODE_WET, + DPCODE_FAN_LIGHT, ]: tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) # Main device switch is handled by the Fan object @@ -116,13 +135,31 @@ def _setup_entities(hass, entry: ConfigEntry, device_ids: list[str]) -> list[Ent 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 + elif function.startswith(DPCODE_SWITCH): + entities.append(TuyaHaSwitch(device, device_manager, function)) tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + + elif device.category == "kfj": + if function in [ + DPCODE_SWITCH, + DPCODE_START, + DPCODE_PAUSE, + DPCODE_WARM, + DPCODE_CLEANING, + ]: + entities.append(TuyaHaSwitch(device, device_manager, function)) + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + + elif device.category == "sd": + if function in [DPCODE_VOICE ]: + entities.append(TuyaHaSwitch(device, device_manager, function)) + tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) + + # Main device switch else: if function.startswith(DPCODE_START): tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) - if function.startswith(DPCODE_SWITCH): + elif function.startswith(DPCODE_SWITCH): tuya_ha_switch = TuyaHaSwitch(device, device_manager, function) if tuya_ha_switch is not None: From ac134a381e723b9779df63b1bf7b637763014028 Mon Sep 17 00:00:00 2001 From: erchuan Date: Tue, 12 Oct 2021 20:59:27 +0800 Subject: [PATCH 4/5] fix:some compatibility --- custom_components/tuya_v2/alarm_control_panel.py | 5 ++--- custom_components/tuya_v2/base.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/tuya_v2/alarm_control_panel.py b/custom_components/tuya_v2/alarm_control_panel.py index ce07fea..f83e0a1 100644 --- a/custom_components/tuya_v2/alarm_control_panel.py +++ b/custom_components/tuya_v2/alarm_control_panel.py @@ -58,6 +58,8 @@ def async_discover_device(dev_ids): if not dev_ids: return entities = entities = _setup_entities(hass, entry, dev_ids) + for entity in entities: + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entity.unique_id) async_add_entities(entities) entry.async_on_unload( async_dispatcher_connect( @@ -96,7 +98,6 @@ def _setup_entities( ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_GAS_SENSOR_STATE in device.status: entities.append( TuyaHaAlarm( @@ -109,7 +110,6 @@ def _setup_entities( ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_PIR in device.stastus: entities.append( TuyaHaAlarm( @@ -122,7 +122,6 @@ def _setup_entities( ), ) ) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities diff --git a/custom_components/tuya_v2/base.py b/custom_components/tuya_v2/base.py index 86a508a..9f2e237 100644 --- a/custom_components/tuya_v2/base.py +++ b/custom_components/tuya_v2/base.py @@ -37,7 +37,7 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str | None: """Return a unique ID.""" - return f"tuya.{self.tuya_device.id}" + return f"ty{self.tuya_device.id}" @property def name(self) -> str | None: @@ -49,7 +49,7 @@ def device_info(self): """Return a device description for device registry.""" _device_info = { "identifiers": {(DOMAIN, f"{self.tuya_device.id}")}, - "manufacturer": "Tuya", + "manufacturer": "tuya", "name": self.tuya_device.name, "model": self.tuya_device.product_name, } From cf5782537df3a7dd41d939f2fa7a6d513dc5e955 Mon Sep 17 00:00:00 2001 From: erchuan Date: Wed, 13 Oct 2021 14:28:28 +0800 Subject: [PATCH 5/5] code review --- custom_components/tuya_v2/__init__.py | 83 ++++++++++++++++++++++----- custom_components/tuya_v2/number.py | 8 +-- custom_components/tuya_v2/sensor.py | 2 +- 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/custom_components/tuya_v2/__init__.py b/custom_components/tuya_v2/__init__.py index 3fa4273..4935722 100644 --- a/custom_components/tuya_v2/__init__.py +++ b/custom_components/tuya_v2/__init__.py @@ -1,9 +1,12 @@ """Support for Tuya Smart devices.""" import itertools +from typing import Any import logging import json +import voluptuous as vol + from tuya_iot import ( ProjectType, TuyaDevice, @@ -12,15 +15,18 @@ TuyaHomeManager, TuyaOpenAPI, TuyaOpenMQ, + tuya_logger ) from .aes_cbc import AES_ACCOUNT_KEY, KEY_KEY, XOR_KEY from .aes_cbc import AesCBC as Aes -from homeassistant.config_entries import ConfigEntry +import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.data_entry_flow import UnknownFlow, UnknownStep from .const import ( CONF_ACCESS_ID, @@ -44,19 +50,26 @@ _LOGGER = logging.getLogger(__name__) - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Async setup hass config entry.""" - - _LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data) - - hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} - - success = await _init_tuya_sdk(hass, entry) - if not success: - return False - - return True +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_PROJECT_TYPE): int, + vol.Required(CONF_ENDPOINT): cv.string, + vol.Required(CONF_ACCESS_ID): cv.string, + vol.Required(CONF_ACCESS_SECRET): cv.string, + CONF_USERNAME: cv.string, + CONF_PASSWORD: cv.string, + CONF_COUNTRY_CODE: cv.string, + CONF_APP_TYPE: cv.string, + } + ) + }, + ), + extra=vol.ALLOW_EXTRA, +) def entry_decrypt(hass: HomeAssistant, entry: ConfigEntry, init_entry_data): """Decript or encrypt entry info.""" @@ -95,10 +108,24 @@ def entry_decrypt(hass: HomeAssistant, entry: ConfigEntry, init_entry_data): ) return entry_data +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Async setup hass config entry.""" + + _LOGGER.debug("tuya.__init__.async_setup_entry-->%s", entry.data) + + hass.data[DOMAIN] = {entry.entry_id: {TUYA_HA_TUYA_MAP: {}, TUYA_HA_DEVICES: set()}} + + success = await _init_tuya_sdk(hass, entry) + if not success: + return False + + return True + async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data = entry_decrypt(hass, entry, entry.data) project_type = ProjectType(entry_data[CONF_PROJECT_TYPE]) + api = TuyaOpenAPI( entry_data[CONF_ENDPOINT], entry_data[CONF_ACCESS_ID], @@ -119,6 +146,7 @@ async def _init_tuya_sdk(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data[CONF_PASSWORD], entry_data[CONF_COUNTRY_CODE], entry_data[CONF_APP_TYPE], + ) if response.get("success", False) is False: @@ -187,6 +215,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload +async def async_setup(hass: HomeAssistant, config): + """Set up the Tuya integration.""" + tuya_logger.setLevel(_LOGGER.level) + conf = config.get(DOMAIN) + + _LOGGER.debug(f"Tuya async setup conf {conf}") + if conf is not None: + + async def flow_init() -> Any: + try: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + except UnknownFlow as flow: + _LOGGER.error(flow.args) + except UnknownStep as step: + _LOGGER.error(step.args) + except ValueError as err: + _LOGGER.error(err.args) + _LOGGER.info("Tuya async setup flow_init") + return result + + hass.async_create_task(flow_init()) + + return True + + class DeviceListener(TuyaDeviceListener): """Device Update Listener.""" diff --git a/custom_components/tuya_v2/number.py b/custom_components/tuya_v2/number.py index e12b701..544ce9c 100644 --- a/custom_components/tuya_v2/number.py +++ b/custom_components/tuya_v2/number.py @@ -59,6 +59,8 @@ def async_discover_device(dev_ids): if not dev_ids: return entities = _setup_entities(hass, entry, dev_ids) + for entrty in entitier: + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entrty.unique_id) async_add_entities(entities) entry.async_on_unload( @@ -88,27 +90,21 @@ def _setup_entities( if DPCODE_SENSITIVITY in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_SENSITIVITY)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_TEMPSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_TEMPSET)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_WARMTIME in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_WARMTIME)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_WATERSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_WATERSET)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_POWDERSET in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_POWDERSET)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) if DPCODE_CLOUDRECIPE in device.status: entities.append(TuyaHaNumber(device, device_manager, DPCODE_CLOUDRECIPE)) - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(device_id) return entities diff --git a/custom_components/tuya_v2/sensor.py b/custom_components/tuya_v2/sensor.py index 37c2321..37ef95e 100644 --- a/custom_components/tuya_v2/sensor.py +++ b/custom_components/tuya_v2/sensor.py @@ -134,7 +134,7 @@ def async_discover_device(dev_ids: list[str]): return entities = _setup_entities(hass, entry, dev_ids) for entity in entities: - hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entity._attr_unique_id) + hass.data[DOMAIN][entry.entry_id][TUYA_HA_DEVICES].add(entity.unique_id) async_add_entities(entities)