146 lines
5.7 KiB
Python
146 lines
5.7 KiB
Python
"""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)
|