diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24336fc..128e009 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ --- +ko_fi: jayjojayson custom: ["https://www.paypal.me/quadFlyerFW"] diff --git a/README.md b/README.md index 66a0584..1ca85ac 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Stars](https://img.shields.io/github/stars/jayjojayson/power-flux-card)](https://github.com/jayjojayson/power-flux-card/stargazers) -# Power Flux Card +# Power Flux Card The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. @@ -84,3 +84,151 @@ You can configure the card directly via the visual editor in Home Assistant. - **Donut Chart**: Show the energy mix as a ring around the house. - **Comet Tail / Dashed Line**: Change the flow animation style. - **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). + + +
+ Custom Colors with card_mod and Jinja2 Templates + +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. +
diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index d39601d..2400503 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -18,14 +18,14 @@ const lang_de = { "editor.label_toggle": "Label im Kreis anzeigen", "editor.compact_view": "Kompakte Ansicht (evcc)", "editor.hide_inactive": "Inaktive Röhren ausblenden", - "editor.entity": "Entität (Watt)", + "editor.entity": "Kombinierter Batterie Sensor (W)", "editor.label": "Beschriftung", "editor.icon": "Icon", "editor.back": "Zurück", "editor.battery_soc_label": "Ladestand (%)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", - "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).", + "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_3_title": "🏊 Rechts (Türkis)", @@ -39,11 +39,22 @@ const lang_de = { "editor.hide_consumer_icons": "Icons unten ausblenden", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", - "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", - "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", - "editor.color_picker": "Farbe anpassen", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", + "editor.color_picker": "Bubble", + "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.label_solar": "Solar", @@ -51,8 +62,8 @@ const lang_de = { "card.label_battery": "Batterie", "card.label_house": "Verbrauch", "card.label_car": "E-Auto", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heizung", + "card.label_pool": "Pool", } }; const lang_en = { @@ -70,14 +81,14 @@ const lang_en = { "editor.label_toggle": "Show Label in Bubble", "editor.compact_view": "Compact View (evcc)", "editor.hide_inactive": "Hide Inactive Pipes", - "editor.entity": "Entity (Watt)", + "editor.entity": "Combined Battery Sensor (W)", "editor.label": "Label", "editor.icon": "Icon", "editor.back": "Back", "editor.battery_soc_label": "State of Charge (%)", "editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)", - "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).", + "editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_3_title": "🏊 Right (Cyan)", @@ -91,11 +102,22 @@ const lang_en = { "editor.hide_consumer_icons": "Hide Consumer Icons", "editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.secondary_sensor": "Secondary Sensor (display only)", - "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", - "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", - "editor.color_picker": "Custom Color", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", + "editor.color_picker": "Bubble 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.label_solar": "Solar", @@ -103,8 +125,8 @@ const lang_en = { "card.label_battery": "Battery", "card.label_house": "Consumption", "card.label_car": "Car", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heater", + "card.label_pool": "Pool", } }; @@ -164,7 +186,7 @@ class PowerFluxCardEditor extends LitElement { if (!this._config || !this.hass) return; const target = ev.target; - const key = target.configValue || this._currentConfigValue; + const key = target.configValue; let value; if (target.tagName === 'HA-SWITCH') { @@ -183,6 +205,7 @@ class PowerFluxCardEditor extends LitElement { const entityKeys = [ 'solar', 'grid', 'grid_export', 'grid_combined', 'battery', 'battery_soc', 'grid_to_battery', + 'battery_charge', 'battery_discharge', 'house', 'consumer_1', 'consumer_2', 'consumer_3', 'secondary_solar', 'secondary_grid', 'secondary_battery', @@ -278,6 +301,34 @@ 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` +
+ ${items.map(item => { + const color = this._config[item.key] || item.default; + const hasCustom = !!this._config[item.key]; + return html` +
+ this._colorChanged(item.key, e)}> + ${item.label} + ${hasCustom ? html` this._resetColor(item.key)}>` : ''} +
+ `; + })} +
+ `; + } + static get styles() { return css` .card-config { @@ -385,8 +436,8 @@ class PowerFluxCardEditor extends LitElement { -webkit-appearance: none; border: 2px solid var(--divider-color); border-radius: 50%; - width: 36px; - height: 36px; + width: 30px; + height: 30px; padding: 2px; cursor: pointer; background: transparent; @@ -410,6 +461,13 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } + .color-picker-quad { + display: flex; + gap: 8px; + } + .color-picker-quad .color-picker-row { + flex: 1; + } `; } @@ -448,7 +506,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} + ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
@@ -482,11 +540,13 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
+
${this._localize('editor.grid_combined_hint')}
-
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} @@ -514,7 +574,9 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} + ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} + + ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@@ -549,13 +611,14 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} +
- ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
- ${this._localize('editor.grid_to_battery_hint')} + ${this._localize('editor.battery_separate_hint')}
- + ${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'))} +
+
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + +
+ +
+ ${this._localize('editor.grid_to_battery_hint')} +
+ ${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._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')} + ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -626,6 +700,8 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_sensor_hint')}
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
@@ -659,9 +735,38 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_1_hide_pipe')} + +
+ + ${this._config.consumer_1_hide_pipe === true ? html` + + ` : ''} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')} + ${this._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
@@ -686,9 +791,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')} + ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
@@ -713,9 +827,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')} + ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
`; } @@ -849,6 +972,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; + const color = colorOverride || 'var(--icon-solar-color)'; return html``; } if (type === 'grid') { const animate = Math.round(val) > 0 ? 'pulse' : ''; - const color = colorOverride || 'var(--neon-blue)'; + const color = colorOverride || 'var(--icon-grid-color)'; return html``; } if (type === 'battery') { const soc = Math.min(Math.max(val, 0), 100) / 100; const rectHeight = 14 * soc; const rectY = 18 - rectHeight; - const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-battery-color)'; + const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; + return html``; } if (type === 'house') { - const strokeColor = colorOverride || 'var(--neon-pink)'; + const strokeColor = colorOverride || 'var(--icon-house-color)'; return html``; } if (type === 'car') { - const c = colorOverride || 'var(--consumer-1-color)'; + const c = colorOverride || 'var(--icon-consumer-1-color)'; return html``; } if (type === 'heater') { - const c = colorOverride || 'var(--consumer-2-color)'; + const c = colorOverride || 'var(--icon-consumer-2-color)'; return html``; } if (type === 'pool') { - const c = colorOverride || 'var(--consumer-3-color)'; + const c = colorOverride || 'var(--icon-consumer-3-color)'; return html``; } return html``; @@ -1293,6 +1482,11 @@ console.log( 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 --- _renderSVGPath(d, color) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); @@ -1346,6 +1540,9 @@ console.log( const state = this.hass.states[entity]; 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 hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); @@ -1356,7 +1553,7 @@ console.log( if (this.config.invert_battery) { battery *= -1; } - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); @@ -1376,8 +1573,12 @@ console.log( gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -1471,9 +1672,9 @@ console.log( const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -1491,10 +1692,10 @@ console.log( let icon = ''; let iconColor = ''; - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } - if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } + if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } + if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } + if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } const path = this._createBracketPath(bottomX, width, 'up'); bottomBrackets.push({ @@ -1537,14 +1738,19 @@ console.log(
- ${barSegments.map(s => html` + ${barSegments.map(s => { + const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)' + : s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)' + : s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)' + : (s.color === 'var(--export-purple)' ? 'white' : 'black'); + return html`
s.entityId && this._handleClick(s.entityId)}> ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
- `)} + `})}
@@ -1614,13 +1820,7 @@ console.log( const val = parseFloat(state.state); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); const unit = state.attributes.unit_of_measurement || ''; - if (unit === 'W' || unit === 'Wh') { - return this._formatPower(val); - } - if (unit === 'kWh' || unit === 'kW') { - return val.toFixed(1) + ' ' + unit; - } - return val.toFixed(1) + (unit ? ' ' + unit : ''); + 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 : ''); }; // Determine existence of main entities @@ -1638,26 +1838,35 @@ console.log( const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; // Custom Labels for Consumers - const labelC1 = this.config.consumer_1_label || "E-Auto"; - const labelC2 = this.config.consumer_2_label || "Heizung"; - const labelC3 = this.config.consumer_3_label || "Pool"; + const labelC1 = this.config.consumer_1_label || this._localize('card.label_car'); + const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); + const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const getVal = (entity) => { const state = this.hass.states[entity]; return state ? parseFloat(state.state) || 0 : 0; }; + const getValKw = (entity, isKw) => { + return getVal(entity) * (isKw ? 1000 : 1); + }; - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; + const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; + const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; - const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0); - const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0); - const showC3 = (entities.consumer_3 && Math.round(c3Val) > 0); + const alwaysShowConsumer = this.config.show_consumer_always === true; + const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); + const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); + const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); 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 gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); @@ -1687,8 +1896,12 @@ console.log( } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -1715,6 +1928,9 @@ console.log( const gridToHouse = Math.max(0, gridImport - gridToBatt); 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 topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; let baseHeight = anyBottomVisible ? 480 : 340; @@ -1796,8 +2012,30 @@ console.log( const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + // --- Grid Donut Gradient --- + let gridGradientVal = ''; + 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) => { if (val <= 1) return "opacity: 0;"; @@ -1877,27 +2115,24 @@ console.log( const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); + const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); return html``; } return this._renderIcon(type, val, color); }; - const getCustomClass = (icon) => icon ? 'has-custom-icon' : ''; - const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { if (!isVisible) return html``; const customIcon = this.config[`${configKey}_icon`]; let iconContent; - const isCustom = !hideConsumerIcons && !!customIcon; - const dynamicClass = isCustom ? 'has-custom-icon' : ''; + const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; if (hideConsumerIcons) { iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { iconContent = this._renderIcon(iconType, val); } @@ -1905,12 +2140,16 @@ console.log( const secEntity = entities[`secondary_${configKey}`]; const hasSecondary = !!(secEntity && secEntity !== ""); + const textStyle = this.config[`color_text_${configKey}`] + ? `color: var(--text-${configKey.replace(/_/g, '-')}-color);` + : getConsumerColorStyle(hexColor); + return html` -
this._handleClick(entities[configKey])}> ${iconContent} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} -
${this._formatPower(val)}
+
${this._formatPower(val)}
`; }; @@ -1929,13 +2168,20 @@ console.log( const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; 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 pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370"; - const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; + const houseTextStyle = this.config.color_text_house + ? 'color: var(--text-house-color);' + : (houseTextCol ? `color: ${houseTextCol};` : ''); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const strokeWidthVal = showDashedLine ? 4 : 8; @@ -1951,33 +2197,33 @@ console.log( - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -1985,38 +2231,39 @@ console.log( ${hasSolar ? html` -
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} -
${this._formatPower(solarVal)}
+
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> - ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} -
+
${isGridExporting ? html`` : (isGridActive ? html`` : '')} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
` : ''} ${hasBattery ? html` -
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} -
${Math.round(battSoc)}%
+
${Math.round(battSoc)}%
` : ''} -
this._handleClick(entities.house)}> - ${renderMainIcon('house', 0, null, houseDominantColor)} + ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)} -
${this._formatPower(house)}
+
${this._formatPower(houseDisplay)}
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} diff --git a/docs/README-de.md b/docs/README-de.md index 3e90900..8f75370 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -86,3 +86,151 @@ Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigur - **Donut Chart**: Zeigt den Energiemix als Ring um das Haus an. - **Kometenschweif / Gestrichelte Linie**: Ändern Sie den Stil der Flussanimation. - **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). + + +
+ Custom Farben mit card_mod und Jinja2 Templates + +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. +
\ No newline at end of file diff --git a/src/lang-de.js b/src/lang-de.js index f7220c7..8e7845b 100644 --- a/src/lang-de.js +++ b/src/lang-de.js @@ -13,14 +13,14 @@ export default { "editor.label_toggle": "Label im Kreis anzeigen", "editor.compact_view": "Kompakte Ansicht (evcc)", "editor.hide_inactive": "Inaktive Röhren ausblenden", - "editor.entity": "Entität (Watt)", + "editor.entity": "Kombinierter Batterie Sensor (W)", "editor.label": "Beschriftung", "editor.icon": "Icon", "editor.back": "Zurück", "editor.battery_soc_label": "Ladestand (%)", "editor.house_total_title": "🏠 Gesamtverbrauch (optional)", "editor.house_sensor_label": "Sensor für Hausverbrauch (optional)", - "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (compact view).", + "editor.house_sensor_hint": "Wird benötigt, damit das Haus-Icon anklickbar ist (more-details). Ansonsten wird der Hausverbrauch berechnet.", "editor.consumer_1_title": "🚗 Links (Lila)", "editor.consumer_2_title": "♨️ Mitte (Orange)", "editor.consumer_3_title": "🏊 Rechts (Türkis)", @@ -34,11 +34,22 @@ export default { "editor.hide_consumer_icons": "Icons unten ausblenden", "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", - "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (Watt)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (W, Optional)", "editor.grid_to_battery_hint": "Optional: separater Sensor für den Netz-zu-Batterie Fluss. Wenn leer, wird der Wert automatisch berechnet.", "editor.grid_combined_sensor": "Kombinierter Netz-Sensor (W, Optional)", - "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt die getrennten Import/Export Sensoren.", - "editor.color_picker": "Farbe anpassen", + "editor.grid_combined_hint": "Ein Sensor für Import UND Export: positiv = Import, negativ = Export. Überschreibt den kombinierten Import/Export Sensor.", + "editor.color_picker": "Bubble", + "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.label_solar": "Solar", @@ -46,7 +57,7 @@ export default { "card.label_battery": "Batterie", "card.label_house": "Verbrauch", "card.label_car": "E-Auto", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heizung", + "card.label_pool": "Pool", } }; diff --git a/src/lang-en.js b/src/lang-en.js index f955cb9..46ae1ca 100644 --- a/src/lang-en.js +++ b/src/lang-en.js @@ -13,14 +13,14 @@ export default { "editor.label_toggle": "Show Label in Bubble", "editor.compact_view": "Compact View (evcc)", "editor.hide_inactive": "Hide Inactive Pipes", - "editor.entity": "Entity (Watt)", + "editor.entity": "Combined Battery Sensor (W)", "editor.label": "Label", "editor.icon": "Icon", "editor.back": "Back", "editor.battery_soc_label": "State of Charge (%)", "editor.house_total_title": "🏠 Total Consumption (optional)", "editor.house_sensor_label": "Sensor for House Consumption (optional)", - "editor.house_sensor_hint": "Required to make the house icon clickable (compact view).", + "editor.house_sensor_hint": "Required to make the house icon clickable (more-details). Otherwise, the house consumption is calculated.", "editor.consumer_1_title": "🚗 Left (Purple)", "editor.consumer_2_title": "♨️ Center (Orange)", "editor.consumer_3_title": "🏊 Right (Cyan)", @@ -34,11 +34,22 @@ export default { "editor.hide_consumer_icons": "Hide Consumer Icons", "editor.invert_consumer_1": "Invert Sensor Value (+/-)", "editor.secondary_sensor": "Secondary Sensor (display only)", - "editor.grid_to_battery_sensor": "Grid to Battery Sensor (Watt)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (W, optional)", "editor.grid_to_battery_hint": "Optional: separate sensor for grid-to-battery flow. If empty, the value is calculated automatically.", "editor.grid_combined_sensor": "Combined Grid Sensor (W, Optional)", - "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides separate import/export sensors.", - "editor.color_picker": "Custom Color", + "editor.grid_combined_hint": "Single sensor for import AND export: positive = import, negative = export. Overrides combined import/export sensor.", + "editor.color_picker": "Bubble 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.label_solar": "Solar", @@ -46,7 +57,7 @@ export default { "card.label_battery": "Battery", "card.label_house": "Consumption", "card.label_car": "Car", - "card.label_import": "Import", - "card.label_export": "Export", + "card.label_heater": "Heater", + "card.label_pool": "Pool", } }; diff --git a/src/power-flux-card-editor.js b/src/power-flux-card-editor.js index bf1177a..7702902 100644 --- a/src/power-flux-card-editor.js +++ b/src/power-flux-card-editor.js @@ -48,7 +48,7 @@ class PowerFluxCardEditor extends LitElement { if (!this._config || !this.hass) return; const target = ev.target; - const key = target.configValue || this._currentConfigValue; + const key = target.configValue; let value; if (target.tagName === 'HA-SWITCH') { @@ -67,6 +67,7 @@ class PowerFluxCardEditor extends LitElement { const entityKeys = [ 'solar', 'grid', 'grid_export', 'grid_combined', 'battery', 'battery_soc', 'grid_to_battery', + 'battery_charge', 'battery_discharge', 'house', 'consumer_1', 'consumer_2', 'consumer_3', 'secondary_solar', 'secondary_grid', 'secondary_battery', @@ -162,6 +163,34 @@ 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` +
+ ${items.map(item => { + const color = this._config[item.key] || item.default; + const hasCustom = !!this._config[item.key]; + return html` +
+ this._colorChanged(item.key, e)}> + ${item.label} + ${hasCustom ? html` this._resetColor(item.key)}>` : ''} +
+ `; + })} +
+ `; + } + static get styles() { return css` .card-config { @@ -269,8 +298,8 @@ class PowerFluxCardEditor extends LitElement { -webkit-appearance: none; border: 2px solid var(--divider-color); border-radius: 50%; - width: 36px; - height: 36px; + width: 30px; + height: 30px; padding: 2px; cursor: pointer; background: transparent; @@ -294,6 +323,13 @@ class PowerFluxCardEditor extends LitElement { .color-reset-btn:hover { color: var(--primary-color); } + .color-picker-quad { + display: flex; + gap: 8px; + } + .color-picker-quad .color-picker-row { + flex: 1; + } `; } @@ -332,7 +368,7 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_solar || "", 'secondary_solar', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_solar', this._localize('editor.color_picker'), '#ffdd00')} + ${this._renderColorPickerQuad('color_solar', 'color_pipe_solar', 'color_text_solar', 'color_icon_solar', '#ffdd00')}
@@ -366,11 +402,13 @@ class PowerFluxCardEditor extends LitElement {
${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
+
${this._localize('editor.grid_combined_hint')}
-
${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} @@ -398,7 +436,9 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_grid || "", 'secondary_grid', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_grid', this._localize('editor.color_picker'), '#3b82f6')} + ${this._renderColorPickerQuad('color_grid', 'color_pipe_grid', 'color_text_grid', 'color_icon_grid', '#3b82f6')} + + ${this._renderColorPicker('color_export', this._localize('editor.export_color'), '#ff3333')}
@@ -433,13 +473,14 @@ class PowerFluxCardEditor extends LitElement { ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} - ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} +
- ${this._renderEntitySelector(entitySelectorSchema, entities.grid_to_battery || "", 'grid_to_battery', this._localize('editor.grid_to_battery_sensor'))}
- ${this._localize('editor.grid_to_battery_hint')} + ${this._localize('editor.battery_separate_hint')}
- + ${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'))} +
+
+ + ${this._renderEntitySelector(entitySelectorSchema, entities.battery_soc, 'battery_soc', this._localize('editor.battery_soc_label'))} + +
+ +
+ ${this._localize('editor.grid_to_battery_hint')} +
+ ${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._renderColorPicker('color_battery', this._localize('editor.color_picker'), '#00ff88')} + ${this._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -510,6 +562,8 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.house_sensor_hint')}
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')}
@@ -543,9 +597,38 @@ class PowerFluxCardEditor extends LitElement { >
+
+ ${this._localize('editor.consumer_1_hide_pipe')} + +
+ + ${this._config.consumer_1_hide_pipe === true ? html` + + ` : ''} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_1 || "", 'secondary_consumer_1', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_1', this._localize('editor.color_picker'), '#a855f7')} + ${this._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
@@ -570,9 +653,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_2 || "", 'secondary_consumer_2', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_2', this._localize('editor.color_picker'), '#f97316')} + ${this._renderColorPickerQuad('color_consumer_2', 'color_pipe_consumer_2', 'color_text_consumer_2', 'color_icon_consumer_2', '#f97316')}
@@ -597,9 +689,18 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ ${this._renderEntitySelector(entitySelectorSchema, entities.secondary_consumer_3 || "", 'secondary_consumer_3', this._localize('editor.secondary_sensor'))} - ${this._renderColorPicker('color_consumer_3', this._localize('editor.color_picker'), '#06b6d4')} + ${this._renderColorPickerQuad('color_consumer_3', 'color_pipe_consumer_3', 'color_text_consumer_3', 'color_icon_consumer_3', '#06b6d4')}
`; } @@ -733,6 +834,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; + const color = colorOverride || 'var(--icon-solar-color)'; return html``; } if (type === 'grid') { const animate = Math.round(val) > 0 ? 'pulse' : ''; - const color = colorOverride || 'var(--neon-blue)'; + const color = colorOverride || 'var(--icon-grid-color)'; return html``; } if (type === 'battery') { const soc = Math.min(Math.max(val, 0), 100) / 100; const rectHeight = 14 * soc; const rectY = 18 - rectHeight; - const rectColor = soc > 0.2 ? 'var(--neon-green)' : 'var(--neon-red)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-battery-color)'; + const rectColor = soc > 0.2 ? strokeColor : 'var(--neon-red)'; + return html``; } if (type === 'house') { - const strokeColor = colorOverride || 'var(--neon-pink)'; + const strokeColor = colorOverride || 'var(--icon-house-color)'; return html``; } if (type === 'car') { - const c = colorOverride || 'var(--consumer-1-color)'; + const c = colorOverride || 'var(--icon-consumer-1-color)'; return html``; } if (type === 'heater') { - const c = colorOverride || 'var(--consumer-2-color)'; + const c = colorOverride || 'var(--icon-consumer-2-color)'; return html``; } if (type === 'pool') { - const c = colorOverride || 'var(--consumer-3-color)'; + const c = colorOverride || 'var(--icon-consumer-3-color)'; return html``; } return html``; @@ -425,6 +482,11 @@ console.log( 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 --- _renderSVGPath(d, color) { const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); @@ -478,6 +540,9 @@ console.log( const state = this.hass.states[entity]; 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 hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); @@ -488,7 +553,7 @@ console.log( if (this.config.invert_battery) { battery *= -1; } - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; // EV Value if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); @@ -508,8 +573,12 @@ console.log( gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -603,9 +672,9 @@ console.log( const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); let icon = ''; let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-yellow)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--neon-blue)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-green)'; } + if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--icon-solar-color)'; } + if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--icon-grid-color)'; } + if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--icon-battery-color)'; } return { path, width: s.widthPx, center: s.startPx + (s.widthPx / 2), icon, iconColor, val: s.val, entityId: s.entityId }; }); @@ -623,10 +692,10 @@ console.log( let icon = ''; let iconColor = ''; - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = this._getConsumerColor(1); } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } - if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--neon-green)'; } + if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--icon-house-color)'; } + if (type === 'car') { icon = 'mdi:car-electric'; iconColor = 'var(--icon-consumer-1-color)'; } + if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-color)'; } + if (type === 'battery') { icon = 'mdi:battery-charging-high'; iconColor = 'var(--icon-battery-color)'; } const path = this._createBracketPath(bottomX, width, 'up'); bottomBrackets.push({ @@ -669,14 +738,19 @@ console.log(
- ${barSegments.map(s => html` + ${barSegments.map(s => { + const textColor = s.type === 'solar' && this.config.color_text_solar ? 'var(--text-solar-color)' + : s.type === 'grid' && this.config.color_text_grid ? 'var(--text-grid-color)' + : s.type === 'battery' && this.config.color_text_battery ? 'var(--text-battery-color)' + : (s.color === 'var(--export-purple)' ? 'white' : 'black'); + return html`
s.entityId && this._handleClick(s.entityId)}> ${s.widthPx > 35 ? this._formatPower(s.val) : ''}
- `)} + `})}
@@ -746,13 +820,7 @@ console.log( const val = parseFloat(state.state); if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); const unit = state.attributes.unit_of_measurement || ''; - if (unit === 'W' || unit === 'Wh') { - return this._formatPower(val); - } - if (unit === 'kWh' || unit === 'kW') { - return val.toFixed(1) + ' ' + unit; - } - return val.toFixed(1) + (unit ? ' ' + unit : ''); + 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 : ''); }; // Determine existence of main entities @@ -770,26 +838,35 @@ console.log( const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; // Custom Labels for Consumers - const labelC1 = this.config.consumer_1_label || "E-Auto"; - const labelC2 = this.config.consumer_2_label || "Heizung"; - const labelC3 = this.config.consumer_3_label || "Pool"; + const labelC1 = this.config.consumer_1_label || this._localize('card.label_car'); + const labelC2 = this.config.consumer_2_label || this._localize('card.label_heater'); + const labelC3 = this.config.consumer_3_label || this._localize('card.label_pool'); const getVal = (entity) => { const state = this.hass.states[entity]; return state ? parseFloat(state.state) || 0 : 0; }; + const getValKw = (entity, isKw) => { + return getVal(entity) * (isKw ? 1000 : 1); + }; - let c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; + let c1Val = entities.consumer_1 ? getValKw(entities.consumer_1, this.config.consumer_1_unit_kw === true) : 0; if (this.config.invert_consumer_1) { c1Val *= -1; } c1Val = Math.abs(c1Val); - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 0; + const c2Val = entities.consumer_2 ? getValKw(entities.consumer_2, this.config.consumer_2_unit_kw === true) : 0; + const c3Val = entities.consumer_3 ? getValKw(entities.consumer_3, this.config.consumer_3_unit_kw === true) : 0; - const showC1 = (entities.consumer_1 && Math.round(c1Val) > 0); - const showC2 = (entities.consumer_2 && Math.round(c2Val) > 0); - const showC3 = (entities.consumer_3 && Math.round(c3Val) > 0); + const alwaysShowConsumer = this.config.show_consumer_always === true; + const showC1 = (entities.consumer_1 && (alwaysShowConsumer || Math.round(c1Val) > 0)); + const showC2 = (entities.consumer_2 && (alwaysShowConsumer || Math.round(c2Val) > 0)); + const showC3 = (entities.consumer_3 && (alwaysShowConsumer || Math.round(c3Val) > 0)); 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 gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; const gridMain = hasGridCombined ? gridCombinedVal : (hasGrid ? getVal(entities.grid) : 0); @@ -819,8 +896,12 @@ console.log( } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // Check for separate battery charge/discharge sensors + const hasBattChargeSensor = !!(entities.battery_charge && entities.battery_charge !== ""); + const hasBattDischargeSensor = !!(entities.battery_discharge && entities.battery_discharge !== ""); + + const batteryCharge = hasBattChargeSensor ? Math.abs(getVal(entities.battery_charge)) : (battery > 0 ? battery : 0); + const batteryDischarge = hasBattDischargeSensor ? Math.abs(getVal(entities.battery_discharge)) : (battery < 0 ? Math.abs(battery) : 0); let solarToBatt = 0; let gridToBatt = 0; @@ -847,6 +928,9 @@ console.log( const gridToHouse = Math.max(0, gridImport - gridToBatt); 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 topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; let baseHeight = anyBottomVisible ? 480 : 340; @@ -928,8 +1012,30 @@ console.log( const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridExporting ? 'var(--neon-red)' : (isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'); + // --- Grid Donut Gradient --- + let gridGradientVal = ''; + 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) => { if (val <= 1) return "opacity: 0;"; @@ -1009,27 +1115,24 @@ console.log( const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); + const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--icon-solar-color);' : (type === 'grid' ? 'color: var(--icon-grid-color);' : (type === 'battery' ? 'color: var(--icon-battery-color);' : (type === 'house' ? 'color: var(--icon-house-color);' : '')))); return html``; } return this._renderIcon(type, val, color); }; - const getCustomClass = (icon) => icon ? 'has-custom-icon' : ''; - const renderConsumer = (isVisible, cssClass, configKey, label, iconType, val, hexColor) => { if (!isVisible) return html``; const customIcon = this.config[`${configKey}_icon`]; let iconContent; - const isCustom = !hideConsumerIcons && !!customIcon; - const dynamicClass = isCustom ? 'has-custom-icon' : ''; + const iconColorVar = `var(--icon-${configKey.replace(/_/g, '-')}-color)`; if (hideConsumerIcons) { iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { iconContent = this._renderIcon(iconType, val); } @@ -1037,12 +1140,16 @@ console.log( const secEntity = entities[`secondary_${configKey}`]; const hasSecondary = !!(secEntity && secEntity !== ""); + const textStyle = this.config[`color_text_${configKey}`] + ? `color: var(--text-${configKey.replace(/_/g, '-')}-color);` + : getConsumerColorStyle(hexColor); + return html` -
this._handleClick(entities[configKey])}> ${iconContent} ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} -
${this._formatPower(val)}
+
${this._formatPower(val)}
`; }; @@ -1061,13 +1168,20 @@ console.log( const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; const pathGridImport = "M 210 160 L 210 220"; 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 pathBattHouse = "M 370 160 Q 370 265 255 265"; const pathHouseC1 = "M 165 265 Q 50 265 50 370"; const pathHouseC2 = "M 210 310 L 210 370"; const pathHouseC3 = "M 255 265 Q 370 265 370 370"; - const houseTextStyle = houseTextCol ? `color: ${houseTextCol};` : ''; + const houseTextStyle = this.config.color_text_house + ? 'color: var(--text-house-color);' + : (houseTextCol ? `color: ${houseTextCol};` : ''); const dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); const strokeWidthVal = showDashedLine ? 4 : 8; @@ -1083,33 +1197,33 @@ console.log( - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -1117,38 +1231,39 @@ console.log( ${hasSolar ? html` -
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} -
${this._formatPower(solarVal)}
+
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
this._handleClick(entities.grid_combined || entities.grid)}> - ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridColor)} + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, gridIconColor)} ${renderSecondaryOrLabel(labelGridText, showLabelGrid, entities.secondary_grid, hasSecondaryGrid)} -
+
${isGridExporting ? html`` : (isGridActive ? html`` : '')} ${this._formatPower(isGridExporting ? gridExport : gridImport)}
` : ''} ${hasBattery ? html` -
this._handleClick(entities.battery)}> ${renderMainIcon('battery', battSoc, iconBattery)} ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} -
${Math.round(battSoc)}%
+
${Math.round(battSoc)}%
` : ''} -
this._handleClick(entities.house)}> - ${renderMainIcon('house', 0, null, houseDominantColor)} + ${renderMainIcon('house', 0, null, this.config.color_icon_house ? 'var(--icon-house-color)' : houseDominantColor)} ${renderLabel(labelHouseText, showLabelHouse)} -
${this._formatPower(house)}
+
${this._formatPower(houseDisplay)}
${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))}