diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 1526864..14a5c70 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -35,11 +35,6 @@ jobs: - name: Run hassfest validation uses: home-assistant/actions/hassfest@master - - name: Print hassfest version - if: always() - run: | - echo "Hassfest version: $(hassfest --version)" - concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/custom_components/ha_text_ai/__init__.py b/custom_components/ha_text_ai/__init__.py index c41d001..eded54f 100644 --- a/custom_components/ha_text_ai/__init__.py +++ b/custom_components/ha_text_ai/__init__.py @@ -16,7 +16,7 @@ import asyncio import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_NAME, EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -379,6 +379,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Register update listener for options changes entry.async_on_unload(entry.add_update_listener(async_update_options)) + # HA Core stop does not unload entries, so close the dedicated + # session on the CLOSE event too; unload removes this listener + # and closes the session via APIClient.shutdown() instead. + async def _async_close_session_on_stop(_event) -> None: + if not session.closed: + await session.close() + + entry.async_on_unload( + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_session_on_stop + ) + ) + _LOGGER.debug("Setup completed for %s", instance_name) return True diff --git a/custom_components/ha_text_ai/utils.py b/custom_components/ha_text_ai/utils.py index 887e4e2..3d069b3 100644 --- a/custom_components/ha_text_ai/utils.py +++ b/custom_components/ha_text_ai/utils.py @@ -17,7 +17,6 @@ from urllib.parse import urlparse import aiohttp from aiohttp.abc import AbstractResolver -from aiohttp.resolver import DefaultResolver from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant @@ -86,10 +85,8 @@ class _PinnedResolver(AbstractResolver): def __init__( self, pinned: dict[str, list[tuple[str, int]]], - fallback: AbstractResolver | None = None, ) -> None: self._pinned = pinned - self._fallback = fallback or DefaultResolver() async def resolve( self, @@ -99,7 +96,11 @@ class _PinnedResolver(AbstractResolver): ) -> list[dict[str, Any]]: entries = self._pinned.get(host.lower()) if entries is None: - return await self._fallback.resolve(host, port, family) + # Every request on a pinned session must target the validated + # host. Resolving anything else means a request escaped the pin + # (a new call site or a config bug) — fail closed rather than + # fall back to live, unvalidated DNS. + raise OSError(f"Refusing to resolve unpinned host: {host}") return [ { "hostname": host, @@ -113,7 +114,7 @@ class _PinnedResolver(AbstractResolver): ] async def close(self) -> None: - await self._fallback.close() + """Nothing to close; the resolver holds only the pinned map.""" def _family_for(ip: str) -> int: """Return AF_INET or AF_INET6 based on the IP literal."""