Compare commits

..

No commits in common. "main" and "v_2.2" have entirely different histories.
main ... v_2.2

8 changed files with 247 additions and 1038 deletions

1
.github/FUNDING.yml vendored
View file

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

148
README.md
View file

@ -84,151 +84,3 @@ 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,14 +18,14 @@ 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": "Kombinierter Batterie Sensor (W)", "editor.entity": "Entität (Watt)",
"editor.label": "Beschriftung", "editor.label": "Beschriftung",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Zurück", "editor.back": "Zurück",
"editor.battery_soc_label": "Ladestand (%)", "editor.battery_soc_label": "Ladestand (%)",
"editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)",
"editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)",
"editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).",
"editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_1_title": "🚗 Links (Lila)",
"editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_2_title": "♨️ Mitte (Orange)",
"editor.consumer_3_title": "🏊 Rechts (Türkis)", "editor.consumer_3_title": "🏊 Rechts (Türkis)",
@ -39,22 +39,11 @@ 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 (W, Optional)", "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)",
"editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_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 die getrennten Import/Export Sensoren.",
"editor.color_picker": "Bubble", "editor.color_picker": "Farbe anpassen",
"editor.pipe_color": "Pipe",
"editor.export_color": "Export",
"editor.consumer_unit_kw": "Sensor meldet in kW",
"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",
@ -62,8 +51,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_heater": "Heizung", "card.label_import": "Import",
"card.label_pool": "Pool", "card.label_export": "Export",
} }
}; };
const lang_en = { const lang_en = {
@ -81,14 +70,14 @@ 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": "Combined Battery Sensor (W)", "editor.entity": "Entity (Watt)",
"editor.label": "Label", "editor.label": "Label",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Back", "editor.back": "Back",
"editor.battery_soc_label": "State of Charge (%)", "editor.battery_soc_label": "State of Charge (%)",
"editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_total_title": "🏠 Total Consumption (optional)",
"editor.house_sensor_label": "Sensor for House Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)",
"editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).",
"editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_1_title": "🚗 Left (Purple)",
"editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_2_title": "♨️ Center (Orange)",
"editor.consumer_3_title": "🏊 Right (Cyan)", "editor.consumer_3_title": "🏊 Right (Cyan)",
@ -102,22 +91,11 @@ 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 (W, optional)", "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)",
"editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_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 separate import/export sensors.",
"editor.color_picker": "Bubble Color", "editor.color_picker": "Custom Color",
"editor.pipe_color": "Pipe Color",
"editor.export_color": "Export Color",
"editor.consumer_unit_kw": "Sensor reports in kW",
"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",
@ -125,8 +103,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_heater": "Heater", "card.label_import": "Import",
"card.label_pool": "Pool", "card.label_export": "Export",
} }
}; };
@ -186,7 +164,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; const key = target.configValue || this._currentConfigValue;
let value; let value;
if (target.tagName === 'HA-SWITCH') { if (target.tagName === 'HA-SWITCH') {
@ -205,7 +183,6 @@ 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',
@ -301,34 +278,6 @@ class PowerFluxCardEditor extends LitElement {
`; `;
} }
_renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) {
const items = [
{ key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor },
];
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`
<div class="color-picker-row">
<input type="color"
.value=${color}
@input=${(e) => this._colorChanged(item.key, e)}>
<span class="color-label">${item.label}</span>
${hasCustom ? html`<ha-icon class="color-reset-btn"
icon="mdi:refresh"
@click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
</div>
`;
})}
</div>
`;
}
static get styles() { static get styles() {
return css` return css`
.card-config { .card-config {
@ -436,8 +385,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: 30px; width: 36px;
height: 30px; height: 36px;
padding: 2px; padding: 2px;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
@ -461,13 +410,6 @@ class PowerFluxCardEditor extends LitElement {
.color-reset-btn:hover { .color-reset-btn:hover {
color: var(--primary-color); color: var(--primary-color);
} }
.color-picker-quad {
display: flex;
gap: 8px;
}
.color-picker-quad .color-picker-row {
flex: 1;
}
`; `;
} }
@ -506,7 +448,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._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')} ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')}
<div class="separator"></div> <div class="separator"></div>
@ -540,13 +482,11 @@ class PowerFluxCardEditor extends LitElement {
</div> </div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))}
<div class="separator"></div>
<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_combined_hint')} ${this._localize('editor.grid_combined_hint')}
</div> </div>
<div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")}
@ -574,9 +514,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._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')}
${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
<div class="separator"></div> <div class="separator"></div>
@ -611,13 +549,12 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))}
<div class="separator"></div> ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))}
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
<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.battery_separate_hint')} ${this._localize('editor.grid_to_battery_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>
@ -639,20 +576,9 @@ 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._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')} ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')}
<div class="separator"></div> <div class="separator"></div>
@ -700,8 +626,6 @@ 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">
@ -735,38 +659,9 @@ 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;">
<span>${this._localize('editor.consumer_unit_kw')}</span>
<ha-switch
.checked=${this._config.consumer_1_unit_kw === true}
.configValue=${'consumer_1_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')} ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -791,18 +686,9 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<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>
<ha-switch
.checked=${this._config.consumer_2_unit_kw === true}
.configValue=${'consumer_2_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')} ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -827,18 +713,9 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<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>
<ha-switch
.checked=${this._config.consumer_3_unit_kw === true}
.configValue=${'consumer_3_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')} ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')}
</div> </div>
`; `;
} }
@ -972,15 +849,6 @@ class PowerFluxCardEditor extends LitElement {
<div class="switch-label">${this._localize('editor.hide_inactive')}</div> <div class="switch-label">${this._localize('editor.hide_inactive')}</div>
</div> </div>
<div class="switch-row">
<ha-switch
.checked=${this._config.show_consumer_always === true}
.configValue=${'show_consumer_always'}
@change=${this._valueChanged}
></ha-switch>
<div class="switch-label">${this._localize('editor.show_consumer_always')}</div>
</div>
<div class="switch-row"> <div class="switch-row">
<ha-switch <ha-switch
.checked=${this._config.compact_view === true} .checked=${this._config.compact_view === true}
@ -1003,7 +871,7 @@ customElements.define("power-flux-card-editor", PowerFluxCardEditor);
console.log( console.log(
"%c⚡ Power Flux Card v_2.4 ready", "%c⚡ Power Flux Card v_2.1 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;"
); );
@ -1040,12 +908,6 @@ console.log(
return { return {
zoom: 0.9, zoom: 0.9,
compact_view: false, compact_view: false,
consumer_1_unit_kw: false,
consumer_2_unit_kw: false,
consumer_3_unit_kw: 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,
@ -1068,8 +930,6 @@ 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: "",
@ -1122,31 +982,9 @@ console.log(
'color_solar': '--neon-yellow', 'color_solar': '--neon-yellow',
'color_grid': '--neon-blue', 'color_grid': '--neon-blue',
'color_battery': '--neon-green', 'color_battery': '--neon-green',
'color_export': '--export-color',
'color_consumer_1': '--consumer-1-color', 'color_consumer_1': '--consumer-1-color',
'color_consumer_2': '--consumer-2-color', 'color_consumer_2': '--consumer-2-color',
'color_consumer_3': '--consumer-3-color', 'color_consumer_3': '--consumer-3-color',
'color_pipe_solar': '--pipe-solar-color',
'color_pipe_grid': '--pipe-grid-color',
'color_pipe_battery': '--pipe-battery-color',
'color_pipe_consumer_1': '--pipe-consumer-1-color',
'color_pipe_consumer_2': '--pipe-consumer-2-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]) {
@ -1174,31 +1012,11 @@ 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;
--consumer-1-color: #a855f7; --consumer-1-color: #a855f7;
--consumer-2-color: #f97316; --consumer-2-color: #f97316;
--consumer-3-color: #06b6d4; --consumer-3-color: #06b6d4;
--pipe-solar-color: var(--neon-yellow);
--pipe-grid-color: var(--neon-blue);
--pipe-battery-color: var(--neon-green);
--pipe-consumer-1-color: var(--consumer-1-color);
--pipe-consumer-2-color: var(--consumer-2-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]) {
@ -1207,8 +1025,8 @@ 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;
--consumer-1-color: #7c3aed; --consumer-1-color: #7c3aed;
--consumer-2-color: #ea580c; --consumer-2-color: #ea580c;
--consumer-3-color: #0891b2; --consumer-3-color: #0891b2;
@ -1287,6 +1105,11 @@ 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;
@ -1317,8 +1140,8 @@ console.log(
.bubble.tinted { background: rgba(255, 255, 255, 0.05); } .bubble.tinted { background: rgba(255, 255, 255, 0.05); }
.bubble.tinted.solar { background: color-mix(in srgb, var(--neon-yellow), transparent 85%); } .bubble.tinted.solar { background: color-mix(in srgb, var(--neon-yellow), transparent 85%); }
.bubble.tinted.grid { background: color-mix(in srgb, var(--neon-blue), transparent 85%); } .bubble.tinted.grid { background: color-mix(in srgb, var(--neon-blue), transparent 85%); }
.bubble.tinted.grid.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); } .bubble.tinted.grid.exporting { background: color-mix(in srgb, var(--neon-red), transparent 85%); }
.bubble.grid.exporting { border-color: var(--export-color); } .bubble.grid.exporting { border-color: var(--neon-red); }
.bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); } .bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); }
.bubble.tinted.c1 { background: color-mix(in srgb, var(--consumer-1-color), transparent 85%); } .bubble.tinted.c1 { background: color-mix(in srgb, var(--consumer-1-color), transparent 85%); }
.bubble.tinted.c2 { background: color-mix(in srgb, var(--consumer-2-color), transparent 85%); } .bubble.tinted.c2 { background: color-mix(in srgb, var(--consumer-2-color), transparent 85%); }
@ -1335,16 +1158,6 @@ console.log(
-webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none; -webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none;
} }
.bubble.grid.donut { border: none !important; background: transparent; }
.bubble.grid.donut.tinted { background: color-mix(in srgb, var(--neon-blue), transparent 85%); }
.bubble.grid.donut.tinted.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); }
.bubble.grid.donut::before {
content: ""; position: absolute; inset: 0; border-radius: 50%; padding: 4px;
background: var(--grid-gradient, var(--neon-blue));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none;
}
.icon-svg, .icon-custom { .icon-svg, .icon-custom {
width: 33px; height: 33px; position: absolute; top: 10px; left: 50%; margin-left: -17px; z-index: 2; display: block; width: 33px; height: 33px; position: absolute; top: 10px; left: 50%; margin-left: -17px; z-index: 2; display: block;
} }
@ -1384,7 +1197,6 @@ console.log(
.glow.solar { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-yellow), transparent 60%); } .glow.solar { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-yellow), transparent 60%); }
.glow.battery { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-green), transparent 60%); } .glow.battery { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-green), transparent 60%); }
.glow.grid { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-blue), transparent 60%); } .glow.grid { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-blue), transparent 60%); }
.glow.grid.exporting { box-shadow: 0 0 15px color-mix(in srgb, var(--export-color), transparent 60%); }
.glow.c1 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-1-color), transparent 60%); } .glow.c1 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-1-color), transparent 60%); }
.glow.c2 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-2-color), transparent 60%); } .glow.c2 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-2-color), transparent 60%); }
.glow.c3 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-3-color), transparent 60%); } .glow.c3 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-3-color), transparent 60%); }
@ -1400,33 +1212,33 @@ console.log(
svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; } .bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; }
.bg-solar { stroke: var(--pipe-solar-color); } .bg-solar { stroke: var(--neon-yellow); }
.bg-grid { stroke: var(--pipe-grid-color); } .bg-grid { stroke: var(--neon-blue); }
.bg-battery { stroke: var(--pipe-battery-color); } .bg-battery { stroke: var(--neon-green); }
.bg-export { stroke: var(--export-color); } .bg-export { stroke: var(--neon-red); }
.bg-c1 { stroke: var(--pipe-consumer-1-color); } .bg-c1 { stroke: var(--consumer-1-color); }
.bg-c2 { stroke: var(--pipe-consumer-2-color); } .bg-c2 { stroke: var(--consumer-2-color); }
.bg-c3 { stroke: var(--pipe-consumer-3-color); } .bg-c3 { stroke: var(--consumer-3-color); }
.flow-line { .flow-line {
fill: none; stroke-width: var(--flow-stroke-width, 8px); stroke-linecap: round; stroke-dasharray: var(--flow-dasharray); fill: none; stroke-width: var(--flow-stroke-width, 8px); stroke-linecap: round; stroke-dasharray: var(--flow-dasharray);
animation: dash linear infinite; opacity: 0; transition: opacity 0.5s; animation: dash linear infinite; opacity: 0; transition: opacity 0.5s;
} }
.flow-solar { stroke: var(--pipe-solar-color); } .flow-solar { stroke: var(--neon-yellow); }
.flow-grid { stroke: var(--pipe-grid-color); } .flow-grid { stroke: var(--neon-blue); }
.flow-battery { stroke: var(--pipe-battery-color); } .flow-battery { stroke: var(--neon-green); }
.flow-export { stroke: var(--export-color); } .flow-export { stroke: var(--neon-red); }
@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; transition: opacity 0.3s ease; font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: 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(--neon-yellow); }
.text-grid { fill: var(--pipe-grid-color); } .text-grid { fill: var(--neon-blue); }
.text-export { fill: var(--export-color); } .text-export { fill: var(--neon-red); }
.text-battery { fill: var(--pipe-battery-color); } .text-battery { fill: var(--neon-green); }
`; `;
} }
@ -1434,36 +1246,35 @@ 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(--icon-solar-color)'; const color = colorOverride || 'var(--neon-yellow)';
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(--icon-grid-color)'; const color = colorOverride || 'var(--neon-blue)';
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 strokeColor = colorOverride || 'var(--icon-battery-color)'; const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)';
const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; 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>`;
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(--icon-house-color)'; const strokeColor = colorOverride || 'var(--neon-pink)';
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(--icon-consumer-1-color)'; const c = colorOverride || 'var(--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(--icon-consumer-2-color)'; const c = colorOverride || 'var(--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(--icon-consumer-3-color)'; const c = colorOverride || 'var(--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``;
@ -1482,11 +1293,6 @@ console.log(
return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1]; return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1];
} }
_getConsumerPipeColor(index) {
const style = getComputedStyle(this);
return style.getPropertyValue(`--pipe-consumer-${index}-color`).trim() || this._getConsumerColor(index);
}
// --- DOM NODE SVG GENERATOR --- // --- DOM NODE SVG GENERATOR ---
_renderSVGPath(d, color) { _renderSVGPath(d, color) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@ -1540,9 +1346,6 @@ console.log(
const state = this.hass.states[entity]; const state = this.hass.states[entity];
return state ? parseFloat(state.state) || 0 : 0; return state ? parseFloat(state.state) || 0 : 0;
}; };
const getValKw = (entity, isKw) => {
return getVal(entity) * (isKw ? 1000 : 1);
};
const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0;
const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== "");
@ -1553,7 +1356,7 @@ console.log(
if (this.config.invert_battery) { if (this.config.invert_battery) {
battery *= -1; battery *= -1;
} }
let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value
if (this.config.invert_consumer_1) { c1Val *= -1; } if (this.config.invert_consumer_1) { c1Val *= -1; }
c1Val = Math.abs(c1Val); c1Val = Math.abs(c1Val);
@ -1573,12 +1376,8 @@ console.log(
gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; gridExport = gridMain < 0 ? Math.abs(gridMain) : 0;
} }
// Check for separate battery charge/discharge sensors const batteryCharge = battery > 0 ? battery : 0;
const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); const batteryDischarge = battery < 0 ? Math.abs(battery) : 0;
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;
@ -1672,9 +1471,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(--icon-solar-color)'; } if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; }
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; }
return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
@ -1692,10 +1491,10 @@ console.log(
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -1738,19 +1537,14 @@ console.log(
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => { ${barSegments.map(s => html`
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: ${textColor}; cursor: ${s.entityId ? 'pointer' : 'default'};" style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}" 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 -->
@ -1820,7 +1614,13 @@ 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') { 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 : ''); if (unit === 'W' || unit === 'Wh') {
return this._formatPower(val);
}
if (unit === 'kWh' || unit === 'kW') {
return val.toFixed(1) + ' ' + unit;
}
return val.toFixed(1) + (unit ? ' ' + unit : '');
}; };
// Determine existence of main entities // Determine existence of main entities
@ -1838,35 +1638,26 @@ 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 || this._localize('card.label_car'); const labelC1 = this.config.consumer_1_label || "E-Auto";
const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); const labelC2 = this.config.consumer_2_label || "Heizung";
const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const labelC3 = this.config.consumer_3_label || "Pool";
const getVal = (entity) => { const getVal = (entity) => {
const state = this.hass.states[entity]; const state = this.hass.states[entity];
return state ? parseFloat(state.state) || 0 : 0; return state ? parseFloat(state.state) || 0 : 0;
}; };
const getValKw = (entity, isKw) => {
return getVal(entity) * (isKw ? 1000 : 1);
};
let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0;
if (this.config.invert_consumer_1) { c1Val *= -1; } if (this.config.invert_consumer_1) { c1Val *= -1; }
c1Val = Math.abs(c1Val); c1Val = Math.abs(c1Val);
const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0;
const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0;
const alwaysShowConsumer = this.config.show_consumer_always === true; const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0);
const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0);
const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); const showC3 = (entities.consumer_3 && 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);
@ -1896,12 +1687,8 @@ console.log(
} }
} }
// Check for separate battery charge/discharge sensors const batteryCharge = battery > 0 ? battery : 0;
const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); const batteryDischarge = battery < 0 ? Math.abs(battery) : 0;
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;
@ -1928,9 +1715,6 @@ 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;
@ -2012,30 +1796,8 @@ console.log(
const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0;
const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0;
// --- Grid Donut Gradient --- const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)';
let gridGradientVal = ''; const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)');
if (showDonut && hasGrid && isGridActive) {
const gridTotal = gridToHouse + gridToBatt + gridExport;
if (gridTotal > 0) {
const gPctToHouse = (gridToHouse / gridTotal) * 100;
const gPctToBatt = (gridToBatt / gridTotal) * 100;
const gPctExport = (gridExport / gridTotal) * 100;
let gStops = [];
let gCurrent = 0;
if (gPctToHouse > 0) { gStops.push(`var(--neon-blue) ${gCurrent}% ${gCurrent + gPctToHouse}%`); gCurrent += gPctToHouse; }
if (gPctToBatt > 0) { gStops.push(`var(--neon-green) ${gCurrent}% ${gCurrent + gPctToBatt}%`); gCurrent += gPctToBatt; }
if (gPctExport > 0) { gStops.push(`var(--export-color) ${gCurrent}% ${gCurrent + gPctExport}%`); gCurrent += gPctExport; }
if (gCurrent < 99.9) { gStops.push(`var(--neon-blue) ${gCurrent}% 100%`); }
gridGradientVal = `conic-gradient(from 330deg, ${gStops.join(', ')})`;
} else {
gridGradientVal = isGridExporting ? 'var(--export-color)' : 'var(--neon-blue)';
}
}
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 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;";
@ -2115,24 +1877,27 @@ 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(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : '')));
return html`<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 iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; const isCustom = !hideConsumerIcons && !!customIcon;
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: ${iconColorVar};"></ha-icon>`; iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${hexColor};"></ha-icon>`;
} else { } else {
iconContent = this._renderIcon(iconType, val); iconContent = this._renderIcon(iconType, val);
} }
@ -2140,16 +1905,12 @@ 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} ${cssClass.replace('c', 'node-c')} ${tintClass} ${glowClass}" <div class="bubble ${cssClass} node ${cssClass.replace('c', 'node-c')} ${tintClass} ${dynamicClass} ${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="${textStyle}">${this._formatPower(val)}</div> <div class="value" style="${getConsumerColorStyle(hexColor)}">${this._formatPower(val)}</div>
</div> </div>
`; `;
}; };
@ -2168,20 +1929,13 @@ console.log(
const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathSolarBatt = "M 50 70 Q 210 -20 370 70";
const pathGridImport = "M 210 160 L 210 220"; const pathGridImport = "M 210 160 L 210 220";
const pathGridExport = "M 95 115 Q 130 145 165 115"; const pathGridExport = "M 95 115 Q 130 145 165 115";
const pathHouseExport = "M 210 220 L 210 160";
const exportFromSolar = solarVal > 1;
const activeExportPath = exportFromSolar ? pathGridExport : pathHouseExport;
const exportTextX = exportFromSolar ? '130' : '185';
const exportTextY = exportFromSolar ? '145' : '195';
const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathGridToBatt = "M 255 115 Q 290 145 325 115";
const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathBattHouse = "M 370 160 Q 370 265 255 265";
const pathHouseC1 = "M 165 265 Q 50 265 50 370"; const pathHouseC1 = "M 165 265 Q 50 265 50 370";
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 = this.config.color_text_house const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : '';
? '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;
@ -2197,33 +1951,33 @@ console.log(
<path class="bg-path bg-solar" d="${pathSolarBatt}" style="${getPipeStyle(solarToBatt)} ${styleSolarBatt}" /> <path class="bg-path bg-solar" d="${pathSolarBatt}" style="${getPipeStyle(solarToBatt)} ${styleSolarBatt}" />
<path class="bg-path bg-grid" d="${pathGridImport}" style="${getPipeStyle(gridToHouse)} ${styleGrid}" /> <path class="bg-path bg-grid" d="${pathGridImport}" style="${getPipeStyle(gridToHouse)} ${styleGrid}" />
<path class="bg-path bg-export" d="${activeExportPath}" style="${getPipeStyle(gridExport)} ${styleGrid}" /> <path class="bg-path bg-export" d="${pathGridExport}" style="${getPipeStyle(gridExport)} ${styleGrid}" />
<path class="bg-path bg-grid" d="${pathGridToBatt}" style="${getPipeStyle(gridToBatt)} ${styleGridBatt}" /> <path class="bg-path bg-grid" d="${pathGridToBatt}" style="${getPipeStyle(gridToBatt)} ${styleGridBatt}" />
<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(c1PipeActive, c1Val)}" /> <path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(showC1, c1Val)}" />
<path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" /> <path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerColor(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._getConsumerColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" />
<path class="flow-line flow-solar" d="${pathSolarHouse}" style="${getAnimStyle(solarToHouse)} ${styleSolar}" /> <path class="flow-line flow-solar" d="${pathSolarHouse}" style="${getAnimStyle(solarToHouse)} ${styleSolar}" />
<path class="flow-line flow-solar" d="${pathSolarBatt}" style="${getAnimStyle(solarToBatt)} ${styleSolarBatt}" /> <path class="flow-line flow-solar" d="${pathSolarBatt}" style="${getAnimStyle(solarToBatt)} ${styleSolarBatt}" />
<path class="flow-line flow-grid" d="${pathGridImport}" style="${getAnimStyle(gridToHouse)} ${styleGrid}" /> <path class="flow-line flow-grid" d="${pathGridImport}" style="${getAnimStyle(gridToHouse)} ${styleGrid}" />
<path class="flow-line flow-export" d="${activeExportPath}" style="${getAnimStyle(gridExport)} ${styleGrid}" /> <path class="flow-line flow-export" d="${pathGridExport}" style="${getAnimStyle(gridExport)} ${styleGrid}" />
<path class="flow-line flow-grid" d="${pathGridToBatt}" style="${getAnimStyle(gridToBatt)} ${styleGridBatt}" /> <path class="flow-line flow-grid" d="${pathGridToBatt}" style="${getAnimStyle(gridToBatt)} ${styleGridBatt}" />
<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(c1PipeActive, c1Val)}" /> <path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerColor(1)}" style="${getConsumerAnimStyle(showC1, c1Val)}" />
<path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" /> <path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerColor(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._getConsumerColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" />
<text x="100" y="235" class="${textClass} text-solar" style="${getTextStyle(solarToHouse, 'solar')} ${styleSolar}">${this._formatPower(solarToHouse)}</text> <text x="100" y="235" class="${textClass} text-solar" style="${getTextStyle(solarToHouse, 'solar')} ${styleSolar}">${this._formatPower(solarToHouse)}</text>
<text x="210" y="45" class="${textClass} text-solar" style="${getTextStyle(solarToBatt, 'solar')} ${styleSolarBatt}">${this._formatPower(solarToBatt)}</text> <text x="210" y="45" class="${textClass} text-solar" style="${getTextStyle(solarToBatt, 'solar')} ${styleSolarBatt}">${this._formatPower(solarToBatt)}</text>
<text x="235" y="195" class="${textClass} text-grid" style="${getTextStyle(gridToHouse, 'grid')} ${styleGrid}">${this._formatPower(gridToHouse)}</text> <text x="235" y="195" class="${textClass} text-grid" style="${getTextStyle(gridToHouse, 'grid')} ${styleGrid}">${this._formatPower(gridToHouse)}</text>
<text x="${exportTextX}" y="${exportTextY}" class="${textClass} text-export" style="${getTextStyle(gridExport, 'grid')} ${styleGrid}">${this._formatPower(gridExport)}</text> <text x="130" y="145" class="${textClass} text-export" style="${getTextStyle(gridExport, 'grid')} ${styleGrid}">${this._formatPower(gridExport)}</text>
<text x="290" y="145" class="${textClass} text-grid" style="${getTextStyle(gridToBatt, 'grid')} ${styleGridBatt}">${this._formatPower(gridToBatt)}</text> <text x="290" y="145" class="${textClass} text-grid" style="${getTextStyle(gridToBatt, 'grid')} ${styleGridBatt}">${this._formatPower(gridToBatt)}</text>
<text x="320" y="235" class="${textClass} text-battery" style="${getTextStyle(batteryDischarge, 'battery')} ${styleBattery}">${this._formatPower(batteryDischarge)}</text> <text x="320" y="235" class="${textClass} text-battery" style="${getTextStyle(batteryDischarge, 'battery')} ${styleBattery}">${this._formatPower(batteryDischarge)}</text>
@ -2231,39 +1985,38 @@ console.log(
</svg> </svg>
${hasSolar ? html` ${hasSolar ? html`
<div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node-solar ${tintClass} ${isSolarActive ? glowClass : ''}" <div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node node-solar ${tintClass} ${isSolarActive ? glowClass : ''} ${getCustomClass(iconSolar)}"
@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 ? (this.config.color_text_solar ? 'color: var(--text-solar-color);' : getColorStyle('--neon-yellow')) : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div> <div class="value" style="${isSolarActive ? 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-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''}" <div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node node-grid ${tintClass} ${isGridActive ? glowClass : ''} ${getCustomClass(iconGrid)}"
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, gridIconColor)} ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)}
${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)}
<div class="value" style="color: ${gridTextColor};"> <div class="value" style="color: ${gridColor};">
${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-battery ${tintClass} ${glowClass}" <div class="bubble battery node node-battery ${tintClass} ${glowClass} ${getCustomClass(iconBattery)}"
@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="${this.config.color_text_battery ? 'color: var(--text-battery-color);' : getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div> <div class="value" style="${getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div>
</div>` : ''} </div>` : ''}
<div class="bubble house node-house ${showDonut ? 'donut' : ''} ${tintClass}" <div class="bubble house node node-house ${showDonut ? 'donut' : ''} ${tintClass}"
style="${houseBubbleStyle}" style="${houseBubbleStyle}"
@click=${() => this._handleClick(entities.house)}> @click=${() => this._handleClick(entities.house)}>
${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderMainIcon('house', 0, null, houseDominantColor)}
${renderLabel(labelHouseText, showLabelHouse)} ${renderLabel(labelHouseText, showLabelHouse)}
<div class="value" style="${houseTextStyle}">${this._formatPower(houseDisplay)}</div> <div class="value" style="${houseTextStyle}">${this._formatPower(house)}</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,151 +86,3 @@ 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,14 +13,14 @@ 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": "Kombinierter Batterie Sensor (W)", "editor.entity": "Entität (Watt)",
"editor.label": "Beschriftung", "editor.label": "Beschriftung",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Zurück", "editor.back": "Zurück",
"editor.battery_soc_label": "Ladestand (%)", "editor.battery_soc_label": "Ladestand (%)",
"editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)",
"editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)",
"editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).",
"editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_1_title": "🚗 Links (Lila)",
"editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_2_title": "♨️ Mitte (Orange)",
"editor.consumer_3_title": "🏊 Rechts (Türkis)", "editor.consumer_3_title": "🏊 Rechts (Türkis)",
@ -34,22 +34,11 @@ 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 (W, Optional)", "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)",
"editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_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 die getrennten Import/Export Sensoren.",
"editor.color_picker": "Bubble", "editor.color_picker": "Farbe anpassen",
"editor.pipe_color": "Pipe",
"editor.export_color": "Export",
"editor.consumer_unit_kw": "Sensor meldet in kW",
"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",
@ -57,7 +46,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_heater": "Heizung", "card.label_import": "Import",
"card.label_pool": "Pool", "card.label_export": "Export",
} }
}; };

View file

@ -13,14 +13,14 @@ 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": "Combined Battery Sensor (W)", "editor.entity": "Entity (Watt)",
"editor.label": "Label", "editor.label": "Label",
"editor.icon": "Icon", "editor.icon": "Icon",
"editor.back": "Back", "editor.back": "Back",
"editor.battery_soc_label": "State of Charge (%)", "editor.battery_soc_label": "State of Charge (%)",
"editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_total_title": "🏠 Total Consumption (optional)",
"editor.house_sensor_label": "Sensor for House Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)",
"editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).",
"editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_1_title": "🚗 Left (Purple)",
"editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_2_title": "♨️ Center (Orange)",
"editor.consumer_3_title": "🏊 Right (Cyan)", "editor.consumer_3_title": "🏊 Right (Cyan)",
@ -34,22 +34,11 @@ 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 (W, optional)", "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)",
"editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_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 separate import/export sensors.",
"editor.color_picker": "Bubble Color", "editor.color_picker": "Custom Color",
"editor.pipe_color": "Pipe Color",
"editor.export_color": "Export Color",
"editor.consumer_unit_kw": "Sensor reports in kW",
"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",
@ -57,7 +46,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_heater": "Heater", "card.label_import": "Import",
"card.label_pool": "Pool", "card.label_export": "Export",
} }
}; };

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; const key = target.configValue || this._currentConfigValue;
let value; let value;
if (target.tagName === 'HA-SWITCH') { if (target.tagName === 'HA-SWITCH') {
@ -67,7 +67,6 @@ 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',
@ -163,34 +162,6 @@ class PowerFluxCardEditor extends LitElement {
`; `;
} }
_renderColorPickerQuad(bubbleKey, pipeKey, textKey, iconKey, defaultColor) {
const items = [
{ key: bubbleKey, label: this._localize('editor.color_picker'), default: defaultColor },
];
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`
<div class="color-picker-row">
<input type="color"
.value=${color}
@input=${(e) => this._colorChanged(item.key, e)}>
<span class="color-label">${item.label}</span>
${hasCustom ? html`<ha-icon class="color-reset-btn"
icon="mdi:refresh"
@click=${() => this._resetColor(item.key)}></ha-icon>` : ''}
</div>
`;
})}
</div>
`;
}
static get styles() { static get styles() {
return css` return css`
.card-config { .card-config {
@ -298,8 +269,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: 30px; width: 36px;
height: 30px; height: 36px;
padding: 2px; padding: 2px;
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
@ -323,13 +294,6 @@ class PowerFluxCardEditor extends LitElement {
.color-reset-btn:hover { .color-reset-btn:hover {
color: var(--primary-color); color: var(--primary-color);
} }
.color-picker-quad {
display: flex;
gap: 8px;
}
.color-picker-quad .color-picker-row {
flex: 1;
}
`; `;
} }
@ -368,7 +332,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._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')} ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')}
<div class="separator"></div> <div class="separator"></div>
@ -402,13 +366,11 @@ class PowerFluxCardEditor extends LitElement {
</div> </div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))}
<div class="separator"></div>
<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_combined_hint')} ${this._localize('editor.grid_combined_hint')}
</div> </div>
<div class="separator"></div>
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")}
@ -436,9 +398,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._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')}
${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
<div class="separator"></div> <div class="separator"></div>
@ -473,13 +433,12 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))}
<div class="separator"></div> ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))}
${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
<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.battery_separate_hint')} ${this._localize('editor.grid_to_battery_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>
@ -501,20 +460,9 @@ 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._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')} ${this._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')}
<div class="separator"></div> <div class="separator"></div>
@ -562,8 +510,6 @@ 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">
@ -597,38 +543,9 @@ 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;">
<span>${this._localize('editor.consumer_unit_kw')}</span>
<ha-switch
.checked=${this._config.consumer_1_unit_kw === true}
.configValue=${'consumer_1_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')} ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -653,18 +570,9 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<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>
<ha-switch
.checked=${this._config.consumer_2_unit_kw === true}
.configValue=${'consumer_2_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')} ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')}
</div> </div>
<div class="consumer-group"> <div class="consumer-group">
@ -689,18 +597,9 @@ class PowerFluxCardEditor extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-selector> ></ha-selector>
<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>
<ha-switch
.checked=${this._config.consumer_3_unit_kw === true}
.configValue=${'consumer_3_unit_kw'}
@change=${this._valueChanged}
></ha-switch>
</div>
${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._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')} ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')}
</div> </div>
`; `;
} }
@ -834,15 +733,6 @@ class PowerFluxCardEditor extends LitElement {
<div class="switch-label">${this._localize('editor.hide_inactive')}</div> <div class="switch-label">${this._localize('editor.hide_inactive')}</div>
</div> </div>
<div class="switch-row">
<ha-switch
.checked=${this._config.show_consumer_always === true}
.configValue=${'show_consumer_always'}
@change=${this._valueChanged}
></ha-switch>
<div class="switch-label">${this._localize('editor.show_consumer_always')}</div>
</div>
<div class="switch-row"> <div class="switch-row">
<ha-switch <ha-switch
.checked=${this._config.compact_view === true} .checked=${this._config.compact_view === true}

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.4 ready", "%c⚡ Power Flux Card v_2.1 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;"
); );
@ -40,12 +40,6 @@ console.log(
return { return {
zoom: 0.9, zoom: 0.9,
compact_view: false, compact_view: false,
consumer_1_unit_kw: false,
consumer_2_unit_kw: false,
consumer_3_unit_kw: 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,
@ -68,8 +62,6 @@ 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: "",
@ -122,31 +114,9 @@ console.log(
'color_solar': '--neon-yellow', 'color_solar': '--neon-yellow',
'color_grid': '--neon-blue', 'color_grid': '--neon-blue',
'color_battery': '--neon-green', 'color_battery': '--neon-green',
'color_export': '--export-color',
'color_consumer_1': '--consumer-1-color', 'color_consumer_1': '--consumer-1-color',
'color_consumer_2': '--consumer-2-color', 'color_consumer_2': '--consumer-2-color',
'color_consumer_3': '--consumer-3-color', 'color_consumer_3': '--consumer-3-color',
'color_pipe_solar': '--pipe-solar-color',
'color_pipe_grid': '--pipe-grid-color',
'color_pipe_battery': '--pipe-battery-color',
'color_pipe_consumer_1': '--pipe-consumer-1-color',
'color_pipe_consumer_2': '--pipe-consumer-2-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]) {
@ -174,31 +144,11 @@ 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;
--consumer-1-color: #a855f7; --consumer-1-color: #a855f7;
--consumer-2-color: #f97316; --consumer-2-color: #f97316;
--consumer-3-color: #06b6d4; --consumer-3-color: #06b6d4;
--pipe-solar-color: var(--neon-yellow);
--pipe-grid-color: var(--neon-blue);
--pipe-battery-color: var(--neon-green);
--pipe-consumer-1-color: var(--consumer-1-color);
--pipe-consumer-2-color: var(--consumer-2-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]) {
@ -207,8 +157,8 @@ 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;
--consumer-1-color: #7c3aed; --consumer-1-color: #7c3aed;
--consumer-2-color: #ea580c; --consumer-2-color: #ea580c;
--consumer-3-color: #0891b2; --consumer-3-color: #0891b2;
@ -287,6 +237,11 @@ 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;
@ -317,8 +272,8 @@ console.log(
.bubble.tinted { background: rgba(255, 255, 255, 0.05); } .bubble.tinted { background: rgba(255, 255, 255, 0.05); }
.bubble.tinted.solar { background: color-mix(in srgb, var(--neon-yellow), transparent 85%); } .bubble.tinted.solar { background: color-mix(in srgb, var(--neon-yellow), transparent 85%); }
.bubble.tinted.grid { background: color-mix(in srgb, var(--neon-blue), transparent 85%); } .bubble.tinted.grid { background: color-mix(in srgb, var(--neon-blue), transparent 85%); }
.bubble.tinted.grid.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); } .bubble.tinted.grid.exporting { background: color-mix(in srgb, var(--neon-red), transparent 85%); }
.bubble.grid.exporting { border-color: var(--export-color); } .bubble.grid.exporting { border-color: var(--neon-red); }
.bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); } .bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); }
.bubble.tinted.c1 { background: color-mix(in srgb, var(--consumer-1-color), transparent 85%); } .bubble.tinted.c1 { background: color-mix(in srgb, var(--consumer-1-color), transparent 85%); }
.bubble.tinted.c2 { background: color-mix(in srgb, var(--consumer-2-color), transparent 85%); } .bubble.tinted.c2 { background: color-mix(in srgb, var(--consumer-2-color), transparent 85%); }
@ -335,16 +290,6 @@ console.log(
-webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none; -webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none;
} }
.bubble.grid.donut { border: none !important; background: transparent; }
.bubble.grid.donut.tinted { background: color-mix(in srgb, var(--neon-blue), transparent 85%); }
.bubble.grid.donut.tinted.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); }
.bubble.grid.donut::before {
content: ""; position: absolute; inset: 0; border-radius: 50%; padding: 4px;
background: var(--grid-gradient, var(--neon-blue));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor; mask-composite: exclude; z-index: -1; pointer-events: none;
}
.icon-svg, .icon-custom { .icon-svg, .icon-custom {
width: 33px; height: 33px; position: absolute; top: 10px; left: 50%; margin-left: -17px; z-index: 2; display: block; width: 33px; height: 33px; position: absolute; top: 10px; left: 50%; margin-left: -17px; z-index: 2; display: block;
} }
@ -384,7 +329,6 @@ console.log(
.glow.solar { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-yellow), transparent 60%); } .glow.solar { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-yellow), transparent 60%); }
.glow.battery { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-green), transparent 60%); } .glow.battery { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-green), transparent 60%); }
.glow.grid { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-blue), transparent 60%); } .glow.grid { box-shadow: 0 0 15px color-mix(in srgb, var(--neon-blue), transparent 60%); }
.glow.grid.exporting { box-shadow: 0 0 15px color-mix(in srgb, var(--export-color), transparent 60%); }
.glow.c1 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-1-color), transparent 60%); } .glow.c1 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-1-color), transparent 60%); }
.glow.c2 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-2-color), transparent 60%); } .glow.c2 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-2-color), transparent 60%); }
.glow.c3 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-3-color), transparent 60%); } .glow.c3 { box-shadow: 0 0 15px color-mix(in srgb, var(--consumer-3-color), transparent 60%); }
@ -400,33 +344,33 @@ console.log(
svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; } svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; } .bg-path { fill: none; stroke-width: 6; transition: opacity 0.3s ease; }
.bg-solar { stroke: var(--pipe-solar-color); } .bg-solar { stroke: var(--neon-yellow); }
.bg-grid { stroke: var(--pipe-grid-color); } .bg-grid { stroke: var(--neon-blue); }
.bg-battery { stroke: var(--pipe-battery-color); } .bg-battery { stroke: var(--neon-green); }
.bg-export { stroke: var(--export-color); } .bg-export { stroke: var(--neon-red); }
.bg-c1 { stroke: var(--pipe-consumer-1-color); } .bg-c1 { stroke: var(--consumer-1-color); }
.bg-c2 { stroke: var(--pipe-consumer-2-color); } .bg-c2 { stroke: var(--consumer-2-color); }
.bg-c3 { stroke: var(--pipe-consumer-3-color); } .bg-c3 { stroke: var(--consumer-3-color); }
.flow-line { .flow-line {
fill: none; stroke-width: var(--flow-stroke-width, 8px); stroke-linecap: round; stroke-dasharray: var(--flow-dasharray); fill: none; stroke-width: var(--flow-stroke-width, 8px); stroke-linecap: round; stroke-dasharray: var(--flow-dasharray);
animation: dash linear infinite; opacity: 0; transition: opacity 0.5s; animation: dash linear infinite; opacity: 0; transition: opacity 0.5s;
} }
.flow-solar { stroke: var(--pipe-solar-color); } .flow-solar { stroke: var(--neon-yellow); }
.flow-grid { stroke: var(--pipe-grid-color); } .flow-grid { stroke: var(--neon-blue); }
.flow-battery { stroke: var(--pipe-battery-color); } .flow-battery { stroke: var(--neon-green); }
.flow-export { stroke: var(--export-color); } .flow-export { stroke: var(--neon-red); }
@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; transition: opacity 0.3s ease; font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: 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(--neon-yellow); }
.text-grid { fill: var(--pipe-grid-color); } .text-grid { fill: var(--neon-blue); }
.text-export { fill: var(--export-color); } .text-export { fill: var(--neon-red); }
.text-battery { fill: var(--pipe-battery-color); } .text-battery { fill: var(--neon-green); }
`; `;
} }
@ -434,36 +378,35 @@ 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(--icon-solar-color)'; const color = colorOverride || 'var(--neon-yellow)';
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(--icon-grid-color)'; const color = colorOverride || 'var(--neon-blue)';
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 strokeColor = colorOverride || 'var(--icon-battery-color)'; const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)';
const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; 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>`;
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(--icon-house-color)'; const strokeColor = colorOverride || 'var(--neon-pink)';
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(--icon-consumer-1-color)'; const c = colorOverride || 'var(--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(--icon-consumer-2-color)'; const c = colorOverride || 'var(--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(--icon-consumer-3-color)'; const c = colorOverride || 'var(--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``;
@ -482,11 +425,6 @@ console.log(
return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1]; return style.getPropertyValue(`--consumer-${index}-color`).trim() || ['#a855f7', '#f97316', '#06b6d4'][index - 1];
} }
_getConsumerPipeColor(index) {
const style = getComputedStyle(this);
return style.getPropertyValue(`--pipe-consumer-${index}-color`).trim() || this._getConsumerColor(index);
}
// --- DOM NODE SVG GENERATOR --- // --- DOM NODE SVG GENERATOR ---
_renderSVGPath(d, color) { _renderSVGPath(d, color) {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
@ -540,9 +478,6 @@ console.log(
const state = this.hass.states[entity]; const state = this.hass.states[entity];
return state ? parseFloat(state.state) || 0 : 0; return state ? parseFloat(state.state) || 0 : 0;
}; };
const getValKw = (entity, isKw) => {
return getVal(entity) * (isKw ? 1000 : 1);
};
const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0; const solar = entities.solar ? Math.max(0, getVal(entities.solar)) : 0;
const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== "");
@ -553,7 +488,7 @@ console.log(
if (this.config.invert_battery) { if (this.config.invert_battery) {
battery *= -1; battery *= -1;
} }
let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value
if (this.config.invert_consumer_1) { c1Val *= -1; } if (this.config.invert_consumer_1) { c1Val *= -1; }
c1Val = Math.abs(c1Val); c1Val = Math.abs(c1Val);
@ -573,12 +508,8 @@ console.log(
gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; gridExport = gridMain < 0 ? Math.abs(gridMain) : 0;
} }
// Check for separate battery charge/discharge sensors const batteryCharge = battery > 0 ? battery : 0;
const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); const batteryDischarge = battery < 0 ? Math.abs(battery) : 0;
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;
@ -672,9 +603,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(--icon-solar-color)'; } if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; }
if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; }
if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; }
return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId };
}); });
@ -692,10 +623,10 @@ console.log(
let icon = ''; let icon = '';
let iconColor = ''; let iconColor = '';
if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; }
if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); }
if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; }
if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; }
const path = this._createBracketPath(bottomX, width, 'up'); const path = this._createBracketPath(bottomX, width, 'up');
bottomBrackets.push({ bottomBrackets.push({
@ -738,19 +669,14 @@ console.log(
<!-- MAIN BAR --> <!-- MAIN BAR -->
<div class="compact-bar-wrapper"> <div class="compact-bar-wrapper">
${barSegments.map(s => { ${barSegments.map(s => html`
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: ${textColor}; cursor: ${s.entityId ? 'pointer' : 'default'};" style="width: ${s.widthPct}%; background: ${s.color}; color: ${s.color === 'var(--export-purple)' ? 'white' : 'black'}; cursor: ${s.entityId ? 'pointer' : 'default'};"
title="${this._formatPower(s.val)}" 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 -->
@ -820,7 +746,13 @@ 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') { 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 : ''); if (unit === 'W' || unit === 'Wh') {
return this._formatPower(val);
}
if (unit === 'kWh' || unit === 'kW') {
return val.toFixed(1) + ' ' + unit;
}
return val.toFixed(1) + (unit ? ' ' + unit : '');
}; };
// Determine existence of main entities // Determine existence of main entities
@ -838,35 +770,26 @@ 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 || this._localize('card.label_car'); const labelC1 = this.config.consumer_1_label || "E-Auto";
const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); const labelC2 = this.config.consumer_2_label || "Heizung";
const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const labelC3 = this.config.consumer_3_label || "Pool";
const getVal = (entity) => { const getVal = (entity) => {
const state = this.hass.states[entity]; const state = this.hass.states[entity];
return state ? parseFloat(state.state) || 0 : 0; return state ? parseFloat(state.state) || 0 : 0;
}; };
const getValKw = (entity, isKw) => {
return getVal(entity) * (isKw ? 1000 : 1);
};
let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0;
if (this.config.invert_consumer_1) { c1Val *= -1; } if (this.config.invert_consumer_1) { c1Val *= -1; }
c1Val = Math.abs(c1Val); c1Val = Math.abs(c1Val);
const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0;
const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0;
const alwaysShowConsumer = this.config.show_consumer_always === true; const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0);
const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0);
const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); const showC3 = (entities.consumer_3 && 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);
@ -896,12 +819,8 @@ console.log(
} }
} }
// Check for separate battery charge/discharge sensors const batteryCharge = battery > 0 ? battery : 0;
const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); const batteryDischarge = battery < 0 ? Math.abs(battery) : 0;
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;
@ -928,9 +847,6 @@ 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;
@ -1012,30 +928,8 @@ console.log(
const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0;
const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0;
// --- Grid Donut Gradient --- const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)';
let gridGradientVal = ''; const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)');
if (showDonut && hasGrid && isGridActive) {
const gridTotal = gridToHouse + gridToBatt + gridExport;
if (gridTotal > 0) {
const gPctToHouse = (gridToHouse / gridTotal) * 100;
const gPctToBatt = (gridToBatt / gridTotal) * 100;
const gPctExport = (gridExport / gridTotal) * 100;
let gStops = [];
let gCurrent = 0;
if (gPctToHouse > 0) { gStops.push(`var(--neon-blue) ${gCurrent}% ${gCurrent + gPctToHouse}%`); gCurrent += gPctToHouse; }
if (gPctToBatt > 0) { gStops.push(`var(--neon-green) ${gCurrent}% ${gCurrent + gPctToBatt}%`); gCurrent += gPctToBatt; }
if (gPctExport > 0) { gStops.push(`var(--export-color) ${gCurrent}% ${gCurrent + gPctExport}%`); gCurrent += gPctExport; }
if (gCurrent < 99.9) { gStops.push(`var(--neon-blue) ${gCurrent}% 100%`); }
gridGradientVal = `conic-gradient(from 330deg, ${gStops.join(', ')})`;
} else {
gridGradientVal = isGridExporting ? 'var(--export-color)' : 'var(--neon-blue)';
}
}
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 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;";
@ -1115,24 +1009,27 @@ 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(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : '')));
return html`<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 iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; const isCustom = !hideConsumerIcons && !!customIcon;
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: ${iconColorVar};"></ha-icon>`; iconContent = html`<ha-icon icon="${customIcon}" class="icon-custom" style="color: ${hexColor};"></ha-icon>`;
} else { } else {
iconContent = this._renderIcon(iconType, val); iconContent = this._renderIcon(iconType, val);
} }
@ -1140,16 +1037,12 @@ 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} ${cssClass.replace('c', 'node-c')} ${tintClass} ${glowClass}" <div class="bubble ${cssClass} node ${cssClass.replace('c', 'node-c')} ${tintClass} ${dynamicClass} ${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="${textStyle}">${this._formatPower(val)}</div> <div class="value" style="${getConsumerColorStyle(hexColor)}">${this._formatPower(val)}</div>
</div> </div>
`; `;
}; };
@ -1168,20 +1061,13 @@ console.log(
const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathSolarBatt = "M 50 70 Q 210 -20 370 70";
const pathGridImport = "M 210 160 L 210 220"; const pathGridImport = "M 210 160 L 210 220";
const pathGridExport = "M 95 115 Q 130 145 165 115"; const pathGridExport = "M 95 115 Q 130 145 165 115";
const pathHouseExport = "M 210 220 L 210 160";
const exportFromSolar = solarVal > 1;
const activeExportPath = exportFromSolar ? pathGridExport : pathHouseExport;
const exportTextX = exportFromSolar ? '130' : '185';
const exportTextY = exportFromSolar ? '145' : '195';
const pathGridToBatt = "M 255 115 Q 290 145 325 115"; const pathGridToBatt = "M 255 115 Q 290 145 325 115";
const pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathBattHouse = "M 370 160 Q 370 265 255 265";
const pathHouseC1 = "M 165 265 Q 50 265 50 370"; const pathHouseC1 = "M 165 265 Q 50 265 50 370";
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 = this.config.color_text_house const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : '';
? '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;
@ -1197,33 +1083,33 @@ console.log(
<path class="bg-path bg-solar" d="${pathSolarBatt}" style="${getPipeStyle(solarToBatt)} ${styleSolarBatt}" /> <path class="bg-path bg-solar" d="${pathSolarBatt}" style="${getPipeStyle(solarToBatt)} ${styleSolarBatt}" />
<path class="bg-path bg-grid" d="${pathGridImport}" style="${getPipeStyle(gridToHouse)} ${styleGrid}" /> <path class="bg-path bg-grid" d="${pathGridImport}" style="${getPipeStyle(gridToHouse)} ${styleGrid}" />
<path class="bg-path bg-export" d="${activeExportPath}" style="${getPipeStyle(gridExport)} ${styleGrid}" /> <path class="bg-path bg-export" d="${pathGridExport}" style="${getPipeStyle(gridExport)} ${styleGrid}" />
<path class="bg-path bg-grid" d="${pathGridToBatt}" style="${getPipeStyle(gridToBatt)} ${styleGridBatt}" /> <path class="bg-path bg-grid" d="${pathGridToBatt}" style="${getPipeStyle(gridToBatt)} ${styleGridBatt}" />
<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(c1PipeActive, c1Val)}" /> <path d="${pathHouseC1}" fill="none" stroke="${this._getConsumerColor(1)}" stroke-width="6" style="${getConsumerPipeStyle(showC1, c1Val)}" />
<path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerPipeColor(2)}" stroke-width="6" style="${getConsumerPipeStyle(showC2, c2Val)}" /> <path d="${pathHouseC2}" fill="none" stroke="${this._getConsumerColor(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._getConsumerColor(3)}" stroke-width="6" style="${getConsumerPipeStyle(showC3, c3Val)}" />
<path class="flow-line flow-solar" d="${pathSolarHouse}" style="${getAnimStyle(solarToHouse)} ${styleSolar}" /> <path class="flow-line flow-solar" d="${pathSolarHouse}" style="${getAnimStyle(solarToHouse)} ${styleSolar}" />
<path class="flow-line flow-solar" d="${pathSolarBatt}" style="${getAnimStyle(solarToBatt)} ${styleSolarBatt}" /> <path class="flow-line flow-solar" d="${pathSolarBatt}" style="${getAnimStyle(solarToBatt)} ${styleSolarBatt}" />
<path class="flow-line flow-grid" d="${pathGridImport}" style="${getAnimStyle(gridToHouse)} ${styleGrid}" /> <path class="flow-line flow-grid" d="${pathGridImport}" style="${getAnimStyle(gridToHouse)} ${styleGrid}" />
<path class="flow-line flow-export" d="${activeExportPath}" style="${getAnimStyle(gridExport)} ${styleGrid}" /> <path class="flow-line flow-export" d="${pathGridExport}" style="${getAnimStyle(gridExport)} ${styleGrid}" />
<path class="flow-line flow-grid" d="${pathGridToBatt}" style="${getAnimStyle(gridToBatt)} ${styleGridBatt}" /> <path class="flow-line flow-grid" d="${pathGridToBatt}" style="${getAnimStyle(gridToBatt)} ${styleGridBatt}" />
<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(c1PipeActive, c1Val)}" /> <path class="flow-line" d="${pathHouseC1}" stroke="${this._getConsumerColor(1)}" style="${getConsumerAnimStyle(showC1, c1Val)}" />
<path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerPipeColor(2)}" style="${getConsumerAnimStyle(showC2, c2Val)}" /> <path class="flow-line" d="${pathHouseC2}" stroke="${this._getConsumerColor(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._getConsumerColor(3)}" style="${getConsumerAnimStyle(showC3, c3Val)}" />
<text x="100" y="235" class="${textClass} text-solar" style="${getTextStyle(solarToHouse, 'solar')} ${styleSolar}">${this._formatPower(solarToHouse)}</text> <text x="100" y="235" class="${textClass} text-solar" style="${getTextStyle(solarToHouse, 'solar')} ${styleSolar}">${this._formatPower(solarToHouse)}</text>
<text x="210" y="45" class="${textClass} text-solar" style="${getTextStyle(solarToBatt, 'solar')} ${styleSolarBatt}">${this._formatPower(solarToBatt)}</text> <text x="210" y="45" class="${textClass} text-solar" style="${getTextStyle(solarToBatt, 'solar')} ${styleSolarBatt}">${this._formatPower(solarToBatt)}</text>
<text x="235" y="195" class="${textClass} text-grid" style="${getTextStyle(gridToHouse, 'grid')} ${styleGrid}">${this._formatPower(gridToHouse)}</text> <text x="235" y="195" class="${textClass} text-grid" style="${getTextStyle(gridToHouse, 'grid')} ${styleGrid}">${this._formatPower(gridToHouse)}</text>
<text x="${exportTextX}" y="${exportTextY}" class="${textClass} text-export" style="${getTextStyle(gridExport, 'grid')} ${styleGrid}">${this._formatPower(gridExport)}</text> <text x="130" y="145" class="${textClass} text-export" style="${getTextStyle(gridExport, 'grid')} ${styleGrid}">${this._formatPower(gridExport)}</text>
<text x="290" y="145" class="${textClass} text-grid" style="${getTextStyle(gridToBatt, 'grid')} ${styleGridBatt}">${this._formatPower(gridToBatt)}</text> <text x="290" y="145" class="${textClass} text-grid" style="${getTextStyle(gridToBatt, 'grid')} ${styleGridBatt}">${this._formatPower(gridToBatt)}</text>
<text x="320" y="235" class="${textClass} text-battery" style="${getTextStyle(batteryDischarge, 'battery')} ${styleBattery}">${this._formatPower(batteryDischarge)}</text> <text x="320" y="235" class="${textClass} text-battery" style="${getTextStyle(batteryDischarge, 'battery')} ${styleBattery}">${this._formatPower(batteryDischarge)}</text>
@ -1231,39 +1117,38 @@ console.log(
</svg> </svg>
${hasSolar ? html` ${hasSolar ? html`
<div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node-solar ${tintClass} ${isSolarActive ? glowClass : ''}" <div class="bubble ${isSolarActive ? 'solar' : 'inactive'} node node-solar ${tintClass} ${isSolarActive ? glowClass : ''} ${getCustomClass(iconSolar)}"
@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 ? (this.config.color_text_solar ? 'color: var(--text-solar-color);' : getColorStyle('--neon-yellow')) : `color: ${solarColor};`}">${this._formatPower(solarVal)}</div> <div class="value" style="${isSolarActive ? 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-grid ${showDonut && isGridActive ? 'donut' : ''} ${tintClass} ${isGridActive ? glowClass : ''}" <div class="bubble ${isGridActive ? (isGridExporting ? 'grid exporting' : 'grid') : 'inactive'} node node-grid ${tintClass} ${isGridActive ? glowClass : ''} ${getCustomClass(iconGrid)}"
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, gridIconColor)} ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)}
${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)}
<div class="value" style="color: ${gridTextColor};"> <div class="value" style="color: ${gridColor};">
${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-battery ${tintClass} ${glowClass}" <div class="bubble battery node node-battery ${tintClass} ${glowClass} ${getCustomClass(iconBattery)}"
@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="${this.config.color_text_battery ? 'color: var(--text-battery-color);' : getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div> <div class="value" style="${getColorStyle('--neon-green')}">${Math.round(battSoc)}%</div>
</div>` : ''} </div>` : ''}
<div class="bubble house node-house ${showDonut ? 'donut' : ''} ${tintClass}" <div class="bubble house node node-house ${showDonut ? 'donut' : ''} ${tintClass}"
style="${houseBubbleStyle}" style="${houseBubbleStyle}"
@click=${() => this._handleClick(entities.house)}> @click=${() => this._handleClick(entities.house)}>
${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderMainIcon('house', 0, null, houseDominantColor)}
${renderLabel(labelHouseText, showLabelHouse)} ${renderLabel(labelHouseText, showLabelHouse)}
<div class="value" style="${houseTextStyle}">${this._formatPower(houseDisplay)}</div> <div class="value" style="${houseTextStyle}">${this._formatPower(house)}</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))}