Compare commits

..

6 commits
v_2.3 ... main

Author SHA1 Message Date
f19730ff1d Fix: Secondary sensor display - Wh/EUR precision
- Separate Wh from W in getSecondaryVal: Wh values >= 1000
  now display as kWh (e.g., 794 Wh -> 0.79 kWh)
- EUR/ct/€ units now show 2 decimal places (0.28 EUR/kWh
  instead of 0.3)
- W values continue using _formatPower() as before

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 10:27:27 +01:00
jayjojayson
1031af5b3d
v_2.4 2026-03-04 13:04:31 +01:00
jayjojayson
97fd58f671
v_2.4 2026-03-04 01:24:16 +01:00
jayjojayson
2dc24eca1b v_2.4 2026-03-04 01:05:14 +01:00
jayjojayson
9430bc9bcf power-flux-card Auto-build 2026-03-03 22:31:03 +00:00
jayjojayson
4ad407f030 v_2.4 2026-03-03 23:30:52 +01:00
8 changed files with 714 additions and 227 deletions

1
.github/FUNDING.yml vendored
View file

@ -1,2 +1,3 @@
--- ---
ko_fi: jayjojayson
custom: ["https://www.paypal.me/quadFlyerFW"] custom: ["https://www.paypal.me/quadFlyerFW"]

150
README.md
View file

@ -7,7 +7,7 @@
[![Stars](https://img.shields.io/github/stars/jayjojayson/power-flux-card)](https://github.com/jayjojayson/power-flux-card/stargazers) [![Stars](https://img.shields.io/github/stars/jayjojayson/power-flux-card)](https://github.com/jayjojayson/power-flux-card/stargazers)
# Power Flux Card # Power Flux Card
The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations.
@ -84,3 +84,151 @@ You can configure the card directly via the visual editor in Home Assistant.
- **Donut Chart**: Show the energy mix as a ring around the house. - **Donut Chart**: Show the energy mix as a ring around the house.
- **Comet Tail / Dashed Line**: Change the flow animation style. - **Comet Tail / Dashed Line**: Change the flow animation style.
- **Compact View**: Switch to the bar chart layout. - **Compact View**: Switch to the bar chart layout.
- **Color Options**: Define custom colors for each source and consumer.
- **Grid Import/Export**: Configure separate or combined entities.
- **Grid-to-Battery**: Optional direct sensor for Grid-to-Battery flow.
- **Separate Battery Sensors**: Optional separate sensors for battery charge and discharge.
- **Secondary Sensors**: Display alternative values in the main circles (e.g., daily yield, current charge power).
<details>
<summary> <b>Custom Colors with card_mod and Jinja2 Templates</b></summary>
With the [card_mod](https://github.com/thomasloven/lovelace-card-mod) integration, you can dynamically override the CSS variables of the Power Flux Card using Jinja2 templates. This allows you to change colors based on sensor values — e.g., green solar icon during production, grey when idle.
### Available CSS Variables
| Variable | Description |
|---|---|
| `--neon-yellow` | Bubble color Solar |
| `--neon-blue` | Bubble color Grid |
| `--neon-green` | Bubble color Battery |
| `--neon-pink` | Bubble color House |
| `--pipe-solar-color` | Pipe color Solar |
| `--pipe-grid-color` | Pipe color Grid |
| `--pipe-battery-color` | Pipe color Battery |
| `--icon-solar-color` | Icon color Solar |
| `--icon-grid-color` | Icon color Grid |
| `--icon-battery-color` | Icon color Battery |
| `--icon-house-color` | Icon color House |
| `--icon-consumer-1-color` | Icon color Consumer 1 |
| `--text-solar-color` | Text color Solar |
| `--text-grid-color` | Text color Grid |
| `--text-battery-color` | Text color Battery |
| `--text-house-color` | Text color House |
| `--text-consumer-1-color` | Text color Consumer 1 |
| `--consumer-1-color` | Bubble color Consumer 1 |
| `--consumer-2-color` | Bubble color Consumer 2 |
| `--consumer-3-color` | Bubble color Consumer 3 |
| `--export-color` | Color for Export |
### Example 1: Solar Icon — green during production, grey when idle
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% if states('sensor.solar_power') | float > 0 %}
--icon-solar-color: #00ff88;
{% else %}
--icon-solar-color: #9e9e9e;
{% endif %}
}
```
### Example 2: Grid text color — red on export, blue on import
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid_combined: sensor.grid_power_combined
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% if states('sensor.grid_power_combined') | float < 0 %}
--text-grid-color: #ff3333;
{% else %}
--text-grid-color: #3b82f6;
{% endif %}
}
```
### Example 3: Battery bubble — color based on State of Charge (SoC)
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% set soc = states('sensor.battery_soc') | float %}
{% if soc > 80 %}
--neon-green: #00ff88;
{% elif soc > 30 %}
--neon-green: #f59e0b;
{% else %}
--neon-green: #ff3333;
{% endif %}
}
```
### Example 4: Consumer 1 pipe — visible only at high power, otherwise transparent
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
consumer_1: sensor.wallbox_power
card_mod:
style: |
:host {
{% if states('sensor.wallbox_power') | float > 500 %}
--pipe-consumer-1-color: #a855f7;
--icon-consumer-1-color: #a855f7;
{% else %}
--pipe-consumer-1-color: rgba(168, 85, 247, 0.2);
--icon-consumer-1-color: #9e9e9e;
{% endif %}
}
```
### Example 5: Multiple colors at once — night mode (everything dimmed when Solar = 0)
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
consumer_1: sensor.wallbox_power
card_mod:
style: |
:host {
{% if states('sensor.solar_power') | float == 0 %}
--icon-solar-color: #555555;
--text-solar-color: #777777;
--neon-yellow: #666633;
--pipe-solar-color: #444422;
{% endif %}
}
```
> **Note:** card_mod must be installed separately via HACS. Templates are evaluated on every state update, so colors change in real time.
</details>

View file

@ -18,7 +18,7 @@ const lang_de = {
"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": "Kombinierter Batterie Sensor (W)",
"editor.label": "Beschriftung", "editor.label": "Beschriftung",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Zurück", "editor.back": "Zurück",
@ -39,15 +39,22 @@ const lang_de = {
"editor.hide_consumer_icons": "Icons unten ausblenden", "editor.hide_consumer_icons": "Icons unten ausblenden",
"editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)",
"editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)",
"editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)",
"editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "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_sensor": "Kombinierter Netz-Sensor (W, Optional)",
"editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.",
"editor.color_picker": "Kreis Farbe", "editor.color_picker": "Bubble",
"editor.pipe_color": "Rohr Farbe", "editor.pipe_color": "Pipe",
"editor.export_color": "Export Farbe", "editor.export_color": "Export",
"editor.consumer_unit_kw": "Sensor meldet in kW", "editor.consumer_unit_kw": "Sensor meldet in kW",
"editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen",
"editor.battery_charge_sensor": "Batterie-Ladung Sensor (W, Optional)",
"editor.battery_discharge_sensor": "Batterie-Entladung Sensor (W, Optional)",
"editor.battery_separate_hint": "Optional: Separate Sensoren für Laden/Entladen. Überschreiben den Hauptsensor für die Berechnung.",
"editor.consumer_1_hide_pipe": "Pipe bei geringer Leistung ausblenden",
"editor.consumer_pipe_threshold": "Pipe-Schwellenwert (Watt)",
"editor.text_color": "Text",
"editor.icon_color": "Icon",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",
@ -55,8 +62,8 @@ const lang_de = {
"card.label_battery": "Batterie", "card.label_battery": "Batterie",
"card.label_house": "Verbrauch", "card.label_house": "Verbrauch",
"card.label_car": "E-Auto", "card.label_car": "E-Auto",
"card.label_import": "Import", "card.label_heater": "Heizung",
"card.label_export": "Export", "card.label_pool": "Pool",
} }
}; };
const lang_en = { const lang_en = {
@ -74,7 +81,7 @@ const lang_en = {
"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": "Combined Battery Sensor (W)",
"editor.label": "Label", "editor.label": "Label",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Back", "editor.back": "Back",
@ -95,7 +102,7 @@ const lang_en = {
"editor.hide_consumer_icons": "Hide Consumer Icons", "editor.hide_consumer_icons": "Hide Consumer Icons",
"editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.invert_consumer_1": "Invert Sensor Value (+/-)",
"editor.secondary_sensor": "Secondary Sensor (display only)", "editor.secondary_sensor": "Secondary Sensor (display only)",
"editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)",
"editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "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_sensor": "Combined Grid Sensor (W, Optional)",
"editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.",
@ -104,6 +111,13 @@ const lang_en = {
"editor.export_color": "Export Color", "editor.export_color": "Export Color",
"editor.consumer_unit_kw": "Sensor reports in kW", "editor.consumer_unit_kw": "Sensor reports in kW",
"editor.show_consumer_always": "Show Consumers at zero watts", "editor.show_consumer_always": "Show Consumers at zero watts",
"editor.battery_charge_sensor": "Battery Charge Sensor (W, Optional)",
"editor.battery_discharge_sensor": "Battery Discharge Sensor (W, Optional)",
"editor.battery_separate_hint": "Optional: Separate sensors for charge/discharge. Override the main sensor for calculations.",
"editor.consumer_1_hide_pipe": "Hide pipe at low power",
"editor.consumer_pipe_threshold": "Pipe Threshold (Watts)",
"editor.text_color": "Text Color",
"editor.icon_color": "Icon Color",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",
@ -111,8 +125,8 @@ const lang_en = {
"card.label_battery": "Battery", "card.label_battery": "Battery",
"card.label_house": "Consumption", "card.label_house": "Consumption",
"card.label_car": "Car", "card.label_car": "Car",
"card.label_import": "Import", "card.label_heater": "Heater",
"card.label_export": "Export", "card.label_pool": "Pool",
} }
}; };
@ -172,7 +186,7 @@ class PowerFluxCardEditor extends LitElement {
if (!this._config || !this.hass) return; if (!this._config || !this.hass) return;
const target = ev.target; const target = ev.target;
const key = target.configValue || this._currentConfigValue; const key = target.configValue;
let value; let value;
if (target.tagName === 'HA-SWITCH') { if (target.tagName === 'HA-SWITCH') {
@ -191,6 +205,7 @@ class PowerFluxCardEditor extends LitElement {
const entityKeys = [ const entityKeys = [
'solar', 'grid', 'grid_export', 'grid_combined', 'solar', 'grid', 'grid_export', 'grid_combined',
'battery', 'battery_soc', 'grid_to_battery', 'battery', 'battery_soc', 'grid_to_battery',
'battery_charge', 'battery_discharge',
'house', 'house',
'consumer_1', 'consumer_2', 'consumer_3', 'consumer_1', 'consumer_2', 'consumer_3',
'secondary_solar', 'secondary_grid', 'secondary_battery', 'secondary_solar', 'secondary_grid', 'secondary_battery',
@ -286,31 +301,30 @@ class PowerFluxCardEditor extends LitElement {
`; `;
} }
_renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { _renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) {
const bubbleColor = this._config[bubbleKey] || defaultColor; const items = [
const pipeColor = this._config[pipeKey] || defaultColor; { key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor },
const hasBubbleCustom = !!this._config[bubbleKey]; ];
const hasPipeCustom = !!this._config[pipeKey]; if (pipeKey) items.push({ key: pipeKey, label: this._localize('editor.pipe_color'), default: defaultColor });
items.push({ key: textKey, label: this._localize('editor.text_color'), default: defaultColor });
items.push({ key: iconKey, label: this._localize('editor.icon_color'), default: defaultColor });
return html` return html`
<div class="color-picker-dual"> <div class="color-picker-quad">
<div class="color-picker-row"> ${items.map(item => {
<input type="color" const color = this._config[item.key] || item.default;
.value=${bubbleColor} const hasCustom = !!this._config[item.key];
@input=${(e) => this._colorChanged(bubbleKey, e)}> return html`
<span class="color-label">${this._localize('editor.color_picker')}</span> <div class="color-picker-row">
${hasBubbleCustom ? html`<ha-icon class="color-reset-btn" <input type="color"
icon="mdi:refresh" .value=${color}
@click=${() => this._resetColor(bubbleKey)}></ha-icon>` : ''} @input=${(e) => this._colorChanged(item.key, e)}>
</div> <span class="color-label">${item.label}</span>
<div class="color-picker-row"> ${hasCustom ? html`<ha-icon class="color-reset-btn"
<input type="color" icon="mdi:refresh"
.value=${pipeColor} @click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
@input=${(e) => this._colorChanged(pipeKey, e)}> </div>
<span class="color-label">${this._localize('editor.pipe_color')}</span> `;
${hasPipeCustom ? html`<ha-icon class="color-reset-btn" })}
icon="mdi:refresh"
@click=${() => this._resetColor(pipeKey)}></ha-icon>` : ''}
</div>
</div> </div>
`; `;
} }
@ -422,8 +436,8 @@ class PowerFluxCardEditor extends LitElement {
-webkit-appearance: none; -webkit-appearance: none;
border: 2px solid var(--divider-color); border: 2px solid var(--divider-color);
border-radius: 50%; border-radius: 50%;
width: 36px; width: 30px;
height: 36px; height: 30px;
padding: 2px; padding: 2px;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
@ -447,11 +461,11 @@ class PowerFluxCardEditor extends LitElement {
.color-reset-btn:hover { .color-reset-btn:hover {
color: var(--primary-color); color: var(--primary-color);
} }
.color-picker-dual { .color-picker-quad {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.color-picker-dual .color-picker-row { .color-picker-quad .color-picker-row {
flex: 1; flex: 1;
} }
`; `;
@ -492,7 +506,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')} ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
<div class="separator"></div> <div class="separator"></div>
@ -560,7 +574,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')}
${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')} ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@ -597,13 +611,14 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))}
${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} <div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;"> <div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.grid_to_battery_hint')} ${this._localize('editor.battery_separate_hint')}
</div> </div>
${this._renderEntitySelector(entitySelectorSchema, entities.battery_charge || "", 'battery_charge', this._localize('editor.battery_charge_sensor'))}
${this._renderEntitySelector(entitySelectorSchema, entities.battery_discharge || "", 'battery_discharge', this._localize('editor.battery_discharge_sensor'))}
<div class="separator"></div> <div class="separator"></div>
<ha-selector <ha-selector
@ -624,9 +639,20 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))}
<div class="separator"></div>
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.grid_to_battery_hint')}
</div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')} ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
<div class="separator"></div> <div class="separator"></div>
@ -674,6 +700,8 @@ class PowerFluxCardEditor extends LitElement {
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;"> <div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.house_sensor_hint')} ${this._localize('editor.house_sensor_hint')}
</div> </div>
${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -707,6 +735,26 @@ class PowerFluxCardEditor extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px;">
<span>${this._localize('editor.consumer_1_hide_pipe')}</span>
<ha-switch
.checked=${this._config.consumer_1_hide_pipe === true}
.configValue=${'consumer_1_hide_pipe'}
@change=${this._valueChanged}
></ha-switch>
</div>
${this._config.consumer_1_hide_pipe === true ? html`
<ha-selector
.hass=${this.hass}
.selector=${{ number: { min: 0, max: 2000, step: 10, mode: "slider" } }}
.value=${this._config.consumer_1_pipe_threshold !== undefined ? this._config.consumer_1_pipe_threshold : 0}
.configValue=${'consumer_1_pipe_threshold'}
.label=${this._localize('editor.consumer_pipe_threshold')}
@value-changed=${this._valueChanged}
></ha-selector>
` : ''}
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px; margin-bottom: 8px;">
<span>${this._localize('editor.consumer_unit_kw')}</span> <span>${this._localize('editor.consumer_unit_kw')}</span>
<ha-switch <ha-switch
@ -718,7 +766,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_1', 'color_pipe_consumer_1', '#a855f7')} ${this._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -754,7 +802,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')} ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -790,7 +838,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')} ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
</div> </div>
`; `;
} }
@ -955,7 +1003,7 @@ customElements.define("power-flux-card-editor", PowerFluxCardEditor);
console.log( console.log(
"%c⚡ Power Flux Card v_2.1 ready", "%c⚡ Power Flux Card v_2.4 ready",
"background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;"
); );
@ -996,6 +1044,8 @@ console.log(
consumer_2_unit_kw: false, consumer_2_unit_kw: false,
consumer_3_unit_kw: false, consumer_3_unit_kw: false,
show_consumer_always: false, show_consumer_always: false,
consumer_1_hide_pipe: false,
consumer_1_pipe_threshold: 0,
show_donut_border: false, show_donut_border: false,
show_neon_glow: true, show_neon_glow: true,
show_comet_tail: false, show_comet_tail: false,
@ -1018,6 +1068,8 @@ console.log(
grid_combined: "", grid_combined: "",
battery: "", battery: "",
battery_soc: "", battery_soc: "",
battery_charge: "",
battery_discharge: "",
house: "", house: "",
consumer_1: "", consumer_1: "",
consumer_2: "", consumer_2: "",
@ -1080,6 +1132,21 @@ console.log(
'color_pipe_consumer_1': '--pipe-consumer-1-color', 'color_pipe_consumer_1': '--pipe-consumer-1-color',
'color_pipe_consumer_2': '--pipe-consumer-2-color', 'color_pipe_consumer_2': '--pipe-consumer-2-color',
'color_pipe_consumer_3': '--pipe-consumer-3-color', 'color_pipe_consumer_3': '--pipe-consumer-3-color',
'color_house': '--neon-pink',
'color_icon_solar': '--icon-solar-color',
'color_icon_grid': '--icon-grid-color',
'color_icon_battery': '--icon-battery-color',
'color_icon_house': '--icon-house-color',
'color_icon_consumer_1': '--icon-consumer-1-color',
'color_icon_consumer_2': '--icon-consumer-2-color',
'color_icon_consumer_3': '--icon-consumer-3-color',
'color_text_solar': '--text-solar-color',
'color_text_grid': '--text-grid-color',
'color_text_battery': '--text-battery-color',
'color_text_house': '--text-house-color',
'color_text_consumer_1': '--text-consumer-1-color',
'color_text_consumer_2': '--text-consumer-2-color',
'color_text_consumer_3': '--text-consumer-3-color',
}; };
for (const [configKey, cssVar] of Object.entries(colorMap)) { for (const [configKey, cssVar] of Object.entries(colorMap)) {
if (this.config[configKey]) { if (this.config[configKey]) {
@ -1107,7 +1174,6 @@ console.log(
--neon-green: #00ff88; --neon-green: #00ff88;
--neon-pink: #ff0080; --neon-pink: #ff0080;
--neon-red: #ff3333; --neon-red: #ff3333;
--grid-grey: #9e9e9e;
--export-purple: #a855f7; --export-purple: #a855f7;
--export-color: #ff3333; --export-color: #ff3333;
--consumer-1-color: #a855f7; --consumer-1-color: #a855f7;
@ -1119,6 +1185,20 @@ console.log(
--pipe-consumer-1-color: var(--consumer-1-color); --pipe-consumer-1-color: var(--consumer-1-color);
--pipe-consumer-2-color: var(--consumer-2-color); --pipe-consumer-2-color: var(--consumer-2-color);
--pipe-consumer-3-color: var(--consumer-3-color); --pipe-consumer-3-color: var(--consumer-3-color);
--icon-solar-color: var(--neon-yellow);
--icon-grid-color: var(--neon-blue);
--icon-battery-color: var(--neon-green);
--icon-house-color: var(--neon-pink);
--icon-consumer-1-color: var(--consumer-1-color);
--icon-consumer-2-color: var(--consumer-2-color);
--icon-consumer-3-color: var(--consumer-3-color);
--text-solar-color: var(--neon-yellow);
--text-grid-color: var(--neon-blue);
--text-battery-color: var(--neon-green);
--text-house-color: var(--neon-pink);
--text-consumer-1-color: var(--consumer-1-color);
--text-consumer-2-color: var(--consumer-2-color);
--text-consumer-3-color: var(--consumer-3-color);
--flow-dasharray: 0 380; --flow-dasharray: 0 380;
} }
:host([data-theme-light]) { :host([data-theme-light]) {
@ -1127,7 +1207,6 @@ console.log(
--neon-green: #059669; --neon-green: #059669;
--neon-pink: #db2777; --neon-pink: #db2777;
--neon-red: #dc2626; --neon-red: #dc2626;
--grid-grey: #6b7280;
--export-purple: #7c3aed; --export-purple: #7c3aed;
--export-color: #dc2626; --export-color: #dc2626;
--consumer-1-color: #7c3aed; --consumer-1-color: #7c3aed;
@ -1208,11 +1287,6 @@ console.log(
overflow: hidden; overflow: hidden;
} }
/* Source Colors */
.src-solar { background: var(--neon-yellow); color: black; }
.src-grid { background: var(--neon-blue); color: black; }
.src-battery { background: var(--neon-green); color: black; }
/* --- STANDARD VIEW STYLES --- */ /* --- STANDARD VIEW STYLES --- */
.scale-wrapper { .scale-wrapper {
width: 420px; width: 420px;
@ -1346,7 +1420,7 @@ console.log(
@keyframes dash { to { stroke-dashoffset: -1500; } } @keyframes dash { to { stroke-dashoffset: -1500; } }
.flow-text { .flow-text {
font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: transition: opacity 0.3s ease; font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease;
} }
.flow-text.no-shadow { filter: none; } .flow-text.no-shadow { filter: none; }
.text-solar { fill: var(--pipe-solar-color); } .text-solar { fill: var(--pipe-solar-color); }
@ -1360,35 +1434,36 @@ console.log(
_renderIcon(type, val = 0, colorOverride = null) { _renderIcon(type, val = 0, colorOverride = null) {
if (type === 'solar') { if (type === 'solar') {
const animate = Math.round(val) > 0 ? 'spin-slow' : ''; const animate = Math.round(val) > 0 ? 'spin-slow' : '';
const color = colorOverride || 'var(--neon-yellow)'; const color = colorOverride || 'var(--icon-solar-color)';
return html`<svg class="icon-svg ${animate}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`; return html`<svg class="icon-svg ${animate}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`;
} }
if (type === 'grid') { if (type === 'grid') {
const animate = Math.round(val) > 0 ? 'pulse' : ''; const animate = Math.round(val) > 0 ? 'pulse' : '';
const color = colorOverride || 'var(--neon-blue)'; const color = colorOverride || 'var(--icon-grid-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"></path><path d="M5 8L19 8"></path><path d="M4 14L20 14"></path><path d="M2 22L22 22"></path><circle class="${animate}" cx="12" cy="4" r="4" fill="${color}" stroke="none"></circle></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"></path><path d="M5 8L19 8"></path><path d="M4 14L20 14"></path><path d="M2 22L22 22"></path><circle class="${animate}" cx="12" cy="4" r="4" fill="${color}" stroke="none"></circle></svg>`;
} }
if (type === 'battery') { if (type === 'battery') {
const soc = Math.min(Math.max(val, 0), 100) / 100; const soc = Math.min(Math.max(val, 0), 100) / 100;
const rectHeight = 14 * soc; const rectHeight = 14 * soc;
const rectY = 18 - rectHeight; const rectY = 18 - rectHeight;
const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; const strokeColor = colorOverride || 'var(--icon-battery-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="var(--neon-green)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="12" height="16" rx="2" ry="2"></rect><line x1="10" y1="2" x2="14" y2="2"></line><rect x="7" y="${rectY}" width="10" height="${rectHeight}" fill="${rectColor}" stroke="none"></rect></svg>`; const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="12" height="16" rx="2" ry="2"></rect><line x1="10" y1="2" x2="14" y2="2"></line><rect x="7" y="${rectY}" width="10" height="${rectHeight}" fill="${rectColor}" stroke="none"></rect></svg>`;
} }
if (type === 'house') { if (type === 'house') {
const strokeColor = colorOverride || 'var(--neon-pink)'; const strokeColor = colorOverride || 'var(--icon-house-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`;
} }
if (type === 'car') { if (type === 'car') {
const c = colorOverride || 'var(--consumer-1-color)'; const c = colorOverride || 'var(--icon-consumer-1-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path><circle cx="7" cy="17" r="2"></circle><circle cx="17" cy="17" r="2"></circle><path d="M14 17h-5"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path><circle cx="7" cy="17" r="2"></circle><circle cx="17" cy="17" r="2"></circle><path d="M14 17h-5"></path></svg>`;
} }
if (type === 'heater') { if (type === 'heater') {
const c = colorOverride || 'var(--consumer-2-color)'; const c = colorOverride || 'var(--icon-consumer-2-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20a4 4 0 0 0 4-4V8a4 4 0 0 0-8 0v8a4 4 0 0 0 4 4z"></path><path class="float" style="animation-delay: 0s;" d="M8 4c0-1.5 1-2 2-2s2 .5 2 2"></path><path class="float" style="animation-delay: 0.5s;" d="M14 4c0-1.5 1-2 2-2s2 .5 2 2"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20a4 4 0 0 0 4-4V8a4 4 0 0 0-8 0v8a4 4 0 0 0 4 4z"></path><path class="float" style="animation-delay: 0s;" d="M8 4c0-1.5 1-2 2-2s2 .5 2 2"></path><path class="float" style="animation-delay: 0.5s;" d="M14 4c0-1.5 1-2 2-2s2 .5 2 2"></path></svg>`;
} }
if (type === 'pool') { if (type === 'pool') {
const c = colorOverride || 'var(--consumer-3-color)'; const c = colorOverride || 'var(--icon-consumer-3-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h20"></path><path class="float" d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"></path><path d="M12 2v6"></path><path d="M9 5h6"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h20"></path><path class="float" d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"></path><path d="M12 2v6"></path><path d="M9 5h6"></path></svg>`;
} }
return html``; return html``;
@ -1498,8 +1573,12 @@ console.log(
gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; gridExport = gridMain < 0 ? Math.abs(gridMain) : 0;
} }
const batteryCharge = battery > 0 ? battery : 0; // Check for separate battery charge/discharge sensors
const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== "");
const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== "");
const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0);
const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0);
let solarToBatt = 0; let solarToBatt = 0;
let gridToBatt = 0; let gridToBatt = 0;
@ -1593,9 +1672,9 @@ console.log(
const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); const path = this._createBracketPath(s.startPx, s.widthPx, 'down');
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; }
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; }
return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
@ -1613,10 +1692,10 @@ console.log(
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -1659,14 +1738,19 @@ console.log(
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => html` ${barSegments.map(s => {
const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)'
: s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)'
: s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)'
: (s.color === 'var(--export-purple)' ? 'white' : 'black');
return html`
<div class="bar-segment" <div class="bar-segment"
style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};" style="width: ${s.widthPct}%; background: ${s.color}; color: ${textColor}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}" title="${this._formatPower(s.val)}"
@click=${() => s.entityId && this._handleClick(s.entityId)}> @click=${() => s.entityId && this._handleClick(s.entityId)}>
${s.widthPx > 35 ? this._formatPower(s.val) : ''} ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
</div> </div>
`)} `})}
</div> </div>
<!-- BOTTOM BRACKETS --> <!-- BOTTOM BRACKETS -->
@ -1736,13 +1820,7 @@ console.log(
const val = parseFloat(state.state); const val = parseFloat(state.state);
if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : '');
const unit = state.attributes.unit_of_measurement || ''; const unit = state.attributes.unit_of_measurement || '';
if (unit === 'W' || unit === 'Wh') { if (unit === 'W') { return this._formatPower(val); } if (unit === 'Wh') { if (Math.abs(val) >= 1000) return (val / 1000).toFixed(2) + ' kWh'; return Math.round(val) + ' Wh'; } if (unit === 'kWh' || unit === 'kW') { return val.toFixed(1) + ' ' + unit; } if (unit.includes('EUR') || unit.includes('ct') || unit.includes('€')) { return val.toFixed(2) + ' ' + unit; } return val.toFixed(1) + (unit ? ' ' + unit : '');
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 // Determine existence of main entities
@ -1760,9 +1838,9 @@ console.log(
const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow';
// Custom Labels for Consumers // Custom Labels for Consumers
const labelC1 = this.config.consumer_1_label || "E-Auto"; const labelC1 = this.config.consumer_1_label || this._localize('card.label_car');
const labelC2 = this.config.consumer_2_label || "Heizung"; const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater');
const labelC3 = this.config.consumer_3_label || "Pool"; const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool');
const getVal = (entity) => { const getVal = (entity) => {
const state = this.hass.states[entity]; const state = this.hass.states[entity];
@ -1784,6 +1862,11 @@ console.log(
const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0));
const anyBottomVisible = showC1 || showC2 || showC3; const anyBottomVisible = showC1 || showC2 || showC3;
// Consumer 1 pipe threshold
const hideC1Pipe = this.config.consumer_1_hide_pipe === true;
const c1PipeThreshold = this.config.consumer_1_pipe_threshold || 0;
const c1PipeActive = showC1 && (!hideC1Pipe || c1Val >= c1PipeThreshold);
const solar = hasSolar ? getVal(entities.solar) : 0; const solar = hasSolar ? getVal(entities.solar) : 0;
const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0;
const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0);
@ -1813,8 +1896,12 @@ console.log(
} }
} }
const batteryCharge = battery > 0 ? battery : 0; // Check for separate battery charge/discharge sensors
const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== "");
const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== "");
const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0);
const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0);
let solarToBatt = 0; let solarToBatt = 0;
let gridToBatt = 0; let gridToBatt = 0;
@ -1841,6 +1928,9 @@ console.log(
const gridToHouse = Math.max(0, gridImport - gridToBatt); const gridToHouse = Math.max(0, gridImport - gridToBatt);
const house = solarToHouse + gridToHouse + batteryDischarge; const house = solarToHouse + gridToHouse + batteryDischarge;
// Use house entity for display if defined, otherwise use calculated value
const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house;
const isTopArcActive = (solarToBatt > 0); const isTopArcActive = (solarToBatt > 0);
const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50;
let baseHeight = anyBottomVisible ? 480 : 340; let baseHeight = anyBottomVisible ? 480 : 340;
@ -1942,8 +2032,10 @@ console.log(
} }
} }
const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; const solarColor = isSolarActive ? 'var(--icon-solar-color)' : 'var(--secondary-text-color)';
const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)');
const gridIconColor = (isGridActive && this.config.color_icon_grid) ? 'var(--icon-grid-color)' : gridColor;
const gridTextColor = (isGridActive && this.config.color_text_grid) ? 'var(--text-grid-color)' : gridColor;
const getAnimStyle = (val) => { const getAnimStyle = (val) => {
if (val <= 1) return "opacity: 0;"; if (val <= 1) return "opacity: 0;";
@ -2023,27 +2115,24 @@ console.log(
const renderMainIcon = (type, val, customIcon, color = null) => { const renderMainIcon = (type, val, customIcon, color = null) => {
if (customIcon) { 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);' : ''))); const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : ''))));
return html`<ha-icon icon="${customIcon}" class="icon-custom" style="${style}"></ha-icon>`; return html`<ha-icon icon="${customIcon}" class="icon-custom" style="${style}"></ha-icon>`;
} }
return this._renderIcon(type, val, color); return this._renderIcon(type, val, color);
}; };
const getCustomClass = (icon) => icon ? 'has-custom-icon' : '';
const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => {
if (!isVisible) return html``; if (!isVisible) return html``;
const customIcon = this.config[`${configKey}_icon`]; const customIcon = this.config[`${configKey}_icon`];
let iconContent; let iconContent;
const isCustom = !hideConsumerIcons && !!customIcon; const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`;
const dynamicClass = isCustom ? 'has-custom-icon' : '';
if (hideConsumerIcons) { if (hideConsumerIcons) {
iconContent = html``; iconContent = html``;
} else if (customIcon) { } else if (customIcon) {
iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${hexColor};"></ha-icon>`; iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${iconColorVar};"></ha-icon>`;
} else { } else {
iconContent = this._renderIcon(iconType, val); iconContent = this._renderIcon(iconType, val);
} }
@ -2051,12 +2140,16 @@ console.log(
const secEntity = entities[`secondary_${configKey}`]; const secEntity = entities[`secondary_${configKey}`];
const hasSecondary = !!(secEntity && secEntity !== ""); const hasSecondary = !!(secEntity && secEntity !== "");
const textStyle = this.config[`color_text_${configKey}`]
? `color: var(--text-${configKey.replace(/_/g, '-')}-color);`
: getConsumerColorStyle(hexColor);
return html` return html`
<div class="bubble ${cssClass} node ${cssClass.replace('c', 'node-c')} ${tintClass} ${dynamicClass} ${glowClass}" <div class="bubble ${cssClass} ${cssClass.replace('c', 'node-c')} ${tintClass} ${glowClass}"
@click=${() => this._handleClick(entities[configKey])}> @click=${() => this._handleClick(entities[configKey])}>
${iconContent} ${iconContent}
${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)}
<div class="value" style="${getConsumerColorStyle(hexColor)}">${this._formatPower(val)}</div> <div class="value" style="${textStyle}">${this._formatPower(val)}</div>
</div> </div>
`; `;
}; };
@ -2086,7 +2179,9 @@ console.log(
const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC2 = "M 210 310 L 210 370";
const pathHouseC3 = "M 255 265 Q 370 265 370 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370";
const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; const houseTextStyle = this.config.color_text_house
? 'color: var(--text-house-color);'
: (houseTextCol ? `color: ${houseTextCol};` : '');
const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380');
const strokeWidthVal = showDashedLine ? 4 : 8; const strokeWidthVal = showDashedLine ? 4 : 8;
@ -2107,7 +2202,7 @@ console.log(
<path class="bg-path bg-battery" d="${pathBattHouse}" style="${getPipeStyle(batteryDischarge)} ${styleBattery}" /> <path class="bg-path bg-battery" d="${pathBattHouse}" style="${getPipeStyle(batteryDischarge)} ${styleBattery}" />
<path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerPipeColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(showC1, c1Val)}" /> <path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerPipeColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(c1PipeActive, c1Val)}" />
<path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" /> <path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" />
<path d="${pathHouseC3}" fill="none" stroke="${this._getConsumerPipeColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" /> <path d="${pathHouseC3}" fill="none" stroke="${this._getConsumerPipeColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" />
@ -2120,7 +2215,7 @@ console.log(
<path class="flow-line flow-battery" d="${pathBattHouse}" style="${getAnimStyle(batteryDischarge)} ${styleBattery}" /> <path class="flow-line flow-battery" d="${pathBattHouse}" style="${getAnimStyle(batteryDischarge)} ${styleBattery}" />
<path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerPipeColor(1)}" style="${getConsumerAnimStyle(showC1, c1Val)}" /> <path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerPipeColor(1)}" style="${getConsumerAnimStyle(c1PipeActive, c1Val)}" />
<path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" /> <path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" />
<path class="flow-line" d="${pathHouseC3}" stroke="${this._getConsumerPipeColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" /> <path class="flow-line" d="${pathHouseC3}" stroke="${this._getConsumerPipeColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" />
@ -2136,39 +2231,39 @@ console.log(
</svg> </svg>
${hasSolar ? html` ${hasSolar ? html`
<div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node node-solar ${tintClass} ${isSolarActive ? glowClass : ''} ${getCustomClass(iconSolar)}" <div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node-solar ${tintClass} ${isSolarActive ? glowClass : ''}"
@click=${() => this._handleClick(entities.solar)}> @click=${() => this._handleClick(entities.solar)}>
${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderMainIcon('solar', solarVal, iconSolar, solarColor)}
${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)}
<div class="value" style="${isSolarActive ? getColorStyle('--neon-yellow') : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div> <div class="value" style="${isSolarActive ? (this.config.color_text_solar ? 'color: var(--text-solar-color);' : getColorStyle('--neon-yellow')) : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div>
</div>` : ''} </div>` : ''}
${hasGrid ? html` ${hasGrid ? html`
<div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node node-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''} ${getCustomClass(iconGrid)}" <div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''}"
style="${showDonut && isGridActive ? `--grid-gradient: ${gridGradientVal};` : ''}" style="${showDonut && isGridActive ? `--grid-gradient: ${gridGradientVal};` : ''}"
@click=${() => this._handleClick(entities.grid_combined || entities.grid)}> @click=${() => this._handleClick(entities.grid_combined || entities.grid)}>
${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)}
${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)}
<div class="value" style="color: ${gridColor};"> <div class="value" style="color: ${gridTextColor};">
${isGridExporting ? html`<span class="direction-arrow">&#9650;</span>` : (isGridActive ? html`<span class="direction-arrow">&#9660;</span>` : '')} ${isGridExporting ? html`<span class="direction-arrow">&#9650;</span>` : (isGridActive ? html`<span class="direction-arrow">&#9660;</span>` : '')}
${this._formatPower(isGridExporting ? gridExport : gridImport)} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
</div> </div>
</div>` : ''} </div>` : ''}
${hasBattery ? html` ${hasBattery ? html`
<div class="bubble battery node node-battery ${tintClass} ${glowClass} ${getCustomClass(iconBattery)}" <div class="bubble battery node-battery ${tintClass} ${glowClass}"
@click=${() => this._handleClick(entities.battery)}> @click=${() => this._handleClick(entities.battery)}>
${renderMainIcon('battery', battSoc, iconBattery)} ${renderMainIcon('battery', battSoc, iconBattery)}
${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)}
<div class="value" style="${getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div> <div class="value" style="${this.config.color_text_battery ? 'color: var(--text-battery-color);' : getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div>
</div>` : ''} </div>` : ''}
<div class="bubble house node node-house ${showDonut ? 'donut' : ''} ${tintClass}" <div class="bubble house node-house ${showDonut ? 'donut' : ''} ${tintClass}"
style="${houseBubbleStyle}" style="${houseBubbleStyle}"
@click=${() => this._handleClick(entities.house)}> @click=${() => this._handleClick(entities.house)}>
${renderMainIcon('house', 0, null, houseDominantColor)} ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)}
${renderLabel(labelHouseText, showLabelHouse)} ${renderLabel(labelHouseText, showLabelHouse)}
<div class="value" style="${houseTextStyle}">${this._formatPower(house)}</div> <div class="value" style="${houseTextStyle}">${this._formatPower(houseDisplay)}</div>
</div> </div>
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))}

View file

@ -86,3 +86,151 @@ Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigur
- **Donut Chart**: Zeigt den Energiemix als Ring um das Haus an. - **Donut Chart**: Zeigt den Energiemix als Ring um das Haus an.
- **Kometenschweif / Gestrichelte Linie**: Ändern Sie den Stil der Flussanimation. - **Kometenschweif / Gestrichelte Linie**: Ändern Sie den Stil der Flussanimation.
- **Kompakte Ansicht**: Wechseln Sie zum Balkendiagramm-Layout. - **Kompakte Ansicht**: Wechseln Sie zum Balkendiagramm-Layout.
- **Farboptionen**: Definieren Sie benutzerdefinierte Farben für jede Quelle und Verbraucher.
- **Netz-Import/Export**: Konfigurieren Sie separate oder kombinierte Entitäten.
- **Netz-zu-Batterie**: Optionaler direkter Sensor für den Netz-zu-Batterie-Fluss.
- **Batterie getrennte Sensoren**: Optional separate Sensoren für Batterie-Ladung und -Entladung.
- **Sekundäre Sensoren**: Zeigen Sie alternative Werte in den Hauptkreisen an (z.B. Tagesertrag, aktuelle Ladeleistung).
<details>
<summary> <b>Custom Farben mit card_mod und Jinja2 Templates</b></summary>
Mit der [card_mod](https://github.com/thomasloven/lovelace-card-mod) Integration können die CSS-Variablen der Power Flux Card dynamisch per Jinja2-Templates überschrieben werden. So lassen sich Farben abhängig von Sensorwerten ändern — z.B. Solar-Icon grün bei Produktion, grau bei Stillstand.
### Verfügbare CSS-Variablen
| Variable | Beschreibung |
|---|---|
| `--neon-yellow` | Bubble-Farbe Solar |
| `--neon-blue` | Bubble-Farbe Grid |
| `--neon-green` | Bubble-Farbe Batterie |
| `--neon-pink` | Bubble-Farbe Haus |
| `--pipe-solar-color` | Pipe-Farbe Solar |
| `--pipe-grid-color` | Pipe-Farbe Grid |
| `--pipe-battery-color` | Pipe-Farbe Batterie |
| `--icon-solar-color` | Icon-Farbe Solar |
| `--icon-grid-color` | Icon-Farbe Grid |
| `--icon-battery-color` | Icon-Farbe Batterie |
| `--icon-house-color` | Icon-Farbe Haus |
| `--icon-consumer-1-color` | Icon-Farbe Consumer 1 |
| `--text-solar-color` | Text-Farbe Solar |
| `--text-grid-color` | Text-Farbe Grid |
| `--text-battery-color` | Text-Farbe Batterie |
| `--text-house-color` | Text-Farbe Haus |
| `--text-consumer-1-color` | Text-Farbe Consumer 1 |
| `--consumer-1-color` | Bubble-Farbe Consumer 1 |
| `--consumer-2-color` | Bubble-Farbe Consumer 2 |
| `--consumer-3-color` | Bubble-Farbe Consumer 3 |
| `--export-color` | Farbe für Export |
### Beispiel 1: Solar-Icon — grün bei Produktion, grau bei Stillstand
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% if states('sensor.solar_power') | float > 0 %}
--icon-solar-color: #00ff88;
{% else %}
--icon-solar-color: #9e9e9e;
{% endif %}
}
```
### Beispiel 2: Grid-Textfarbe — rot bei Export, blau bei Import
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid_combined: sensor.grid_power_combined
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% if states('sensor.grid_power_combined') | float < 0 %}
--text-grid-color: #ff3333;
{% else %}
--text-grid-color: #3b82f6;
{% endif %}
}
```
### Beispiel 3: Batterie-Bubble — Farbe nach Ladestand (SoC)
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
card_mod:
style: |
:host {
{% set soc = states('sensor.battery_soc') | float %}
{% if soc > 80 %}
--neon-green: #00ff88;
{% elif soc > 30 %}
--neon-green: #f59e0b;
{% else %}
--neon-green: #ff3333;
{% endif %}
}
```
### Beispiel 4: Consumer-1-Pipe — sichtbar nur bei hoher Leistung, sonst transparent
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
consumer_1: sensor.wallbox_power
card_mod:
style: |
:host {
{% if states('sensor.wallbox_power') | float > 500 %}
--pipe-consumer-1-color: #a855f7;
--icon-consumer-1-color: #a855f7;
{% else %}
--pipe-consumer-1-color: rgba(168, 85, 247, 0.2);
--icon-consumer-1-color: #9e9e9e;
{% endif %}
}
```
### Beispiel 5: Mehrere Farben gleichzeitig — Nachtmodus (alles gedimmt wenn Solar = 0)
```yaml
type: custom:power-flux-card
entities:
solar: sensor.solar_power
grid: sensor.grid_power
battery: sensor.battery_power
battery_soc: sensor.battery_soc
consumer_1: sensor.wallbox_power
card_mod:
style: |
:host {
{% if states('sensor.solar_power') | float == 0 %}
--icon-solar-color: #555555;
--text-solar-color: #777777;
--neon-yellow: #666633;
--pipe-solar-color: #444422;
{% endif %}
}
```
> **Hinweis:** card_mod muss separat über HACS installiert werden. Die Templates werden bei jedem State-Update ausgewertet, die Farben ändern sich also in Echtzeit.
</details>

View file

@ -13,7 +13,7 @@ export default {
"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": "Kombinierter Batterie Sensor (W)",
"editor.label": "Beschriftung", "editor.label": "Beschriftung",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Zurück", "editor.back": "Zurück",
@ -34,15 +34,22 @@ export default {
"editor.hide_consumer_icons": "Icons unten ausblenden", "editor.hide_consumer_icons": "Icons unten ausblenden",
"editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)",
"editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)",
"editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)",
"editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "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_sensor": "Kombinierter Netz-Sensor (W, Optional)",
"editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.",
"editor.color_picker": "Kreis Farbe", "editor.color_picker": "Bubble",
"editor.pipe_color": "Rohr Farbe", "editor.pipe_color": "Pipe",
"editor.export_color": "Export Farbe", "editor.export_color": "Export",
"editor.consumer_unit_kw": "Sensor meldet in kW", "editor.consumer_unit_kw": "Sensor meldet in kW",
"editor.show_consumer_always": "Verbraucher bei null Watt anzeigen", "editor.show_consumer_always": "Verbraucher bei null Watt anzeigen",
"editor.battery_charge_sensor": "Batterie-Ladung Sensor (W, Optional)",
"editor.battery_discharge_sensor": "Batterie-Entladung Sensor (W, Optional)",
"editor.battery_separate_hint": "Optional: Separate Sensoren für Laden/Entladen. Überschreiben den Hauptsensor für die Berechnung.",
"editor.consumer_1_hide_pipe": "Pipe bei geringer Leistung ausblenden",
"editor.consumer_pipe_threshold": "Pipe-Schwellenwert (Watt)",
"editor.text_color": "Text",
"editor.icon_color": "Icon",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",
@ -50,7 +57,7 @@ export default {
"card.label_battery": "Batterie", "card.label_battery": "Batterie",
"card.label_house": "Verbrauch", "card.label_house": "Verbrauch",
"card.label_car": "E-Auto", "card.label_car": "E-Auto",
"card.label_import": "Import", "card.label_heater": "Heizung",
"card.label_export": "Export", "card.label_pool": "Pool",
} }
}; };

View file

@ -13,7 +13,7 @@ export default {
"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": "Combined Battery Sensor (W)",
"editor.label": "Label", "editor.label": "Label",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Back", "editor.back": "Back",
@ -34,7 +34,7 @@ export default {
"editor.hide_consumer_icons": "Hide Consumer Icons", "editor.hide_consumer_icons": "Hide Consumer Icons",
"editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.invert_consumer_1": "Invert Sensor Value (+/-)",
"editor.secondary_sensor": "Secondary Sensor (display only)", "editor.secondary_sensor": "Secondary Sensor (display only)",
"editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)",
"editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "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_sensor": "Combined Grid Sensor (W, Optional)",
"editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.",
@ -43,6 +43,13 @@ export default {
"editor.export_color": "Export Color", "editor.export_color": "Export Color",
"editor.consumer_unit_kw": "Sensor reports in kW", "editor.consumer_unit_kw": "Sensor reports in kW",
"editor.show_consumer_always": "Show Consumers at zero watts", "editor.show_consumer_always": "Show Consumers at zero watts",
"editor.battery_charge_sensor": "Battery Charge Sensor (W, Optional)",
"editor.battery_discharge_sensor": "Battery Discharge Sensor (W, Optional)",
"editor.battery_separate_hint": "Optional: Separate sensors for charge/discharge. Override the main sensor for calculations.",
"editor.consumer_1_hide_pipe": "Hide pipe at low power",
"editor.consumer_pipe_threshold": "Pipe Threshold (Watts)",
"editor.text_color": "Text Color",
"editor.icon_color": "Icon Color",
}, },
card: { card: {
"card.label_solar": "Solar", "card.label_solar": "Solar",
@ -50,7 +57,7 @@ export default {
"card.label_battery": "Battery", "card.label_battery": "Battery",
"card.label_house": "Consumption", "card.label_house": "Consumption",
"card.label_car": "Car", "card.label_car": "Car",
"card.label_import": "Import", "card.label_heater": "Heater",
"card.label_export": "Export", "card.label_pool": "Pool",
} }
}; };

View file

@ -48,7 +48,7 @@ class PowerFluxCardEditor extends LitElement {
if (!this._config || !this.hass) return; if (!this._config || !this.hass) return;
const target = ev.target; const target = ev.target;
const key = target.configValue || this._currentConfigValue; const key = target.configValue;
let value; let value;
if (target.tagName === 'HA-SWITCH') { if (target.tagName === 'HA-SWITCH') {
@ -67,6 +67,7 @@ class PowerFluxCardEditor extends LitElement {
const entityKeys = [ const entityKeys = [
'solar', 'grid', 'grid_export', 'grid_combined', 'solar', 'grid', 'grid_export', 'grid_combined',
'battery', 'battery_soc', 'grid_to_battery', 'battery', 'battery_soc', 'grid_to_battery',
'battery_charge', 'battery_discharge',
'house', 'house',
'consumer_1', 'consumer_2', 'consumer_3', 'consumer_1', 'consumer_2', 'consumer_3',
'secondary_solar', 'secondary_grid', 'secondary_battery', 'secondary_solar', 'secondary_grid', 'secondary_battery',
@ -162,31 +163,30 @@ class PowerFluxCardEditor extends LitElement {
`; `;
} }
_renderColorPickerDual(bubbleKey, pipeKey, defaultColor) { _renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) {
const bubbleColor = this._config[bubbleKey] || defaultColor; const items = [
const pipeColor = this._config[pipeKey] || defaultColor; { key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor },
const hasBubbleCustom = !!this._config[bubbleKey]; ];
const hasPipeCustom = !!this._config[pipeKey]; if (pipeKey) items.push({ key: pipeKey, label: this._localize('editor.pipe_color'), default: defaultColor });
items.push({ key: textKey, label: this._localize('editor.text_color'), default: defaultColor });
items.push({ key: iconKey, label: this._localize('editor.icon_color'), default: defaultColor });
return html` return html`
<div class="color-picker-dual"> <div class="color-picker-quad">
<div class="color-picker-row"> ${items.map(item => {
<input type="color" const color = this._config[item.key] || item.default;
.value=${bubbleColor} const hasCustom = !!this._config[item.key];
@input=${(e) => this._colorChanged(bubbleKey, e)}> return html`
<span class="color-label">${this._localize('editor.color_picker')}</span> <div class="color-picker-row">
${hasBubbleCustom ? html`<ha-icon class="color-reset-btn" <input type="color"
icon="mdi:refresh" .value=${color}
@click=${() => this._resetColor(bubbleKey)}></ha-icon>` : ''} @input=${(e) => this._colorChanged(item.key, e)}>
</div> <span class="color-label">${item.label}</span>
<div class="color-picker-row"> ${hasCustom ? html`<ha-icon class="color-reset-btn"
<input type="color" icon="mdi:refresh"
.value=${pipeColor} @click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
@input=${(e) => this._colorChanged(pipeKey, e)}> </div>
<span class="color-label">${this._localize('editor.pipe_color')}</span> `;
${hasPipeCustom ? html`<ha-icon class="color-reset-btn" })}
icon="mdi:refresh"
@click=${() => this._resetColor(pipeKey)}></ha-icon>` : ''}
</div>
</div> </div>
`; `;
} }
@ -298,8 +298,8 @@ class PowerFluxCardEditor extends LitElement {
-webkit-appearance: none; -webkit-appearance: none;
border: 2px solid var(--divider-color); border: 2px solid var(--divider-color);
border-radius: 50%; border-radius: 50%;
width: 36px; width: 30px;
height: 36px; height: 30px;
padding: 2px; padding: 2px;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
@ -323,11 +323,11 @@ class PowerFluxCardEditor extends LitElement {
.color-reset-btn:hover { .color-reset-btn:hover {
color: var(--primary-color); color: var(--primary-color);
} }
.color-picker-dual { .color-picker-quad {
display: flex; display: flex;
gap: 8px; gap: 8px;
} }
.color-picker-dual .color-picker-row { .color-picker-quad .color-picker-row {
flex: 1; flex: 1;
} }
`; `;
@ -368,7 +368,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_solar', 'color_pipe_solar', '#ffdd00')} ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
<div class="separator"></div> <div class="separator"></div>
@ -436,7 +436,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_grid', 'color_pipe_grid', '#3b82f6')} ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')}
${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')} ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@ -473,13 +473,14 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))}
${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} <div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;"> <div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.grid_to_battery_hint')} ${this._localize('editor.battery_separate_hint')}
</div> </div>
${this._renderEntitySelector(entitySelectorSchema, entities.battery_charge || "", 'battery_charge', this._localize('editor.battery_charge_sensor'))}
${this._renderEntitySelector(entitySelectorSchema, entities.battery_discharge || "", 'battery_discharge', this._localize('editor.battery_discharge_sensor'))}
<div class="separator"></div> <div class="separator"></div>
<ha-selector <ha-selector
@ -500,9 +501,20 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))}
<div class="separator"></div>
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.grid_to_battery_hint')}
</div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_battery || "", 'secondary_battery', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_battery', 'color_pipe_battery', '#00ff88')} ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
<div class="separator"></div> <div class="separator"></div>
@ -550,6 +562,8 @@ class PowerFluxCardEditor extends LitElement {
<div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;"> <div style="font-size: 0.8em; color: var(--secondary-text-color); margin-top: 4px;">
${this._localize('editor.house_sensor_hint')} ${this._localize('editor.house_sensor_hint')}
</div> </div>
${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -583,6 +597,26 @@ class PowerFluxCardEditor extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px;">
<span>${this._localize('editor.consumer_1_hide_pipe')}</span>
<ha-switch
.checked=${this._config.consumer_1_hide_pipe === true}
.configValue=${'consumer_1_hide_pipe'}
@change=${this._valueChanged}
></ha-switch>
</div>
${this._config.consumer_1_hide_pipe === true ? html`
<ha-selector
.hass=${this.hass}
.selector=${{ number: { min: 0, max: 2000, step: 10, mode: "slider" } }}
.value=${this._config.consumer_1_pipe_threshold !== undefined ? this._config.consumer_1_pipe_threshold : 0}
.configValue=${'consumer_1_pipe_threshold'}
.label=${this._localize('editor.consumer_pipe_threshold')}
@value-changed=${this._valueChanged}
></ha-selector>
` : ''}
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 8px; margin-bottom: 8px;">
<span>${this._localize('editor.consumer_unit_kw')}</span> <span>${this._localize('editor.consumer_unit_kw')}</span>
<ha-switch <ha-switch
@ -594,7 +628,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_1', 'color_pipe_consumer_1', '#a855f7')} ${this._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -630,7 +664,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_2', 'color_pipe_consumer_2', '#f97316')} ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -666,7 +700,7 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))}
${this._renderColorPickerDual('color_consumer_3', 'color_pipe_consumer_3', '#06b6d4')} ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
</div> </div>
`; `;
} }

View file

@ -3,7 +3,7 @@ import lang_en from "./lang-en.js";
import lang_de from "./lang-de.js"; import lang_de from "./lang-de.js";
console.log( console.log(
"%c⚡ Power Flux Card v_2.1 ready", "%c⚡ Power Flux Card v_2.4 ready",
"background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;"
); );
@ -44,6 +44,8 @@ console.log(
consumer_2_unit_kw: false, consumer_2_unit_kw: false,
consumer_3_unit_kw: false, consumer_3_unit_kw: false,
show_consumer_always: false, show_consumer_always: false,
consumer_1_hide_pipe: false,
consumer_1_pipe_threshold: 0,
show_donut_border: false, show_donut_border: false,
show_neon_glow: true, show_neon_glow: true,
show_comet_tail: false, show_comet_tail: false,
@ -66,6 +68,8 @@ console.log(
grid_combined: "", grid_combined: "",
battery: "", battery: "",
battery_soc: "", battery_soc: "",
battery_charge: "",
battery_discharge: "",
house: "", house: "",
consumer_1: "", consumer_1: "",
consumer_2: "", consumer_2: "",
@ -128,6 +132,21 @@ console.log(
'color_pipe_consumer_1': '--pipe-consumer-1-color', 'color_pipe_consumer_1': '--pipe-consumer-1-color',
'color_pipe_consumer_2': '--pipe-consumer-2-color', 'color_pipe_consumer_2': '--pipe-consumer-2-color',
'color_pipe_consumer_3': '--pipe-consumer-3-color', 'color_pipe_consumer_3': '--pipe-consumer-3-color',
'color_house': '--neon-pink',
'color_icon_solar': '--icon-solar-color',
'color_icon_grid': '--icon-grid-color',
'color_icon_battery': '--icon-battery-color',
'color_icon_house': '--icon-house-color',
'color_icon_consumer_1': '--icon-consumer-1-color',
'color_icon_consumer_2': '--icon-consumer-2-color',
'color_icon_consumer_3': '--icon-consumer-3-color',
'color_text_solar': '--text-solar-color',
'color_text_grid': '--text-grid-color',
'color_text_battery': '--text-battery-color',
'color_text_house': '--text-house-color',
'color_text_consumer_1': '--text-consumer-1-color',
'color_text_consumer_2': '--text-consumer-2-color',
'color_text_consumer_3': '--text-consumer-3-color',
}; };
for (const [configKey, cssVar] of Object.entries(colorMap)) { for (const [configKey, cssVar] of Object.entries(colorMap)) {
if (this.config[configKey]) { if (this.config[configKey]) {
@ -155,7 +174,6 @@ console.log(
--neon-green: #00ff88; --neon-green: #00ff88;
--neon-pink: #ff0080; --neon-pink: #ff0080;
--neon-red: #ff3333; --neon-red: #ff3333;
--grid-grey: #9e9e9e;
--export-purple: #a855f7; --export-purple: #a855f7;
--export-color: #ff3333; --export-color: #ff3333;
--consumer-1-color: #a855f7; --consumer-1-color: #a855f7;
@ -167,6 +185,20 @@ console.log(
--pipe-consumer-1-color: var(--consumer-1-color); --pipe-consumer-1-color: var(--consumer-1-color);
--pipe-consumer-2-color: var(--consumer-2-color); --pipe-consumer-2-color: var(--consumer-2-color);
--pipe-consumer-3-color: var(--consumer-3-color); --pipe-consumer-3-color: var(--consumer-3-color);
--icon-solar-color: var(--neon-yellow);
--icon-grid-color: var(--neon-blue);
--icon-battery-color: var(--neon-green);
--icon-house-color: var(--neon-pink);
--icon-consumer-1-color: var(--consumer-1-color);
--icon-consumer-2-color: var(--consumer-2-color);
--icon-consumer-3-color: var(--consumer-3-color);
--text-solar-color: var(--neon-yellow);
--text-grid-color: var(--neon-blue);
--text-battery-color: var(--neon-green);
--text-house-color: var(--neon-pink);
--text-consumer-1-color: var(--consumer-1-color);
--text-consumer-2-color: var(--consumer-2-color);
--text-consumer-3-color: var(--consumer-3-color);
--flow-dasharray: 0 380; --flow-dasharray: 0 380;
} }
:host([data-theme-light]) { :host([data-theme-light]) {
@ -175,7 +207,6 @@ console.log(
--neon-green: #059669; --neon-green: #059669;
--neon-pink: #db2777; --neon-pink: #db2777;
--neon-red: #dc2626; --neon-red: #dc2626;
--grid-grey: #6b7280;
--export-purple: #7c3aed; --export-purple: #7c3aed;
--export-color: #dc2626; --export-color: #dc2626;
--consumer-1-color: #7c3aed; --consumer-1-color: #7c3aed;
@ -256,11 +287,6 @@ console.log(
overflow: hidden; overflow: hidden;
} }
/* Source Colors */
.src-solar { background: var(--neon-yellow); color: black; }
.src-grid { background: var(--neon-blue); color: black; }
.src-battery { background: var(--neon-green); color: black; }
/* --- STANDARD VIEW STYLES --- */ /* --- STANDARD VIEW STYLES --- */
.scale-wrapper { .scale-wrapper {
width: 420px; width: 420px;
@ -394,7 +420,7 @@ console.log(
@keyframes dash { to { stroke-dashoffset: -1500; } } @keyframes dash { to { stroke-dashoffset: -1500; } }
.flow-text { .flow-text {
font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: transition: opacity 0.3s ease; font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease;
} }
.flow-text.no-shadow { filter: none; } .flow-text.no-shadow { filter: none; }
.text-solar { fill: var(--pipe-solar-color); } .text-solar { fill: var(--pipe-solar-color); }
@ -408,35 +434,36 @@ console.log(
_renderIcon(type, val = 0, colorOverride = null) { _renderIcon(type, val = 0, colorOverride = null) {
if (type === 'solar') { if (type === 'solar') {
const animate = Math.round(val) > 0 ? 'spin-slow' : ''; const animate = Math.round(val) > 0 ? 'spin-slow' : '';
const color = colorOverride || 'var(--neon-yellow)'; const color = colorOverride || 'var(--icon-solar-color)';
return html`<svg class="icon-svg ${animate}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`; return html`<svg class="icon-svg ${animate}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>`;
} }
if (type === 'grid') { if (type === 'grid') {
const animate = Math.round(val) > 0 ? 'pulse' : ''; const animate = Math.round(val) > 0 ? 'pulse' : '';
const color = colorOverride || 'var(--neon-blue)'; const color = colorOverride || 'var(--icon-grid-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"></path><path d="M5 8L19 8"></path><path d="M4 14L20 14"></path><path d="M2 22L22 22"></path><circle class="${animate}" cx="12" cy="4" r="4" fill="${color}" stroke="none"></circle></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L12 22"></path><path d="M5 8L19 8"></path><path d="M4 14L20 14"></path><path d="M2 22L22 22"></path><circle class="${animate}" cx="12" cy="4" r="4" fill="${color}" stroke="none"></circle></svg>`;
} }
if (type === 'battery') { if (type === 'battery') {
const soc = Math.min(Math.max(val, 0), 100) / 100; const soc = Math.min(Math.max(val, 0), 100) / 100;
const rectHeight = 14 * soc; const rectHeight = 14 * soc;
const rectY = 18 - rectHeight; const rectY = 18 - rectHeight;
const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; const strokeColor = colorOverride || 'var(--icon-battery-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="var(--neon-green)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="12" height="16" rx="2" ry="2"></rect><line x1="10" y1="2" x2="14" y2="2"></line><rect x="7" y="${rectY}" width="10" height="${rectHeight}" fill="${rectColor}" stroke="none"></rect></svg>`; const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="12" height="16" rx="2" ry="2"></rect><line x1="10" y1="2" x2="14" y2="2"></line><rect x="7" y="${rectY}" width="10" height="${rectHeight}" fill="${rectColor}" stroke="none"></rect></svg>`;
} }
if (type === 'house') { if (type === 'house') {
const strokeColor = colorOverride || 'var(--neon-pink)'; const strokeColor = colorOverride || 'var(--icon-house-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>`;
} }
if (type === 'car') { if (type === 'car') {
const c = colorOverride || 'var(--consumer-1-color)'; const c = colorOverride || 'var(--icon-consumer-1-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path><circle cx="7" cy="17" r="2"></circle><circle cx="17" cy="17" r="2"></circle><path d="M14 17h-5"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"></path><circle cx="7" cy="17" r="2"></circle><circle cx="17" cy="17" r="2"></circle><path d="M14 17h-5"></path></svg>`;
} }
if (type === 'heater') { if (type === 'heater') {
const c = colorOverride || 'var(--consumer-2-color)'; const c = colorOverride || 'var(--icon-consumer-2-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20a4 4 0 0 0 4-4V8a4 4 0 0 0-8 0v8a4 4 0 0 0 4 4z"></path><path class="float" style="animation-delay: 0s;" d="M8 4c0-1.5 1-2 2-2s2 .5 2 2"></path><path class="float" style="animation-delay: 0.5s;" d="M14 4c0-1.5 1-2 2-2s2 .5 2 2"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20a4 4 0 0 0 4-4V8a4 4 0 0 0-8 0v8a4 4 0 0 0 4 4z"></path><path class="float" style="animation-delay: 0s;" d="M8 4c0-1.5 1-2 2-2s2 .5 2 2"></path><path class="float" style="animation-delay: 0.5s;" d="M14 4c0-1.5 1-2 2-2s2 .5 2 2"></path></svg>`;
} }
if (type === 'pool') { if (type === 'pool') {
const c = colorOverride || 'var(--consumer-3-color)'; const c = colorOverride || 'var(--icon-consumer-3-color)';
return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h20"></path><path class="float" d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"></path><path d="M12 2v6"></path><path d="M9 5h6"></path></svg>`; return html`<svg class="icon-svg" viewBox="0 0 24 24" fill="none" stroke="${c}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h20"></path><path class="float" d="M2 16c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2"></path><path d="M12 2v6"></path><path d="M9 5h6"></path></svg>`;
} }
return html``; return html``;
@ -546,8 +573,12 @@ console.log(
gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; gridExport = gridMain < 0 ? Math.abs(gridMain) : 0;
} }
const batteryCharge = battery > 0 ? battery : 0; // Check for separate battery charge/discharge sensors
const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== "");
const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== "");
const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0);
const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0);
let solarToBatt = 0; let solarToBatt = 0;
let gridToBatt = 0; let gridToBatt = 0;
@ -641,9 +672,9 @@ console.log(
const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); const path = this._createBracketPath(s.startPx, s.widthPx, 'down');
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; }
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; }
return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
@ -661,10 +692,10 @@ console.log(
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -707,14 +738,19 @@ console.log(
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => html` ${barSegments.map(s => {
const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)'
: s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)'
: s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)'
: (s.color === 'var(--export-purple)' ? 'white' : 'black');
return html`
<div class="bar-segment" <div class="bar-segment"
style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};" style="width: ${s.widthPct}%; background: ${s.color}; color: ${textColor}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}" title="${this._formatPower(s.val)}"
@click=${() => s.entityId && this._handleClick(s.entityId)}> @click=${() => s.entityId && this._handleClick(s.entityId)}>
${s.widthPx > 35 ? this._formatPower(s.val) : ''} ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
</div> </div>
`)} `})}
</div> </div>
<!-- BOTTOM BRACKETS --> <!-- BOTTOM BRACKETS -->
@ -784,13 +820,7 @@ console.log(
const val = parseFloat(state.state); const val = parseFloat(state.state);
if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : '');
const unit = state.attributes.unit_of_measurement || ''; const unit = state.attributes.unit_of_measurement || '';
if (unit === 'W' || unit === 'Wh') { if (unit === 'W') { return this._formatPower(val); } if (unit === 'Wh') { if (Math.abs(val) >= 1000) return (val / 1000).toFixed(2) + ' kWh'; return Math.round(val) + ' Wh'; } if (unit === 'kWh' || unit === 'kW') { return val.toFixed(1) + ' ' + unit; } if (unit.includes('EUR') || unit.includes('ct') || unit.includes('€')) { return val.toFixed(2) + ' ' + unit; } return val.toFixed(1) + (unit ? ' ' + unit : '');
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 // Determine existence of main entities
@ -808,9 +838,9 @@ console.log(
const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow';
// Custom Labels for Consumers // Custom Labels for Consumers
const labelC1 = this.config.consumer_1_label || "E-Auto"; const labelC1 = this.config.consumer_1_label || this._localize('card.label_car');
const labelC2 = this.config.consumer_2_label || "Heizung"; const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater');
const labelC3 = this.config.consumer_3_label || "Pool"; const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool');
const getVal = (entity) => { const getVal = (entity) => {
const state = this.hass.states[entity]; const state = this.hass.states[entity];
@ -832,6 +862,11 @@ console.log(
const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0));
const anyBottomVisible = showC1 || showC2 || showC3; const anyBottomVisible = showC1 || showC2 || showC3;
// Consumer 1 pipe threshold
const hideC1Pipe = this.config.consumer_1_hide_pipe === true;
const c1PipeThreshold = this.config.consumer_1_pipe_threshold || 0;
const c1PipeActive = showC1 && (!hideC1Pipe || c1Val >= c1PipeThreshold);
const solar = hasSolar ? getVal(entities.solar) : 0; const solar = hasSolar ? getVal(entities.solar) : 0;
const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0;
const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0);
@ -861,8 +896,12 @@ console.log(
} }
} }
const batteryCharge = battery > 0 ? battery : 0; // Check for separate battery charge/discharge sensors
const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== "");
const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== "");
const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0);
const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0);
let solarToBatt = 0; let solarToBatt = 0;
let gridToBatt = 0; let gridToBatt = 0;
@ -889,6 +928,9 @@ console.log(
const gridToHouse = Math.max(0, gridImport - gridToBatt); const gridToHouse = Math.max(0, gridImport - gridToBatt);
const house = solarToHouse + gridToHouse + batteryDischarge; const house = solarToHouse + gridToHouse + batteryDischarge;
// Use house entity for display if defined, otherwise use calculated value
const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house;
const isTopArcActive = (solarToBatt > 0); const isTopArcActive = (solarToBatt > 0);
const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50;
let baseHeight = anyBottomVisible ? 480 : 340; let baseHeight = anyBottomVisible ? 480 : 340;
@ -990,8 +1032,10 @@ console.log(
} }
} }
const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; const solarColor = isSolarActive ? 'var(--icon-solar-color)' : 'var(--secondary-text-color)';
const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); const gridColor = isGridExporting ? 'var(--export-color)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)');
const gridIconColor = (isGridActive && this.config.color_icon_grid) ? 'var(--icon-grid-color)' : gridColor;
const gridTextColor = (isGridActive && this.config.color_text_grid) ? 'var(--text-grid-color)' : gridColor;
const getAnimStyle = (val) => { const getAnimStyle = (val) => {
if (val <= 1) return "opacity: 0;"; if (val <= 1) return "opacity: 0;";
@ -1071,27 +1115,24 @@ console.log(
const renderMainIcon = (type, val, customIcon, color = null) => { const renderMainIcon = (type, val, customIcon, color = null) => {
if (customIcon) { 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);' : ''))); const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : ''))));
return html`<ha-icon icon="${customIcon}" class="icon-custom" style="${style}"></ha-icon>`; return html`<ha-icon icon="${customIcon}" class="icon-custom" style="${style}"></ha-icon>`;
} }
return this._renderIcon(type, val, color); return this._renderIcon(type, val, color);
}; };
const getCustomClass = (icon) => icon ? 'has-custom-icon' : '';
const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => {
if (!isVisible) return html``; if (!isVisible) return html``;
const customIcon = this.config[`${configKey}_icon`]; const customIcon = this.config[`${configKey}_icon`];
let iconContent; let iconContent;
const isCustom = !hideConsumerIcons && !!customIcon; const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`;
const dynamicClass = isCustom ? 'has-custom-icon' : '';
if (hideConsumerIcons) { if (hideConsumerIcons) {
iconContent = html``; iconContent = html``;
} else if (customIcon) { } else if (customIcon) {
iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${hexColor};"></ha-icon>`; iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${iconColorVar};"></ha-icon>`;
} else { } else {
iconContent = this._renderIcon(iconType, val); iconContent = this._renderIcon(iconType, val);
} }
@ -1099,12 +1140,16 @@ console.log(
const secEntity = entities[`secondary_${configKey}`]; const secEntity = entities[`secondary_${configKey}`];
const hasSecondary = !!(secEntity && secEntity !== ""); const hasSecondary = !!(secEntity && secEntity !== "");
const textStyle = this.config[`color_text_${configKey}`]
? `color: var(--text-${configKey.replace(/_/g, '-')}-color);`
: getConsumerColorStyle(hexColor);
return html` return html`
<div class="bubble ${cssClass} node ${cssClass.replace('c', 'node-c')} ${tintClass} ${dynamicClass} ${glowClass}" <div class="bubble ${cssClass} ${cssClass.replace('c', 'node-c')} ${tintClass} ${glowClass}"
@click=${() => this._handleClick(entities[configKey])}> @click=${() => this._handleClick(entities[configKey])}>
${iconContent} ${iconContent}
${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)}
<div class="value" style="${getConsumerColorStyle(hexColor)}">${this._formatPower(val)}</div> <div class="value" style="${textStyle}">${this._formatPower(val)}</div>
</div> </div>
`; `;
}; };
@ -1134,7 +1179,9 @@ console.log(
const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC2 = "M 210 310 L 210 370";
const pathHouseC3 = "M 255 265 Q 370 265 370 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370";
const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; const houseTextStyle = this.config.color_text_house
? 'color: var(--text-house-color);'
: (houseTextCol ? `color: ${houseTextCol};` : '');
const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380');
const strokeWidthVal = showDashedLine ? 4 : 8; const strokeWidthVal = showDashedLine ? 4 : 8;
@ -1155,7 +1202,7 @@ console.log(
<path class="bg-path bg-battery" d="${pathBattHouse}" style="${getPipeStyle(batteryDischarge)} ${styleBattery}" /> <path class="bg-path bg-battery" d="${pathBattHouse}" style="${getPipeStyle(batteryDischarge)} ${styleBattery}" />
<path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerPipeColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(showC1, c1Val)}" /> <path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerPipeColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(c1PipeActive, c1Val)}" />
<path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" /> <path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" />
<path d="${pathHouseC3}" fill="none" stroke="${this._getConsumerPipeColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" /> <path d="${pathHouseC3}" fill="none" stroke="${this._getConsumerPipeColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" />
@ -1168,7 +1215,7 @@ console.log(
<path class="flow-line flow-battery" d="${pathBattHouse}" style="${getAnimStyle(batteryDischarge)} ${styleBattery}" /> <path class="flow-line flow-battery" d="${pathBattHouse}" style="${getAnimStyle(batteryDischarge)} ${styleBattery}" />
<path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerPipeColor(1)}" style="${getConsumerAnimStyle(showC1, c1Val)}" /> <path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerPipeColor(1)}" style="${getConsumerAnimStyle(c1PipeActive, c1Val)}" />
<path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" /> <path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" />
<path class="flow-line" d="${pathHouseC3}" stroke="${this._getConsumerPipeColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" /> <path class="flow-line" d="${pathHouseC3}" stroke="${this._getConsumerPipeColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" />
@ -1184,39 +1231,39 @@ console.log(
</svg> </svg>
${hasSolar ? html` ${hasSolar ? html`
<div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node node-solar ${tintClass} ${isSolarActive ? glowClass : ''} ${getCustomClass(iconSolar)}" <div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node-solar ${tintClass} ${isSolarActive ? glowClass : ''}"
@click=${() => this._handleClick(entities.solar)}> @click=${() => this._handleClick(entities.solar)}>
${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderMainIcon('solar', solarVal, iconSolar, solarColor)}
${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)}
<div class="value" style="${isSolarActive ? getColorStyle('--neon-yellow') : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div> <div class="value" style="${isSolarActive ? (this.config.color_text_solar ? 'color: var(--text-solar-color);' : getColorStyle('--neon-yellow')) : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div>
</div>` : ''} </div>` : ''}
${hasGrid ? html` ${hasGrid ? html`
<div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node node-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''} ${getCustomClass(iconGrid)}" <div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''}"
style="${showDonut && isGridActive ? `--grid-gradient: ${gridGradientVal};` : ''}" style="${showDonut && isGridActive ? `--grid-gradient: ${gridGradientVal};` : ''}"
@click=${() => this._handleClick(entities.grid_combined || entities.grid)}> @click=${() => this._handleClick(entities.grid_combined || entities.grid)}>
${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)}
${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)}
<div class="value" style="color: ${gridColor};"> <div class="value" style="color: ${gridTextColor};">
${isGridExporting ? html`<span class="direction-arrow">&#9650;</span>` : (isGridActive ? html`<span class="direction-arrow">&#9660;</span>` : '')} ${isGridExporting ? html`<span class="direction-arrow">&#9650;</span>` : (isGridActive ? html`<span class="direction-arrow">&#9660;</span>` : '')}
${this._formatPower(isGridExporting ? gridExport : gridImport)} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
</div> </div>
</div>` : ''} </div>` : ''}
${hasBattery ? html` ${hasBattery ? html`
<div class="bubble battery node node-battery ${tintClass} ${glowClass} ${getCustomClass(iconBattery)}" <div class="bubble battery node-battery ${tintClass} ${glowClass}"
@click=${() => this._handleClick(entities.battery)}> @click=${() => this._handleClick(entities.battery)}>
${renderMainIcon('battery', battSoc, iconBattery)} ${renderMainIcon('battery', battSoc, iconBattery)}
${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)}
<div class="value" style="${getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div> <div class="value" style="${this.config.color_text_battery ? 'color: var(--text-battery-color);' : getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div>
</div>` : ''} </div>` : ''}
<div class="bubble house node node-house ${showDonut ? 'donut' : ''} ${tintClass}" <div class="bubble house node-house ${showDonut ? 'donut' : ''} ${tintClass}"
style="${houseBubbleStyle}" style="${houseBubbleStyle}"
@click=${() => this._handleClick(entities.house)}> @click=${() => this._handleClick(entities.house)}>
${renderMainIcon('house', 0, null, houseDominantColor)} ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)}
${renderLabel(labelHouseText, showLabelHouse)} ${renderLabel(labelHouseText, showLabelHouse)}
<div class="value" style="${houseTextStyle}">${this._formatPower(house)}</div> <div class="value" style="${houseTextStyle}">${this._formatPower(houseDisplay)}</div>
</div> </div>
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))}