"""The Notification Store integration.""" from __future__ import annotations from datetime import timedelta from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from .const import ( ATTR_COUNT, DEFAULT_NAME, DEFAULT_SCAN_INTERVAL, DEFAULT_STORE_PATH, DOMAIN, EVENT_NOTIFICATION_ADDED, EVENT_NOTIFICATION_READ, EVENT_NOTIFICATIONS_CLEARED, FIELD_ENTRY_ID, FIELD_ICON, FIELD_INDEX, FIELD_LEVEL, FIELD_MESSAGE, FIELD_NOTIFICATION_ID, FIELD_SOURCE, FIELD_TAG, FIELD_TITLE, LEVEL_INFO, LEVELS, PLATFORMS, SERVICE_ADD_NOTIFICATION, SERVICE_CLEAR_ALL, SERVICE_CLEAR_READ, SERVICE_MARK_ALL_READ, SERVICE_MARK_INDEX_READ, SERVICE_MARK_ONE_READ, SERVICE_RELOAD, CONF_SCAN_INTERVAL, CONF_STORE_PATH, ) from .coordinator import NotificationStoreCoordinator from .store import NotificationStore SERVICE_ADD_SCHEMA = vol.Schema( { vol.Required(FIELD_TITLE): cv.string, vol.Required(FIELD_MESSAGE): cv.string, vol.Optional(FIELD_LEVEL, default=LEVEL_INFO): vol.In(LEVELS), vol.Optional(FIELD_TAG, default=""): cv.string, vol.Optional(FIELD_SOURCE, default=""): cv.string, vol.Optional(FIELD_ICON, default=""): cv.string, vol.Optional(FIELD_ENTRY_ID): cv.string, } ) SERVICE_MARK_ONE_SCHEMA = vol.Schema( { vol.Required(FIELD_NOTIFICATION_ID): cv.string, vol.Optional(FIELD_ENTRY_ID): cv.string, } ) SERVICE_MARK_INDEX_SCHEMA = vol.Schema( { vol.Required(FIELD_INDEX): vol.Coerce(int), vol.Optional(FIELD_ENTRY_ID): cv.string, } ) SERVICE_OPTIONAL_ENTRY_SCHEMA = vol.Schema({vol.Optional(FIELD_ENTRY_ID): cv.string}) def _entry_data(entry: ConfigEntry) -> dict[str, Any]: """Return merged config entry data and options.""" return {**entry.data, **entry.options} def _get_coordinator(hass: HomeAssistant, entry_id: str | None = None) -> NotificationStoreCoordinator: """Return a coordinator for a service call.""" domain_data = hass.data.get(DOMAIN, {}) coordinators = domain_data.get("coordinators", {}) if entry_id: if entry_id not in coordinators: raise ValueError(f"Unknown Notification Store entry_id: {entry_id}") return coordinators[entry_id] if not coordinators: raise ValueError("No Notification Store config entry is loaded") return next(iter(coordinators.values())) async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: """Set up services for Notification Store.""" hass.data.setdefault(DOMAIN, {"coordinators": {}, "services_registered": False}) if hass.data[DOMAIN].get("services_registered"): return True async def add_notification(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) notification = await coordinator.store.async_add_notification( title=call.data[FIELD_TITLE], message=call.data[FIELD_MESSAGE], level=call.data[FIELD_LEVEL], tag=call.data[FIELD_TAG], source=call.data[FIELD_SOURCE], icon=call.data[FIELD_ICON], ) await coordinator.async_request_refresh() hass.bus.async_fire(EVENT_NOTIFICATION_ADDED, notification) async def mark_all_read(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) changed = await coordinator.store.async_mark_all_read() await coordinator.async_request_refresh() hass.bus.async_fire(EVENT_NOTIFICATION_READ, {ATTR_COUNT: changed, "all": True}) async def mark_one_read(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) notification_id = call.data[FIELD_NOTIFICATION_ID] found = await coordinator.store.async_mark_one_read(notification_id) await coordinator.async_request_refresh() hass.bus.async_fire( EVENT_NOTIFICATION_READ, {FIELD_NOTIFICATION_ID: notification_id, "found": found}, ) async def mark_index_read(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) notification_id = await coordinator.store.async_mark_index_read(call.data[FIELD_INDEX]) await coordinator.async_request_refresh() hass.bus.async_fire( EVENT_NOTIFICATION_READ, {FIELD_INDEX: call.data[FIELD_INDEX], FIELD_NOTIFICATION_ID: notification_id}, ) async def clear_read(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) removed = await coordinator.store.async_clear_read() await coordinator.async_request_refresh() hass.bus.async_fire( EVENT_NOTIFICATIONS_CLEARED, {ATTR_COUNT: removed, "mode": "read"}, ) async def clear_all(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) removed = await coordinator.store.async_clear_all() await coordinator.async_request_refresh() hass.bus.async_fire( EVENT_NOTIFICATIONS_CLEARED, {ATTR_COUNT: removed, "mode": "all"}, ) async def reload_store(call: ServiceCall) -> None: coordinator = _get_coordinator(hass, call.data.get(FIELD_ENTRY_ID)) await coordinator.async_request_refresh() hass.services.async_register(DOMAIN, SERVICE_ADD_NOTIFICATION, add_notification, schema=SERVICE_ADD_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_MARK_ALL_READ, mark_all_read, schema=SERVICE_OPTIONAL_ENTRY_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_MARK_ONE_READ, mark_one_read, schema=SERVICE_MARK_ONE_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_MARK_INDEX_READ, mark_index_read, schema=SERVICE_MARK_INDEX_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_CLEAR_READ, clear_read, schema=SERVICE_OPTIONAL_ENTRY_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_CLEAR_ALL, clear_all, schema=SERVICE_OPTIONAL_ENTRY_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_RELOAD, reload_store, schema=SERVICE_OPTIONAL_ENTRY_SCHEMA) hass.data[DOMAIN]["services_registered"] = True return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Notification Store from a config entry.""" data = _entry_data(entry) store_path = data.get(CONF_STORE_PATH, DEFAULT_STORE_PATH) scan_interval = max(5, int(data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))) store = NotificationStore(hass, store_path) coordinator = NotificationStoreCoordinator( hass, store, update_interval=timedelta(seconds=scan_interval), ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {"coordinators": {}, "services_registered": False}) hass.data[DOMAIN].setdefault("coordinators", {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options updates.""" await hass.config_entries.async_reload(entry.entry_id) 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: hass.data[DOMAIN].get("coordinators", {}).pop(entry.entry_id, None) return unload_ok