Add service to reload recipes
This commit is contained in:
parent
7e3b4674c4
commit
f876730ead
|
|
@ -4,10 +4,12 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
|
||||
|
|
@ -106,6 +108,35 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
|
|||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, _register_card_resources
|
||||
)
|
||||
|
||||
async def _handle_reload(call: ServiceCall) -> None:
|
||||
"""cookcli.reload : resynchronise l'add-on puis rafraîchit la liste."""
|
||||
entries = hass.data.get(DOMAIN, {})
|
||||
requested = call.data.get("entry_id")
|
||||
|
||||
if requested is not None:
|
||||
if requested not in entries:
|
||||
raise ServiceValidationError(f"Serveur CookCLI inconnu : {requested}")
|
||||
targets = {requested: entries[requested]}
|
||||
else:
|
||||
targets = dict(entries)
|
||||
|
||||
for entry_id, data in targets.items():
|
||||
try:
|
||||
await data["client"].async_reload()
|
||||
except CookCliApiError as err:
|
||||
raise HomeAssistantError(f"Reload CookCLI impossible : {err}") from err
|
||||
await data["coordinator"].async_refresh()
|
||||
# Les cartes ouvertes écoutent cet événement pour recharger leur liste.
|
||||
hass.bus.async_fire(f"{DOMAIN}_reloaded", {"entry_id": entry_id})
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
"reload",
|
||||
_handle_reload,
|
||||
schema=vol.Schema({vol.Optional("entry_id"): str}),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,12 +58,14 @@ from contextlib import asynccontextmanager
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .const import RELOAD_PORT
|
||||
|
||||
import aiohttp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||
|
||||
RELOAD_TIMEOUT = aiohttp.ClientTimeout(total=120)
|
||||
|
||||
class CookCliApiError(Exception):
|
||||
"""Erreur générique lors d'un appel à l'API CookCLI."""
|
||||
|
|
@ -92,6 +94,7 @@ class CookCliApiClient:
|
|||
def __init__(self, session: aiohttp.ClientSession, host: str, port: int) -> None:
|
||||
self._session = session
|
||||
self._base_url = f"http://{host}:{port}/api"
|
||||
self._reload_url = f"http://{host}:{RELOAD_PORT}/reload"
|
||||
|
||||
async def async_test_connection(self) -> None:
|
||||
"""Vérifie que le serveur répond, utilisé par le config_flow."""
|
||||
|
|
@ -113,6 +116,10 @@ class CookCliApiClient:
|
|||
data = await self._request_json(f"/recipes/{path}")
|
||||
return _resolve_recipe(data)
|
||||
|
||||
async def async_reload(self) -> None:
|
||||
async with self._get(self._reload_url, method="POST", timeout=RELOAD_TIMEOUT):
|
||||
pass
|
||||
|
||||
async def async_get_image(self, image_ref: str) -> tuple[bytes, str | None]:
|
||||
"""Récupère les octets d'une image statique CookCLI.
|
||||
|
||||
|
|
@ -143,13 +150,16 @@ class CookCliApiClient:
|
|||
return f"/static/{image_ref}"
|
||||
|
||||
@asynccontextmanager
|
||||
async def _get(self, endpoint: str):
|
||||
"""Contexte partagé pour un GET vers CookCLI, avec gestion d'erreurs."""
|
||||
url = f"{self._base_url}{endpoint}"
|
||||
async def _get(
|
||||
self,
|
||||
endpoint: str,
|
||||
method: str = "GET",
|
||||
timeout: aiohttp.ClientTimeout = DEFAULT_TIMEOUT,
|
||||
):
|
||||
"""Contexte partagé pour un appel vers CookCLI, avec gestion d'erreurs."""
|
||||
url = endpoint if endpoint.startswith("http") else f"{self._base_url}{endpoint}"
|
||||
try:
|
||||
async with self._session.request(
|
||||
"GET", url, timeout=DEFAULT_TIMEOUT
|
||||
) as resp:
|
||||
async with self._session.request(method, url, timeout=timeout) as resp:
|
||||
if resp.status >= 400:
|
||||
raise CookCliResponseError(
|
||||
f"CookCLI a répondu {resp.status} pour {url}"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ CONF_HOST = "host"
|
|||
CONF_PORT = "port"
|
||||
|
||||
DEFAULT_PORT = 9081
|
||||
RELOAD_PORT = 8098
|
||||
|
||||
# Nom du service tel que poussé par bashio::discovery.set côté add-on
|
||||
HASSIO_DISCOVERY_SLUG = "cookcli"
|
||||
|
|
|
|||
7
custom_components/cookcli/services.yaml
Normal file
7
custom_components/cookcli/services.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
reload:
|
||||
fields:
|
||||
entry_id:
|
||||
required: false
|
||||
selector:
|
||||
config_entry:
|
||||
integration: cookcli
|
||||
|
|
@ -30,5 +30,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "Recharger les recettes",
|
||||
"description": "Resynchronise les recettes depuis le dépôt git de l'add-on.",
|
||||
"fields": {
|
||||
"entry_id": {
|
||||
"name": "Serveur CookCLI",
|
||||
"description": "Laisser vide pour recharger tous les serveurs."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "Reload recipes",
|
||||
"description": "Sync recipes from git repository in the addon.",
|
||||
"fields": {
|
||||
"entry_id": {
|
||||
"name": "CookCLI Server",
|
||||
"description": "Leave empty to reload all servers."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,5 +30,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"name": "Recharger les recettes",
|
||||
"description": "Resynchronise les recettes depuis le dépôt git de l'add-on.",
|
||||
"fields": {
|
||||
"entry_id": {
|
||||
"name": "Serveur CookCLI",
|
||||
"description": "Laisser vide pour recharger tous les serveurs."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ class CookCliCard extends HTMLElement {
|
|||
this._error = null;
|
||||
this._lastFetch = 0;
|
||||
this._selectedTag = null;
|
||||
this._reloading = false;
|
||||
this._reloadError = null;
|
||||
|
||||
if (!this.shadowRoot) {
|
||||
this.attachShadow({ mode: "open" });
|
||||
|
|
@ -77,6 +79,7 @@ class CookCliCard extends HTMLElement {
|
|||
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
this._subscribe();
|
||||
if (this._recipes === null && !this._loading) {
|
||||
this._fetchRecipes();
|
||||
}
|
||||
|
|
@ -136,6 +139,10 @@ class CookCliCard extends HTMLElement {
|
|||
}
|
||||
|
||||
_onClick(event) {
|
||||
if (event.target.closest("[data-action='reload']")) {
|
||||
this._reload();
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-tag-clear]")) {
|
||||
this._selectedTag = null;
|
||||
this._render();
|
||||
|
|
@ -193,6 +200,63 @@ class CookCliCard extends HTMLElement {
|
|||
return this._recipes.filter((r) => this._tagsOf(r).includes(this._selectedTag));
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._subscribe();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this._unsub) {
|
||||
this._unsub();
|
||||
this._unsub = null;
|
||||
}
|
||||
}
|
||||
|
||||
async _subscribe() {
|
||||
if (this._unsub || this._subscribing || !this._hass || !this.isConnected) return;
|
||||
this._subscribing = true;
|
||||
try {
|
||||
this._unsub = await this._hass.connection.subscribeEvents(
|
||||
(ev) => this._onReloaded(ev),
|
||||
"cookcli_reloaded"
|
||||
);
|
||||
} catch (err) {
|
||||
// Pas d'abonnement : le bouton retombe sur un rafraîchissement manuel.
|
||||
} finally {
|
||||
this._subscribing = false;
|
||||
}
|
||||
// La carte a pu être retirée du DOM pendant l'attente.
|
||||
if (!this.isConnected && this._unsub) {
|
||||
this._unsub();
|
||||
this._unsub = null;
|
||||
}
|
||||
}
|
||||
|
||||
_onReloaded(ev) {
|
||||
const target = this._config.entry_id;
|
||||
if (target && ev.data && ev.data.entry_id !== target) return;
|
||||
this._fetchRecipes();
|
||||
}
|
||||
|
||||
async _reload() {
|
||||
if (this._reloading || !this._hass) return;
|
||||
this._reloading = true;
|
||||
this._reloadError = null;
|
||||
this._render();
|
||||
try {
|
||||
const data = {};
|
||||
if (this._config.entry_id) data.entry_id = this._config.entry_id;
|
||||
await this._hass.callService("cookcli", "reload", data);
|
||||
// Normalement l'événement a déjà déclenché le rechargement ;
|
||||
// on ne le refait à la main que si l'abonnement a échoué.
|
||||
if (!this._unsub) await this._fetchRecipes();
|
||||
} catch (err) {
|
||||
this._reloadError = (err && err.message) || "Échec du rechargement";
|
||||
} finally {
|
||||
this._reloading = false;
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
|
||||
_renderTagFilter() {
|
||||
if (!this._tagFilterEnabled()) return "";
|
||||
const tags = this._allTags();
|
||||
|
|
@ -255,10 +319,24 @@ class CookCliCard extends HTMLElement {
|
|||
const title = this._config.title || "Recettes";
|
||||
const previous = this.shadowRoot.querySelector(".tag-filter");
|
||||
const scrollLeft = previous ? previous.scrollLeft : 0;
|
||||
|
||||
const errorMsg = this._reloadError
|
||||
? `<div class="state-msg error reload-error">${this._escape(this._reloadError)}</div>`
|
||||
: "";
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>${this._styles()}</style>
|
||||
<ha-card header="${this._escape(title)}">
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<span class="card-title">${this._escape(title)}</span>
|
||||
<ha-icon-button
|
||||
class="reload-btn ${this._reloading ? "spinning" : ""}"
|
||||
data-action="reload"
|
||||
label="Recharger les recettes"
|
||||
${this._reloading ? "disabled" : ""}
|
||||
>
|
||||
<ha-icon icon="mdi:refresh"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
${errorMsg}
|
||||
<div class="card-content">${this._renderList()}</div>
|
||||
</ha-card>
|
||||
`;
|
||||
|
|
@ -332,6 +410,20 @@ class CookCliCard extends HTMLElement {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px 8px 16px;
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
line-height: 32px;
|
||||
}
|
||||
.reload-error { padding: 0 16px 8px; }
|
||||
.reload-btn.spinning ha-icon { animation: cookcli-spin 1s linear infinite; }
|
||||
@keyframes cookcli-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.tag-filter {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
|
|
|||
Loading…
Reference in a new issue