Introduced migration; fixed update when adding device

This commit is contained in:
rospogrigio
2022-05-19 13:36:55 +02:00
parent d4c20ebfed
commit afc0230796
7 changed files with 348 additions and 216 deletions

View File

@@ -11,12 +11,16 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_REGION,
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICES, CONF_DEVICES,
CONF_ENTITIES, CONF_ENTITIES,
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_PLATFORM, CONF_PLATFORM,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD, SERVICE_RELOAD,
) )
@@ -31,10 +35,11 @@ from .config_flow import config_schema
from .const import ( from .const import (
ATTR_UPDATED_AT, ATTR_UPDATED_AT,
CONF_PRODUCT_KEY, CONF_PRODUCT_KEY,
CONF_USER_ID,
DATA_CLOUD, DATA_CLOUD,
DATA_DISCOVERY, DATA_DISCOVERY,
DOMAIN, DOMAIN,
TUYA_DEVICE TUYA_DEVICES,
) )
from .discovery import TuyaDiscovery from .discovery import TuyaDiscovery
@@ -62,6 +67,7 @@ SERVICE_SET_DP_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the LocalTuya integration component.""" """Set up the LocalTuya integration component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
print("SETUP")
device_cache = {} device_cache = {}
@@ -84,14 +90,11 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def _handle_set_dp(event): async def _handle_set_dp(event):
"""Handle set_dp service call.""" """Handle set_dp service call."""
entry = async_config_entry_by_device_id(hass, event.data[CONF_DEVICE_ID]) dev_id = event.data[CONF_DEVICE_ID]
if not entry: if dev_id not in hass.data[DOMAIN][TUYA_DEVICES]:
raise HomeAssistantError("unknown device id") raise HomeAssistantError("unknown device id")
if entry.entry_id not in hass.data[DOMAIN]: device = hass.data[DOMAIN][TUYA_DEVICES][dev_id]
raise HomeAssistantError("device has not been discovered")
device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE]
if not device.connected: if not device.connected:
raise HomeAssistantError("not connected to device") 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]: elif entry.entry_id in hass.data[DOMAIN]:
_LOGGER.debug("Device %s found with IP %s", device_id, device_ip) _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() 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) discovery = TuyaDiscovery(_device_discovered)
def _shutdown(event): def _shutdown(event):
@@ -168,13 +159,16 @@ async def async_setup(hass: HomeAssistant, config: dict):
async def _async_reconnect(now): async def _async_reconnect(now):
"""Try connecting to devices not already connected to.""" """Try connecting to devices not already connected to."""
for entry_id, value in hass.data[DOMAIN].items(): for device in hass.data[DOMAIN][TUYA_DEVICES]:
if entry_id == DATA_DISCOVERY:
continue
device = value[TUYA_DEVICE]
if not device.connected: if not device.connected:
device.async_connect() 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) async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL)
@@ -191,16 +185,69 @@ async def async_setup(hass: HomeAssistant, config: dict):
return True return True
async def async_migrate_entry(domain): async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry.""" """Migrate old entries merging all of them in one."""
print("WHYYYYYYY") new_version = 2
_LOGGER.error("WHHHYYYYYY") 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 return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
print("SETUP ENTRY STARTED!")
"""Set up LocalTuya integration from a config entry.""" """Set up LocalTuya integration from a config entry."""
unsub_listener = entry.add_update_listener(update_listener) 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) # device = TuyaDevice(hass, entry.data)
# #
@@ -212,13 +259,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def setup_entities(dev_id): async def setup_entities(dev_id):
dev_entry = entry.data[CONF_DEVICES][dev_id] dev_entry = entry.data[CONF_DEVICES][dev_id]
device = TuyaDevice(hass, dev_entry) device = TuyaDevice(hass, dev_entry)
hass.data[DOMAIN][dev_id] = { hass.data[DOMAIN][TUYA_DEVICES][dev_id] = device
UNSUB_LISTENER: unsub_listener,
TUYA_DEVICE: device,
}
platforms = set(entity[CONF_PLATFORM] for entity in dev_entry[CONF_ENTITIES]) platforms = set(entity[CONF_PLATFORM] for entity in dev_entry[CONF_ENTITIES])
print("DEV {} platforms: {}".format(dev_id, platforms))
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_setup(entry, platform) 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() device.async_connect()
await async_remove_orphan_entities(hass, entry) await async_remove_orphan_entities(hass, entry)
print("SETUP_ENTITIES for {} ENDED".format(dev_id))
for dev_id in entry.data[CONF_DEVICES]: for dev_id in entry.data[CONF_DEVICES]:
hass.async_create_task(setup_entities(dev_id)) hass.async_create_task(setup_entities(dev_id))
print("SETUP ENTRY ENDED!")
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry.""" """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( unload_ok = all(
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_unload(entry, component) hass.config_entries.async_forward_entry_unload(entry, component)
for component in set( for component in platforms
entity[CONF_PLATFORM] for entity in entry.data[CONF_ENTITIES]
)
] ]
) )
) )
hass.data[DOMAIN][entry.entry_id][UNSUB_LISTENER]() hass.data[DOMAIN][UNSUB_LISTENER]()
await hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE].close() for dev_id, device in hass.data[DOMAIN][TUYA_DEVICES].items():
if unload_ok: await device.close()
hass.data[DOMAIN].pop(entry.entry_id)
if unload_ok:
hass.data[DOMAIN][TUYA_DEVICES] = {}
# print("ASYNC_UNLOAD_ENTRY ENDED")
return True return True
async def update_listener(hass, config_entry): async def update_listener(hass, config_entry):
"""Update listener.""" """Update listener."""
print("UPDATE_LISTENER INVOKED")
await hass.config_entries.async_reload(config_entry.entry_id) 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( async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool: ) -> bool:
"""Remove a config entry from a device.""" """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] 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]: if dev_id not in config_entry.data[CONF_DEVICES]:
_LOGGER.debug( _LOGGER.debug(
"Device ID %s not found in config entry: finalizing device removal", "Device ID %s not found in config entry: finalizing device removal", dev_id
dev_id
) )
return True return True
_LOGGER.debug("Closing device connection for %s", dev_id) await hass.data[DOMAIN][TUYA_DEVICES][dev_id].close()
await hass.data[DOMAIN][dev_id][TUYA_DEVICE].close()
new_data = config_entry.data.copy() new_data = config_entry.data.copy()
new_data[CONF_DEVICES].pop(dev_id) new_data[CONF_DEVICES].pop(dev_id)
@@ -287,7 +338,6 @@ async def async_remove_config_entry_device(
config_entry, config_entry,
data=new_data, data=new_data,
) )
_LOGGER.debug("Config entry updated")
ent_reg = await er.async_get_registry(hass) ent_reg = await er.async_get_registry(hass)
entities = { entities = {
@@ -298,13 +348,13 @@ async def async_remove_config_entry_device(
for entity_id in entities.values(): for entity_id in entities.values():
ent_reg.async_remove(entity_id) ent_reg.async_remove(entity_id)
print("REMOVED {}".format(entity_id)) print("REMOVED {}".format(entity_id))
_LOGGER.debug("Removed %s entities: finalizing device removal", len(entities))
return True return True
async def async_remove_orphan_entities(hass, entry): async def async_remove_orphan_entities(hass, entry):
"""Remove entities associated with config entry that has been removed.""" """Remove entities associated with config entry that has been removed."""
return
ent_reg = await er.async_get_registry(hass) ent_reg = await er.async_get_registry(hass)
entities = { entities = {
ent.unique_id: ent.entity_id ent.unique_id: ent.entity_id
@@ -312,7 +362,7 @@ async def async_remove_orphan_entities(hass, entry):
} }
print("ENTITIES ORPHAN {}".format(entities)) print("ENTITIES ORPHAN {}".format(entities))
return return
res = ent_reg.async_remove('switch.aa') res = ent_reg.async_remove("switch.aa")
print("RESULT ORPHAN {}".format(res)) print("RESULT ORPHAN {}".format(res))
# del entities[101] # del entities[101]

View File

@@ -127,7 +127,7 @@ class TuyaCloudApi:
) )
return f"Error {r_json['code']}: {r_json['msg']}" 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)) # print("DEV__LIST: {}".format(self._device_list))
return "ok" return "ok"

View File

@@ -28,7 +28,7 @@ from .const import (
CONF_PROTOCOL_VERSION, CONF_PROTOCOL_VERSION,
DOMAIN, DOMAIN,
DATA_CLOUD, DATA_CLOUD,
TUYA_DEVICE, TUYA_DEVICES,
) )
_LOGGER = logging.getLogger(__name__) _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 This is a generic method and each platform should lock domain and
entity_class with functools.partial. entity_class with functools.partial.
""" """
print("ASYNC_SETUP_ENTRY: {} {} {}".format(
config_entry.data,
entity_class,
flow_schema(None).items())
)
entities = [] entities = []
for dev_id in config_entry.data[CONF_DEVICES]: for dev_id in config_entry.data[CONF_DEVICES]:
@@ -77,7 +72,7 @@ async def async_setup_entry(
if len(entities_to_setup) > 0: 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)) dps_config_fields = list(get_dps_for_platform(flow_schema))
@@ -94,8 +89,6 @@ async def async_setup_entry(
entity_config[CONF_ID], entity_config[CONF_ID],
) )
) )
print("ADDING {} entities".format(len(entities)))
async_add_entities(entities) async_add_entities(entities)
@@ -209,11 +202,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
"New local key for %s: from %s to %s", "New local key for %s: from %s to %s",
dev_id, dev_id,
old_key, old_key,
self._local_key self._local_key,
) )
self.exception( self.exception(
f"Connect to {self._config_entry[CONF_HOST]} failed: %s", f"Connect to {self._config_entry[CONF_HOST]} failed: %s", type(e)
type(e)
) )
if self._interface is not None: if self._interface is not None:
await self._interface.close() await self._interface.close()
@@ -243,8 +235,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
if self._disconnect_task is not None: if self._disconnect_task is not None:
self._disconnect_task() self._disconnect_task()
self.debug( self.debug(
"Closed connection with device %s.", "Closed connection with device %s.", self._config_entry[CONF_FRIENDLY_NAME]
self._config_entry[CONF_FRIENDLY_NAME]
) )
async def set_dp(self, state, dp_index): async def set_dp(self, state, dp_index):

View File

@@ -21,7 +21,7 @@ from homeassistant.const import (
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, CONF_CLIENT_SECRET,
CONF_REGION, CONF_REGION,
CONF_USERNAME,
) )
from homeassistant.core import callback from homeassistant.core import callback
@@ -49,7 +49,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORM_TO_ADD = "platform_to_add" PLATFORM_TO_ADD = "platform_to_add"
NO_ADDITIONAL_ENTITIES = "no_additional_entities" NO_ADDITIONAL_ENTITIES = "no_additional_entities"
DISCOVERED_DEVICE = "discovered_device" SELECTED_DEVICE = "selected_device"
CUSTOM_DEVICE = "..." CUSTOM_DEVICE = "..."
@@ -71,10 +71,11 @@ CLOUD_SETUP_SCHEMA = vol.Schema(
vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Required(CONF_USER_ID): 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_FRIENDLY_NAME): str,
vol.Required(CONF_LOCAL_KEY): 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.""" """Create schema for devices step."""
devices = {} devices = {}
for dev_id, dev in discovered_devices.items(): for dev_id, dev_host in discovered_devices.items():
dev_name = dev_id dev_name = dev_id
if dev_id in cloud_devices_list.keys(): if dev_id in cloud_devices_list.keys():
dev_name = cloud_devices_list[dev_id][CONF_NAME] 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( # devices.update(
# { # {
@@ -118,15 +120,13 @@ def devices_schema(discovered_devices, cloud_devices_list):
# for ent in entries # for ent in entries
# } # }
# ) # )
return vol.Schema( return vol.Schema({vol.Required(SELECTED_DEVICE): vol.In(devices)})
{vol.Required(DISCOVERED_DEVICE): vol.In(devices)}
)
def options_schema(entities): def options_schema(entities):
"""Create schema for options.""" """Create schema for options."""
entity_names = [ 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( return vol.Schema(
{ {
@@ -258,7 +258,7 @@ async def attempt_cloud_connection(hass, user_input):
user_input.get(CONF_REGION), user_input.get(CONF_REGION),
user_input.get(CONF_CLIENT_ID), user_input.get(CONF_CLIENT_ID),
user_input.get(CONF_CLIENT_SECRET), 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() res = await tuya_api.async_get_access_token()
@@ -306,17 +306,18 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
placeholders = {"msg": res["msg"]} placeholders = {"msg": res["msg"]}
defaults = {} defaults = {}
defaults[CONF_REGION] = 'eu' defaults[CONF_REGION] = "eu"
defaults[CONF_CLIENT_ID] = 'xx' defaults[CONF_CLIENT_ID] = "xxx"
defaults[CONF_CLIENT_SECRET] = 'xx' defaults[CONF_CLIENT_SECRET] = "xxx"
defaults[CONF_USER_ID] = 'xx' defaults[CONF_USER_ID] = "xxx"
defaults[CONF_USERNAME] = "xxx"
defaults.update(user_input or {}) defaults.update(user_input or {})
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults),
errors=errors, errors=errors,
description_placeholders=placeholders description_placeholders=placeholders,
) )
async def _create_entry(self, user_input): async def _create_entry(self, user_input):
@@ -331,13 +332,15 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
user_input[CONF_DEVICES] = {} user_input[CONF_DEVICES] = {}
return self.async_create_entry( return self.async_create_entry(
title="LocalTuya", title=user_input.get(CONF_USERNAME),
data=user_input, data=user_input,
) )
async def async_step_import(self, user_input): async def async_step_import(self, user_input):
"""Handle import from YAML.""" """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]) # await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
# self._abort_if_unique_id_configured(updates=user_input) # self._abort_if_unique_id_configured(updates=user_input)
# return self.async_create_entry( # 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.dps_strings = config_entry.data.get(CONF_DPS_STRINGS, gen_dps_strings())
# self.entities = config_entry.data[CONF_ENTITIES] # self.entities = config_entry.data[CONF_ENTITIES]
self.selected_device = None self.selected_device = None
self.basic_info = None self.editing_device = False
self.device_data = None
self.dps_strings = [] self.dps_strings = []
self.selected_platform = None self.selected_platform = None
self.devices = {} self.discovered_devices = {}
self.entities = [] self.entities = []
self.data = None
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Manage basic options.""" """Manage basic options."""
@@ -369,6 +372,8 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
return await self.async_step_cloud_setup() return await self.async_step_cloud_setup()
if user_input.get(CONF_ACTION) == CONF_ADD_DEVICE: if user_input.get(CONF_ACTION) == CONF_ADD_DEVICE:
return await self.async_step_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( return self.async_show_form(
step_id="init", step_id="init",
@@ -403,27 +408,28 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
step_id="cloud_setup", step_id="cloud_setup",
data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults), data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults),
errors=errors, errors=errors,
description_placeholders=placeholders description_placeholders=placeholders,
) )
async def async_step_add_device(self, user_input=None): async def async_step_add_device(self, user_input=None):
"""Handle adding a new device.""" """Handle adding a new device."""
# Use cache if available or fallback to manual discovery # Use cache if available or fallback to manual discovery
self.editing_device = False
errors = {} errors = {}
if user_input is not None: if user_input is not None:
print("Selected {}".format(user_input)) print("Selected {}".format(user_input))
if user_input[DISCOVERED_DEVICE] != CUSTOM_DEVICE: if user_input[SELECTED_DEVICE] != CUSTOM_DEVICE:
self.selected_device = user_input[DISCOVERED_DEVICE] self.selected_device = user_input[SELECTED_DEVICE]
return await self.async_step_basic_info() return await self.async_step_configure_device()
discovered_devices = {} self.discovered_devices = {}
data = self.hass.data.get(DOMAIN) data = self.hass.data.get(DOMAIN)
if data and DATA_DISCOVERY in data: if data and DATA_DISCOVERY in data:
discovered_devices = data[DATA_DISCOVERY].devices self.discovered_devices = data[DATA_DISCOVERY].devices
else: else:
try: try:
discovered_devices = await discover() self.discovered_devices = await discover()
except OSError as ex: except OSError as ex:
if ex.errno == errno.EADDRINUSE: if ex.errno == errno.EADDRINUSE:
errors["base"] = "address_in_use" errors["base"] = "address_in_use"
@@ -433,37 +439,90 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
_LOGGER.exception("discovery failed") _LOGGER.exception("discovery failed")
errors["base"] = "discovery_failed" errors["base"] = "discovery_failed"
self.devices = { devices = {
ip: dev dev_id: dev["ip"]
for ip, dev in discovered_devices.items() for dev_id, dev in self.discovered_devices.items()
if dev["gwId"] not in self.config_entry.data[CONF_DEVICES] if dev["gwId"] not in self.config_entry.data[CONF_DEVICES]
} }
print("SCHEMA DEVS {}".format(devices))
return self.async_show_form( return self.async_show_form(
step_id="add_device", step_id="add_device",
data_schema=devices_schema( data_schema=devices_schema(
self.devices, devices, self.hass.data[DOMAIN][DATA_CLOUD]._device_list
self.hass.data[DOMAIN][DATA_CLOUD]._device_list
), ),
errors=errors, 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.""" """Handle input of basic info."""
errors = {} errors = {}
dev_id = self.selected_device dev_id = self.selected_device
if user_input is not None: if user_input is not None:
print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID)) print("INPUT1!! {} {}".format(user_input, dev_id))
try: try:
self.basic_info = user_input self.device_data = user_input.copy()
if dev_id is not None: 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 # self.selected_device
# ]["productKey"] # ]["productKey"]
cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD]._device_list cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD]._device_list
if dev_id in cloud_devs: 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) self.dps_strings = await validate_input(self.hass, user_input)
print("ZIO KEN!! {} ".format(self.dps_strings)) print("ZIO KEN!! {} ".format(self.dps_strings))
@@ -478,21 +537,19 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
_LOGGER.exception("Unexpected exception") _LOGGER.exception("Unexpected exception")
errors["base"] = "unknown" 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 = {}
defaults.update(user_input or {}) if self.editing_device:
if dev_id is not None: # If selected device exists as a config entry, load config from it
device = self.devices[dev_id] 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_HOST] = device.get("ip")
defaults[CONF_DEVICE_ID] = device.get("gwId") defaults[CONF_DEVICE_ID] = device.get("gwId")
defaults[CONF_PROTOCOL_VERSION] = device.get("version") defaults[CONF_PROTOCOL_VERSION] = device.get("version")
@@ -500,29 +557,100 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
if dev_id in cloud_devs: if dev_id in cloud_devs:
defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY) defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY)
defaults[CONF_FRIENDLY_NAME] = cloud_devs[dev_id].get(CONF_NAME) 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( return self.async_show_form(
step_id="basic_info", step_id="configure_device",
data_schema=schema_defaults(BASIC_INFO_SCHEMA, **defaults), data_schema=schema,
errors=errors, 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): async def async_step_entity(self, user_input=None):
"""Manage entity settings.""" """Manage entity settings."""
errors = {} errors = {}
if user_input is not None: 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)) print("ZIO KEN!! {} ".format(self.dps_strings))
entity = strip_dps_values(user_input, self.dps_strings) entity = strip_dps_values(user_input, self.dps_strings)
entity[CONF_ID] = self.current_entity[CONF_ID] entity[CONF_ID] = self.current_entity[CONF_ID]
entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM] 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.hass.config_entries.async_update_entry(
self.config_entry, self.config_entry,
title=self.data[CONF_FRIENDLY_NAME], title=self.device_data[CONF_FRIENDLY_NAME],
data=self.data, data=self.device_data,
) )
return self.async_create_entry(title="", 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): async def async_step_configure_entity(self, user_input=None):
"""Handle asking if user wants to add another entity.""" """Manage entity settings."""
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."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID)) print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID))
already_configured = any( already_configured = any(
switch[CONF_ID] == int(user_input[CONF_ID].split(" ")[0]) entity[CONF_ID] == int(user_input[CONF_ID].split(" ")[0])
for switch in self.entities for entity in self.entities
) )
if not already_configured: if not already_configured:
user_input[CONF_PLATFORM] = self.selected_platform user_input[CONF_PLATFORM] = self.selected_platform
self.entities.append(strip_dps_values(user_input, self.dps_strings)) 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" 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( return self.async_show_form(
step_id="add_entity", step_id="configure_entity",
data_schema=platform_schema(self.selected_platform, self.dps_strings), data_schema=schema,
errors=errors, errors=errors,
description_placeholders={"platform": self.selected_platform}, description_placeholders=placeholders,
) )
async def async_step_yaml_import(self, user_input=None): async def async_step_yaml_import(self, user_input=None):
"""Manage YAML imports.""" """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: # if user_input is not None:
# return self.async_create_entry(title="", data={}) # return self.async_create_entry(title="", data={})
# return self.async_show_form(step_id="yaml_import") # return self.async_show_form(step_id="yaml_import")
@@ -644,7 +726,7 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
@property @property
def current_entity(self): def current_entity(self):
"""Existing configuration for entity currently being edited.""" """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): class CannotConnect(exceptions.HomeAssistantError):

View File

@@ -19,7 +19,7 @@ PLATFORMS = [
"vacuum", "vacuum",
] ]
TUYA_DEVICE = "tuya_device" TUYA_DEVICES = "tuya_devices"
ATTR_CURRENT = "current" ATTR_CURRENT = "current"
ATTR_CURRENT_CONSUMPTION = "current_consumption" ATTR_CURRENT_CONSUMPTION = "current_consumption"

View File

@@ -1,7 +1,7 @@
{ {
"domain": "localtuya", "domain": "localtuya",
"name": "LocalTuya integration", "name": "LocalTuya integration",
"version": "3.2.1", "version": "4.0.0",
"documentation": "https://github.com/rospogrigio/localtuya/", "documentation": "https://github.com/rospogrigio/localtuya/",
"dependencies": [], "dependencies": [],
"codeowners": [ "codeowners": [

View File

@@ -23,7 +23,8 @@
"region": "API server region", "region": "API server region",
"client_id": "Client ID", "client_id": "Client ID",
"client_secret": "Secret", "client_secret": "Secret",
"user_id": "User ID" "user_id": "User ID",
"user_name": "Username"
} }
} }
} }
@@ -62,19 +63,27 @@
"title": "Add a new device", "title": "Add a new device",
"description": "Pick one of the automatically discovered devices or `...` to manually to add a device.", "description": "Pick one of the automatically discovered devices or `...` to manually to add a device.",
"data": { "data": {
"discovered_device": "Discovered Devices" "selected_device": "Discovered Devices"
} }
}, },
"basic_info": { "edit_device": {
"title": "Add Tuya device", "title": "Edit a new 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.", "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": { "data": {
"friendly_name": "Name", "friendly_name": "Name",
"host": "Host", "host": "Host",
"device_id": "Device ID", "device_id": "Device ID",
"local_key": "Local key", "local_key": "Local key",
"protocol_version": "Protocol Version", "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": { "pick_entity_type": {
@@ -85,9 +94,9 @@
"no_additional_entities": "Do not add any more entities" "no_additional_entities": "Do not add any more entities"
} }
}, },
"add_entity": { "configure_entity": {
"title": "Add new entity", "title": "Configure 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.", "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": { "data": {
"id": "ID", "id": "ID",
"friendly_name": "Friendly name", "friendly_name": "Friendly name",