/** * Carte Lovelace pour l'intégration CookCLI. * * Consomme les commandes websocket exposées par l'intégration : * - cookcli/recipes -> liste (path, name, time, servings, tags) * - cookcli/recipe -> détail (title, ingredients, cookware, sections) * * Limitation connue : les images de recettes ne sont pas affichées. Le * serveur CookCLI n'est joignable depuis le navigateur ni sur son port * ingress (restreint au Supervisor) ni sur son port direct (non exposé au * LAN) — il faudrait un endpoint de proxy côté intégration pour les servir. * Voir le README du projet pour plus de détails si tu veux l'ajouter. * * Installation : * 1. Copie ce fichier dans config/www/cookcli-card.js * 2. Paramètres -> Tableaux de bord -> menu ⋮ -> Ressources -> Ajouter : * URL "/local/cookcli-card.js", type "Module JavaScript" * 3. Ajoute une carte manuelle sur un tableau de bord : * type: custom:cookcli-card * title: Mes recettes */ class CookCliCard extends HTMLElement { static getStubConfig() { return { title: "Recettes" }; } setConfig(config) { this._config = config || {}; this._view = "list"; this._recipes = null; this._selected = null; this._loading = false; this._error = null; if (!this.shadowRoot) { this.attachShadow({ mode: "open" }); this.shadowRoot.addEventListener("click", (event) => this._onClick(event)); } this._render(); } set hass(hass) { this._hass = hass; if (this._recipes === null && !this._loading) { this._fetchRecipes(); } } getCardSize() { return 6; } _wsMessage(extra) { const msg = { ...extra }; if (this._config.entry_id) { msg.entry_id = this._config.entry_id; } return msg; } async _fetchRecipes() { this._loading = true; this._error = null; this._render(); try { const result = await this._hass.connection.sendMessagePromise( this._wsMessage({ type: "cookcli/recipes" }) ); this._recipes = result.recipes || []; } catch (err) { this._error = (err && err.message) || "Erreur inconnue"; } finally { this._loading = false; this._render(); } } async _openRecipe(path) { this._loading = true; this._error = null; this._render(); try { const result = await this._hass.connection.sendMessagePromise( this._wsMessage({ type: "cookcli/recipe", path }) ); this._selected = result; this._view = "detail"; } catch (err) { this._error = (err && err.message) || "Erreur inconnue"; } finally { this._loading = false; this._render(); } } _onClick(event) { const backEl = event.target.closest("[data-action='back']"); if (backEl) { this._view = "list"; this._selected = null; this._error = null; this._render(); return; } const itemEl = event.target.closest("[data-path]"); if (itemEl) { this._openRecipe(itemEl.dataset.path); } } _escape(str) { const div = document.createElement("div"); div.textContent = str ?? ""; return div.innerHTML; } _formatQuantity(quantity) { if (!quantity) return ""; const value = quantity.value ?? ""; const unit = quantity.unit ?? ""; return `${value} ${unit}`.trim(); } _renderStepItem(item) { switch (item.type) { case "text": return this._escape(item.value); case "ingredient": { const qty = this._formatQuantity(item.quantity); return `${this._escape(item.name)}${ qty ? ` (${this._escape(qty)})` : "" }`; } case "cookware": return `${this._escape(item.name)}`; case "timer": { const duration = item.duration ?? ""; const unit = item.unit ?? ""; return `⏱ ${this._escape(String(duration))}${ unit ? " " + this._escape(unit) : "" }`; } default: return ""; } } _renderList() { if (this._loading) { return `