mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Support for hierarchy of individual energy devices (#23185)
* Support for hierarchy of individual energy devices * better config ui * replace parent_stat w included_in_stat * semi-working * update order suffix in id when hidden changes * rollback some ordering changes, update strings * Remove hidden tracking, add untracked label to name. * Update dialog-energy-device-settings.ts * Change sort algorithm
This commit is contained in:
parent
8dab7c598e
commit
6fbc7b2efe
@ -96,6 +96,7 @@ export interface DeviceConsumptionEnergyPreference {
|
|||||||
// This is an ever increasing value
|
// This is an ever increasing value
|
||||||
stat_consumption: string;
|
stat_consumption: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
included_in_stat?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FlowFromGridSourceEnergyPreference {
|
export interface FlowFromGridSourceEnergyPreference {
|
||||||
|
@ -147,6 +147,7 @@ export class EnergyDeviceSettings extends LitElement {
|
|||||||
const origDevice: DeviceConsumptionEnergyPreference =
|
const origDevice: DeviceConsumptionEnergyPreference =
|
||||||
ev.currentTarget.closest(".row").device;
|
ev.currentTarget.closest(".row").device;
|
||||||
showEnergySettingsDeviceDialog(this, {
|
showEnergySettingsDeviceDialog(this, {
|
||||||
|
statsMetadata: this.statsMetadata,
|
||||||
device: { ...origDevice },
|
device: { ...origDevice },
|
||||||
device_consumptions: this.preferences
|
device_consumptions: this.preferences
|
||||||
.device_consumption as DeviceConsumptionEnergyPreference[],
|
.device_consumption as DeviceConsumptionEnergyPreference[],
|
||||||
@ -163,6 +164,7 @@ export class EnergyDeviceSettings extends LitElement {
|
|||||||
|
|
||||||
private _addDevice() {
|
private _addDevice() {
|
||||||
showEnergySettingsDeviceDialog(this, {
|
showEnergySettingsDeviceDialog(this, {
|
||||||
|
statsMetadata: this.statsMetadata,
|
||||||
device_consumptions: this.preferences
|
device_consumptions: this.preferences
|
||||||
.device_consumption as DeviceConsumptionEnergyPreference[],
|
.device_consumption as DeviceConsumptionEnergyPreference[],
|
||||||
saveCallback: async (device) => {
|
saveCallback: async (device) => {
|
||||||
@ -188,12 +190,21 @@ export class EnergyDeviceSettings extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this._savePreferences({
|
const newPrefs = {
|
||||||
...this.preferences,
|
...this.preferences,
|
||||||
device_consumption: this.preferences.device_consumption.filter(
|
device_consumption: this.preferences.device_consumption.filter(
|
||||||
(device) => device !== deviceToDelete
|
(device) => device !== deviceToDelete
|
||||||
),
|
),
|
||||||
|
};
|
||||||
|
newPrefs.device_consumption.forEach((d, idx) => {
|
||||||
|
if (d.included_in_stat === deviceToDelete.stat_consumption) {
|
||||||
|
newPrefs.device_consumption[idx] = {
|
||||||
|
...newPrefs.device_consumption[idx],
|
||||||
|
};
|
||||||
|
delete newPrefs.device_consumption[idx].included_in_stat;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
await this._savePreferences(newPrefs);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
|
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,16 @@ import type { CSSResultGroup } from "lit";
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
import "../../../../components/entity/ha-entity-picker";
|
import "../../../../components/entity/ha-entity-picker";
|
||||||
import "../../../../components/entity/ha-statistic-picker";
|
import "../../../../components/entity/ha-statistic-picker";
|
||||||
import "../../../../components/ha-dialog";
|
import "../../../../components/ha-dialog";
|
||||||
import "../../../../components/ha-formfield";
|
import "../../../../components/ha-formfield";
|
||||||
import "../../../../components/ha-radio";
|
import "../../../../components/ha-radio";
|
||||||
|
import "../../../../components/ha-select";
|
||||||
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||||
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||||
|
import { getStatisticLabel } from "../../../../data/recorder";
|
||||||
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
@ -36,19 +39,45 @@ export class DialogEnergyDeviceSettings
|
|||||||
|
|
||||||
private _excludeList?: string[];
|
private _excludeList?: string[];
|
||||||
|
|
||||||
|
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||||
|
|
||||||
public async showDialog(
|
public async showDialog(
|
||||||
params: EnergySettingsDeviceDialogParams
|
params: EnergySettingsDeviceDialogParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
this._device = this._params.device;
|
||||||
|
this._computePossibleParents();
|
||||||
this._energy_units = (
|
this._energy_units = (
|
||||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||||
).units;
|
).units;
|
||||||
this._device = this._params.device;
|
|
||||||
this._excludeList = this._params.device_consumptions
|
this._excludeList = this._params.device_consumptions
|
||||||
.map((entry) => entry.stat_consumption)
|
.map((entry) => entry.stat_consumption)
|
||||||
.filter((id) => id !== this._device?.stat_consumption);
|
.filter((id) => id !== this._device?.stat_consumption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _computePossibleParents() {
|
||||||
|
if (!this._device || !this._params) {
|
||||||
|
this._possibleParents = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const children: string[] = [];
|
||||||
|
const devices = this._params.device_consumptions;
|
||||||
|
function getChildren(stat) {
|
||||||
|
devices.forEach((d) => {
|
||||||
|
if (d.included_in_stat === stat) {
|
||||||
|
children.push(d.stat_consumption);
|
||||||
|
getChildren(d.stat_consumption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getChildren(this._device.stat_consumption);
|
||||||
|
this._possibleParents = this._params.device_consumptions.filter(
|
||||||
|
(d) =>
|
||||||
|
d.stat_consumption !== this._device!.stat_consumption &&
|
||||||
|
!children.includes(d.stat_consumption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this._params = undefined;
|
this._params = undefined;
|
||||||
this._device = undefined;
|
this._device = undefined;
|
||||||
@ -105,10 +134,46 @@ export class DialogEnergyDeviceSettings
|
|||||||
type="text"
|
type="text"
|
||||||
.disabled=${!this._device}
|
.disabled=${!this._device}
|
||||||
.value=${this._device?.name || ""}
|
.value=${this._device?.name || ""}
|
||||||
|
.placeholder=${this._device
|
||||||
|
? getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
this._device.stat_consumption,
|
||||||
|
this._params?.statsMetadata?.[this._device.stat_consumption]
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
@input=${this._nameChanged}
|
@input=${this._nameChanged}
|
||||||
>
|
>
|
||||||
</ha-textfield>
|
</ha-textfield>
|
||||||
|
|
||||||
|
<ha-select
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption.dialog.included_in_device"
|
||||||
|
)}
|
||||||
|
.value=${this._device?.included_in_stat || ""}
|
||||||
|
.helper=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption.dialog.included_in_device_helper"
|
||||||
|
)}
|
||||||
|
.disabled=${!this._device}
|
||||||
|
@selected=${this._parentSelected}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
${this._possibleParents.map(
|
||||||
|
(stat) => html`
|
||||||
|
<mwc-list-item .value=${stat.stat_consumption}
|
||||||
|
>${stat.name ||
|
||||||
|
getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
stat.stat_consumption,
|
||||||
|
this._params?.statsMetadata?.[stat.stat_consumption]
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-select>
|
||||||
|
|
||||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||||
${this.hass.localize("ui.common.cancel")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
@ -129,6 +194,7 @@ export class DialogEnergyDeviceSettings
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._device = { stat_consumption: ev.detail.value };
|
this._device = { stat_consumption: ev.detail.value };
|
||||||
|
this._computePossibleParents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _nameChanged(ev) {
|
private _nameChanged(ev) {
|
||||||
@ -142,6 +208,17 @@ export class DialogEnergyDeviceSettings
|
|||||||
this._device = newDevice;
|
this._device = newDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _parentSelected(ev) {
|
||||||
|
const newDevice = {
|
||||||
|
...this._device!,
|
||||||
|
included_in_stat: ev.target!.value,
|
||||||
|
} as DeviceConsumptionEnergyPreference;
|
||||||
|
if (!newDevice.included_in_stat) {
|
||||||
|
delete newDevice.included_in_stat;
|
||||||
|
}
|
||||||
|
this._device = newDevice;
|
||||||
|
}
|
||||||
|
|
||||||
private async _save() {
|
private async _save() {
|
||||||
try {
|
try {
|
||||||
await this._params!.saveCallback(this._device!);
|
await this._params!.saveCallback(this._device!);
|
||||||
@ -158,6 +235,10 @@ export class DialogEnergyDeviceSettings
|
|||||||
ha-statistic-picker {
|
ha-statistic-picker {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
ha-select {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
ha-textfield {
|
ha-textfield {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -72,6 +72,7 @@ export interface EnergySettingsWaterDialogParams {
|
|||||||
export interface EnergySettingsDeviceDialogParams {
|
export interface EnergySettingsDeviceDialogParams {
|
||||||
device?: DeviceConsumptionEnergyPreference;
|
device?: DeviceConsumptionEnergyPreference;
|
||||||
device_consumptions: DeviceConsumptionEnergyPreference[];
|
device_consumptions: DeviceConsumptionEnergyPreference[];
|
||||||
|
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,6 +216,16 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
const computedStyle = getComputedStyle(this);
|
const computedStyle = getComputedStyle(this);
|
||||||
|
|
||||||
|
const devices = energyData.prefs.device_consumption;
|
||||||
|
|
||||||
|
const childMap: Record<string, string[]> = {};
|
||||||
|
devices.forEach((d) => {
|
||||||
|
if (d.included_in_stat) {
|
||||||
|
childMap[d.included_in_stat] = childMap[d.included_in_stat] || [];
|
||||||
|
childMap[d.included_in_stat].push(d.stat_consumption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const growthValues = {};
|
const growthValues = {};
|
||||||
energyData.prefs.device_consumption.forEach((device) => {
|
energyData.prefs.device_consumption.forEach((device) => {
|
||||||
const value =
|
const value =
|
||||||
@ -225,11 +235,22 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
growthValues[device.stat_consumption] = value;
|
growthValues[device.stat_consumption] = value;
|
||||||
});
|
});
|
||||||
|
const growthValuesExChildren = {};
|
||||||
|
energyData.prefs.device_consumption.forEach((device) => {
|
||||||
|
growthValuesExChildren[device.stat_consumption] = (
|
||||||
|
childMap[device.stat_consumption] || []
|
||||||
|
).reduce(
|
||||||
|
(acc, child) => acc - growthValues[child],
|
||||||
|
growthValues[device.stat_consumption]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const sorted_devices = energyData.prefs.device_consumption.map(
|
const sorted_devices = energyData.prefs.device_consumption.map(
|
||||||
(device) => device.stat_consumption
|
(device) => device.stat_consumption
|
||||||
);
|
);
|
||||||
sorted_devices.sort((a, b) => growthValues[b] - growthValues[a]);
|
sorted_devices.sort(
|
||||||
|
(a, b) => growthValuesExChildren[b] - growthValuesExChildren[a]
|
||||||
|
);
|
||||||
|
|
||||||
const datasets: BarSeriesOption[] = [];
|
const datasets: BarSeriesOption[] = [];
|
||||||
|
|
||||||
@ -254,6 +275,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
energyData.statsMetadata,
|
energyData.statsMetadata,
|
||||||
energyData.prefs.device_consumption,
|
energyData.prefs.device_consumption,
|
||||||
sorted_devices,
|
sorted_devices,
|
||||||
|
childMap,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -268,25 +290,24 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
);
|
);
|
||||||
datasets.push(untrackedCompareData);
|
datasets.push(untrackedCompareData);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
// add empty dataset so compare bars are first
|
// add empty dataset so compare bars are first
|
||||||
// `stack: devices` so it doesn't take up space yet
|
// `stack: devices` so it doesn't take up space yet
|
||||||
const firstId =
|
|
||||||
energyData.prefs.device_consumption[0]?.stat_consumption ?? "untracked";
|
|
||||||
datasets.push({
|
datasets.push({
|
||||||
id: "compare-" + firstId,
|
id: "compare-placeholder",
|
||||||
type: "bar",
|
type: "bar",
|
||||||
stack: "devices",
|
stack: energyData.statsCompare ? "devicesCompare" : "devices",
|
||||||
data: [],
|
data: [],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const processedData = this._processDataSet(
|
const processedData = this._processDataSet(
|
||||||
computedStyle,
|
computedStyle,
|
||||||
data,
|
data,
|
||||||
energyData.statsMetadata,
|
energyData.statsMetadata,
|
||||||
energyData.prefs.device_consumption,
|
energyData.prefs.device_consumption,
|
||||||
sorted_devices
|
sorted_devices,
|
||||||
|
childMap
|
||||||
);
|
);
|
||||||
|
|
||||||
datasets.push(...processedData);
|
datasets.push(...processedData);
|
||||||
@ -377,6 +398,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||||
devices: DeviceConsumptionEnergyPreference[],
|
devices: DeviceConsumptionEnergyPreference[],
|
||||||
sorted_devices: string[],
|
sorted_devices: string[],
|
||||||
|
childMap: Record<string, string[]>,
|
||||||
compare = false
|
compare = false
|
||||||
) {
|
) {
|
||||||
const data: BarSeriesOption[] = [];
|
const data: BarSeriesOption[] = [];
|
||||||
@ -400,7 +422,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
const consumptionData: BarSeriesOption["data"] = [];
|
const consumptionData: BarSeriesOption["data"] = [];
|
||||||
|
|
||||||
// Process gas consumption data.
|
// Process device consumption data.
|
||||||
if (source.stat_consumption in statistics) {
|
if (source.stat_consumption in statistics) {
|
||||||
const stats = statistics[source.stat_consumption];
|
const stats = statistics[source.stat_consumption];
|
||||||
|
|
||||||
@ -415,7 +437,15 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
if (prevStart === point.start) {
|
if (prevStart === point.start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const dataPoint = [point.start, point.change];
|
let sumChildren = 0;
|
||||||
|
const children = childMap[source.stat_consumption] || [];
|
||||||
|
children.forEach((c) => {
|
||||||
|
const cStats = statistics[c];
|
||||||
|
sumChildren +=
|
||||||
|
cStats?.find((cStat) => cStat.start === point.start)?.change || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataPoint = [point.start, point.change - sumChildren];
|
||||||
if (compare) {
|
if (compare) {
|
||||||
dataPoint[2] = dataPoint[0];
|
dataPoint[2] = dataPoint[0];
|
||||||
dataPoint[0] = compareTransform(new Date(point.start)).getTime();
|
dataPoint[0] = compareTransform(new Date(point.start)).getTime();
|
||||||
@ -425,6 +455,17 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name =
|
||||||
|
(source.name ||
|
||||||
|
getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
source.stat_consumption,
|
||||||
|
statisticsMetaData[source.stat_consumption]
|
||||||
|
)) +
|
||||||
|
(source.stat_consumption in childMap
|
||||||
|
? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked")})`
|
||||||
|
: "");
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
type: "bar",
|
type: "bar",
|
||||||
cursor: "default",
|
cursor: "default",
|
||||||
@ -432,13 +473,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
id: compare
|
id: compare
|
||||||
? `compare-${source.stat_consumption}-${order}`
|
? `compare-${source.stat_consumption}-${order}`
|
||||||
: `${source.stat_consumption}-${order}`,
|
: `${source.stat_consumption}-${order}`,
|
||||||
name:
|
name,
|
||||||
source.name ||
|
|
||||||
getStatisticLabel(
|
|
||||||
this.hass,
|
|
||||||
source.stat_consumption,
|
|
||||||
statisticsMetaData[source.stat_consumption]
|
|
||||||
),
|
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderColor: compare ? color + "7F" : color,
|
borderColor: compare ? color + "7F" : color,
|
||||||
},
|
},
|
||||||
@ -451,16 +486,17 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
return sorted_devices
|
return sorted_devices
|
||||||
.map(
|
.map(
|
||||||
(device) =>
|
(device) =>
|
||||||
data.find((d) => {
|
data.find((d) => this._getStatIdFromId(d.id as string) === device)!
|
||||||
const id = (d.id as string)
|
|
||||||
.replace(/^compare-/, "") // Remove compare- prefix
|
|
||||||
.replace(/-\d+$/, ""); // Remove numeric suffix
|
|
||||||
return id === device;
|
|
||||||
})!
|
|
||||||
)
|
)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getStatIdFromId(id: string): string {
|
||||||
|
return id
|
||||||
|
.replace(/^compare-/, "") // Remove compare- prefix
|
||||||
|
.replace(/-\d+$/, ""); // Remove numeric suffix
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
.card-header {
|
.card-header {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
@ -2897,7 +2897,9 @@
|
|||||||
"header": "Add a device",
|
"header": "Add a device",
|
||||||
"display_name": "Display name",
|
"display_name": "Display name",
|
||||||
"device_consumption_energy": "Device energy consumption",
|
"device_consumption_energy": "Device energy consumption",
|
||||||
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}."
|
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
|
||||||
|
"included_in_device": "Upstream device",
|
||||||
|
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -6422,7 +6424,8 @@
|
|||||||
"previous_energy_usage": "Previous energy usage"
|
"previous_energy_usage": "Previous energy usage"
|
||||||
},
|
},
|
||||||
"energy_devices_detail_graph": {
|
"energy_devices_detail_graph": {
|
||||||
"untracked_consumption": "Untracked consumption"
|
"untracked_consumption": "Untracked consumption",
|
||||||
|
"untracked": "untracked"
|
||||||
},
|
},
|
||||||
"carbon_consumed_gauge": {
|
"carbon_consumed_gauge": {
|
||||||
"card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!",
|
"card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user