diff --git a/build.js b/build.js index 923e3df..f6b905f 100644 --- a/build.js +++ b/build.js @@ -15,26 +15,40 @@ console.log('Processing languages...'); const langFiles = fs.readdirSync(SRC_DIR).filter(file => file.startsWith('lang-') && file.endsWith('.js')); let langsScript = ''; +// We will construct the translation objects +let langDefinitions = ''; +let mergeScript = ` +const editorTranslations = {}; +const cardTranslations = {}; +`; + langFiles.forEach(file => { const langCode = file.replace('lang-', '').replace('.js', ''); let content = fs.readFileSync(path.join(SRC_DIR, file), 'utf8'); - // Extract object + // Remove export default content = content.replace('export default', '').trim(); if (content.endsWith(';')) { content = content.slice(0, -1); } - const varName = langCode; + // Assign to a variable + langDefinitions += `const lang_${langCode} = ${content};\n`; - langsScript += `const ${varName} = ${content};\n`; + // Add to merge logic + mergeScript += `editorTranslations['${langCode}'] = lang_${langCode}.editor;\n`; + mergeScript += `cardTranslations['${langCode}'] = lang_${langCode}.card;\n`; }); +langsScript = langDefinitions + mergeScript; + +const imagesScript = ''; + // Process Editor console.log('Processing editor...'); let editorContent = fs.readFileSync(path.join(SRC_DIR, 'power-flux-card-editor.js'), 'utf8'); -// Remove imports -editorContent = editorContent.replace(/import .* from .*/g, ''); +// Remove imports/exports if any +editorContent = editorContent.replace(/import .* from .*/g, '').replace(/export .*/g, ''); // Process Main Card console.log('Processing main card...'); @@ -42,12 +56,6 @@ let mainContent = fs.readFileSync(path.join(SRC_DIR, 'power-flux-card.js'), 'utf // Remove imports mainContent = mainContent.replace(/import .* from .*/g, ''); -// Replace getConfigElement -mainContent = mainContent.replace( - /static async getConfigElement\(\) \{[\s\S]*?return document\.createElement\("power-flux-card-editor"\);\s*\}/, - `static async getConfigElement() { return document.createElement("power-flux-card-editor"); }` -); - // 5. Combine everything console.log('Writing output...'); const finalContent = ` diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js new file mode 100644 index 0000000..b481368 --- /dev/null +++ b/dist/power-flux-card.js @@ -0,0 +1,1664 @@ + +/** + * Power Flux Card (Bundled) + * Generated by build.js + */ +const lang_de = { + editor: { + "card.label_import": "Import", + "card.label_export": "Export", + "editor.main_title": "Haupt Entitäten", + "editor.solar_section": "Solar/PV", + "editor.grid_section": "Netz Import/Export", + "editor.battery_section": "Batterie", + "editor.consumers_section": "Zusätzliche Verbraucher", + "editor.options_section": "Darstellung & Optionen", + "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", + "editor.label_toggle": "Label im Kreis anzeigen", + "editor.compact_view": "Kompakte Ansicht (evcc)", + "editor.hide_inactive": "Inaktive Röhren ausblenden", + "editor.entity": "Entität (Watt)", + "editor.label": "Beschriftung", + "editor.icon": "Icon", + }, + card: { + "card.label_solar": "Solar", + "card.label_grid": "Netz", + "card.label_battery": "Batterie", + "card.label_house": "Verbrauch", + "card.label_car": "E-Auto", + "card.label_import": "Import", + "card.label_export": "Export", + } +}; +const lang_en = { + editor: { + "card.label_import": "Import", + "card.label_export": "Export", + "editor.main_title": "Main Entities", + "editor.solar_section": "Solar", + "editor.grid_section": "Grid Connection", + "editor.battery_section": "Battery", + "editor.consumers_section": "Additional Consumers", + "editor.options_section": "Appearance & Options", + "editor.flow_rate_title": "Show Flow Rates (W) on pipes", + "editor.label_toggle": "Show Label in Bubble", + "editor.compact_view": "Compact View (evcc)", + "editor.hide_inactive": "Hide Inactive Pipes", + "editor.entity": "Entity (Watt)", + "editor.label": "Label", + "editor.icon": "Icon", + }, + card: { + "card.label_solar": "Solar", + "card.label_grid": "Grid", + "card.label_battery": "Battery", + "card.label_house": "Consumption", + "card.label_car": "Car", + "card.label_import": "Import", + "card.label_export": "Export", + } +}; + +const editorTranslations = {}; +const cardTranslations = {}; +editorTranslations['de'] = lang_de.editor; +cardTranslations['de'] = lang_de.card; +editorTranslations['en'] = lang_en.editor; +cardTranslations['en'] = lang_en.card; + + + +const fireEvent = (node, type, detail, options) => { + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + event.detail = detail; + node.dispatchEvent(event); + return event; +}; + +const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); +const html = LitElement.prototype.html; +const css = LitElement.prototype.css; + +class PowerFluxCardEditor extends LitElement { + + static get properties() { + return { + hass: {}, + _config: { state: true }, + _subView: { state: true } // Controls which sub-page is open (null = main) + }; + } + + setConfig(config) { + this._config = config; + } + + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = editorTranslations[lang] || editorTranslations['en']; + return dict[key] || editorTranslations['en'][key] || key; + } + + _valueChanged(ev) { + if (!this._config || !this.hass) return; + + const target = ev.target; + const key = target.configValue || this._currentConfigValue; + + let value; + if (target.tagName === 'HA-SWITCH') { + value = target.checked; + } else if (ev.detail && 'value' in ev.detail) { + value = ev.detail.value; + } else { + value = target.value; + } + + if (value === null || value === undefined) { + value = ""; + } + + if (key) { + const entityKeys = [ + 'solar', 'grid', 'grid_export', + 'battery', 'battery_soc', + 'consumer_1', 'consumer_2', 'consumer_3' + ]; + + let newConfig = { ...this._config }; + + if (entityKeys.includes(key)) { + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: value }; + newConfig.entities = newEntities; + } else { + newConfig[key] = value; + + if (key === 'show_comet_tail' && value === true) { + newConfig.show_dashed_line = false; + } + if (key === 'show_dashed_line' && value === true) { + newConfig.show_comet_tail = false; + } + } + + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + } + + _goSubView(view) { + this._subView = view; + } + + _goBack() { + this._subView = null; + } + + static get styles() { + return css` + .card-config { + display: flex; + flex-direction: column; + padding-bottom: 24px; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + } + .back-btn { + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-weight: bold; + color: var(--primary-color); + } + .menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 13px; + cursor: pointer; + transition: background 0.2s; + } + .menu-item:hover { + background: rgba(var(--rgb-primary-text-color), 0.05); + } + .menu-icon { + display: flex; + align-items: center; + gap: 12px; + font-weight: bold; + } + .switch-row { + display: flex; + align-items: center; + gap: 16px; + padding: 8px 0; + margin-top: 8px; + } + .switch-label { + font-weight: bold; + } + .section-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 15px; + margin-bottom: 15px; + padding-bottom: 4px; + border-bottom: 1px solid var(--divider-color); + } + ha-selector { + width: 100%; + display: block; + margin-bottom: 12px; + } + .consumer-group { + padding: 10px; + border-radius: 8px; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 12px; + } + .consumer-title { + font-weight: bold; + margin-bottom: 8px; + color: var(--primary-text-color); + } + .separator { + border-bottom: 1px solid var(--divider-color); + margin: 10px 0; + } + `; + } + + // --- SUBVIEW RENDERING --- + + _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.solar_section')}

+
+ + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.grid_section')}

+
+ + + + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.battery_section')}

+
+ + + + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.consumers_section')}

+
+ +
+
🚗 Links (Lila)
+ + + + + +
+ +
+
♨️ Mitte (Orange)
+ + + + + +
+ +
+
🏊 Rechts (Türkis)
+ + + + + +
+ `; + } + + render() { + if (!this.hass || !this._config) { + return html``; + } + + const entities = this._config.entities || {}; + + const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; + const textSelectorSchema = { text: {} }; + const iconSelectorSchema = { icon: {} }; + + // SUBVIEW ROUTING + if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + + + // MAIN MENU VIEW + return html` +
+ +
${this._localize('editor.main_title')}
+ + + + + + + + + +
${this._localize('editor.options_section')}
+ +
+ +
+ +
+ +
Neon Glow
+
+ +
+ +
Donut Chart (Grid/Haus)
+
+ +
+ +
Comet Tail Effect
+
+ +
+ +
Dashed Line Animation
+
+ +
+ +
Farbige Textwerte
+
+ +
+ +
Icons unten ausblenden
+
+ +
+ +
${this._localize('editor.hide_inactive')}
+
+ +
+ +
${this._localize('editor.compact_view')}
+
+ +
+ `; + } +} + +customElements.define("power-flux-card-editor", PowerFluxCardEditor); + + +console.log( + "%c⚡ Power-Flux-Card v_2.0 ready", + "background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" +); + +class PowerFluxCard extends LitElement { + + static get properties() { + return { + hass: {}, + config: {}, + _cardWidth: { state: true }, + }; + } + + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = cardTranslations[lang] || cardTranslations['en']; + return dict[key] || cardTranslations['en'][key] || key; + } + + static async getConfigElement() { + return document.createElement("power-flux-card-editor"); + } + + static getStubConfig() { + return { + zoom: 0.9, + compact_view: false, + show_donut_border: false, + show_neon_glow: true, + show_comet_tail: false, + show_dashed_line: false, + show_tinted_background: false, + hide_inactive_flows: true, + show_flow_rate_solar: true, + show_flow_rate_grid: true, + show_flow_rate_battery: true, + show_label_solar: false, + show_label_grid: false, + show_label_battery: false, + show_label_house: false, + use_colored_values: false, + hide_consumer_icons: false, + entities: { + solar: "", + grid: "", + grid_export: "", + battery: "", + battery_soc: "", + consumer_1: "", + consumer_2: "", + consumer_3: "" + } + }; + } + + setConfig(config) { + if (!config.entities) { + // Init allow + } + this.config = config; + } + + firstUpdated() { + this._resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.contentRect.width > 0) { + this._cardWidth = entry.contentRect.width; + } + } + }); + this._resizeObserver.observe(this); + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + } + + static get styles() { + return css` + :host { + display: block; + --neon-yellow: #ffdd00; + --neon-blue: #3b82f6; + --neon-green: #00ff88; + --neon-pink: #ff0080; + --neon-red: #ff3333; + --grid-grey: #9e9e9e; + --export-purple: #a855f7; + --flow-dasharray: 0 380; + } + ha-card { + padding: 0; + position: relative; + overflow: hidden; + transition: height 0.3s ease; + } + + /* --- COMPACT VIEW STYLES --- */ + .compact-container { + padding: 16px 20px; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 120px; + box-sizing: border-box; + } + + .compact-bracket { + height: 24px; + width: 100%; + position: relative; + } + .bracket-svg { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + overflow: visible; /* Important for icons */ + } + .bracket-line { + fill: none; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; + transition: d 0.5s ease; + } + .compact-icon-wrapper { + position: absolute; + top: -6px; /* Default top, overridden inline */ + padding: 0 8px; + display: flex; + align-items: center; + justify-content: center; + transition: left 0.5s ease; + } + .compact-icon { + --mdc-icon-size: 20px; + } + + .compact-bar-wrapper { + height: 36px; + width: 100%; + background: #333; + border-radius: 5px; + margin: 4px 0; + display: flex; + overflow: hidden; + position: relative; + } + + .bar-segment { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: bold; + color: black; + transition: width 0.5s ease; + white-space: nowrap; + overflow: hidden; + } + + /* Source Colors */ + .src-solar { background: var(--neon-green); color: black; } + .src-grid { background: var(--grid-grey); color: black; } + .src-battery { background: var(--neon-yellow); color: black; } + + /* --- STANDARD VIEW STYLES --- */ + .scale-wrapper { + width: 420px; + transform-origin: top center; + transition: transform 0.1s linear; + margin: 0 auto; + } + + .absolute-container { + position: relative; + width: 100%; + transition: top 0.3s ease; + } + + .bubble { + width: 90px; + height: 90px; + border-radius: 50%; + background: transparent; + border: 2px solid var(--divider-color, #333); + display: block; + position: absolute; + z-index: 2; + transition: all 0.3s ease; + box-sizing: border-box; + } + + .bubble.tinted { background: rgba(255, 255, 255, 0.05); } + .bubble.tinted.solar { background: color-mix(in srgb, var(--neon-yellow), transparent 85%); } + .bubble.tinted.grid { background: color-mix(in srgb, var(--neon-blue), transparent 85%); } + .bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); } + .bubble.tinted.c1 { background: color-mix(in srgb, #a855f7, transparent 85%); } + .bubble.tinted.c2 { background: color-mix(in srgb, #f97316, transparent 85%); } + .bubble.tinted.c3 { background: color-mix(in srgb, #06b6d4, transparent 85%); } + + .bubble.house { border-color: var(--neon-pink); } + .bubble.house.tinted { background: color-mix(in srgb, var(--neon-pink), transparent 85%); } + .bubble.house.donut { border: none !important; --house-gradient: var(--neon-pink); background: transparent; } + .bubble.house.donut.tinted { background: color-mix(in srgb, var(--neon-pink), transparent 85%); } + .bubble.house.donut::before { + content: ""; position: absolute; inset: 0; border-radius: 50%; padding: 4px; + background: var(--house-gradient); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none; + } + + .icon-svg, .icon-custom { + width: 34px; height: 34px; position: absolute; top: 13px; left: 50%; margin-left: -17px; z-index: 2; display: block; + } + .icon-custom { --mdc-icon-size: 34px; } + + .sub { + font-size: 9px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; + line-height: 1.1; z-index: 2; position: absolute; top: 48px; left: 0; width: 100%; text-align: center; margin: 0; pointer-events: none; + } + + .value { + font-weight: bold; font-size: 15px; white-space: nowrap; z-index: 2; transition: color 0.3s ease; + line-height: 1.2; position: absolute; bottom: 11px; left: 0; width: 100%; text-align: center; margin: 0; + } + + @keyframes spin { 100% { transform: rotate(360deg); } } + .spin-slow { animation: spin 12s linear infinite; transform-origin: center; } + + @keyframes pulse-opacity { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } + .pulse { animation: pulse-opacity 2s ease-in-out infinite; } + + @keyframes float-y { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-2px); } } + .float { animation: float-y 3s ease-in-out infinite; } + + .solar { border-color: var(--neon-yellow); } + .battery { border-color: var(--neon-green); } + .grid { border-color: var(--neon-blue); } + .c1 { border-color: #a855f7; } + .c2 { border-color: #f97316; } + .c3 { border-color: #06b6d4; } + .inactive { border-color: var(--secondary-text-color); } + + .glow.solar { box-shadow: 0 0 15px rgba(255, 221, 0, 0.4); } + .glow.battery { box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); } + .glow.grid { box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); } + .glow.c1 { box-shadow: 0 0 15px rgba(168, 85, 247, 0.4); } + .glow.c2 { box-shadow: 0 0 15px rgba(249, 115, 22, 0.4); } + .glow.c3 { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); } + + .node-solar { top: 70px; left: 5px; } + .node-grid { top: 70px; left: 165px; } + .node-battery { top: 70px; left: 325px; } + .node-house { top: 220px; left: 165px; } + .node-c1 { top: 370px; left: 5px; } + .node-c2 { top: 370px; left: 165px; } + .node-c3 { top: 370px; left: 325px; } + + svg { position: absolute; top: 7; left: 25; width: 100%; height: 100%; z-index: 1; pointer-events: none; } + + .bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; } + .bg-solar { stroke: var(--neon-yellow); } + .bg-grid { stroke: var(--neon-blue); } + .bg-battery { stroke: var(--neon-green); } + .bg-export { stroke: var(--neon-red); } + .bg-c1 { stroke: #a855f7; } + .bg-c2 { stroke: #f97316; } + .bg-c3 { stroke: #06b6d4; } + + .flow-line { + fill: none; stroke-width: var(--flow-stroke-width, 8px); stroke-linecap: round; stroke-dasharray: var(--flow-dasharray); + animation: dash linear infinite; opacity: 0; transition: opacity 0.5s; + } + .flow-solar { stroke: var(--neon-yellow); } + .flow-grid { stroke: var(--neon-blue); } + .flow-battery { stroke: var(--neon-green); } + .flow-export { stroke: var(--neon-red); } + + @keyframes dash { to { stroke-dashoffset: -1500; } } + + .flow-text { + font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.8)); transition: opacity 0.3s ease; + } + .flow-text.no-shadow { filter: none; } + .text-solar { fill: var(--neon-yellow); } + .text-grid { fill: var(--neon-blue); } + .text-export { fill: var(--neon-red); } + .text-battery { fill: var(--neon-green); } + `; + } + + // --- SVG ICON RENDERER --- + _renderIcon(type, val = 0, colorOverride = null) { + if (type === 'solar') { + const animate = Math.round(val) > 0 ? 'spin-slow' : ''; + const color = colorOverride || 'var(--neon-yellow)'; + return html``; + } + if (type === 'grid') { + const animate = Math.round(val) > 0 ? 'pulse' : ''; + const color = colorOverride || 'var(--neon-blue)'; + return html``; + } + if (type === 'battery') { + const soc = Math.min(Math.max(val, 0), 100) / 100; + const rectHeight = 14 * soc; + const rectY = 18 - rectHeight; + const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; + return html``; + } + if (type === 'house') { + const strokeColor = colorOverride || 'var(--neon-pink)'; + return html``; + } + if (type === 'car') { + return html``; + } + if (type === 'heater') { + return html``; + } + if (type === 'pool') { + return html``; + } + return html``; + } + + _formatPower(val) { + if (val === 0) return "0"; + if (Math.abs(val) >= 1000) { + return (val / 1000).toFixed(1) + " kW"; + } + return Math.round(val) + " W"; + } + + // --- DOM NODE SVG GENERATOR --- + _renderSVGPath(d, color) { + const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); + path.setAttribute("d", d); + path.setAttribute("class", "bracket-line"); + path.setAttribute("stroke", color); + path.setAttribute("stroke-width", "1.5"); + path.setAttribute("fill", "none"); + path.style.stroke = color; + path.style.fill = "none"; + return path; + } + + // --- SQUARE BRACKET GENERATOR --- + _createBracketPath(startPx, widthPx, direction) { + if (widthPx < 5) return ""; + + const r = 5; + const startX = startPx; + const endX = startPx + widthPx; + + let yBase, yLine; + + if (direction === 'down') { + yBase = 24; + yLine = 4; + } else { + yBase = 0; + yLine = 20; + } + + const height = Math.abs(yBase - yLine); + const rEff = Math.min(r, height / 2, widthPx / 2); + + const yCorner = direction === 'down' ? yLine + rEff : yLine - rEff; + + return ` + M ${startX} ${yBase} + L ${startX} ${yCorner} + Q ${startX} ${yLine} ${startX + rEff} ${yLine} + L ${endX - rEff} ${yLine} + Q ${endX} ${yLine} ${endX} ${yCorner} + L ${endX} ${yBase} + `; + } + + // --- RENDER COMPACT VIEW --- + _renderCompactView(entities) { + // 1. Get Values + const getVal = (entity) => { + const state = this.hass.states[entity]; + return state ? parseFloat(state.state) || 0 : 0; + }; + + const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; + const gridMain = entities.grid ? getVal(entities.grid) : 0; + const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; + const battery = entities.battery ? getVal(entities.battery) : 0; + const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + + // 2. Logic Calculation + let gridImport = 0; + let gridExport = 0; + + if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExportSensor); + } else { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; + } + + const batteryCharge = battery > 0 ? battery : 0; + const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + + let solarToBatt = 0; + let gridToBatt = 0; + + if (batteryCharge > 0) { + if (solar >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solar; + gridToBatt = batteryCharge - solar; + } + } + + const solarTotalToCons = Math.max(0, solar - solarToBatt - gridExport); + const gridTotalToCons = Math.max(0, gridImport - gridToBatt); + const battTotalToCons = batteryDischarge; + + const totalCons = solarTotalToCons + gridTotalToCons + battTotalToCons; + + // Calculate Splits + let evPower = 0; + let housePower = totalCons; + + if (c1Val > 0 && totalCons > 0) { + evPower = Math.min(c1Val, totalCons); + housePower = totalCons - evPower; + } + + // Calculate Total Bar Width (Flux) + // The Bar represents: Battery Discharge + Solar + Grid Import + // This MUST equal: House + EV + Export + Battery Charge + + // SOURCES (for Bar Segments) + const srcBattery = batteryDischarge; + const srcSolar = solar; // Solar includes Export + Charge + Cons + const srcGrid = gridImport; + + const totalFlux = srcBattery + srcSolar + srcGrid; + + // DESTINATIONS (for Bottom Brackets) + const destHouse = housePower; + const destEV = evPower; + const destExport = gridExport; + // Note: Battery Charge is also a destination (internal flow), but usually not bracketed if we only want "Consumers" + // If we don't bracket Charge, there will be a gap. We can accept that or add a Charge bracket. + // Given user request "Only EV... and Grid Export", we stick to those. + + const threshold = 0.1; + const availableWidth = (this._cardWidth && this._cardWidth > 0) ? this._cardWidth : (this.offsetWidth || 400); + const fullWidth = availableWidth - 40; + + if (totalFlux <= threshold) { + return html`
Waiting for data...
`; + } + + // --- GENERATE BAR SEGMENTS (Aggregated by Source) --- + // Order: Battery -> Solar -> Grid + const barSegments = []; + let currentX = 0; + + const addSegment = (val, color, type, label) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; + barSegments.push({ + val, + color, + widthPct: pct * 100, + widthPx: width, + startPx: currentX, + type, + label + }); + currentX += width; + } + + addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery'); + addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar'); + addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid'); + + // --- GENERATE TOP BRACKETS (Based on Bar Segments) --- + const topBrackets = barSegments.map(s => { + const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); + let icon = ''; + let iconColor = ''; + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-green)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } + + return { path, width: s.widthPx, center: s.startPx + (s.widthPx/2), icon, iconColor }; + }); + + // --- GENERATE BOTTOM BRACKETS (Independent Calculation) --- + // Order: House -> EV -> Export + const bottomBrackets = []; + let bottomX = 0; + + const addBottomBracket = (val, type) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; + + let icon = ''; + let iconColor = ''; + + if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } + if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } + + const path = this._createBracketPath(bottomX, width, 'up'); + bottomBrackets.push({ + path, + width: width, + center: bottomX + (width/2), + icon, + iconColor + }); + bottomX += width; + }; + + addBottomBracket(destHouse, 'house'); + addBottomBracket(destEV, 'car'); + addBottomBracket(destExport, 'export'); + + // Note: If there is Battery Charging happening, bottomX will not reach fullWidth. + // This leaves a gap at the end (or between segments depending on logic), which is visually correct + // as "Internal/Stored Energy" is not an external output. + + return html` + +
+ +
+ + ${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} + + ${topBrackets.map(b => b.width > 20 ? html` +
+ +
` : '')} +
+ + +
+ ${barSegments.map(s => html` +
+ ${s.widthPx > 35 ? this._formatPower(s.val) : ''} +
+ `)} +
+ + +
+ + ${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} + + ${bottomBrackets.map(b => b.width > 20 ? html` +
+ +
` : '')} +
+
+
+ `; + } + + // --- RENDER STANDARD VIEW --- + _renderStandardView(entities) { + // FIX: Default to hidden unless explicitly set to false + const hideInactive = this.config.hide_inactive_flows !== false; + + const globalFlowRate = this.config.show_flow_rates !== false; + + // FLOW RATE TOGGLES + const showFlowSolar = this.config.show_flow_rate_solar !== undefined ? this.config.show_flow_rate_solar : globalFlowRate; + const showFlowGrid = this.config.show_flow_rate_grid !== undefined ? this.config.show_flow_rate_grid : globalFlowRate; + const showFlowBattery = this.config.show_flow_rate_battery !== undefined ? this.config.show_flow_rate_battery : globalFlowRate; + + // LABEL TOGGLES + const showLabelSolar = this.config.show_label_solar === true; + const showLabelGrid = this.config.show_label_grid === true; + const showLabelBattery = this.config.show_label_battery === true; + const showLabelHouse = this.config.show_label_house === true; + + const useColoredValues = this.config.use_colored_values === true; + const showDonut = this.config.show_donut_border === true; + const showTail = this.config.show_comet_tail === true; + const showDashedLine = this.config.show_dashed_line === true; + const showTint = this.config.show_tinted_background === true; + const hideConsumerIcons = this.config.hide_consumer_icons === true; + const showNeonGlow = this.config.show_neon_glow !== false; + + // CUSTOM LABELS + const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); + const labelGridText = this.config.grid_label || this._localize('card.label_import'); + const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); + const labelHouseText = this.config.house_label || this._localize('card.label_house'); + + // CUSTOM ICONS + const iconSolar = this.config.solar_icon; + const iconGrid = this.config.grid_icon; + const iconBattery = this.config.battery_icon; + + // Determine existence of main entities + const hasSolar = !!(entities.solar && entities.solar !== ""); + const hasGrid = !!(entities.grid && entities.grid !== ""); + const hasBattery = !!(entities.battery && entities.battery !== ""); + + const styleSolar = hasSolar ? '' : 'display: none;'; + const styleSolarBatt = (hasSolar && hasBattery) ? '' : 'display: none;'; + const styleGrid = hasGrid ? '' : 'display: none;'; + const styleGridBatt = (hasGrid && hasBattery) ? '' : 'display: none;'; + const styleBattery = hasBattery ? '' : 'display: none;'; + + const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; + + // Custom Labels for Consumers + const labelC1 = this.config.consumer_1_label || "E-Auto"; + const labelC2 = this.config.consumer_2_label || "Heizung"; + const labelC3 = this.config.consumer_3_label || "Pool"; + + const getVal = (entity) => { + const state = this.hass.states[entity]; + return state ? parseFloat(state.state) || 0 : 0; + }; + + const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; + const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; + + const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0); + const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0); + const showC3 = (entities.consumer_3 && Math.round(c3Val) > 0); + const anyBottomVisible = showC1 || showC2 || showC3; + + const solar = hasSolar ? getVal(entities.solar) : 0; + const gridMain = hasGrid ? getVal(entities.grid) : 0; + const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; + const battery = hasBattery ? getVal(entities.battery) : 0; + const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; + + const solarVal = Math.max(0, solar); + + let gridImport = 0; + let gridExport = 0; + + if (hasGrid) { + if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExpSensor); + } else { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; + } + } + + const batteryCharge = battery > 0 ? battery : 0; + const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + + let solarToBatt = 0; + let gridToBatt = 0; + + if (hasBattery && batteryCharge > 0) { + if (solarVal >= batteryCharge) { + solarToBatt = batteryCharge; + gridToBatt = 0; + } else { + solarToBatt = solarVal; + gridToBatt = batteryCharge - solarVal; + } + } + + const solarToHouse = Math.max(0, solarVal - solarToBatt - gridExport); + const gridToHouse = Math.max(0, gridImport - gridToBatt); + const house = solarToHouse + gridToHouse + batteryDischarge; + + const isTopArcActive = (solarToBatt > 0); + const topShift = isTopArcActive ? 0 : 50; + let baseHeight = anyBottomVisible ? 480 : 340; + const contentHeight = baseHeight - topShift; + + const designWidth = 420; + const availableWidth = this._cardWidth || designWidth; + let scale = availableWidth / designWidth; + const userZoom = this.config.zoom !== undefined ? this.config.zoom : 0.9; + scale = scale * userZoom; + + if (scale < 0.5) scale = 0.5; + if (scale > 1.5) scale = 1.5; + + const finalCardHeightPx = contentHeight * scale; + + let houseGradientVal = ''; + let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + const tintClass = showTint ? 'tinted' : ''; + const glowClass = showNeonGlow ? 'glow' : ''; + + let houseDominantColor = 'var(--neon-pink)'; + if (house > 0) { + if (solarToHouse >= gridToHouse && solarToHouse >= batteryDischarge) { + houseDominantColor = 'var(--neon-yellow)'; + } else if (gridToHouse >= solarToHouse && gridToHouse >= batteryDischarge) { + houseDominantColor = 'var(--neon-blue)'; + } else if (batteryDischarge >= solarToHouse && batteryDischarge >= gridToHouse) { + houseDominantColor = 'var(--neon-green)'; + } + } + + if (showDonut) { + if (house > 0) { + const pctSolar = (solarToHouse / house) * 100; + const pctGrid = (gridToHouse / house) * 100; + const pctBatt = (batteryDischarge / house) * 100; + + let stops = []; + let current = 0; + if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } + if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } + if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } + if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } + + houseGradientVal = `conic-gradient(${stops.join(', ')})`; + + if (useColoredValues) { + const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); + if (maxVal > 0) { + if (maxVal === solarToHouse) houseTextCol = 'var(--neon-yellow)'; + else if (maxVal === gridToHouse) houseTextCol = 'var(--neon-blue)'; + else if (maxVal === batteryDischarge) houseTextCol = 'var(--neon-green)'; + } else { + houseTextCol = 'var(--neon-pink)'; + } + } + } else { + houseGradientVal = `var(--neon-pink)`; + houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + } + } else { + houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + } + + const houseTintStyle = showTint + ? `background: color-mix(in srgb, ${houseDominantColor}, transparent 85%);` + : ''; + + const houseGlowStyle = showNeonGlow + ? `box-shadow: 0 0 15px color-mix(in srgb, ${houseDominantColor}, transparent 60%);` + : `box-shadow: none;`; + + const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; + + const isSolarActive = Math.round(solarVal) > 0; + const isGridActive = Math.round(gridImport) > 0; + + const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; + const gridColor = isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'; + + const getAnimStyle = (val) => { + if (val <= 1) return "opacity: 0;"; + const userMinDuration = 7; + const userMaxDuration = 11; + const userFactor = 20000; + let duration = userFactor / val; + duration = Math.max(userMinDuration, Math.min(userMaxDuration, duration)); + + // Adjust speed for dashed line (Factor to slow down: 5x) + if (showDashedLine) { + duration = duration * 5; + } + + return `opacity: 1; animation-duration: ${duration}s;`; + }; + + const getPipeStyle = (val) => { + if (!hideInactive) return "opacity: 0.2;"; + return val > 1 ? "opacity: 0.2;" : "opacity: 0;"; + }; + + const getTextStyle = (val, type) => { + let isVisible = false; + if (type === 'solar') isVisible = showFlowSolar; + else if (type === 'grid') isVisible = showFlowGrid; + else if (type === 'battery') isVisible = showFlowBattery; + + if (!isVisible) return "display: none;"; + return val > 5 ? "opacity: 1;" : "opacity: 0;"; + }; + + const getColorStyle = (colorVar) => { + return useColoredValues ? `color: var(${colorVar});` : ''; + }; + const getConsumerColorStyle = (hex) => { + return useColoredValues ? `color: ${hex};` : ''; + } + + const renderLabel = (text, isVisible) => { + if (!isVisible) return html``; + return html`
${text}
`; + }; + + const renderMainIcon = (type, val, customIcon, color = null) => { + if (customIcon) { + const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); + return html``; + } + return this._renderIcon(type, val, color); + }; + + const getCustomClass = (icon) => icon ? 'has-custom-icon' : ''; + + const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { + if (!isVisible) return html``; + + const customIcon = this.config[`${configKey}_icon`]; + let iconContent; + + const isCustom = !hideConsumerIcons && !!customIcon; + const dynamicClass = isCustom ? 'has-custom-icon' : ''; + + if (hideConsumerIcons) { + iconContent = html``; + } else if (customIcon) { + iconContent = html``; + } else { + iconContent = this._renderIcon(iconType, val); + } + + return html` +
+ ${iconContent} + ${renderLabel(label, true)} +
${this._formatPower(val)}
+
+ `; + }; + + const getConsumerPipeStyle = (isActive, val) => { + if (!isActive) return "display: none;"; + return getPipeStyle(val); + }; + + const getConsumerAnimStyle = (isActive, val) => { + if (!isActive) return "display: none;"; + return getAnimStyle(val); + }; + + const pathSolarHouse = "M 50 160 Q 50 265 165 265"; + const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; + const pathGridImport = "M 210 160 L 210 220"; + const pathGridExport = "M 165 115 Q 130 145 95 115"; + const pathGridToBatt = "M 255 115 Q 290 145 325 115"; + const pathBattHouse = "M 370 160 Q 370 265 255 265"; + const pathHouseC1 = "M 165 265 Q 50 265 50 370"; + const pathHouseC2 = "M 210 310 L 210 370"; + const pathHouseC3 = "M 255 265 Q 370 265 370 370"; + + const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; + const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); + const strokeWidthVal = showDashedLine ? 4 : 8; + + return html` + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${this._formatPower(solarToHouse)} + ${this._formatPower(solarToBatt)} + + ${this._formatPower(gridToHouse)} + ${this._formatPower(gridExport)} + ${this._formatPower(gridToBatt)} + + ${this._formatPower(batteryDischarge)} + + + + ${hasSolar ? html` +
+ ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} + ${renderLabel(labelSolarText, showLabelSolar)} +
${this._formatPower(solarVal)}
+
` : ''} + + ${hasGrid ? html` +
+ ${renderMainIcon('grid', gridImport, iconGrid, gridColor)} + ${renderLabel(labelGridText, showLabelGrid)} +
${this._formatPower(gridImport)}
+
` : ''} + + ${hasBattery ? html` +
+ ${renderMainIcon('battery', battSoc, iconBattery)} + ${renderLabel(labelBatteryText, showLabelBattery)} +
${Math.round(battSoc)}%
+
` : ''} + +
+ ${renderMainIcon('house', 0, null, houseDominantColor)} + ${renderLabel(labelHouseText, showLabelHouse)} +
${this._formatPower(house)}
+
+ + ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, '#a855f7')} + ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, '#f97316')} + ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, '#06b6d4')} + +
+
+
+ `; + } + + render() { + if (!this.config || !this.hass) return html``; + + // SWITCH VIEW BASED ON CONFIG + if (this.config.compact_view === true) { + return this._renderCompactView(this.config.entities || {}); + } else { + return this._renderStandardView(this.config.entities || {}); + } + } +} + +customElements.define("power-flux-card", PowerFluxCard); + +window.customCards = window.customCards || []; +window.customCards.push({ + type: "power-flux-card", + name: "Power Flux Card", + description: "Advanced Animated Energy Flow Card", +}); diff --git a/src/lang-de.js b/src/lang-de.js new file mode 100644 index 0000000..332556d --- /dev/null +++ b/src/lang-de.js @@ -0,0 +1,28 @@ +export default { + editor: { + "card.label_import": "Import", + "card.label_export": "Export", + "editor.main_title": "Haupt Entitäten", + "editor.solar_section": "Solar/PV", + "editor.grid_section": "Netz Import/Export", + "editor.battery_section": "Batterie", + "editor.consumers_section": "Zusätzliche Verbraucher", + "editor.options_section": "Darstellung & Optionen", + "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", + "editor.label_toggle": "Label im Kreis anzeigen", + "editor.compact_view": "Kompakte Ansicht (evcc)", + "editor.hide_inactive": "Inaktive Röhren ausblenden", + "editor.entity": "Entität (Watt)", + "editor.label": "Beschriftung", + "editor.icon": "Icon", + }, + card: { + "card.label_solar": "Solar", + "card.label_grid": "Netz", + "card.label_battery": "Batterie", + "card.label_house": "Verbrauch", + "card.label_car": "E-Auto", + "card.label_import": "Import", + "card.label_export": "Export", + } +}; diff --git a/src/lang-en.js b/src/lang-en.js new file mode 100644 index 0000000..bde372d --- /dev/null +++ b/src/lang-en.js @@ -0,0 +1,28 @@ +export default { + editor: { + "card.label_import": "Import", + "card.label_export": "Export", + "editor.main_title": "Main Entities", + "editor.solar_section": "Solar", + "editor.grid_section": "Grid Connection", + "editor.battery_section": "Battery", + "editor.consumers_section": "Additional Consumers", + "editor.options_section": "Appearance & Options", + "editor.flow_rate_title": "Show Flow Rates (W) on pipes", + "editor.label_toggle": "Show Label in Bubble", + "editor.compact_view": "Compact View (evcc)", + "editor.hide_inactive": "Hide Inactive Pipes", + "editor.entity": "Entity (Watt)", + "editor.label": "Label", + "editor.icon": "Icon", + }, + card: { + "card.label_solar": "Solar", + "card.label_grid": "Grid", + "card.label_battery": "Battery", + "card.label_house": "Consumption", + "card.label_car": "Car", + "card.label_import": "Import", + "card.label_export": "Export", + } +}; diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js new file mode 100644 index 0000000..84ed4d9 --- /dev/null +++ b/src/power-flux-card-editor.js @@ -0,0 +1,610 @@ +const fireEvent = (node, type, detail, options) => { + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + event.detail = detail; + node.dispatchEvent(event); + return event; +}; + +const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); +const html = LitElement.prototype.html; +const css = LitElement.prototype.css; + +class PowerFluxCardEditor extends LitElement { + + static get properties() { + return { + hass: {}, + _config: { state: true }, + _subView: { state: true } // Controls which sub-page is open (null = main) + }; + } + + setConfig(config) { + this._config = config; + } + + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = editorTranslations[lang] || editorTranslations['en']; + return dict[key] || editorTranslations['en'][key] || key; + } + + _valueChanged(ev) { + if (!this._config || !this.hass) return; + + const target = ev.target; + const key = target.configValue || this._currentConfigValue; + + let value; + if (target.tagName === 'HA-SWITCH') { + value = target.checked; + } else if (ev.detail && 'value' in ev.detail) { + value = ev.detail.value; + } else { + value = target.value; + } + + if (value === null || value === undefined) { + value = ""; + } + + if (key) { + const entityKeys = [ + 'solar', 'grid', 'grid_export', + 'battery', 'battery_soc', + 'consumer_1', 'consumer_2', 'consumer_3' + ]; + + let newConfig = { ...this._config }; + + if (entityKeys.includes(key)) { + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: value }; + newConfig.entities = newEntities; + } else { + newConfig[key] = value; + + if (key === 'show_comet_tail' && value === true) { + newConfig.show_dashed_line = false; + } + if (key === 'show_dashed_line' && value === true) { + newConfig.show_comet_tail = false; + } + } + + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + } + + _goSubView(view) { + this._subView = view; + } + + _goBack() { + this._subView = null; + } + + static get styles() { + return css` + .card-config { + display: flex; + flex-direction: column; + padding-bottom: 24px; + } + .header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + } + .back-btn { + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + font-weight: bold; + color: var(--primary-color); + } + .menu-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 13px; + cursor: pointer; + transition: background 0.2s; + } + .menu-item:hover { + background: rgba(var(--rgb-primary-text-color), 0.05); + } + .menu-icon { + display: flex; + align-items: center; + gap: 12px; + font-weight: bold; + } + .switch-row { + display: flex; + align-items: center; + gap: 16px; + padding: 8px 0; + margin-top: 8px; + } + .switch-label { + font-weight: bold; + } + .section-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 15px; + margin-bottom: 15px; + padding-bottom: 4px; + border-bottom: 1px solid var(--divider-color); + } + ha-selector { + width: 100%; + display: block; + margin-bottom: 12px; + } + .consumer-group { + padding: 10px; + border-radius: 8px; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 12px; + } + .consumer-title { + font-weight: bold; + margin-bottom: 8px; + color: var(--primary-text-color); + } + .separator { + border-bottom: 1px solid var(--divider-color); + margin: 10px 0; + } + `; + } + + // --- SUBVIEW RENDERING --- + + _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.solar_section')}

+
+ + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.grid_section')}

+
+ + + + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.battery_section')}

+
+ + + + + +
+ + + + + +
+ +
+ +
${this._localize('editor.label_toggle')}
+
+ +
+ +
${this._localize('editor.flow_rate_title')}
+
+ `; + } + + _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html` +
+
+ Zurück +
+

${this._localize('editor.consumers_section')}

+
+ +
+
🚗 Links (Lila)
+ + + + + +
+ +
+
♨️ Mitte (Orange)
+ + + + + +
+ +
+
🏊 Rechts (Türkis)
+ + + + + +
+ `; + } + + render() { + if (!this.hass || !this._config) { + return html``; + } + + const entities = this._config.entities || {}; + + const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; + const textSelectorSchema = { text: {} }; + const iconSelectorSchema = { icon: {} }; + + // SUBVIEW ROUTING + if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + + + // MAIN MENU VIEW + return html` +
+ +
${this._localize('editor.main_title')}
+ + + + + + + + + +
${this._localize('editor.options_section')}
+ +
+ +
+ +
+ +
Neon Glow
+
+ +
+ +
Donut Chart (Grid/Haus)
+
+ +
+ +
Comet Tail Effect
+
+ +
+ +
Dashed Line Animation
+
+ +
+ +
Farbige Textwerte
+
+ +
+ +
Icons unten ausblenden
+
+ +
+ +
${this._localize('editor.hide_inactive')}
+
+ +
+ +
${this._localize('editor.compact_view')}
+
+ +
+ `; + } +} + +customElements.define("power-flux-card-editor", PowerFluxCardEditor); diff --git a/src/power-flux-card.js b/src/power-flux-card.js index 41102e2..6c70037 100644 --- a/src/power-flux-card.js +++ b/src/power-flux-card.js @@ -3,677 +3,6 @@ console.log( "background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" ); -const fireEvent = (node, type, detail, options) => { - options = options || {}; - detail = detail === null || detail === undefined ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed, - }); - event.detail = detail; - node.dispatchEvent(event); - return event; -}; - -const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); -const html = LitElement.prototype.html; -const css = LitElement.prototype.css; - -// --- EDITOR TRANSLATIONS --- -const editorTranslations = { - de: { - "card.label_import": "Import", - "card.label_export": "Export", - "editor.main_title": "Haupt Entitäten", - "editor.solar_section": "Solar/PV", - "editor.grid_section": "Netz Import/Export", - "editor.battery_section": "Batterie", - "editor.consumers_section": "Zusätzliche Verbraucher", - "editor.options_section": "Darstellung & Optionen", - "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", - "editor.label_toggle": "Label im Kreis anzeigen", - "editor.compact_view": "Kompakte Ansicht (evcc)", - "editor.hide_inactive": "Inaktive Röhren ausblenden", - "editor.entity": "Entität (Watt)", - "editor.label": "Beschriftung", - "editor.icon": "Icon", - }, - en: { - "card.label_import": "Import", - "card.label_export": "Export", - "editor.main_title": "Main Entities", - "editor.solar_section": "Solar", - "editor.grid_section": "Grid Connection", - "editor.battery_section": "Battery", - "editor.consumers_section": "Additional Consumers", - "editor.options_section": "Appearance & Options", - "editor.flow_rate_title": "Show Flow Rates (W) on pipes", - "editor.label_toggle": "Show Label in Bubble", - "editor.compact_view": "Compact View (evcc)", - "editor.hide_inactive": "Hide Inactive Pipes", - "editor.entity": "Entity (Watt)", - "editor.label": "Label", - "editor.icon": "Icon", - } -}; - -class PowerFluxCardEditor extends LitElement { - - static get properties() { - return { - hass: {}, - _config: { state: true }, - _subView: { state: true } // Controls which sub-page is open (null = main) - }; - } - - setConfig(config) { - this._config = config; - } - - _localize(key) { - const lang = this.hass && this.hass.language ? this.hass.language : 'en'; - const dict = editorTranslations[lang] || editorTranslations['en']; - return dict[key] || editorTranslations['en'][key] || key; - } - - _valueChanged(ev) { - if (!this._config || !this.hass) return; - - const target = ev.target; - const key = target.configValue || this._currentConfigValue; - - let value; - if (target.tagName === 'HA-SWITCH') { - value = target.checked; - } else if (ev.detail && 'value' in ev.detail) { - value = ev.detail.value; - } else { - value = target.value; - } - - if (value === null || value === undefined) { - value = ""; - } - - if (key) { - const entityKeys = [ - 'solar', 'grid', 'grid_export', - 'battery', 'battery_soc', - 'consumer_1', 'consumer_2', 'consumer_3' - ]; - - let newConfig = { ...this._config }; - - if (entityKeys.includes(key)) { - const currentEntities = newConfig.entities || {}; - const newEntities = { ...currentEntities, [key]: value }; - newConfig.entities = newEntities; - } else { - newConfig[key] = value; - - if (key === 'show_comet_tail' && value === true) { - newConfig.show_dashed_line = false; - } - if (key === 'show_dashed_line' && value === true) { - newConfig.show_comet_tail = false; - } - } - - this._config = newConfig; - fireEvent(this, "config-changed", { config: this._config }); - } - } - - _goSubView(view) { - this._subView = view; - } - - _goBack() { - this._subView = null; - } - - static get styles() { - return css` - .card-config { - display: flex; - flex-direction: column; - padding-bottom: 24px; - } - .header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; - } - .back-btn { - cursor: pointer; - display: flex; - align-items: center; - gap: 8px; - font-weight: bold; - color: var(--primary-color); - } - .menu-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - border-bottom: 1px solid var(--divider-color); - margin-bottom: 13px; - cursor: pointer; - transition: background 0.2s; - } - .menu-item:hover { - background: rgba(var(--rgb-primary-text-color), 0.05); - } - .menu-icon { - display: flex; - align-items: center; - gap: 12px; - font-weight: bold; - } - .switch-row { - display: flex; - align-items: center; - gap: 16px; - padding: 8px 0; - margin-top: 8px; - } - .switch-label { - font-weight: bold; - } - .section-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 15px; - margin-bottom: 15px; - padding-bottom: 4px; - border-bottom: 1px solid var(--divider-color); - } - ha-selector { - width: 100%; - display: block; - margin-bottom: 12px; - } - .consumer-group { - padding: 10px; - border-radius: 8px; - border-bottom: 1px solid var(--divider-color); - margin-bottom: 12px; - } - .consumer-title { - font-weight: bold; - margin-bottom: 8px; - color: var(--primary-text-color); - } - .separator { - border-bottom: 1px solid var(--divider-color); - margin: 10px 0; - } - `; - } - - // --- SUBVIEW RENDERING --- - - _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` -
-
- Zurück -
-

${this._localize('editor.solar_section')}

-
- - - -
- - - - - -
- -
- -
${this._localize('editor.label_toggle')}
-
- -
- -
${this._localize('editor.flow_rate_title')}
-
- `; - } - - _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` -
-
- Zurück -
-

${this._localize('editor.grid_section')}

-
- - - - - -
- - - - - -
- -
- -
${this._localize('editor.label_toggle')}
-
- -
- -
${this._localize('editor.flow_rate_title')}
-
- `; - } - - _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` -
-
- Zurück -
-

${this._localize('editor.battery_section')}

-
- - - - - -
- - - - - -
- -
- -
${this._localize('editor.label_toggle')}
-
- -
- -
${this._localize('editor.flow_rate_title')}
-
- `; - } - - _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` -
-
- Zurück -
-

${this._localize('editor.consumers_section')}

-
- -
-
🚗 Links (Lila)
- - - - - -
- -
-
♨️ Mitte (Orange)
- - - - - -
- -
-
🏊 Rechts (Türkis)
- - - - - -
- `; - } - - render() { - if (!this.hass || !this._config) { - return html``; - } - - const entities = this._config.entities || {}; - - const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; - const textSelectorSchema = { text: {} }; - const iconSelectorSchema = { icon: {} }; - - // SUBVIEW ROUTING - if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - - - // MAIN MENU VIEW - return html` -
- -
${this._localize('editor.main_title')}
- - - - - - - - - -
${this._localize('editor.options_section')}
- -
- -
- -
- -
Neon Glow
-
- -
- -
Donut Chart (Grid/Haus)
-
- -
- -
Comet Tail Effect
-
- -
- -
Dashed Line Animation
-
- -
- -
Farbige Textwerte
-
- -
- -
Icons unten ausblenden
-
- -
- -
${this._localize('editor.hide_inactive')}
-
- -
- -
${this._localize('editor.compact_view')}
-
- -
- `; - } -} - -customElements.define("power-flux-card-editor", PowerFluxCardEditor); - -// --- CARD TRANSLATIONS --- -const cardTranslations = { - de: { - "card.label_solar": "Solar", - "card.label_grid": "Netz", - "card.label_battery": "Batterie", - "card.label_house": "Verbrauch", - "card.label_car": "E-Auto", - "card.label_import": "Import", - "card.label_export": "Export", - }, - en: { - "card.label_solar": "Solar", - "card.label_grid": "Grid", - "card.label_battery": "Battery", - "card.label_house": "Consumption", - "card.label_car": "Car", - "card.label_import": "Import", - "card.label_export": "Export", - } -}; - class PowerFluxCard extends LitElement { static get properties() {