Add service to reload recipes
This commit is contained in:
parent
7e3b4674c4
commit
f876730ead
|
|
@ -4,10 +4,12 @@ from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.components.http import StaticPathConfig
|
from homeassistant.components.http import StaticPathConfig
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||||
|
|
||||||
|
|
@ -106,6 +108,35 @@ async def async_setup(hass: HomeAssistant, config) -> bool:
|
||||||
hass.bus.async_listen_once(
|
hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_STARTED, _register_card_resources
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,14 @@ from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .const import RELOAD_PORT
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
RELOAD_TIMEOUT = aiohttp.ClientTimeout(total=120)
|
||||||
|
|
||||||
class CookCliApiError(Exception):
|
class CookCliApiError(Exception):
|
||||||
"""Erreur générique lors d'un appel à l'API CookCLI."""
|
"""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:
|
def __init__(self, session: aiohttp.ClientSession, host: str, port: int) -> None:
|
||||||
self._session = session
|
self._session = session
|
||||||
self._base_url = f"http://{host}:{port}/api"
|
self._base_url = f"http://{host}:{port}/api"
|
||||||
|
self._reload_url = f"http://{host}:{RELOAD_PORT}/reload"
|
||||||
|
|
||||||
async def async_test_connection(self) -> None:
|
async def async_test_connection(self) -> None:
|
||||||
"""Vérifie que le serveur répond, utilisé par le config_flow."""
|
"""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}")
|
data = await self._request_json(f"/recipes/{path}")
|
||||||
return _resolve_recipe(data)
|
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]:
|
async def async_get_image(self, image_ref: str) -> tuple[bytes, str | None]:
|
||||||
"""Récupère les octets d'une image statique CookCLI.
|
"""Récupère les octets d'une image statique CookCLI.
|
||||||
|
|
||||||
|
|
@ -143,13 +150,16 @@ class CookCliApiClient:
|
||||||
return f"/static/{image_ref}"
|
return f"/static/{image_ref}"
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def _get(self, endpoint: str):
|
async def _get(
|
||||||
"""Contexte partagé pour un GET vers CookCLI, avec gestion d'erreurs."""
|
self,
|
||||||
url = f"{self._base_url}{endpoint}"
|
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:
|
try:
|
||||||
async with self._session.request(
|
async with self._session.request(method, url, timeout=timeout) as resp:
|
||||||
"GET", url, timeout=DEFAULT_TIMEOUT
|
|
||||||
) as resp:
|
|
||||||
if resp.status >= 400:
|
if resp.status >= 400:
|
||||||
raise CookCliResponseError(
|
raise CookCliResponseError(
|
||||||
f"CookCLI a répondu {resp.status} pour {url}"
|
f"CookCLI a répondu {resp.status} pour {url}"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ CONF_HOST = "host"
|
||||||
CONF_PORT = "port"
|
CONF_PORT = "port"
|
||||||
|
|
||||||
DEFAULT_PORT = 9081
|
DEFAULT_PORT = 9081
|
||||||
|
RELOAD_PORT = 8098
|
||||||
|
|
||||||
# Nom du service tel que poussé par bashio::discovery.set côté add-on
|
# Nom du service tel que poussé par bashio::discovery.set côté add-on
|
||||||
HASSIO_DISCOVERY_SLUG = "cookcli"
|
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._error = null;
|
||||||
this._lastFetch = 0;
|
this._lastFetch = 0;
|
||||||
this._selectedTag = null;
|
this._selectedTag = null;
|
||||||
|
this._reloading = false;
|
||||||
|
this._reloadError = null;
|
||||||
|
|
||||||
if (!this.shadowRoot) {
|
if (!this.shadowRoot) {
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
|
|
@ -77,6 +79,7 @@ class CookCliCard extends HTMLElement {
|
||||||
|
|
||||||
set hass(hass) {
|
set hass(hass) {
|
||||||
this._hass = hass;
|
this._hass = hass;
|
||||||
|
this._subscribe();
|
||||||
if (this._recipes === null && !this._loading) {
|
if (this._recipes === null && !this._loading) {
|
||||||
this._fetchRecipes();
|
this._fetchRecipes();
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +139,10 @@ class CookCliCard extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onClick(event) {
|
_onClick(event) {
|
||||||
|
if (event.target.closest("[data-action='reload']")) {
|
||||||
|
this._reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.target.closest("[data-tag-clear]")) {
|
if (event.target.closest("[data-tag-clear]")) {
|
||||||
this._selectedTag = null;
|
this._selectedTag = null;
|
||||||
this._render();
|
this._render();
|
||||||
|
|
@ -193,6 +200,63 @@ class CookCliCard extends HTMLElement {
|
||||||
return this._recipes.filter((r) => this._tagsOf(r).includes(this._selectedTag));
|
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() {
|
_renderTagFilter() {
|
||||||
if (!this._tagFilterEnabled()) return "";
|
if (!this._tagFilterEnabled()) return "";
|
||||||
const tags = this._allTags();
|
const tags = this._allTags();
|
||||||
|
|
@ -255,10 +319,24 @@ class CookCliCard extends HTMLElement {
|
||||||
const title = this._config.title || "Recettes";
|
const title = this._config.title || "Recettes";
|
||||||
const previous = this.shadowRoot.querySelector(".tag-filter");
|
const previous = this.shadowRoot.querySelector(".tag-filter");
|
||||||
const scrollLeft = previous ? previous.scrollLeft : 0;
|
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 = `
|
this.shadowRoot.innerHTML = `
|
||||||
<style>${this._styles()}</style>
|
<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>
|
<div class="card-content">${this._renderList()}</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
|
|
@ -332,6 +410,20 @@ class CookCliCard extends HTMLElement {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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 {
|
.tag-filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue