diff --git a/custom_components/localtuya/__init__.py b/custom_components/localtuya/__init__.py index e08980a..a32ab83 100644 --- a/custom_components/localtuya/__init__.py +++ b/custom_components/localtuya/__init__.py @@ -11,12 +11,16 @@ 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_USERNAME, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) @@ -31,10 +35,11 @@ from .config_flow import config_schema from .const import ( ATTR_UPDATED_AT, CONF_PRODUCT_KEY, + CONF_USER_ID, DATA_CLOUD, DATA_DISCOVERY, DOMAIN, - TUYA_DEVICE + TUYA_DEVICES, ) from .discovery import TuyaDiscovery @@ -62,6 +67,7 @@ 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 = {} @@ -84,14 +90,11 @@ async def async_setup(hass: HomeAssistant, config: dict): async def _handle_set_dp(event): """Handle set_dp service call.""" - entry = async_config_entry_by_device_id(hass, event.data[CONF_DEVICE_ID]) - if not entry: + dev_id = event.data[CONF_DEVICE_ID] + if dev_id not in hass.data[DOMAIN][TUYA_DEVICES]: raise HomeAssistantError("unknown device id") - if entry.entry_id not in hass.data[DOMAIN]: - raise HomeAssistantError("device has not been discovered") - - device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE] + device = hass.data[DOMAIN][TUYA_DEVICES][dev_id] if not device.connected: raise HomeAssistantError("not connected to device") @@ -138,21 +141,9 @@ async def async_setup(hass: HomeAssistant, config: dict): elif entry.entry_id in hass.data[DOMAIN]: _LOGGER.debug("Device %s found with IP %s", device_id, device_ip) - device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE] + device = hass.data[DOMAIN][TUYA_DEVICES][device_id] device.async_connect() - client_id = 'xx' - secret = 'xx' - uid = 'xx' - tuya_api = TuyaCloudApi(hass, "eu", client_id, secret, uid) - res = await tuya_api.async_get_access_token() - _LOGGER.debug("ACCESS TOKEN RES: %s", res) - res = await tuya_api.async_get_devices_list() - for dev_id, dev in tuya_api._device_list.items(): - print(f"Name: {dev['name']} \t dev_id {dev['id']} \t key {dev['local_key']} ") - hass.data[DOMAIN][DATA_CLOUD] = tuya_api - - _LOGGER.debug("\n\nSTARTING DISCOVERY") discovery = TuyaDiscovery(_device_discovered) def _shutdown(event): @@ -168,13 +159,16 @@ async def async_setup(hass: HomeAssistant, config: dict): async def _async_reconnect(now): """Try connecting to devices not already connected to.""" - for entry_id, value in hass.data[DOMAIN].items(): - if entry_id == DATA_DISCOVERY: - continue - - device = value[TUYA_DEVICE] + for device in hass.data[DOMAIN][TUYA_DEVICES]: if not device.connected: device.async_connect() + # for entry_id, value in hass.data[DOMAIN].items(): + # if entry_id == DATA_DISCOVERY: + # continue + # + # device = value[TUYA_DEVICE] + # if not device.connected: + # device.async_connect() async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL) @@ -191,16 +185,69 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -async def async_migrate_entry(domain): - """Migrate old entry.""" - print("WHYYYYYYY") - _LOGGER.error("WHHHYYYYYY") +async def async_migrate_entry(hass, config_entry: ConfigEntry): + """Migrate old entries merging all of them in one.""" + new_version = 2 + if config_entry.version == 1: + _LOGGER.debug("Migrating config entry from version %s", config_entry.version) + stored_entries = hass.config_entries.async_entries(DOMAIN) + + if config_entry.entry_id == stored_entries[0].entry_id: + _LOGGER.debug( + "Migrating the first config entry (%s)", config_entry.entry_id + ) + new_data = {} + new_data[CONF_REGION] = "eu" + new_data[CONF_CLIENT_ID] = "xxx" + new_data[CONF_CLIENT_SECRET] = "xxx" + new_data[CONF_USER_ID] = "xxx" + new_data[CONF_USERNAME] = DOMAIN + new_data[CONF_DEVICES] = { + config_entry.data[CONF_DEVICE_ID]: config_entry.data.copy() + } + config_entry.version = new_version + hass.config_entries.async_update_entry( + config_entry, title=DOMAIN, data=new_data + ) + else: + _LOGGER.debug( + "Merging the config entry %s into the main one", config_entry.entry_id + ) + new_data = stored_entries[0].data.copy() + new_data[CONF_DEVICES].update( + {config_entry.data[CONF_DEVICE_ID]: config_entry.data.copy()} + ) + hass.config_entries.async_update_entry(stored_entries[0], data=new_data) + await hass.config_entries.async_remove(config_entry.entry_id) + + _LOGGER.info( + "Entry %s successfully migrated to version %s.", + config_entry.entry_id, + new_version, + ) return True 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 + hass.data[DOMAIN][TUYA_DEVICES] = {} + + region = entry.data[CONF_REGION] + client_id = entry.data[CONF_CLIENT_ID] + secret = entry.data[CONF_CLIENT_SECRET] + user_id = entry.data[CONF_USER_ID] + tuya_api = TuyaCloudApi(hass, region, client_id, secret, user_id) + res = await tuya_api.async_get_access_token() + _LOGGER.debug("ACCESS TOKEN RES: %s . CLOUD DEVICES:", res) + res = await tuya_api.async_get_devices_list() + for dev_id, dev in tuya_api._device_list.items(): + _LOGGER.debug( + "Name: %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) # @@ -212,13 +259,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def setup_entities(dev_id): dev_entry = entry.data[CONF_DEVICES][dev_id] device = TuyaDevice(hass, dev_entry) - hass.data[DOMAIN][dev_id] = { - UNSUB_LISTENER: unsub_listener, - TUYA_DEVICE: device, - } + hass.data[DOMAIN][TUYA_DEVICES][dev_id] = device platforms = set(entity[CONF_PLATFORM] for entity in dev_entry[CONF_ENTITIES]) - print("DEV {} platforms: {}".format(dev_id, platforms)) await asyncio.gather( *[ hass.config_entries.async_forward_entry_setup(entry, platform) @@ -228,56 +271,64 @@ 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]: + platforms[entity[CONF_PLATFORM]] = True + unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, component) - for component in set( - entity[CONF_PLATFORM] for entity in entry.data[CONF_ENTITIES] - ) + for component in platforms ] ) ) - hass.data[DOMAIN][entry.entry_id][UNSUB_LISTENER]() - await hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE].close() - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) + hass.data[DOMAIN][UNSUB_LISTENER]() + for dev_id, device in hass.data[DOMAIN][TUYA_DEVICES].items(): + 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( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry ) -> bool: """Remove a config entry from a device.""" - print("REMOVING {} FROM {}".format(device_entry.identifiers, config_entry.data)) dev_id = list(device_entry.identifiers)[0][1].split("_")[-1] - _LOGGER.debug("Removing %s", dev_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 + "Device ID %s not found in config entry: finalizing device removal", dev_id ) return True - _LOGGER.debug("Closing device connection for %s", dev_id) - await hass.data[DOMAIN][dev_id][TUYA_DEVICE].close() + await hass.data[DOMAIN][TUYA_DEVICES][dev_id].close() new_data = config_entry.data.copy() new_data[CONF_DEVICES].pop(dev_id) @@ -287,7 +338,6 @@ async def async_remove_config_entry_device( config_entry, data=new_data, ) - _LOGGER.debug("Config entry updated") ent_reg = await er.async_get_registry(hass) entities = { @@ -298,13 +348,13 @@ async def async_remove_config_entry_device( for entity_id in entities.values(): ent_reg.async_remove(entity_id) print("REMOVED {}".format(entity_id)) - _LOGGER.debug("Removed %s entities: finalizing device removal", len(entities)) return True async def async_remove_orphan_entities(hass, entry): """Remove entities associated with config entry that has been removed.""" + return ent_reg = await er.async_get_registry(hass) entities = { ent.unique_id: ent.entity_id @@ -312,7 +362,7 @@ async def async_remove_orphan_entities(hass, entry): } print("ENTITIES ORPHAN {}".format(entities)) return - res = ent_reg.async_remove('switch.aa') + res = ent_reg.async_remove("switch.aa") print("RESULT ORPHAN {}".format(res)) # del entities[101] diff --git a/custom_components/localtuya/cloud_api.py b/custom_components/localtuya/cloud_api.py index e4b4140..b6343ea 100755 --- a/custom_components/localtuya/cloud_api.py +++ b/custom_components/localtuya/cloud_api.py @@ -127,7 +127,7 @@ class TuyaCloudApi: ) return f"Error {r_json['code']}: {r_json['msg']}" - self._device_list = {dev['id']: dev for dev in r_json["result"]} + self._device_list = {dev["id"]: dev for dev in r_json["result"]} # print("DEV__LIST: {}".format(self._device_list)) return "ok" diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 1c7f61d..0ebb5e0 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -28,7 +28,7 @@ from .const import ( CONF_PROTOCOL_VERSION, DOMAIN, DATA_CLOUD, - TUYA_DEVICE, + TUYA_DEVICES, ) _LOGGER = logging.getLogger(__name__) @@ -57,11 +57,6 @@ async def async_setup_entry( This is a generic method and each platform should lock domain and entity_class with functools.partial. """ - print("ASYNC_SETUP_ENTRY: {} {} {}".format( - config_entry.data, - entity_class, - flow_schema(None).items()) - ) entities = [] for dev_id in config_entry.data[CONF_DEVICES]: @@ -77,7 +72,7 @@ async def async_setup_entry( if len(entities_to_setup) > 0: - tuyainterface = hass.data[DOMAIN][dev_id][TUYA_DEVICE] + tuyainterface = hass.data[DOMAIN][TUYA_DEVICES][dev_id] dps_config_fields = list(get_dps_for_platform(flow_schema)) @@ -94,8 +89,6 @@ async def async_setup_entry( entity_config[CONF_ID], ) ) - print("ADDING {} entities".format(len(entities))) - async_add_entities(entities) @@ -209,11 +202,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): "New local key for %s: from %s to %s", dev_id, old_key, - self._local_key + self._local_key, ) self.exception( - f"Connect to {self._config_entry[CONF_HOST]} failed: %s", - type(e) + f"Connect to {self._config_entry[CONF_HOST]} failed: %s", type(e) ) if self._interface is not None: await self._interface.close() @@ -243,8 +235,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): if self._disconnect_task is not None: self._disconnect_task() self.debug( - "Closed connection with device %s.", - self._config_entry[CONF_FRIENDLY_NAME] + "Closed connection with device %s.", self._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 902efac..7c69cbb 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -21,7 +21,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_REGION, - + CONF_USERNAME, ) from homeassistant.core import callback @@ -49,7 +49,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_TO_ADD = "platform_to_add" NO_ADDITIONAL_ENTITIES = "no_additional_entities" -DISCOVERED_DEVICE = "discovered_device" +SELECTED_DEVICE = "selected_device" CUSTOM_DEVICE = "..." @@ -71,10 +71,11 @@ CLOUD_SETUP_SCHEMA = vol.Schema( vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Required(CONF_USER_ID): cv.string, + vol.Optional(CONF_USERNAME, default=DOMAIN): cv.string, } ) -BASIC_INFO_SCHEMA = vol.Schema( +CONFIGURE_DEVICE_SCHEMA = vol.Schema( { vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_LOCAL_KEY): str, @@ -101,16 +102,17 @@ PICK_ENTITY_SCHEMA = vol.Schema( ) -def devices_schema(discovered_devices, cloud_devices_list): +def devices_schema(discovered_devices, cloud_devices_list, add_custom_device=True): """Create schema for devices step.""" devices = {} - for dev_id, dev in discovered_devices.items(): + for dev_id, dev_host in discovered_devices.items(): dev_name = dev_id if dev_id in cloud_devices_list.keys(): dev_name = cloud_devices_list[dev_id][CONF_NAME] - devices[dev_id] = f"{dev_name} ({discovered_devices[dev_id]['ip']})" + devices[dev_id] = f"{dev_name} ({dev_host})" - devices.update({CUSTOM_DEVICE: CUSTOM_DEVICE}) + if add_custom_device: + devices.update({CUSTOM_DEVICE: CUSTOM_DEVICE}) # devices.update( # { @@ -118,15 +120,13 @@ def devices_schema(discovered_devices, cloud_devices_list): # for ent in entries # } # ) - return vol.Schema( - {vol.Required(DISCOVERED_DEVICE): vol.In(devices)} - ) + return vol.Schema({vol.Required(SELECTED_DEVICE): vol.In(devices)}) def options_schema(entities): """Create schema for options.""" entity_names = [ - f"{entity[CONF_ID]} {entity[CONF_FRIENDLY_NAME]}" for entity in entities + f"{entity[CONF_ID]}: {entity[CONF_FRIENDLY_NAME]}" for entity in entities ] return vol.Schema( { @@ -258,7 +258,7 @@ async def attempt_cloud_connection(hass, user_input): user_input.get(CONF_REGION), user_input.get(CONF_CLIENT_ID), user_input.get(CONF_CLIENT_SECRET), - user_input.get(CONF_USER_ID) + user_input.get(CONF_USER_ID), ) res = await tuya_api.async_get_access_token() @@ -306,17 +306,18 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"msg": res["msg"]} defaults = {} - defaults[CONF_REGION] = 'eu' - defaults[CONF_CLIENT_ID] = 'xx' - defaults[CONF_CLIENT_SECRET] = 'xx' - defaults[CONF_USER_ID] = 'xx' + 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( step_id="user", data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), errors=errors, - description_placeholders=placeholders + description_placeholders=placeholders, ) async def _create_entry(self, user_input): @@ -331,13 +332,15 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_DEVICES] = {} return self.async_create_entry( - title="LocalTuya", + title=user_input.get(CONF_USERNAME), data=user_input, ) async def async_step_import(self, user_input): """Handle import from YAML.""" - _LOGGER.error("Configuration via YAML file is no longer supported by this integration.") + _LOGGER.error( + "Configuration via YAML file is no longer supported by this integration." + ) # await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) # self._abort_if_unique_id_configured(updates=user_input) # return self.async_create_entry( @@ -354,12 +357,12 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): # self.dps_strings = config_entry.data.get(CONF_DPS_STRINGS, gen_dps_strings()) # self.entities = config_entry.data[CONF_ENTITIES] self.selected_device = None - self.basic_info = None + self.editing_device = False + self.device_data = None self.dps_strings = [] self.selected_platform = None - self.devices = {} + self.discovered_devices = {} self.entities = [] - self.data = None async def async_step_init(self, user_input=None): """Manage basic options.""" @@ -369,6 +372,8 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_cloud_setup() if user_input.get(CONF_ACTION) == CONF_ADD_DEVICE: return await self.async_step_add_device() + if user_input.get(CONF_ACTION) == CONF_EDIT_DEVICE: + return await self.async_step_edit_device() return self.async_show_form( step_id="init", @@ -403,27 +408,28 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): step_id="cloud_setup", data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), errors=errors, - description_placeholders=placeholders + description_placeholders=placeholders, ) async def async_step_add_device(self, user_input=None): """Handle adding a new device.""" # Use cache if available or fallback to manual discovery + self.editing_device = False errors = {} if user_input is not None: print("Selected {}".format(user_input)) - if user_input[DISCOVERED_DEVICE] != CUSTOM_DEVICE: - self.selected_device = user_input[DISCOVERED_DEVICE] - return await self.async_step_basic_info() + if user_input[SELECTED_DEVICE] != CUSTOM_DEVICE: + self.selected_device = user_input[SELECTED_DEVICE] + return await self.async_step_configure_device() - discovered_devices = {} + self.discovered_devices = {} data = self.hass.data.get(DOMAIN) if data and DATA_DISCOVERY in data: - discovered_devices = data[DATA_DISCOVERY].devices + self.discovered_devices = data[DATA_DISCOVERY].devices else: try: - discovered_devices = await discover() + self.discovered_devices = await discover() except OSError as ex: if ex.errno == errno.EADDRINUSE: errors["base"] = "address_in_use" @@ -433,37 +439,90 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): _LOGGER.exception("discovery failed") errors["base"] = "discovery_failed" - self.devices = { - ip: dev - for ip, dev in discovered_devices.items() + devices = { + dev_id: dev["ip"] + for dev_id, dev in self.discovered_devices.items() if dev["gwId"] not in self.config_entry.data[CONF_DEVICES] } + print("SCHEMA DEVS {}".format(devices)) return self.async_show_form( step_id="add_device", data_schema=devices_schema( - self.devices, - self.hass.data[DOMAIN][DATA_CLOUD]._device_list + devices, self.hass.data[DOMAIN][DATA_CLOUD]._device_list ), errors=errors, ) - async def async_step_basic_info(self, user_input=None): + async def async_step_edit_device(self, user_input=None): + """Handle editing a device.""" + self.editing_device = True + # Use cache if available or fallback to manual discovery + print("AAA") + errors = {} + if user_input is not None: + print("Selected {}".format(user_input)) + self.selected_device = user_input[SELECTED_DEVICE] + dev_conf = self.config_entry.data[CONF_DEVICES][self.selected_device] + self.dps_strings = dev_conf.get(CONF_DPS_STRINGS, gen_dps_strings()) + self.entities = dev_conf[CONF_ENTITIES] + print("Selected DPS {} ENT {}".format(self.dps_strings, self.entities)) + + return await self.async_step_configure_device() + + print("BBB: {}".format(self.config_entry.data[CONF_DEVICES])) + devices = {} + for dev_id, configured_dev in self.config_entry.data[CONF_DEVICES].items(): + devices[dev_id] = configured_dev[CONF_HOST] + print("SCHEMA DEVS {}".format(devices)) + + return self.async_show_form( + step_id="edit_device", + data_schema=devices_schema( + devices, self.hass.data[DOMAIN][DATA_CLOUD]._device_list, False + ), + errors=errors, + ) + + async def async_step_configure_device(self, user_input=None): """Handle input of basic info.""" errors = {} dev_id = self.selected_device if user_input is not None: - print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID)) + print("INPUT1!! {} {}".format(user_input, dev_id)) try: - self.basic_info = user_input + self.device_data = user_input.copy() if dev_id is not None: - # self.basic_info[CONF_PRODUCT_KEY] = self.devices[ + # self.device_data[CONF_PRODUCT_KEY] = self.devices[ # self.selected_device # ]["productKey"] cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD]._device_list if dev_id in cloud_devs: - self.basic_info[CONF_MODEL] = cloud_devs[dev_id].get(CONF_PRODUCT_NAME) + self.device_data[CONF_MODEL] = cloud_devs[dev_id].get( + CONF_PRODUCT_NAME + ) + if self.editing_device: + self.device_data.update( + { + CONF_DEVICE_ID: dev_id, + CONF_MODEL: self.device_data[CONF_MODEL], + CONF_DPS_STRINGS: self.dps_strings, + CONF_ENTITIES: [], + } + ) + if len(user_input[CONF_ENTITIES]) > 0: + entity_ids = [ + int(entity.split(":")[0]) + for entity in user_input[CONF_ENTITIES] + ] + device_config = self.config_entry.data[CONF_DEVICES][dev_id] + self.entities = [ + entity + for entity in device_config[CONF_ENTITIES] + if entity[CONF_ID] in entity_ids + ] + return await self.async_step_configure_entity() self.dps_strings = await validate_input(self.hass, user_input) print("ZIO KEN!! {} ".format(self.dps_strings)) @@ -478,21 +537,19 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - # If selected device exists as a config entry, load config from it - if self.selected_device in self.config_entry.data[CONF_DEVICES]: - print("ALREADY EXISTING!! {}".format(self.selected_device)) - # entry = self.config_entry.data[CONF_DEVICES][self.selected_device] - # await self.async_set_unique_id(entry.data[CONF_DEVICE_ID]) - # self.basic_info = entry.data.copy() - # self.dps_strings = self.basic_info.pop(CONF_DPS_STRINGS).copy() - # self.entities = self.basic_info.pop(CONF_ENTITIES).copy() - # return await self.async_step_pick_entity_type() - - # Insert default values from discovery and cloud if present defaults = {} - defaults.update(user_input or {}) - if dev_id is not None: - device = self.devices[dev_id] + if self.editing_device: + # If selected device exists as a config entry, load config from it + print("ALREADY EXISTING!! {}".format(dev_id)) + defaults = self.config_entry.data[CONF_DEVICES][dev_id].copy() + print("ALREADY EXISTING!! {} {}".format(self.entities, defaults)) + schema = schema_defaults(options_schema(self.entities), **defaults) + placeholders = {"for_device": f" for device `{dev_id}`"} + print("SCHEMA!! {}".format(schema)) + elif dev_id is not None: + # Insert default values from discovery and cloud if present + print("NEW DEVICE!! {}".format(dev_id)) + device = self.discovered_devices[dev_id] defaults[CONF_HOST] = device.get("ip") defaults[CONF_DEVICE_ID] = device.get("gwId") defaults[CONF_PROTOCOL_VERSION] = device.get("version") @@ -500,29 +557,100 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): if dev_id in cloud_devs: defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY) defaults[CONF_FRIENDLY_NAME] = cloud_devs[dev_id].get(CONF_NAME) + schema = schema_defaults(CONFIGURE_DEVICE_SCHEMA, **defaults) + placeholders = {"for_device": ""} + defaults.update(user_input or {}) return self.async_show_form( - step_id="basic_info", - data_schema=schema_defaults(BASIC_INFO_SCHEMA, **defaults), + step_id="configure_device", + data_schema=schema, errors=errors, + description_placeholders=placeholders, ) + async def async_step_pick_entity_type(self, user_input=None): + """Handle asking if user wants to add another entity.""" + if user_input is not None: + print("INPUT4!! {} {}".format(user_input, self.device_data)) + if user_input.get(NO_ADDITIONAL_ENTITIES): + config = { + **self.device_data, + CONF_DPS_STRINGS: self.dps_strings, + CONF_ENTITIES: self.entities, + } + print("NEW CONFIG!! {}".format(config)) + + # 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]: + print("AGGIORNO !! {}".format(dev_id)) + self.hass.config_entries.async_update_entry( + self.config_entry, data=config + ) + return self.async_abort( + reason="device_success", + description_placeholders={ + "dev_name": config.get(CONF_FRIENDLY_NAME), + "action": "updated", + }, + ) + + print("CREO NUOVO DEVICE!! {}".format(dev_id)) + new_data = self.config_entry.data.copy() + print("PRE: {}".format(new_data)) + new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) + # new_data[CONF_DEVICES]["AZZ"] = "OK" + new_data[CONF_DEVICES].update({dev_id: config}) + print("POST: {}".format(new_data)) + + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + ) + return self.async_create_entry(title="", data={}) + self.async_create_entry(title="", data={}) + + self.selected_platform = user_input[PLATFORM_TO_ADD] + return await self.async_step_configure_entity() + + # Add a checkbox that allows bailing out from config flow if at least one + # entity has been added + schema = PICK_ENTITY_SCHEMA + if self.selected_platform is not None: + schema = schema.extend( + {vol.Required(NO_ADDITIONAL_ENTITIES, default=True): bool} + ) + + return self.async_show_form(step_id="pick_entity_type", data_schema=schema) + + def available_dps_strings(self): + available_dps = [] + # print("FILTERING!! {} {}".format(self.dps_strings, self.entities)) + used_dps = [str(entity[CONF_ID]) for entity in self.entities] + # print("FILTERING-- {} {}".format(self.dps_strings, used_dps)) + for dp_string in self.dps_strings: + dp = dp_string.split(" ")[0] + # print("FILTERING2!! {} {}".format(dp_string, dp)) + if dp not in used_dps: + available_dps.append(dp_string) + return available_dps + async def async_step_entity(self, user_input=None): """Manage entity settings.""" errors = {} if user_input is not None: - print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID)) + print("INPUT2!! {} {}".format(user_input, CONF_DEVICE_ID)) print("ZIO KEN!! {} ".format(self.dps_strings)) entity = strip_dps_values(user_input, self.dps_strings) entity[CONF_ID] = self.current_entity[CONF_ID] entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM] - self.data[CONF_ENTITIES].append(entity) + self.device_data[CONF_ENTITIES].append(entity) - if len(self.entities) == len(self.data[CONF_ENTITIES]): + if len(self.entities) == len(self.device_data[CONF_ENTITIES]): self.hass.config_entries.async_update_entry( self.config_entry, - title=self.data[CONF_FRIENDLY_NAME], - data=self.data, + title=self.device_data[CONF_FRIENDLY_NAME], + data=self.device_data, ) return self.async_create_entry(title="", data={}) @@ -541,102 +669,56 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): }, ) - async def async_step_pick_entity_type(self, user_input=None): - """Handle asking if user wants to add another entity.""" - if user_input is not None: - print("INPUT3!! {} {}".format(user_input, self.basic_info)) - print("AAAZIO KEN!! {} ".format(self.dps_strings)) - print("NAAA!! {} ".format(user_input.get(NO_ADDITIONAL_ENTITIES))) - if user_input.get(NO_ADDITIONAL_ENTITIES): - print("INPUT4!! {}".format(self.dps_strings)) - print("INPUT4!! {}".format(self.entities)) - config = { - **self.basic_info, - CONF_DPS_STRINGS: self.dps_strings, - CONF_ENTITIES: self.entities, - } - print("NEW CONFIG!! {}".format(config)) - # self.config_entry.data[CONF_DEVICES] - - entry = async_config_entry_by_device_id(self.hass, self.unique_id) - dev_id = self.basic_info.get(CONF_DEVICE_ID) - if dev_id in self.config_entry.data[CONF_DEVICES]: - print("AGGIORNO !! {}".format(dev_id)) - self.hass.config_entries.async_update_entry(self.config_entry, data=config) - return self.async_abort( - reason="device_success", - description_placeholders={ - "dev_name": config.get(CONF_FRIENDLY_NAME), - "action": "updated" - } - ) - - print("CREO NUOVO DEVICE!! {}".format(dev_id)) - new_data = self.config_entry.data.copy() - print("PRE: {}".format(new_data)) - new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000)) - # new_data[CONF_DEVICES]["AZZ"] = "OK" - new_data[CONF_DEVICES].update({dev_id: config}) - print("POST: {}".format(new_data)) - - self.hass.config_entries.async_update_entry( - self.config_entry, - data=new_data, - ) - return self.async_create_entry(title="", data={}) - self.async_create_entry(title="", data={}) - print("DONE! now message {}".format(new_data)) - return self.async_abort( - reason="device_success", - description_placeholders={ - "dev_name": config.get(CONF_FRIENDLY_NAME), - "action": "created" - } - ) - # return self.async_create_entry( - # title=config[CONF_FRIENDLY_NAME], data=config - # ) - print("MA ZZZIO KEN!! {} ".format(self.dps_strings)) - - self.selected_platform = user_input[PLATFORM_TO_ADD] - return await self.async_step_add_entity() - - # Add a checkbox that allows bailing out from config flow if at least one - # entity has been added - schema = PICK_ENTITY_SCHEMA - if self.selected_platform is not None: - schema = schema.extend( - {vol.Required(NO_ADDITIONAL_ENTITIES, default=True): bool} - ) - - return self.async_show_form(step_id="pick_entity_type", data_schema=schema) - - async def async_step_add_entity(self, user_input=None): - """Handle adding a new entity.""" + async def async_step_configure_entity(self, user_input=None): + """Manage entity settings.""" errors = {} if user_input is not None: print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID)) already_configured = any( - switch[CONF_ID] == int(user_input[CONF_ID].split(" ")[0]) - for switch in self.entities + entity[CONF_ID] == int(user_input[CONF_ID].split(" ")[0]) + for entity in self.entities ) if not already_configured: user_input[CONF_PLATFORM] = self.selected_platform self.entities.append(strip_dps_values(user_input, self.dps_strings)) - return await self.async_step_pick_entity_type() + # new entity added. Let's check if there are more left... + print("ADDED. Remaining... {}".format(self.available_dps_strings())) + user_input = None + if len(self.available_dps_strings()) == 0: + user_input = {NO_ADDITIONAL_ENTITIES: True} + return await self.async_step_pick_entity_type(user_input) errors["base"] = "entity_already_configured" + if self.editing_device: + schema = platform_schema( + self.current_entity[CONF_PLATFORM], self.dps_strings, allow_id=False + ) + schema = schema_defaults(schema, self.dps_strings, **self.current_entity) + placeholders = { + "entity": f"entity with DP {self.current_entity[CONF_ID]}", + "platform": self.current_entity[CONF_PLATFORM], + } + else: + available_dps = self.available_dps_strings() + schema = platform_schema(self.selected_platform, available_dps) + placeholders = { + "entity": "an entity", + "platform": self.selected_platform, + } + return self.async_show_form( - step_id="add_entity", - data_schema=platform_schema(self.selected_platform, self.dps_strings), + step_id="configure_entity", + data_schema=schema, errors=errors, - description_placeholders={"platform": self.selected_platform}, + description_placeholders=placeholders, ) async def async_step_yaml_import(self, user_input=None): """Manage YAML imports.""" - _LOGGER.error("Configuration via YAML file is no longer supported by this integration.") + _LOGGER.error( + "Configuration via YAML file is no longer supported by this integration." + ) # if user_input is not None: # return self.async_create_entry(title="", data={}) # return self.async_show_form(step_id="yaml_import") @@ -644,7 +726,7 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow): @property def current_entity(self): """Existing configuration for entity currently being edited.""" - return self.entities[len(self.data[CONF_ENTITIES])] + return self.entities[len(self.device_data[CONF_ENTITIES])] class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/localtuya/const.py b/custom_components/localtuya/const.py index f50fca2..8feafad 100644 --- a/custom_components/localtuya/const.py +++ b/custom_components/localtuya/const.py @@ -19,7 +19,7 @@ PLATFORMS = [ "vacuum", ] -TUYA_DEVICE = "tuya_device" +TUYA_DEVICES = "tuya_devices" ATTR_CURRENT = "current" ATTR_CURRENT_CONSUMPTION = "current_consumption" diff --git a/custom_components/localtuya/manifest.json b/custom_components/localtuya/manifest.json index 7f19f9f..bb6f2ea 100644 --- a/custom_components/localtuya/manifest.json +++ b/custom_components/localtuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "localtuya", "name": "LocalTuya integration", - "version": "3.2.1", + "version": "4.0.0", "documentation": "https://github.com/rospogrigio/localtuya/", "dependencies": [], "codeowners": [ diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 7a5b314..b80cf9e 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -23,7 +23,8 @@ "region": "API server region", "client_id": "Client ID", "client_secret": "Secret", - "user_id": "User ID" + "user_id": "User ID", + "user_name": "Username" } } } @@ -62,19 +63,27 @@ "title": "Add a new device", "description": "Pick one of the automatically discovered devices or `...` to manually to add a device.", "data": { - "discovered_device": "Discovered Devices" + "selected_device": "Discovered Devices" } }, - "basic_info": { - "title": "Add Tuya device", - "description": "Fill in the basic device details. The name entered here will be used to identify the device itself (as seen in the `Integrations` page). You will add entities and give them names in the following steps.", + "edit_device": { + "title": "Edit a new device", + "description": "Pick the configured device you wish to edit.", + "data": { + "selected_device": "Configured Devices" + } + }, + "configure_device": { + "title": "Configure Tuya device", + "description": "Fill in the device details{for_device}.", "data": { "friendly_name": "Name", "host": "Host", "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval (seconds, only when not updating automatically)" + "scan_interval": "Scan interval (seconds, only when not updating automatically)", + "entities": "Entities (uncheck an entity to remove it)" } }, "pick_entity_type": { @@ -85,9 +94,9 @@ "no_additional_entities": "Do not add any more entities" } }, - "add_entity": { - "title": "Add new entity", - "description": "Please fill out the details for an entity with type `{platform}`. All settings except for `ID` can be changed from the Options page later.", + "configure_entity": { + "title": "Configure entity", + "description": "Please fill out the details for {entity} with type `{platform}`. All settings except for `ID` can be changed from the Options page later.", "data": { "id": "ID", "friendly_name": "Friendly name",