/* * Carte OPNsense (custom:opnsense-card) pour Home Assistant, au style des cartes * MyElectricalData new (Carte Enedis Linky New) et des Cartes Serveur HA : * passerelles (état, latence, pertes, gigue), débits des interfaces, CPU, RAM, disque, * table d'états, tampons réseau, températures, baux DHCP, mise à jour d'OPNsense, * redémarrage et arrêt confirmés en deux clics. * * Données : l'intégration OPNsense de HACS (travisghansen/hass-opnsense), ou, sans intégration, * le package REST fourni dans le dépôt (packages/opnsense.yaml). Voir le README. */ (() => { const CARD_VERSION = "0.1.0"; /* ------------------------------------------------------------- affichage */ const fmt = (n, digits = 1) => n === null || n === undefined ? "—" : Number(n).toLocaleString("fr-FR", { minimumFractionDigits: digits, maximumFractionDigits: digits }); const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); /** Classe de thème de la carte : * v2 : style MyElectricalData, toujours bleu nuit ; * auto : style MyElectricalData, bleu nuit si Home Assistant est en mode sombre, clair sinon ; * ha : couleurs du thème Home Assistant (clair ou sombre). */ const themeClass = (theme, hass) => { const light = hass?.themes?.darkMode === false; if (theme === "ha") return `th-ha${light ? " light" : ""}`; if (theme === "auto") return `th-v2${light ? " light" : ""}`; return "th-v2"; }; const since = (iso) => { const t = new Date(iso).getTime(); if (!Number.isFinite(t)) return null; const m = Math.max(0, Math.floor((Date.now() - t) / 60000)); const d = Math.floor(m / 1440), h = Math.floor((m % 1440) / 60), mn = m % 60; return d ? `${d} j ${h} h` : h ? `${h} h ${String(mn).padStart(2, "0")} min` : `${mn} min`; }; const level = (v, warn, alert) => (v === null ? "" : v >= alert ? "b-alert" : v >= warn ? "b-warn" : "b-ok"); const SRV_STYLE = ``; const PVE_STYLE = ``; /* ------------------------------------------------------------------ style */ const STYLE = ``; /* ----------------------------------------------------------------- éditeur */ const THEME_SELECT = { name: "theme", selector: { select: { mode: "dropdown", options: [ { value: "v2", label: "Style MyElectricalData (bleu nuit)" }, { value: "auto", label: "Style MyElectricalData, clair ou sombre selon Home Assistant" }, { value: "ha", label: "Thème Home Assistant (clair ou sombre)" }, ], }, }, }; /** Éditeur visuel : formulaire Home Assistant construit à partir du schéma. */ function makeEditor(schema, labels, defaults) { const hasDays = schema.some((s) => s.name === "days"); return class extends HTMLElement { setConfig(config) { this._config = { ...config }; this._draw(); } set hass(hass) { this._hass = hass; if (this._form) this._form.hass = hass; else this._draw(); } _draw() { if (!this._hass || !this._config) return; if (!this._form) { this._form = document.createElement("ha-form"); this._form.computeLabel = (s) => labels[s.name] || s.name; this._form.addEventListener("value-changed", (ev) => { const v = { ...ev.detail.value }; if (hasDays && v.days !== undefined) v.days = Number(v.days); this._config = v; this.dispatchEvent(new CustomEvent("config-changed", { detail: { config: v }, bubbles: true, composed: true })); }); this.appendChild(this._form); } this._form.hass = this._hass; this._form.schema = schema; this._form.data = hasDays ? { ...defaults, ...this._config, days: String(this._config.days || defaults.days) } : { ...defaults, ...this._config }; } }; } /* ============================================================== OPNsense */ /* * Carte OPNsense (custom:opnsense-card) pour l'intégration hass-opnsense (HACS) : * état et mise à jour du pare-feu, ressources (CPU, RAM, disque, tables), températures, * passerelles (état, latence, pertes), débits des interfaces, baux DHCP, et redémarrage ou * arrêt confirmés en deux clics. */ const OPN_DEFAULTS = { title: "", icon: "mdi:wall-fire", icon_color: "", show_info: true, show_resources: true, show_gateways: true, show_interfaces: true, show_actions: true, interfaces: [], // interfaces affichées (vide = celles qui ont des débits) theme: "v2", }; /** Rôle d'une entité OPNsense d'après la fin de son nom (noms anglais fixés par l'intégration). */ const OPN_ROLES = [ ["cpu", /_cpu_usage$/], ["mem", /_memory_used_percentage$/], ["mem_used", /_memory_used$/], ["mem_total", /_memory_physmem$/], ["swap", /_memory_swap_used_percentage$/], ["mbuf", /_memory_buffers_used_percentage$/], ["pf", /_pf_state_table_used_percentage$/], ["load1", /_system_load_average_one_minute$/], ["boot", /_system_boottime$/], ["dhcp", /_dhcp_leases_all$/], ["certs", /_certificates$/], ["disk", /_filesystem_used_percentage(?:_root)?$/], ["ups", /_ups_status$/], ["ups_batt", /_ups_battery_charge$/], ]; /** État d'une passerelle (dpinger) et d'une interface, en français. */ const OPN_GW = { online: ["En ligne", "ok"], none: ["En ligne", "ok"], down: ["Hors ligne", "bad"], loss: ["Pertes", "warn"], delay: ["Latence", "warn"], delay_loss: ["Latence et pertes", "warn"], force_down: ["Désactivée", "off"], offline: ["Hors ligne", "bad"], latency: ["Latence", "warn"], packetloss: ["Pertes", "warn"] }; const opnGw = (s) => OPN_GW[String(s ?? "").toLowerCase().replace(/[+\s]+/g, "_")] || [s ? String(s) : "Inconnu", "off"]; /** Débit en bit/s d'un capteur, selon son unité (kB/s par défaut de l'intégration, ou converti par Home Assistant). */ const OPN_RATE = { "b/s": 8, "kb/s": 8e3, "kib/s": 8192, "mb/s": 8e6, "mib/s": 8388608, "gb/s": 8e9, "bit/s": 1, "kbit/s": 1e3, "mbit/s": 1e6, "gbit/s": 1e9 }; /** Ordre d'affichage : WAN d'abord, puis LAN, puis le reste par nom. */ const opnRank = (n) => (/^wan/i.test(n) ? 0 : /^lan/i.test(n) ? 1 : /wan/i.test(n) ? 0.5 : 2); const bitrate = (bps) => (bps === null ? "—" : bps >= 1e9 ? `${fmt(bps / 1e9, 2)} Gbit/s` : bps >= 1e6 ? `${fmt(bps / 1e6, bps < 1e7 ? 2 : 1)} Mbit/s` : bps >= 1e3 ? `${fmt(bps / 1e3, 0)} kbit/s` : `${fmt(bps, 0)} bit/s`); class OpnsenseCard extends HTMLElement { static getConfigElement() { return document.createElement("opnsense-card-editor"); } static getStubConfig() { return {}; } setConfig(config) { this._config = { ...OPN_DEFAULTS, ...(config || {}) }; this._map = null; this._sig = null; if (!this.shadowRoot) this.attachShadow({ mode: "open" }); if (!this._clicks) { this.shadowRoot.addEventListener("click", (this._clicks = (ev) => { const act = ev.target.closest?.("[data-act]"); if (act) { // Redémarrer, arrêter : premier clic arme, second clic dans les 5 s confirme (service de l'intégration) ev.stopPropagation(); const what = act.dataset.act; if (this._armed === what) { this._armed = null; if (this._map?.rest) this._hass?.callService("rest_command", `opnsense_${what === "reboot" ? "reboot" : "halt"}`, {}); else this._hass?.callService("opnsense", what === "reboot" ? "system_reboot" : "system_halt", this._map?.device ? { device_id: this._map.device } : {}); } else { this._armed = what; clearTimeout(this._armTimer); this._armTimer = setTimeout(() => { this._armed = null; this._sig = null; this._render(); }, 5000); } this._sig = null; this._render(); return; } const el = ev.target.closest?.("[data-entity]"); if (el?.dataset.entity) { const e = new Event("hass-more-info", { bubbles: true, composed: true }); e.detail = { entityId: el.dataset.entity }; this.dispatchEvent(e); } })); } this._render(); } set hass(hass) { if (this._hass?.entities !== hass.entities || this._hass?.devices !== hass.devices) this._map = null; this._hass = hass; this._render(); } getCardSize() { return 8; } _st(id) { return id ? this._hass?.states?.[id] : undefined; } _num(id) { const s = this._st(id); if (!s || s.state === "unavailable" || s.state === "unknown" || s.state === "") return null; const v = Number(s.state); return Number.isFinite(v) ? v : null; } /** Entités d'un pare-feu OPNsense (appareil choisi, sinon le premier) rangées par rôle, interfaces et passerelles. */ _resolve() { if (this._map) return this._map; const hass = this._hass, c = this._config, reg = hass.entities || {}; // entités restaurées (plus fournies par aucune intégration, ex. ancien package) : ignorées const live = (id) => !reg[id]?.hidden && !hass.states[id]?.attributes?.restored; let ids = Object.keys(hass.states).filter((id) => reg[id]?.platform === "opnsense" && live(id)); // Sans intégration : capteurs du package REST (packages/opnsense.yaml), reconnus à leur préfixe const rest = !ids.length; if (rest) { const pre = new RegExp(`^(sensor|binary_sensor|update)\\.${String(c.prefix || "opnsense").toLowerCase().replace(/[^a-z0-9_]/g, "")}_`); ids = Object.keys(hass.states).filter((id) => pre.test(id) && live(id)); } const device = rest ? null : c.device || reg[ids.find((id) => id.startsWith("sensor."))]?.device_id || null; const mine = ids.filter((id) => !device || reg[id]?.device_id === device); const m = { device, rest, roles: {}, ifaces: {}, gws: {}, temps: [], update: null, notices: null }; for (const id of mine) { const dom = id.split(".")[0]; if (dom === "update" || (dom === "binary_sensor" && /_firmware_updates?_available$/.test(id))) { m.update ||= id; continue; } if (dom === "binary_sensor" && /_pending_notices_present$/.test(id)) { m.notices = id; continue; } if (dom !== "sensor") continue; // package REST : toutes les passerelles et toutes les interfaces dans un capteur chacun if (/_gateways$/.test(id)) { m.gwAll = id; continue; } if (/_traffic$/.test(id)) { m.traffic = id; continue; } let x = /_interface_(.+?)_(status|traffic_in_rate|traffic_out_rate)$/.exec(id); if (x) { (m.ifaces[x[1]] ||= { key: x[1] })[x[2] === "status" ? "status" : x[2] === "traffic_in_rate" ? "in" : "out"] = id; continue; } x = /_gateway_(.+?)_(status|delay|loss|stddev|address)$/.exec(id); if (x) { (m.gws[x[1]] ||= { key: x[1] })[x[2]] = id; continue; } if (/_temp_/.test(id) && /°C|°F/.test(hass.states[id].attributes?.unit_of_measurement || "")) { m.temps.push(id); continue; } for (const [role, re] of OPN_ROLES) if (re.test(id)) { m.roles[role] ||= id; break; } } const dev = hass.devices?.[device]; m.name = c.title || dev?.name_by_user || dev?.name || "OPNsense"; m.version = dev?.sw_version || null; m.model = [dev?.manufacturer, dev?.model].filter(Boolean).join(" "); this._map = m; return m; } /** Nom lisible d'une interface ou d'une passerelle : nom de l'entité sans le préfixe de l'appareil. */ _label(id, re, fallback) { const fn = String(this._st(id)?.attributes?.friendly_name || ""); const x = re.exec(fn); return x ? x[1] : fallback.replace(/_/g, " ").toUpperCase(); } /** Passerelles du package REST : « nom,état,latence,pertes,gigue|… » (valeurs en ms et %). */ _gwPack(id) { const n = (v) => (v === "" || v === undefined || !Number.isFinite(Number(v)) ? null : Number(v)); return String(this._st(id)?.state || "").split("|").map((row) => row.split(",")).filter((f) => f.length >= 2 && f[0]) .map(([name, st, d, l, j]) => ({ key: name, status: id, name, state: opnGw(st), delay: n(d), loss: n(l), jitter: n(j), ip: null })) .sort((a, b) => opnRank(a.name) - opnRank(b.name) || a.name.localeCompare(b.name, "fr")); } /** Interfaces du package REST : « heure|nom,octets reçus,octets envoyés|… ». La carte calcule les * débits entre deux relevés (le précédent est repris de l'historique au chargement de la page). */ _trafPack(id) { const parse = (state) => { const [t, ...rows] = String(state || "").split("|"); const out = { t: Number(t), i: {} }; for (const row of rows) { const [name, rx, tx] = row.split(","); if (name) out.i[name] = [Number(rx), Number(tx)]; } return Number.isFinite(out.t) && out.t > 0 ? out : null; }; const cur = parse(this._st(id)?.state); if (!cur) return []; const tr = (this._tr ||= { last: null, rates: {} }); if (tr.last && cur.t > tr.last.t) { const dt = cur.t - tr.last.t; for (const [name, [rx, tx]] of Object.entries(cur.i)) { const p = tr.last.i[name]; // compteur remis à zéro (redémarrage) : pas de débit négatif if (p) tr.rates[name] = [Math.max(0, ((rx - p[0]) * 8) / dt), Math.max(0, ((tx - p[1]) * 8) / dt)]; } } if (!tr.last || cur.t > tr.last.t) tr.last = cur; if (!tr.seeded && this._hass?.callApi) { tr.seeded = true; const start = new Date(Date.now() - 10 * 60000).toISOString(); this._hass.callApi("GET", `history/period/${start}?filter_entity_id=${encodeURIComponent(id)}&minimal_response&no_attributes&significant_changes_only=0`) .then((res) => { const prev = (res?.[0] || []).map((h) => parse(h.state)).filter((x) => x && x.t < cur.t).pop(); if (!prev || Object.keys(tr.rates).length) return; tr.last = prev; this._trafPack(id); // recalcule avec le relevé précédent this._sig = null; this._render(); }) .catch(() => { /* sans historique : le débit s'affiche au relevé suivant */ }); } return Object.keys(cur.i).map((name) => ({ key: name, in: id, out: id, name, up: null, rin: tr.rates[name]?.[0] ?? null, rout: tr.rates[name]?.[1] ?? null })) .sort((a, b) => opnRank(a.name) - opnRank(b.name) || a.name.localeCompare(b.name, "fr")); } _bps(id) { const v = this._num(id); if (v === null) return null; const u = String(this._st(id)?.attributes?.unit_of_measurement || "kB/s").toLowerCase().replace(/\s/g, ""); return Math.max(0, v * (OPN_RATE[u] || 8e3)); // un compteur remis à zéro donne une dérivée négative } _render() { if (!this._config || !this._hass || !this.shadowRoot) return; const c = this._config, m = this._resolve(); const ids = [...Object.values(m.roles), ...Object.values(m.ifaces).flatMap((i) => Object.values(i)), ...Object.values(m.gws).flatMap((g) => Object.values(g)), ...m.temps, m.update, m.notices, m.gwAll, m.traffic]; const sig = `${c.theme}|${this._hass.themes?.darkMode}|${this._armed}|${Math.floor(Date.now() / 60000)}|` + ids.map((id) => this._st(id)?.state ?? "-").join("|"); if (sig === this._sig) return; this._sig = sig; const sec = (icon, t, body) => (body ? `
${t}
${body}
` : ""); const gauge = (id, icon, label, v, sub, warn = 75, alert = 90) => (v === null ? "" : `
${label}${fmt(v, 0)} %
${sub ? `
${sub}
` : ""}
`); const led = (ok, label, id, cls = "") => `${label}`; const found = Object.keys(m.roles).length || Object.keys(m.gws).length || Object.keys(m.ifaces).length || m.gwAll || m.traffic; if (!found) { this.shadowRoot.innerHTML = `${STYLE}${SRV_STYLE}${PVE_STYLE}
${esc(c.title || "OPNsense")}
Aucun pare-feu OPNsense trouvéInstallez l'intégration OPNsense (HACS : travisghansen/hass-opnsense), ou le package REST fourni avec la carte (packages/opnsense.yaml), puis rechargez la page.
`; return; } // --- passerelles : la passerelle par défaut d'abord (WAN), puis les autres const gwRows = Object.values(m.gws).length ? Object.values(m.gws) : []; let gws = gwRows.map((g) => ({ ...g, name: this._label(g.status || g.delay, /Gateway (.+?) (?:status|delay|loss|stddev|address)$/i, g.key), state: opnGw(this._st(g.status)?.state), delay: this._num(g.delay), loss: this._num(g.loss), jitter: this._num(g.stddev), ip: this._st(g.address)?.state })) .sort((a, b) => opnRank(a.name) - opnRank(b.name) || a.name.localeCompare(b.name, "fr")); if (!gws.length && m.gwAll) gws = this._gwPack(m.gwAll); const gwDown = gws.filter((g) => g.state[1] === "bad"), gwWarn = gws.filter((g) => g.state[1] === "warn"); const upd = this._st(m.update), updOn = upd?.state === "on"; const notice = this._st(m.notices)?.state === "on"; const leds = [ gws.length ? led(!gwDown.length, gwDown.length > 1 ? `${gwDown.length} passerelles hors ligne` : gwDown.length ? `${gwDown[0].name} hors ligne` : "Passerelles", (gwDown[0] || gws[0]).status) : "", upd ? `${updOn ? "Mise à jour" : "À jour"}` : "", notice ? led(false, "Alertes", m.notices) : "", ].join(""); // --- informations const boot = this._st(m.roles.boot)?.state; const inst = upd?.attributes?.installed_version || m.version, last = upd?.attributes?.latest_version; const dhcp = this._num(m.roles.dhcp), load = this._num(m.roles.load1); const info = c.show_info !== false ? [ boot && since(boot) ? `
Uptime
${esc(since(boot))}
` : "", inst || upd ? `
OPNsense
${updOn ? "Dispo" : "Version"}${esc((updOn ? last : inst) || "—")}
${upd ? `
État${updOn ? "À installer" : "À jour"}
` : ""}
` : "", dhcp !== null ? `
Baux DHCP
${fmt(dhcp, 0)}
` : "", load !== null ? `
Charge
${fmt(load, 2)}
` : "", ].filter(Boolean) : []; // --- ressources const memPct = this._num(m.roles.mem), memUsed = this._num(m.roles.mem_used), memTot = this._num(m.roles.mem_total); const memU = String(this._st(m.roles.mem_total)?.attributes?.unit_of_measurement || "B"); const toGo = (v, u) => (v === null ? null : v / ({ b: 1e9, kb: 1e6, kib: 1048576, mb: 1e3, mib: 1024 / 1.048576, gb: 1, gib: 1 / 1.073741824 }[u.toLowerCase()] || 1e9)); const res = c.show_resources !== false ? [ gauge(m.roles.cpu, "mdi:cpu-64-bit", "CPU", this._num(m.roles.cpu), "", 70, 90), gauge(m.roles.mem, "mdi:memory", "RAM", memPct, memUsed !== null && memTot !== null ? `${fmt(toGo(memUsed, memU), 1)} Go / ${fmt(toGo(memTot, memU), 1)} Go` : "", 80, 92), gauge(m.roles.disk, "mdi:harddisk", "Disque", this._num(m.roles.disk), "", 80, 90), gauge(m.roles.pf, "mdi:table-network", "Table d'états", this._num(m.roles.pf), "", 70, 90), gauge(m.roles.mbuf, "mdi:memory-arrow-down", "Tampons réseau", this._num(m.roles.mbuf), m.rest ? "" : "utilisés / alloués", m.rest ? 80 : 101, m.rest ? 95 : 101), gauge(m.roles.swap, "mdi:swap-horizontal", "Swap", this._num(m.roles.swap), "", 50, 80), ].filter(Boolean) : []; const temps = c.show_resources !== false ? m.temps.map((id) => { const v = this._num(id); if (v === null) return ""; const label = this._label(id, /Temp (.+)$/i, id.split("_temp_").pop()); return `
${esc(label)}
${fmt(v, 0)} °C
`; }).filter(Boolean) : []; // --- passerelles const gwHtml = c.show_gateways !== false ? gws.map((g) => `
${esc(g.name)}${esc(g.state[1] === "ok" && g.delay === null && g.loss === null && g.jitter === null ? "En ligne · sans surveillance" : g.state[0])}${g.ip && !/^(unknown|unavailable|~)$/.test(g.ip) ? ` · ${esc(g.ip)}` : ""}
${g.delay === null ? "—" : `${fmt(g.delay, g.delay < 10 ? 1 : 0)} ms`}${g.loss === null ? "—" : `${fmt(g.loss, 0)} %`}${g.jitter === null ? "—" : `± ${fmt(g.jitter, 1)} ms`}
`).join("") : ""; const gwHead = gws.length ? `
LatencePertesGigue
` : ""; // --- interfaces : débits entrant et sortant const want = (Array.isArray(c.interfaces) ? c.interfaces : String(c.interfaces || "").split(/[,\s]+/)).map((x) => String(x).toLowerCase()).filter(Boolean); const ifs = Object.values(m.ifaces).filter((i) => (i.in || i.out) && (!want.length || want.includes(i.key.toLowerCase()) || want.includes(this._label(i.in || i.out, /Interface (.+?) Traffic/i, i.key).toLowerCase()))) .map((i) => ({ ...i, name: this._label(i.in || i.out || i.status, /Interface (.+?) (?:Traffic|Status)/i, i.key), up: this._st(i.status)?.state, rin: this._bps(i.in), rout: this._bps(i.out) })) .sort((a, b) => opnRank(a.name) - opnRank(b.name) || a.name.localeCompare(b.name, "fr")); if (!ifs.length && m.traffic) ifs.push(...this._trafPack(m.traffic).filter((i) => !want.length || want.includes(i.name.toLowerCase()))); const ifHtml = c.show_interfaces !== false ? ifs.map((i) => { const down = i.up && !/^(up|active|connected|unknown|unavailable)$/i.test(i.up); return `
${esc(i.name)}${down ? `Déconnectée` : i.up === null ? "Reçu · envoyé" : "Connectée"}
${bitrate(i.rin)}${bitrate(i.rout)}
`; }).join("") : ""; // --- actions const act = (what, icon, label, cls) => { const armed = this._armed === what; return ``; }; // Intégration : services opnsense.* ; package REST : rest_command.opnsense_reboot et opnsense_halt, s'ils sont déclarés const svc = this._hass.services || {}; const can = (what) => (m.rest ? !!svc.rest_command?.[`opnsense_${what}`] : !!svc.opnsense?.[what === "reboot" ? "system_reboot" : "system_halt"]); const acts = c.show_actions !== false ? [can("reboot") ? act("reboot", "mdi:restart", "Redémarrer", "") : "", can("halt") ? act("halt", "mdi:power", "Arrêter", "stop") : ""].join("") : ""; const sub = [inst ? `OPNsense ${inst}` : m.model, gws.length ? (gwDown.length ? `${gwDown.length} passerelle${gwDown.length > 1 ? "s" : ""} hors ligne` : gwWarn.length ? "passerelle dégradée" : "Internet OK") : ""].filter(Boolean).join(" · "); const body = [ c.show_info !== false && info.length ? sec("mdi:information-outline", "Informations", `
${info.join("")}
`) : "", res.length || temps.length ? sec("mdi:speedometer", "Ressources", `${res.length ? `
${res.join("")}
` : ""}${temps.length ? `
${temps.join("")}
` : ""}`) : "", sec("mdi:router-network", "Passerelles", gwHtml ? `
${gwHead}${gwHtml}
` : ""), sec("mdi:swap-vertical", "Interfaces", ifHtml ? `
${ifHtml}
` : ""), sec("mdi:gesture-tap-button", "Actions", acts ? `
${acts}
Le redémarrage coupe Internet le temps du démarrage (et Home Assistant s'il passe par ce pare-feu).
` : ""), ].join(""); const alertCls = gwDown.length ? "srv-alert" : ""; this.shadowRoot.innerHTML = `${STYLE}${SRV_STYLE}${PVE_STYLE}${OPN_STYLE}
${esc(m.name)}
${sub ? `
${esc(sub)}
` : ""}
${leds ? `
${leds}
` : ""}
${body}
`; } } const OPN_STYLE = ``; const OPN_SCHEMA = [ { name: "title", selector: { text: {} } }, { name: "icon", selector: { icon: {} } }, { name: "icon_color", selector: { text: {} } }, { name: "device", selector: { device: { integration: "opnsense" } } }, { name: "prefix", selector: { text: {} } }, { name: "interfaces", selector: { text: { multiple: true } } }, { name: "show_info", selector: { boolean: {} } }, { name: "show_resources", selector: { boolean: {} } }, { name: "show_gateways", selector: { boolean: {} } }, { name: "show_interfaces", selector: { boolean: {} } }, { name: "show_actions", selector: { boolean: {} } }, THEME_SELECT, ]; const OPN_LABELS = { title: "Titre (vide = nom du pare-feu)", icon: "Icône", icon_color: "Couleur de l'icône", device: "Pare-feu OPNsense (vide = le premier)", prefix: "Sans intégration : début du nom des capteurs du package REST (vide = opnsense)", interfaces: "Interfaces affichées (vide = toutes celles qui ont des débits)", show_info: "Section Informations (uptime, version, baux DHCP, charge)", show_resources: "Section Ressources (CPU, RAM, disque, tables, températures)", show_gateways: "Section Passerelles (état, latence, pertes)", show_interfaces: "Section Interfaces (débits)", show_actions: "Boutons redémarrer et arrêter (deux clics)", theme: "Couleurs de la carte", }; const OpnsenseCardEditor = makeEditor(OPN_SCHEMA, OPN_LABELS, OPN_DEFAULTS); /* ---------------------------------------------------------- enregistrement */ if (typeof customElements !== "undefined") { if (!customElements.get("opnsense-card")) customElements.define("opnsense-card", OpnsenseCard); if (!customElements.get("opnsense-card-editor")) customElements.define("opnsense-card-editor", OpnsenseCardEditor); window.customCards = window.customCards || []; if (!window.customCards.some((c) => c.type === "opnsense-card")) { window.customCards.push({ type: "opnsense-card", name: "Carte OPNsense", description: "Pare-feu OPNsense : passerelles (état, latence, pertes), débits des interfaces, CPU, RAM, table d'états, températures, mise à jour, redémarrage confirmé.", preview: true, }); } console.info(`%c OPNSENSE-CARD %c ${CARD_VERSION} `, "background:#d94f00;color:#fff;font-weight:700", "background:#1d93e3;color:#fff"); } })();