This commit is contained in:
jayjojayson 2026-02-17 23:01:12 +01:00
parent d5a4eb33ef
commit 181b64c590
6 changed files with 1242 additions and 1046 deletions

View file

@ -14,6 +14,7 @@ const lang_de = {
"editor.consumers_section": "Zusätzliche Verbraucher", "editor.consumers_section": "Zusätzliche Verbraucher",
"editor.options_section": "Darstellung & Optionen", "editor.options_section": "Darstellung & Optionen",
"editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen",
"editor.invert_battery": "Wert umkehren (+/-)",
"editor.label_toggle": "Label im Kreis anzeigen", "editor.label_toggle": "Label im Kreis anzeigen",
"editor.compact_view": "Kompakte Ansicht (evcc)", "editor.compact_view": "Kompakte Ansicht (evcc)",
"editor.hide_inactive": "Inaktive Röhren ausblenden", "editor.hide_inactive": "Inaktive Röhren ausblenden",
@ -42,6 +43,7 @@ const lang_en = {
"editor.consumers_section": "Additional Consumers", "editor.consumers_section": "Additional Consumers",
"editor.options_section": "Appearance & Options", "editor.options_section": "Appearance & Options",
"editor.flow_rate_title": "Show Flow Rates (W) on pipes", "editor.flow_rate_title": "Show Flow Rates (W) on pipes",
"editor.invert_battery": "Invert Power Value (+/-)",
"editor.label_toggle": "Show Label in Bubble", "editor.label_toggle": "Show Label in Bubble",
"editor.compact_view": "Compact View (evcc)", "editor.compact_view": "Compact View (evcc)",
"editor.hide_inactive": "Hide Inactive Pipes", "editor.hide_inactive": "Hide Inactive Pipes",
@ -69,6 +71,19 @@ cardTranslations['en'] = lang_en.card;
const editorTranslations = {
"en": lang_en.editor,
"de": lang_de.editor
};
const cardTranslations = {
"en": lang_en.card,
"de": lang_de.card
};
const fireEvent = (node, type, detail, options) => { const fireEvent = (node, type, detail, options) => {
options = options || {}; options = options || {};
detail = detail === null || detail === undefined ? {} : detail; detail = detail === null || detail === undefined ? {} : detail;
@ -129,6 +144,7 @@ class PowerFluxCardEditor extends LitElement {
const entityKeys = [ const entityKeys = [
'solar', 'grid', 'grid_export', 'solar', 'grid', 'grid_export',
'battery', 'battery_soc', 'battery', 'battery_soc',
'house',
'consumer_1', 'consumer_2', 'consumer_3' 'consumer_1', 'consumer_2', 'consumer_3'
]; ];
@ -440,6 +456,15 @@ class PowerFluxCardEditor extends LitElement {
></ha-switch> ></ha-switch>
<div class="switch-label">${this._localize('editor.flow_rate_title')}</div> <div class="switch-label">${this._localize('editor.flow_rate_title')}</div>
</div> </div>
<div class="switch-row">
<ha-switch
.checked=${this._config.invert_battery === true}
.configValue=${'invert_battery'}
@change=${this._valueChanged}
></ha-switch>
<div class="switch-label">${this._localize('editor.invert_battery')}</div>
</div>
`; `;
} }
@ -452,6 +477,21 @@ class PowerFluxCardEditor extends LitElement {
<h2>${this._localize('editor.consumers_section')}</h2> <h2>${this._localize('editor.consumers_section')}</h2>
</div> </div>
<div class="consumer-group">
<div class="consumer-title">🏠 Gesamthausverbrauch (Optional)</div>
<ha-selector
.hass=${this.hass}
.selector=${entitySelectorSchema}
.value=${entities.house || ""}
.configValue=${'house'}
.label=${'Sensor für Hausverbrauch (Optional)'}
@value-changed=${this._valueChanged}
></ha-selector>
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
Wird benötigt, damit das Haus-Icon anklickbar ist.
</div>
</div>
<div class="consumer-group"> <div class="consumer-group">
<div class="consumer-title" style="color: #a855f7;">🚗 Links (Lila)</div> <div class="consumer-title" style="color: #a855f7;">🚗 Links (Lila)</div>
<ha-selector <ha-selector
@ -681,13 +721,26 @@ class PowerFluxCardEditor extends LitElement {
customElements.define("power-flux-card-editor", PowerFluxCardEditor); customElements.define("power-flux-card-editor", PowerFluxCardEditor);
console.log( console.log(
"%c⚡ Power-Flux-Card v_2.0 ready", "%c⚡ Power Flux Card v_2.1 ready",
"background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;"
); );
class PowerFluxCard extends LitElement { (function () {
const cardTranslations = {
"en": lang_en.card,
"de": lang_de.card
};
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 PowerFluxCard extends LitElement {
static get properties() { static get properties() {
return { return {
hass: {}, hass: {},
@ -731,6 +784,7 @@ class PowerFluxCard extends LitElement {
grid_export: "", grid_export: "",
battery: "", battery: "",
battery_soc: "", battery_soc: "",
house: "",
consumer_1: "", consumer_1: "",
consumer_2: "", consumer_2: "",
consumer_3: "" consumer_3: ""
@ -738,6 +792,16 @@ class PowerFluxCard extends LitElement {
}; };
} }
_handleClick(entityId) {
if (!entityId) return;
const event = new Event("hass-more-info", {
bubbles: true,
composed: true,
});
event.detail = { entityId };
this.dispatchEvent(event);
}
setConfig(config) { setConfig(config) {
if (!config.entities) { if (!config.entities) {
// Init allow // Init allow
@ -948,7 +1012,7 @@ class PowerFluxCard extends LitElement {
.node-c2 { top: 370px; left: 165px; } .node-c2 { top: 370px; left: 165px; }
.node-c3 { top: 370px; left: 325px; } .node-c3 { top: 370px; left: 325px; }
svg { position: absolute; top: 7; left: 25; width: 100%; height: 100%; z-index: 1; pointer-events: none; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; } .bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; }
.bg-solar { stroke: var(--neon-yellow); } .bg-solar { stroke: var(--neon-yellow); }
@ -1081,7 +1145,10 @@ class PowerFluxCard extends LitElement {
const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0;
const gridMain = entities.grid ? getVal(entities.grid) : 0; const gridMain = entities.grid ? getVal(entities.grid) : 0;
const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0;
const battery = entities.battery ? getVal(entities.battery) : 0; let battery = entities.battery ? getVal(entities.battery) : 0;
if (this.config.invert_battery) {
battery *= -1;
}
const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value
// 2. Logic Calculation // 2. Logic Calculation
@ -1159,7 +1226,7 @@ class PowerFluxCard extends LitElement {
const barSegments = []; const barSegments = [];
let currentX = 0; let currentX = 0;
const addSegment = (val, color, type, label) => { const addSegment = (val, color, type, label, entityId) => {
if (val <= threshold) return; if (val <= threshold) return;
const pct = val / totalFlux; const pct = val / totalFlux;
const width = pct * fullWidth; const width = pct * fullWidth;
@ -1170,14 +1237,15 @@ class PowerFluxCard extends LitElement {
widthPx: width, widthPx: width,
startPx: currentX, startPx: currentX,
type, type,
label label,
entityId
}); });
currentX += width; currentX += width;
} }
addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery'); addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery', entities.battery);
addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar'); addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar', entities.solar);
addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid'); addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid', entities.grid);
// --- GENERATE TOP BRACKETS (Based on Bar Segments) --- // --- GENERATE TOP BRACKETS (Based on Bar Segments) ---
const topBrackets = barSegments.map(s => { const topBrackets = barSegments.map(s => {
@ -1188,7 +1256,7 @@ class PowerFluxCard extends LitElement {
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } 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 }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
// --- GENERATE BOTTOM BRACKETS (Independent Calculation) --- // --- GENERATE BOTTOM BRACKETS (Independent Calculation) ---
@ -1196,7 +1264,7 @@ class PowerFluxCard extends LitElement {
const bottomBrackets = []; const bottomBrackets = [];
let bottomX = 0; let bottomX = 0;
const addBottomBracket = (val, type) => { const addBottomBracket = (val, type, entityId = null) => {
if (val <= threshold) return; if (val <= threshold) return;
const pct = val / totalFlux; const pct = val / totalFlux;
const width = pct * fullWidth; const width = pct * fullWidth;
@ -1207,6 +1275,7 @@ class PowerFluxCard extends LitElement {
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -1214,14 +1283,17 @@ class PowerFluxCard extends LitElement {
width: width, width: width,
center: bottomX + (width / 2), center: bottomX + (width / 2),
icon, icon,
iconColor iconColor,
val,
entityId
}); });
bottomX += width; bottomX += width;
}; };
addBottomBracket(destHouse, 'house'); addBottomBracket(destHouse, 'house', entities.house);
addBottomBracket(destEV, 'car'); addBottomBracket(destEV, 'car', entities.consumer_1);
addBottomBracket(destExport, 'export'); addBottomBracket(destExport, 'export', entities.grid_export || entities.grid);
addBottomBracket(batteryCharge, 'battery', entities.battery);
// Note: If there is Battery Charging happening, bottomX will not reach fullWidth. // 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 // This leaves a gap at the end (or between segments depending on logic), which is visually correct
@ -1236,7 +1308,10 @@ class PowerFluxCard extends LitElement {
${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))}
</svg> </svg>
${topBrackets.map(b => b.width > 20 ? html` ${topBrackets.map(b => b.width > 20 ? html`
<div class="compact-icon-wrapper" style="left: ${b.center}px; transform: translateX(-50%); top: 4px;"> <div class="compact-icon-wrapper"
style="left: ${b.center}px; transform: translateX(-50%); top: 4px; cursor: ${b.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(b.val)}"
@click=${() => b.entityId && this._handleClick(b.entityId)}>
<ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon> <ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon>
</div>` : '')} </div>` : '')}
</div> </div>
@ -1244,7 +1319,10 @@ class PowerFluxCard extends LitElement {
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => html` ${barSegments.map(s => html`
<div class="bar-segment" style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'};"> <div class="bar-segment"
style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}"
@click=${() => s.entityId && this._handleClick(s.entityId)}>
${s.widthPx > 35 ? this._formatPower(s.val) : ''} ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
</div> </div>
`)} `)}
@ -1256,7 +1334,10 @@ class PowerFluxCard extends LitElement {
${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))}
</svg> </svg>
${bottomBrackets.map(b => b.width > 20 ? html` ${bottomBrackets.map(b => b.width > 20 ? html`
<div class="compact-icon-wrapper" style="left: ${b.center}px; transform: translateX(-50%); top: -3px;"> <div class="compact-icon-wrapper"
style="left: ${b.center}px; transform: translateX(-50%); top: -3px; cursor: ${b.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(b.val)}"
@click=${() => b.entityId && this._handleClick(b.entityId)}>
<ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon> <ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon>
</div>` : '')} </div>` : '')}
</div> </div>
@ -1337,7 +1418,10 @@ class PowerFluxCard extends LitElement {
const solar = hasSolar ? getVal(entities.solar) : 0; const solar = hasSolar ? getVal(entities.solar) : 0;
const gridMain = hasGrid ? getVal(entities.grid) : 0; const gridMain = hasGrid ? getVal(entities.grid) : 0;
const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0;
const battery = hasBattery ? getVal(entities.battery) : 0; let battery = hasBattery ? getVal(entities.battery) : 0;
if (this.config.invert_battery) {
battery *= -1;
}
const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0;
const solarVal = Math.max(0, solar); const solarVal = Math.max(0, solar);
@ -1655,6 +1739,7 @@ class PowerFluxCard extends LitElement {
} }
customElements.define("power-flux-card", PowerFluxCard); customElements.define("power-flux-card", PowerFluxCard);
})();
window.customCards = window.customCards || []; window.customCards = window.customCards || [];
window.customCards.push({ window.customCards.push({

View file

@ -9,12 +9,28 @@ export default {
"editor.consumers_section": "Zusätzliche Verbraucher", "editor.consumers_section": "Zusätzliche Verbraucher",
"editor.options_section": "Darstellung & Optionen", "editor.options_section": "Darstellung & Optionen",
"editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen",
"editor.invert_battery": "Wert umkehren (+/-)",
"editor.label_toggle": "Label im Kreis anzeigen", "editor.label_toggle": "Label im Kreis anzeigen",
"editor.compact_view": "Kompakte Ansicht (evcc)", "editor.compact_view": "Kompakte Ansicht (evcc)",
"editor.hide_inactive": "Inaktive Röhren ausblenden", "editor.hide_inactive": "Inaktive Röhren ausblenden",
"editor.entity": "Entität (Watt)", "editor.entity": "Entität (Watt)",
"editor.label": "Beschriftung", "editor.label": "Beschriftung",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Zurück",
"editor.battery_soc_label": "Ladestand (%)",
"editor.house_total_title": "🏠 Gesamtverbrauch (optional)",
"editor.house_sensor_label": "Sensor für Hausverbrauch (optional)",
"editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist.",
"editor.consumer_1_title": "🚗 Links (Lila)",
"editor.consumer_2_title": "♨️ Mitte (Orange)",
"editor.consumer_3_title": "🏊 Rechts (Türkis)",
"editor.zoom_label": "🔍 Zoom (Standard View)",
"editor.neon_glow": "Neon Glow",
"editor.donut_chart": "Donut Chart (Grid/Haus)",
"editor.comet_tail": "Comet Tail Effect",
"editor.dashed_line": "Dashed Line Effect",
"editor.colored_values": "Farbige Textwerte",
"editor.hide_consumer_icons": "Icons unten ausblenden",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",

View file

@ -9,12 +9,28 @@ export default {
"editor.consumers_section": "Additional Consumers", "editor.consumers_section": "Additional Consumers",
"editor.options_section": "Appearance & Options", "editor.options_section": "Appearance & Options",
"editor.flow_rate_title": "Show Flow Rates (W) on pipes", "editor.flow_rate_title": "Show Flow Rates (W) on pipes",
"editor.invert_battery": "Invert Power Value (+/-)",
"editor.label_toggle": "Show Label in Bubble", "editor.label_toggle": "Show Label in Bubble",
"editor.compact_view": "Compact View (evcc)", "editor.compact_view": "Compact View (evcc)",
"editor.hide_inactive": "Hide Inactive Pipes", "editor.hide_inactive": "Hide Inactive Pipes",
"editor.entity": "Entity (Watt)", "editor.entity": "Entity (Watt)",
"editor.label": "Label", "editor.label": "Label",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Back",
"editor.battery_soc_label": "State of Charge (%)",
"editor.house_total_title": "🏠 Total Consumption (optional)",
"editor.house_sensor_label": "Sensor for House Consumption (optional)",
"editor.house_sensor_hint": "Required to make the house icon clickable.",
"editor.consumer_1_title": "🚗 Left (Purple)",
"editor.consumer_2_title": "♨️ Center (Orange)",
"editor.consumer_3_title": "🏊 Right (Cyan)",
"editor.zoom_label": "🔍 Zoom (Standard View)",
"editor.neon_glow": "Neon Glow",
"editor.donut_chart": "Donut Chart (Grid/House)",
"editor.comet_tail": "Comet Tail Effect",
"editor.dashed_line": "Dashed Line Effect",
"editor.colored_values": "Colored Text Values",
"editor.hide_consumer_icons": "Hide Consumer Icons",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",

View file

@ -1,3 +1,12 @@
import lang_en from "./lang-en.js";
import lang_de from "./lang-de.js";
const editorTranslations = {
"en": lang_en.editor,
"de": lang_de.editor
};
const fireEvent = (node, type, detail, options) => { const fireEvent = (node, type, detail, options) => {
options = options || {}; options = options || {};
detail = detail === null || detail === undefined ? {} : detail; detail = detail === null || detail === undefined ? {} : detail;
@ -58,6 +67,7 @@ class PowerFluxCardEditor extends LitElement {
const entityKeys = [ const entityKeys = [
'solar', 'grid', 'grid_export', 'solar', 'grid', 'grid_export',
'battery', 'battery_soc', 'battery', 'battery_soc',
'house',
'consumer_1', 'consumer_2', 'consumer_3' 'consumer_1', 'consumer_2', 'consumer_3'
]; ];
@ -178,7 +188,7 @@ class PowerFluxCardEditor extends LitElement {
return html` return html`
<div class="header"> <div class="header">
<div class="back-btn" @click=${this._goBack}> <div class="back-btn" @click=${this._goBack}>
<ha-icon icon="mdi:arrow-left"></ha-icon> Zurück <ha-icon icon="mdi:arrow-left"></ha-icon> ${this._localize('editor.back')}
</div> </div>
<h2>${this._localize('editor.solar_section')}</h2> <h2>${this._localize('editor.solar_section')}</h2>
</div> </div>
@ -216,7 +226,7 @@ class PowerFluxCardEditor extends LitElement {
<div class="switch-row"> <div class="switch-row">
<ha-switch <ha-switch
.checked=${this._config.show_label_solar !== false} .checked=${this._config.show_label_solar === true}
.configValue=${'show_label_solar'} .configValue=${'show_label_solar'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
@ -238,7 +248,7 @@ class PowerFluxCardEditor extends LitElement {
return html` return html`
<div class="header"> <div class="header">
<div class="back-btn" @click=${this._goBack}> <div class="back-btn" @click=${this._goBack}>
<ha-icon icon="mdi:arrow-left"></ha-icon> Zurück <ha-icon icon="mdi:arrow-left"></ha-icon> ${this._localize('editor.back')}
</div> </div>
<h2>${this._localize('editor.grid_section')}</h2> <h2>${this._localize('editor.grid_section')}</h2>
</div> </div>
@ -285,7 +295,7 @@ class PowerFluxCardEditor extends LitElement {
<div class="switch-row"> <div class="switch-row">
<ha-switch <ha-switch
.checked=${this._config.show_label_grid !== false} .checked=${this._config.show_label_grid === true}
.configValue=${'show_label_grid'} .configValue=${'show_label_grid'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
@ -307,7 +317,7 @@ class PowerFluxCardEditor extends LitElement {
return html` return html`
<div class="header"> <div class="header">
<div class="back-btn" @click=${this._goBack}> <div class="back-btn" @click=${this._goBack}>
<ha-icon icon="mdi:arrow-left"></ha-icon> Zurück <ha-icon icon="mdi:arrow-left"></ha-icon> ${this._localize('editor.back')}
</div> </div>
<h2>${this._localize('editor.battery_section')}</h2> <h2>${this._localize('editor.battery_section')}</h2>
</div> </div>
@ -326,7 +336,7 @@ class PowerFluxCardEditor extends LitElement {
.selector=${entitySelectorSchema} .selector=${entitySelectorSchema}
.value=${entities.battery_soc} .value=${entities.battery_soc}
.configValue=${'battery_soc'} .configValue=${'battery_soc'}
.label=${"Ladestand (%)"} .label=${this._localize('editor.battery_soc_label')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
@ -354,7 +364,7 @@ class PowerFluxCardEditor extends LitElement {
<div class="switch-row"> <div class="switch-row">
<ha-switch <ha-switch
.checked=${this._config.show_label_battery !== false} .checked=${this._config.show_label_battery === true}
.configValue=${'show_label_battery'} .configValue=${'show_label_battery'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
@ -369,6 +379,15 @@ class PowerFluxCardEditor extends LitElement {
></ha-switch> ></ha-switch>
<div class="switch-label">${this._localize('editor.flow_rate_title')}</div> <div class="switch-label">${this._localize('editor.flow_rate_title')}</div>
</div> </div>
<div class="switch-row">
<ha-switch
.checked=${this._config.invert_battery === true}
.configValue=${'invert_battery'}
@change=${this._valueChanged}
></ha-switch>
<div class="switch-label">${this._localize('editor.invert_battery')}</div>
</div>
`; `;
} }
@ -376,13 +395,28 @@ class PowerFluxCardEditor extends LitElement {
return html` return html`
<div class="header"> <div class="header">
<div class="back-btn" @click=${this._goBack}> <div class="back-btn" @click=${this._goBack}>
<ha-icon icon="mdi:arrow-left"></ha-icon> Zurück <ha-icon icon="mdi:arrow-left"></ha-icon> ${this._localize('editor.back')}
</div> </div>
<h2>${this._localize('editor.consumers_section')}</h2> <h2>${this._localize('editor.consumers_section')}</h2>
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
<div class="consumer-title" style="color: #a855f7;">🚗 Links (Lila)</div> <div class="consumer-title">${this._localize('editor.house_total_title')}</div>
<ha-selector
.hass=${this.hass}
.selector=${entitySelectorSchema}
.value=${entities.house || ""}
.configValue=${'house'}
.label=${this._localize('editor.house_sensor_label')}
@value-changed=${this._valueChanged}
></ha-selector>
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.house_sensor_hint')}
</div>
</div>
<div class="consumer-group">
<div class="consumer-title" style="color: #a855f7;">${this._localize('editor.consumer_1_title')}</div>
<ha-selector <ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${entitySelectorSchema} .selector=${entitySelectorSchema}
@ -412,7 +446,7 @@ class PowerFluxCardEditor extends LitElement {
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
<div class="consumer-title" style="color: #f97316;"> Mitte (Orange)</div> <div class="consumer-title" style="color: #f97316;">${this._localize('editor.consumer_2_title')}</div>
<ha-selector <ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${entitySelectorSchema} .selector=${entitySelectorSchema}
@ -442,7 +476,7 @@ class PowerFluxCardEditor extends LitElement {
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
<div class="consumer-title" style="color: #06b6d4;">🏊 Rechts (Türkis)</div> <div class="consumer-title" style="color: #06b6d4;">${this._localize('editor.consumer_3_title')}</div>
<ha-selector <ha-selector
.hass=${this.hass} .hass=${this.hass}
.selector=${entitySelectorSchema} .selector=${entitySelectorSchema}
@ -525,7 +559,7 @@ class PowerFluxCardEditor extends LitElement {
.selector=${{ number: { min: 0.5, max: 1.5, step: 0.05, mode: "slider" } }} .selector=${{ number: { min: 0.5, max: 1.5, step: 0.05, mode: "slider" } }}
.value=${this._config.zoom !== undefined ? this._config.zoom : 0.9} .value=${this._config.zoom !== undefined ? this._config.zoom : 0.9}
.configValue=${'zoom'} .configValue=${'zoom'}
.label=${"🔍 Zoom (Standard View)"} .label=${this._localize('editor.zoom_label')}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
</div> </div>
@ -536,7 +570,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'show_neon_glow'} .configValue=${'show_neon_glow'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Neon Glow</div> <div class="switch-label">${this._localize('editor.neon_glow')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">
@ -545,7 +579,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'show_donut_border'} .configValue=${'show_donut_border'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Donut Chart (Grid/Haus)</div> <div class="switch-label">${this._localize('editor.donut_chart')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">
@ -554,7 +588,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'show_comet_tail'} .configValue=${'show_comet_tail'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Comet Tail Effect</div> <div class="switch-label">${this._localize('editor.comet_tail')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">
@ -563,7 +597,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'show_dashed_line'} .configValue=${'show_dashed_line'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Dashed Line Animation</div> <div class="switch-label">${this._localize('editor.dashed_line')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">
@ -572,7 +606,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'use_colored_values'} .configValue=${'use_colored_values'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Farbige Textwerte</div> <div class="switch-label">${this._localize('editor.colored_values')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">
@ -581,7 +615,7 @@ class PowerFluxCardEditor extends LitElement {
.configValue=${'hide_consumer_icons'} .configValue=${'hide_consumer_icons'}
@change=${this._valueChanged} @change=${this._valueChanged}
></ha-switch> ></ha-switch>
<div class="switch-label">Icons unten ausblenden</div> <div class="switch-label">${this._localize('editor.hide_consumer_icons')}</div>
</div> </div>
<div class="switch-row"> <div class="switch-row">

View file

@ -1,10 +1,23 @@
import { } from "./power-flux-card-editor.js";
import lang_en from "./lang-en.js";
import lang_de from "./lang-de.js";
console.log( console.log(
"%c⚡ Power-Flux-Card v_2.0 ready", "%c⚡ Power Flux Card v_2.1 ready",
"background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;"
); );
class PowerFluxCard extends LitElement { (function (lang_en, lang_de) {
const cardTranslations = {
"en": lang_en.card,
"de": lang_de.card
};
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 PowerFluxCard extends LitElement {
static get properties() { static get properties() {
return { return {
hass: {}, hass: {},
@ -48,6 +61,7 @@ class PowerFluxCard extends LitElement {
grid_export: "", grid_export: "",
battery: "", battery: "",
battery_soc: "", battery_soc: "",
house: "",
consumer_1: "", consumer_1: "",
consumer_2: "", consumer_2: "",
consumer_3: "" consumer_3: ""
@ -55,6 +69,16 @@ class PowerFluxCard extends LitElement {
}; };
} }
_handleClick(entityId) {
if (!entityId) return;
const event = new Event("hass-more-info", {
bubbles: true,
composed: true,
});
event.detail = { entityId };
this.dispatchEvent(event);
}
setConfig(config) { setConfig(config) {
if (!config.entities) { if (!config.entities) {
// Init allow // Init allow
@ -265,7 +289,7 @@ class PowerFluxCard extends LitElement {
.node-c2 { top: 370px; left: 165px; } .node-c2 { top: 370px; left: 165px; }
.node-c3 { top: 370px; left: 325px; } .node-c3 { top: 370px; left: 325px; }
svg { position: absolute; top: 7; left: 25; width: 100%; height: 100%; z-index: 1; pointer-events: none; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; } .bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; }
.bg-solar { stroke: var(--neon-yellow); } .bg-solar { stroke: var(--neon-yellow); }
@ -398,7 +422,10 @@ class PowerFluxCard extends LitElement {
const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0;
const gridMain = entities.grid ? getVal(entities.grid) : 0; const gridMain = entities.grid ? getVal(entities.grid) : 0;
const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0;
const battery = entities.battery ? getVal(entities.battery) : 0; let battery = entities.battery ? getVal(entities.battery) : 0;
if (this.config.invert_battery) {
battery *= -1;
}
const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value
// 2. Logic Calculation // 2. Logic Calculation
@ -476,7 +503,7 @@ class PowerFluxCard extends LitElement {
const barSegments = []; const barSegments = [];
let currentX = 0; let currentX = 0;
const addSegment = (val, color, type, label) => { const addSegment = (val, color, type, label, entityId) => {
if (val <= threshold) return; if (val <= threshold) return;
const pct = val / totalFlux; const pct = val / totalFlux;
const width = pct * fullWidth; const width = pct * fullWidth;
@ -487,14 +514,15 @@ class PowerFluxCard extends LitElement {
widthPx: width, widthPx: width,
startPx: currentX, startPx: currentX,
type, type,
label label,
entityId
}); });
currentX += width; currentX += width;
} }
addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery'); addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery', entities.battery);
addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar'); addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar', entities.solar);
addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid'); addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid', entities.grid);
// --- GENERATE TOP BRACKETS (Based on Bar Segments) --- // --- GENERATE TOP BRACKETS (Based on Bar Segments) ---
const topBrackets = barSegments.map(s => { const topBrackets = barSegments.map(s => {
@ -505,7 +533,7 @@ class PowerFluxCard extends LitElement {
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } 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 }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
// --- GENERATE BOTTOM BRACKETS (Independent Calculation) --- // --- GENERATE BOTTOM BRACKETS (Independent Calculation) ---
@ -513,7 +541,7 @@ class PowerFluxCard extends LitElement {
const bottomBrackets = []; const bottomBrackets = [];
let bottomX = 0; let bottomX = 0;
const addBottomBracket = (val, type) => { const addBottomBracket = (val, type, entityId = null) => {
if (val <= threshold) return; if (val <= threshold) return;
const pct = val / totalFlux; const pct = val / totalFlux;
const width = pct * fullWidth; const width = pct * fullWidth;
@ -524,6 +552,7 @@ class PowerFluxCard extends LitElement {
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -531,14 +560,17 @@ class PowerFluxCard extends LitElement {
width: width, width: width,
center: bottomX + (width / 2), center: bottomX + (width / 2),
icon, icon,
iconColor iconColor,
val,
entityId
}); });
bottomX += width; bottomX += width;
}; };
addBottomBracket(destHouse, 'house'); addBottomBracket(destHouse, 'house', entities.house);
addBottomBracket(destEV, 'car'); addBottomBracket(destEV, 'car', entities.consumer_1);
addBottomBracket(destExport, 'export'); addBottomBracket(destExport, 'export', entities.grid_export || entities.grid);
addBottomBracket(batteryCharge, 'battery', entities.battery);
// Note: If there is Battery Charging happening, bottomX will not reach fullWidth. // 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 // This leaves a gap at the end (or between segments depending on logic), which is visually correct
@ -553,7 +585,10 @@ class PowerFluxCard extends LitElement {
${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))}
</svg> </svg>
${topBrackets.map(b => b.width > 20 ? html` ${topBrackets.map(b => b.width > 20 ? html`
<div class="compact-icon-wrapper" style="left: ${b.center}px; transform: translateX(-50%); top: 4px;"> <div class="compact-icon-wrapper"
style="left: ${b.center}px; transform: translateX(-50%); top: 4px; cursor: ${b.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(b.val)}"
@click=${() => b.entityId && this._handleClick(b.entityId)}>
<ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon> <ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon>
</div>` : '')} </div>` : '')}
</div> </div>
@ -561,7 +596,10 @@ class PowerFluxCard extends LitElement {
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => html` ${barSegments.map(s => html`
<div class="bar-segment" style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'};"> <div class="bar-segment"
style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}"
@click=${() => s.entityId && this._handleClick(s.entityId)}>
${s.widthPx > 35 ? this._formatPower(s.val) : ''} ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
</div> </div>
`)} `)}
@ -573,7 +611,10 @@ class PowerFluxCard extends LitElement {
${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))}
</svg> </svg>
${bottomBrackets.map(b => b.width > 20 ? html` ${bottomBrackets.map(b => b.width > 20 ? html`
<div class="compact-icon-wrapper" style="left: ${b.center}px; transform: translateX(-50%); top: -3px;"> <div class="compact-icon-wrapper"
style="left: ${b.center}px; transform: translateX(-50%); top: -3px; cursor: ${b.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(b.val)}"
@click=${() => b.entityId && this._handleClick(b.entityId)}>
<ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon> <ha-icon icon="${b.icon}" class="compact-icon" style="color: ${b.iconColor};"></ha-icon>
</div>` : '')} </div>` : '')}
</div> </div>
@ -654,7 +695,10 @@ class PowerFluxCard extends LitElement {
const solar = hasSolar ? getVal(entities.solar) : 0; const solar = hasSolar ? getVal(entities.solar) : 0;
const gridMain = hasGrid ? getVal(entities.grid) : 0; const gridMain = hasGrid ? getVal(entities.grid) : 0;
const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0;
const battery = hasBattery ? getVal(entities.battery) : 0; let battery = hasBattery ? getVal(entities.battery) : 0;
if (this.config.invert_battery) {
battery *= -1;
}
const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0;
const solarVal = Math.max(0, solar); const solarVal = Math.max(0, solar);
@ -972,6 +1016,7 @@ class PowerFluxCard extends LitElement {
} }
customElements.define("power-flux-card", PowerFluxCard); customElements.define("power-flux-card", PowerFluxCard);
})(lang_en, lang_de);
window.customCards = window.customCards || []; window.customCards = window.customCards || [];
window.customCards.push({ window.customCards.push({