From ea7025cd7c3339081dc5a265530636edb97a451e Mon Sep 17 00:00:00 2001 From: ualex73 Date: Sun, 26 Jan 2025 11:34:29 +0100 Subject: [PATCH] WIP: Reworked entry load/unload --- custom_components/monitor_docker/__init__.py | 95 +++------- .../monitor_docker/config_flow.py | 4 +- custom_components/monitor_docker/helpers.py | 175 +++++++++--------- 3 files changed, 113 insertions(+), 161 deletions(-) diff --git a/custom_components/monitor_docker/__init__.py b/custom_components/monitor_docker/__init__.py index 454cd06..b409f12 100644 --- a/custom_components/monitor_docker/__init__.py +++ b/custom_components/monitor_docker/__init__.py @@ -15,6 +15,7 @@ Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.reload import async_setup_reload_service @@ -182,90 +183,44 @@ async def RunDocker(hass: HomeAssistant, entry: ConfigType) -> None: ################################################################# -async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up this integration using UI.""" - async def RunDocker(hass: HomeAssistant, entry: ConfigType) -> None: - """Wrapper around function for a separated thread.""" - - # Create docker instance, it will have asyncio threads - hass.data[DOMAIN][entry[CONF_NAME]] = {} - hass.data[DOMAIN][entry[CONF_NAME]][CONFIG] = entry - - startCount = 0 - - while True: - doLoop = True - - try: - hass.data[DOMAIN][entry[CONF_NAME]][API] = DockerAPI(hass, entry) - await hass.data[DOMAIN][entry[CONF_NAME]][API].init(startCount) - await hass.data[DOMAIN][entry[CONF_NAME]][API].run() - await hass.config_entries.async_forward_entry_setups( - config_entry, PLATFORMS - ) - # await hass.data[DOMAIN][entry[CONF_NAME]][API].load() - except Exception as err: - doLoop = False - if entry[CONF_RETRY] == 0: - raise - else: - _LOGGER.error("Failed Docker connect: %s", str(err)) - _LOGGER.error("Retry in %d seconds", entry[CONF_RETRY]) - await asyncio.sleep(entry[CONF_RETRY]) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} - startCount += 1 + api = None - if doLoop: - # Now run forever in this separated thread - # loop.run_forever() + try: + api = DockerAPI(hass, entry.data) + await api.init() + await api.run() - # We only get here if a docker instance disconnected or HASS is stopping - if not hass.data[DOMAIN][entry[CONF_NAME]][API]._dockerStopped: - # If HASS stopped, do not retry - break + hass.data[DOMAIN][entry.data[CONF_NAME]] = {} + hass.data[DOMAIN][entry.data[CONF_NAME]][CONFIG] = entry.data + hass.data[DOMAIN][entry.data[CONF_NAME]][API] = api - # config_entry.async_on_unload(config_entry.add_update_listener(async_reload_entry)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + except Exception as err: + _LOGGER.error("[%s]: Failed to setup, error=%s", entry.data[CONF_NAME],str(err)) - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} + if api: + await api.destroy() - # For now this is disabled as selection of conditions is made in the UI. - # - # # Default MONITORED_CONDITIONS_LIST also contains allinone, so we need to fix it up here - # if len(config_entry.data[CONF_MONITORED_CONDITIONS]) == 0: - # # Add whole list, including allinone. Make a copy, no reference - # config_entry.data[CONF_MONITORED_CONDITIONS] = MONITORED_CONDITIONS_LIST.copy() - # # remove the allinone - # config_entry.data[CONF_MONITORED_CONDITIONS].remove(CONTAINER_INFO_ALLINONE) - # - # # Check if CONF_MONITORED_CONDITIONS has only ALLINONE, then expand to all - # if ( - # len(config_entry.data[CONF_MONITORED_CONDITIONS]) == 1 - # and CONTAINER_INFO_ALLINONE in config_entry.data[CONF_MONITORED_CONDITIONS] - # ): - # config_entry.data[CONF_MONITORED_CONDITIONS] = list(MONITORED_CONDITIONS_LIST) + list( - # [CONTAINER_INFO_ALLINONE] - # ) - - # Will not work with reconfigure since it will have the same name, and unique name is already ensured in config_flow - # if config_entry.data[CONF_NAME] in hass.data[DOMAIN]: - # _LOGGER.error( - # "Instance %s is duplicate, please assign an unique name", - # config_entry.data[CONF_NAME], - # ) - # return False - - # Each docker hosts runs in its own thread. We need to pass hass too, for the load_platform - asyncio.create_task(RunDocker(hass, config_entry.data)) + raise ConfigEntryNotReady(f"Failed to setup {err}") from err return True ################################################################# -async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) + + _LOGGER.debug("async_unload_entry") + + await hass.data[DOMAIN][entry.data[CONF_NAME]][API].destroy() + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) ################################################################# diff --git a/custom_components/monitor_docker/config_flow.py b/custom_components/monitor_docker/config_flow.py index 4079927..7a87307 100644 --- a/custom_components/monitor_docker/config_flow.py +++ b/custom_components/monitor_docker/config_flow.py @@ -102,8 +102,8 @@ async def async_step_user( # Test connection to Docker try: self._docker_api = DockerAPI(self.hass, user_input) - if not await self._docker_api.init(): - errors["base"] = "invalid_connection" + await self._docker_api.init() + #errors["base"] = "invalid_connection" except Exception as e: # pylint: disable=broad-except _LOGGER.exception("Unhandled exception in user step") errors["base"] = str(e) diff --git a/custom_components/monitor_docker/helpers.py b/custom_components/monitor_docker/helpers.py index 1dd702c..eda7848 100644 --- a/custom_components/monitor_docker/helpers.py +++ b/custom_components/monitor_docker/helpers.py @@ -108,6 +108,10 @@ def __init__(self, hass: HomeAssistant, config: ConfigType): self._subscribers: list[Callable] = [] self._api: aiodocker.Docker = None + self._tcp_connector = None + self._tcp_session = None + self._tcp_ssl_context = None + _LOGGER.debug("[%s]: Helper version: %s", self._instance, VERSION) self._interval: int = config[CONF_SCAN_INTERVAL] @@ -120,113 +124,104 @@ def __init__(self, hass: HomeAssistant, config: ConfigType): ) ############################################################# - async def init(self, startCount: int = 0) -> bool: + async def init(self, startCount: int = 0): # Set to None when called twice, etc self._api = None _LOGGER.debug("[%s]: DockerAPI init()", self._instance) - try: - # Try to fix unix:// to unix:/// (3 are required by aiodocker) - url: str = self._config[CONF_URL] - if ( - url is not None - and url.find("unix://") == 0 - and url.find("unix:///") == -1 - ): - url = url.replace("unix://", "unix:///") + # Get URL + url: str = self._config[CONF_URL] - # When we reconnect with tcp, we should delay - docker is maybe not fully ready - if startCount > 0 and url is not None and url.find("unix:") != 0: - await asyncio.sleep(5) + # HA sometimes makes the url="", which aiodocker does not like + if url is not None and url == "": + url = None - # Check if it is a tcp connection or not - tcpConnection = False + # Try to fix unix:// to unix:/// (3 are required by aiodocker) + if url is not None and url.find("unix://") == 0 and url.find("unix:///") == -1: + url = url.replace("unix://", "unix:///") - # Remove Docker environment variables - os.environ.pop("DOCKER_TLS_VERIFY", None) - os.environ.pop("DOCKER_CERT_PATH", None) + # When we reconnect with tcp, we should delay - docker is maybe not fully ready + if startCount > 0 and url is not None and url.find("unix:") != 0: + await asyncio.sleep(5) - # Setup Docker parameters - connector = None - session = None - ssl_context = None + # Remove Docker environment variables + os.environ.pop("DOCKER_TLS_VERIFY", None) + os.environ.pop("DOCKER_CERT_PATH", None) - if url is not None: - _LOGGER.debug("[%s]: Docker URL is '%s'", self._instance, url) - else: - _LOGGER.debug( - "[%s]: Docker URL is auto-detect (most likely using 'unix://var/run/docker.socket')", - self._instance, - ) + # Setup Docker parameters + self._tcp_connector = None + self._tcp_session = None + self._tcp_ssl_context = None - # If is not empty or an Unix socket, then do check TCP/SSL - if url is not None and url.find("unix:") == -1: - # Check if URL is valid - if not ( - url.find("tcp:") == 0 - or url.find("http:") == 0 - or url.find("https:") == 0 - ): - raise ValueError( - f"[{self._instance}] Docker URL '{url}' does not start with tcp:, http: or https:" - ) + if url is not None: + _LOGGER.debug("[%s]: Docker URL is '%s'", self._instance, url) + else: + _LOGGER.debug( + "[%s]: Docker URL is auto-detect (most likely using 'unix://var/run/docker.socket')", + self._instance, + ) - if self._config[CONF_CERTPATH] and url.find("http:") == 0: - # fixup URL and warn - _LOGGER.warning( - "[%s] Docker URL '%s' should be https instead of http when using certificate path", - self._instance, - url, - ) - url = url.replace("http:", "https:") + # If is not empty or an Unix socket, then do check TCP/SSL + if url and url.find("unix:") == -1: + # Check if URL is valid + if not ( + url.find("tcp:") == 0 + or url.find("http:") == 0 + or url.find("https:") == 0 + ): + raise ValueError( + f"[{self._instance}] Docker URL '{url}' does not start with tcp:, http: or https:" + ) - if self._config[CONF_CERTPATH] and url.find("tcp:") == 0: - # fixup URL and warn - _LOGGER.warning( - "[%s] Docker URL '%s' should be https instead of tcp when using certificate path", - self._instance, - url, - ) - url = url.replace("tcp:", "https:") + if self._config[CONF_CERTPATH] and url.find("http:") == 0: + # fixup URL and warn + _LOGGER.warning( + "[%s] Docker URL '%s' should be https instead of http when using certificate path", + self._instance, + url, + ) + url = url.replace("http:", "https:") - if self._config[CONF_CERTPATH]: - _LOGGER.debug( - "[%s]: Docker certification path is '%s' SSL/TLS will be used", - self._instance, - self._config[CONF_CERTPATH], - ) + if self._config[CONF_CERTPATH] and url.find("tcp:") == 0: + # fixup URL and warn + _LOGGER.warning( + "[%s] Docker URL '%s' should be https instead of tcp when using certificate path", + self._instance, + url, + ) + url = url.replace("tcp:", "https:") - # Create our SSL context object - ssl_context = await self._hass.async_add_executor_job( - self._docker_ssl_context - ) + if self._config[CONF_CERTPATH]: + _LOGGER.debug( + "[%s]: Docker certification path is '%s' SSL/TLS will be used", + self._instance, + self._config[CONF_CERTPATH], + ) - # Setup new TCP connection, otherwise timeout takes toooo long - connector = TCPConnector(ssl=ssl_context) - session = ClientSession( - connector=connector, - timeout=ClientTimeout( - connect=5, - sock_connect=5, - total=10, - ), + # Create our SSL context object + self._tcp_ssl_context = await self._hass.async_add_executor_job( + self._docker_ssl_context ) - # Initiate the aiodocker instance now - self._api = aiodocker.Docker( - url=url, connector=connector, session=session, ssl_context=ssl_context + # Setup new TCP connection, otherwise timeout takes toooo long + self._tcp_connector = TCPConnector(ssl=self._tcp_ssl_context) + self._tcp_session = ClientSession( + connector=self._tcp_connector, + timeout=ClientTimeout( + connect=5, + sock_connect=5, + total=10, + ), ) - except Exception as err: - exc_info = True if str(err) == "" else False - _LOGGER.error( - "[%s]: Can not connect to Docker API (%s)", - self._instance, - str(err), - exc_info=exc_info, - ) - return False + # Initiate the aiodocker instance now. Could raise an exception + self._api = aiodocker.Docker( + url=url, + connector=self._tcp_connector, + session=self._tcp_session, + ssl_context=self._tcp_ssl_context, + ) versionInfo = await self._api.version() version: str | None = versionInfo.get("Version", None) @@ -245,8 +240,6 @@ async def init(self, startCount: int = 0) -> bool: # Add container name to the list self._containers[cname] = None - return True - ############################################################# async def run(self): @@ -326,6 +319,10 @@ async def destroy(self) -> None: await container.destroy() # TBD clear container from list? + # Close session if initialized + if self._tcp_session: + self._tcp_session.detach() + # Clear api value # self._api = None