Skip to content

Commit f9b3b8e

Browse files
committed
feat: add automatic remaining device power
Add an optional virtual device that displays home power not accounted for by configured devices. The remaining value is calculated from total home consumption minus the sum of configured device power and is clamped to zero.
1 parent 97d8004 commit f9b3b8e

1 file changed

Lines changed: 82 additions & 11 deletions

File tree

compact-power-card.js

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,27 @@ class CompactPowerCard extends CompactPowerCardBase {
8484
{
8585
name: "enable_device_power_lines",
8686
selector: { boolean: { } },
87-
},
87+
},
88+
{
89+
name: "show_remaining_device",
90+
selector: { boolean: {} },
91+
},
92+
{
93+
name: "remaining_device_name",
94+
selector: { text: {} },
95+
},
96+
{
97+
name: "remaining_device_icon",
98+
selector: { icon: {} },
99+
},
100+
{
101+
name: "remaining_device_color",
102+
selector: { text: {} },
103+
},
104+
{
105+
name: "remaining_device_threshold",
106+
selector: { number: { step: 1 } },
107+
},
88108
{
89109
name: "disable_home_gradient",
90110
selector: { boolean: {} },
@@ -3127,7 +3147,7 @@ class CompactPowerCard extends CompactPowerCardBase {
31273147
? this._config?.entities?.battery_labels || this._config?.entities?.battery?.labels
31283148
: batteryCfg?.labels;
31293149
const batteryLabels = this._normalizeLabels(batteryLabelsSource, null);
3130-
const { sources: normalizedSources } = this._getSourcesConfig();
3150+
let { sources: normalizedSources } = this._getSourcesConfig();
31313151
const enableDevicePowerLines = this._useDevicePowerLines();
31323152
const allowGlow = this._allowGlowEffects();
31333153
const homeGlowOpacity = allowGlow ? 0.3 : 0;
@@ -3369,6 +3389,38 @@ class CompactPowerCard extends CompactPowerCardBase {
33693389
pvUnitRaw || gridUnitRaw || batteryUnitRaw || homeUnitOverride || "W";
33703390
const homeUnit = "W";
33713391
const homeNumericW = this._toWatts(homeNumeric, inferredHomeUnit);
3392+
3393+
// Optional virtual device: all household usage not covered by configured devices.
3394+
if (this._coerceBoolean(this._config?.show_remaining_device, false)) {
3395+
const knownDeviceWatts = normalizedSources.reduce((sum, src) => {
3396+
const entity = src?.entity || null;
3397+
if (!this._isPowerDevice(entity)) return sum;
3398+
const attribute = src?.attribute || null;
3399+
const unit = this.hass?.states?.[entity]?.attributes?.unit_of_measurement || "";
3400+
const meta = this._getPowerMeta(entity, unit, attribute);
3401+
const watts = Number.isFinite(meta?.watts) ? Math.max(meta.watts, 0) : 0;
3402+
return sum + watts;
3403+
}, 0);
3404+
3405+
const remainingWatts = Math.max(
3406+
(Number.isFinite(homeNumericW) ? homeNumericW : 0) - knownDeviceWatts,
3407+
0
3408+
);
3409+
3410+
normalizedSources = [
3411+
...normalizedSources,
3412+
{
3413+
entity: "__cpc_remaining_devices__",
3414+
virtual: true,
3415+
virtual_watts: remainingWatts,
3416+
name: this._config?.remaining_device_name || "Overige",
3417+
icon: this._config?.remaining_device_icon || "mdi:devices",
3418+
color: this._config?.remaining_device_color || "#9e9e9e",
3419+
threshold: this._config?.remaining_device_threshold ?? 5,
3420+
subtract_from_home: false,
3421+
},
3422+
];
3423+
}
33723424
const homeEffectiveW = this._homeEffective || 0;
33733425
const homeEffectiveRender = homeEffectiveW;
33743426
const homeThresholdDisplay = this._toWatts(this._parseThreshold(homeCfg.threshold), "W", true);
@@ -3534,13 +3586,22 @@ class CompactPowerCard extends CompactPowerCardBase {
35343586
const name = src.name || null;
35353587
const nameEntity = name && this.hass?.states?.[name] ? name : null;
35363588
const displayName = nameEntity ? this._formatEntityStateWithUnit(nameEntity) : name;
3537-
const icon = src.icon || this._getEntityIcon(entity, "mdi:power-plug");
3538-
const isPowerDevice = this._isPowerDevice(entity);
3539-
const st = entity ? this.hass?.states?.[entity] : null;
3540-
const raw = attribute ? st?.attributes?.[attribute] : st?.state;
3541-
const isUnavailable = this._isUnavailableState(raw);
3542-
const numeric = isUnavailable ? 0 : this._getNumericMaybe(entity, attribute);
3543-
const unit = st?.attributes?.unit_of_measurement || "";
3589+
const isVirtual = this._coerceBoolean(src.virtual, false);
3590+
const icon = src.icon || (isVirtual ? "mdi:devices" : this._getEntityIcon(entity, "mdi:power-plug"));
3591+
const isPowerDevice = isVirtual || this._isPowerDevice(entity);
3592+
const st = !isVirtual && entity ? this.hass?.states?.[entity] : null;
3593+
const raw = isVirtual
3594+
? src.virtual_watts
3595+
: attribute
3596+
? st?.attributes?.[attribute]
3597+
: st?.state;
3598+
const isUnavailable = isVirtual ? false : this._isUnavailableState(raw);
3599+
const numeric = isVirtual
3600+
? Number(src.virtual_watts) || 0
3601+
: isUnavailable
3602+
? 0
3603+
: this._getNumericMaybe(entity, attribute);
3604+
const unit = isVirtual ? "W" : st?.attributes?.unit_of_measurement || "";
35443605
const decimals = this._getDecimalPlaces(src);
35453606
const numericW = isPowerDevice
35463607
? isUnavailable
@@ -3581,6 +3642,7 @@ class CompactPowerCard extends CompactPowerCardBase {
35813642
hidden,
35823643
numeric: hasPowerNumeric ? numericW : numeric ?? 0,
35833644
isPowerDevice,
3645+
isVirtual,
35843646
threshold,
35853647
forceHideUnderThreshold,
35863648
};
@@ -3589,7 +3651,12 @@ class CompactPowerCard extends CompactPowerCardBase {
35893651
const visibleSources = deviceSources.filter(
35903652
(src) => !(src.forceHideUnderThreshold && src.hidden)
35913653
);
3592-
const maxDevices = Math.min(visibleSources.length, maxItemsByColumns);
3654+
// Show all configured devices, including the virtual remaining-usage device.
3655+
// The previous column-based cap could silently remove the last item (usually "Overige").
3656+
const configuredMaxDevices = this._parseNumberStrict(this._config?.max_device_items);
3657+
const maxDevices = Number.isFinite(configuredMaxDevices)
3658+
? Math.max(1, Math.min(visibleSources.length, Math.floor(configuredMaxDevices)))
3659+
: visibleSources.length;
35933660
const deviceVisible = visibleSources.slice(0, maxDevices);
35943661

35953662
if (maxDevices > 0) {
@@ -3628,7 +3695,7 @@ class CompactPowerCard extends CompactPowerCardBase {
36283695

36293696
const sources = deviceVisible.map((src, idx) => {
36303697
const pos = sourcePositions[idx] || { x: homeX, y: homeRowYBase };
3631-
const key = src.entity || `idx-${src.sourceIndex}`;
3698+
const key = src.isVirtual ? "__cpc_remaining_devices__" : src.entity || `idx-${src.sourceIndex}`;
36323699
let active = false;
36333700
let flicker = false;
36343701
let flickerUntil = 0;
@@ -4270,6 +4337,8 @@ class CompactPowerCard extends CompactPowerCardBase {
42704337
@click=${() => {
42714338
if (src.switchEntity) {
42724339
this._toggleDeviceSwitch(src);
4340+
} else if (src.isVirtual) {
4341+
this._openMoreInfo(homeCfg.entity || null);
42734342
} else {
42744343
this._openMoreInfo(src.entity || null);
42754344
}
@@ -4286,6 +4355,8 @@ class CompactPowerCard extends CompactPowerCardBase {
42864355
@click=${() => {
42874356
if (src.switchEntity) {
42884357
this._toggleDeviceSwitch(src);
4358+
} else if (src.isVirtual) {
4359+
this._openMoreInfo(homeCfg.entity || null);
42894360
} else {
42904361
this._openMoreInfo(src.entity || null);
42914362
}

0 commit comments

Comments
 (0)