"""Spook - Your homie. Custom integration for Home Assistant.""" from __future__ import annotations import asyncio from typing import TYPE_CHECKING from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, RESTART_EXIT_CODE, ) from homeassistant.core import ( CoreState, callback, ) from homeassistant.helpers import issue_registry as ir from .automation_runs import async_setup_automation_runs from .const import DOMAIN, LOGGER, PLATFORMS from .entity_filtering import async_setup_all_entity_ids_cache_invalidation from .integration_linking import link_sub_integrations, unlink_sub_integrations from .listeners import async_listen_once_tracked from .repairs import SpookRepairManager from .run_history import async_setup_run_history from .services import SpookServiceManager from .setup_helpers import async_forward_setup_entry from .timed_states import async_setup_timed_states if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry from homeassistant.core import Event, HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" # Symlink all sub integrations from Spook to the parent integrations folder # if one is missing, we have to restart Home Assistant. # This is a workaround for the fact that Home Assistant doesn't support # sub integrations. if await hass.async_add_executor_job(link_sub_integrations, hass): LOGGER.debug("Newly symlinked sub integrations, restarting Home Assistant") @callback def _restart(_: Event | None = None) -> None: """Restart Home Assistant.""" hass.data["homeassistant_stop"] = asyncio.create_task( hass.async_stop(RESTART_EXIT_CODE), ) # User asked to restart Home Assistant in the config flow. if hass.data.get(DOMAIN) == "Boo!": _restart() return False # Should be OK to restart. Better to do it before anything else started. if hass.state == CoreState.starting: _restart() return False # If all other fails, but we are not running yet... wait for it. if hass.state == CoreState.not_running: # Listen to both... just in case. hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _restart) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _restart) return False LOGGER.info( "Home Assistant needs to be restarted in for Spook to complete setting up", ) ir.async_create_issue( hass=hass, domain=DOMAIN, issue_id="restart_required", is_fixable=True, severity=ir.IssueSeverity.WARNING, translation_key="restart_required", ) # Forward async_setup_entry to ectoplasms await async_forward_setup_entry(hass, entry) # Before the services, because the actions that hold an automation in a # state reach for this the moment they are called, and registering them # first leaves a window where calling one finds nothing there. # # This also picks up automations held before the last restart. An # automation keeps whatever state it had, so without this a snooze that # spanned a restart would be a disable nobody remembers making. entry.async_on_unload(await async_setup_timed_states(hass)) # Set up services services = SpookServiceManager(hass) await services.async_setup() entry.async_on_unload(services.async_on_unload) # Who you gonna call? SpookRepairManager! repairs = SpookRepairManager(hass) async def _ghost_busters(_: Event | None = None) -> None: """Send them in, time for some ghost chasing.""" await repairs.async_setup() entry.async_on_unload(repairs.async_on_unload) if hass.state == CoreState.running: # Home Assistant is already up, so the started event has been and gone. # This is the reload path, and setting up for the first time on a # running instance. Waiting for an event that cannot fire again would # leave every repair check dead until a core restart, silently. await _ghost_busters() else: # Otherwise wait for a settled instance, so repairs do not inspect a # half-loaded Home Assistant and report things that are still coming. entry.async_on_unload( async_listen_once_tracked(hass, EVENT_HOMEASSISTANT_STARTED, _ghost_busters) ) # Set up platforms await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Set up the all entity ids cache invalidation entry.async_on_unload(async_setup_all_entity_ids_cache_invalidation(hass)) # Start noting which automation runs under which context. It has to begin # here rather than when a condition first asks: a condition inside an # action sequence is only built once that sequence runs, by which point # the automation it wants to know about has already been and gone. entry.async_on_unload(async_setup_automation_runs(hass)) # And when they ran, for the conditions that count runs rather than # contexts. Same reason it cannot wait to be asked. entry.async_on_unload(async_setup_run_history(hass)) # Yay, we didn't got spooked! return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_remove_entry(hass: HomeAssistant, _: ConfigEntry) -> None: """Remove a config entry.""" await hass.async_add_executor_job(unlink_sub_integrations, hass)