Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 154 additions & 22 deletions www/custom-components/prism-energy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* - Weather effects (rain, snow, fog, sun, moon, stars)
* - Day/Night transitions with house dimming
* - Sunrise/Sunset effects
* - Support for multiple EVs (up to 2)
*
* @version 1.2.6
* @version 1.2.7
* @author BangerTech
*/

Expand All @@ -33,6 +34,9 @@ class PrismEnergyCard extends HTMLElement {
battery_power: "",
home_consumption: "",
ev_power: "",
ev_name: "E-Auto 1",
ev2_power: "",
ev2_name: "E-Auto 2",
autarky: "",
image: "/local/community/Prism-Dashboard/images/prism-energy-home.png",
max_solar_power: 10000,
Expand Down Expand Up @@ -67,6 +71,9 @@ class PrismEnergyCard extends HTMLElement {
ev_pill_top: 72,
ev_pill_left: 22,
ev_pill_scale: 1.0,
ev2_pill_top: 72,
ev2_pill_left: 35,
ev2_pill_scale: 1.0,
// Custom Pills (optional)
custom_pill_1_entity: "",
custom_pill_1_icon: "mdi:thermometer",
Expand Down Expand Up @@ -170,6 +177,21 @@ class PrismEnergyCard extends HTMLElement {
label: "EV Charging Power (optional)",
selector: { entity: { domain: "sensor" } }
},
{
name: "ev_name",
label: "EV1 Display Name (default: E-Auto 1)",
selector: { text: {} }
},
{
name: "ev2_power",
label: "Second EV Charging Power (optional)",
selector: { entity: { domain: "sensor" } }
},
{
name: "ev2_name",
label: "EV2 Display Name (default: E-Auto 2)",
selector: { text: {} }
},
{
name: "autarky",
label: "Autarky % (optional)",
Expand Down Expand Up @@ -365,17 +387,38 @@ class PrismEnergyCard extends HTMLElement {
schema: [
{
name: "ev_pill_top",
label: "Ev pill top",
label: "EV1 pill top",
selector: { number: { min: 0, max: 100, step: 1, mode: "box" } }
},
{
name: "ev_pill_left",
label: "Ev pill left",
label: "EV1 pill left",
selector: { number: { min: 0, max: 100, step: 1, mode: "box" } }
},
{
name: "ev_pill_scale",
label: "Ev pill size",
label: "EV1 pill size",
selector: { number: { min: 0.5, max: 2.0, step: 0.1, mode: "box" } }
}
]
},
{
type: "grid",
name: "",
schema: [
{
name: "ev2_pill_top",
label: "EV2 pill top",
selector: { number: { min: 0, max: 100, step: 1, mode: "box" } }
},
{
name: "ev2_pill_left",
label: "EV2 pill left",
selector: { number: { min: 0, max: 100, step: 1, mode: "box" } }
},
{
name: "ev2_pill_scale",
label: "EV2 pill size",
selector: { number: { min: 0.5, max: 2.0, step: 0.1, mode: "box" } }
}
]
Expand Down Expand Up @@ -563,6 +606,9 @@ class PrismEnergyCard extends HTMLElement {
battery_power: config.battery_power || "",
home_consumption: config.home_consumption || "",
ev_power: config.ev_power || "",
ev_name: config.ev_name || "E-Auto 1",
ev2_power: config.ev2_power || "",
ev2_name: config.ev2_name || "E-Auto 2",
autarky: config.autarky || "",
image: config.image || "/local/community/Prism-Dashboard/images/prism-energy-home.png",
show_details: config.show_details !== false,
Expand Down Expand Up @@ -599,6 +645,9 @@ class PrismEnergyCard extends HTMLElement {
ev_pill_top: config.ev_pill_top ?? 72,
ev_pill_left: config.ev_pill_left ?? 22,
ev_pill_scale: config.ev_pill_scale ?? 1.0,
ev2_pill_top: config.ev2_pill_top ?? 72,
ev2_pill_left: config.ev2_pill_left ?? 35,
ev2_pill_scale: config.ev2_pill_scale ?? 1.0,
// Custom Pills
custom_pill_1_entity: config.custom_pill_1_entity || "",
custom_pill_1_icon: config.custom_pill_1_icon || "mdi:thermometer",
Expand Down Expand Up @@ -694,6 +743,7 @@ class PrismEnergyCard extends HTMLElement {
const batteryPower = this._getStateInWatts(this._config.battery_power, 0);
const homeConsumption = this._getStateInWatts(this._config.home_consumption, 0);
const evPower = this._getStateInWatts(this._config.ev_power, 0);
const ev2Power = this._getStateInWatts(this._config.ev2_power, 0);
const autarky = this._getState(this._config.autarky, 0); // Autarky is percentage

// Determine states for labels
Expand All @@ -703,6 +753,7 @@ class PrismEnergyCard extends HTMLElement {
const isBatteryCharging = batteryPower < -50;
const isBatteryDischarging = batteryPower > 50;
const isEvCharging = evPower > 50;
const isEv2Charging = ev2Power > 50;
const hasBattery = !!this._config.battery_soc;

// Update pill values
Expand Down Expand Up @@ -735,6 +786,12 @@ class PrismEnergyCard extends HTMLElement {
this._updatePillIconClass('.pill-ev .pill-icon', isEvCharging, 'bg-ev');
this._updatePillIconClass('.pill-ev .pill-icon ha-icon', isEvCharging, 'color-ev');
}

if (this._config.ev2_power) {
this._updateElement('.pill-ev2 .pill-val', isEv2Charging ? this._formatPower(ev2Power) : this._t('idle'));
this._updatePillIconClass('.pill-ev2 .pill-icon', isEv2Charging, 'bg-ev');
this._updatePillIconClass('.pill-ev2 .pill-icon ha-icon', isEv2Charging, 'color-ev');
}

if (this._config.autarky) {
this._updateElement('.autarkie-text', `${Math.round(autarky)}%`);
Expand Down Expand Up @@ -859,17 +916,21 @@ class PrismEnergyCard extends HTMLElement {
const batteryPower = this._getStateInWatts(this._config.battery_power, 0);
const homeConsumption = this._getStateInWatts(this._config.home_consumption, 0);
const evPower = this._getStateInWatts(this._config.ev_power, 0);
const ev2Power = this._getStateInWatts(this._config.ev2_power, 0);

const isGridExport = gridPower < -50;
const isEvCharging = evPower > 50;
const isEv2Charging = ev2Power > 50;
const hasEV = !!this._config.ev_power;
const hasEV2 = !!this._config.ev2_power;

const colors = {
solar: '#F59E0B',
grid: '#3B82F6',
battery: '#10B981',
home: '#8B5CF6',
ev: '#EC4899'
ev: '#EC4899',
ev2: '#EF4444',
};

// Update Solar module values if configured
Expand Down Expand Up @@ -911,17 +972,38 @@ class PrismEnergyCard extends HTMLElement {
valEl.style.color = isEvCharging ? colors.ev : 'rgba(255,255,255,0.4)';
}
}
if (consumptionRows[2] && hasEV2) {
const valEl = consumptionRows[2].querySelector('.detail-val');
if (valEl) {
valEl.textContent = isEv2Charging ? this._formatPower(ev2Power) : this._t('idle');
valEl.style.color = isEv2Charging ? colors.ev2 : 'rgba(255,255,255,0.4)';
}
}

// Update Consumption bar
const consumptionBar = this.shadowRoot.querySelector('.details-grid .detail-col:nth-child(3) .detail-bar');
if (consumptionBar) {
if (hasEV && isEvCharging) {
const totalConsumption = homeConsumption + evPower;
if ((hasEV && isEvCharging) || (hasEV2 && isEv2Charging)) {
let totalConsumption = homeConsumption;
if (hasEV && isEvCharging) totalConsumption += evPower;
if (hasEV2 && isEv2Charging) totalConsumption += ev2Power;

const totalPercent = Math.min(100, (totalConsumption / this._config.max_consumption) * 100);
const homeWidth = totalPercent * (homeConsumption / totalConsumption);
const evWidth = totalPercent * (evPower / totalConsumption);
// Use flex-basis and no whitespace between segments
consumptionBar.innerHTML = `<div class="detail-fill-stack"><div class="detail-fill-segment" style="flex-basis:${homeWidth}%;background:${colors.home}"></div><div class="detail-fill-segment" style="flex-basis:${evWidth}%;background:${colors.ev}"></div></div>`;
let evWidth = 0;
let ev2Width = 0;

if (hasEV && isEvCharging) evWidth = totalPercent * (evPower / totalConsumption);
if (hasEV2 && isEv2Charging) ev2Width = totalPercent * (ev2Power / totalConsumption);

// Build stacked bar with all active segments
let stackHTML = '<div class="detail-fill-stack">';
stackHTML += `<div class="detail-fill-segment" style="flex-basis:${homeWidth}%;background:${colors.home}"></div>`;
if (hasEV && isEvCharging) stackHTML += `<div class="detail-fill-segment" style="flex-basis:${evWidth}%;background:${colors.ev}"></div>`;
if (hasEV2 && isEv2Charging) stackHTML += `<div class="detail-fill-segment" style="flex-basis:${ev2Width}%;background:${colors.ev2}"></div>`;
stackHTML += '</div>';

consumptionBar.innerHTML = stackHTML;
} else {
consumptionBar.innerHTML = `<div class="detail-fill" style="width: ${Math.min(100, (homeConsumption / this._config.max_consumption) * 100)}%; background: ${colors.home};"></div>`;
}
Expand Down Expand Up @@ -959,14 +1041,17 @@ class PrismEnergyCard extends HTMLElement {
const batteryPower = this._getStateInWatts(this._config.battery_power, 0);
const homeConsumption = this._getStateInWatts(this._config.home_consumption, 0);
const evPower = this._getStateInWatts(this._config.ev_power, 0);
const ev2Power = this._getStateInWatts(this._config.ev2_power, 0);

const isSolarActive = solarPower > 50;
const isGridImport = gridPower > 50;
const isGridExport = gridPower < -50;
const isBatteryCharging = batteryPower < -50;
const isBatteryDischarging = batteryPower > 50;
const isEvCharging = evPower > 50;
const isEv2Charging = ev2Power > 50;
const hasEV = !!this._config.ev_power;
const hasEV2 = !!this._config.ev2_power;
const hasBattery = !!this._config.battery_soc;

// Show/hide flow groups based on state
Expand All @@ -982,6 +1067,11 @@ class PrismEnergyCard extends HTMLElement {
// EV is treated as sub-load of home - only one line from home to EV
this._setFlowVisibility('flow-home-ev', isEvCharging);
}

if (hasEV2) {
// EV2 is treated as sub-load of home - only one line from home to EV2
this._setFlowVisibility('flow-home-ev2', isEv2Charging);
}
}

_setFlowVisibility(className, visible) {
Expand Down Expand Up @@ -1795,9 +1885,11 @@ class PrismEnergyCard extends HTMLElement {
const batteryPower = this._getStateInWatts(this._config.battery_power, 0);
const homeConsumption = this._getStateInWatts(this._config.home_consumption, 0);
const evPower = this._getStateInWatts(this._config.ev_power, 0);
const ev2Power = this._getStateInWatts(this._config.ev2_power, 0);
const autarky = this._getState(this._config.autarky, 0); // Autarky is percentage

const hasEV = !!this._config.ev_power;
const hasEV2 = !!this._config.ev2_power;
const hasAutarky = !!this._config.autarky;
const hasBattery = !!this._config.battery_soc;
const houseImg = this._config.image;
Expand All @@ -1814,6 +1906,7 @@ class PrismEnergyCard extends HTMLElement {
const isBatteryCharging = batteryPower < -50;
const isBatteryDischarging = batteryPower > 50;
const isEvCharging = evPower > 50;
const isEv2Charging = ev2Power > 50;

// Battery icon based on SOC
let batteryIcon = "mdi:battery";
Expand All @@ -1832,7 +1925,8 @@ class PrismEnergyCard extends HTMLElement {
grid: { x: this._config.grid_pill_left, y: this._config.grid_pill_top, scale: this._config.grid_pill_scale },
home: { x: this._config.home_pill_left, y: this._config.home_pill_top, scale: this._config.home_pill_scale },
battery: { x: this._config.battery_pill_left, y: this._config.battery_pill_top, scale: this._config.battery_pill_scale },
ev: { x: this._config.ev_pill_left, y: this._config.ev_pill_top, scale: this._config.ev_pill_scale }
ev: { x: this._config.ev_pill_left, y: this._config.ev_pill_top, scale: this._config.ev_pill_scale },
ev2: { x: this._config.ev2_pill_left, y: this._config.ev2_pill_top, scale: this._config.ev2_pill_scale }
};

// Helper to calculate control point for smooth curves
Expand All @@ -1856,8 +1950,9 @@ class PrismEnergyCard extends HTMLElement {
batteryToHome: `M ${pillPos.battery.x} ${pillPos.battery.y} Q ${midPoint(pillPos.battery, pillPos.home).x} ${midPoint(pillPos.battery, pillPos.home).y} ${pillPos.home.x} ${pillPos.home.y}`,
batteryToGrid: `M ${pillPos.battery.x} ${pillPos.battery.y} Q ${midPoint(pillPos.battery, pillPos.grid).x} ${midPoint(pillPos.battery, pillPos.grid).y} ${pillPos.grid.x} ${pillPos.grid.y}`,

// EV flow from home (EV is sub-load of home)
homeToEv: `M ${pillPos.home.x} ${pillPos.home.y} Q ${midPoint(pillPos.home, pillPos.ev).x} ${midPoint(pillPos.home, pillPos.ev).y} ${pillPos.ev.x} ${pillPos.ev.y}`
// EV flows from home (EV is sub-load of home)
homeToEv: `M ${pillPos.home.x} ${pillPos.home.y} Q ${midPoint(pillPos.home, pillPos.ev).x} ${midPoint(pillPos.home, pillPos.ev).y} ${pillPos.ev.x} ${pillPos.ev.y}`,
homeToEv2: `M ${pillPos.home.x} ${pillPos.home.y} Q ${midPoint(pillPos.home, pillPos.ev2).x} ${midPoint(pillPos.home, pillPos.ev2).y} ${pillPos.ev2.x} ${pillPos.ev2.y}`
};

// Colors
Expand All @@ -1866,7 +1961,8 @@ class PrismEnergyCard extends HTMLElement {
grid: '#3B82F6',
battery: '#10B981',
home: '#8B5CF6',
ev: '#EC4899'
ev: '#EC4899',
ev2: '#EF4444'
};

this.shadowRoot.innerHTML = `
Expand Down Expand Up @@ -2705,6 +2801,7 @@ class PrismEnergyCard extends HTMLElement {

<!-- EV Flow (sub-load of home) -->
${hasEV ? this._renderFlow(paths.homeToEv, colors.ev, isEvCharging, false, 'flow-home-ev') : ''}
${hasEV2 ? this._renderFlow(paths.homeToEv2, colors.ev2, isEv2Charging, false, 'flow-home-ev2') : ''}
</svg>

<!-- Solar Pill (Top - Roof) - Clickable for history -->
Expand Down Expand Up @@ -2761,7 +2858,20 @@ class PrismEnergyCard extends HTMLElement {
</div>
<div class="pill-content">
<span class="pill-val">${isEvCharging ? this._formatPower(evPower) : this._t('idle')}</span>
<span class="pill-label">EV</span>
<span class="pill-label">EV1</span>
</div>
</div>
` : ''}

<!-- EV2 Pill (Bottom - Second Carport) - Clickable for history -->
${hasEV2 ? `
<div class="pill pill-ev2" style="top: ${pillPos.ev2.y}%; left: ${pillPos.ev2.x}%; --pill-scale: ${pillPos.ev2.scale};" data-entity="${this._config.ev2_power}">
<div class="pill-icon ${isEv2Charging ? 'bg-ev' : 'bg-inactive'}">
<ha-icon icon="mdi:car-electric" class="${isEv2Charging ? 'color-ev' : 'color-inactive'}"></ha-icon>
</div>
<div class="pill-content">
<span class="pill-val">${isEv2Charging ? this._formatPower(ev2Power) : this._t('idle')}</span>
<span class="pill-label">EV2</span>
</div>
</div>
` : ''}
Expand Down Expand Up @@ -2809,18 +2919,40 @@ class PrismEnergyCard extends HTMLElement {
</div>
${hasEV ? `
<div class="detail-row">
<span class="detail-label">E-Auto</span>
<span class="detail-label">${this._config.ev_name}</span>
<span class="detail-val" style="color: ${isEvCharging ? colors.ev : 'rgba(255,255,255,0.4)'};">${isEvCharging ? this._formatPower(evPower) : this._t('idle')}</span>
</div>
` : ''}
${hasEV2 ? `
<div class="detail-row">
<span class="detail-label">${this._config.ev2_name}</span>
<span class="detail-val" style="color: ${isEv2Charging ? colors.ev : 'rgba(255,255,0,0.4)'};">${isEv2Charging ? this._formatPower(ev2Power) : this._t('idle')}</span>
</div>
` : ''}
</div>
<div class="detail-bar">
${hasEV && isEvCharging ? (() => {
const totalConsumption = homeConsumption + evPower;
${(hasEV && isEvCharging) || (hasEV2 && isEv2Charging) ? (() => {
let totalConsumption = homeConsumption;
let segments = `<div class="detail-fill-segment" style="flex-basis:${homeConsumption}px;background:${colors.home}"></div>`;

if (hasEV && isEvCharging) totalConsumption += evPower;
if (hasEV2 && isEv2Charging) totalConsumption += ev2Power;

const totalPercent = Math.min(100, (totalConsumption / this._config.max_consumption) * 100);
const homeWidth = totalPercent * (homeConsumption / totalConsumption);
const evWidth = totalPercent * (evPower / totalConsumption);
return `<div class="detail-fill-stack"><div class="detail-fill-segment" style="flex-basis:${homeWidth}%;background:${colors.home}"></div><div class="detail-fill-segment" style="flex-basis:${evWidth}%;background:${colors.ev}"></div></div>`;
let evWidth = 0;
let ev2Width = 0;

if (hasEV && isEvCharging) evWidth = totalPercent * (evPower / totalConsumption);
if (hasEV2 && isEv2Charging) ev2Width = totalPercent * (ev2Power / totalConsumption);

segments = `<div class="detail-fill-stack">`;
segments += `<div class="detail-fill-segment" style="flex-basis:${homeWidth}%;background:${colors.home}"></div>`;
if (hasEV && isEvCharging) segments += `<div class="detail-fill-segment" style="flex-basis:${evWidth}%;background:${colors.ev}"></div>`;
if (hasEV2 && isEv2Charging) segments += `<div class="detail-fill-segment" style="flex-basis:${ev2Width}%;background:${colors.ev2}"></div>`;
segments += `</div>`;

return segments;
})() : `
<div class="detail-fill" style="width: ${Math.min(100, (homeConsumption / this._config.max_consumption) * 100)}%; background: ${colors.home};"></div>
`}
Expand Down Expand Up @@ -2870,9 +3002,9 @@ window.customCards.push({
});

console.info(
`%c PRISM-ENERGY %c v1.2.6 %c Responsive Details Section `,
`%c PRISM-ENERGY %c v1.2.7 %c Multi-EV Support `,
'background: #F59E0B; color: black; font-weight: bold; padding: 2px 6px; border-radius: 4px 0 0 4px;',
'background: #1e2024; color: white; font-weight: bold; padding: 2px 6px;',
'background: #3B82F6; color: white; font-weight: bold; padding: 2px 6px; border-radius: 0 4px 4px 0;'
'background: #EC4899; color: white; font-weight: bold; padding: 2px 6px; border-radius: 0 4px 4px 0;'
);