diff --git a/README.md b/README.md
index b331d24..66a0584 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,19 @@ If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗
- **Donut Chart**: Optional donut chart around the house icon showing energy mix.
- **Comet Tail / Dashed Lines**: Choose your preferred animation style.
- **Zoom**: Adjustable scale to fit your dashboard.
+ - **Custom Colors**: Define custom colors for each source and consumer via the editor.
+ - **Background Color**: Enable a slightly tinted background for the circles in the default view.
+- **More Info**: Click on any source/consumer for detailed information in a more-info dialog.
+- **Grid Import/Export**: Supports both separate Import/Export entities or a combined entity with positive/negative values.
+- **Grid-to-Battery**: Optional direct sensor for Grid-to-Battery flow, bypassing the standard calculation.
+- **Secondary Sensors**: Optionally display a secondary sensor value in the main circles (e.g., daily yield for Solar, current charge/discharge power for Battery) and consumer bubbles.
- **Localization**: Fully translated in English and German.
- **Visual Editor**: easy configuration via the Home Assistant UI.
+[](https://www.youtube.com/watch?v=HGFBJJRWGW0)
+
+---
+
### 🚀 Installation
### HACS (Recommended)
@@ -54,6 +64,8 @@ If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗
- URL: `/local/community/power-flux-card/power-flux-card.js`
- Type: JavaScript Module
+---
+
### ⚙️ Configuration
You can configure the card directly via the visual editor in Home Assistant.
diff --git a/docs/README-de.md b/docs/README-de.md
index 96f99ef..3e90900 100644
--- a/docs/README-de.md
+++ b/docs/README-de.md
@@ -26,9 +26,21 @@ Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewert
- **Donut Chart**: Optionales Donut-Diagramm um das Haus-Icon, das den Energiemix zeigt.
- **Kometenschweif / Gestrichelte Linien**: Wählen Sie Ihren bevorzugten Animationsstil.
- **Zoom**: Anpassbare Größe für Ihr Dashboard.
+ - **Benutzerdefinierte Farben**: Definiere benutzerdefinierte Farben für jede Quelle und jeden Verbraucher über den Editor.
+ - **Hintergrundfarbe**: Aktiviere einen leicht getönten Hintergrund für die Kreise in der Standard-Ansicht.
+- **Dynamische Animationsgeschwindigkeit**: Partikelgeschwindigkeit und -dichte passen sich dem aktuellen Energiefluss an.
+- **Weitere Informationen**: Klicke auf eine beliebige Quelle/Verbraucher, um detaillierte Informationen in einem More-Info-Dialog anzuzeigen.
+- **Netz-Import/Export**: Unterstützt sowohl separate Import/Export-Entitäten als auch eine kombinierte Entität mit positiven/negativen Werten.
+- **Netz-zu-Batterie**: Optionaler direkter Sensor für den Netz-zu-Batterie-Fluss, der die Standardberechnung umgeht.
+- **Sekundäre Sensoren**: Optional können sekundäre Sensorwerte in den Hauptkreisen (z.B. Tagesertrag für Solar, aktuelle Lade-/Entladeleistung für Batterie) angezeigt werden.
- **Lokalisierung**: Vollständig übersetzt in Deutsch und Englisch.
- **Visueller Editor**: Einfache Konfiguration über die Home Assistant UI.
+[](https://www.youtube.com/watch?v=HGFBJJRWGW0
+)
+
+---
+
### 🚀 Installation
### HACS (Empfohlen)
@@ -53,6 +65,9 @@ Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewert
- URL: `/local/community/power-flux-card/power-flux-card.js`
- Typ: JavaScript Module
+
+---
+
### ⚙️ Konfiguration
Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigurieren.
diff --git a/src/lang-de.js b/src/lang-de.js
index bb15bc2..f7220c7 100644
--- a/src/lang-de.js
+++ b/src/lang-de.js
@@ -29,8 +29,16 @@ export default {
"editor.donut_chart": "Donut Chart (Grid/Haus)",
"editor.comet_tail": "Comet Tail Effect",
"editor.dashed_line": "Dashed Line Effect",
+ "editor.tinted_background": "Farbiger Hintergrund in Kreisen",
"editor.colored_values": "Farbige Textwerte",
"editor.hide_consumer_icons": "Icons unten ausblenden",
+ "editor.invert_consumer_1": "Sensorwert invertieren (+/-)",
+ "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)",
+ "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)",
+ "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.",
+ "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)",
+ "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.",
+ "editor.color_picker": "Farbe anpassen",
},
card: {
"card.label_solar": "Solar",
diff --git a/src/lang-en.js b/src/lang-en.js
index 774f660..f955cb9 100644
--- a/src/lang-en.js
+++ b/src/lang-en.js
@@ -29,8 +29,16 @@ export default {
"editor.donut_chart": "Donut Chart (Grid/House)",
"editor.comet_tail": "Comet Tail Effect",
"editor.dashed_line": "Dashed Line Effect",
+ "editor.tinted_background": "Tinted Background in Bubbles",
"editor.colored_values": "Colored Text Values",
"editor.hide_consumer_icons": "Hide Consumer Icons",
+ "editor.invert_consumer_1": "Invert Sensor Value (+/-)",
+ "editor.secondary_sensor": "Secondary Sensor (display only)",
+ "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)",
+ "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.",
+ "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)",
+ "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.",
+ "editor.color_picker": "Custom Color",
},
card: {
"card.label_solar": "Solar",
diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js
index fc488e0..bf1177a 100644
--- a/src/power-flux-card-editor.js
+++ b/src/power-flux-card-editor.js
@@ -65,10 +65,12 @@ class PowerFluxCardEditor extends LitElement {
if (key) {
const entityKeys = [
- 'solar', 'grid', 'grid_export',
- 'battery', 'battery_soc',
+ 'solar', 'grid', 'grid_export', 'grid_combined',
+ 'battery', 'battery_soc', 'grid_to_battery',
'house',
- 'consumer_1', 'consumer_2', 'consumer_3'
+ 'consumer_1', 'consumer_2', 'consumer_3',
+ 'secondary_solar', 'secondary_grid', 'secondary_battery',
+ 'secondary_consumer_1', 'secondary_consumer_2', 'secondary_consumer_3'
];
let newConfig = { ...this._config };
@@ -101,6 +103,65 @@ class PowerFluxCardEditor extends LitElement {
this._subView = null;
}
+ _clearEntity(key) {
+ const newConfig = { ...this._config };
+ const currentEntities = newConfig.entities || {};
+ const newEntities = { ...currentEntities, [key]: "" };
+ newConfig.entities = newEntities;
+ this._config = newConfig;
+ fireEvent(this, "config-changed", { config: this._config });
+ }
+
+ _colorChanged(key, ev) {
+ const newConfig = { ...this._config, [key]: ev.target.value };
+ this._config = newConfig;
+ fireEvent(this, "config-changed", { config: this._config });
+ }
+
+ _resetColor(key) {
+ const newConfig = { ...this._config };
+ delete newConfig[key];
+ this._config = newConfig;
+ fireEvent(this, "config-changed", { config: this._config });
+ }
+
+ _renderEntitySelector(entitySelectorSchema, value, configValue, label) {
+ const val = value || "";
+ return html`
+
+
+ ${val ? html` this._clearEntity(configValue)}
+ > ` : ''}
+
+ `;
+ }
+
+ _renderColorPicker(key, label, defaultColor) {
+ const currentColor = this._config[key] || defaultColor;
+ const hasCustom = !!this._config[key];
+ return html`
+
+ this._colorChanged(key, e)}>
+ ${label}
+ ${hasCustom ? html` this._resetColor(key)}> ` : ''}
+
+ `;
+ }
+
static get styles() {
return css`
.card-config {
@@ -179,6 +240,60 @@ class PowerFluxCardEditor extends LitElement {
border-bottom: 1px solid var(--divider-color);
margin: 10px 0;
}
+ .entity-picker-wrapper {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+ .entity-picker-wrapper ha-selector {
+ flex: 1;
+ }
+ .clear-entity-btn {
+ --mdc-icon-size: 20px;
+ color: var(--secondary-text-color);
+ cursor: pointer;
+ flex-shrink: 0;
+ margin-top: -12px;
+ }
+ .clear-entity-btn:hover {
+ color: var(--error-color, #db4437);
+ }
+ .color-picker-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 0;
+ }
+ .color-picker-row input[type="color"] {
+ -webkit-appearance: none;
+ border: 2px solid var(--divider-color);
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ padding: 2px;
+ cursor: pointer;
+ background: transparent;
+ }
+ .color-picker-row input[type="color"]::-webkit-color-swatch-wrapper {
+ padding: 0;
+ }
+ .color-picker-row input[type="color"]::-webkit-color-swatch {
+ border: none;
+ border-radius: 50%;
+ }
+ .color-label {
+ flex: 1;
+ font-size: 14px;
+ }
+ .color-reset-btn {
+ --mdc-icon-size: 20px;
+ color: var(--secondary-text-color);
+ cursor: pointer;
+ }
+ .color-reset-btn:hover {
+ color: var(--primary-color);
+ }
`;
}
@@ -193,14 +308,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.solar_section')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.solar, 'solar', this._localize('editor.entity'))}
@@ -222,6 +330,10 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged}
>
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')}
+
@@ -253,23 +365,16 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.grid_section')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))}
+
+ ${this._localize('editor.grid_combined_hint')}
+
-
+
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")}
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid_export, 'grid_export', this._localize('card.label_export') + " (W, Optional)")}
@@ -291,6 +396,10 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged}
>
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')}
+
@@ -322,23 +431,14 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.battery_section')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))}
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
+
+ ${this._localize('editor.grid_to_battery_hint')}
+
@@ -359,6 +459,10 @@ class PowerFluxCardEditor extends LitElement {
.label=${this._localize('editor.icon') + " (Optional)"}
@value-changed=${this._valueChanged}
>
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')}
@@ -402,14 +506,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_total_title')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.house || "", 'house', this._localize('editor.house_sensor_label'))}
${this._localize('editor.house_sensor_hint')}
@@ -417,14 +514,7 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.consumer_1_title')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_1, 'consumer_1', this._localize('editor.entity'))}
+
+
+ ${this._localize('editor.invert_consumer_1')}
+
+
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')}
${this._localize('editor.consumer_2_title')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_2, 'consumer_2', this._localize('editor.entity'))}
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')}
${this._localize('editor.consumer_3_title')}
-
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_3, 'consumer_3', this._localize('editor.entity'))}
+
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))}
+
+ ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')}
`;
}
@@ -600,6 +697,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.dashed_line')}
+
+
+
${this._localize('editor.tinted_background')}
+
+
`;
}
if (type === 'car') {
- return html` `;
+ const c = colorOverride || 'var(--consumer-1-color)';
+ return html` `;
}
if (type === 'heater') {
- return html` `;
+ const c = colorOverride || 'var(--consumer-2-color)';
+ return html` `;
}
if (type === 'pool') {
- return html` `;
+ const c = colorOverride || 'var(--consumer-3-color)';
+ return html` `;
}
return html``;
}
@@ -365,6 +420,11 @@ console.log(
return Math.round(val) + " W";
}
+ _getConsumerColor(index) {
+ const style = getComputedStyle(this);
+ return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1];
+ }
+
// --- DOM NODE SVG GENERATOR ---
_renderSVGPath(d, color) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@@ -420,19 +480,27 @@ console.log(
};
const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0;
- const gridMain = entities.grid ? getVal(entities.grid) : 0;
+ const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== "");
+ const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0;
+ const gridMain = hasGridCombined ? gridCombinedVal : (entities.grid ? getVal(entities.grid) : 0);
const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 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
+ let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value
+ if (this.config.invert_consumer_1) { c1Val *= -1; }
+ c1Val = Math.abs(c1Val);
// 2. Logic Calculation
let gridImport = 0;
let gridExport = 0;
- if (entities.grid_export && entities.grid_export !== "") {
+ if (hasGridCombined) {
+ // COMBINED SENSOR: positive = import, negative = export
+ gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0;
+ gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0;
+ } else if (entities.grid_export && entities.grid_export !== "") {
gridImport = gridMain > 0 ? gridMain : 0;
gridExport = Math.abs(gridExportSensor);
} else {
@@ -447,12 +515,18 @@ console.log(
let gridToBatt = 0;
if (batteryCharge > 0) {
- if (solar >= batteryCharge) {
- solarToBatt = batteryCharge;
- gridToBatt = 0;
+ const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== "");
+ if (hasGridToBattSensor) {
+ gridToBatt = Math.abs(getVal(entities.grid_to_battery));
+ solarToBatt = Math.max(0, batteryCharge - gridToBatt);
} else {
- solarToBatt = solar;
- gridToBatt = batteryCharge - solar;
+ if (solar >= batteryCharge) {
+ solarToBatt = batteryCharge;
+ gridToBatt = 0;
+ } else {
+ solarToBatt = solar;
+ gridToBatt = batteryCharge - solar;
+ }
}
}
@@ -520,18 +594,18 @@ console.log(
currentX += width;
}
- addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery', entities.battery);
- addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar', entities.solar);
- addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid', entities.grid);
+ addSegment(srcBattery, 'var(--neon-green)', 'battery', 'battery', entities.battery);
+ addSegment(srcSolar, 'var(--neon-yellow)', 'solar', 'solar', entities.solar);
+ addSegment(srcGrid, 'var(--neon-blue)', 'grid', 'grid', entities.grid_combined || entities.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)'; }
+ if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; }
+ if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; }
+ if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; }
return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
});
@@ -550,7 +624,7 @@ console.log(
let iconColor = '';
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 = this._getConsumerColor(1); }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; }
@@ -569,7 +643,7 @@ console.log(
addBottomBracket(destHouse, 'house', entities.house);
addBottomBracket(destEV, 'car', entities.consumer_1);
- addBottomBracket(destExport, 'export', entities.grid_export || entities.grid);
+ addBottomBracket(destExport, 'export', entities.grid_combined || entities.grid_export || entities.grid);
addBottomBracket(batteryCharge, 'battery', entities.battery);
// Note: If there is Battery Charging happening, bottomX will not reach fullWidth.
@@ -651,7 +725,7 @@ console.log(
// 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 labelGridText = this.config.grid_label || this._localize('card.label_grid');
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');
@@ -660,9 +734,31 @@ console.log(
const iconGrid = this.config.grid_icon;
const iconBattery = this.config.battery_icon;
+ // SECONDARY SENSORS (display only)
+ const hasSecondarySolar = !!(entities.secondary_solar && entities.secondary_solar !== "");
+ const hasSecondaryGrid = !!(entities.secondary_grid && entities.secondary_grid !== "");
+ const hasSecondaryBattery = !!(entities.secondary_battery && entities.secondary_battery !== "");
+
+ const getSecondaryVal = (entity) => {
+ if (!entity) return '';
+ const state = this.hass.states[entity];
+ if (!state) return '';
+ const val = parseFloat(state.state);
+ if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : '');
+ const unit = state.attributes.unit_of_measurement || '';
+ if (unit === 'W' || unit === 'Wh') {
+ return this._formatPower(val);
+ }
+ if (unit === 'kWh' || unit === 'kW') {
+ return val.toFixed(1) + ' ' + unit;
+ }
+ return val.toFixed(1) + (unit ? ' ' + unit : '');
+ };
+
// Determine existence of main entities
const hasSolar = !!(entities.solar && entities.solar !== "");
- const hasGrid = !!(entities.grid && entities.grid !== "");
+ const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== "");
+ const hasGrid = !!(entities.grid && entities.grid !== "") || hasGridCombined;
const hasBattery = !!(entities.battery && entities.battery !== "");
const styleSolar = hasSolar ? '' : 'display: none;';
@@ -683,7 +779,9 @@ console.log(
return state ? parseFloat(state.state) || 0 : 0;
};
- const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0;
+ let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0;
+ if (this.config.invert_consumer_1) { c1Val *= -1; }
+ c1Val = Math.abs(c1Val);
const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0;
const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0;
@@ -693,7 +791,8 @@ console.log(
const anyBottomVisible = showC1 || showC2 || showC3;
const solar = hasSolar ? getVal(entities.solar) : 0;
- const gridMain = hasGrid ? getVal(entities.grid) : 0;
+ const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0;
+ const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0);
const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0;
let battery = hasBattery ? getVal(entities.battery) : 0;
if (this.config.invert_battery) {
@@ -707,7 +806,11 @@ console.log(
let gridExport = 0;
if (hasGrid) {
- if (entities.grid_export && entities.grid_export !== "") {
+ if (hasGridCombined) {
+ // COMBINED SENSOR: positive = import, negative = export
+ gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0;
+ gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0;
+ } else if (entities.grid_export && entities.grid_export !== "") {
gridImport = gridMain > 0 ? gridMain : 0;
gridExport = Math.abs(gridExpSensor);
} else {
@@ -723,12 +826,20 @@ console.log(
let gridToBatt = 0;
if (hasBattery && batteryCharge > 0) {
- if (solarVal >= batteryCharge) {
- solarToBatt = batteryCharge;
- gridToBatt = 0;
+ const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== "");
+ if (hasGridToBattSensor) {
+ // Use dedicated grid-to-battery sensor
+ gridToBatt = Math.abs(getVal(entities.grid_to_battery));
+ solarToBatt = Math.max(0, batteryCharge - gridToBatt);
} else {
- solarToBatt = solarVal;
- gridToBatt = batteryCharge - solarVal;
+ // Calculate: solar prioritized
+ if (solarVal >= batteryCharge) {
+ solarToBatt = batteryCharge;
+ gridToBatt = 0;
+ } else {
+ solarToBatt = solarVal;
+ gridToBatt = batteryCharge - solarVal;
+ }
}
}
@@ -751,6 +862,8 @@ console.log(
if (scale > 1.5) scale = 1.5;
const finalCardHeightPx = contentHeight * scale;
+ const visualWidth = 420 * scale;
+ const centerMarginLeft = Math.max(0, (availableWidth - visualWidth) / 2);
let houseGradientVal = '';
let houseTextCol = useColoredValues ? 'var(--neon-pink)' : '';
@@ -777,11 +890,11 @@ console.log(
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 (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; }
if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); }
- houseGradientVal = `conic-gradient(${stops.join(', ')})`;
+ houseGradientVal = `conic-gradient(from 330deg, ${stops.join(', ')})`;
if (useColoredValues) {
const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge);
@@ -812,25 +925,51 @@ console.log(
const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`;
const isSolarActive = Math.round(solarVal) > 0;
- const isGridActive = Math.round(gridImport) > 0;
+ const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0;
+ const isGridExporting = Math.round(gridExport) > 0 && 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 gridColor = isGridExporting ? 'var(--neon-red)' : (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;
+ // --- Dynamic speed based on power ---
+ // Higher power = faster animation (shorter duration)
+ // Range: 2s (very fast, ~5000W+) to 12s (slow, ~50W)
+ const minDuration = 4;
+ const maxDuration = 12;
+ const factor = 12000;
+ let duration = factor / val;
+ duration = Math.max(minDuration, Math.min(maxDuration, duration));
+
+ // --- Dynamic particle density based on power ---
+ // Higher power = more/denser particles (shorter gap)
+ // Lower power = fewer/sparse particles (longer gap)
+ let dashSize, gapSize;
+ if (showTail) {
+ // Comet tail: vary tail length with power
+ dashSize = Math.round(15 + (val / 200) * 25); // 15-40
+ dashSize = Math.min(dashSize, 40);
+ gapSize = Math.round(380 - (val / 200) * 200); // 380-180
+ gapSize = Math.max(gapSize, 180);
+ } else if (showDashedLine) {
+ // Dashed line: vary dash density
+ dashSize = Math.round(8 + (val / 500) * 10); // 8-18
+ dashSize = Math.min(dashSize, 18);
+ gapSize = Math.round(18 - (val / 1000) * 10); // 18-8
+ gapSize = Math.max(gapSize, 8);
+ duration = duration * 5; // Dashed lines are slower
+ } else {
+ // Default dots: vary dot count/density
+ dashSize = 0; // stays as dots
+ gapSize = Math.round(380 - (val / 200) * 250); // 380-130
+ gapSize = Math.max(gapSize, 130);
}
- return `opacity: 1; animation-duration: ${duration}s;`;
+ const dynamicDash = `${dashSize} ${gapSize}`;
+
+ return `opacity: 1; animation-duration: ${duration}s; stroke-dasharray: ${dynamicDash};`;
};
const getPipeStyle = (val) => {
@@ -860,6 +999,14 @@ console.log(
return html`${text}
`;
};
+ const renderSecondaryOrLabel = (labelText, showLabel, secondaryEntity, hasSecondary) => {
+ if (hasSecondary) {
+ const secVal = getSecondaryVal(secondaryEntity);
+ return html`${secVal}
`;
+ }
+ return renderLabel(labelText, showLabel);
+ };
+
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);' : '')));
@@ -887,10 +1034,14 @@ console.log(
iconContent = this._renderIcon(iconType, val);
}
+ const secEntity = entities[`secondary_${configKey}`];
+ const hasSecondary = !!(secEntity && secEntity !== "");
+
return html`
-
+
this._handleClick(entities[configKey])}>
${iconContent}
- ${renderLabel(label, true)}
+ ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)}
${this._formatPower(val)}
`;
@@ -909,7 +1060,7 @@ console.log(
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 pathGridExport = "M 95 115 Q 130 145 165 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";
@@ -923,7 +1074,7 @@ console.log(
return html`
-
+
@@ -937,9 +1088,9 @@ console.log(
-
-
-
+
+
+
@@ -950,9 +1101,9 @@ console.log(
-
-
-
+
+
+
${this._formatPower(solarToHouse)}
${this._formatPower(solarToBatt)}
@@ -966,36 +1117,43 @@ console.log(
${hasSolar ? html`
-
+
this._handleClick(entities.solar)}>
${renderMainIcon('solar', solarVal, iconSolar, solarColor)}
- ${renderLabel(labelSolarText, showLabelSolar)}
+ ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)}
${this._formatPower(solarVal)}
` : ''}
${hasGrid ? html`
-
- ${renderMainIcon('grid', gridImport, iconGrid, gridColor)}
- ${renderLabel(labelGridText, showLabelGrid)}
-
${this._formatPower(gridImport)}
+
this._handleClick(entities.grid_combined || entities.grid)}>
+ ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)}
+ ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)}
+
+ ${isGridExporting ? html`▲ ` : (isGridActive ? html`▼ ` : '')}
+ ${this._formatPower(isGridExporting ? gridExport : gridImport)}
+
` : ''}
${hasBattery ? html`
-
+
this._handleClick(entities.battery)}>
${renderMainIcon('battery', battSoc, iconBattery)}
- ${renderLabel(labelBatteryText, showLabelBattery)}
+ ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)}
${Math.round(battSoc)}%
` : ''}
+ style="${houseBubbleStyle}"
+ @click=${() => this._handleClick(entities.house)}>
${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')}
+ ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))}
+ ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, this._getConsumerColor(2))}
+ ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, this._getConsumerColor(3))}