From bd44ef8645bec72c6e7104e8950631baaedbe434 Mon Sep 17 00:00:00 2001 From: Jay Stothard Date: Thu, 16 Jul 2026 05:08:59 -0700 Subject: [PATCH] fix(gateway): restore multiplex secondary adapters Partial cherry-pick of a7ffbbff7 from PR #63256: secondary-profile adapter creation errors no longer abort the whole secondary startup (try/except around _create_adapter + loud warning on None return), and Home Assistant's check_ha_requirements() becomes dep-only with the credential moved to a new validate_ha_config() so secondary profiles whose HASS_TOKEN lives in the profile secret scope are not silently dropped by the registry gate. Telegram diagnostic hunks and profile-label stamping dropped: the regression they targeted does not exist on current main and they conflict with the connect() teardown fence. --- gateway/run.py | 19 +++++++++++++++++-- plugins/platforms/homeassistant/adapter.py | 15 +++++++++------ tests/gateway/test_homeassistant.py | 21 ++++++++++++++++----- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 5b6a34370595..bf6a7d7b9245 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8828,9 +8828,24 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew f"'{profile_name}'s config.yaml (configure it only on the " f"default profile)." ) - with _profile_runtime_scope(profile_home): - adapter = self._create_adapter(platform, platform_config) + try: + with _profile_runtime_scope(profile_home): + adapter = self._create_adapter(platform, platform_config) + except Exception as e: + logger.error( + "[MULTIPLEX] Profile '%s': _create_adapter('%s') raised %s", + profile_name, + platform.value, + e, + exc_info=True, + ) + continue if not adapter: + logger.warning( + "[MULTIPLEX] Profile '%s': skipping platform '%s' - adapter creation returned None", + profile_name, + platform.value, + ) continue # Same-token conflict detection — refuse a duplicate poll. diff --git a/plugins/platforms/homeassistant/adapter.py b/plugins/platforms/homeassistant/adapter.py index 6d59a30c0b47..2168e3747c36 100644 --- a/plugins/platforms/homeassistant/adapter.py +++ b/plugins/platforms/homeassistant/adapter.py @@ -40,12 +40,14 @@ logger = logging.getLogger(__name__) def check_ha_requirements() -> bool: - """Check if Home Assistant dependencies are available and configured.""" - if not AIOHTTP_AVAILABLE: - return False - if not os.getenv("HASS_TOKEN"): - return False - return True + """Check if Home Assistant runtime dependencies are available.""" + return bool(AIOHTTP_AVAILABLE) + + +def validate_ha_config(config: PlatformConfig) -> bool: + """Accept either a scoped config token or an env-provided HASS_TOKEN.""" + token = getattr(config, "token", None) or os.getenv("HASS_TOKEN", "") + return bool(str(token or "").strip()) class HomeAssistantAdapter(BasePlatformAdapter): @@ -560,6 +562,7 @@ def register(ctx) -> None: label="Home Assistant", adapter_factory=_build_adapter, check_fn=check_ha_requirements, + validate_config=validate_ha_config, is_connected=_is_connected, required_env=["HASS_TOKEN"], install_hint="pip install aiohttp", diff --git a/tests/gateway/test_homeassistant.py b/tests/gateway/test_homeassistant.py index 8b6b83cf9cf4..4c5d0cf09644 100644 --- a/tests/gateway/test_homeassistant.py +++ b/tests/gateway/test_homeassistant.py @@ -17,6 +17,7 @@ from gateway.config import ( from plugins.platforms.homeassistant.adapter import ( HomeAssistantAdapter, check_ha_requirements, + validate_ha_config, ) @@ -26,12 +27,8 @@ from plugins.platforms.homeassistant.adapter import ( class TestCheckRequirements: - def test_returns_false_without_token(self, monkeypatch): + def test_returns_true_when_aiohttp_present(self, monkeypatch): monkeypatch.delenv("HASS_TOKEN", raising=False) - assert check_ha_requirements() is False - - def test_returns_true_with_token(self, monkeypatch): - monkeypatch.setenv("HASS_TOKEN", "test-token") assert check_ha_requirements() is True @patch("plugins.platforms.homeassistant.adapter.AIOHTTP_AVAILABLE", False) @@ -40,6 +37,20 @@ class TestCheckRequirements: assert check_ha_requirements() is False +class TestValidateConfig: + def test_returns_false_without_token_in_config_or_env(self, monkeypatch): + monkeypatch.delenv("HASS_TOKEN", raising=False) + assert validate_ha_config(PlatformConfig(enabled=True)) is False + + def test_returns_true_with_token_in_env(self, monkeypatch): + monkeypatch.setenv("HASS_TOKEN", "test-token") + assert validate_ha_config(PlatformConfig(enabled=True)) is True + + def test_returns_true_with_token_in_config(self, monkeypatch): + monkeypatch.delenv("HASS_TOKEN", raising=False) + assert validate_ha_config(PlatformConfig(enabled=True, token="cfg-token")) is True + + # --------------------------------------------------------------------------- # _format_state_change - pure function, all domain branches # ---------------------------------------------------------------------------