From 6be13c5ba07d9b7778ae68b8939580b3658600be Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 23 May 2022 12:31:15 +0200 Subject: [PATCH] Obfuscated diagnostics; fixed IP auto-update; cleaned logs --- custom_components/localtuya/__init__.py | 82 ++++++++-------------- custom_components/localtuya/climate.py | 32 ++++----- custom_components/localtuya/cloud_api.py | 3 +- custom_components/localtuya/common.py | 24 +++---- custom_components/localtuya/config_flow.py | 31 ++++---- custom_components/localtuya/diagnostics.py | 36 +++++++--- custom_components/localtuya/vacuum.py | 29 ++++---- 7 files changed, 112 insertions(+), 125 deletions(-) diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index e547a37..9701b4a 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -5,27 +5,26 @@ import time from datetime import timedelta import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntry import homeassistant.helpers.entity_registry as er import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_REGION, CONF_DEVICE_ID, CONF_DEVICES, CONF_ENTITIES, CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_REGION, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.event import async_track_time_interval from .cloud_api import TuyaCloudApi @@ -66,7 +65,6 @@ SERVICE_SET_DP_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: dict): """Set up the LocalTuya integration component.""" hass.data.setdefault(DOMAIN, {}) - print("SETUP") device_cache = {} @@ -134,10 +132,11 @@ async def async_setup(hass: HomeAssistant, config: dict): # settings triggers a reload of the config entry, which tears down the device # so no need to connect in that case. if updated: - new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) - hass.config_entries.async_update_entry( - entry, data=new_data + _LOGGER.debug( + "Updating keys for device %s: %s %s", device_id, device_ip, product_key ) + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + hass.config_entries.async_update_entry(entry, data=new_data) device = hass.data[DOMAIN][TUYA_DEVICES][device_id] if not device.connected: device.async_connect() @@ -148,26 +147,17 @@ async def async_setup(hass: HomeAssistant, config: dict): if not device.connected: device.async_connect() - discovery = TuyaDiscovery(_device_discovered) - def _shutdown(event): """Clean up resources when shutting down.""" discovery.close() - try: - await discovery.start() - hass.data[DOMAIN][DATA_DISCOVERY] = discovery - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("failed to set up discovery") - async def _async_reconnect(now): """Try connecting to devices not already connected to.""" for device_id, device in hass.data[DOMAIN][TUYA_DEVICES].items(): if not device.connected: device.async_connect() - # async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL) + async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL) hass.helpers.service.async_register_admin_service( DOMAIN, @@ -179,6 +169,14 @@ async def async_setup(hass: HomeAssistant, config: dict): DOMAIN, SERVICE_SET_DP, _handle_set_dp, schema=SERVICE_SET_DP_SCHEMA ) + discovery = TuyaDiscovery(_device_discovered) + try: + await discovery.start() + hass.data[DOMAIN][DATA_DISCOVERY] = discovery + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("failed to set up discovery") + return True @@ -228,7 +226,6 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - print("SETUP ENTRY STARTED!") """Set up LocalTuya integration from a config entry.""" unsub_listener = entry.add_update_listener(update_listener) hass.data[DOMAIN][UNSUB_LISTENER] = unsub_listener @@ -244,19 +241,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Cloud API connection failed: %s", res) _LOGGER.info("Cloud API connection succeeded.") res = await tuya_api.async_get_devices_list() - for dev_id, dev in tuya_api._device_list.items(): - _LOGGER.debug( - "Cloud device: %s \t dev_id %s \t key %s", dev["name"], dev["id"], dev["local_key"] - ) hass.data[DOMAIN][DATA_CLOUD] = tuya_api - # device = TuyaDevice(hass, entry.data) - # - # hass.data[DOMAIN][entry.entry_id] = { - # UNSUB_LISTENER: unsub_listener, - # TUYA_DEVICE: device, - # } - # async def setup_entities(dev_id): dev_entry = entry.data[CONF_DEVICES][dev_id] device = TuyaDevice(hass, entry, dev_id) @@ -272,19 +258,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device.async_connect() await async_remove_orphan_entities(hass, entry) - print("SETUP_ENTITIES for {} ENDED".format(dev_id)) for dev_id in entry.data[CONF_DEVICES]: hass.async_create_task(setup_entities(dev_id)) - print("SETUP ENTRY ENDED!") - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - # print("ASYNC_UNLOAD_ENTRY INVOKED...") platforms = {} for dev_id, dev_entry in entry.data[CONF_DEVICES].items(): for entity in dev_entry[CONF_ENTITIES]: @@ -301,20 +283,18 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data[DOMAIN][UNSUB_LISTENER]() for dev_id, device in hass.data[DOMAIN][TUYA_DEVICES].items(): - await device.close() + if device.connected: + await device.close() if unload_ok: hass.data[DOMAIN][TUYA_DEVICES] = {} - # print("ASYNC_UNLOAD_ENTRY ENDED") return True async def update_listener(hass, config_entry): """Update listener.""" - print("UPDATE_LISTENER INVOKED") await hass.config_entries.async_reload(config_entry.entry_id) - print("UPDATE_LISTENER RELOADED {}".format(config_entry.entry_id)) async def async_remove_config_entry_device( @@ -323,9 +303,18 @@ async def async_remove_config_entry_device( """Remove a config entry from a device.""" dev_id = list(device_entry.identifiers)[0][1].split("_")[-1] + ent_reg = await er.async_get_registry(hass) + entities = { + ent.unique_id: ent.entity_id + for ent in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) + if dev_id in ent.unique_id + } + for entity_id in entities.values(): + ent_reg.async_remove(entity_id) + if dev_id not in config_entry.data[CONF_DEVICES]: - _LOGGER.debug( - "Device ID %s not found in config entry: finalizing device removal", dev_id + _LOGGER.info( + "Device %s not found in config entry: finalizing device removal", dev_id ) return True @@ -340,15 +329,7 @@ async def async_remove_config_entry_device( data=new_data, ) - ent_reg = await er.async_get_registry(hass) - entities = { - ent.unique_id: ent.entity_id - for ent in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id) - if dev_id in ent.unique_id - } - for entity_id in entities.values(): - ent_reg.async_remove(entity_id) - print("REMOVED {}".format(entity_id)) + _LOGGER.info("Device %s removed.", dev_id) return True @@ -361,11 +342,8 @@ async def async_remove_orphan_entities(hass, entry): ent.unique_id: ent.entity_id for ent in er.async_entries_for_config_entry(ent_reg, entry.entry_id) } - print("ENTITIES ORPHAN {}".format(entities)) + _LOGGER.info("ENTITIES ORPHAN %s", entities) return - res = ent_reg.async_remove("switch.aa") - print("RESULT ORPHAN {}".format(res)) - # del entities[101] for entity in entry.data[CONF_ENTITIES]: if entity[CONF_ID] in entities: diff --git a/custom_components/localtuya/climate.py b/custom_components/localtuya/climate.py index 9d9358f..8474a21 100644 --- a/custom_components/localtuya/climate.py +++ b/custom_components/localtuya/climate.py @@ -11,18 +11,18 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - PRESET_NONE, - PRESET_ECO, - PRESET_AWAY, - PRESET_HOME, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -37,21 +37,21 @@ from homeassistant.const import ( from .common import LocalTuyaEntity, async_setup_entry from .const import ( CONF_CURRENT_TEMPERATURE_DP, - CONF_MAX_TEMP_DP, - CONF_MIN_TEMP_DP, - CONF_PRECISION, - CONF_TARGET_PRECISION, - CONF_TARGET_TEMPERATURE_DP, - CONF_TEMPERATURE_STEP, - CONF_HVAC_MODE_DP, - CONF_HVAC_MODE_SET, + CONF_ECO_DP, + CONF_ECO_VALUE, CONF_HEURISTIC_ACTION, CONF_HVAC_ACTION_DP, CONF_HVAC_ACTION_SET, - CONF_ECO_DP, - CONF_ECO_VALUE, + CONF_HVAC_MODE_DP, + CONF_HVAC_MODE_SET, + CONF_MAX_TEMP_DP, + CONF_MIN_TEMP_DP, + CONF_PRECISION, CONF_PRESET_DP, CONF_PRESET_SET, + CONF_TARGET_PRECISION, + CONF_TARGET_TEMPERATURE_DP, + CONF_TEMPERATURE_STEP, ) _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/localtuya/cloud_api.py b/custom_components/localtuya/cloud_api.py index ac82c4e..b55c46f 100755 --- a/custom_components/localtuya/cloud_api.py +++ b/custom_components/localtuya/cloud_api.py @@ -3,9 +3,10 @@ import functools import hashlib import hmac import json -import requests import time +import requests + # Signature algorithm. def calc_sign(msg, key): diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index c7b8c0d..d3e691b 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -1,8 +1,8 @@ """Code shared between all platforms.""" import asyncio import logging -from datetime import timedelta import time +from datetime import timedelta from homeassistant.const import ( CONF_DEVICE_ID, @@ -16,11 +16,11 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) from homeassistant.core import callback -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from . import pytuya @@ -28,8 +28,8 @@ from .const import ( ATTR_UPDATED_AT, CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, - DOMAIN, DATA_CLOUD, + DOMAIN, TUYA_DEVICES, ) @@ -74,6 +74,10 @@ async def async_setup_entry( if len(entities_to_setup) > 0: + if dev_id not in hass.data[DOMAIN][TUYA_DEVICES]: + print("STRANO: {}".format(hass.data[DOMAIN][TUYA_DEVICES])) + return + tuyainterface = hass.data[DOMAIN][TUYA_DEVICES][dev_id] dps_config_fields = list(get_dps_for_platform(flow_schema)) @@ -127,7 +131,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): super().__init__() self._hass = hass self._config_entry = config_entry - self._dev_config_entry = config_entry.data[CONF_DEVICES][dev_id] + self._dev_config_entry = config_entry.data[CONF_DEVICES][dev_id].copy() self._interface = None self._status = {} self.dps_to_request = {} @@ -205,9 +209,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): except Exception as e: # pylint: disable=broad-except self.exception(f"Connect to {self._dev_config_entry[CONF_HOST]} failed") - self.error("BBBB: %s", type(type(e))) - if 'json.decode' in str(type(e)): - self.error("BBBB2") + if "json.decode" in str(type(e)): await self.update_local_key() if self._interface is not None: @@ -220,7 +222,6 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): await self._hass.data[DOMAIN][DATA_CLOUD].async_get_devices_list() cloud_devs = self._hass.data[DOMAIN][DATA_CLOUD]._device_list if dev_id in cloud_devs: - old_key = self._local_key self._local_key = cloud_devs[dev_id].get(CONF_LOCAL_KEY) new_data = self._config_entry.data.copy() new_data[CONF_DEVICES][dev_id][CONF_LOCAL_KEY] = self._local_key @@ -229,10 +230,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self._config_entry, data=new_data, ) - self.debug( - "New local key for %s: from %s to %s", - dev_id, old_key, self._local_key - ) + self.info("local_key updated for device %s.", dev_id) async def _async_refresh(self, _now): if self._interface is not None: @@ -250,7 +248,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): self._disconnect_task() self.debug( "Closed connection with device %s.", - self._dev_config_entry[CONF_FRIENDLY_NAME] + self._dev_config_entry[CONF_FRIENDLY_NAME], ) async def set_dp(self, state, dp_index): diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index e901289..3de2284 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -9,6 +9,8 @@ import homeassistant.helpers.entity_registry as er import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, CONF_DEVICE_ID, CONF_DEVICES, CONF_ENTITIES, @@ -18,29 +20,27 @@ from homeassistant.const import ( CONF_MODEL, CONF_NAME, CONF_PLATFORM, - CONF_SCAN_INTERVAL, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_REGION, + CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.core import callback from .cloud_api import TuyaCloudApi -from .common import async_config_entry_by_device_id, pytuya +from .common import pytuya from .const import ( + ATTR_UPDATED_AT, CONF_ACTION, CONF_ADD_DEVICE, + CONF_DPS_STRINGS, CONF_EDIT_DEVICE, - CONF_SETUP_CLOUD, CONF_LOCAL_KEY, CONF_PRODUCT_NAME, CONF_PROTOCOL_VERSION, + CONF_SETUP_CLOUD, CONF_USER_ID, - CONF_DPS_STRINGS, - ATTR_UPDATED_AT, - DATA_DISCOVERY, DATA_CLOUD, + DATA_DISCOVERY, DOMAIN, PLATFORMS, ) @@ -304,11 +304,6 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"msg": res["msg"]} defaults = {} - defaults[CONF_REGION] = "eu" - defaults[CONF_CLIENT_ID] = "xxx" - defaults[CONF_CLIENT_SECRET] = "xxx" - defaults[CONF_USER_ID] = "xxx" - defaults[CONF_USERNAME] = "xxx" defaults.update(user_input or {}) return self.async_show_form( @@ -383,16 +378,17 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): cloud_devs = cloud_api._device_list for dev_id, dev in new_data[CONF_DEVICES].items(): if CONF_MODEL not in dev and dev_id in cloud_devs: - new_data[CONF_DEVICES][dev_id][CONF_MODEL] = cloud_devs[dev_id].get( - CONF_PRODUCT_NAME - ) + model = cloud_devs[dev_id].get(CONF_PRODUCT_NAME) + new_data[CONF_DEVICES][dev_id][CONF_MODEL] = model new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) self.hass.config_entries.async_update_entry( self.config_entry, data=new_data, ) - return self.async_create_entry(title=new_data.get(CONF_USERNAME), data={}) + return self.async_create_entry( + title=new_data.get(CONF_USERNAME), data={} + ) errors["base"] = res["reason"] placeholders = {"msg": res["msg"]} @@ -559,7 +555,6 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): CONF_ENTITIES: self.entities, } - # entry = async_config_entry_by_device_id(self.hass, self.unique_id) dev_id = self.device_data.get(CONF_DEVICE_ID) if dev_id in self.config_entry.data[CONF_DEVICES]: self.hass.config_entries.async_update_entry( diff --git a/custom_components/localtuya/diagnostics.py b/custom_components/localtuya/diagnostics.py index 72c3979..9ea0f6c 100644 --- a/custom_components/localtuya/diagnostics.py +++ b/custom_components/localtuya/diagnostics.py @@ -1,13 +1,18 @@ """Diagnostics support for LocalTuya.""" from __future__ import annotations + from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICES +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DEVICES from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry -from .const import DOMAIN, DATA_CLOUD +from .const import CONF_LOCAL_KEY, CONF_USER_ID, DATA_CLOUD, DOMAIN + +CLOUD_DEVICES = "cloud_devices" +DEVICE_CONFIG = "device_config" +DEVICE_CLOUD_INFO = "device_cloud_info" async def async_get_config_entry_diagnostics( @@ -15,14 +20,20 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" data = {} - data = {**entry.data} - # print("DATA is {}".format(data)) + data = entry.data.copy() tuya_api = hass.data[DOMAIN][DATA_CLOUD] - data["cloud_devices"] = tuya_api._device_list - # censoring private information - # data["token"] = re.sub(r"[^\-]", "*", data["token"]) - # data["userId"] = re.sub(r"[^\-]", "*", data["userId"]) + for field in [CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_USER_ID]: + data[field] = f"{data[field][0:3]}...{data[field][-3:]}" + for dev_id, dev in data[CONF_DEVICES].items(): + local_key = data[CONF_DEVICES][dev_id][CONF_LOCAL_KEY] + local_key_obfuscated = f"{local_key[0:3]}...{local_key[-3:]}" + data[CONF_DEVICES][dev_id][CONF_LOCAL_KEY] = local_key_obfuscated + data[CLOUD_DEVICES] = tuya_api._device_list + for dev_id, dev in data[CLOUD_DEVICES].items(): + local_key = data[CLOUD_DEVICES][dev_id][CONF_LOCAL_KEY] + local_key_obfuscated = f"{local_key[0:3]}...{local_key[-3:]}" + data[CLOUD_DEVICES][dev_id][CONF_LOCAL_KEY] = local_key_obfuscated return data @@ -32,9 +43,14 @@ async def async_get_device_diagnostics( """Return diagnostics for a device entry.""" data = {} dev_id = list(device.identifiers)[0][1].split("_")[-1] - data["device_config"] = entry.data[CONF_DEVICES][dev_id] + data[DEVICE_CONFIG] = entry.data[CONF_DEVICES][dev_id] + local_key = data[DEVICE_CONFIG][CONF_LOCAL_KEY] + data[DEVICE_CONFIG][CONF_LOCAL_KEY] = f"{local_key[0:3]}...{local_key[-3:]}" + tuya_api = hass.data[DOMAIN][DATA_CLOUD] if dev_id in tuya_api._device_list: - data["device_cloud_info"] = tuya_api._device_list[dev_id] + data[DEVICE_CLOUD_INFO] = tuya_api._device_list[dev_id] + local_key = data[DEVICE_CLOUD_INFO][CONF_LOCAL_KEY] + data[DEVICE_CLOUD_INFO][CONF_LOCAL_KEY] = f"{local_key[0:3]}...{local_key[-3:]}" # data["log"] = hass.data[DOMAIN][CONF_DEVICES][dev_id].logger.retrieve_log() return data diff --git a/custom_components/localtuya/vacuum.py b/custom_components/localtuya/vacuum.py index 9a14399..dc75044 100644 --- a/custom_components/localtuya/vacuum.py +++ b/custom_components/localtuya/vacuum.py @@ -7,41 +7,40 @@ from homeassistant.components.vacuum import ( DOMAIN, STATE_CLEANING, STATE_DOCKED, - STATE_IDLE, - STATE_RETURNING, - STATE_PAUSED, STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_START, SUPPORT_STATE, SUPPORT_STATUS, SUPPORT_STOP, - SUPPORT_LOCATE, StateVacuumEntity, ) from .common import LocalTuyaEntity, async_setup_entry - from .const import ( - CONF_POWERGO_DP, - CONF_IDLE_STATUS_VALUE, - CONF_RETURNING_STATUS_VALUE, - CONF_DOCKED_STATUS_VALUE, CONF_BATTERY_DP, - CONF_MODE_DP, - CONF_MODES, - CONF_FAN_SPEED_DP, - CONF_FAN_SPEEDS, - CONF_CLEAN_TIME_DP, CONF_CLEAN_AREA_DP, CONF_CLEAN_RECORD_DP, - CONF_LOCATE_DP, + CONF_CLEAN_TIME_DP, + CONF_DOCKED_STATUS_VALUE, + CONF_FAN_SPEED_DP, + CONF_FAN_SPEEDS, CONF_FAULT_DP, + CONF_IDLE_STATUS_VALUE, + CONF_LOCATE_DP, + CONF_MODE_DP, + CONF_MODES, CONF_PAUSED_STATE, + CONF_POWERGO_DP, CONF_RETURN_MODE, + CONF_RETURNING_STATUS_VALUE, CONF_STOP_STATUS, )