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 9dba5a2..1ca85ac 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,14 @@ [![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 smooth animations. +The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assistant. It visualizes the power distribution between Solar, Grid, Battery, and Consumers with beautiful neon effects and diffrent animations. -power-flux-card -power-flux-card +If you like the Card, I would appreciate a Star rating ⭐ from you. 🤗 + +power-flux-card power-flux-card +power-flux-card power-flux-card ### ✨ Features @@ -24,9 +26,19 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assis - **Donut Chart**: Optional donut chart around the house icon showing energy mix. - **Comet Tail / Dashed Lines**: Choose your preferred animation style. - **Zoom**: Adjustable scale to fit your dashboard. + - **Custom Colors**: Define custom colors for each source and consumer via the editor. + - **Background Color**: Enable a slightly tinted background for the circles in the default view. +- **More Info**: Click on any source/consumer for detailed information in a more-info dialog. +- **Grid Import/Export**: Supports both separate Import/Export entities or a combined entity with positive/negative values. +- **Grid-to-Battery**: Optional direct sensor for Grid-to-Battery flow, bypassing the standard calculation. +- **Secondary Sensors**: Optionally display a secondary sensor value in the main circles (e.g., daily yield for Solar, current charge/discharge power for Battery) and consumer bubbles. - **Localization**: Fully translated in English and German. - **Visual Editor**: easy configuration via the Home Assistant UI. +[![Support](https://img.shields.io/badge/Features-Video%20german-steelblue?style=for-the-badge&logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=HGFBJJRWGW0) + +--- + ### 🚀 Installation ### HACS (Recommended) @@ -35,10 +47,9 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assis [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=jayjojayson&repository=power-flux-card&category=plugin) -- The "Detailed Charts Panel" should now be available in HACS. Click on "INSTALL". +- The "Power Flux Card" should now be available in HACS. Click on "INSTALL". - The resource will be automatically added to your Lovelace configuration. -- Create the file `detail-charts-views.js` in the `/config/www/` folder. -- +- Create the file `power-flux-card.js` in the `/config/www/` folder. #### HACS (manual) 1. Ensure HACS is installed. @@ -48,11 +59,13 @@ The ⚡ Power Flux Card is an advanced, animated energy flow card for Home Assis #### Manual Installation 1. Download `power-flux-card.js` from the [Releases](../../releases) page. -2. Upload it to your `www` folder in Home Assistant. +2. Upload it to `www/community/power-flux-card/` folder in Home Assistant. 3. Add the resource in your Dashboard configuration: - - URL: `/local/power-flux-card.js` + - URL: `/local/community/power-flux-card/power-flux-card.js` - Type: JavaScript Module +--- + ### ⚙️ Configuration You can configure the card directly via the visual editor in Home Assistant. @@ -71,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/build.js b/build.js index f6b905f..0b26f7c 100644 --- a/build.js +++ b/build.js @@ -6,7 +6,7 @@ const DIST_DIR = 'dist'; const OUTPUT_FILE = path.join(DIST_DIR, 'power-flux-card.js'); // Ensure dist dir exists -if (!fs.existsSync(DIST_DIR)){ +if (!fs.existsSync(DIST_DIR)) { fs.mkdirSync(DIST_DIR); } @@ -24,17 +24,17 @@ const cardTranslations = {}; langFiles.forEach(file => { const langCode = file.replace('lang-', '').replace('.js', ''); - + let content = fs.readFileSync(path.join(SRC_DIR, file), 'utf8'); // Remove export default content = content.replace('export default', '').trim(); if (content.endsWith(';')) { content = content.slice(0, -1); } - + // Assign to a variable langDefinitions += `const lang_${langCode} = ${content};\n`; - + // Add to merge logic mergeScript += `editorTranslations['${langCode}'] = lang_${langCode}.editor;\n`; mergeScript += `cardTranslations['${langCode}'] = lang_${langCode}.card;\n`; @@ -49,6 +49,9 @@ console.log('Processing editor...'); let editorContent = fs.readFileSync(path.join(SRC_DIR, 'power-flux-card-editor.js'), 'utf8'); // Remove imports/exports if any editorContent = editorContent.replace(/import .* from .*/g, '').replace(/export .*/g, ''); +// Remove editorTranslations/cardTranslations declarations (already created by build merge script) +editorContent = editorContent.replace(/const editorTranslations\s*=\s*\{[^}]*\};/gs, ''); +editorContent = editorContent.replace(/const cardTranslations\s*=\s*\{[^}]*\};/gs, ''); // Process Main Card console.log('Processing main card...'); diff --git a/dist/power-flux-card.js b/dist/power-flux-card.js index d958ace..2400503 100644 --- a/dist/power-flux-card.js +++ b/dist/power-flux-card.js @@ -14,12 +14,47 @@ const lang_de = { "editor.consumers_section": "Zusätzliche Verbraucher", "editor.options_section": "Darstellung & Optionen", "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", + "editor.invert_battery": "Wert umkehren (+/-)", "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 (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)", + "editor.zoom_label": "🔍 Zoom (Standard View)", + "editor.neon_glow": "Neon Glow", + "editor.donut_chart": "Donut Chart (Grid/Haus)", + "editor.comet_tail": "Comet Tail Effect", + "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Farbiger Hintergrund in Kreisen", + "editor.colored_values": "Farbige Textwerte", + "editor.hide_consumer_icons": "Icons unten ausblenden", + "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", + "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (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 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", @@ -27,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 = { @@ -42,12 +77,47 @@ const lang_en = { "editor.consumers_section": "Additional Consumers", "editor.options_section": "Appearance & Options", "editor.flow_rate_title": "Show Flow Rates (W) on pipes", + "editor.invert_battery": "Invert Power Value (+/-)", "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 (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)", + "editor.zoom_label": "🔍 Zoom (Standard View)", + "editor.neon_glow": "Neon Glow", + "editor.donut_chart": "Donut Chart (Grid/House)", + "editor.comet_tail": "Comet Tail Effect", + "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Tinted Background in Bubbles", + "editor.colored_values": "Colored Text Values", + "editor.hide_consumer_icons": "Hide Consumer Icons", + "editor.invert_consumer_1": "Invert Sensor Value (+/-)", + "editor.secondary_sensor": "Secondary Sensor (display only)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (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 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", @@ -55,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", } }; @@ -69,17 +139,23 @@ cardTranslations['en'] = lang_en.card; + + + + + + const fireEvent = (node, type, detail, options) => { - options = options || {}; - detail = detail === null || detail === undefined ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed, - }); - event.detail = detail; - node.dispatchEvent(event); - return event; + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + event.detail = detail; + node.dispatchEvent(event); + return event; }; const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); @@ -87,83 +163,174 @@ const html = LitElement.prototype.html; const css = LitElement.prototype.css; class PowerFluxCardEditor extends LitElement { - - static get properties() { - return { - hass: {}, - _config: { state: true }, - _subView: { state: true } // Controls which sub-page is open (null = main) - }; - } - setConfig(config) { - this._config = config; - } - - _localize(key) { - const lang = this.hass && this.hass.language ? this.hass.language : 'en'; - const dict = editorTranslations[lang] || editorTranslations['en']; - return dict[key] || editorTranslations['en'][key] || key; - } - - _valueChanged(ev) { - if (!this._config || !this.hass) return; - - const target = ev.target; - const key = target.configValue || this._currentConfigValue; - - let value; - if (target.tagName === 'HA-SWITCH') { - value = target.checked; - } else if (ev.detail && 'value' in ev.detail) { - value = ev.detail.value; - } else { - value = target.value; - } - - if (value === null || value === undefined) { - value = ""; + static get properties() { + return { + hass: {}, + _config: { state: true }, + _subView: { state: true } // Controls which sub-page is open (null = main) + }; } - if (key) { - const entityKeys = [ - 'solar', 'grid', 'grid_export', - 'battery', 'battery_soc', - 'consumer_1', 'consumer_2', 'consumer_3' - ]; + setConfig(config) { + this._config = config; + } - let newConfig = { ...this._config }; + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = editorTranslations[lang] || editorTranslations['en']; + return dict[key] || editorTranslations['en'][key] || key; + } - if (entityKeys.includes(key)) { - const currentEntities = newConfig.entities || {}; - const newEntities = { ...currentEntities, [key]: value }; - newConfig.entities = newEntities; + _valueChanged(ev) { + if (!this._config || !this.hass) return; + + const target = ev.target; + const key = target.configValue; + + let value; + if (target.tagName === 'HA-SWITCH') { + value = target.checked; + } else if (ev.detail && 'value' in ev.detail) { + value = ev.detail.value; } else { - newConfig[key] = value; - - if (key === 'show_comet_tail' && value === true) { - newConfig.show_dashed_line = false; - } - if (key === 'show_dashed_line' && value === true) { - newConfig.show_comet_tail = false; - } + value = target.value; } + if (value === null || value === undefined) { + value = ""; + } + + if (key) { + 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', + 'secondary_consumer_1', 'secondary_consumer_2', 'secondary_consumer_3' + ]; + + let newConfig = { ...this._config }; + + if (entityKeys.includes(key)) { + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: value }; + newConfig.entities = newEntities; + } else { + newConfig[key] = value; + + if (key === 'show_comet_tail' && value === true) { + newConfig.show_dashed_line = false; + } + if (key === 'show_dashed_line' && value === true) { + newConfig.show_comet_tail = false; + } + } + + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + } + + _goSubView(view) { + this._subView = view; + } + + _goBack() { + this._subView = null; + } + + _clearEntity(key) { + const newConfig = { ...this._config }; + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: "" }; + newConfig.entities = newEntities; this._config = newConfig; fireEvent(this, "config-changed", { config: this._config }); } - } - _goSubView(view) { - this._subView = view; - } + _colorChanged(key, ev) { + const newConfig = { ...this._config, [key]: ev.target.value }; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } - _goBack() { - this._subView = null; - } + _resetColor(key) { + const newConfig = { ...this._config }; + delete newConfig[key]; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } - static get styles() { - return css` + _renderEntitySelector(entitySelectorSchema, value, configValue, label) { + const val = value || ""; + return html` +
+ + ${val ? html` this._clearEntity(configValue)} + >` : ''} +
+ `; + } + + _renderColorPicker(key, label, defaultColor) { + const currentColor = this._config[key] || defaultColor; + const hasCustom = !!this._config[key]; + return html` +
+ this._colorChanged(key, e)}> + ${label} + ${hasCustom ? html` this._resetColor(key)}>` : ''} +
+ `; + } + + _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 { display: flex; flex-direction: column; @@ -240,28 +407,82 @@ class PowerFluxCardEditor extends LitElement { border-bottom: 1px solid var(--divider-color); margin: 10px 0; } + .entity-picker-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 4px; + } + .entity-picker-wrapper ha-selector { + flex: 1; + } + .clear-entity-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + flex-shrink: 0; + margin-top: -12px; + } + .clear-entity-btn:hover { + color: var(--error-color, #db4437); + } + .color-picker-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + } + .color-picker-row input[type="color"] { + -webkit-appearance: none; + border: 2px solid var(--divider-color); + border-radius: 50%; + width: 30px; + height: 30px; + padding: 2px; + cursor: pointer; + background: transparent; + } + .color-picker-row input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + .color-picker-row input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 50%; + } + .color-label { + flex: 1; + font-size: 14px; + } + .color-reset-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + } + .color-reset-btn:hover { + color: var(--primary-color); + } + .color-picker-quad { + display: flex; + gap: 8px; + } + .color-picker-quad .color-picker-row { + flex: 1; + } `; - } + } - // --- SUBVIEW RENDERING --- + // --- SUBVIEW RENDERING --- - _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.solar_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.solar, 'solar', this._localize('editor.entity'))}
@@ -283,11 +504,15 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${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')} +
@@ -303,34 +528,29 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.flow_rate_title')}
`; - } + } - _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.grid_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
- +
+ ${this._localize('editor.grid_combined_hint')} +
+ + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_export, 'grid_export', this._localize('card.label_export') + " (W, Optional)")}
@@ -352,11 +572,17 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${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_export', this._localize('editor.export_color'), '#ff3333')} +
@@ -372,35 +598,27 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.flow_rate_title')}
`; - } + } - _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.battery_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} + +
+ +
+ ${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._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -440,28 +673,40 @@ class PowerFluxCardEditor extends LitElement { >
${this._localize('editor.flow_rate_title')}
- `; - } - _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` +
+ +
${this._localize('editor.invert_battery')}
+
+ `; + } + + _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.consumers_section')}

-
🚗 Links (Lila)
- +
${this._localize('editor.house_total_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.house || "", 'house', this._localize('editor.house_sensor_label'))} +
+ ${this._localize('editor.house_sensor_hint')} +
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')} +
+ +
+
${this._localize('editor.consumer_1_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_1, 'consumer_1', this._localize('editor.entity'))} + +
+ ${this._localize('editor.invert_consumer_1')} + +
+ +
+ ${this._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._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
-
♨️ Mitte (Orange)
- +
${this._localize('editor.consumer_2_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_2, 'consumer_2', this._localize('editor.entity'))} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ + ${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')}
-
🏊 Rechts (Türkis)
- +
${this._localize('editor.consumer_3_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_3, 'consumer_3', this._localize('editor.entity'))} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ + ${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')}
`; - } - - render() { - if (!this.hass || !this._config) { - return html``; } - const entities = this._config.entities || {}; + render() { + if (!this.hass || !this._config) { + return html``; + } - const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; - const textSelectorSchema = { text: {} }; - const iconSelectorSchema = { icon: {} }; + const entities = this._config.entities || {}; - // SUBVIEW ROUTING - if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; + const textSelectorSchema = { text: {} }; + const iconSelectorSchema = { icon: {} }; + + // SUBVIEW ROUTING + if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - // MAIN MENU VIEW - return html` + // MAIN MENU VIEW + return html`
${this._localize('editor.main_title')}
@@ -596,7 +895,7 @@ class PowerFluxCardEditor extends LitElement { .selector=${{ number: { min: 0.5, max: 1.5, step: 0.05, mode: "slider" } }} .value=${this._config.zoom !== undefined ? this._config.zoom : 0.9} .configValue=${'zoom'} - .label=${"🔍 Zoom (Standard View)"} + .label=${this._localize('editor.zoom_label')} @value-changed=${this._valueChanged} >
@@ -607,7 +906,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_neon_glow'} @change=${this._valueChanged} > -
Neon Glow
+
${this._localize('editor.neon_glow')}
@@ -616,7 +915,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_donut_border'} @change=${this._valueChanged} > -
Donut Chart (Grid/Haus)
+
${this._localize('editor.donut_chart')}
@@ -625,7 +924,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_comet_tail'} @change=${this._valueChanged} > -
Comet Tail Effect
+
${this._localize('editor.comet_tail')}
@@ -634,7 +933,16 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_dashed_line'} @change=${this._valueChanged} > -
Dashed Line Animation
+
${this._localize('editor.dashed_line')}
+
+ +
+ +
${this._localize('editor.tinted_background')}
@@ -643,7 +951,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'use_colored_values'} @change=${this._valueChanged} > -
Farbige Textwerte
+
${this._localize('editor.colored_values')}
@@ -652,7 +960,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'hide_consumer_icons'} @change=${this._valueChanged} > -
Icons unten ausblenden
+
${this._localize('editor.hide_consumer_icons')}
@@ -664,6 +972,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
`; - } + } } customElements.define("power-flux-card-editor", PowerFluxCardEditor); + + + + console.log( - "%c⚡ Power-Flux-Card v_2.0 ready", - "background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" + "%c⚡ Power Flux Card v_2.4 ready", + "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" ); -class PowerFluxCard extends LitElement { - - static get properties() { - return { - hass: {}, - config: {}, - _cardWidth: { state: true }, - }; - } +(function (lang_en, lang_de) { + const cardTranslations = { + "en": lang_en.card, + "de": lang_de.card + }; - _localize(key) { - const lang = this.hass && this.hass.language ? this.hass.language : 'en'; - const dict = cardTranslations[lang] || cardTranslations['en']; - return dict[key] || cardTranslations['en'][key] || key; - } + const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); + const html = LitElement.prototype.html; + const css = LitElement.prototype.css; - static async getConfigElement() { - return document.createElement("power-flux-card-editor"); - } - - static getStubConfig() { - return { - zoom: 0.9, - compact_view: false, - show_donut_border: false, - show_neon_glow: true, - show_comet_tail: false, - show_dashed_line: false, - show_tinted_background: false, - hide_inactive_flows: true, - show_flow_rate_solar: true, - show_flow_rate_grid: true, - show_flow_rate_battery: true, - show_label_solar: false, - show_label_grid: false, - show_label_battery: false, - show_label_house: false, - use_colored_values: false, - hide_consumer_icons: false, - entities: { - solar: "", - grid: "", - grid_export: "", - battery: "", - battery_soc: "", - consumer_1: "", - consumer_2: "", - consumer_3: "" - } - }; - } - - setConfig(config) { - if (!config.entities) { - // Init allow + class PowerFluxCard extends LitElement { + static get properties() { + return { + hass: {}, + config: {}, + _cardWidth: { state: true }, + }; } - this.config = config; - } - firstUpdated() { - this._resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - if (entry.contentRect.width > 0) { - this._cardWidth = entry.contentRect.width; + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = cardTranslations[lang] || cardTranslations['en']; + return dict[key] || cardTranslations['en'][key] || key; + } + + static async getConfigElement() { + return document.createElement("power-flux-card-editor"); + } + + static getStubConfig() { + return { + zoom: 0.9, + 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_neon_glow: true, + show_comet_tail: false, + show_dashed_line: false, + show_tinted_background: false, + hide_inactive_flows: true, + show_flow_rate_solar: true, + show_flow_rate_grid: true, + show_flow_rate_battery: true, + show_label_solar: false, + show_label_grid: false, + show_label_battery: false, + show_label_house: false, + use_colored_values: false, + hide_consumer_icons: false, + entities: { + solar: "", + grid: "", + grid_export: "", + grid_combined: "", + battery: "", + battery_soc: "", + battery_charge: "", + battery_discharge: "", + house: "", + consumer_1: "", + consumer_2: "", + consumer_3: "" + } + }; + } + + _handleClick(entityId) { + if (!entityId) return; + const event = new Event("hass-more-info", { + bubbles: true, + composed: true, + }); + event.detail = { entityId }; + this.dispatchEvent(event); + } + + setConfig(config) { + if (!config.entities) { + // Init allow + } + this.config = config; + } + + firstUpdated() { + this._resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.contentRect.width > 0) { + this._cardWidth = entry.contentRect.width; + } + } + }); + this._resizeObserver.observe(this); + } + + updated(changedProps) { + super.updated(changedProps); + if (changedProps.has('hass') && this.hass) { + const isDark = this.hass.themes?.darkMode !== false; + if (isDark) { + this.removeAttribute('data-theme-light'); + } else { + this.setAttribute('data-theme-light', ''); + } + } + // Apply custom colors from config + if (this.config) { + const colorMap = { + 'color_solar': '--neon-yellow', + 'color_grid': '--neon-blue', + 'color_battery': '--neon-green', + 'color_export': '--export-color', + 'color_consumer_1': '--consumer-1-color', + 'color_consumer_2': '--consumer-2-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)) { + if (this.config[configKey]) { + this.style.setProperty(cssVar, this.config[configKey]); + } else { + this.style.removeProperty(cssVar); + } } } - }); - this._resizeObserver.observe(this); - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._resizeObserver) { - this._resizeObserver.disconnect(); } - } - static get styles() { - return css` + disconnectedCallback() { + super.disconnectedCallback(); + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + } + + static get styles() { + return css` :host { display: block; --neon-yellow: #ffdd00; @@ -772,10 +1174,45 @@ class PowerFluxCard extends LitElement { --neon-green: #00ff88; --neon-pink: #ff0080; --neon-red: #ff3333; - --grid-grey: #9e9e9e; --export-purple: #a855f7; + --export-color: #ff3333; + --consumer-1-color: #a855f7; + --consumer-2-color: #f97316; + --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; } + :host([data-theme-light]) { + --neon-yellow: #c8a800; + --neon-blue: #2563eb; + --neon-green: #059669; + --neon-pink: #db2777; + --neon-red: #dc2626; + --export-purple: #7c3aed; + --export-color: #dc2626; + --consumer-1-color: #7c3aed; + --consumer-2-color: #ea580c; + --consumer-3-color: #0891b2; + } ha-card { padding: 0; position: relative; @@ -829,7 +1266,7 @@ class PowerFluxCard extends LitElement { .compact-bar-wrapper { height: 36px; width: 100%; - background: #333; + background: var(--card-background-color, #333); border-radius: 5px; margin: 4px 0; display: flex; @@ -850,17 +1287,11 @@ class PowerFluxCard extends LitElement { overflow: hidden; } - /* Source Colors */ - .src-solar { background: var(--neon-green); color: black; } - .src-grid { background: var(--grid-grey); color: black; } - .src-battery { background: var(--neon-yellow); color: black; } - /* --- STANDARD VIEW STYLES --- */ .scale-wrapper { width: 420px; - transform-origin: top center; - transition: transform 0.1s linear; - margin: 0 auto; + transform-origin: top left; + transition: transform 0.1s linear; } .absolute-container { @@ -880,15 +1311,18 @@ class PowerFluxCard extends LitElement { z-index: 2; transition: all 0.3s ease; box-sizing: border-box; + cursor: pointer; } .bubble.tinted { background: rgba(255, 255, 255, 0.05); } .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.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); } + .bubble.grid.exporting { border-color: var(--export-color); } .bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); } - .bubble.tinted.c1 { background: color-mix(in srgb, #a855f7, transparent 85%); } - .bubble.tinted.c2 { background: color-mix(in srgb, #f97316, transparent 85%); } - .bubble.tinted.c3 { background: color-mix(in srgb, #06b6d4, 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.c3 { background: color-mix(in srgb, var(--consumer-3-color), transparent 85%); } .bubble.house { border-color: var(--neon-pink); } .bubble.house.tinted { background: color-mix(in srgb, var(--neon-pink), transparent 85%); } @@ -900,21 +1334,35 @@ class PowerFluxCard extends LitElement { -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; } + + .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 { - width: 34px; height: 34px; position: absolute; top: 13px; 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; } .icon-custom { --mdc-icon-size: 34px; } .sub { font-size: 9px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; - line-height: 1.1; z-index: 2; position: absolute; top: 48px; left: 0; width: 100%; text-align: center; margin: 0; pointer-events: none; + line-height: 1.1; z-index: 2; position: absolute; top: 46px; left: 0; width: 100%; text-align: center; margin: 0; pointer-events: none; + } + .sub.secondary-val { + text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 10px; } .value { font-weight: bold; font-size: 15px; white-space: nowrap; z-index: 2; transition: color 0.3s ease; line-height: 1.2; position: absolute; bottom: 11px; left: 0; width: 100%; text-align: center; margin: 0; } + .direction-arrow { font-size: 12px; margin-right: 0px; vertical-align: top; } @keyframes spin { 100% { transform: rotate(360deg); } } .spin-slow { animation: spin 12s linear infinite; transform-origin: center; } @@ -928,17 +1376,18 @@ class PowerFluxCard extends LitElement { .solar { border-color: var(--neon-yellow); } .battery { border-color: var(--neon-green); } .grid { border-color: var(--neon-blue); } - .c1 { border-color: #a855f7; } - .c2 { border-color: #f97316; } - .c3 { border-color: #06b6d4; } + .c1 { border-color: var(--consumer-1-color); } + .c2 { border-color: var(--consumer-2-color); } + .c3 { border-color: var(--consumer-3-color); } .inactive { border-color: var(--secondary-text-color); } - .glow.solar { box-shadow: 0 0 15px rgba(255, 221, 0, 0.4); } - .glow.battery { box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); } - .glow.grid { box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); } - .glow.c1 { box-shadow: 0 0 15px rgba(168, 85, 247, 0.4); } - .glow.c2 { box-shadow: 0 0 15px rgba(249, 115, 22, 0.4); } - .glow.c3 { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); } + .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.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.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%); } .node-solar { top: 70px; left: 5px; } .node-grid { top: 70px; left: 165px; } @@ -948,84 +1397,98 @@ class PowerFluxCard extends LitElement { .node-c2 { top: 370px; left: 165px; } .node-c3 { top: 370px; left: 325px; } - svg { position: absolute; top: 7; left: 25; 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-solar { stroke: var(--neon-yellow); } - .bg-grid { stroke: var(--neon-blue); } - .bg-battery { stroke: var(--neon-green); } - .bg-export { stroke: var(--neon-red); } - .bg-c1 { stroke: #a855f7; } - .bg-c2 { stroke: #f97316; } - .bg-c3 { stroke: #06b6d4; } + .bg-solar { stroke: var(--pipe-solar-color); } + .bg-grid { stroke: var(--pipe-grid-color); } + .bg-battery { stroke: var(--pipe-battery-color); } + .bg-export { stroke: var(--export-color); } + .bg-c1 { stroke: var(--pipe-consumer-1-color); } + .bg-c2 { stroke: var(--pipe-consumer-2-color); } + .bg-c3 { stroke: var(--pipe-consumer-3-color); } .flow-line { 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; } - .flow-solar { stroke: var(--neon-yellow); } - .flow-grid { stroke: var(--neon-blue); } - .flow-battery { stroke: var(--neon-green); } - .flow-export { stroke: var(--neon-red); } + .flow-solar { stroke: var(--pipe-solar-color); } + .flow-grid { stroke: var(--pipe-grid-color); } + .flow-battery { stroke: var(--pipe-battery-color); } + .flow-export { stroke: var(--export-color); } @keyframes dash { to { stroke-dashoffset: -1500; } } .flow-text { - font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.8)); transition: opacity 0.3s ease; + font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease; } .flow-text.no-shadow { filter: none; } - .text-solar { fill: var(--neon-yellow); } - .text-grid { fill: var(--neon-blue); } - .text-export { fill: var(--neon-red); } - .text-battery { fill: var(--neon-green); } + .text-solar { fill: var(--pipe-solar-color); } + .text-grid { fill: var(--pipe-grid-color); } + .text-export { fill: var(--export-color); } + .text-battery { fill: var(--pipe-battery-color); } `; - } + } - // --- SVG ICON RENDERER --- - _renderIcon(type, val = 0, colorOverride = null) { + // --- SVG ICON RENDERER --- + _renderIcon(type, val = 0, colorOverride = null) { if (type === 'solar') { - const animate = Math.round(val) > 0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; - return html``; + const animate = Math.round(val) > 0 ? 'spin-slow' : ''; + 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)'; - return html``; + const animate = Math.round(val) > 0 ? 'pulse' : ''; + 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 soc = Math.min(Math.max(val, 0), 100) / 100; + const rectHeight = 14 * soc; + const rectY = 18 - rectHeight; + 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)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-house-color)'; + return html``; } if (type === 'car') { - return html``; + const c = colorOverride || 'var(--icon-consumer-1-color)'; + return html``; } if (type === 'heater') { - return html``; + const c = colorOverride || 'var(--icon-consumer-2-color)'; + return html``; } if (type === 'pool') { - return html``; + const c = colorOverride || 'var(--icon-consumer-3-color)'; + return html``; } return html``; - } + } - _formatPower(val) { + _formatPower(val) { if (val === 0) return "0"; if (Math.abs(val) >= 1000) { - return (val / 1000).toFixed(1) + " kW"; + return (val / 1000).toFixed(1) + " kW"; } return Math.round(val) + " W"; - } + } - // --- DOM NODE SVG GENERATOR --- - _renderSVGPath(d, color) { + _getConsumerColor(index) { + const style = getComputedStyle(this); + 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"); path.setAttribute("d", d); path.setAttribute("class", "bracket-line"); @@ -1035,29 +1498,29 @@ class PowerFluxCard extends LitElement { path.style.stroke = color; path.style.fill = "none"; return path; - } + } - // --- SQUARE BRACKET GENERATOR --- - _createBracketPath(startPx, widthPx, direction) { + // --- SQUARE BRACKET GENERATOR --- + _createBracketPath(startPx, widthPx, direction) { if (widthPx < 5) return ""; - - const r = 5; + + const r = 5; const startX = startPx; const endX = startPx + widthPx; - + let yBase, yLine; - + if (direction === 'down') { - yBase = 24; - yLine = 4; + yBase = 24; + yLine = 4; } else { - yBase = 0; - yLine = 20; + yBase = 0; + yLine = 20; } const height = Math.abs(yBase - yLine); const rEff = Math.min(r, height / 2, widthPx / 2); - + const yCorner = direction === 'down' ? yLine + rEff : yLine - rEff; return ` @@ -1068,79 +1531,103 @@ class PowerFluxCard extends LitElement { Q ${endX} ${yLine} ${endX} ${yCorner} L ${endX} ${yBase} `; - } + } - // --- RENDER COMPACT VIEW --- - _renderCompactView(entities) { + // --- RENDER COMPACT VIEW --- + _renderCompactView(entities) { // 1. Get Values const getVal = (entity) => { - const state = this.hass.states[entity]; - return state ? parseFloat(state.state) || 0 : 0; + 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 gridMain = entities.grid ? getVal(entities.grid) : 0; + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (entities.grid ? getVal(entities.grid) : 0); const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; - const battery = entities.battery ? getVal(entities.battery) : 0; - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let battery = entities.battery ? getVal(entities.battery) : 0; + if (this.config.invert_battery) { + battery *= -1; + } + 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); // 2. Logic Calculation let gridImport = 0; let gridExport = 0; - if (entities.grid_export && entities.grid_export !== "") { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = Math.abs(gridExportSensor); + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExportSensor); } else { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; + gridImport = gridMain > 0 ? gridMain : 0; + 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; if (batteryCharge > 0) { + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); + } else { if (solar >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + solarToBatt = batteryCharge; + gridToBatt = 0; } else { - solarToBatt = solar; - gridToBatt = batteryCharge - solar; + solarToBatt = solar; + gridToBatt = batteryCharge - solar; } + } } const solarTotalToCons = Math.max(0, solar - solarToBatt - gridExport); const gridTotalToCons = Math.max(0, gridImport - gridToBatt); const battTotalToCons = batteryDischarge; - + const totalCons = solarTotalToCons + gridTotalToCons + battTotalToCons; - + // Calculate Splits let evPower = 0; let housePower = totalCons; - + if (c1Val > 0 && totalCons > 0) { - evPower = Math.min(c1Val, totalCons); - housePower = totalCons - evPower; + evPower = Math.min(c1Val, totalCons); + housePower = totalCons - evPower; } - + // Calculate Total Bar Width (Flux) // The Bar represents: Battery Discharge + Solar + Grid Import // This MUST equal: House + EV + Export + Battery Charge - + // SOURCES (for Bar Segments) const srcBattery = batteryDischarge; - const srcSolar = solar; // Solar includes Export + Charge + Cons - const srcGrid = gridImport; - + const srcSolar = solar; // Solar includes Export + Charge + Cons + const srcGrid = gridImport; + const totalFlux = srcBattery + srcSolar + srcGrid; - + // DESTINATIONS (for Bottom Brackets) - const destHouse = housePower; - const destEV = evPower; + const destHouse = housePower; + const destEV = evPower; const destExport = gridExport; // Note: Battery Charge is also a destination (internal flow), but usually not bracketed if we only want "Consumers" // If we don't bracket Charge, there will be a gap. We can accept that or add a Charge bracket. @@ -1148,10 +1635,10 @@ class PowerFluxCard extends LitElement { const threshold = 0.1; const availableWidth = (this._cardWidth && this._cardWidth > 0) ? this._cardWidth : (this.offsetWidth || 400); - const fullWidth = availableWidth - 40; + const fullWidth = availableWidth - 40; if (totalFlux <= threshold) { - return html`
Waiting for data...
`; + return html`
Waiting for data...
`; } // --- GENERATE BAR SEGMENTS (Aggregated by Source) --- @@ -1159,36 +1646,37 @@ class PowerFluxCard extends LitElement { const barSegments = []; let currentX = 0; - const addSegment = (val, color, type, label) => { - if (val <= threshold) return; - const pct = val / totalFlux; - const width = pct * fullWidth; - barSegments.push({ - val, - color, - widthPct: pct * 100, - widthPx: width, - startPx: currentX, - type, - label - }); - currentX += width; + const addSegment = (val, color, type, label, entityId) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; + barSegments.push({ + val, + color, + widthPct: pct * 100, + widthPx: width, + startPx: currentX, + type, + label, + entityId + }); + currentX += width; } - addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery'); - addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar'); - addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid'); + addSegment(srcBattery, 'var(--neon-green)', 'battery', 'battery', entities.battery); + addSegment(srcSolar, 'var(--neon-yellow)', 'solar', 'solar', entities.solar); + addSegment(srcGrid, 'var(--neon-blue)', 'grid', 'grid', entities.grid_combined || entities.grid); // --- GENERATE TOP BRACKETS (Based on Bar Segments) --- const topBrackets = barSegments.map(s => { - const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); - let icon = ''; - let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-green)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } - - return { path, width: s.widthPx, center: s.startPx + (s.widthPx/2), icon, iconColor }; + const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); + let icon = ''; + let iconColor = ''; + 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 }; }); // --- GENERATE BOTTOM BRACKETS (Independent Calculation) --- @@ -1196,32 +1684,36 @@ class PowerFluxCard extends LitElement { const bottomBrackets = []; let bottomX = 0; - const addBottomBracket = (val, type) => { - if (val <= threshold) return; - const pct = val / totalFlux; - const width = pct * fullWidth; - - let icon = ''; - let iconColor = ''; - - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } + const addBottomBracket = (val, type, entityId = null) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; - const path = this._createBracketPath(bottomX, width, 'up'); - bottomBrackets.push({ - path, - width: width, - center: bottomX + (width/2), - icon, - iconColor - }); - bottomX += width; + let icon = ''; + let iconColor = ''; + + 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({ + path, + width: width, + center: bottomX + (width / 2), + icon, + iconColor, + val, + entityId + }); + bottomX += width; }; - addBottomBracket(destHouse, 'house'); - addBottomBracket(destEV, 'car'); - addBottomBracket(destExport, 'export'); + addBottomBracket(destHouse, 'house', entities.house); + addBottomBracket(destEV, 'car', entities.consumer_1); + addBottomBracket(destExport, 'export', entities.grid_combined || entities.grid_export || entities.grid); + addBottomBracket(batteryCharge, 'battery', entities.battery); // Note: If there is Battery Charging happening, bottomX will not reach fullWidth. // This leaves a gap at the end (or between segments depending on logic), which is visually correct @@ -1236,18 +1728,29 @@ class PowerFluxCard extends LitElement { ${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${topBrackets.map(b => b.width > 20 ? html` -
+
b.entityId && this._handleClick(b.entityId)}>
` : '')}
- ${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) : ''}
- `)} + `})}
@@ -1256,313 +1759,436 @@ class PowerFluxCard extends LitElement { ${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${bottomBrackets.map(b => b.width > 20 ? html` -
+
b.entityId && this._handleClick(b.entityId)}>
` : '')}
`; - } - - // --- RENDER STANDARD VIEW --- - _renderStandardView(entities) { - // FIX: Default to hidden unless explicitly set to false - const hideInactive = this.config.hide_inactive_flows !== false; - - const globalFlowRate = this.config.show_flow_rates !== false; - - // FLOW RATE TOGGLES - const showFlowSolar = this.config.show_flow_rate_solar !== undefined ? this.config.show_flow_rate_solar : globalFlowRate; - const showFlowGrid = this.config.show_flow_rate_grid !== undefined ? this.config.show_flow_rate_grid : globalFlowRate; - const showFlowBattery = this.config.show_flow_rate_battery !== undefined ? this.config.show_flow_rate_battery : globalFlowRate; - - // LABEL TOGGLES - const showLabelSolar = this.config.show_label_solar === true; - const showLabelGrid = this.config.show_label_grid === true; - const showLabelBattery = this.config.show_label_battery === true; - const showLabelHouse = this.config.show_label_house === true; - - const useColoredValues = this.config.use_colored_values === true; - const showDonut = this.config.show_donut_border === true; - const showTail = this.config.show_comet_tail === true; - const showDashedLine = this.config.show_dashed_line === true; - const showTint = this.config.show_tinted_background === true; - const hideConsumerIcons = this.config.hide_consumer_icons === true; - const showNeonGlow = this.config.show_neon_glow !== false; - - // CUSTOM LABELS - const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); - const labelGridText = this.config.grid_label || this._localize('card.label_import'); - const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); - const labelHouseText = this.config.house_label || this._localize('card.label_house'); - - // CUSTOM ICONS - const iconSolar = this.config.solar_icon; - const iconGrid = this.config.grid_icon; - const iconBattery = this.config.battery_icon; - - // Determine existence of main entities - const hasSolar = !!(entities.solar && entities.solar !== ""); - const hasGrid = !!(entities.grid && entities.grid !== ""); - const hasBattery = !!(entities.battery && entities.battery !== ""); - - const styleSolar = hasSolar ? '' : 'display: none;'; - const styleSolarBatt = (hasSolar && hasBattery) ? '' : 'display: none;'; - const styleGrid = hasGrid ? '' : 'display: none;'; - const styleGridBatt = (hasGrid && hasBattery) ? '' : 'display: none;'; - const styleBattery = hasBattery ? '' : 'display: none;'; - - 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 getVal = (entity) => { - const state = this.hass.states[entity]; - return state ? parseFloat(state.state) || 0 : 0; - }; - - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 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 anyBottomVisible = showC1 || showC2 || showC3; - - const solar = hasSolar ? getVal(entities.solar) : 0; - const gridMain = hasGrid ? getVal(entities.grid) : 0; - const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; - const battery = hasBattery ? getVal(entities.battery) : 0; - const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; - - const solarVal = Math.max(0, solar); - - let gridImport = 0; - let gridExport = 0; - - if (hasGrid) { - if (entities.grid_export && entities.grid_export !== "") { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = Math.abs(gridExpSensor); - } else { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; - } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // --- RENDER STANDARD VIEW --- + _renderStandardView(entities) { + // FIX: Default to hidden unless explicitly set to false + const hideInactive = this.config.hide_inactive_flows !== false; - let solarToBatt = 0; - let gridToBatt = 0; + const globalFlowRate = this.config.show_flow_rates !== false; - if (hasBattery && batteryCharge > 0) { - if (solarVal >= batteryCharge) { + // FLOW RATE TOGGLES + const showFlowSolar = this.config.show_flow_rate_solar !== undefined ? this.config.show_flow_rate_solar : globalFlowRate; + const showFlowGrid = this.config.show_flow_rate_grid !== undefined ? this.config.show_flow_rate_grid : globalFlowRate; + const showFlowBattery = this.config.show_flow_rate_battery !== undefined ? this.config.show_flow_rate_battery : globalFlowRate; + + // LABEL TOGGLES + const showLabelSolar = this.config.show_label_solar === true; + const showLabelGrid = this.config.show_label_grid === true; + const showLabelBattery = this.config.show_label_battery === true; + const showLabelHouse = this.config.show_label_house === true; + + const useColoredValues = this.config.use_colored_values === true; + const showDonut = this.config.show_donut_border === true; + const showTail = this.config.show_comet_tail === true; + const showDashedLine = this.config.show_dashed_line === true; + const showTint = this.config.show_tinted_background === true; + const hideConsumerIcons = this.config.hide_consumer_icons === true; + const showNeonGlow = this.config.show_neon_glow !== false; + + // CUSTOM LABELS + const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); + const labelGridText = this.config.grid_label || this._localize('card.label_grid'); + const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); + const labelHouseText = this.config.house_label || this._localize('card.label_house'); + + // CUSTOM ICONS + const iconSolar = this.config.solar_icon; + const iconGrid = this.config.grid_icon; + const iconBattery = this.config.battery_icon; + + // SECONDARY SENSORS (display only) + const hasSecondarySolar = !!(entities.secondary_solar && entities.secondary_solar !== ""); + const hasSecondaryGrid = !!(entities.secondary_grid && entities.secondary_grid !== ""); + const hasSecondaryBattery = !!(entities.secondary_battery && entities.secondary_battery !== ""); + + const getSecondaryVal = (entity) => { + if (!entity) return ''; + const state = this.hass.states[entity]; + if (!state) return ''; + const val = parseFloat(state.state); + if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); + const unit = state.attributes.unit_of_measurement || ''; + if (unit === 'W') { 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 + const hasSolar = !!(entities.solar && entities.solar !== ""); + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const hasGrid = !!(entities.grid && entities.grid !== "") || hasGridCombined; + const hasBattery = !!(entities.battery && entities.battery !== ""); + + const styleSolar = hasSolar ? '' : 'display: none;'; + const styleSolarBatt = (hasSolar && hasBattery) ? '' : 'display: none;'; + const styleGrid = hasGrid ? '' : 'display: none;'; + const styleGridBatt = (hasGrid && hasBattery) ? '' : 'display: none;'; + const styleBattery = hasBattery ? '' : 'display: none;'; + + const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; + + // Custom Labels for Consumers + 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 ? 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 ? 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 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); + const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; + let battery = hasBattery ? getVal(entities.battery) : 0; + if (this.config.invert_battery) { + battery *= -1; + } + const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; + + const solarVal = Math.max(0, solar); + + let gridImport = 0; + let gridExport = 0; + + if (hasGrid) { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExpSensor); + } else { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = gridMain < 0 ? Math.abs(gridMain) : 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; + + if (hasBattery && batteryCharge > 0) { + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + // Use dedicated grid-to-battery sensor + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); + } else { + // Calculate: solar prioritized + if (solarVal >= batteryCharge) { solarToBatt = batteryCharge; gridToBatt = 0; - } else { - solarToBatt = solarVal; + } else { + solarToBatt = solarVal; gridToBatt = batteryCharge - solarVal; + } } - } + } - const solarToHouse = Math.max(0, solarVal - solarToBatt - gridExport); - const gridToHouse = Math.max(0, gridImport - gridToBatt); - const house = solarToHouse + gridToHouse + batteryDischarge; + const solarToHouse = Math.max(0, solarVal - solarToBatt - gridExport); + const gridToHouse = Math.max(0, gridImport - gridToBatt); + const house = solarToHouse + gridToHouse + batteryDischarge; - const isTopArcActive = (solarToBatt > 0); - const topShift = isTopArcActive ? 0 : 50; - let baseHeight = anyBottomVisible ? 480 : 340; - const contentHeight = baseHeight - topShift; + // Use house entity for display if defined, otherwise use calculated value + const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house; - const designWidth = 420; - const availableWidth = this._cardWidth || designWidth; - let scale = availableWidth / designWidth; - const userZoom = this.config.zoom !== undefined ? this.config.zoom : 0.9; - scale = scale * userZoom; + const isTopArcActive = (solarToBatt > 0); + const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; + let baseHeight = anyBottomVisible ? 480 : 340; + const contentHeight = baseHeight - topShift; - if (scale < 0.5) scale = 0.5; - if (scale > 1.5) scale = 1.5; + const designWidth = 420; + const availableWidth = this._cardWidth || designWidth; + let scale = availableWidth / designWidth; + const userZoom = this.config.zoom !== undefined ? this.config.zoom : 0.9; + scale = scale * userZoom; - const finalCardHeightPx = contentHeight * scale; + if (scale < 0.5) scale = 0.5; + if (scale > 1.5) scale = 1.5; - let houseGradientVal = ''; - let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; - const tintClass = showTint ? 'tinted' : ''; - const glowClass = showNeonGlow ? 'glow' : ''; - - let houseDominantColor = 'var(--neon-pink)'; - if (house > 0) { + const finalCardHeightPx = contentHeight * scale; + const visualWidth = 420 * scale; + const centerMarginLeft = Math.max(0, (availableWidth - visualWidth) / 2); + + let houseGradientVal = ''; + let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + const tintClass = showTint ? 'tinted' : ''; + const glowClass = showNeonGlow ? 'glow' : ''; + + let houseDominantColor = 'var(--neon-pink)'; + if (house > 0) { if (solarToHouse >= gridToHouse && solarToHouse >= batteryDischarge) { - houseDominantColor = 'var(--neon-yellow)'; + houseDominantColor = 'var(--neon-yellow)'; } else if (gridToHouse >= solarToHouse && gridToHouse >= batteryDischarge) { - houseDominantColor = 'var(--neon-blue)'; + houseDominantColor = 'var(--neon-blue)'; } else if (batteryDischarge >= solarToHouse && batteryDischarge >= gridToHouse) { - houseDominantColor = 'var(--neon-green)'; + houseDominantColor = 'var(--neon-green)'; } - } + } - if (showDonut) { + if (showDonut) { if (house > 0) { - const pctSolar = (solarToHouse / house) * 100; - const pctGrid = (gridToHouse / house) * 100; - const pctBatt = (batteryDischarge / house) * 100; - - let stops = []; - let current = 0; - if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } - if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } - if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } - if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } - - houseGradientVal = `conic-gradient(${stops.join(', ')})`; + const pctSolar = (solarToHouse / house) * 100; + const pctGrid = (gridToHouse / house) * 100; + const pctBatt = (batteryDischarge / house) * 100; - if (useColoredValues) { - const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); - if (maxVal > 0) { - if (maxVal === solarToHouse) houseTextCol = 'var(--neon-yellow)'; - else if (maxVal === gridToHouse) houseTextCol = 'var(--neon-blue)'; - else if (maxVal === batteryDischarge) houseTextCol = 'var(--neon-green)'; - } else { - houseTextCol = 'var(--neon-pink)'; - } + let stops = []; + let current = 0; + if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } + if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } + if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } + if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } + + houseGradientVal = `conic-gradient(from 330deg, ${stops.join(', ')})`; + + if (useColoredValues) { + const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); + if (maxVal > 0) { + if (maxVal === solarToHouse) houseTextCol = 'var(--neon-yellow)'; + else if (maxVal === gridToHouse) houseTextCol = 'var(--neon-blue)'; + else if (maxVal === batteryDischarge) houseTextCol = 'var(--neon-green)'; + } else { + houseTextCol = 'var(--neon-pink)'; } + } } else { - houseGradientVal = `var(--neon-pink)`; - houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + houseGradientVal = `var(--neon-pink)`; + houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; } - } else { + } else { houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; - } + } - const houseTintStyle = showTint - ? `background: color-mix(in srgb, ${houseDominantColor}, transparent 85%);` + const houseTintStyle = showTint + ? `background: color-mix(in srgb, ${houseDominantColor}, transparent 85%);` : ''; - - const houseGlowStyle = showNeonGlow + + const houseGlowStyle = showNeonGlow ? `box-shadow: 0 0 15px color-mix(in srgb, ${houseDominantColor}, transparent 60%);` : `box-shadow: none;`; - const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; + const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; - const isSolarActive = Math.round(solarVal) > 0; - const isGridActive = Math.round(gridImport) > 0; + const isSolarActive = Math.round(solarVal) > 0; + const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; + const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'; - - const getAnimStyle = (val) => { - if (val <= 1) return "opacity: 0;"; - const userMinDuration = 7; - const userMaxDuration = 11; - const userFactor = 20000; - let duration = userFactor / val; - duration = Math.max(userMinDuration, Math.min(userMaxDuration, duration)); - - // Adjust speed for dashed line (Factor to slow down: 5x) - if (showDashedLine) { - duration = duration * 5; + // --- 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)'; } - - return `opacity: 1; animation-duration: ${duration}s;`; - }; + } - const getPipeStyle = (val) => { - if (!hideInactive) return "opacity: 0.2;"; - return val > 1 ? "opacity: 0.2;" : "opacity: 0;"; - }; + 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 getTextStyle = (val, type) => { + const getAnimStyle = (val) => { + if (val <= 1) return "opacity: 0;"; + + // --- Dynamic speed based on power --- + // Higher power = faster animation (shorter duration) + // Range: 2s (very fast, ~5000W+) to 12s (slow, ~50W) + const minDuration = 4; + const maxDuration = 12; + const factor = 12000; + let duration = factor / val; + duration = Math.max(minDuration, Math.min(maxDuration, duration)); + + // --- Dynamic particle density based on power --- + // Higher power = more/denser particles (shorter gap) + // Lower power = fewer/sparse particles (longer gap) + let dashSize, gapSize; + if (showTail) { + // Comet tail: vary tail length with power + dashSize = Math.round(15 + (val / 200) * 25); // 15-40 + dashSize = Math.min(dashSize, 40); + gapSize = Math.round(380 - (val / 200) * 200); // 380-180 + gapSize = Math.max(gapSize, 180); + } else if (showDashedLine) { + // Dashed line: vary dash density + dashSize = Math.round(8 + (val / 500) * 10); // 8-18 + dashSize = Math.min(dashSize, 18); + gapSize = Math.round(18 - (val / 1000) * 10); // 18-8 + gapSize = Math.max(gapSize, 8); + duration = duration * 5; // Dashed lines are slower + } else { + // Default dots: vary dot count/density + dashSize = 0; // stays as dots + gapSize = Math.round(380 - (val / 200) * 250); // 380-130 + gapSize = Math.max(gapSize, 130); + } + + const dynamicDash = `${dashSize} ${gapSize}`; + + return `opacity: 1; animation-duration: ${duration}s; stroke-dasharray: ${dynamicDash};`; + }; + + const getPipeStyle = (val) => { + if (!hideInactive) return "opacity: 0.2;"; + return val > 1 ? "opacity: 0.2;" : "opacity: 0;"; + }; + + const getTextStyle = (val, type) => { let isVisible = false; if (type === 'solar') isVisible = showFlowSolar; else if (type === 'grid') isVisible = showFlowGrid; else if (type === 'battery') isVisible = showFlowBattery; - - if (!isVisible) return "display: none;"; - return val > 5 ? "opacity: 1;" : "opacity: 0;"; - }; - const getColorStyle = (colorVar) => { + if (!isVisible) return "display: none;"; + return val > 5 ? "opacity: 1;" : "opacity: 0;"; + }; + + const getColorStyle = (colorVar) => { return useColoredValues ? `color: var(${colorVar});` : ''; - }; - const getConsumerColorStyle = (hex) => { + }; + const getConsumerColorStyle = (hex) => { return useColoredValues ? `color: ${hex};` : ''; - } + } - const renderLabel = (text, isVisible) => { + const renderLabel = (text, isVisible) => { if (!isVisible) return html``; return html`
${text}
`; - }; + }; - const renderMainIcon = (type, val, customIcon, color = null) => { + const renderSecondaryOrLabel = (labelText, showLabel, secondaryEntity, hasSecondary) => { + if (hasSecondary) { + const secVal = getSecondaryVal(secondaryEntity); + return html`
${secVal}
`; + } + return renderLabel(labelText, showLabel); + }; + + const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); - return html``; + 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) => { + 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``; + iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { - iconContent = this._renderIcon(iconType, val); + iconContent = this._renderIcon(iconType, val); } + 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} - ${renderLabel(label, true)} -
${this._formatPower(val)}
+ ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} +
${this._formatPower(val)}
`; - }; + }; - const getConsumerPipeStyle = (isActive, val) => { + const getConsumerPipeStyle = (isActive, val) => { if (!isActive) return "display: none;"; return getPipeStyle(val); - }; - - const getConsumerAnimStyle = (isActive, val) => { + }; + + const getConsumerAnimStyle = (isActive, val) => { if (!isActive) return "display: none;"; return getAnimStyle(val); - }; + }; - const pathSolarHouse = "M 50 160 Q 50 265 165 265"; - const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; - const pathGridImport = "M 210 160 L 210 220"; - const pathGridExport = "M 165 115 Q 130 145 95 115"; - const 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 pathSolarHouse = "M 50 160 Q 50 265 165 265"; + const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; + const pathGridImport = "M 210 160 L 210 220"; + const pathGridExport = "M 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 dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); - const strokeWidthVal = showDashedLine ? 4 : 8; + 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; - return html` + return html` -
+
@@ -1571,33 +2197,33 @@ class PowerFluxCard extends LitElement { - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -1605,56 +2231,65 @@ class PowerFluxCard extends LitElement { ${hasSolar ? html` -
+
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} - ${renderLabel(labelSolarText, showLabelSolar)} -
${this._formatPower(solarVal)}
+ ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} +
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
- ${renderMainIcon('grid', gridImport, iconGrid, gridColor)} - ${renderLabel(labelGridText, showLabelGrid)} -
${this._formatPower(gridImport)}
+
this._handleClick(entities.grid_combined || entities.grid)}> + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, 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)} - ${renderLabel(labelBatteryText, showLabelBattery)} -
${Math.round(battSoc)}%
+ ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} +
${Math.round(battSoc)}%
` : ''} -
- ${renderMainIcon('house', 0, null, houseDominantColor)} +
this._handleClick(entities.house)}> + ${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, '#a855f7')} - ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, '#f97316')} - ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, '#06b6d4')} + ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} + ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, this._getConsumerColor(2))} + ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, this._getConsumerColor(3))}
`; - } + } - render() { - if (!this.config || !this.hass) return html``; - - // SWITCH VIEW BASED ON CONFIG - if (this.config.compact_view === true) { + render() { + if (!this.config || !this.hass) return html``; + + // SWITCH VIEW BASED ON CONFIG + if (this.config.compact_view === true) { return this._renderCompactView(this.config.entities || {}); - } else { + } else { return this._renderStandardView(this.config.entities || {}); + } } } -} -customElements.define("power-flux-card", PowerFluxCard); + customElements.define("power-flux-card", PowerFluxCard); +})(lang_en, lang_de); window.customCards = window.customCards || []; window.customCards.push({ diff --git a/docs/README-de.md b/docs/README-de.md index 71ad75d..8f75370 100644 --- a/docs/README-de.md +++ b/docs/README-de.md @@ -9,10 +9,12 @@ # Power Flux Card -Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und flüssigen Animationen. +Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Home Assistant. Sie visualisiert die Energieverteilung zwischen Solar, Netz, Batterie und Verbrauchern mit wunderschönen Neon-Effekten und verschiedenen Animationen. -power-flux-card -power-flux-card +Wenn euch die custom Card gefällt, würde ich mich sehr über eine Sternebewertung ⭐ freuen. 🤗 + +power-flux-card power-flux-card +power-flux-card power-flux-card ### ✨ Funktionen @@ -24,9 +26,21 @@ Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Ho - **Donut Chart**: Optionales Donut-Diagramm um das Haus-Icon, das den Energiemix zeigt. - **Kometenschweif / Gestrichelte Linien**: Wählen Sie Ihren bevorzugten Animationsstil. - **Zoom**: Anpassbare Größe für Ihr Dashboard. + - **Benutzerdefinierte Farben**: Definiere benutzerdefinierte Farben für jede Quelle und jeden Verbraucher über den Editor. + - **Hintergrundfarbe**: Aktiviere einen leicht getönten Hintergrund für die Kreise in der Standard-Ansicht. +- **Dynamische Animationsgeschwindigkeit**: Partikelgeschwindigkeit und -dichte passen sich dem aktuellen Energiefluss an. +- **Weitere Informationen**: Klicke auf eine beliebige Quelle/Verbraucher, um detaillierte Informationen in einem More-Info-Dialog anzuzeigen. +- **Netz-Import/Export**: Unterstützt sowohl separate Import/Export-Entitäten als auch eine kombinierte Entität mit positiven/negativen Werten. +- **Netz-zu-Batterie**: Optionaler direkter Sensor für den Netz-zu-Batterie-Fluss, der die Standardberechnung umgeht. +- **Sekundäre Sensoren**: Optional können sekundäre Sensorwerte in den Hauptkreisen (z.B. Tagesertrag für Solar, aktuelle Lade-/Entladeleistung für Batterie) angezeigt werden. - **Lokalisierung**: Vollständig übersetzt in Deutsch und Englisch. - **Visueller Editor**: Einfache Konfiguration über die Home Assistant UI. +[![Watch the video](https://img.youtube.com/vi/HGFBJJRWGW0/0.jpg)](https://www.youtube.com/watch?v=HGFBJJRWGW0 +) + +--- + ### 🚀 Installation ### HACS (Empfohlen) @@ -35,25 +49,28 @@ Die ⚡Power Flux Card ist eine erweiterte, animierte Energiefluss-Karte für Ho [![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=jayjojayson&repository=power-flux-card&category=plugin) -- Das "Detailed Charts Panel" sollte nun in HACS verfügbar sein. Klicke auf "INSTALLIEREN" ("INSTALL"). +- Das "Power Flux Card" sollte nun in HACS verfügbar sein. Klicke auf "INSTALLIEREN" ("INSTALL"). - Die Ressource wird automatisch zu deiner Lovelace-Konfiguration hinzugefügt. -- + #### HACS (manuell) -1. Stellen Sie sicher, dass HACS installiert ist. -2. Fügen Sie dieses Repository als benutzerdefiniertes Repository in HACS hinzu. -3. Suchen Sie nach "Power Flux Card" und installieren Sie es. -4. Laden Sie die Ressourcen neu, falls Sie dazu aufgefordert werden. +1. Stelle sicher, dass HACS installiert ist. +2. Füge dieses Repository als benutzerdefiniertes Repository in HACS hinzu. +3. Suche nach "Power Flux Card" und installieren Sie es. +4. Lade die Ressourcen neu, falls Sie dazu aufgefordert werden. #### Manuelle Installation -1. Laden Sie die Datei `power-flux-card.js` von der [Releases](../../releases)-Seite herunter. -2. Laden Sie sie in Ihren `www`-Ordner in Home Assistant hoch. -3. Fügen Sie die Ressource in Ihrer Dashboard-Konfiguration hinzu: - - URL: `/local/power-flux-card.js` +1. Lade die Datei `power-flux-card.js` von der [Releases](../../releases)-Seite herunter. +2. Lade sie in Ihren `www/community/power-flux-card/`-Ordner in Home Assistant hoch. +3. Füge die Ressource in Ihrer Dashboard-Konfiguration hinzu: + - URL: `/local/community/power-flux-card/power-flux-card.js` - Typ: JavaScript Module + +--- + ### ⚙️ Konfiguration -Sie können die Karte direkt über den visuellen Editor in Home Assistant konfigurieren. +Du kannst die Karte direkt über den visuellen Editor in Home Assistant konfigurieren. **Haupt-Entitäten:** - **Solar**: Erzeugung (W). @@ -61,7 +78,7 @@ Sie können die Karte direkt über den visuellen Editor in Home Assistant konfig - **Batterie**: Batterieleistung (W) und Ladestand (%). **Zusätzliche Verbraucher:** -- Sie können bis zu 3 individuelle Verbraucher (z.B. Auto, Heizung, Pool) mit eigenen Icons und Beschriftungen hinzufügen. +- Du kannst bis zu 3 individuelle Verbraucher (z.B. Auto, Heizung, Pool) mit eigenen Icons und Beschriftungen hinzufügen. **Optionen:** - **Zoom**: Passen Sie die Größe der Karte an. @@ -69,3 +86,151 @@ Sie können die Karte direkt über den visuellen Editor in Home Assistant konfig - **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/docs/images/power-flux-card-ani.gif b/docs/images/power-flux-card-ani.gif new file mode 100644 index 0000000..cb3a364 Binary files /dev/null and b/docs/images/power-flux-card-ani.gif differ diff --git a/docs/images/power-flux-card-compact.jpg b/docs/images/power-flux-card-compact.jpg index 1c9f043..19fafd0 100644 Binary files a/docs/images/power-flux-card-compact.jpg and b/docs/images/power-flux-card-compact.jpg differ diff --git a/docs/images/power-flux-card-compact2.jpg b/docs/images/power-flux-card-compact2.jpg new file mode 100644 index 0000000..04dc763 Binary files /dev/null and b/docs/images/power-flux-card-compact2.jpg differ diff --git a/src/lang-de.js b/src/lang-de.js index 332556d..8e7845b 100644 --- a/src/lang-de.js +++ b/src/lang-de.js @@ -9,12 +9,47 @@ export default { "editor.consumers_section": "Zusätzliche Verbraucher", "editor.options_section": "Darstellung & Optionen", "editor.flow_rate_title": "Flussraten (W) an Röhren anzeigen", + "editor.invert_battery": "Wert umkehren (+/-)", "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 (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)", + "editor.zoom_label": "🔍 Zoom (Standard View)", + "editor.neon_glow": "Neon Glow", + "editor.donut_chart": "Donut Chart (Grid/Haus)", + "editor.comet_tail": "Comet Tail Effect", + "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Farbiger Hintergrund in Kreisen", + "editor.colored_values": "Farbige Textwerte", + "editor.hide_consumer_icons": "Icons unten ausblenden", + "editor.invert_consumer_1": "Sensorwert invertieren (+/-)", + "editor.secondary_sensor": "Zweiter Sensor (nur Anzeige)", + "editor.grid_to_battery_sensor": "Netz-zu-Batterie Sensor (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 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", @@ -22,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 bde372d..46ae1ca 100644 --- a/src/lang-en.js +++ b/src/lang-en.js @@ -9,12 +9,47 @@ export default { "editor.consumers_section": "Additional Consumers", "editor.options_section": "Appearance & Options", "editor.flow_rate_title": "Show Flow Rates (W) on pipes", + "editor.invert_battery": "Invert Power Value (+/-)", "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 (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)", + "editor.zoom_label": "🔍 Zoom (Standard View)", + "editor.neon_glow": "Neon Glow", + "editor.donut_chart": "Donut Chart (Grid/House)", + "editor.comet_tail": "Comet Tail Effect", + "editor.dashed_line": "Dashed Line Effect", + "editor.tinted_background": "Tinted Background in Bubbles", + "editor.colored_values": "Colored Text Values", + "editor.hide_consumer_icons": "Hide Consumer Icons", + "editor.invert_consumer_1": "Invert Sensor Value (+/-)", + "editor.secondary_sensor": "Secondary Sensor (display only)", + "editor.grid_to_battery_sensor": "Grid to Battery Sensor (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 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", @@ -22,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 84ed4d9..7702902 100644 --- a/src/power-flux-card-editor.js +++ b/src/power-flux-card-editor.js @@ -1,14 +1,23 @@ +import lang_en from "./lang-en.js"; +import lang_de from "./lang-de.js"; + +const editorTranslations = { + "en": lang_en.editor, + "de": lang_de.editor +}; + + const fireEvent = (node, type, detail, options) => { - options = options || {}; - detail = detail === null || detail === undefined ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed, - }); - event.detail = detail; - node.dispatchEvent(event); - return event; + options = options || {}; + detail = detail === null || detail === undefined ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed, + }); + event.detail = detail; + node.dispatchEvent(event); + return event; }; const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); @@ -16,83 +25,174 @@ const html = LitElement.prototype.html; const css = LitElement.prototype.css; class PowerFluxCardEditor extends LitElement { - - static get properties() { - return { - hass: {}, - _config: { state: true }, - _subView: { state: true } // Controls which sub-page is open (null = main) - }; - } - setConfig(config) { - this._config = config; - } - - _localize(key) { - const lang = this.hass && this.hass.language ? this.hass.language : 'en'; - const dict = editorTranslations[lang] || editorTranslations['en']; - return dict[key] || editorTranslations['en'][key] || key; - } - - _valueChanged(ev) { - if (!this._config || !this.hass) return; - - const target = ev.target; - const key = target.configValue || this._currentConfigValue; - - let value; - if (target.tagName === 'HA-SWITCH') { - value = target.checked; - } else if (ev.detail && 'value' in ev.detail) { - value = ev.detail.value; - } else { - value = target.value; - } - - if (value === null || value === undefined) { - value = ""; + static get properties() { + return { + hass: {}, + _config: { state: true }, + _subView: { state: true } // Controls which sub-page is open (null = main) + }; } - if (key) { - const entityKeys = [ - 'solar', 'grid', 'grid_export', - 'battery', 'battery_soc', - 'consumer_1', 'consumer_2', 'consumer_3' - ]; + setConfig(config) { + this._config = config; + } - let newConfig = { ...this._config }; + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = editorTranslations[lang] || editorTranslations['en']; + return dict[key] || editorTranslations['en'][key] || key; + } - if (entityKeys.includes(key)) { - const currentEntities = newConfig.entities || {}; - const newEntities = { ...currentEntities, [key]: value }; - newConfig.entities = newEntities; + _valueChanged(ev) { + if (!this._config || !this.hass) return; + + const target = ev.target; + const key = target.configValue; + + let value; + if (target.tagName === 'HA-SWITCH') { + value = target.checked; + } else if (ev.detail && 'value' in ev.detail) { + value = ev.detail.value; } else { - newConfig[key] = value; - - if (key === 'show_comet_tail' && value === true) { - newConfig.show_dashed_line = false; - } - if (key === 'show_dashed_line' && value === true) { - newConfig.show_comet_tail = false; - } + value = target.value; } + if (value === null || value === undefined) { + value = ""; + } + + if (key) { + 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', + 'secondary_consumer_1', 'secondary_consumer_2', 'secondary_consumer_3' + ]; + + let newConfig = { ...this._config }; + + if (entityKeys.includes(key)) { + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: value }; + newConfig.entities = newEntities; + } else { + newConfig[key] = value; + + if (key === 'show_comet_tail' && value === true) { + newConfig.show_dashed_line = false; + } + if (key === 'show_dashed_line' && value === true) { + newConfig.show_comet_tail = false; + } + } + + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } + } + + _goSubView(view) { + this._subView = view; + } + + _goBack() { + this._subView = null; + } + + _clearEntity(key) { + const newConfig = { ...this._config }; + const currentEntities = newConfig.entities || {}; + const newEntities = { ...currentEntities, [key]: "" }; + newConfig.entities = newEntities; this._config = newConfig; fireEvent(this, "config-changed", { config: this._config }); } - } - _goSubView(view) { - this._subView = view; - } + _colorChanged(key, ev) { + const newConfig = { ...this._config, [key]: ev.target.value }; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } - _goBack() { - this._subView = null; - } + _resetColor(key) { + const newConfig = { ...this._config }; + delete newConfig[key]; + this._config = newConfig; + fireEvent(this, "config-changed", { config: this._config }); + } - static get styles() { - return css` + _renderEntitySelector(entitySelectorSchema, value, configValue, label) { + const val = value || ""; + return html` +
+ + ${val ? html` this._clearEntity(configValue)} + >` : ''} +
+ `; + } + + _renderColorPicker(key, label, defaultColor) { + const currentColor = this._config[key] || defaultColor; + const hasCustom = !!this._config[key]; + return html` +
+ this._colorChanged(key, e)}> + ${label} + ${hasCustom ? html` this._resetColor(key)}>` : ''} +
+ `; + } + + _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 { display: flex; flex-direction: column; @@ -169,28 +269,82 @@ class PowerFluxCardEditor extends LitElement { border-bottom: 1px solid var(--divider-color); margin: 10px 0; } + .entity-picker-wrapper { + position: relative; + display: flex; + align-items: center; + gap: 4px; + } + .entity-picker-wrapper ha-selector { + flex: 1; + } + .clear-entity-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + flex-shrink: 0; + margin-top: -12px; + } + .clear-entity-btn:hover { + color: var(--error-color, #db4437); + } + .color-picker-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + } + .color-picker-row input[type="color"] { + -webkit-appearance: none; + border: 2px solid var(--divider-color); + border-radius: 50%; + width: 30px; + height: 30px; + padding: 2px; + cursor: pointer; + background: transparent; + } + .color-picker-row input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; + } + .color-picker-row input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 50%; + } + .color-label { + flex: 1; + font-size: 14px; + } + .color-reset-btn { + --mdc-icon-size: 20px; + color: var(--secondary-text-color); + cursor: pointer; + } + .color-reset-btn:hover { + color: var(--primary-color); + } + .color-picker-quad { + display: flex; + gap: 8px; + } + .color-picker-quad .color-picker-row { + flex: 1; + } `; - } + } - // --- SUBVIEW RENDERING --- + // --- SUBVIEW RENDERING --- - _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.solar_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.solar, 'solar', this._localize('editor.entity'))}
@@ -212,11 +366,15 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${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')} +
@@ -232,34 +390,29 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.flow_rate_title')}
`; - } + } - _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.grid_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_combined || "", 'grid_combined', this._localize('editor.grid_combined_sensor'))} + +
- +
+ ${this._localize('editor.grid_combined_hint')} +
+ + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid, 'grid', this._localize('card.label_import') + " (W)")} + + ${this._renderEntitySelector(entitySelectorSchema, entities.grid_export, 'grid_export', this._localize('card.label_export') + " (W, Optional)")}
@@ -281,11 +434,17 @@ class PowerFluxCardEditor extends LitElement { @value-changed=${this._valueChanged} > + ${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_export', this._localize('editor.export_color'), '#ff3333')} +
@@ -301,35 +460,27 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.flow_rate_title')}
`; - } + } - _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` + _renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.battery_section')}

- + ${this._renderEntitySelector(entitySelectorSchema, entities.battery, 'battery', this._localize('editor.entity'))} + +
+ +
+ ${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._renderColorPickerQuad('color_battery', 'color_pipe_battery', 'color_text_battery', 'color_icon_battery', '#00ff88')}
@@ -369,28 +535,40 @@ class PowerFluxCardEditor extends LitElement { >
${this._localize('editor.flow_rate_title')}
- `; - } - _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { - return html` +
+ +
${this._localize('editor.invert_battery')}
+
+ `; + } + + _renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema) { + return html`
- Zurück + ${this._localize('editor.back')}

${this._localize('editor.consumers_section')}

-
🚗 Links (Lila)
- +
${this._localize('editor.house_total_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.house || "", 'house', this._localize('editor.house_sensor_label'))} +
+ ${this._localize('editor.house_sensor_hint')} +
+ + ${this._renderColorPickerQuad('color_house', null, 'color_text_house', 'color_icon_house', '#ff0080')} +
+ +
+
${this._localize('editor.consumer_1_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_1, 'consumer_1', this._localize('editor.entity'))} + +
+ ${this._localize('editor.invert_consumer_1')} + +
+ +
+ ${this._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._renderColorPickerQuad('color_consumer_1', 'color_pipe_consumer_1', 'color_text_consumer_1', 'color_icon_consumer_1', '#a855f7')}
-
♨️ Mitte (Orange)
- +
${this._localize('editor.consumer_2_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_2, 'consumer_2', this._localize('editor.entity'))} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ + ${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')}
-
🏊 Rechts (Türkis)
- +
${this._localize('editor.consumer_3_title')}
+ ${this._renderEntitySelector(entitySelectorSchema, entities.consumer_3, 'consumer_3', this._localize('editor.entity'))} + +
+ ${this._localize('editor.consumer_unit_kw')} + +
+ + ${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')}
`; - } - - render() { - if (!this.hass || !this._config) { - return html``; } - const entities = this._config.entities || {}; + render() { + if (!this.hass || !this._config) { + return html``; + } - const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; - const textSelectorSchema = { text: {} }; - const iconSelectorSchema = { icon: {} }; + const entities = this._config.entities || {}; - // SUBVIEW ROUTING - if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + const entitySelectorSchema = { entity: { domain: ["sensor", "input_number"] } }; + const textSelectorSchema = { text: {} }; + const iconSelectorSchema = { icon: {} }; + + // SUBVIEW ROUTING + if (this._subView === 'solar') return this._renderSolarView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'grid') return this._renderGridView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'battery') return this._renderBatteryView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); + if (this._subView === 'consumers') return this._renderConsumersView(entities, entitySelectorSchema, textSelectorSchema, iconSelectorSchema); - // MAIN MENU VIEW - return html` + // MAIN MENU VIEW + return html`
${this._localize('editor.main_title')}
@@ -525,7 +757,7 @@ class PowerFluxCardEditor extends LitElement { .selector=${{ number: { min: 0.5, max: 1.5, step: 0.05, mode: "slider" } }} .value=${this._config.zoom !== undefined ? this._config.zoom : 0.9} .configValue=${'zoom'} - .label=${"🔍 Zoom (Standard View)"} + .label=${this._localize('editor.zoom_label')} @value-changed=${this._valueChanged} >
@@ -536,7 +768,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_neon_glow'} @change=${this._valueChanged} > -
Neon Glow
+
${this._localize('editor.neon_glow')}
@@ -545,7 +777,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_donut_border'} @change=${this._valueChanged} > -
Donut Chart (Grid/Haus)
+
${this._localize('editor.donut_chart')}
@@ -554,7 +786,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_comet_tail'} @change=${this._valueChanged} > -
Comet Tail Effect
+
${this._localize('editor.comet_tail')}
@@ -563,7 +795,16 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'show_dashed_line'} @change=${this._valueChanged} > -
Dashed Line Animation
+
${this._localize('editor.dashed_line')}
+
+ +
+ +
${this._localize('editor.tinted_background')}
@@ -572,7 +813,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'use_colored_values'} @change=${this._valueChanged} > -
Farbige Textwerte
+
${this._localize('editor.colored_values')}
@@ -581,7 +822,7 @@ class PowerFluxCardEditor extends LitElement { .configValue=${'hide_consumer_icons'} @change=${this._valueChanged} > -
Icons unten ausblenden
+
${this._localize('editor.hide_consumer_icons')}
@@ -593,6 +834,15 @@ class PowerFluxCardEditor extends LitElement {
${this._localize('editor.hide_inactive')}
+
+ +
${this._localize('editor.show_consumer_always')}
+
+
`; - } + } } customElements.define("power-flux-card-editor", PowerFluxCardEditor); diff --git a/src/power-flux-card.js b/src/power-flux-card.js index 6c70037..842366d 100644 --- a/src/power-flux-card.js +++ b/src/power-flux-card.js @@ -1,87 +1,172 @@ +import { } from "./power-flux-card-editor.js"; +import lang_en from "./lang-en.js"; +import lang_de from "./lang-de.js"; + console.log( - "%c⚡ Power-Flux-Card v_2.0 ready", - "background: #2ecc71; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" + "%c⚡ Power Flux Card v_2.4 ready", + "background: #d19525ff; color: #000; padding: 2px 6px; border-radius: 4px; font-weight: bold;" ); -class PowerFluxCard extends LitElement { - - static get properties() { - return { - hass: {}, - config: {}, - _cardWidth: { state: true }, - }; - } +(function (lang_en, lang_de) { + const cardTranslations = { + "en": lang_en.card, + "de": lang_de.card + }; - _localize(key) { - const lang = this.hass && this.hass.language ? this.hass.language : 'en'; - const dict = cardTranslations[lang] || cardTranslations['en']; - return dict[key] || cardTranslations['en'][key] || key; - } + const LitElement = customElements.get("ha-lit-element") || Object.getPrototypeOf(customElements.get("home-assistant-main")); + const html = LitElement.prototype.html; + const css = LitElement.prototype.css; - static async getConfigElement() { - return document.createElement("power-flux-card-editor"); - } - - static getStubConfig() { - return { - zoom: 0.9, - compact_view: false, - show_donut_border: false, - show_neon_glow: true, - show_comet_tail: false, - show_dashed_line: false, - show_tinted_background: false, - hide_inactive_flows: true, - show_flow_rate_solar: true, - show_flow_rate_grid: true, - show_flow_rate_battery: true, - show_label_solar: false, - show_label_grid: false, - show_label_battery: false, - show_label_house: false, - use_colored_values: false, - hide_consumer_icons: false, - entities: { - solar: "", - grid: "", - grid_export: "", - battery: "", - battery_soc: "", - consumer_1: "", - consumer_2: "", - consumer_3: "" - } - }; - } - - setConfig(config) { - if (!config.entities) { - // Init allow + class PowerFluxCard extends LitElement { + static get properties() { + return { + hass: {}, + config: {}, + _cardWidth: { state: true }, + }; } - this.config = config; - } - firstUpdated() { - this._resizeObserver = new ResizeObserver(entries => { - for (const entry of entries) { - if (entry.contentRect.width > 0) { - this._cardWidth = entry.contentRect.width; + _localize(key) { + const lang = this.hass && this.hass.language ? this.hass.language : 'en'; + const dict = cardTranslations[lang] || cardTranslations['en']; + return dict[key] || cardTranslations['en'][key] || key; + } + + static async getConfigElement() { + return document.createElement("power-flux-card-editor"); + } + + static getStubConfig() { + return { + zoom: 0.9, + 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_neon_glow: true, + show_comet_tail: false, + show_dashed_line: false, + show_tinted_background: false, + hide_inactive_flows: true, + show_flow_rate_solar: true, + show_flow_rate_grid: true, + show_flow_rate_battery: true, + show_label_solar: false, + show_label_grid: false, + show_label_battery: false, + show_label_house: false, + use_colored_values: false, + hide_consumer_icons: false, + entities: { + solar: "", + grid: "", + grid_export: "", + grid_combined: "", + battery: "", + battery_soc: "", + battery_charge: "", + battery_discharge: "", + house: "", + consumer_1: "", + consumer_2: "", + consumer_3: "" + } + }; + } + + _handleClick(entityId) { + if (!entityId) return; + const event = new Event("hass-more-info", { + bubbles: true, + composed: true, + }); + event.detail = { entityId }; + this.dispatchEvent(event); + } + + setConfig(config) { + if (!config.entities) { + // Init allow + } + this.config = config; + } + + firstUpdated() { + this._resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.contentRect.width > 0) { + this._cardWidth = entry.contentRect.width; + } + } + }); + this._resizeObserver.observe(this); + } + + updated(changedProps) { + super.updated(changedProps); + if (changedProps.has('hass') && this.hass) { + const isDark = this.hass.themes?.darkMode !== false; + if (isDark) { + this.removeAttribute('data-theme-light'); + } else { + this.setAttribute('data-theme-light', ''); + } + } + // Apply custom colors from config + if (this.config) { + const colorMap = { + 'color_solar': '--neon-yellow', + 'color_grid': '--neon-blue', + 'color_battery': '--neon-green', + 'color_export': '--export-color', + 'color_consumer_1': '--consumer-1-color', + 'color_consumer_2': '--consumer-2-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)) { + if (this.config[configKey]) { + this.style.setProperty(cssVar, this.config[configKey]); + } else { + this.style.removeProperty(cssVar); + } } } - }); - this._resizeObserver.observe(this); - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._resizeObserver) { - this._resizeObserver.disconnect(); } - } - static get styles() { - return css` + disconnectedCallback() { + super.disconnectedCallback(); + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + } + } + + static get styles() { + return css` :host { display: block; --neon-yellow: #ffdd00; @@ -89,10 +174,45 @@ class PowerFluxCard extends LitElement { --neon-green: #00ff88; --neon-pink: #ff0080; --neon-red: #ff3333; - --grid-grey: #9e9e9e; --export-purple: #a855f7; + --export-color: #ff3333; + --consumer-1-color: #a855f7; + --consumer-2-color: #f97316; + --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; } + :host([data-theme-light]) { + --neon-yellow: #c8a800; + --neon-blue: #2563eb; + --neon-green: #059669; + --neon-pink: #db2777; + --neon-red: #dc2626; + --export-purple: #7c3aed; + --export-color: #dc2626; + --consumer-1-color: #7c3aed; + --consumer-2-color: #ea580c; + --consumer-3-color: #0891b2; + } ha-card { padding: 0; position: relative; @@ -146,7 +266,7 @@ class PowerFluxCard extends LitElement { .compact-bar-wrapper { height: 36px; width: 100%; - background: #333; + background: var(--card-background-color, #333); border-radius: 5px; margin: 4px 0; display: flex; @@ -167,17 +287,11 @@ class PowerFluxCard extends LitElement { overflow: hidden; } - /* Source Colors */ - .src-solar { background: var(--neon-green); color: black; } - .src-grid { background: var(--grid-grey); color: black; } - .src-battery { background: var(--neon-yellow); color: black; } - /* --- STANDARD VIEW STYLES --- */ .scale-wrapper { width: 420px; - transform-origin: top center; - transition: transform 0.1s linear; - margin: 0 auto; + transform-origin: top left; + transition: transform 0.1s linear; } .absolute-container { @@ -197,15 +311,18 @@ class PowerFluxCard extends LitElement { z-index: 2; transition: all 0.3s ease; box-sizing: border-box; + cursor: pointer; } .bubble.tinted { background: rgba(255, 255, 255, 0.05); } .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.exporting { background: color-mix(in srgb, var(--export-color), transparent 85%); } + .bubble.grid.exporting { border-color: var(--export-color); } .bubble.tinted.battery { background: color-mix(in srgb, var(--neon-green), transparent 85%); } - .bubble.tinted.c1 { background: color-mix(in srgb, #a855f7, transparent 85%); } - .bubble.tinted.c2 { background: color-mix(in srgb, #f97316, transparent 85%); } - .bubble.tinted.c3 { background: color-mix(in srgb, #06b6d4, 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.c3 { background: color-mix(in srgb, var(--consumer-3-color), transparent 85%); } .bubble.house { border-color: var(--neon-pink); } .bubble.house.tinted { background: color-mix(in srgb, var(--neon-pink), transparent 85%); } @@ -217,21 +334,35 @@ class PowerFluxCard extends LitElement { -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; } + + .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 { - width: 34px; height: 34px; position: absolute; top: 13px; 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; } .icon-custom { --mdc-icon-size: 34px; } .sub { font-size: 9px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; - line-height: 1.1; z-index: 2; position: absolute; top: 48px; left: 0; width: 100%; text-align: center; margin: 0; pointer-events: none; + line-height: 1.1; z-index: 2; position: absolute; top: 46px; left: 0; width: 100%; text-align: center; margin: 0; pointer-events: none; + } + .sub.secondary-val { + text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 10px; } .value { font-weight: bold; font-size: 15px; white-space: nowrap; z-index: 2; transition: color 0.3s ease; line-height: 1.2; position: absolute; bottom: 11px; left: 0; width: 100%; text-align: center; margin: 0; } + .direction-arrow { font-size: 12px; margin-right: 0px; vertical-align: top; } @keyframes spin { 100% { transform: rotate(360deg); } } .spin-slow { animation: spin 12s linear infinite; transform-origin: center; } @@ -245,17 +376,18 @@ class PowerFluxCard extends LitElement { .solar { border-color: var(--neon-yellow); } .battery { border-color: var(--neon-green); } .grid { border-color: var(--neon-blue); } - .c1 { border-color: #a855f7; } - .c2 { border-color: #f97316; } - .c3 { border-color: #06b6d4; } + .c1 { border-color: var(--consumer-1-color); } + .c2 { border-color: var(--consumer-2-color); } + .c3 { border-color: var(--consumer-3-color); } .inactive { border-color: var(--secondary-text-color); } - .glow.solar { box-shadow: 0 0 15px rgba(255, 221, 0, 0.4); } - .glow.battery { box-shadow: 0 0 15px rgba(0, 255, 136, 0.4); } - .glow.grid { box-shadow: 0 0 15px rgba(59, 130, 246, 0.4); } - .glow.c1 { box-shadow: 0 0 15px rgba(168, 85, 247, 0.4); } - .glow.c2 { box-shadow: 0 0 15px rgba(249, 115, 22, 0.4); } - .glow.c3 { box-shadow: 0 0 15px rgba(6, 182, 212, 0.4); } + .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.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.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%); } .node-solar { top: 70px; left: 5px; } .node-grid { top: 70px; left: 165px; } @@ -265,84 +397,98 @@ class PowerFluxCard extends LitElement { .node-c2 { top: 370px; left: 165px; } .node-c3 { top: 370px; left: 325px; } - svg { position: absolute; top: 7; left: 25; 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-solar { stroke: var(--neon-yellow); } - .bg-grid { stroke: var(--neon-blue); } - .bg-battery { stroke: var(--neon-green); } - .bg-export { stroke: var(--neon-red); } - .bg-c1 { stroke: #a855f7; } - .bg-c2 { stroke: #f97316; } - .bg-c3 { stroke: #06b6d4; } + .bg-solar { stroke: var(--pipe-solar-color); } + .bg-grid { stroke: var(--pipe-grid-color); } + .bg-battery { stroke: var(--pipe-battery-color); } + .bg-export { stroke: var(--export-color); } + .bg-c1 { stroke: var(--pipe-consumer-1-color); } + .bg-c2 { stroke: var(--pipe-consumer-2-color); } + .bg-c3 { stroke: var(--pipe-consumer-3-color); } .flow-line { 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; } - .flow-solar { stroke: var(--neon-yellow); } - .flow-grid { stroke: var(--neon-blue); } - .flow-battery { stroke: var(--neon-green); } - .flow-export { stroke: var(--neon-red); } + .flow-solar { stroke: var(--pipe-solar-color); } + .flow-grid { stroke: var(--pipe-grid-color); } + .flow-battery { stroke: var(--pipe-battery-color); } + .flow-export { stroke: var(--export-color); } @keyframes dash { to { stroke-dashoffset: -1500; } } .flow-text { - font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.8)); transition: opacity 0.3s ease; + font-size: 10px; font-weight: bold; text-anchor: middle; fill: #fff; transition: opacity 0.3s ease; } .flow-text.no-shadow { filter: none; } - .text-solar { fill: var(--neon-yellow); } - .text-grid { fill: var(--neon-blue); } - .text-export { fill: var(--neon-red); } - .text-battery { fill: var(--neon-green); } + .text-solar { fill: var(--pipe-solar-color); } + .text-grid { fill: var(--pipe-grid-color); } + .text-export { fill: var(--export-color); } + .text-battery { fill: var(--pipe-battery-color); } `; - } + } - // --- SVG ICON RENDERER --- - _renderIcon(type, val = 0, colorOverride = null) { + // --- SVG ICON RENDERER --- + _renderIcon(type, val = 0, colorOverride = null) { if (type === 'solar') { - const animate = Math.round(val) > 0 ? 'spin-slow' : ''; - const color = colorOverride || 'var(--neon-yellow)'; - return html``; + const animate = Math.round(val) > 0 ? 'spin-slow' : ''; + 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)'; - return html``; + const animate = Math.round(val) > 0 ? 'pulse' : ''; + 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 soc = Math.min(Math.max(val, 0), 100) / 100; + const rectHeight = 14 * soc; + const rectY = 18 - rectHeight; + 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)'; - return html``; + const strokeColor = colorOverride || 'var(--icon-house-color)'; + return html``; } if (type === 'car') { - return html``; + const c = colorOverride || 'var(--icon-consumer-1-color)'; + return html``; } if (type === 'heater') { - return html``; + const c = colorOverride || 'var(--icon-consumer-2-color)'; + return html``; } if (type === 'pool') { - return html``; + const c = colorOverride || 'var(--icon-consumer-3-color)'; + return html``; } return html``; - } + } - _formatPower(val) { + _formatPower(val) { if (val === 0) return "0"; if (Math.abs(val) >= 1000) { - return (val / 1000).toFixed(1) + " kW"; + return (val / 1000).toFixed(1) + " kW"; } return Math.round(val) + " W"; - } + } - // --- DOM NODE SVG GENERATOR --- - _renderSVGPath(d, color) { + _getConsumerColor(index) { + const style = getComputedStyle(this); + 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"); path.setAttribute("d", d); path.setAttribute("class", "bracket-line"); @@ -352,29 +498,29 @@ class PowerFluxCard extends LitElement { path.style.stroke = color; path.style.fill = "none"; return path; - } + } - // --- SQUARE BRACKET GENERATOR --- - _createBracketPath(startPx, widthPx, direction) { + // --- SQUARE BRACKET GENERATOR --- + _createBracketPath(startPx, widthPx, direction) { if (widthPx < 5) return ""; - - const r = 5; + + const r = 5; const startX = startPx; const endX = startPx + widthPx; - + let yBase, yLine; - + if (direction === 'down') { - yBase = 24; - yLine = 4; + yBase = 24; + yLine = 4; } else { - yBase = 0; - yLine = 20; + yBase = 0; + yLine = 20; } const height = Math.abs(yBase - yLine); const rEff = Math.min(r, height / 2, widthPx / 2); - + const yCorner = direction === 'down' ? yLine + rEff : yLine - rEff; return ` @@ -385,79 +531,103 @@ class PowerFluxCard extends LitElement { Q ${endX} ${yLine} ${endX} ${yCorner} L ${endX} ${yBase} `; - } + } - // --- RENDER COMPACT VIEW --- - _renderCompactView(entities) { + // --- RENDER COMPACT VIEW --- + _renderCompactView(entities) { // 1. Get Values const getVal = (entity) => { - const state = this.hass.states[entity]; - return state ? parseFloat(state.state) || 0 : 0; + 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 gridMain = entities.grid ? getVal(entities.grid) : 0; + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const gridCombinedVal = hasGridCombined ? getVal(entities.grid_combined) : 0; + const gridMain = hasGridCombined ? gridCombinedVal : (entities.grid ? getVal(entities.grid) : 0); const gridExportSensor = entities.grid_export ? getVal(entities.grid_export) : 0; - const battery = entities.battery ? getVal(entities.battery) : 0; - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; // EV Value + let battery = entities.battery ? getVal(entities.battery) : 0; + if (this.config.invert_battery) { + battery *= -1; + } + 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); // 2. Logic Calculation let gridImport = 0; let gridExport = 0; - if (entities.grid_export && entities.grid_export !== "") { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = Math.abs(gridExportSensor); + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExportSensor); } else { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; + gridImport = gridMain > 0 ? gridMain : 0; + 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; if (batteryCharge > 0) { + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); + } else { if (solar >= batteryCharge) { - solarToBatt = batteryCharge; - gridToBatt = 0; + solarToBatt = batteryCharge; + gridToBatt = 0; } else { - solarToBatt = solar; - gridToBatt = batteryCharge - solar; + solarToBatt = solar; + gridToBatt = batteryCharge - solar; } + } } const solarTotalToCons = Math.max(0, solar - solarToBatt - gridExport); const gridTotalToCons = Math.max(0, gridImport - gridToBatt); const battTotalToCons = batteryDischarge; - + const totalCons = solarTotalToCons + gridTotalToCons + battTotalToCons; - + // Calculate Splits let evPower = 0; let housePower = totalCons; - + if (c1Val > 0 && totalCons > 0) { - evPower = Math.min(c1Val, totalCons); - housePower = totalCons - evPower; + evPower = Math.min(c1Val, totalCons); + housePower = totalCons - evPower; } - + // Calculate Total Bar Width (Flux) // The Bar represents: Battery Discharge + Solar + Grid Import // This MUST equal: House + EV + Export + Battery Charge - + // SOURCES (for Bar Segments) const srcBattery = batteryDischarge; - const srcSolar = solar; // Solar includes Export + Charge + Cons - const srcGrid = gridImport; - + const srcSolar = solar; // Solar includes Export + Charge + Cons + const srcGrid = gridImport; + const totalFlux = srcBattery + srcSolar + srcGrid; - + // DESTINATIONS (for Bottom Brackets) - const destHouse = housePower; - const destEV = evPower; + const destHouse = housePower; + const destEV = evPower; const destExport = gridExport; // Note: Battery Charge is also a destination (internal flow), but usually not bracketed if we only want "Consumers" // If we don't bracket Charge, there will be a gap. We can accept that or add a Charge bracket. @@ -465,10 +635,10 @@ class PowerFluxCard extends LitElement { const threshold = 0.1; const availableWidth = (this._cardWidth && this._cardWidth > 0) ? this._cardWidth : (this.offsetWidth || 400); - const fullWidth = availableWidth - 40; + const fullWidth = availableWidth - 40; if (totalFlux <= threshold) { - return html`
Waiting for data...
`; + return html`
Waiting for data...
`; } // --- GENERATE BAR SEGMENTS (Aggregated by Source) --- @@ -476,36 +646,37 @@ class PowerFluxCard extends LitElement { const barSegments = []; let currentX = 0; - const addSegment = (val, color, type, label) => { - if (val <= threshold) return; - const pct = val / totalFlux; - const width = pct * fullWidth; - barSegments.push({ - val, - color, - widthPct: pct * 100, - widthPx: width, - startPx: currentX, - type, - label - }); - currentX += width; + const addSegment = (val, color, type, label, entityId) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; + barSegments.push({ + val, + color, + widthPct: pct * 100, + widthPx: width, + startPx: currentX, + type, + label, + entityId + }); + currentX += width; } - addSegment(srcBattery, 'var(--neon-yellow)', 'battery', 'battery'); - addSegment(srcSolar, 'var(--neon-green)', 'solar', 'solar'); - addSegment(srcGrid, 'var(--grid-grey)', 'grid', 'grid'); + addSegment(srcBattery, 'var(--neon-green)', 'battery', 'battery', entities.battery); + addSegment(srcSolar, 'var(--neon-yellow)', 'solar', 'solar', entities.solar); + addSegment(srcGrid, 'var(--neon-blue)', 'grid', 'grid', entities.grid_combined || entities.grid); // --- GENERATE TOP BRACKETS (Based on Bar Segments) --- const topBrackets = barSegments.map(s => { - const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); - let icon = ''; - let iconColor = ''; - if (s.type === 'solar') { icon = 'mdi:weather-sunny'; iconColor = 'var(--neon-green)'; } - if (s.type === 'grid') { icon = 'mdi:transmission-tower'; iconColor = 'var(--grid-grey)'; } - if (s.type === 'battery') { icon = 'mdi:battery-high'; iconColor = 'var(--neon-yellow)'; } - - return { path, width: s.widthPx, center: s.startPx + (s.widthPx/2), icon, iconColor }; + const path = this._createBracketPath(s.startPx, s.widthPx, 'down'); + let icon = ''; + let iconColor = ''; + 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 }; }); // --- GENERATE BOTTOM BRACKETS (Independent Calculation) --- @@ -513,32 +684,36 @@ class PowerFluxCard extends LitElement { const bottomBrackets = []; let bottomX = 0; - const addBottomBracket = (val, type) => { - if (val <= threshold) return; - const pct = val / totalFlux; - const width = pct * fullWidth; - - let icon = ''; - let iconColor = ''; - - if (type === 'house') { icon = 'mdi:home'; iconColor = 'var(--primary-text-color)'; } - if (type === 'car') { icon = 'mdi:car-electric'; iconColor = '#a855f7'; } - if (type === 'export') { icon = 'mdi:arrow-right-box'; iconColor = 'var(--export-purple)'; } + const addBottomBracket = (val, type, entityId = null) => { + if (val <= threshold) return; + const pct = val / totalFlux; + const width = pct * fullWidth; - const path = this._createBracketPath(bottomX, width, 'up'); - bottomBrackets.push({ - path, - width: width, - center: bottomX + (width/2), - icon, - iconColor - }); - bottomX += width; + let icon = ''; + let iconColor = ''; + + 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({ + path, + width: width, + center: bottomX + (width / 2), + icon, + iconColor, + val, + entityId + }); + bottomX += width; }; - addBottomBracket(destHouse, 'house'); - addBottomBracket(destEV, 'car'); - addBottomBracket(destExport, 'export'); + addBottomBracket(destHouse, 'house', entities.house); + addBottomBracket(destEV, 'car', entities.consumer_1); + addBottomBracket(destExport, 'export', entities.grid_combined || entities.grid_export || entities.grid); + addBottomBracket(batteryCharge, 'battery', entities.battery); // Note: If there is Battery Charging happening, bottomX will not reach fullWidth. // This leaves a gap at the end (or between segments depending on logic), which is visually correct @@ -553,18 +728,29 @@ class PowerFluxCard extends LitElement { ${topBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${topBrackets.map(b => b.width > 20 ? html` -
+
b.entityId && this._handleClick(b.entityId)}>
` : '')}
- ${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) : ''}
- `)} + `})}
@@ -573,313 +759,436 @@ class PowerFluxCard extends LitElement { ${bottomBrackets.map(b => this._renderSVGPath(b.path, b.iconColor))} ${bottomBrackets.map(b => b.width > 20 ? html` -
+
b.entityId && this._handleClick(b.entityId)}>
` : '')}
`; - } - - // --- RENDER STANDARD VIEW --- - _renderStandardView(entities) { - // FIX: Default to hidden unless explicitly set to false - const hideInactive = this.config.hide_inactive_flows !== false; - - const globalFlowRate = this.config.show_flow_rates !== false; - - // FLOW RATE TOGGLES - const showFlowSolar = this.config.show_flow_rate_solar !== undefined ? this.config.show_flow_rate_solar : globalFlowRate; - const showFlowGrid = this.config.show_flow_rate_grid !== undefined ? this.config.show_flow_rate_grid : globalFlowRate; - const showFlowBattery = this.config.show_flow_rate_battery !== undefined ? this.config.show_flow_rate_battery : globalFlowRate; - - // LABEL TOGGLES - const showLabelSolar = this.config.show_label_solar === true; - const showLabelGrid = this.config.show_label_grid === true; - const showLabelBattery = this.config.show_label_battery === true; - const showLabelHouse = this.config.show_label_house === true; - - const useColoredValues = this.config.use_colored_values === true; - const showDonut = this.config.show_donut_border === true; - const showTail = this.config.show_comet_tail === true; - const showDashedLine = this.config.show_dashed_line === true; - const showTint = this.config.show_tinted_background === true; - const hideConsumerIcons = this.config.hide_consumer_icons === true; - const showNeonGlow = this.config.show_neon_glow !== false; - - // CUSTOM LABELS - const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); - const labelGridText = this.config.grid_label || this._localize('card.label_import'); - const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); - const labelHouseText = this.config.house_label || this._localize('card.label_house'); - - // CUSTOM ICONS - const iconSolar = this.config.solar_icon; - const iconGrid = this.config.grid_icon; - const iconBattery = this.config.battery_icon; - - // Determine existence of main entities - const hasSolar = !!(entities.solar && entities.solar !== ""); - const hasGrid = !!(entities.grid && entities.grid !== ""); - const hasBattery = !!(entities.battery && entities.battery !== ""); - - const styleSolar = hasSolar ? '' : 'display: none;'; - const styleSolarBatt = (hasSolar && hasBattery) ? '' : 'display: none;'; - const styleGrid = hasGrid ? '' : 'display: none;'; - const styleGridBatt = (hasGrid && hasBattery) ? '' : 'display: none;'; - const styleBattery = hasBattery ? '' : 'display: none;'; - - 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 getVal = (entity) => { - const state = this.hass.states[entity]; - return state ? parseFloat(state.state) || 0 : 0; - }; - - const c1Val = entities.consumer_1 ? getVal(entities.consumer_1) : 0; - const c2Val = entities.consumer_2 ? getVal(entities.consumer_2) : 0; - const c3Val = entities.consumer_3 ? getVal(entities.consumer_3) : 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 anyBottomVisible = showC1 || showC2 || showC3; - - const solar = hasSolar ? getVal(entities.solar) : 0; - const gridMain = hasGrid ? getVal(entities.grid) : 0; - const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; - const battery = hasBattery ? getVal(entities.battery) : 0; - const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; - - const solarVal = Math.max(0, solar); - - let gridImport = 0; - let gridExport = 0; - - if (hasGrid) { - if (entities.grid_export && entities.grid_export !== "") { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = Math.abs(gridExpSensor); - } else { - gridImport = gridMain > 0 ? gridMain : 0; - gridExport = gridMain < 0 ? Math.abs(gridMain) : 0; - } } - const batteryCharge = battery > 0 ? battery : 0; - const batteryDischarge = battery < 0 ? Math.abs(battery) : 0; + // --- RENDER STANDARD VIEW --- + _renderStandardView(entities) { + // FIX: Default to hidden unless explicitly set to false + const hideInactive = this.config.hide_inactive_flows !== false; - let solarToBatt = 0; - let gridToBatt = 0; + const globalFlowRate = this.config.show_flow_rates !== false; - if (hasBattery && batteryCharge > 0) { - if (solarVal >= batteryCharge) { + // FLOW RATE TOGGLES + const showFlowSolar = this.config.show_flow_rate_solar !== undefined ? this.config.show_flow_rate_solar : globalFlowRate; + const showFlowGrid = this.config.show_flow_rate_grid !== undefined ? this.config.show_flow_rate_grid : globalFlowRate; + const showFlowBattery = this.config.show_flow_rate_battery !== undefined ? this.config.show_flow_rate_battery : globalFlowRate; + + // LABEL TOGGLES + const showLabelSolar = this.config.show_label_solar === true; + const showLabelGrid = this.config.show_label_grid === true; + const showLabelBattery = this.config.show_label_battery === true; + const showLabelHouse = this.config.show_label_house === true; + + const useColoredValues = this.config.use_colored_values === true; + const showDonut = this.config.show_donut_border === true; + const showTail = this.config.show_comet_tail === true; + const showDashedLine = this.config.show_dashed_line === true; + const showTint = this.config.show_tinted_background === true; + const hideConsumerIcons = this.config.hide_consumer_icons === true; + const showNeonGlow = this.config.show_neon_glow !== false; + + // CUSTOM LABELS + const labelSolarText = this.config.solar_label || this._localize('card.label_solar'); + const labelGridText = this.config.grid_label || this._localize('card.label_grid'); + const labelBatteryText = this.config.battery_label || (entities.battery && this.hass.states[entities.battery] && this.hass.states[entities.battery].state > 0 ? '+' : '-') + " " + this._localize('card.label_battery'); + const labelHouseText = this.config.house_label || this._localize('card.label_house'); + + // CUSTOM ICONS + const iconSolar = this.config.solar_icon; + const iconGrid = this.config.grid_icon; + const iconBattery = this.config.battery_icon; + + // SECONDARY SENSORS (display only) + const hasSecondarySolar = !!(entities.secondary_solar && entities.secondary_solar !== ""); + const hasSecondaryGrid = !!(entities.secondary_grid && entities.secondary_grid !== ""); + const hasSecondaryBattery = !!(entities.secondary_battery && entities.secondary_battery !== ""); + + const getSecondaryVal = (entity) => { + if (!entity) return ''; + const state = this.hass.states[entity]; + if (!state) return ''; + const val = parseFloat(state.state); + if (isNaN(val)) return state.state + (state.attributes.unit_of_measurement ? ' ' + state.attributes.unit_of_measurement : ''); + const unit = state.attributes.unit_of_measurement || ''; + if (unit === 'W') { 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 + const hasSolar = !!(entities.solar && entities.solar !== ""); + const hasGridCombined = !!(entities.grid_combined && entities.grid_combined !== ""); + const hasGrid = !!(entities.grid && entities.grid !== "") || hasGridCombined; + const hasBattery = !!(entities.battery && entities.battery !== ""); + + const styleSolar = hasSolar ? '' : 'display: none;'; + const styleSolarBatt = (hasSolar && hasBattery) ? '' : 'display: none;'; + const styleGrid = hasGrid ? '' : 'display: none;'; + const styleGridBatt = (hasGrid && hasBattery) ? '' : 'display: none;'; + const styleBattery = hasBattery ? '' : 'display: none;'; + + const textClass = showNeonGlow ? 'flow-text' : 'flow-text no-shadow'; + + // Custom Labels for Consumers + 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 ? 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 ? 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 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); + const gridExpSensor = (hasGrid && entities.grid_export) ? getVal(entities.grid_export) : 0; + let battery = hasBattery ? getVal(entities.battery) : 0; + if (this.config.invert_battery) { + battery *= -1; + } + const battSoc = (hasBattery && entities.battery_soc) ? getVal(entities.battery_soc) : 0; + + const solarVal = Math.max(0, solar); + + let gridImport = 0; + let gridExport = 0; + + if (hasGrid) { + if (hasGridCombined) { + // COMBINED SENSOR: positive = import, negative = export + gridImport = gridCombinedVal > 0 ? gridCombinedVal : 0; + gridExport = gridCombinedVal < 0 ? Math.abs(gridCombinedVal) : 0; + } else if (entities.grid_export && entities.grid_export !== "") { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = Math.abs(gridExpSensor); + } else { + gridImport = gridMain > 0 ? gridMain : 0; + gridExport = gridMain < 0 ? Math.abs(gridMain) : 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; + + if (hasBattery && batteryCharge > 0) { + const hasGridToBattSensor = !!(entities.grid_to_battery && entities.grid_to_battery !== ""); + if (hasGridToBattSensor) { + // Use dedicated grid-to-battery sensor + gridToBatt = Math.abs(getVal(entities.grid_to_battery)); + solarToBatt = Math.max(0, batteryCharge - gridToBatt); + } else { + // Calculate: solar prioritized + if (solarVal >= batteryCharge) { solarToBatt = batteryCharge; gridToBatt = 0; - } else { - solarToBatt = solarVal; + } else { + solarToBatt = solarVal; gridToBatt = batteryCharge - solarVal; + } } - } + } - const solarToHouse = Math.max(0, solarVal - solarToBatt - gridExport); - const gridToHouse = Math.max(0, gridImport - gridToBatt); - const house = solarToHouse + gridToHouse + batteryDischarge; + const solarToHouse = Math.max(0, solarVal - solarToBatt - gridExport); + const gridToHouse = Math.max(0, gridImport - gridToBatt); + const house = solarToHouse + gridToHouse + batteryDischarge; - const isTopArcActive = (solarToBatt > 0); - const topShift = isTopArcActive ? 0 : 50; - let baseHeight = anyBottomVisible ? 480 : 340; - const contentHeight = baseHeight - topShift; + // Use house entity for display if defined, otherwise use calculated value + const houseDisplay = (entities.house && entities.house !== "") ? getVal(entities.house) : house; - const designWidth = 420; - const availableWidth = this._cardWidth || designWidth; - let scale = availableWidth / designWidth; - const userZoom = this.config.zoom !== undefined ? this.config.zoom : 0.9; - scale = scale * userZoom; + const isTopArcActive = (solarToBatt > 0); + const topShift = (isTopArcActive || (!hideInactive && hasSolar && hasBattery)) ? 0 : 50; + let baseHeight = anyBottomVisible ? 480 : 340; + const contentHeight = baseHeight - topShift; - if (scale < 0.5) scale = 0.5; - if (scale > 1.5) scale = 1.5; + const designWidth = 420; + const availableWidth = this._cardWidth || designWidth; + let scale = availableWidth / designWidth; + const userZoom = this.config.zoom !== undefined ? this.config.zoom : 0.9; + scale = scale * userZoom; - const finalCardHeightPx = contentHeight * scale; + if (scale < 0.5) scale = 0.5; + if (scale > 1.5) scale = 1.5; - let houseGradientVal = ''; - let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; - const tintClass = showTint ? 'tinted' : ''; - const glowClass = showNeonGlow ? 'glow' : ''; - - let houseDominantColor = 'var(--neon-pink)'; - if (house > 0) { + const finalCardHeightPx = contentHeight * scale; + const visualWidth = 420 * scale; + const centerMarginLeft = Math.max(0, (availableWidth - visualWidth) / 2); + + let houseGradientVal = ''; + let houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + const tintClass = showTint ? 'tinted' : ''; + const glowClass = showNeonGlow ? 'glow' : ''; + + let houseDominantColor = 'var(--neon-pink)'; + if (house > 0) { if (solarToHouse >= gridToHouse && solarToHouse >= batteryDischarge) { - houseDominantColor = 'var(--neon-yellow)'; + houseDominantColor = 'var(--neon-yellow)'; } else if (gridToHouse >= solarToHouse && gridToHouse >= batteryDischarge) { - houseDominantColor = 'var(--neon-blue)'; + houseDominantColor = 'var(--neon-blue)'; } else if (batteryDischarge >= solarToHouse && batteryDischarge >= gridToHouse) { - houseDominantColor = 'var(--neon-green)'; + houseDominantColor = 'var(--neon-green)'; } - } + } - if (showDonut) { + if (showDonut) { if (house > 0) { - const pctSolar = (solarToHouse / house) * 100; - const pctGrid = (gridToHouse / house) * 100; - const pctBatt = (batteryDischarge / house) * 100; - - let stops = []; - let current = 0; - if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } - if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } - if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } - if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } - - houseGradientVal = `conic-gradient(${stops.join(', ')})`; + const pctSolar = (solarToHouse / house) * 100; + const pctGrid = (gridToHouse / house) * 100; + const pctBatt = (batteryDischarge / house) * 100; - if (useColoredValues) { - const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); - if (maxVal > 0) { - if (maxVal === solarToHouse) houseTextCol = 'var(--neon-yellow)'; - else if (maxVal === gridToHouse) houseTextCol = 'var(--neon-blue)'; - else if (maxVal === batteryDischarge) houseTextCol = 'var(--neon-green)'; - } else { - houseTextCol = 'var(--neon-pink)'; - } + let stops = []; + let current = 0; + if (pctSolar > 0) { stops.push(`var(--neon-yellow) ${current}% ${current + pctSolar}%`); current += pctSolar; } + if (pctBatt > 0) { stops.push(`var(--neon-green) ${current}% ${current + pctBatt}%`); current += pctBatt; } + if (pctGrid > 0) { stops.push(`var(--neon-blue) ${current}% ${current + pctGrid}%`); current += pctGrid; } + if (current < 99.9) { stops.push(`var(--neon-pink) ${current}% 100%`); } + + houseGradientVal = `conic-gradient(from 330deg, ${stops.join(', ')})`; + + if (useColoredValues) { + const maxVal = Math.max(solarToHouse, gridToHouse, batteryDischarge); + if (maxVal > 0) { + if (maxVal === solarToHouse) houseTextCol = 'var(--neon-yellow)'; + else if (maxVal === gridToHouse) houseTextCol = 'var(--neon-blue)'; + else if (maxVal === batteryDischarge) houseTextCol = 'var(--neon-green)'; + } else { + houseTextCol = 'var(--neon-pink)'; } + } } else { - houseGradientVal = `var(--neon-pink)`; - houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; + houseGradientVal = `var(--neon-pink)`; + houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; } - } else { + } else { houseTextCol = useColoredValues ? 'var(--neon-pink)' : ''; - } + } - const houseTintStyle = showTint - ? `background: color-mix(in srgb, ${houseDominantColor}, transparent 85%);` + const houseTintStyle = showTint + ? `background: color-mix(in srgb, ${houseDominantColor}, transparent 85%);` : ''; - - const houseGlowStyle = showNeonGlow + + const houseGlowStyle = showNeonGlow ? `box-shadow: 0 0 15px color-mix(in srgb, ${houseDominantColor}, transparent 60%);` : `box-shadow: none;`; - const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; + const houseBubbleStyle = `${showDonut ? `--house-gradient: ${houseGradientVal};` : ''} ${houseTintStyle} ${houseGlowStyle}`; - const isSolarActive = Math.round(solarVal) > 0; - const isGridActive = Math.round(gridImport) > 0; + const isSolarActive = Math.round(solarVal) > 0; + const isGridActive = Math.round(gridImport) > 0 || Math.round(gridExport) > 0; + const isGridExporting = Math.round(gridExport) > 0 && Math.round(gridImport) === 0; - const solarColor = isSolarActive ? 'var(--neon-yellow)' : 'var(--secondary-text-color)'; - const gridColor = isGridActive ? 'var(--neon-blue)' : 'var(--secondary-text-color)'; - - const getAnimStyle = (val) => { - if (val <= 1) return "opacity: 0;"; - const userMinDuration = 7; - const userMaxDuration = 11; - const userFactor = 20000; - let duration = userFactor / val; - duration = Math.max(userMinDuration, Math.min(userMaxDuration, duration)); - - // Adjust speed for dashed line (Factor to slow down: 5x) - if (showDashedLine) { - duration = duration * 5; + // --- 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)'; } - - return `opacity: 1; animation-duration: ${duration}s;`; - }; + } - const getPipeStyle = (val) => { - if (!hideInactive) return "opacity: 0.2;"; - return val > 1 ? "opacity: 0.2;" : "opacity: 0;"; - }; + 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 getTextStyle = (val, type) => { + const getAnimStyle = (val) => { + if (val <= 1) return "opacity: 0;"; + + // --- Dynamic speed based on power --- + // Higher power = faster animation (shorter duration) + // Range: 2s (very fast, ~5000W+) to 12s (slow, ~50W) + const minDuration = 4; + const maxDuration = 12; + const factor = 12000; + let duration = factor / val; + duration = Math.max(minDuration, Math.min(maxDuration, duration)); + + // --- Dynamic particle density based on power --- + // Higher power = more/denser particles (shorter gap) + // Lower power = fewer/sparse particles (longer gap) + let dashSize, gapSize; + if (showTail) { + // Comet tail: vary tail length with power + dashSize = Math.round(15 + (val / 200) * 25); // 15-40 + dashSize = Math.min(dashSize, 40); + gapSize = Math.round(380 - (val / 200) * 200); // 380-180 + gapSize = Math.max(gapSize, 180); + } else if (showDashedLine) { + // Dashed line: vary dash density + dashSize = Math.round(8 + (val / 500) * 10); // 8-18 + dashSize = Math.min(dashSize, 18); + gapSize = Math.round(18 - (val / 1000) * 10); // 18-8 + gapSize = Math.max(gapSize, 8); + duration = duration * 5; // Dashed lines are slower + } else { + // Default dots: vary dot count/density + dashSize = 0; // stays as dots + gapSize = Math.round(380 - (val / 200) * 250); // 380-130 + gapSize = Math.max(gapSize, 130); + } + + const dynamicDash = `${dashSize} ${gapSize}`; + + return `opacity: 1; animation-duration: ${duration}s; stroke-dasharray: ${dynamicDash};`; + }; + + const getPipeStyle = (val) => { + if (!hideInactive) return "opacity: 0.2;"; + return val > 1 ? "opacity: 0.2;" : "opacity: 0;"; + }; + + const getTextStyle = (val, type) => { let isVisible = false; if (type === 'solar') isVisible = showFlowSolar; else if (type === 'grid') isVisible = showFlowGrid; else if (type === 'battery') isVisible = showFlowBattery; - - if (!isVisible) return "display: none;"; - return val > 5 ? "opacity: 1;" : "opacity: 0;"; - }; - const getColorStyle = (colorVar) => { + if (!isVisible) return "display: none;"; + return val > 5 ? "opacity: 1;" : "opacity: 0;"; + }; + + const getColorStyle = (colorVar) => { return useColoredValues ? `color: var(${colorVar});` : ''; - }; - const getConsumerColorStyle = (hex) => { + }; + const getConsumerColorStyle = (hex) => { return useColoredValues ? `color: ${hex};` : ''; - } + } - const renderLabel = (text, isVisible) => { + const renderLabel = (text, isVisible) => { if (!isVisible) return html``; return html`
${text}
`; - }; + }; - const renderMainIcon = (type, val, customIcon, color = null) => { + const renderSecondaryOrLabel = (labelText, showLabel, secondaryEntity, hasSecondary) => { + if (hasSecondary) { + const secVal = getSecondaryVal(secondaryEntity); + return html`
${secVal}
`; + } + return renderLabel(labelText, showLabel); + }; + + const renderMainIcon = (type, val, customIcon, color = null) => { if (customIcon) { - const style = color ? `color: ${color};` : (type === 'solar' ? 'color: var(--neon-yellow);' : (type === 'grid' ? 'color: var(--neon-blue);' : (type === 'battery' ? 'color: var(--neon-green);' : ''))); - return html``; + 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) => { + 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``; + iconContent = html``; } else if (customIcon) { - iconContent = html``; + iconContent = html``; } else { - iconContent = this._renderIcon(iconType, val); + iconContent = this._renderIcon(iconType, val); } + 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} - ${renderLabel(label, true)} -
${this._formatPower(val)}
+ ${renderSecondaryOrLabel(label, true, secEntity, hasSecondary)} +
${this._formatPower(val)}
`; - }; + }; - const getConsumerPipeStyle = (isActive, val) => { + const getConsumerPipeStyle = (isActive, val) => { if (!isActive) return "display: none;"; return getPipeStyle(val); - }; - - const getConsumerAnimStyle = (isActive, val) => { + }; + + const getConsumerAnimStyle = (isActive, val) => { if (!isActive) return "display: none;"; return getAnimStyle(val); - }; + }; - const pathSolarHouse = "M 50 160 Q 50 265 165 265"; - const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; - const pathGridImport = "M 210 160 L 210 220"; - const pathGridExport = "M 165 115 Q 130 145 95 115"; - const 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 pathSolarHouse = "M 50 160 Q 50 265 165 265"; + const pathSolarBatt = "M 50 70 Q 210 -20 370 70"; + const pathGridImport = "M 210 160 L 210 220"; + const pathGridExport = "M 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 dashArrayVal = showTail ? '30 360' : (showDashedLine ? '13 13' : '0 380'); - const strokeWidthVal = showDashedLine ? 4 : 8; + 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; - return html` + return html` -
+
@@ -888,33 +1197,33 @@ class PowerFluxCard extends LitElement { - + - - - + + + - + - - - + + + ${this._formatPower(solarToHouse)} ${this._formatPower(solarToBatt)} ${this._formatPower(gridToHouse)} - ${this._formatPower(gridExport)} + ${this._formatPower(gridExport)} ${this._formatPower(gridToBatt)} ${this._formatPower(batteryDischarge)} @@ -922,56 +1231,65 @@ class PowerFluxCard extends LitElement { ${hasSolar ? html` -
+
this._handleClick(entities.solar)}> ${renderMainIcon('solar', solarVal, iconSolar, solarColor)} - ${renderLabel(labelSolarText, showLabelSolar)} -
${this._formatPower(solarVal)}
+ ${renderSecondaryOrLabel(labelSolarText, showLabelSolar, entities.secondary_solar, hasSecondarySolar)} +
${this._formatPower(solarVal)}
` : ''} ${hasGrid ? html` -
- ${renderMainIcon('grid', gridImport, iconGrid, gridColor)} - ${renderLabel(labelGridText, showLabelGrid)} -
${this._formatPower(gridImport)}
+
this._handleClick(entities.grid_combined || entities.grid)}> + ${renderMainIcon('grid', isGridExporting ? gridExport : gridImport, iconGrid, 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)} - ${renderLabel(labelBatteryText, showLabelBattery)} -
${Math.round(battSoc)}%
+ ${renderSecondaryOrLabel(labelBatteryText, showLabelBattery, entities.secondary_battery, hasSecondaryBattery)} +
${Math.round(battSoc)}%
` : ''} -
- ${renderMainIcon('house', 0, null, houseDominantColor)} +
this._handleClick(entities.house)}> + ${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, '#a855f7')} - ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, '#f97316')} - ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, '#06b6d4')} + ${renderConsumer(showC1, 'c1', 'consumer_1', labelC1, 'car', c1Val, this._getConsumerColor(1))} + ${renderConsumer(showC2, 'c2', 'consumer_2', labelC2, 'heater', c2Val, this._getConsumerColor(2))} + ${renderConsumer(showC3, 'c3', 'consumer_3', labelC3, 'pool', c3Val, this._getConsumerColor(3))}
`; - } + } - render() { - if (!this.config || !this.hass) return html``; - - // SWITCH VIEW BASED ON CONFIG - if (this.config.compact_view === true) { + render() { + if (!this.config || !this.hass) return html``; + + // SWITCH VIEW BASED ON CONFIG + if (this.config.compact_view === true) { return this._renderCompactView(this.config.entities || {}); - } else { + } else { return this._renderStandardView(this.config.entities || {}); + } } } -} -customElements.define("power-flux-card", PowerFluxCard); + customElements.define("power-flux-card", PowerFluxCard); +})(lang_en, lang_de); window.customCards = window.customCards || []; window.customCards.push({