Battery Boost UI: disable button when battery on hold (#28489)
This commit is contained in:
parent
2499c09b94
commit
948dda081c
13 changed files with 132 additions and 39 deletions
|
|
@ -2,7 +2,7 @@
|
|||
<button
|
||||
class="root position-relative"
|
||||
tabindex="0"
|
||||
:class="{ active, belowLimit, full }"
|
||||
:class="{ active, belowLimit, batteryHold, full }"
|
||||
:style="{ '--soc': `${adjustedSoc}%` }"
|
||||
:disabled="disabled"
|
||||
:aria-label="ariaLabel"
|
||||
|
|
@ -17,17 +17,17 @@
|
|||
<div class="progress-bar bg-primary progress-bar-striped progress-bar-animated"></div>
|
||||
</div>
|
||||
<div class="icon-wrapper" :style="iconStyle">
|
||||
<BatteryBoost :active="active && !belowLimit" />
|
||||
<BatteryBoost :active="active && available" />
|
||||
</div>
|
||||
<div class="icon-wrapper text-white" :style="iconActiveStyle">
|
||||
<BatteryBoost :active="active && !belowLimit" />
|
||||
<BatteryBoost :active="active && available" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { CHARGE_MODE, type Timeout } from "@/types/evcc";
|
||||
import { BATTERY_MODE, CHARGE_MODE, type Timeout } from "@/types/evcc";
|
||||
import BatteryBoost from "../MaterialIcon/BatteryBoost.vue";
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -40,6 +40,7 @@ export default defineComponent({
|
|||
batteryBoostLimit: { type: Number, default: 100 },
|
||||
mode: String as PropType<CHARGE_MODE>,
|
||||
batterySoc: { type: Number, default: 0 },
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
},
|
||||
emits: ["updated", "status"],
|
||||
data() {
|
||||
|
|
@ -66,6 +67,12 @@ export default defineComponent({
|
|||
belowLimit(): boolean {
|
||||
return this.batterySoc < this.batteryBoostLimit;
|
||||
},
|
||||
batteryHold(): boolean {
|
||||
return this.batteryMode === BATTERY_MODE.HOLD;
|
||||
},
|
||||
available(): boolean {
|
||||
return !this.belowLimit && !this.batteryHold;
|
||||
},
|
||||
iconStyle() {
|
||||
return {
|
||||
clipPath: this.active ? `inset(0 0 calc(var(--soc)) 0)` : undefined,
|
||||
|
|
@ -76,6 +83,7 @@ export default defineComponent({
|
|||
},
|
||||
ariaLabel(): string {
|
||||
const t = (key: string) => this.$t(`main.loadpointSettings.batteryBoost.${key}`);
|
||||
if (this.batteryHold) return t("stateHold");
|
||||
if (this.active) return t("stateActive");
|
||||
if (this.belowLimit) return t("stateBelowLimit");
|
||||
return t("stateReady");
|
||||
|
|
@ -105,6 +113,12 @@ export default defineComponent({
|
|||
});
|
||||
const newValue = !this.active;
|
||||
|
||||
// battery hold: only show message, don't toggle
|
||||
if (newValue && this.batteryHold) {
|
||||
status("batteryBoostHold");
|
||||
return;
|
||||
}
|
||||
|
||||
// below limit: only show message, don't toggle
|
||||
if (newValue && this.belowLimit) {
|
||||
status("batteryBoostBelowLimit");
|
||||
|
|
@ -157,7 +171,8 @@ export default defineComponent({
|
|||
color: inherit;
|
||||
opacity: 0.25;
|
||||
}
|
||||
.root.belowLimit:not(:disabled) {
|
||||
.root.belowLimit:not(:disabled),
|
||||
.root.batteryHold:not(:disabled) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.root:before,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import type {
|
|||
Vehicle,
|
||||
Forecast,
|
||||
SMART_COST_TYPE,
|
||||
BATTERY_MODE,
|
||||
} from "@/types/evcc";
|
||||
import type { PlanStrategy } from "@/components/ChargingPlans/types";
|
||||
|
||||
|
|
@ -160,6 +161,7 @@ export default defineComponent({
|
|||
batteryBoostLimit: { type: Number, default: 100 },
|
||||
batteryConfigured: Boolean,
|
||||
batterySoc: Number,
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
|
||||
// session
|
||||
sessionEnergy: Number,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
:pvConfigured="pvConfigured"
|
||||
:batteryConfigured="batteryConfigured"
|
||||
:batterySoc="batterySoc"
|
||||
:batteryMode="batteryMode"
|
||||
:forecast="forecast"
|
||||
class="h-100"
|
||||
:class="{ 'loadpoint-unselected': !selected(loadpoint.id) }"
|
||||
|
|
@ -67,7 +68,7 @@ import "@h2d2/shopicons/es/filled/lightning";
|
|||
|
||||
import Loadpoint from "./Loadpoint.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import type { UiLoadpoint, SMART_COST_TYPE, Timeout, Vehicle } from "@/types/evcc";
|
||||
import type { UiLoadpoint, SMART_COST_TYPE, Timeout, Vehicle, BATTERY_MODE } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Loadpoints",
|
||||
|
|
@ -87,6 +88,7 @@ export default defineComponent({
|
|||
pvConfigured: Boolean,
|
||||
batteryConfigured: Boolean,
|
||||
batterySoc: Number,
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
forecast: Object, // as PropType<Forecast>,
|
||||
},
|
||||
emits: ["id-changed"],
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
:pvConfigured="pvConfigured"
|
||||
:batteryConfigured="batteryConfigured"
|
||||
:batterySoc="batterySoc"
|
||||
:batteryMode="batteryMode"
|
||||
:forecast="forecast"
|
||||
:selectedId="selectedLoadpointId"
|
||||
@id-changed="selectedLoadpointChanged"
|
||||
|
|
@ -101,6 +102,7 @@ import type {
|
|||
Sponsor,
|
||||
FatalError,
|
||||
EvOpt,
|
||||
BATTERY_MODE,
|
||||
} from "@/types/evcc";
|
||||
import store from "@/store";
|
||||
import type { Grid } from "./types";
|
||||
|
|
@ -134,7 +136,7 @@ export default defineComponent({
|
|||
batteryDischargeControl: Boolean,
|
||||
batteryGridChargeLimit: { type: [Number, null] as PropType<number | null>, default: null },
|
||||
batteryGridChargeActive: Boolean,
|
||||
batteryMode: String,
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
battery: { type: Object as PropType<Battery> },
|
||||
gridCurrents: Array,
|
||||
prioritySoc: Number,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,13 @@ import LimitSocSelect from "./LimitSocSelect.vue";
|
|||
import LimitEnergySelect from "./LimitEnergySelect.vue";
|
||||
import { distanceUnit, distanceValue } from "@/units.ts";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { CHARGE_MODE, type Forecast, type VehicleStatus, type Vehicle } from "@/types/evcc";
|
||||
import {
|
||||
CHARGE_MODE,
|
||||
type BATTERY_MODE,
|
||||
type Forecast,
|
||||
type VehicleStatus,
|
||||
type Vehicle,
|
||||
} from "@/types/evcc";
|
||||
import type { PlanStrategy } from "@/components/ChargingPlans/types";
|
||||
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
|
||||
|
||||
|
|
@ -121,6 +127,7 @@ export default defineComponent({
|
|||
batteryBoostAvailable: Boolean,
|
||||
batteryBoostLimit: { type: Number, default: 100 },
|
||||
batterySoc: Number,
|
||||
batteryMode: String as PropType<BATTERY_MODE>,
|
||||
enabled: Boolean,
|
||||
heating: Boolean,
|
||||
id: [String, Number],
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export interface State {
|
|||
system?: string;
|
||||
timezone?: string;
|
||||
battery?: Battery;
|
||||
batteryMode?: BATTERY_MODE;
|
||||
pv?: Meter[];
|
||||
aux?: Meter[];
|
||||
ext?: Meter[];
|
||||
|
|
@ -377,6 +378,13 @@ export enum CHARGE_MODE {
|
|||
PV = "pv",
|
||||
}
|
||||
|
||||
export enum BATTERY_MODE {
|
||||
UNKNOWN = "unknown",
|
||||
NORMAL = "normal",
|
||||
HOLD = "hold",
|
||||
CHARGE = "charge",
|
||||
}
|
||||
|
||||
export enum PHASES {
|
||||
AUTO = 0,
|
||||
ONE_PHASE = 1,
|
||||
|
|
|
|||
|
|
@ -1156,6 +1156,7 @@
|
|||
"once": "Boost ist für diesen Ladevorgang aktiviert.",
|
||||
"stateActive": "Batterie Boost aktiv",
|
||||
"stateBelowLimit": "Batterie zu niedrig für Boost",
|
||||
"stateHold": "Batterie gesperrt",
|
||||
"stateReady": "Batterie Boost bereit"
|
||||
},
|
||||
"batteryUsage": "Hausbatterie",
|
||||
|
|
@ -1265,6 +1266,7 @@
|
|||
"batteryBoostBelowLimit": "Batterie zu niedrig für Boost.",
|
||||
"batteryBoostDisabled": "Batterie Boost deaktiviert.",
|
||||
"batteryBoostEnabled": "Boost bis Batterie bei {limit}.",
|
||||
"batteryBoostHold": "Batterie gesperrt. Boost nicht verfügbar.",
|
||||
"charging": "Ladevorgang aktiv …",
|
||||
"cheapEnergyCharging": "Günstige Energie verfügbar.",
|
||||
"cheapEnergyNextStart": "Günstige Energie in {duration}.",
|
||||
|
|
|
|||
|
|
@ -1159,6 +1159,7 @@
|
|||
"once": "Boost active for this charging session.",
|
||||
"stateActive": "Battery boost active",
|
||||
"stateBelowLimit": "Battery too low for boost",
|
||||
"stateHold": "Battery locked",
|
||||
"stateReady": "Battery boost ready"
|
||||
},
|
||||
"batteryUsage": "Home Battery",
|
||||
|
|
@ -1268,6 +1269,7 @@
|
|||
"batteryBoostBelowLimit": "Battery too low for boost.",
|
||||
"batteryBoostDisabled": "Battery boost disabled.",
|
||||
"batteryBoostEnabled": "Boost until battery at {limit}.",
|
||||
"batteryBoostHold": "Battery locked. Boost not available.",
|
||||
"charging": "Charging…",
|
||||
"cheapEnergyCharging": "Cheap energy available.",
|
||||
"cheapEnergyNextStart": "Cheap energy in {duration}.",
|
||||
|
|
|
|||
|
|
@ -1089,7 +1089,7 @@
|
|||
},
|
||||
"/loadpoints/{id}/batteryboost/{enable}": {
|
||||
"post": {
|
||||
"description": "Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit.",
|
||||
"description": "Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit. Note: boost will not work while the battery is on hold (e.g. during fast charging or planned charging with discharge prevention enabled).",
|
||||
"externalDocs": {
|
||||
"url": "https://docs.evcc.io/en/docs/features/battery#battery-boost"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -482,7 +482,7 @@ call removeLoadpointVehicle {
|
|||
|
||||
## setLoadpointBatteryBoost
|
||||
|
||||
Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit.
|
||||
Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit. Note: boost will not work while the battery is on hold (e.g. during fast charging or planned charging with discharge prevention enabled).
|
||||
|
||||
**Tags:** loadpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ paths:
|
|||
post:
|
||||
operationId: setLoadpointBatteryBoost
|
||||
summary: Set battery boost
|
||||
description: "Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit."
|
||||
description: "Enable or disable battery boost. When active, the maximum available home battery power is added until the home battery is drained to configured SoC limit. Note: boost will not work while the battery is on hold (e.g. during fast charging or planned charging with discharge prevention enabled)."
|
||||
externalDocs:
|
||||
url: https://docs.evcc.io/en/docs/features/battery#battery-boost
|
||||
tags:
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ loadpoints:
|
|||
- title: Carport
|
||||
charger: charger
|
||||
mode: now
|
||||
- title: Garage
|
||||
charger: charger2
|
||||
mode: off
|
||||
|
||||
chargers:
|
||||
- name: charger
|
||||
|
|
@ -56,6 +59,23 @@ chargers:
|
|||
source: js
|
||||
script: |
|
||||
16
|
||||
- name: charger2
|
||||
type: custom
|
||||
enable:
|
||||
source: js
|
||||
script:
|
||||
enabled:
|
||||
source: js
|
||||
script: |
|
||||
true
|
||||
status:
|
||||
source: js
|
||||
script: |
|
||||
"C"
|
||||
maxcurrent:
|
||||
source: js
|
||||
script: |
|
||||
16
|
||||
|
||||
tariffs:
|
||||
currency: EUR
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { expectModalHidden, newLoadpoint, addDemoCharger, ChargerStatus } from "./utils";
|
||||
import {
|
||||
expectModalHidden,
|
||||
expectModalVisible,
|
||||
newLoadpoint,
|
||||
addDemoCharger,
|
||||
ChargerStatus,
|
||||
openTopNavigation,
|
||||
} from "./utils";
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const CONFIG_BATTERY = "battery-settings.evcc.yaml";
|
||||
|
|
@ -13,15 +20,12 @@ test.describe("boost", async () => {
|
|||
test("activate and deactivate boost in solar mode", async ({ page }) => {
|
||||
await start(CONFIG_BATTERY);
|
||||
await page.goto("/");
|
||||
const boostButton = page.getByTestId("battery-boost-button");
|
||||
const lp = page.getByTestId("loadpoint").first();
|
||||
const boostButton = lp.getByTestId("battery-boost-button");
|
||||
await expect(boostButton).not.toBeVisible();
|
||||
await page
|
||||
.getByTestId("mode")
|
||||
.first()
|
||||
.getByRole("button", { name: "Solar", exact: true })
|
||||
.click();
|
||||
await page.getByTestId("loadpoint-settings-button").nth(1).click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal");
|
||||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("20 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText(
|
||||
"Allow fast charging from home battery until it's drained to 20%."
|
||||
|
|
@ -41,14 +45,11 @@ test.describe("boost", async () => {
|
|||
test("battery too low for boost when limit above soc", async ({ page }) => {
|
||||
await start(CONFIG_BATTERY);
|
||||
await page.goto("/");
|
||||
const boostButton = page.getByTestId("battery-boost-button");
|
||||
await page
|
||||
.getByTestId("mode")
|
||||
.first()
|
||||
.getByRole("button", { name: "Solar", exact: true })
|
||||
.click();
|
||||
await page.getByTestId("loadpoint-settings-button").nth(1).click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal");
|
||||
const lp = page.getByTestId("loadpoint").first();
|
||||
const boostButton = lp.getByTestId("battery-boost-button");
|
||||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("90 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText("drained to 90%");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
|
@ -61,29 +62,61 @@ test.describe("boost", async () => {
|
|||
test("boost button disabled in fast mode", async ({ page }) => {
|
||||
await start(CONFIG_BATTERY);
|
||||
await page.goto("/");
|
||||
const boostButton = page.getByTestId("battery-boost-button");
|
||||
const lp = page.getByTestId("loadpoint").first();
|
||||
const boostButton = lp.getByTestId("battery-boost-button");
|
||||
// set a boost limit in solar mode so the boost button appears
|
||||
await page
|
||||
.getByTestId("mode")
|
||||
.first()
|
||||
.getByRole("button", { name: "Solar", exact: true })
|
||||
.click();
|
||||
await page.getByTestId("loadpoint-settings-button").nth(1).click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal");
|
||||
await lp.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("20 %");
|
||||
await expect(modal.getByTestId("battery-boost")).toContainText("drained to 20%");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await modal.getByLabel("Close").click();
|
||||
await expectModalHidden(modal);
|
||||
// switch to fast mode and verify boost button is disabled
|
||||
await page.getByTestId("mode").first().getByRole("button", { name: "Fast" }).click();
|
||||
await lp.getByTestId("mode").getByRole("button", { name: "Fast" }).click();
|
||||
await expect(boostButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test("boost button disabled when battery is on hold", async ({ page }) => {
|
||||
await start(CONFIG_BATTERY);
|
||||
await page.goto("/");
|
||||
|
||||
// LP1: set solar mode, configure boost limit
|
||||
const lp1 = page.getByTestId("loadpoint").first();
|
||||
await lp1.getByTestId("mode").getByRole("button", { name: "Solar", exact: true }).click();
|
||||
await lp1.getByTestId("loadpoint-settings-button").last().click();
|
||||
const modal = page.getByTestId("loadpoint-settings-modal").first();
|
||||
await modal.getByTestId("battery-boost-limit").selectOption("0 %");
|
||||
await modal.getByLabel("Close").click();
|
||||
await expectModalHidden(modal);
|
||||
|
||||
// enable "Prevent discharge in fast mode"
|
||||
await openTopNavigation(page);
|
||||
await page.getByTestId("topnavigation-battery").click();
|
||||
const batteryModal = page.getByTestId("battery-settings-modal");
|
||||
await expectModalVisible(batteryModal);
|
||||
await batteryModal.getByLabel("Prevent discharge in fast mode and planned charging.").check();
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(batteryModal);
|
||||
|
||||
// LP2: switch to fast mode → triggers global battery hold
|
||||
const lp2 = page.getByTestId("loadpoint").nth(1);
|
||||
await lp2.getByTestId("mode").getByRole("button", { name: "Fast" }).click();
|
||||
|
||||
// LP1: boost button should show hold state
|
||||
const boostButton = lp1.getByTestId("battery-boost-button");
|
||||
await expect(boostButton).toHaveAttribute("aria-label", "Battery locked");
|
||||
|
||||
// clicking should not change state
|
||||
await boostButton.click();
|
||||
await expect(boostButton).toHaveAttribute("aria-label", "Battery locked");
|
||||
});
|
||||
|
||||
test("boost default for ui-created loadpoint", async ({ page }) => {
|
||||
await start(CONFIG_BATTERY);
|
||||
|
||||
// create a second loadpoint
|
||||
// create a third loadpoint
|
||||
await page.goto("/#/config");
|
||||
await newLoadpoint(page, "New Charger");
|
||||
await addDemoCharger(page, undefined, ChargerStatus.Connected);
|
||||
|
|
@ -94,7 +127,7 @@ test.describe("boost", async () => {
|
|||
// restart evcc to apply new loadpoint
|
||||
await restart(CONFIG_BATTERY);
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||
await expect(page.getByTestId("loadpoint")).toHaveCount(3);
|
||||
|
||||
const lp = page.getByTestId("loadpoint").last();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue