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"]

148
README.md
View file

@ -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`
<div class="color-picker-quad">
${items.map(item => {
const color = this._config[item.key] || item.default;
const hasCustom = !!this._config[item.key];
return html` return html`
<div class="color-picker-dual">
<div class="color-picker-row"> <div class="color-picker-row">
<input type="color" <input type="color"
.value=${bubbleColor} .value=${color}
@input=${(e) => this._colorChanged(bubbleKey, e)}> @input=${(e) => this._colorChanged(item.key, e)}>
<span class="color-label">${this._localize('editor.color_picker')}</span> <span class="color-label">${item.label}</span>
${hasBubbleCustom ? html`<ha-icon class="color-reset-btn" ${hasCustom ? html`<ha-icon class="color-reset-btn"
icon="mdi:refresh" icon="mdi:refresh"
@click=${() => this._resetColor(bubbleKey)}></ha-icon>` : ''} @click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
</div>
<div class="color-picker-row">
<input type="color"
.value=${pipeColor}
@input=${(e) => this._colorChanged(pipeKey, e)}>
<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> </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,12 +611,13 @@ 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>
@ -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`
<div class="color-picker-quad">
${items.map(item => {
const color = this._config[item.key] || item.default;
const hasCustom = !!this._config[item.key];
return html` return html`
<div class="color-picker-dual">
<div class="color-picker-row"> <div class="color-picker-row">
<input type="color" <input type="color"
.value=${bubbleColor} .value=${color}
@input=${(e) => this._colorChanged(bubbleKey, e)}> @input=${(e) => this._colorChanged(item.key, e)}>
<span class="color-label">${this._localize('editor.color_picker')}</span> <span class="color-label">${item.label}</span>
${hasBubbleCustom ? html`<ha-icon class="color-reset-btn" ${hasCustom ? html`<ha-icon class="color-reset-btn"
icon="mdi:refresh" icon="mdi:refresh"
@click=${() => this._resetColor(bubbleKey)}></ha-icon>` : ''} @click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
</div>
<div class="color-picker-row">
<input type="color"
.value=${pipeColor}
@input=${(e) => this._colorChanged(pipeKey, e)}>
<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> </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,12 +473,13 @@ 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>
@ -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))}