542 lines
22 KiB
Python
542 lines
22 KiB
Python
"""Meross devices platform loader"""
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Tuple, Dict, Optional, Collection
|
|
|
|
import homeassistant.helpers.config_validation as cv
|
|
import voluptuous as vol
|
|
from homeassistant import config_entries
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryNotReady, ConfigEntryAuthFailed
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
from meross_iot.controller.device import BaseDevice
|
|
from meross_iot.http_api import MerossHttpClient, ErrorCodes
|
|
from meross_iot.manager import MerossManager
|
|
from meross_iot.model.credentials import MerossCloudCreds
|
|
from meross_iot.model.enums import OnlineStatus, Namespace
|
|
from meross_iot.model.exception import CommandTimeoutError
|
|
from meross_iot.model.http.device import HttpDeviceInfo
|
|
from meross_iot.model.http.exception import (
|
|
TokenExpiredException,
|
|
TooManyTokensException,
|
|
UnauthorizedException,
|
|
HttpApiError, BadLoginException,
|
|
)
|
|
|
|
from .common import (
|
|
ATTR_CONFIG,
|
|
CLOUD_HANDLER,
|
|
DOMAIN,
|
|
HA_CLIMATE,
|
|
HA_COVER,
|
|
HA_FAN,
|
|
HA_LIGHT,
|
|
HA_SENSOR,
|
|
HA_SWITCH,
|
|
MANAGER,
|
|
MEROSS_PLATFORMS,
|
|
SENSORS,
|
|
dismiss_notification,
|
|
notify_error,
|
|
log_exception,
|
|
CONF_STORED_CREDS,
|
|
LIMITER,
|
|
CONF_HTTP_ENDPOINT, CONF_MQTT_SKIP_CERT_VALIDATION, HTTP_API_RE,
|
|
HTTP_UPDATE_INTERVAL, DEVICE_LIST_COORDINATOR, calculate_id, DEFAULT_USER_AGENT, CONF_OPT_CUSTOM_USER_AGENT,
|
|
CONF_OVERRIDE_MQTT_ENDPOINT, CONF_OPT_LAN, CONF_OPT_LAN_MQTT_ONLY, TRANSPORT_MODES_TO_ENUM,
|
|
MEROSS_DEFAULT_CLOUD_API_URL
|
|
)
|
|
from .version import MEROSS_IOT_VERSION
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
CONFIG_SCHEMA = vol.Schema(
|
|
{
|
|
DOMAIN: vol.Schema(
|
|
{
|
|
vol.Required(CONF_HTTP_ENDPOINT): cv.string,
|
|
vol.Required(CONF_MQTT_SKIP_CERT_VALIDATION): cv.boolean,
|
|
vol.Optional(CONF_STORED_CREDS): cv.string,
|
|
vol.Optional(CONF_OVERRIDE_MQTT_ENDPOINT): cv.string
|
|
}
|
|
)
|
|
},
|
|
extra=vol.ALLOW_EXTRA,
|
|
)
|
|
|
|
|
|
def print_startup_message(http_devices: List[HttpDeviceInfo]):
|
|
http_info = "\n".join(
|
|
[f"- {x.dev_name} ({x.device_type}) - {x.online_status}" for x in http_devices]
|
|
)
|
|
|
|
start_message = (
|
|
f"\n"
|
|
f"===============================\n"
|
|
f"Meross Cloud Custom component\n"
|
|
f"Developed by Alberto Geniola\n"
|
|
f"Low level library version: {MEROSS_IOT_VERSION}\n"
|
|
f"-------------------------------\n"
|
|
f"This custom component is under development and not yet ready for production use.\n"
|
|
f"In case of errors/misbehave, please report it here: \n"
|
|
f"https://github.com/albertogeniola/meross-homeassistant/issues\n"
|
|
f"\n"
|
|
f"If you like this extension and you want to support it, please consider donating.\n"
|
|
f"-------------------------------\n"
|
|
f"List of devices reported by HTTP API:\n"
|
|
f"{http_info}"
|
|
f"\n==============================="
|
|
)
|
|
_LOGGER.warning(start_message)
|
|
|
|
|
|
class MerossCoordinator(DataUpdateCoordinator):
|
|
def __init__(self,
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
http_api_endpoint: str,
|
|
creds: MerossCloudCreds,
|
|
mqtt_skip_cert_validation: bool,
|
|
mqtt_override_address: Optional[Tuple[str, int]],
|
|
update_interval: timedelta,
|
|
ua_header: str):
|
|
|
|
self._entry = config_entry
|
|
self._http_api_endpoint = http_api_endpoint
|
|
self._cached_creds = creds
|
|
self._skip_cert_validation = mqtt_skip_cert_validation
|
|
self._mqtt_override_address = mqtt_override_address
|
|
self._setup_done = False
|
|
self._ua_header = ua_header
|
|
|
|
# Objects not to be initialized here
|
|
self._client = None
|
|
self._manager = None
|
|
|
|
super().__init__(hass=hass, logger=_LOGGER, name="meross_http_coordinator", update_interval=update_interval,
|
|
update_method=self._async_fetch_http_data)
|
|
|
|
async def _async_fetch_http_data(self):
|
|
try:
|
|
async with asyncio.timeout(10):
|
|
# Fetch devices and compose a quick-access dictionary
|
|
devices = await self._client.async_list_devices()
|
|
return {device.uuid: device for device in devices}
|
|
|
|
except (BadLoginException, TokenExpiredException, UnauthorizedException) as err:
|
|
# Raising ConfigEntryAuthFailed will cancel future updates
|
|
# and start a config flow with SOURCE_REAUTH (async_step_reauth)
|
|
raise ConfigEntryAuthFailed from err
|
|
except HttpApiError as err:
|
|
raise UpdateFailed(f"Error communicating with API: {err}")
|
|
|
|
async def initial_setup(self):
|
|
if self._setup_done:
|
|
raise ValueError("This coordinator was already set up")
|
|
|
|
# Test the stored credentials if any. In case the credentials are invalid
|
|
# try to retrieve a new token
|
|
try:
|
|
self._client, http_devices, creds_renewed = await get_or_test_creds(
|
|
http_api_url=self._http_api_endpoint,
|
|
creds=self._cached_creds,
|
|
ua_header=self._ua_header
|
|
)
|
|
except (BadLoginException, TokenExpiredException, UnauthorizedException) as err:
|
|
raise ConfigEntryAuthFailed from err
|
|
except HttpApiError as err:
|
|
raise ConfigEntryNotReady(f"Error communicating with API: {err}") from err
|
|
|
|
# If a new token was issued, store it into the current entry
|
|
if creds_renewed:
|
|
# Override the new credentials and store them into HA entry
|
|
self._cached_creds = self._client.cloud_credentials
|
|
self.hass.config_entries.async_update_entry(
|
|
entry=self._entry,
|
|
data={
|
|
CONF_HTTP_ENDPOINT: self._cached_creds.domain,
|
|
CONF_STORED_CREDS: {
|
|
"token": self._cached_creds.token,
|
|
"key": self._cached_creds.key,
|
|
"user_id": self._cached_creds.user_id,
|
|
"user_email": self._cached_creds.user_email,
|
|
"issued_on": self._cached_creds.issued_on.isoformat(),
|
|
"domain": self._cached_creds.domain,
|
|
"mqtt_domain": self._cached_creds.mqtt_domain
|
|
},
|
|
},
|
|
)
|
|
|
|
# Now that we are logged in at HTTP api level, instantiate the manager.
|
|
self._manager = MerossManager(
|
|
http_client=self._client,
|
|
mqtt_override_server=self._mqtt_override_address,
|
|
auto_reconnect=True,
|
|
mqtt_skip_cert_validation=self._skip_cert_validation,
|
|
)
|
|
|
|
# Since we already have fetched for the DeviceList, publish it right away
|
|
self.async_set_updated_data({device.uuid: device for device in http_devices})
|
|
|
|
# Print startup message, start the manager and issue a first discovery
|
|
print_startup_message(http_devices=self.data.values())
|
|
_LOGGER.info("Starting meross manager")
|
|
await self._manager.async_init()
|
|
_LOGGER.info("Discovering Meross devices...")
|
|
await self._manager.async_device_discovery()
|
|
|
|
# If no exception is thrown so far, it means setup was successful
|
|
self._setup_done = True
|
|
|
|
@property
|
|
def manager(self) -> MerossManager:
|
|
return self._manager
|
|
|
|
@property
|
|
def client(self) -> MerossHttpClient:
|
|
return self._client
|
|
|
|
|
|
class MerossDevice(Entity):
|
|
def __init__(self,
|
|
device: BaseDevice,
|
|
channel: int,
|
|
device_list_coordinator: DataUpdateCoordinator[Dict[str, HttpDeviceInfo]],
|
|
platform: str,
|
|
supplementary_classifiers: Optional[List[str]] = None,
|
|
override_channel_name: str = None):
|
|
self._coordinator = device_list_coordinator
|
|
self._device = device
|
|
self._channel_id = channel
|
|
self._last_http_state = None
|
|
self._cb_async_remove_listener = None
|
|
|
|
base_name = f"{device.name} ({device.type})"
|
|
if supplementary_classifiers is not None:
|
|
self._id = calculate_id(platform=platform, uuid=device.internal_id, channel=channel,
|
|
supplementary_classifiers=supplementary_classifiers)
|
|
base_name += f" " + " ".join(supplementary_classifiers)
|
|
else:
|
|
self._id = calculate_id(platform=platform, uuid=device.internal_id, channel=channel)
|
|
|
|
if override_channel_name:
|
|
channel_name = override_channel_name
|
|
elif device.channels is not None and len(device.channels) > 0:
|
|
channel_data = device.channels[channel]
|
|
channel_name = channel_data.name
|
|
else:
|
|
channel_name = None
|
|
self._entity_name = f"{base_name} - {channel_name}" if channel_name is not None else base_name
|
|
|
|
@property
|
|
def should_poll(self) -> bool:
|
|
return False
|
|
|
|
async def async_update(self):
|
|
if self.online:
|
|
try:
|
|
await self._device.async_update()
|
|
except CommandTimeoutError as e:
|
|
log_exception(logger=_LOGGER, device=self._device)
|
|
|
|
def _http_data_changed(self) -> None:
|
|
new_data = self._coordinator.data.get(self._device.uuid)
|
|
if self._last_http_state is not None and self._last_http_state.online_status != OnlineStatus.ONLINE and new_data.online_status == OnlineStatus.ONLINE:
|
|
self._last_http_state = new_data
|
|
self.async_schedule_update_ha_state(force_refresh=True)
|
|
else:
|
|
self._last_http_state = new_data
|
|
self.async_schedule_update_ha_state(force_refresh=False)
|
|
|
|
@property
|
|
def online(self) -> bool:
|
|
if not self._coordinator.last_update_success:
|
|
return False
|
|
elif self._last_http_state is not None:
|
|
return self._last_http_state.online_status == OnlineStatus.ONLINE
|
|
else:
|
|
return self._coordinator.data.get(self._device.uuid).online_status == OnlineStatus.ONLINE
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
return self._id
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._entity_name
|
|
|
|
@property
|
|
def device_info(self):
|
|
return {
|
|
'identifiers': {(DOMAIN, self._device.internal_id)},
|
|
'name': self._device.name,
|
|
'manufacturer': 'Meross',
|
|
'model': self._device.type + " " + self._device.hardware_version,
|
|
'sw_version': self._device.firmware_version
|
|
}
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._coordinator.last_update_success and self.online
|
|
|
|
async def _async_push_notification_received(self, namespace: Namespace, data: dict, device_internal_id: str):
|
|
update_state = False
|
|
full_update = False
|
|
|
|
if namespace == Namespace.CONTROL_UNBIND:
|
|
_LOGGER.warning(f"Received unbind event. Removing device %s from HA", self.name)
|
|
await self.platform.async_remove_entity(self.entity_id)
|
|
elif namespace == Namespace.SYSTEM_ONLINE:
|
|
_LOGGER.info(f"Device %s reported online event.", self.name)
|
|
online = OnlineStatus(int(data.get('online').get('status')))
|
|
update_state = True
|
|
full_update = online == OnlineStatus.ONLINE
|
|
elif namespace == Namespace.HUB_ONLINE:
|
|
_LOGGER.info(f"Device {self.name} reported (HUB) online event.")
|
|
online = OnlineStatus(int(data.get('status')))
|
|
update_state = True
|
|
full_update = online == OnlineStatus.ONLINE
|
|
else:
|
|
update_state = True
|
|
full_update = False
|
|
|
|
# In all other cases, just tell HA to update the internal state representation
|
|
if update_state:
|
|
self.async_schedule_update_ha_state(force_refresh=full_update)
|
|
|
|
async def async_added_to_hass(self) -> None:
|
|
self._device.register_push_notification_handler_coroutine(self._async_push_notification_received)
|
|
self._cb_async_remove_listener = self._coordinator.async_add_listener(self._http_data_changed)
|
|
self.hass.data[DOMAIN]["ADDED_ENTITIES_IDS"].add(self.unique_id)
|
|
|
|
async def async_will_remove_from_hass(self) -> None:
|
|
self._device.unregister_push_notification_handler_coroutine(self._async_push_notification_received)
|
|
if self._cb_async_remove_listener is not None:
|
|
self._cb_async_remove_listener()
|
|
self.hass.data[DOMAIN]["ADDED_ENTITIES_IDS"].remove(self.unique_id)
|
|
|
|
|
|
async def get_or_test_creds(
|
|
creds: MerossCloudCreds = None,
|
|
http_api_url: str = MEROSS_DEFAULT_CLOUD_API_URL,
|
|
ua_header: str = DEFAULT_USER_AGENT
|
|
) -> Tuple[MerossHttpClient, List[HttpDeviceInfo], bool]:
|
|
|
|
renewed = False
|
|
http_client = MerossHttpClient(
|
|
cloud_credentials=creds, ua_header=ua_header
|
|
)
|
|
|
|
# The local addon api might not be able to provide the correct domain and mqtt values. In this case,
|
|
# we patch them here.
|
|
if http_api_url is not None and (http_client.cloud_credentials.domain is None or creds.domain.lower().strip() == MEROSS_DEFAULT_CLOUD_API_URL):
|
|
_LOGGER.warning("Returned/Stored DOMAIN within existing credentials is <%s>. Patching the stored credentials with the correct value right away.", http_client.cloud_credentials.domain)
|
|
http_client.cloud_credentials.domain = http_api_url
|
|
renewed = True
|
|
|
|
# Test device listing. If goes ok, return it immediately. This will make API fail and ask re-login
|
|
http_devices = await http_client.async_list_devices()
|
|
return http_client, http_devices, renewed
|
|
|
|
|
|
def _http_info_changed(known: Collection[HttpDeviceInfo], discovered: Collection[HttpDeviceInfo]) -> bool:
|
|
"""Tells when a new device is discovered among the known ones"""
|
|
known_ids = [dev.uuid for dev in known]
|
|
unknown = [dev for dev in discovered if dev.uuid not in known_ids]
|
|
return len(unknown) > 0
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
|
|
"""
|
|
This class is called by the HomeAssistant framework when a configuration entry is provided.
|
|
For us, the configuration entry is the username-password credentials that the user
|
|
needs to access the Meross cloud.
|
|
"""
|
|
|
|
# Retrieve the stored credentials from config-flow
|
|
http_api_endpoint = config_entry.data.get(CONF_HTTP_ENDPOINT)
|
|
_LOGGER.info("Loaded %s: %s", CONF_HTTP_ENDPOINT, http_api_endpoint)
|
|
|
|
str_creds = config_entry.data.get(CONF_STORED_CREDS)
|
|
_LOGGER.info("Loaded %s: %s", CONF_STORED_CREDS, "******")
|
|
|
|
mqtt_skip_cert_validation = config_entry.data.get(CONF_MQTT_SKIP_CERT_VALIDATION, True)
|
|
_LOGGER.info("Skip MQTT cert validation option set to: %s", mqtt_skip_cert_validation)
|
|
|
|
mqtt_override_address = config_entry.data.get(CONF_OVERRIDE_MQTT_ENDPOINT)
|
|
_LOGGER.info("Override MQTT address set to: %s", "no" if mqtt_override_address is None else "yes -> %s" % mqtt_override_address)
|
|
|
|
# Make sure we have all the needed requirements
|
|
if http_api_endpoint is None or HTTP_API_RE.fullmatch(http_api_endpoint) is None:
|
|
raise ConfigEntryAuthFailed("Missing or wrong HTTP_API_ENDPOINT")
|
|
if str_creds is None:
|
|
raise ConfigEntryAuthFailed("Missing credentials. Please re-authenticate.")
|
|
if mqtt_override_address is not None:
|
|
mqtt_host = mqtt_override_address.split(":")[0]
|
|
mqtt_port = int(mqtt_override_address.split(":")[1])
|
|
mqtt_override_address = (mqtt_host, mqtt_port)
|
|
|
|
issued_on = datetime.fromisoformat(str_creds.get("issued_on"))
|
|
creds = MerossCloudCreds(
|
|
domain=str_creds.get("domain", MEROSS_DEFAULT_CLOUD_API_URL),
|
|
mqtt_domain=str_creds.get("mqtt_domain"),
|
|
token=str_creds.get("token"),
|
|
key=str_creds.get("key"),
|
|
user_id=str_creds.get("user_id"),
|
|
user_email=str_creds.get("user_email"),
|
|
issued_on=issued_on
|
|
)
|
|
|
|
# Initialize the HASS structure
|
|
hass.data[DOMAIN] = {}
|
|
hass.data[DOMAIN]["ADDED_ENTITIES_IDS"] = set()
|
|
|
|
# Retrieve options we need
|
|
ua_header = config_entry.options.get(CONF_OPT_CUSTOM_USER_AGENT, DEFAULT_USER_AGENT)
|
|
if ua_header == "" or not isinstance(ua_header, str):
|
|
_LOGGER.warning("Invalid user-agent option specified in config <%s>; defaulting to <%s>", str(ua_header),
|
|
str(DEFAULT_USER_AGENT))
|
|
ua_header = DEFAULT_USER_AGENT
|
|
|
|
try:
|
|
# Setup the coordinator
|
|
meross_coordinator = MerossCoordinator(
|
|
hass=hass,
|
|
config_entry=config_entry,
|
|
http_api_endpoint=http_api_endpoint,
|
|
creds=creds,
|
|
mqtt_skip_cert_validation=mqtt_skip_cert_validation,
|
|
mqtt_override_address=mqtt_override_address,
|
|
update_interval=timedelta(seconds=HTTP_UPDATE_INTERVAL),
|
|
ua_header=ua_header
|
|
)
|
|
|
|
# Initiate the coordinator. This method will also make sure to login to the API,
|
|
# instantiates the manager, starts it and issues a first discovery.
|
|
await meross_coordinator.initial_setup()
|
|
manager = meross_coordinator.manager
|
|
hass.data[DOMAIN][MANAGER] = manager
|
|
hass.data[DOMAIN][DEVICE_LIST_COORDINATOR] = meross_coordinator
|
|
|
|
# Once the manager is ok and the first discovery was issued, we can proceed with platforms setup.
|
|
await hass.config_entries.async_forward_entry_setups(config_entry, MEROSS_PLATFORMS)
|
|
|
|
def _http_api_polled(*args, **kwargs):
|
|
# Whenever a new HTTP device is seen, we issue a discovery
|
|
discovered_devices = meross_coordinator.data
|
|
known_devices = manager.find_devices(device_uuids=discovered_devices.keys())
|
|
if _http_info_changed(known_devices, discovered_devices.values()):
|
|
_LOGGER.info("The HTTP API has found new devices that were unknown to us. Triggering discovery.")
|
|
hass.create_task(manager.async_device_discovery(update_subdevice_status=True,
|
|
cached_http_device_list=discovered_devices.values()))
|
|
|
|
# Register a handler for HTTP events so that we can check for new devices and trigger
|
|
# a discovery when needed
|
|
config_entry.async_on_unload(meross_coordinator.async_add_listener(_http_api_polled))
|
|
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
|
|
|
|
return True
|
|
|
|
except TooManyTokensException:
|
|
msg = (
|
|
"Too many tokens have been issued to this account. "
|
|
"The Remote API refused to issue a new one."
|
|
)
|
|
notify_error(hass, "http_connection", "Meross Cloud", msg)
|
|
log_exception(msg, logger=_LOGGER)
|
|
raise ConfigEntryAuthFailed("Too many tokens have been issued")
|
|
|
|
except (UnauthorizedException, HttpApiError) as ex:
|
|
# Do not retry setup: user must update its credentials
|
|
if ex is UnauthorizedException or ex.error_code in (
|
|
ErrorCodes.CODE_TOKEN_INVALID,
|
|
ErrorCodes.CODE_TOKEN_EXPIRED,
|
|
ErrorCodes.CODE_TOKEN_ERROR,
|
|
):
|
|
raise ConfigEntryAuthFailed("Invalid token or credentials")
|
|
else:
|
|
msg = "Your Meross login credentials are invalid or the network could not be reached at the moment."
|
|
notify_error(
|
|
hass,
|
|
"http_connection",
|
|
"Meross Cloud",
|
|
"Could not connect to the Meross cloud. Please check"
|
|
" your internet connection and your Meross credentials",
|
|
)
|
|
log_exception(msg, logger=_LOGGER)
|
|
raise ConfigEntryNotReady()
|
|
|
|
|
|
async def update_listener(hass, entry):
|
|
"""Handle options update."""
|
|
# Update options
|
|
custom_ua = entry.options.get(CONF_OPT_CUSTOM_USER_AGENT, DEFAULT_USER_AGENT)
|
|
transport_mode = entry.options.get(CONF_OPT_LAN, CONF_OPT_LAN_MQTT_ONLY)
|
|
manager_transport_mode = TRANSPORT_MODES_TO_ENUM[transport_mode]
|
|
manager: MerossManager = hass.data[DOMAIN][MANAGER]
|
|
manager.default_transport_mode = manager_transport_mode
|
|
# So far, the underlying Meross Library requires some "monkey patching" to set the
|
|
# http user agent to be used. It's not nice, but until a public setter gets exposed, we need
|
|
# to do so.
|
|
manager._http_client._ua_header = custom_ua
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
"""Unload a config entry."""
|
|
# Unload entities first
|
|
_LOGGER.info("Removing Meross Cloud integration.")
|
|
_LOGGER.info("Cleaning up resources...")
|
|
|
|
for platform in MEROSS_PLATFORMS:
|
|
_LOGGER.info(f"Cleaning up platform {platform}")
|
|
await hass.config_entries.async_forward_entry_unload(entry, platform)
|
|
|
|
_LOGGER.info("Stopping manager...")
|
|
manager = hass.data[DOMAIN][MANAGER]
|
|
# TODO: Invalidate the token?
|
|
manager.close()
|
|
|
|
_LOGGER.info("Cleaning up memory...")
|
|
for plat in MEROSS_PLATFORMS:
|
|
if plat in hass.data[DOMAIN]:
|
|
hass.data[DOMAIN][plat].clear()
|
|
del hass.data[DOMAIN][plat]
|
|
del hass.data[DOMAIN][MANAGER]
|
|
hass.data[DOMAIN].clear()
|
|
del hass.data[DOMAIN]
|
|
|
|
_LOGGER.info("Meross cloud component removal done.")
|
|
return True
|
|
|
|
|
|
async def async_remove_entry(hass, entry) -> None:
|
|
# TODO
|
|
pass
|
|
|
|
|
|
async def async_setup(hass, config):
|
|
"""
|
|
This method gets called if HomeAssistant has a valid meross_cloud: configuration entry within
|
|
configurations.yaml.
|
|
|
|
Thus, in this method we simply trigger the creation of a config entry.
|
|
|
|
:return:
|
|
"""
|
|
|
|
conf = config.get(DOMAIN)
|
|
hass.data[DOMAIN] = {}
|
|
hass.data[DOMAIN][ATTR_CONFIG] = conf
|
|
|
|
if conf is not None:
|
|
hass.async_create_task(
|
|
hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
|
|
)
|
|
)
|
|
|
|
return True
|