"""Intégration Réveil : planificateur d'événements de réveil.""" from __future__ import annotations import voluptuous as vol from pathlib import Path from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.components.http import StaticPathConfig from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import ( config_validation as cv, entity_registry as er, device_registry as dr, ) from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from .const import ( ATTR_DAY, ATTR_DAYS, ATTR_TIME, DAYS, DOMAIN, PLATFORMS, SERVICE_DISMISS, SERVICE_SET_DAYS, SERVICE_SET_TIME, STORAGE_VERSION, ) from .manager import ReveilConfigEntry, ReveilManager CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) _TARGET_FIELDS = { vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, } SET_TIME_SCHEMA = vol.Schema( { **_TARGET_FIELDS, vol.Optional(ATTR_DAY): vol.In(DAYS), vol.Optional(ATTR_TIME): cv.time, }, extra=vol.ALLOW_EXTRA, ) SET_DAYS_SCHEMA = vol.Schema( { **_TARGET_FIELDS, vol.Required(ATTR_DAYS): vol.All(cv.ensure_list, [vol.In(DAYS)]), }, extra=vol.ALLOW_EXTRA, ) DISMISS_SCHEMA = vol.Schema(_TARGET_FIELDS, extra=vol.ALLOW_EXTRA) def _resolve_managers(hass: HomeAssistant, call: ServiceCall) -> list[ReveilManager]: """Retrouve les réveils ciblés (par appareil ou par entité).""" device_ids: set[str] = set(call.data.get(ATTR_DEVICE_ID, [])) ent_reg = er.async_get(hass) for entity_id in call.data.get(ATTR_ENTITY_ID, []): entity = ent_reg.async_get(entity_id) if entity is None or entity.device_id is None: raise ServiceValidationError(f"L'entité {entity_id} n'appartient à aucun réveil") device_ids.add(entity.device_id) if not device_ids: raise ServiceValidationError("Cible manquante : indiquez un appareil ou une entité Réveil") dev_reg = dr.async_get(hass) managers: list[ReveilManager] = [] for device_id in device_ids: device = dev_reg.async_get(device_id) if device is None: raise ServiceValidationError(f"Appareil inconnu : {device_id}") for entry_id in device.config_entries: entry = hass.config_entries.async_get_entry(entry_id) if ( entry is not None and entry.domain == DOMAIN and entry.state is ConfigEntryState.LOADED ): managers.append(entry.runtime_data) if not managers: raise ServiceValidationError("Aucun réveil actif trouvé pour cette cible") return managers async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Enregistre les services (une seule fois pour tous les réveils).""" async def _register_card_resource(event): lovelace = hass.data.get("lovelace") if not lovelace or not hasattr(lovelace, "resources"): _LOGGER.warning( "Lovelace resources indisponibles, carte non enregistrée" ) return resources = lovelace.resources card_url = f"/api/{DOMAIN}/reveil-card.js" existing = [r for r in resources.async_items() if r.get("url", "").startswith(card_url)] if not existing: await resources.async_create_item({ "url": card_url, "res_type": "module", }) hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STARTED, _register_card_resource ) async def handle_set_time(call: ServiceCall) -> None: day = call.data.get(ATTR_DAY) value = call.data.get(ATTR_TIME) if day is None and value is None: raise ServiceValidationError( "Indiquez une heure (et éventuellement un jour), " "ou un jour seul pour effacer sa surcharge" ) for manager in _resolve_managers(hass, call): await manager.async_set_time(day, value) async def handle_set_days(call: ServiceCall) -> None: for manager in _resolve_managers(hass, call): await manager.async_set_days(call.data[ATTR_DAYS]) async def handle_dismiss(call: ServiceCall) -> None: for manager in _resolve_managers(hass, call): await manager.async_dismiss() hass.services.async_register(DOMAIN, SERVICE_SET_TIME, handle_set_time, schema=SET_TIME_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_SET_DAYS, handle_set_days, schema=SET_DAYS_SCHEMA) hass.services.async_register(DOMAIN, SERVICE_DISMISS, handle_dismiss, schema=DISMISS_SCHEMA) return True async def async_setup_entry(hass: HomeAssistant, entry: ReveilConfigEntry) -> bool: """Crée un réveil à partir d'une config entry.""" manager = ReveilManager(hass, entry) await manager.async_load() entry.runtime_data = manager await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) manager.async_start() entry.async_on_unload(manager.async_stop) entry.async_on_unload(entry.add_update_listener(_async_reload_on_update)) if len(hass.data[DOMAIN]) == 1: www_path = Path(__file__).parent / "www" card_file = www_path / "reveil-card.js" await hass.http.async_register_static_paths([ StaticPathConfig(f"/api/{DOMAIN}/reveil-card.js", str(card_file), cache_headers=False) ]) return True async def async_unload_entry(hass: HomeAssistant, entry: ReveilConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Supprime l'état persistant du réveil.""" await Store(hass, STORAGE_VERSION, f"{DOMAIN}.{entry.entry_id}").async_remove() async def _async_reload_on_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """Recharge le réveil quand ses options changent (durée de progression).""" await hass.config_entries.async_reload(entry.entry_id)