mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-30 14:33:13 +00:00
Compare commits
2 Commits
dev
...
energy-bat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45823fe4a8 | ||
|
|
76568379e7 |
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBattery10,
|
||||
mdiBattery20,
|
||||
mdiBattery30,
|
||||
mdiBattery40,
|
||||
mdiBattery50,
|
||||
mdiBattery60,
|
||||
mdiBattery70,
|
||||
mdiBattery80,
|
||||
mdiBattery90,
|
||||
mdiBatteryAlertVariantOutline,
|
||||
mdiBatteryUnknown,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const BATTERY_ICONS = {
|
||||
@@ -12,6 +26,18 @@ const BATTERY_ICONS = {
|
||||
90: "mdi:battery-90",
|
||||
100: "mdi:battery",
|
||||
};
|
||||
const BATTERY_ICON_PATHS = {
|
||||
10: mdiBattery10,
|
||||
20: mdiBattery20,
|
||||
30: mdiBattery30,
|
||||
40: mdiBattery40,
|
||||
50: mdiBattery50,
|
||||
60: mdiBattery60,
|
||||
70: mdiBattery70,
|
||||
80: mdiBattery80,
|
||||
90: mdiBattery90,
|
||||
100: mdiBattery,
|
||||
};
|
||||
const BATTERY_CHARGING_ICONS = {
|
||||
10: "mdi:battery-charging-10",
|
||||
20: "mdi:battery-charging-20",
|
||||
@@ -57,3 +83,15 @@ export const batteryLevelIcon = (
|
||||
}
|
||||
return BATTERY_ICONS[batteryRound];
|
||||
};
|
||||
|
||||
export const batteryLevelIconPath = (batteryLevel: number | string): string => {
|
||||
const batteryValue = Number(batteryLevel);
|
||||
if (isNaN(batteryValue)) {
|
||||
return mdiBatteryUnknown;
|
||||
}
|
||||
if (batteryValue <= 5) {
|
||||
return mdiBatteryAlertVariantOutline;
|
||||
}
|
||||
const batteryRound = Math.round(batteryValue / 10) * 10;
|
||||
return BATTERY_ICON_PATHS[batteryRound];
|
||||
};
|
||||
|
||||
@@ -164,6 +164,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
|
||||
|
||||
const energyUnitClasses = ["energy"];
|
||||
const socStatisticsUnits = ["%"];
|
||||
const socDeviceClass = "battery";
|
||||
|
||||
@customElement("dialog-energy-battery-settings")
|
||||
export class DialogEnergyBatterySettings
|
||||
@@ -180,6 +182,21 @@ export class DialogEnergyBatterySettings
|
||||
@power-config-changed=${this._handlePowerConfigChanged}
|
||||
></ha-energy-power-config>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.value=${this._source.stat_soc}
|
||||
.includeStatisticsUnitOfMeasurement=${socStatisticsUnits}
|
||||
.includeDeviceClass=${socDeviceClass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.state_of_charge"
|
||||
)}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.state_of_charge_helper"
|
||||
)}
|
||||
@value-changed=${this._statisticSocChanged}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -232,6 +249,13 @@ export class DialogEnergyBatterySettings
|
||||
this._powerConfig = ev.detail.powerConfig;
|
||||
}
|
||||
|
||||
private _statisticSocChanged(ev: ValueChangedEvent<string>) {
|
||||
this._source = {
|
||||
...this._source!,
|
||||
stat_soc: ev.detail.value || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
const source: BatterySourceTypeEnergyPreference = {
|
||||
@@ -245,6 +269,10 @@ export class DialogEnergyBatterySettings
|
||||
source.power_config = { ...this._powerConfig };
|
||||
}
|
||||
|
||||
if (this._source!.stat_soc) {
|
||||
source.stat_soc = this._source!.stat_soc;
|
||||
}
|
||||
|
||||
await this._params!.saveCallback(source);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
@@ -257,7 +285,8 @@ export class DialogEnergyBatterySettings
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-statistic-picker {
|
||||
ha-statistic-picker,
|
||||
ha-energy-power-config {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing, svg } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { batteryLevelIconPath } from "../../../../common/entity/battery_icon";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@@ -100,14 +101,34 @@ class HuiEnergyDistrubutionCard
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
if (
|
||||
hasConfigChanged(this, changedProps) ||
|
||||
changedProps.size > 1 ||
|
||||
!changedProps.has("hass") ||
|
||||
(!!this._data?.co2SignalEntity &&
|
||||
this.hass.states[this._data.co2SignalEntity] !==
|
||||
changedProps.get("hass").states[this._data.co2SignalEntity])
|
||||
);
|
||||
!changedProps.has("hass")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const oldStates = changedProps.get("hass").states;
|
||||
if (
|
||||
this._data?.co2SignalEntity &&
|
||||
this.hass.states[this._data.co2SignalEntity] !==
|
||||
oldStates[this._data.co2SignalEntity]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const batteries = this._data
|
||||
? energySourcesByType(this._data.prefs).battery
|
||||
: undefined;
|
||||
if (
|
||||
batteries?.some(
|
||||
(source) =>
|
||||
source.stat_soc &&
|
||||
this.hass.states[source.stat_soc] !== oldStates[source.stat_soc]
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected willUpdate() {
|
||||
@@ -174,10 +195,24 @@ class HuiEnergyDistrubutionCard
|
||||
|
||||
let totalBatteryIn: number | null = null;
|
||||
let totalBatteryOut: number | null = null;
|
||||
let batteryIconPath = mdiBatteryHigh;
|
||||
|
||||
if (hasBattery) {
|
||||
totalBatteryIn = summedData.total.to_battery ?? 0;
|
||||
totalBatteryOut = summedData.total.from_battery ?? 0;
|
||||
|
||||
const socValues = types
|
||||
.battery!.map((source) =>
|
||||
source.stat_soc
|
||||
? Number(this.hass.states[source.stat_soc]?.state)
|
||||
: NaN
|
||||
)
|
||||
.filter((value) => Number.isFinite(value));
|
||||
if (socValues.length) {
|
||||
const averageSoc =
|
||||
socValues.reduce((sum, value) => sum + value, 0) / socValues.length;
|
||||
batteryIconPath = batteryLevelIconPath(averageSoc);
|
||||
}
|
||||
}
|
||||
|
||||
let returnedToGrid: number | null = null;
|
||||
@@ -569,7 +604,7 @@ class HuiEnergyDistrubutionCard
|
||||
${hasBattery
|
||||
? html` <div class="circle-container battery">
|
||||
<div class="circle">
|
||||
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
|
||||
<ha-svg-icon .path=${batteryIconPath}></ha-svg-icon>
|
||||
<span class="battery-in">
|
||||
<ha-svg-icon
|
||||
class="small"
|
||||
|
||||
@@ -4161,6 +4161,8 @@
|
||||
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
|
||||
"energy_into_battery": "Energy charged into the battery",
|
||||
"energy_out_of_battery": "Energy discharged from the battery",
|
||||
"state_of_charge": "Battery state of charge sensor",
|
||||
"state_of_charge_helper": "Sensor reporting battery state of charge as %.",
|
||||
"power": "Battery power",
|
||||
"power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery.",
|
||||
"sensor_type": "Type of power measurement",
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBattery10,
|
||||
mdiBattery50,
|
||||
mdiBattery90,
|
||||
mdiBatteryAlertVariantOutline,
|
||||
mdiBatteryUnknown,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
batteryIcon,
|
||||
batteryLevelIcon,
|
||||
batteryLevelIconPath,
|
||||
} from "../../../src/common/entity/battery_icon";
|
||||
|
||||
describe("batteryIcon", () => {
|
||||
@@ -43,3 +52,24 @@ describe("batteryLevelIcon", () => {
|
||||
expect(batteryLevelIcon("on")).toBe("mdi:battery-alert");
|
||||
});
|
||||
});
|
||||
|
||||
describe("batteryLevelIconPath", () => {
|
||||
it("rounds to the nearest 10% bucket", () => {
|
||||
expect(batteryLevelIconPath(46)).toBe(mdiBattery50);
|
||||
expect(batteryLevelIconPath(94)).toBe(mdiBattery90);
|
||||
expect(batteryLevelIconPath(95)).toBe(mdiBattery);
|
||||
});
|
||||
|
||||
it("returns the alert path for very low levels", () => {
|
||||
expect(batteryLevelIconPath(0)).toBe(mdiBatteryAlertVariantOutline);
|
||||
expect(batteryLevelIconPath(5)).toBe(mdiBatteryAlertVariantOutline);
|
||||
});
|
||||
|
||||
it("returns the 10% bucket just above the alert threshold", () => {
|
||||
expect(batteryLevelIconPath(6)).toBe(mdiBattery10);
|
||||
});
|
||||
|
||||
it("returns the unknown path for non-numeric input", () => {
|
||||
expect(batteryLevelIconPath("unavailable")).toBe(mdiBatteryUnknown);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user