From f9b3b8ef5bf782e7a07ef742aa1be138d8bb1fcf Mon Sep 17 00:00:00 2001 From: jefafafah Date: Mon, 29 Jun 2026 16:27:20 +0200 Subject: [PATCH 1/2] 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. --- compact-power-card.js | 93 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/compact-power-card.js b/compact-power-card.js index 334e127..a864f9e 100644 --- a/compact-power-card.js +++ b/compact-power-card.js @@ -84,7 +84,27 @@ class CompactPowerCard extends CompactPowerCardBase { { name: "enable_device_power_lines", selector: { boolean: { } }, - }, + }, + { + name: "show_remaining_device", + selector: { boolean: {} }, + }, + { + name: "remaining_device_name", + selector: { text: {} }, + }, + { + name: "remaining_device_icon", + selector: { icon: {} }, + }, + { + name: "remaining_device_color", + selector: { text: {} }, + }, + { + name: "remaining_device_threshold", + selector: { number: { step: 1 } }, + }, { name: "disable_home_gradient", selector: { boolean: {} }, @@ -3127,7 +3147,7 @@ class CompactPowerCard extends CompactPowerCardBase { ? this._config?.entities?.battery_labels || this._config?.entities?.battery?.labels : batteryCfg?.labels; const batteryLabels = this._normalizeLabels(batteryLabelsSource, null); - const { sources: normalizedSources } = this._getSourcesConfig(); + let { sources: normalizedSources } = this._getSourcesConfig(); const enableDevicePowerLines = this._useDevicePowerLines(); const allowGlow = this._allowGlowEffects(); const homeGlowOpacity = allowGlow ? 0.3 : 0; @@ -3369,6 +3389,38 @@ class CompactPowerCard extends CompactPowerCardBase { pvUnitRaw || gridUnitRaw || batteryUnitRaw || homeUnitOverride || "W"; const homeUnit = "W"; const homeNumericW = this._toWatts(homeNumeric, inferredHomeUnit); + + // Optional virtual device: all household usage not covered by configured devices. + if (this._coerceBoolean(this._config?.show_remaining_device, false)) { + const knownDeviceWatts = normalizedSources.reduce((sum, src) => { + const entity = src?.entity || null; + if (!this._isPowerDevice(entity)) return sum; + const attribute = src?.attribute || null; + const unit = this.hass?.states?.[entity]?.attributes?.unit_of_measurement || ""; + const meta = this._getPowerMeta(entity, unit, attribute); + const watts = Number.isFinite(meta?.watts) ? Math.max(meta.watts, 0) : 0; + return sum + watts; + }, 0); + + const remainingWatts = Math.max( + (Number.isFinite(homeNumericW) ? homeNumericW : 0) - knownDeviceWatts, + 0 + ); + + normalizedSources = [ + ...normalizedSources, + { + entity: "__cpc_remaining_devices__", + virtual: true, + virtual_watts: remainingWatts, + name: this._config?.remaining_device_name || "Overige", + icon: this._config?.remaining_device_icon || "mdi:devices", + color: this._config?.remaining_device_color || "#9e9e9e", + threshold: this._config?.remaining_device_threshold ?? 5, + subtract_from_home: false, + }, + ]; + } const homeEffectiveW = this._homeEffective || 0; const homeEffectiveRender = homeEffectiveW; const homeThresholdDisplay = this._toWatts(this._parseThreshold(homeCfg.threshold), "W", true); @@ -3534,13 +3586,22 @@ class CompactPowerCard extends CompactPowerCardBase { const name = src.name || null; const nameEntity = name && this.hass?.states?.[name] ? name : null; const displayName = nameEntity ? this._formatEntityStateWithUnit(nameEntity) : name; - const icon = src.icon || this._getEntityIcon(entity, "mdi:power-plug"); - const isPowerDevice = this._isPowerDevice(entity); - const st = entity ? this.hass?.states?.[entity] : null; - const raw = attribute ? st?.attributes?.[attribute] : st?.state; - const isUnavailable = this._isUnavailableState(raw); - const numeric = isUnavailable ? 0 : this._getNumericMaybe(entity, attribute); - const unit = st?.attributes?.unit_of_measurement || ""; + const isVirtual = this._coerceBoolean(src.virtual, false); + const icon = src.icon || (isVirtual ? "mdi:devices" : this._getEntityIcon(entity, "mdi:power-plug")); + const isPowerDevice = isVirtual || this._isPowerDevice(entity); + const st = !isVirtual && entity ? this.hass?.states?.[entity] : null; + const raw = isVirtual + ? src.virtual_watts + : attribute + ? st?.attributes?.[attribute] + : st?.state; + const isUnavailable = isVirtual ? false : this._isUnavailableState(raw); + const numeric = isVirtual + ? Number(src.virtual_watts) || 0 + : isUnavailable + ? 0 + : this._getNumericMaybe(entity, attribute); + const unit = isVirtual ? "W" : st?.attributes?.unit_of_measurement || ""; const decimals = this._getDecimalPlaces(src); const numericW = isPowerDevice ? isUnavailable @@ -3581,6 +3642,7 @@ class CompactPowerCard extends CompactPowerCardBase { hidden, numeric: hasPowerNumeric ? numericW : numeric ?? 0, isPowerDevice, + isVirtual, threshold, forceHideUnderThreshold, }; @@ -3589,7 +3651,12 @@ class CompactPowerCard extends CompactPowerCardBase { const visibleSources = deviceSources.filter( (src) => !(src.forceHideUnderThreshold && src.hidden) ); - const maxDevices = Math.min(visibleSources.length, maxItemsByColumns); + // Show all configured devices, including the virtual remaining-usage device. + // The previous column-based cap could silently remove the last item (usually "Overige"). + const configuredMaxDevices = this._parseNumberStrict(this._config?.max_device_items); + const maxDevices = Number.isFinite(configuredMaxDevices) + ? Math.max(1, Math.min(visibleSources.length, Math.floor(configuredMaxDevices))) + : visibleSources.length; const deviceVisible = visibleSources.slice(0, maxDevices); if (maxDevices > 0) { @@ -3628,7 +3695,7 @@ class CompactPowerCard extends CompactPowerCardBase { const sources = deviceVisible.map((src, idx) => { const pos = sourcePositions[idx] || { x: homeX, y: homeRowYBase }; - const key = src.entity || `idx-${src.sourceIndex}`; + const key = src.isVirtual ? "__cpc_remaining_devices__" : src.entity || `idx-${src.sourceIndex}`; let active = false; let flicker = false; let flickerUntil = 0; @@ -4270,6 +4337,8 @@ class CompactPowerCard extends CompactPowerCardBase { @click=${() => { if (src.switchEntity) { this._toggleDeviceSwitch(src); + } else if (src.isVirtual) { + this._openMoreInfo(homeCfg.entity || null); } else { this._openMoreInfo(src.entity || null); } @@ -4286,6 +4355,8 @@ class CompactPowerCard extends CompactPowerCardBase { @click=${() => { if (src.switchEntity) { this._toggleDeviceSwitch(src); + } else if (src.isVirtual) { + this._openMoreInfo(homeCfg.entity || null); } else { this._openMoreInfo(src.entity || null); } From 8d36e14c03bd913f2e18ce855dad2de1ae7e78b9 Mon Sep 17 00:00:00 2001 From: jefafafah Date: Mon, 29 Jun 2026 16:34:54 +0200 Subject: [PATCH 2/2] docs: document remaining device configuration options --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index 988449d..0eb51d3 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ This card is designed for the new [Home Assistant Sections UI](https://www.home- | Curved Line Radius | `curve_factor`| Adjusts the curve radius. `0` to `5`, `1` is default. Set to `0` for straight lines. | | Font Size Multiplier | `font_size_multiplier`| Multiplies text sizes on top of the automatic `--cpc-scale`. Default `1.0`. | | Device Power Lines | `show_device_power_lines`| Set to `true` to light up devices when power is flowing beyond a threshold. Default `false` | +| Remaining Device | `show_remaining_device` | Set to `true` to add a virtual device showing home power not accounted for by configured devices. Default `false`. | +| Remaining Device Name | `remaining_device_name` | Display name for the remaining device. Default: `Overige`. | +| Remaining Device Icon | `remaining_device_icon` | MDI icon for the remaining device. Default: `mdi:devices`. | +| Remaining Device Color | `remaining_device_color` | Color for the remaining device icon and label. Default: `#9e9e9e`. | +| Remaining Device Threshold | `remaining_device_threshold` | Hides the remaining device when below this threshold in watts. Default: `5`. | | Home Icon Gradient | `disable_home_gradient`| Set to `true` if you want the home icon to be a single colour. | | Remove Glow Effects | `remove_glow_effects`| Set to `true` to disable drop shadow/glow effects. Default `false` (dark mode only). | | Hide Card Background | `hide_card_background`| Set to `true` to hide the standard HA card background. Default `false`. | @@ -241,6 +246,34 @@ Example below of how devices can be setup: threshold: 50 ``` +## Remaining Device + +The remaining device is an optional virtual device that displays the portion of home power consumption not accounted for by configured devices. + +Enable it with `show_remaining_device: true` at the card level. The value is calculated as: + +`home power − sum of configured device power` + +The result is clamped to zero and exists only inside the card — no Home Assistant template sensor is required. When tapped, the remaining device opens the more-info panel of the home entity. + +| Name | Setting slug | Default | +| ---- | ------------ | ------- | +| Enable | `show_remaining_device` | `false` | +| Name | `remaining_device_name` | `Overige` | +| Icon | `remaining_device_icon` | `mdi:devices` | +| Color | `remaining_device_color` | `#9e9e9e` | +| Threshold | `remaining_device_threshold` | `5` | + +Example: + +```yaml +show_remaining_device: true +remaining_device_name: Other +remaining_device_icon: mdi:help-circle +remaining_device_color: "#9e9e9e" +remaining_device_threshold: 10 +``` + ## Labels (per pv/grid/battery) Labels can be used to display "other" information - that can be more power stats, energy stats, weather, whatever you want. Note: these are just labels, they do not factor into the power diagram or calculations at all.