"""The syncthing integration.""" import asyncio from asyncio import Task import logging import aiosyncthing from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, Platform, ) from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( DOMAIN, EVENTS, RECONNECT_INTERVAL, SERVER_AVAILABLE, SERVER_UNAVAILABLE, ) PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up syncthing from a config entry.""" data = entry.data if DOMAIN not in hass.data: hass.data[DOMAIN] = {} client = aiosyncthing.Syncthing( data[CONF_TOKEN], url=data[CONF_URL], verify_ssl=data[CONF_VERIFY_SSL], ) try: status = await client.system.status() except aiosyncthing.exceptions.SyncthingError as exception: await client.close() raise ConfigEntryNotReady from exception server_id = status["myID"] syncthing = SyncthingClient(hass, client, server_id) syncthing.subscribe() hass.data[DOMAIN][entry.entry_id] = syncthing await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(event: Event) -> None: """Cancel the listen task on Home Assistant stop.""" await syncthing.unsubscribe() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cancel_listen_task) ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: syncthing = hass.data[DOMAIN].pop(entry.entry_id) await syncthing.unsubscribe() return unload_ok class SyncthingClient: """A Syncthing client.""" def __init__( self, hass: HomeAssistant, client: aiosyncthing.Syncthing, server_id: str ) -> None: """Initialize the client.""" self._hass = hass self._client = client self._server_id = server_id self._listen_task: Task[None] | None = None @property def server_id(self) -> str: """Get server id.""" return self._server_id @property def url(self) -> str: """Get server URL.""" return self._client.url @property def database(self) -> aiosyncthing.Database: """Get database namespace client.""" return self._client.database @property def system(self) -> aiosyncthing.System: """Get system namespace client.""" return self._client.system def subscribe(self) -> None: """Start event listener coroutine.""" self._listen_task = asyncio.create_task(self._listen()) async def unsubscribe(self) -> None: """Stop event listener coroutine.""" if self._listen_task: self._listen_task.cancel() await self._client.close() async def _listen(self) -> None: """Listen to Syncthing events.""" events = self._client.events server_was_unavailable = False while True: if await self._server_available(): if server_was_unavailable: _LOGGER.warning( "The syncthing server '%s' is back online", self._client.url ) async_dispatcher_send( self._hass, f"{SERVER_AVAILABLE}-{self._server_id}" ) server_was_unavailable = False else: await asyncio.sleep(RECONNECT_INTERVAL.total_seconds()) continue try: async for event in events.listen(): if events.last_seen_id == 0: continue # skipping historical events from the first batch if event["type"] not in EVENTS: continue signal_name = EVENTS[event["type"]] folder = event["data"].get("folder") or event["data"]["id"] async_dispatcher_send( self._hass, f"{signal_name}-{self._server_id}-{folder}", event, ) except aiosyncthing.exceptions.SyncthingError: _LOGGER.warning( ( "The syncthing server '%s' is not available. Sleeping %i" " seconds and retrying" ), self._client.url, RECONNECT_INTERVAL.total_seconds(), ) async_dispatcher_send( self._hass, f"{SERVER_UNAVAILABLE}-{self._server_id}" ) await asyncio.sleep(RECONNECT_INTERVAL.total_seconds()) server_was_unavailable = True continue async def _server_available(self) -> bool: """Check if the Syncthing server is available.""" try: await self._client.system.ping() except aiosyncthing.exceptions.SyncthingError: return False return True