const CARD_TAG = "k-cfs-card"; const EDITOR_TAG = "k-cfs-card-editor"; const mdi = (name) => `mdi:${name}`; const I18N_URL_BASE = "/ha_creality_ws/i18n/"; const _i18nData = {}; const _i18nPromises = {}; function _loadI18n(lang) { if (_i18nData[lang]) return Promise.resolve(_i18nData[lang]); if (_i18nPromises[lang]) return _i18nPromises[lang]; _i18nPromises[lang] = fetch(`${I18N_URL_BASE}${lang}.json`) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (data) _i18nData[lang] = data; else _i18nPromises[lang] = null; return data; }) .catch(() => { _i18nPromises[lang] = null; return null; }); return _i18nPromises[lang]; } function _resolveLang(hass) { return hass?.locale?.language || hass?.language || "en"; } function _translate(hass, section, fallbackDict, key, vars) { const lang = _resolveLang(hass); const short = lang.split("-")[0]; const remote = _i18nData[lang]?.[section] ?? _i18nData[short]?.[section]; const remoteEn = _i18nData["en"]?.[section]; let text = (remote && key in remote) ? remote[key] : (remoteEn && key in remoteEn) ? remoteEn[key] : (fallbackDict[lang]?.[key] ?? fallbackDict[short]?.[key] ?? fallbackDict["en"]?.[key] ?? key); if (vars) { Object.entries(vars).forEach(([k, v]) => { text = text.replace(new RegExp(`\\{${k}\\}`, "g"), v); }); } return text; } function _requestI18n(instance, hass, onLoaded) { if (instance._i18nRequested) return; instance._i18nRequested = true; const lang = _resolveLang(hass).split("-")[0]; Promise.all([_loadI18n("en"), lang !== "en" ? _loadI18n(lang) : null]).then(onLoaded); } const CFS_TRANSLATIONS = { en: { no_data: "No CFS data available", ext_label: "EXT", cfs_label: "CFS", cfs_number_label: "CFS {number}", // Editor label_card_title: "Card Title", label_external_filament: "External Filament", label_external_color: "External Color", label_external_percent: "External Percent", label_box_temp: "Box {box} Temperature", label_box_humidity: "Box {box} Humidity", label_slot_filament: "Box {box} Slot {slot} Filament", label_slot_color: "Box {box} Slot {slot} Color", label_slot_percent: "Box {box} Slot {slot} Remaining Percent", schema_compact_view: "Compact View (Mini Mode)", schema_show_type_in_mini: "Show Filament Type in Mini Mode", // Editor tab/header tab_entities: "Entities", tab_theme: "Theme", }, }; class KCFSCard extends HTMLElement { constructor() { super(); this._selectedCFS = 0; // Track selected CFS tab in normal mode } static _sanitizeColor(value) { const raw = String(value || "").trim(); if (!raw || ["unknown", "unavailable", "—"].includes(raw.toLowerCase())) { return "#cccccc"; } const hex = raw.startsWith("#") ? raw.slice(1) : raw; if (hex.length === 6 && /^[0-9a-fA-F]+$/.test(hex)) { return `#${hex.toLowerCase()}`; } if (hex.length === 3 && /^[0-9a-fA-F]+$/.test(hex)) { return `#${hex.toLowerCase()}`; } if (hex.length === 7 && hex.startsWith("0") && /^[0-9a-fA-F]+$/.test(hex)) { return `#${hex.slice(1).toLowerCase()}`; } return "#cccccc"; } static _parsePercent(percentObj) { if (!percentObj) return null; const state = percentObj.state; if (state === undefined || state === null) return null; const s = String(state); if (s === "unknown" || s === "unavailable") return null; const n = Number(s); if (Number.isNaN(n) || !Number.isFinite(n)) return null; return Math.max(0, Math.min(100, n)); } static _getHumidityColor(humidityStr) { if (!humidityStr || humidityStr === "—") return '#64b5f6'; // default blue const match = String(humidityStr).match(/(\d+\.?\d*)/); if (!match) return '#64b5f6'; const value = parseFloat(match[1]); if (value < 40) return '#4caf50'; // Green (0-39%) - Ideal if (value < 60) return '#ff9800'; // Orange (40-59%) - Attention return '#f44336'; // Red (60-100%) - Critical } static getStubConfig() { const cfg = { name: "CFS", compact_view: false, show_type_in_mini: false, external_filament: "", external_color: "", external_percent: "", }; for (let box = 0; box < 4; box += 1) { cfg[`box${box}_temp`] = ""; cfg[`box${box}_humidity`] = ""; for (let slot = 0; slot < 4; slot += 1) { cfg[`box${box}_slot${slot}_filament`] = ""; cfg[`box${box}_slot${slot}_color`] = ""; cfg[`box${box}_slot${slot}_percent`] = ""; } } return cfg; } static getConfigElement() { return document.createElement(EDITOR_TAG); } setConfig(config) { this._cfg = { ...KCFSCard.getStubConfig(), ...config }; if (!this._root) { this._root = this.attachShadow({ mode: "open" }); } this._render(); } // i18n helpers ------------------------------------------------------- _resolveLanguage() { return _resolveLang(this._hass); } _t(key, vars) { return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars); } // --------------------------------------------------------------------- set hass(hass) { this._hass = hass; _requestI18n(this, hass, () => { this._update(); }); this._update(); } _render() { if (!this._root) return; const isCompact = this._cfg.compact_view; const style = ` /* inherit HA fonts & typography */ :host { font: inherit; color: var(--primary-text-color); display: block !important; height: auto !important; contain: none !important; position: relative; z-index: 1; } /* unify horizontal padding so right edges line up */ :host { --row-xpad: 6px; } ha-card { overflow: visible !important; display: block !important; height: auto !important; min-height: auto !important; max-height: none !important; contain: none !important; position: relative; z-index: 1; } ha-card.compact-mode, ha-card.normal-mode { height: auto !important; overflow: visible !important; } #content { padding: 14px; } .card { border-radius: var(--ha-card-border-radius, 12px); background: var(--card-background-color); color: var(--primary-text-color); box-shadow: var(--ha-card-box-shadow, 0 2px 4px rgba(0,0,0,.2)); padding: 10px var(--row-xpad) 10px var(--row-xpad); display: flex; flex-direction: column; gap: 6px; } /* === NORMAL MODE === */ .normal-mode {} .header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 12px; } .title-section { display: flex; flex-direction: column; gap: 4px; } .title { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; } .subtitle { font-size: 12px; color: var(--secondary-text-color); font-weight: 500; } .env-info { font-size: 11px; background: rgba(var(--rgb-primary-text-color), 0.08); padding: 6px 10px; border-radius: 12px; color: var(--secondary-text-color); } .unit-selector { display: flex; gap: 8px; margin-bottom: 20px; background: rgba(var(--rgb-primary-text-color), 0.05); padding: 4px; border-radius: 14px; width: fit-content; } .unit-btn { padding: 6px 16px; border-radius: 10px; font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; border: none; color: var(--secondary-text-color); background: transparent; } .unit-btn.active { background: rgba(var(--rgb-primary-text-color), 0.1); color: var(--primary-text-color); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); } .spool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } .spool-card { background: rgba(var(--rgb-primary-text-color), 0.04); border-radius: var(--ha-card-border-radius, 18px); padding: 16px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; border: 1px solid transparent; transition: all 0.3s ease; cursor: pointer; } .spool-card:hover { background: rgba(var(--rgb-primary-text-color), 0.06); } .spool-card.active { background: rgba(var(--rgb-primary-text-color), 0.08); border: 1px solid rgba(var(--rgb-primary-color), 0.3); box-shadow: 0 0 20px rgba(var(--rgb-primary-color), 0.2); } .ring-container { width: 80px; height: 80px; border-radius: 50%; position: relative; display: flex; justify-content: center; align-items: center; margin-bottom: 10px; } .ring-outer { width: 100%; height: 100%; border-radius: 50%; position: absolute; background: conic-gradient( var(--spool-color) var(--spool-pct), rgba(var(--rgb-primary-text-color), 0.08) 0 ); mask: radial-gradient(circle closest-side at 50% 50%, transparent 33px, black 34px); -webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 33px, black 34px); } .ring-inner { width: 66px; height: 66px; background: transparent; border-radius: 50%; position: relative; z-index: 2; display: flex; flex-direction: column; justify-content: center; align-items: center; } .spool-label { font-size: 10px; color: var(--secondary-text-color); text-transform: uppercase; } .spool-pct { font-size: 16px; font-weight: 700; } .material-name { font-size: 13px; font-weight: 600; text-align: center; } .color-name { font-size: 11px; color: var(--secondary-text-color); text-align: center; } .status-badge { position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; background: var(--success-color, #4caf50); border-radius: 50%; box-shadow: 0 0 8px var(--success-color, #4caf50); animation: pulse-badge 2s infinite; } @keyframes pulse-badge { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.7; transform: scale(1.1); } } /* === COMPACT MODE === */ .compact-mode { padding: 14px; } .compact-mode .header { margin-bottom: 12px; } .cfs-rows { display: flex; flex-direction: column; gap: 10px; } .cfs-row { display: flex; align-items: center; gap: 12px; } .cfs-label { width: 48px; font-size: 11px; color: var(--secondary-text-color); font-weight: 600; } .spools-inline { display: flex; gap: 10px; flex: 1; } .spool-mini { width: 34px; height: 34px; border-radius: 50%; position: relative; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; cursor: pointer; transition: transform 0.2s; } .spool-mini:hover { transform: scale(1.05); } .spool-mini::before { content: ''; position: absolute; inset: 0; border-radius: 50%; background: conic-gradient( var(--spool-color) var(--spool-pct), rgba(var(--rgb-primary-text-color), 0.08) 0 ); mask: radial-gradient(circle closest-side at 50% 50%, transparent 12px, black 13px); -webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 12px, black 13px); } .spool-mini::after { content: ''; position: absolute; inset: 4px; background: transparent; border-radius: 50%; z-index: 1; } .spool-mini span { position: relative; z-index: 2; } .spool-mini.active { box-shadow: 0 0 10px var(--spool-color); } .spool-mini.active::after { inset: 3px; } .spool-mini-wrapper { display: flex; flex-direction: column; align-items: center; gap: 2px; } .spool-mini-type { font-size: 8px; color: var(--secondary-text-color); text-transform: uppercase; font-weight: 600; max-width: 40px; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .env-mini { width: 56px; display: flex; flex-direction: column; align-items: flex-end; font-size: 10px; line-height: 1.4; gap: 2px; } .env-mini .temp { color: #ffb74d; font-weight: 600; } .env-mini .hum { font-weight: 600; /* Cor aplicada dinamicamente via inline style */ } /* === EXTERNAL SECTION === */ .external-section { margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(var(--rgb-primary-text-color), 0.08); } .external-normal { background: rgba(var(--rgb-primary-text-color), 0.03); border-radius: 16px; padding: 12px 16px; display: flex; align-items: center; gap: 12px; } .ext-icon { width: 30px; height: 30px; background: var(--primary-color); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: white; } .ext-info { flex-grow: 1; } .ext-name { font-size: 12px; font-weight: 600; } .ext-bar { height: 4px; background: rgba(var(--rgb-primary-text-color), 0.1); border-radius: 2px; margin-top: 6px; overflow: hidden; } .ext-fill { height: 100%; background: var(--primary-color); transition: width 0.3s ease; } .ext-percent { font-size: 12px; color: var(--secondary-text-color); font-weight: 600; } .external-compact { display: flex; align-items: center; gap: 10px; } .ext-dot { width: 34px; height: 34px; border-radius: 50%; background: conic-gradient(var(--primary-color) 100%, rgba(var(--rgb-primary-text-color), 0.1) 0); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: white; } .ext-compact-info { flex: 1; } .ext-compact-info div:first-child { font-size: 12px; font-weight: 600; } .ext-compact-info div:last-child { font-size: 10px; color: var(--secondary-text-color); } .no-data { text-align: center; color: var(--secondary-text-color); padding: 20px; } `; this._root.innerHTML = `
`; } _update() { if (!this._root || !this._hass) return; const contentContainer = this._root.getElementById("content"); if (!contentContainer) return; const states = this._hass.states || {}; const gObj = (eid) => (eid ? states?.[eid] : undefined); const fmtState = (st) => { if (!st) return "—"; const v = st.state; if (v === undefined || v === null) return "—"; const s = String(v); if (s === "unknown" || s === "unavailable") return "—"; if (this._hass && typeof this._hass.formatEntityState === "function") { try { return this._hass.formatEntityState(st); } catch (_) { } } const unit = st.attributes?.unit_of_measurement; const n = Number(s); if (!Number.isNaN(n) && Number.isFinite(n)) { const dp = (typeof st.attributes?.display_precision === "number") ? st.attributes.display_precision : (typeof st.attributes?.suggested_display_precision === "number") ? st.attributes.suggested_display_precision : (unit && /°|c|f/i.test(unit)) ? 1 : 2; const out = n.toFixed(Math.max(0, Math.min(6, dp))); return unit ? `${out} ${unit}` : out; } return unit ? `${s} ${unit}` : s; }; // Collect box data const boxes = {}; for (let boxId = 0; boxId < 4; boxId += 1) { const tempEid = this._cfg[`box${boxId}_temp`]; const humidityEid = this._cfg[`box${boxId}_humidity`]; const slots = []; for (let slotId = 0; slotId < 4; slotId += 1) { const filamentEid = this._cfg[`box${boxId}_slot${slotId}_filament`]; const colorEid = this._cfg[`box${boxId}_slot${slotId}_color`]; const percentEid = this._cfg[`box${boxId}_slot${slotId}_percent`]; if (!filamentEid && !colorEid && !percentEid) { slots.push(null); continue; } const filamentObj = gObj(filamentEid); const colorObj = gObj(colorEid); const percentObj = gObj(percentEid); const name = filamentObj?.state; const type = filamentObj?.attributes?.type; const selected = filamentObj?.attributes?.selected; const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex; const color = KCFSCard._sanitizeColor(rawColor); const percent = KCFSCard._parsePercent(percentObj); const percentText = fmtState(percentObj); slots[slotId] = { id: slotId, boxId, entity_id: filamentEid || colorEid || percentEid, name, type, selected, color, percent, percentText, }; } if (tempEid || humidityEid || slots.some((slot) => slot)) { const humidityFormatted = fmtState(gObj(humidityEid)); boxes[boxId] = { id: boxId, temp: fmtState(gObj(tempEid)), humidity: humidityFormatted, humidityColor: KCFSCard._getHumidityColor(humidityFormatted), slots, }; } } // Collect external data const external = { filament: this._cfg.external_filament, color: this._cfg.external_color, percent: this._cfg.external_percent, }; const hasExternal = external.filament || external.color || external.percent; let externalData = null; if (hasExternal) { const filamentObj = gObj(external.filament); const colorObj = gObj(external.color); const percentObj = gObj(external.percent); const name = filamentObj?.state; const type = filamentObj?.attributes?.type; const selected = filamentObj?.attributes?.selected; const rawColor = colorObj?.state || filamentObj?.attributes?.color_hex; const color = KCFSCard._sanitizeColor(rawColor); const percent = KCFSCard._parsePercent(percentObj); const percentText = fmtState(percentObj); externalData = { id: 0, boxId: -1, entity_id: external.filament || external.color || external.percent, name, type, selected, color, percent, percentText, }; } const boxValues = Object.values(boxes); if (boxValues.length === 0 && !hasExternal) { contentContainer.innerHTML = `
${this._t("no_data")}
`; return; } // Render based on mode if (this._cfg.compact_view) { contentContainer.innerHTML = this._renderCompactMode(boxValues, externalData); } else { contentContainer.innerHTML = this._renderNormalMode(boxValues, externalData); } this._attachEventHandlers(); } _renderNormalMode(boxes, external) { // Ensure we have at least one box if (boxes.length === 0 && !external) { return `
${this._t("no_data")}
`; } // Unit selector (only if we have multiple boxes) let unitSelector = ''; if (boxes.length > 1) { unitSelector = `
${boxes.map((box, idx) => ` `).join('')}
`; } // Get the selected box const selectedBox = boxes[this._selectedCFS] || boxes[0]; if (!selectedBox && !external) { return `
${this._t("no_data")}
`; } // Header with environment info let envInfo = ''; if (selectedBox) { const tempStr = selectedBox.temp !== "—" ? selectedBox.temp : ''; const humStr = selectedBox.humidity !== "—" ? selectedBox.humidity : ''; if (tempStr || humStr) { const tempHtml = tempStr ? `${tempStr}` : ''; const humHtml = humStr ? `${humStr}` : ''; const separator = tempStr && humStr ? ' • ' : ''; envInfo = `
${tempHtml}${separator}${humHtml}
`; } } const header = `
${this._cfg.name || 'Creality CFS'}
${envInfo}
`; // Spool grid let spoolGrid = ''; if (selectedBox) { spoolGrid = `
${selectedBox.slots.map((slot) => this._renderSpoolCard(slot)).join('')}
`; } // External section let externalSection = ''; if (external) { const safeType = external.type && !["unknown", "unavailable", "—", "-"].includes(String(external.type).toLowerCase()) ? external.type : "—"; const safeName = external.name && !["unknown", "unavailable", "—", "-"].includes(String(external.name).toLowerCase()) ? external.name : "—"; const hasFilament = safeType !== "—" && safeName !== "—"; const pct = hasFilament && external.percent !== null ? external.percent : 0; const percentTextDisplay = hasFilament ? (external.percentText || '—') : '—'; const displayName = hasFilament ? `${safeName} ${safeType}` : '—'; externalSection = `
${this._t("ext_label")}
${displayName}
${percentTextDisplay}
`; } return `${unitSelector}${header}${spoolGrid}${externalSection}`; } _renderCompactMode(boxes, external) { if (boxes.length === 0 && !external) { return `
${this._t("no_data")}
`; } // CFS rows let cfsRows = ''; if (boxes.length > 0) { cfsRows = `
${boxes.map((box) => this._renderCFSRow(box)).join('')}
`; } // External section let externalSection = ''; if (external) { const safeType = external.type && !["unknown", "unavailable", "—", "-"].includes(String(external.type).toLowerCase()) ? external.type : "—"; const safeName = external.name && !["unknown", "unavailable", "—", "-"].includes(String(external.name).toLowerCase()) ? external.name : "—"; const hasFilament = safeType !== "—" && safeName !== "—"; const percentTextDisplay = hasFilament ? (external.percentText || '—') : '—'; const displayName = hasFilament ? `${safeName} ${safeType}` : '—'; externalSection = `
${this._t("ext_label")}
${displayName}
${percentTextDisplay}
`; } return `${cfsRows}${externalSection}`; } _renderCFSRow(box) { const tempStr = box.temp !== "—" ? box.temp : ''; const humStr = box.humidity !== "—" ? box.humidity : ''; let envHtml = ''; if (tempStr || humStr) { envHtml = `
${tempStr ? `
${tempStr}
` : ''} ${humStr ? `
${humStr}
` : ''}
`; } return `
${this._t("cfs_number_label", { number: box.id + 1 })}
${box.slots.map((slot) => this._renderSpoolMini(slot)).join('')}
${envHtml}
`; } _renderSpoolCard(slot) { if (!slot) { return `
`; } const isActive = slot.selected === 1 || slot.selected === true; const color = slot.color || '#cccccc'; const safeType = slot.type && !["unknown", "unavailable", "—", "-"].includes(String(slot.type).toLowerCase()) ? slot.type : "—"; const safeName = slot.name && !["unknown", "unavailable", "—", "-"].includes(String(slot.name).toLowerCase()) ? slot.name : "—"; // If no filament (type is "—" or name is "—"), show 0% regardless of actual value const hasFilament = safeType !== "—" && safeName !== "—"; const pct = hasFilament && slot.percent !== null ? slot.percent : 0; const pctDisplay = hasFilament && slot.percent !== null ? Math.round(slot.percent) : 0; const percentTextDisplay = hasFilament ? (slot.percentText || '—') : '—'; const badge = isActive ? '
' : ''; return `
${badge}
${pctDisplay}% ${safeType}
${safeName}
${percentTextDisplay}
`; } _renderSpoolMini(slot) { const showType = this._cfg.show_type_in_mini; if (!slot) { if (showType) { return `
—
—
`; } return `
—
`; } const isActive = slot.selected === 1 || slot.selected === true; const color = slot.color || '#cccccc'; const safeType = slot.type && !["unknown", "unavailable", "—", "-"].includes(String(slot.type).toLowerCase()) ? slot.type : "—"; const safeName = slot.name && !["unknown", "unavailable", "—", "-"].includes(String(slot.name).toLowerCase()) ? slot.name : null; // If no filament (type is "—" or name is empty/dash), show 0% regardless of actual value const hasFilament = safeType !== "—" && safeName !== null; const pct = hasFilament && slot.percent !== null ? slot.percent : 0; const pctDisplay = hasFilament && slot.percent !== null ? Math.round(slot.percent) : 0; if (showType) { return `
${pctDisplay}
${safeType}
`; } return `
${pctDisplay}
`; } _attachEventHandlers() { // Unit selector buttons this._root.querySelectorAll('.unit-btn').forEach(btn => { btn.onclick = () => { const cfsIdx = parseInt(btn.dataset.cfs, 10); if (!isNaN(cfsIdx)) { this._selectedCFS = cfsIdx; this._update(); } }; }); // Spool cards and mini spools - show more info this._root.querySelectorAll('.spool-card, .spool-mini, .spool-mini-wrapper .spool-mini, .external-normal, .external-compact').forEach(el => { const eid = el.dataset.eid; if (!eid) return; el.onclick = () => { this.dispatchEvent(new CustomEvent("hass-more-info", { detail: { entityId: eid }, bubbles: true, composed: true, })); }; }); } getCardSize() { // Return a dynamic size based on compact mode and number of boxes if (this._cfg?.compact_view) { // Count configured boxes let boxCount = 0; for (let box = 0; box < 4; box++) { const hasBox = this._cfg[`box${box}_temp`] || this._cfg[`box${box}_humidity`] || [0, 1, 2, 3].some(s => this._cfg[`box${box}_slot${s}_filament`] || this._cfg[`box${box}_slot${s}_color`] || this._cfg[`box${box}_slot${s}_percent`]); if (hasBox) boxCount++; } // Check for external filament const hasExternal = this._cfg.external_filament || this._cfg.external_color || this._cfg.external_percent; const externalRows = hasExternal ? 1 : 0; // Add extra space when more than 2 rows const extraPadding = (boxCount + externalRows) > 2 ? 1 : 0; return Math.max(1, boxCount + externalRows + extraPadding); } return 5; } getLayoutOptions() { // Count configured boxes for dynamic sizing let boxCount = 0; if (this._cfg) { for (let box = 0; box < 4; box++) { const hasBox = this._cfg[`box${box}_temp`] || this._cfg[`box${box}_humidity`] || [0, 1, 2, 3].some(s => this._cfg[`box${box}_slot${s}_filament`] || this._cfg[`box${box}_slot${s}_color`] || this._cfg[`box${box}_slot${s}_percent`]); if (hasBox) boxCount++; } } // Check for external filament const hasExternal = this._cfg?.external_filament || this._cfg?.external_color || this._cfg?.external_percent; const externalRows = hasExternal ? 1 : 0; // Add extra space when more than 2 rows const totalRows = boxCount + externalRows; const extraPadding = totalRows > 2 ? 1 : 0; const minRows = this._cfg?.compact_view ? Math.max(1, totalRows + extraPadding) : 5; return { grid_rows: minRows, grid_min_rows: minRows, }; } } customElements.define(CARD_TAG, KCFSCard); class KCFSCardEditor extends HTMLElement { // i18n helpers ------------------------------------------------------- _resolveLanguage() { return _resolveLang(this._hass); } _t(key, vars) { return _translate(this._hass, "cfs_card", CFS_TRANSLATIONS, key, vars); } // --------------------------------------------------------------------- set hass(hass) { this._hass = hass; if (this._form) this._form.hass = hass; _requestI18n(this, hass, () => { if (this._root) this._render(); }); } setConfig(config) { this._cfg = { ...KCFSCard.getStubConfig(), ...config }; this._render(); } connectedCallback() { this._render(); } _render() { if (!this._root) { this._root = this.attachShadow({ mode: "open" }); } const style = ` .editor-container { padding: 16px; } .tabs { display: flex; border-bottom: 1px solid var(--divider-color); margin-bottom: 16px; } .tab { padding: 8px 16px; cursor: pointer; border-bottom: 2px solid transparent; } .tab.active { border-bottom-color: var(--primary-color); color: var(--primary-color); } .tab-content { display: none; } .tab-content.active { display: block; } .input-helper { font-size: 0.9em; color: var(--secondary-text-color); margin-top: 4px; padding: 0 8px; } `; this._root.innerHTML = `
${this._t("tab_entities")}
${this._t("tab_theme")}
`; this._setupTabs(); this._setupEntitiesForm(); this._setupThemeForm(); } _setupTabs() { const tabs = this._root.querySelectorAll(".tab"); const contents = this._root.querySelectorAll(".tab-content"); tabs.forEach((tab) => { tab.onclick = () => { tabs.forEach((t) => t.classList.remove("active")); contents.forEach((c) => c.classList.remove("active")); tab.classList.add("active"); this._root.getElementById(`${tab.dataset.tab}-tab`).classList.add("active"); }; }); } _setupEntitiesForm() { this._form = this._root.getElementById("form"); this._form.hass = this._hass; this._form.data = this._cfg; const schema = [ { name: "name", selector: { text: {} } }, { name: "external_filament", selector: { entity: { domain: "sensor" } } }, { name: "external_color", selector: { entity: { domain: "sensor" } } }, { name: "external_percent", selector: { entity: { domain: "sensor" } } }, ]; for (let box = 0; box < 4; box += 1) { schema.push({ name: `box${box}_temp`, selector: { entity: { domain: "sensor" } } }); schema.push({ name: `box${box}_humidity`, selector: { entity: { domain: "sensor" } } }); for (let slot = 0; slot < 4; slot += 1) { schema.push({ name: `box${box}_slot${slot}_filament`, selector: { entity: { domain: "sensor" } } }); schema.push({ name: `box${box}_slot${slot}_color`, selector: { entity: { domain: "sensor" } } }); schema.push({ name: `box${box}_slot${slot}_percent`, selector: { entity: { domain: "sensor" } } }); } } this._form.schema = schema; this._form.computeLabel = (s) => { if (s.name === "name") return this._t("label_card_title"); if (s.name === "external_filament") return this._t("label_external_filament"); if (s.name === "external_color") return this._t("label_external_color"); if (s.name === "external_percent") return this._t("label_external_percent"); const boxMatch = s.name.match(/^box(\d+)_(temp|humidity)$/); if (boxMatch) { const [, boxId, metric] = boxMatch; const key = metric === "temp" ? "label_box_temp" : "label_box_humidity"; return this._t(key, { box: Number(boxId) + 1 }); } const slotMatch = s.name.match(/^box(\d+)_slot(\d+)_(filament|color|percent)$/); if (slotMatch) { const [, boxId, slotId, metric] = slotMatch; const keyMap = { filament: "label_slot_filament", color: "label_slot_color", percent: "label_slot_percent", }; return this._t(keyMap[metric], { box: Number(boxId) + 1, slot: Number(slotId) + 1 }); } return s.name; }; if (this._form.computeHelper) { this._form.computeHelper = () => ""; } this._form.addEventListener("value-changed", (ev) => { this._cfg = { ...this._cfg, ...ev.detail.value }; this._dispatchConfigChange(); }); } _setupThemeForm() { const themeForm = this._root.getElementById("theme-form"); themeForm.hass = this._hass; themeForm.data = this._cfg; themeForm.schema = [ { name: "compact_view", selector: { boolean: {} } }, { name: "show_type_in_mini", selector: { boolean: {} } }, ]; themeForm.computeLabel = (s) => ({ compact_view: this._t("schema_compact_view"), show_type_in_mini: this._t("schema_show_type_in_mini"), }[s.name] || s.name); themeForm.addEventListener("value-changed", (ev) => { this._cfg = { ...this._cfg, ...ev.detail.value }; this._dispatchConfigChange(); }); } _dispatchConfigChange() { this.dispatchEvent(new CustomEvent("config-changed", { detail: { config: this._cfg }, bubbles: true, composed: true, })); } } customElements.define(EDITOR_TAG, KCFSCardEditor); window.customCards = window.customCards || []; window.customCards.push({ type: "k-cfs-card", name: "Creality CFS Card", preview: true, description: "A card to control the Creality Filament System (CFS)" });