Battery Boost Button & Limit (BC) (#27392)

This commit is contained in:
Michael Geers 2026-02-17 12:40:02 +01:00 • committed by GitHub
parent 7f772bad73
commit 0046661568
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 805 additions and 146 deletions

View file

@ -68,6 +68,7 @@
--bs-primary: var(--evcc-darker-green);
--bs-primary-rgb: var(--evcc-darker-green-rgb);
--bs-primary-dim: color-mix(in srgb, var(--bs-primary) 50%, transparent);
--bs-warning: var(--evcc-orange);
--bs-warning-rgb: var(--evcc-orange-rgb);
@ -106,6 +107,7 @@
--evcc-accent2: var(--evcc-dark-green);
--evcc-accent3: var(--evcc-darker-green);
--bs-primary: var(--evcc-dark-green);
--bs-primary-dim: color-mix(in srgb, var(--bs-primary) 70%, transparent);
--bs-border-color-translucent: rgba(255, 255, 255, 0.175);
--bs-code-color: var(--evcc-dark-green);
--evcc-backdrop: #000000aa; /* black + transparent */

View file

@ -0,0 +1,78 @@
import BatteryBoostButton from "./BatteryBoostButton.vue";
import { CHARGE_MODE } from "@/types/evcc";
import type { Meta, StoryFn } from "@storybook/vue3";
export default {
title: "Loadpoints/BatteryBoostButton",
component: BatteryBoostButton,
argTypes: {
batteryBoost: { control: "boolean" },
batteryBoostLimit: { control: { type: "range", min: 0, max: 100, step: 5 } },
batterySoc: { control: { type: "range", min: 0, max: 100, step: 5 } },
mode: { control: "select", options: Object.values(CHARGE_MODE) },
},
parameters: {
layout: "centered",
},
} as Meta<typeof BatteryBoostButton>;
const socLevels = [0, 20, 40, 60, 80, 100];
interface RowConfig {
label: string;
batteryBoost: boolean;
batteryBoostLimit: number;
mode: CHARGE_MODE;
}
const rows: RowConfig[] = [
{ label: "Ready (limit 0)", batteryBoost: false, batteryBoostLimit: 0, mode: CHARGE_MODE.PV },
{ label: "Enabled (limit 0)", batteryBoost: true, batteryBoostLimit: 0, mode: CHARGE_MODE.PV },
{ label: "Ready (limit 50)", batteryBoost: false, batteryBoostLimit: 50, mode: CHARGE_MODE.PV },
{ label: "Enabled (limit 50)", batteryBoost: true, batteryBoostLimit: 50, mode: CHARGE_MODE.PV },
{ label: "Disabled", batteryBoost: false, batteryBoostLimit: 50, mode: CHARGE_MODE.OFF },
];
export const AllStates: StoryFn<typeof BatteryBoostButton> = () => ({
components: { BatteryBoostButton },
setup() {
return { socLevels, rows };
},
template: `
<div style="font-family: sans-serif; font-size: 12px;">
<div style="display: grid; grid-template-columns: 140px repeat(6, 1fr); gap: 16px; align-items: center; text-align: center;">
<div></div>
<div v-for="soc in socLevels" :key="'h-'+soc" style="color: #888; font-weight: 600;">{{ soc }}%</div>
<template v-for="row in rows" :key="row.label">
<div style="text-align: right; font-weight: 600;">{{ row.label }}</div>
<div v-for="soc in socLevels" :key="row.label+'-'+soc">
<BatteryBoostButton
:batteryBoost="row.batteryBoost"
:batteryBoostLimit="row.batteryBoostLimit"
:mode="row.mode"
:batterySoc="soc"
@updated="() => {}"
/>
</div>
</template>
</div>
</div>
`,
});
AllStates.storyName = "All States (grid overview)";
const Template: StoryFn<typeof BatteryBoostButton> = (args) => ({
components: { BatteryBoostButton },
setup() {
return { args };
},
template: '<BatteryBoostButton v-bind="args" @updated="() => {}" />',
});
export const Playground = Template.bind({});
Playground.args = {
batteryBoost: false,
batteryBoostLimit: 50,
batterySoc: 75,
mode: CHARGE_MODE.PV,
};

View file

@ -0,0 +1,259 @@
<template>
<button
class="root position-relative"
tabindex="0"
:class="{ active, belowLimit, full }"
:style="{ '--soc': `${adjustedSoc}%` }"
:disabled="disabled"
:aria-label="ariaLabel"
data-testid="battery-boost-button"
@click="toggle"
>
<div
v-if="active"
class="progress position-absolute"
:style="{ height: `${adjustedSoc}%` }"
>
<div class="progress-bar bg-primary progress-bar-striped progress-bar-animated"></div>
</div>
<div class="icon-wrapper" :style="iconStyle">
<BatteryBoost :active="active && !belowLimit" />
</div>
<div class="icon-wrapper text-white" :style="iconActiveStyle">
<BatteryBoost :active="active && !belowLimit" />
</div>
</button>
</template>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { CHARGE_MODE, type Timeout } from "@/types/evcc";
import BatteryBoost from "../MaterialIcon/BatteryBoost.vue";
export default defineComponent({
name: "BatteryBoostButton",
components: {
BatteryBoost,
},
props: {
batteryBoost: Boolean,
batteryBoostLimit: { type: Number, default: 100 },
mode: String as PropType<CHARGE_MODE>,
batterySoc: { type: Number, default: 0 },
},
emits: ["updated", "status"],
data() {
return {
selected: null as boolean | null,
timeout: null as Timeout | null,
};
},
computed: {
active(): boolean {
return this.selected ?? this.batteryBoost;
},
disabled() {
return this.mode && [CHARGE_MODE.OFF, CHARGE_MODE.NOW].includes(this.mode);
},
adjustedSoc(): number {
const range = 100 - this.batteryBoostLimit;
if (range <= 0) return 0;
return Math.max(
0,
Math.min(100, ((this.batterySoc - this.batteryBoostLimit) / range) * 100)
);
},
belowLimit(): boolean {
return this.batterySoc < this.batteryBoostLimit;
},
iconStyle() {
return {
clipPath: this.active ? `inset(0 0 calc(var(--soc)) 0)` : undefined,
};
},
full(): boolean {
return !this.active && this.adjustedSoc >= 90;
},
ariaLabel(): string {
const t = (key: string) => this.$t(`main.loadpointSettings.batteryBoost.${key}`);
if (this.active) return t("stateActive");
if (this.belowLimit) return t("stateBelowLimit");
return t("stateReady");
},
iconActiveStyle() {
return {
opacity: this.active ? 1 : 0,
clipPath: this.active ? `inset(calc(100% - var(--soc)) 0 0 0)` : undefined,
};
},
},
watch: {
batteryBoost() {
this.clearSelected();
},
},
unmounted() {
this.clearSelected();
},
methods: {
toggle() {
(this.$el as HTMLElement).blur();
const status = (key: string, params?: Record<string, unknown>, type?: string) =>
this.$emit("status", {
message: this.$t(`main.vehicleStatus.${key}`, params ?? {}),
type,
});
const newValue = !this.active;
// below limit: only show message, don't toggle
if (newValue && this.belowLimit) {
status("batteryBoostBelowLimit");
return;
}
// optimistic update, instant user feedback
this.selected = newValue;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(() => this.clearSelected(), 5000);
if (newValue) {
status("batteryBoostEnabled", { limit: `${this.batteryBoostLimit}%` }, "primary");
} else {
status("batteryBoostDisabled");
}
this.$emit("updated", newValue);
},
clearSelected() {
this.selected = null;
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
},
},
});
</script>
<style scoped>
.root {
--size: 32px;
height: var(--size);
width: var(--size);
border-radius: var(--bs-border-radius);
overflow: hidden;
border: none;
background: none;
padding: 0;
color: var(--evcc-default-text);
opacity: 1;
transition: opacity var(--evcc-transition-fast) linear;
}
.root:focus,
.root:active {
outline: var(--bs-focus-ring-width) solid var(--bs-focus-ring-color);
outline-width: var(--bs-focus-ring-width);
}
.root:disabled {
color: inherit;
opacity: 0.25;
}
.root.belowLimit:not(:disabled) {
opacity: 0.5;
}
.root:before,
.root:after {
content: "";
position: absolute;
inset: 0;
border-color: var(--bs-primary);
border-radius: var(--bs-border-radius);
border-width: 2px;
border-style: solid;
transition: opacity var(--evcc-transition-fast) linear;
}
.root:before {
opacity: 0.25;
clip-path: inset(0 0 calc(var(--soc)) 0);
}
.root:after {
clip-path: inset(calc(100% - var(--soc)) 0 0 0);
}
.root:hover:before {
opacity: 0.5;
}
.root.active:after {
display: none;
}
.root.full:after {
clip-path: none;
background: conic-gradient(
from var(--border-angle, 0deg),
var(--bs-primary-dim) 0%,
var(--bs-primary) 12%,
var(--bs-primary-dim) 33%,
var(--bs-primary) 45%,
var(--bs-primary-dim) 66%,
var(--bs-primary) 78%,
var(--bs-primary-dim) 100%
);
border: none;
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
padding: 2px;
animation: rotate-border 3s linear infinite;
}
.root.full:hover:after {
background: var(--bs-primary);
animation: none;
}
@keyframes rotate-border {
to {
--border-angle: 360deg;
}
}
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.progress {
border-radius: 0;
bottom: 0;
left: 0;
right: 0;
}
.progress-bar {
height: 100%;
width: 100%;
background-image: linear-gradient(
0deg,
rgba(255, 255, 255, 0.15) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%,
transparent
) !important;
animation: progress-bar-stripes-down 1s linear infinite !important;
}
@keyframes progress-bar-stripes-down {
from {
background-position: 0 0;
}
to {
background-position: 0 1rem;
}
}
.icon-wrapper {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
transform: translateZ(0); /* fix Safari hover jump */
transition: opacity var(--evcc-transition-fast) ease;
}
</style>

View file

@ -37,7 +37,7 @@
@maxcurrent-updated="setMaxCurrent"
@mincurrent-updated="setMinCurrent"
@phasesconfigured-updated="setPhasesConfigured"
@batteryboost-updated="setBatteryBoost"
@batteryboostlimit-updated="setBatteryBoostLimit"
/>
<div
@ -96,6 +96,7 @@
@change-vehicle="changeVehicle"
@remove-vehicle="removeVehicle"
@open-loadpoint-settings="openSettingsModal"
@batteryboost-updated="setBatteryBoost"
/>
</div>
</template>
@ -156,7 +157,9 @@ export default defineComponent({
chargeDuration: { type: Number, default: 0 },
charging: Boolean,
batteryBoost: Boolean,
batteryBoostLimit: { type: Number, default: 100 },
batteryConfigured: Boolean,
batterySoc: Number,
// session
sessionEnergy: Number,
@ -316,7 +319,8 @@ export default defineComponent({
this.batteryBoost &&
this.charging &&
this.mode &&
!["off", "now"].includes(this.mode)
!["off", "now"].includes(this.mode) &&
(this.batterySoc ?? 0) >= this.batteryBoostLimit
);
},
plannerForecast() {
@ -390,6 +394,9 @@ export default defineComponent({
setBatteryBoost(batteryBoost: boolean) {
api.post(this.apiPath("batteryboost") + `/${batteryBoost ? "1" : "0"}`);
},
setBatteryBoostLimit(limit: number) {
api.post(this.apiPath("batteryboostlimit") + "/" + limit);
},
fmtPower(value: number) {
return this.fmtW(value, POWER_UNIT.AUTO);
},

View file

@ -30,6 +30,7 @@
:gridConfigured="gridConfigured"
:pvConfigured="pvConfigured"
:batteryConfigured="batteryConfigured"
:batterySoc="batterySoc"
:forecast="forecast"
class="h-100"
:class="{ 'loadpoint-unselected': !selected(loadpoint.id) }"
@ -85,6 +86,7 @@ export default defineComponent({
gridConfigured: Boolean,
pvConfigured: Boolean,
batteryConfigured: Boolean,
batterySoc: Number,
forecast: Object, // as PropType<Forecast>,
},
emits: ["id-changed"],

View file

@ -5,33 +5,24 @@
</h6>
<div class="mb-3 row" data-testid="battery-boost">
<label :for="formId('batteryBoost')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
<label :for="formId('batteryBoostLimit')" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ $t("main.loadpointSettings.batteryBoost.label") }}
</label>
<div class="col-sm-8 pe-0">
<div class="form-check form-switch my-1">
<input
:id="formId('batteryBoost')"
v-model="selectedEnabled"
class="form-check-input"
type="checkbox"
role="switch"
:disabled="disabled"
data-testid="battery-boost-checkbox"
@change="handleEnabledChange"
/>
<label :for="formId('batteryBoost')" class="form-check-label">
<div>
{{ $t("main.loadpointSettings.batteryBoost.description") }}
</div>
<span v-if="selectedEnabled && !disabled" class="d-block text-primary">
{{ $t("main.loadpointSettings.batteryBoost.once") }}
</span>
<small v-if="disabled" class="d-block">
{{ $t("main.loadpointSettings.batteryBoost.mode") }}
</small>
</label>
</div>
<div class="col-sm-8 col-lg-4 pe-0 d-flex align-items-center">
<select
:id="formId('batteryBoostLimit')"
v-model.number="selectedLimit"
class="form-select form-select-sm"
data-testid="battery-boost-limit"
@change="handleLimitChange"
>
<option v-for="{ value, name } in limitOptions" :key="value" :value="value">
{{ name }}
</option>
</select>
</div>
<div class="col-sm-8 offset-sm-4 mt-1">
<small class="text-muted">{{ description }}</small>
</div>
</div>
</div>
@ -40,34 +31,62 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import formatter from "@/mixins/formatter";
import { CHARGE_MODE } from "@/types/evcc";
const insertSorted = (arr: number[], num: number) => {
const uniqueSet = new Set(arr);
uniqueSet.add(num);
return [...uniqueSet].sort((a, b) => b - a);
};
export default defineComponent({
mixins: [formatter],
props: {
formId: { type: Function as PropType<(s: string) => string>, default: (s: string) => s },
mode: String as PropType<CHARGE_MODE>,
batteryBoost: Boolean,
formId: {
type: Function as PropType<(s: string) => string>,
default: (s: string) => s,
},
batteryBoostLimit: { type: Number, default: 100 },
},
emits: ["batteryboost-updated"],
emits: ["batteryboostlimit-updated"],
data() {
return {
selectedEnabled: this.batteryBoost,
selectedLimit: this.batteryBoostLimit,
};
},
computed: {
disabled() {
return this.mode && [CHARGE_MODE.OFF, CHARGE_MODE.NOW].includes(this.mode);
description(): string {
if (this.selectedLimit < 100) {
return this.$t("main.loadpointSettings.batteryBoost.description", {
limit: this.fmtPercentage(this.selectedLimit),
});
}
return this.$t("main.loadpointSettings.batteryBoost.descriptionDisabled");
},
limitOptions() {
// generate 5-step values: 100 (disabled), 95, 90, ..., 5, 0
const values = [100];
for (let i = 95; i >= 0; i -= 5) {
values.push(i);
}
// insert current value if non-standard
const opts = insertSorted(values, this.batteryBoostLimit);
return opts.map((value) => ({
value,
name:
value === 100
? this.$t("main.loadpointSettings.batteryBoost.disabled")
: `${value} %`,
}));
},
},
watch: {
batteryBoost(newVal) {
this.selectedEnabled = newVal;
batteryBoostLimit(newVal: number) {
this.selectedLimit = newVal;
},
},
methods: {
handleEnabledChange() {
this.$emit("batteryboost-updated", this.selectedEnabled);
handleLimitChange() {
this.$emit("batteryboostlimit-updated", this.selectedLimit);
},
},
});

View file

@ -34,7 +34,7 @@
v-if="batteryBoostAvailable"
v-bind="batteryBoostProps"
class="mt-2"
@batteryboost-updated="changeBatteryBoost"
@batteryboostlimit-updated="changeBatteryBoostLimit"
/>
<h6>
{{ $t("main.loadpointSettings.currents") }}
@ -167,7 +167,7 @@ export default defineComponent({
phasesConfigured: { type: Number, default: 0 },
chargerPhases1p3p: Boolean,
chargerSinglePhase: Boolean,
batteryBoost: Boolean,
batteryBoostLimit: { type: Number, default: 100 },
batteryBoostAvailable: Boolean,
mode: String,
minSoc: Number,
@ -190,7 +190,7 @@ export default defineComponent({
"phasesconfigured-updated",
"maxcurrent-updated",
"mincurrent-updated",
"batteryboost-updated",
"batteryboostlimit-updated",
],
data() {
return {
@ -285,8 +285,8 @@ export default defineComponent({
modalInvisible() {
this.isModalVisible = false;
},
changeBatteryBoost(boost: boolean) {
this.$emit("batteryboost-updated", boost);
changeBatteryBoostLimit(limit: number) {
this.$emit("batteryboostlimit-updated", limit);
},
},
});

View file

@ -1,6 +1,6 @@
<template>
<svg :style="svgStyle" viewBox="0 0 48 48">
<g>
<g class="battery" :class="{ active }">
<path
fill="currentColor"
d="M35,9.996l-3,0l0,-4c0,-1.097 -0.903,-2 -2,-2l-12,0c-1.097,0 -2,0.903 -2,2l0,4l-3,0c-1.097,0 -2,0.903 -2,2l0,30c0,1.097 0.903,2 2,2l22,0c1.097,0 2,-0.903 2,-2l0,-30c0,-1.097 -0.903,-2 -2,-2Zm-15,-2l8,-0l0,2l-8,-0l0,-2Zm13,32l-18,0l0,-26l18,0l0,26Z"
@ -10,6 +10,29 @@
d="M24.741,18.029c-0.395,-0.103 -0.812,0.073 -1.012,0.43l-5.077,9.05c-0.157,0.278 -0.154,0.619 0.008,0.895c0.162,0.275 0.458,0.445 0.777,0.445l3.727,-0l-0,6.251c-0,0.425 0.297,0.792 0.712,0.88c0.063,0.014 0.126,0.02 0.188,0.02c0.349,-0 0.675,-0.204 0.822,-0.534c0.966,-2.174 1.917,-4.395 2.823,-6.603c0.571,-1.394 1.142,-2.82 1.693,-4.237c0.108,-0.277 0.072,-0.589 -0.096,-0.834c-0.167,-0.245 -0.445,-0.392 -0.742,-0.392l-3.15,-0l-0,-4.5c-0,-0.41 -0.277,-0.767 -0.673,-0.871Z"
/>
</g>
<g class="speed" :class="{ active }">
<path
fill="none"
stroke="currentColor"
stroke-width="4"
stroke-linecap="round"
d="M12,26l-7,0"
/>
<path
fill="none"
stroke="currentColor"
stroke-width="4"
stroke-linecap="round"
d="M11,35l-7,0"
/>
<path
fill="none"
stroke="currentColor"
stroke-width="4"
stroke-linecap="round"
d="M13,17l-7,0"
/>
</g>
</svg>
</template>
@ -20,5 +43,25 @@ import icon from "@/mixins/icon";
export default defineComponent({
name: "BatteryBoost",
mixins: [icon],
props: {
active: { type: Boolean, default: false },
},
});
</script>
<style scoped>
.battery {
transform-origin: 24px 24px;
transition: transform var(--evcc-transition-fast, 250ms) ease;
}
.battery.active {
transform: translateX(7px) skewX(-8deg);
}
.speed {
opacity: 0;
transition: opacity var(--evcc-transition-fast, 250ms) ease;
}
.speed.active {
opacity: 1;
}
</style>

View file

@ -83,6 +83,7 @@ export default defineComponent({
<style scoped>
.value {
font-size: 18px;
overflow: hidden;
}
.extraValue {
color: var(--evcc-gray);
@ -96,5 +97,6 @@ export default defineComponent({
cursor: pointer;
position: absolute;
opacity: 0;
max-width: 100%;
}
</style>

View file

@ -4,7 +4,11 @@
style="min-height: 24px"
data-testid="vehicle-status"
>
<div class="charger-status" data-testid="vehicle-status-charger">
<div
class="charger-status"
:class="chargerStatusType ? `text-${chargerStatusType}` : ''"
data-testid="vehicle-status-charger"
>
{{ chargerStatus }}
</div>
<div class="d-flex flex-wrap justify-content-end gap-3 flex-grow-1">
@ -57,7 +61,7 @@ import { DEFAULT_LOCALE } from "@/i18n.ts";
import formatter from "@/mixins/formatter";
import minuteTicker from "@/mixins/minuteTicker";
import { defineComponent, type PropType } from "vue";
import { SMART_COST_TYPE, type CURRENCY } from "@/types/evcc";
import { SMART_COST_TYPE, type CURRENCY, type VehicleStatus, type Timeout } from "@/types/evcc";
import ClimaterIcon from "../MaterialIcon/Climater.vue";
import DynamicPriceIcon from "../MaterialIcon/DynamicPrice.vue";
@ -73,7 +77,7 @@ import VehicleLimitReachedIcon from "../MaterialIcon/VehicleLimitReached.vue";
import VehicleLimitWarningIcon from "../MaterialIcon/VehicleLimitWarning.vue";
import VehicleMinSocIcon from "../MaterialIcon/VehicleMinSoc.vue";
import WelcomeIcon from "../MaterialIcon/Welcome.vue";
import BatteryBoostIcon from "../MaterialIcon/BatteryBoost.vue";
import SunPauseIcon from "../MaterialIcon/SunPause.vue";
import StatusItem from "./StatusItem.vue";
@ -89,7 +93,7 @@ export default defineComponent({
mixins: [formatter, minuteTicker],
props: {
vehicleSoc: { type: Number, default: 0 },
batteryBoostActive: Boolean,
charging: Boolean,
chargingPlanDisabled: Boolean,
chargerStatusReason: String,
@ -125,8 +129,15 @@ export default defineComponent({
vehicleClimaterActive: Boolean,
vehicleWelcomeActive: Boolean,
vehicleLimitSoc: { type: Number, default: 0 },
statusOverride: { type: Object as PropType<VehicleStatus>, default: undefined },
},
emits: ["open-loadpoint-settings", "open-minsoc-settings", "open-plan-modal"],
data() {
return {
statusOverrideActive: false,
statusTimeout: null as Timeout | null,
};
},
computed: {
pvTimerActive() {
return (
@ -180,7 +191,16 @@ export default defineComponent({
smartFeedInPriorityLimitFmt() {
return this.fmtPricePerKWh(this.smartFeedInPriorityLimit, this.currency, true);
},
chargerStatusType(): string | undefined {
if (this.statusOverrideActive && this.statusOverride) {
return this.statusOverride.type;
}
return undefined;
},
chargerStatus() {
if (this.statusOverrideActive && this.statusOverride) {
return this.statusOverride.message;
}
const t = (key: string) => {
if (this.heating) {
// check for special heating status translation
@ -341,15 +361,6 @@ export default defineComponent({
clickable: true,
clickHandler: () => this.openLoadpointSettings(),
},
{
id: "batteryBoost",
visible: this.batteryBoostActive,
tooltipContent: t("batteryBoost"),
iconComponent: BatteryBoostIcon,
testId: "vehicle-status-batteryboost",
clickable: true,
clickHandler: () => this.openLoadpointSettings(),
},
{
id: "planActive",
visible: this.planProjectedEnd && this.planActive && !this.chargingPlanDisabled,
@ -393,6 +404,20 @@ export default defineComponent({
return items.filter((item) => item.visible);
},
},
watch: {
statusOverride(val: VehicleStatus | undefined) {
if (val) {
this.statusOverrideActive = true;
if (this.statusTimeout) clearTimeout(this.statusTimeout);
this.statusTimeout = setTimeout(() => {
this.statusOverrideActive = false;
}, 2500);
}
},
},
unmounted() {
if (this.statusTimeout) clearTimeout(this.statusTimeout);
},
methods: {
openLoadpointSettings() {
this.$emit("open-loadpoint-settings");

View file

@ -13,13 +13,22 @@
@open-minsoc-settings="openMinSocSettings"
@open-plan-modal="openPlanModal"
/>
<VehicleSoc
v-bind="vehicleSocProps"
class="mt-2 mb-4"
@limit-soc-updated="limitSocUpdated"
@limit-soc-drag="limitSocDrag"
@plan-clicked="openPlanModal"
/>
<div class="mt-2 mb-4 d-flex gap-2">
<BatteryBoostButton
v-if="showBoostButton"
class="flex-grow-0"
v-bind="batteryBoostButtonProps"
@updated="$emit('batteryboost-updated', $event)"
@status="handleBoostStatus"
/>
<VehicleSoc
class="flex-grow-1 position-relative"
v-bind="vehicleSocProps"
@limit-soc-updated="limitSocUpdated"
@limit-soc-drag="limitSocDrag"
@plan-clicked="openPlanModal"
/>
</div>
<div class="details d-flex flex-wrap justify-content-between">
<LabelAndValue
v-if="socBasedCharging"
@ -79,8 +88,9 @@ 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 Vehicle } from "@/types/evcc";
import { CHARGE_MODE, type Forecast, type VehicleStatus, type Vehicle } from "@/types/evcc";
import type { PlanStrategy } from "@/components/ChargingPlans/types";
import BatteryBoostButton from "../Loadpoints/BatteryBoostButton.vue";
export default defineComponent({
name: "Vehicle",
@ -92,6 +102,7 @@ export default defineComponent({
ChargingPlan,
LimitSocSelect,
LimitEnergySelect,
BatteryBoostButton,
},
mixins: [collector, formatter],
props: {
@ -105,7 +116,11 @@ export default defineComponent({
effectivePlanSoc: Number,
effectivePlanTime: String,
effectivePlanStrategy: Object as PropType<PlanStrategy>,
batteryBoost: Boolean,
batteryBoostActive: Boolean,
batteryBoostAvailable: Boolean,
batteryBoostLimit: { type: Number, default: 100 },
batterySoc: Number,
enabled: Boolean,
heating: Boolean,
id: [String, Number],
@ -153,10 +168,12 @@ export default defineComponent({
"change-vehicle",
"remove-vehicle",
"open-loadpoint-settings",
"batteryboost-updated",
],
data() {
return {
displayLimitSoc: this.effectiveLimitSoc,
statusOverride: undefined as VehicleStatus | undefined,
};
},
computed: {
@ -176,7 +193,7 @@ export default defineComponent({
return this.collectProps(Soc);
},
vehicleStatus() {
return this.collectProps(Status);
return { ...this.collectProps(Status), statusOverride: this.statusOverride };
},
vehicleTitleProps() {
return this.collectProps(Title);
@ -184,6 +201,12 @@ export default defineComponent({
chargingPlan() {
return this.collectProps(ChargingPlan);
},
showBoostButton(): boolean {
return this.batteryBoostAvailable && this.batteryBoostLimit < 100;
},
batteryBoostButtonProps() {
return this.collectProps(BatteryBoostButton);
},
formattedSoc() {
if (!this.vehicleSoc) {
return "--";
@ -261,6 +284,9 @@ export default defineComponent({
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined
)?.openPlanModal();
},
handleBoostStatus(status: VehicleStatus) {
this.statusOverride = status;
},
openMinSocSettings() {
(
this.$refs["chargingPlan"] as InstanceType<typeof ChargingPlan> | undefined

View file

@ -538,6 +538,11 @@ export interface Vehicle {
export type Timeout = ReturnType<typeof setInterval> | null;
export interface VehicleStatus {
message: string;
type?: string;
}
export interface Tariff {
rates: Rate[];
lastUpdate: Date;

View file

@ -68,8 +68,8 @@ meters:
vm: shared
script: |
if (state.batterypower < 0) state.batterySoc++; else state.batterySoc--;
if (state.batterySoc < 10) state.batterySoc = 90;
if (state.batterySoc > 90) state.batterySoc = 10;
if (state.batterySoc < 10) state.batterySoc = 100;
if (state.batterySoc > 100) state.batterySoc = 10;
state.batterySoc;
capacity: 13.4
batterymode:

View file

@ -2,26 +2,27 @@ package keys
const (
// loadpoint settings
Title = "title" // loadpoint title
Mode = "mode" // charge mode
DefaultMode = "defaultMode" // default charge mode
Charger = "charger" // charger ref
Meter = "meter" // meter ref
Circuit = "circuit" // circuit ref
DefaultVehicle = "vehicle" // default vehicle ref
Priority = "priority" // priority
MinCurrent = "minCurrent" // min current
MaxCurrent = "maxCurrent" // max current
MinSoc = "minSoc" // min soc
LimitSoc = "limitSoc" // limit soc
LimitEnergy = "limitEnergy" // limit energy
Soc = "soc"
Thresholds = "thresholds"
EnableThreshold = "enableThreshold"
DisableThreshold = "disableThreshold"
EnableDelay = "enableDelay"
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"
Title = "title" // loadpoint title
Mode = "mode" // charge mode
DefaultMode = "defaultMode" // default charge mode
Charger = "charger" // charger ref
Meter = "meter" // meter ref
Circuit = "circuit" // circuit ref
DefaultVehicle = "vehicle" // default vehicle ref
Priority = "priority" // priority
MinCurrent = "minCurrent" // min current
MaxCurrent = "maxCurrent" // max current
MinSoc = "minSoc" // min soc
LimitSoc = "limitSoc" // limit soc
LimitEnergy = "limitEnergy" // limit energy
Soc = "soc"
Thresholds = "thresholds"
EnableThreshold = "enableThreshold"
DisableThreshold = "disableThreshold"
EnableDelay = "enableDelay"
DisableDelay = "disableDelay"
BatteryBoost = "batteryBoost"
BatteryBoostLimit = "batteryBoostLimit"
PhasesConfigured = "phasesConfigured" // desired phase mode (0/1/3, 0 = automatic), user selection
PhasesActive = "phasesActive" // expectedly active phases, taking vehicle into account (1/2/3)

View file

@ -118,6 +118,7 @@ type Loadpoint struct {
smartCostLimit *float64 // always charge if consumption cost is below this value
smartFeedInPriorityLimit *float64 // prevent charging if feed-in cost is above this value
batteryBoost int // battery boost state
batteryBoostLimit int // battery boost soc limit (0-100, 100=disabled)
mode api.ChargeMode
enabled bool // Charger enabled state
@ -286,14 +287,15 @@ func NewLoadpoint(log *util.Logger, settings settings.Settings) *Loadpoint {
bus := evbus.New()
lp := &Loadpoint{
log: log, // logger
settings: settings, // settings
clock: clock, // mockable time
bus: bus, // event bus
mode: api.ModeOff,
status: api.StatusNone,
minCurrent: 6, // A
maxCurrent: 16, // A
log: log, // logger
settings: settings, // settings
clock: clock, // mockable time
bus: bus, // event bus
mode: api.ModeOff,
status: api.StatusNone,
minCurrent: 6, // A
maxCurrent: 16, // A
batteryBoostLimit: 100, // disabled
Soc: loadpoint.SocConfig{
Poll: loadpoint.PollConfig{
Interval: pollInterval,
@ -358,6 +360,9 @@ func (lp *Loadpoint) restoreSettings() {
if v, err := lp.settings.Float(keys.SmartFeedInPriorityLimit); err == nil {
lp.SetSmartFeedInPriorityLimit(&v)
}
if v, err := lp.settings.Int(keys.BatteryBoostLimit); err == nil {
lp.SetBatteryBoostLimit(int(v))
}
var thresholds loadpoint.ThresholdsConfig
if err := lp.settings.Json(keys.Thresholds, &thresholds); err == nil {
@ -699,6 +704,7 @@ func (lp *Loadpoint) Prepare(site site.API, uiChan chan<- util.Param, pushChan c
// battery boost
lp.publish(keys.BatteryBoost, lp.batteryBoost != boostDisabled)
lp.publish(keys.BatteryBoostLimit, lp.batteryBoostLimit)
// read initial charger state to prevent immediately disabling charger
if enabled, err := lp.charger.Enabled(); err == nil {
@ -1860,6 +1866,18 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
// Update is the main control function. It reevaluates meters and charger state
func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, consumption, feedin api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
// auto-disable battery boost when SOC drops below limit
if lp.GetBatteryBoost() != boostDisabled {
limit := lp.GetBatteryBoostLimit()
if limit < 100 {
batterySoc := lp.site.GetBatterySoc()
if batterySoc < float64(limit) {
lp.log.DEBUG.Printf("battery boost disabled: soc below limit (%.0f%% < %d%%)", batterySoc, limit)
_ = lp.SetBatteryBoost(false)
}
}
}
// smart cost
smartCostActive, smartCostNextStart := lp.checkSmartLimit(lp.GetSmartCostLimit(), consumption, true)
lp.publish(keys.SmartCostActive, smartCostActive)

View file

@ -167,6 +167,10 @@ type API interface {
GetBatteryBoost() int
// SetBatteryBoost sets the battery boost
SetBatteryBoost(enable bool) error
// GetBatteryBoostLimit returns the battery boost soc limit
GetBatteryBoostLimit() int
// SetBatteryBoostLimit sets the battery boost soc limit
SetBatteryBoostLimit(int)
//
// smart grid charging

View file

@ -28,6 +28,7 @@ type DynamicConfig struct {
PlanEnergy float64 `json:"planEnergy"`
PlanTime time.Time `json:"planTime"`
PlanPrecondition_ int64 `json:"planPrecondition" mapstructure:"planPrecondition"` // TODO deprecated, keep for compatibility
BatteryBoostLimit int `json:"batteryBoostLimit"`
LimitEnergy float64 `json:"limitEnergy"`
LimitSoc int `json:"limitSoc"`
@ -63,6 +64,7 @@ func (payload DynamicConfig) Apply(lp API) error {
lp.SetThresholds(payload.Thresholds)
lp.SetPlanEnergy(payload.PlanTime, payload.PlanEnergy)
lp.SetPlanStrategy(payload.PlanStrategy)
lp.SetBatteryBoostLimit(payload.BatteryBoostLimit)
lp.SetLimitEnergy(payload.LimitEnergy)
lp.SetLimitSoc(payload.LimitSoc)

View file

@ -179,6 +179,20 @@ func (mr *MockAPIMockRecorder) GetBatteryBoost() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBatteryBoost", reflect.TypeOf((*MockAPI)(nil).GetBatteryBoost))
}
// GetBatteryBoostLimit mocks base method.
func (m *MockAPI) GetBatteryBoostLimit() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBatteryBoostLimit")
ret0, _ := ret[0].(int)
return ret0
}
// GetBatteryBoostLimit indicates an expected call of GetBatteryBoostLimit.
func (mr *MockAPIMockRecorder) GetBatteryBoostLimit() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBatteryBoostLimit", reflect.TypeOf((*MockAPI)(nil).GetBatteryBoostLimit))
}
// GetChargePower mocks base method.
func (m *MockAPI) GetChargePower() float64 {
m.ctrl.T.Helper()
@ -739,6 +753,18 @@ func (mr *MockAPIMockRecorder) SetBatteryBoost(enable any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryBoost", reflect.TypeOf((*MockAPI)(nil).SetBatteryBoost), enable)
}
// SetBatteryBoostLimit mocks base method.
func (m *MockAPI) SetBatteryBoostLimit(arg0 int) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetBatteryBoostLimit", arg0)
}
// SetBatteryBoostLimit indicates an expected call of SetBatteryBoostLimit.
func (mr *MockAPIMockRecorder) SetBatteryBoostLimit(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryBoostLimit", reflect.TypeOf((*MockAPI)(nil).SetBatteryBoostLimit), arg0)
}
// SetChargerRef mocks base method.
func (m *MockAPI) SetChargerRef(arg0 string) {
m.ctrl.T.Helper()

View file

@ -616,6 +616,27 @@ func (lp *Loadpoint) SetBatteryBoost(enable bool) error {
return nil
}
// GetBatteryBoostLimit returns the battery boost soc limit
func (lp *Loadpoint) GetBatteryBoostLimit() int {
lp.RLock()
defer lp.RUnlock()
return lp.batteryBoostLimit
}
// SetBatteryBoostLimit sets the battery boost soc limit
func (lp *Loadpoint) SetBatteryBoostLimit(limit int) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set battery boost limit:", limit)
if lp.batteryBoostLimit != limit {
lp.batteryBoostLimit = limit
lp.settings.SetInt(keys.BatteryBoostLimit, int64(limit))
lp.publish(keys.BatteryBoostLimit, limit)
}
}
// HasChargeMeter determines if a physical charge meter is attached
func (lp *Loadpoint) HasChargeMeter() bool {
_, isWrapped := lp.chargeMeter.(*wrapper.ChargeMeter)

View file

@ -41,6 +41,7 @@ type API interface {
// battery
//
GetBatterySoc() float64
GetPrioritySoc() float64
SetPrioritySoc(float64) error
GetBufferSoc() float64

View file

@ -132,6 +132,13 @@ func (site *Site) SetExtMeterRefs(ref []string) {
settings.SetString(keys.ExtMeters, strings.Join(filterConfigurable(ref), ","))
}
// GetBatterySoc returns the current battery soc
func (site *Site) GetBatterySoc() float64 {
site.RLock()
defer site.RUnlock()
return site.battery.Soc
}
// Loadpoints returns the loadpoints as api interfaces
func (site *Site) Loadpoints() []loadpoint.API {
return lo.Map(site.loadpoints, func(lp *Loadpoint, _ int) loadpoint.API { return lp })

View file

@ -956,7 +956,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Бързо зареждане от домашна батерия.",
"description": "Бързо зареждане от домашна батерия, докато се изтощи до {limit}.",
"label": "Батериен бууст",
"mode": "Достъпно само в режим „слънчев“ и „мин+слънчев“.",
"once": "Усилване активно за тази сесия на зареждане."

View file

@ -981,7 +981,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Povolí využití energie uložené v baterii domu pro nabíjení.",
"description": "Povolí využití energie uložené v baterii domu pro nabíjení, dokud se nevybije na {limit}.",
"label": "Rychlé dobití",
"mode": "Dostupné pouze v solárním režimu a režimu min+solar.",
"once": "Rychlé dobití je aktivováno pro tuto nabíjecí relaci."

View file

@ -983,7 +983,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Hurtig opladning fra husbatteriet.",
"description": "Hurtig opladning fra husbatteriet, indtil det er afladet til {limit}.",
"label": "Batteri boost",
"mode": "Kun tilgængelig i solar og min+solar mode.",
"once": "Boost er aktiveret for denne opladning."

View file

@ -1033,10 +1033,15 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Schnell aus Hausbatterie laden.",
"description": "Schnellladen aus der Hausbatterie erlauben, bis sie auf {limit} entladen ist.",
"descriptionDisabled": "Limit wählen, um Schnellladen aus der Hausbatterie zu erlauben.",
"disabled": "Deaktiviert",
"label": "Batterie Boost",
"mode": "Nur im PV- und Min+PV-Modus verfügbar.",
"once": "Boost ist für diesen Ladevorgang aktiviert."
"once": "Boost ist für diesen Ladevorgang aktiviert.",
"stateActive": "Batterie Boost aktiv",
"stateBelowLimit": "Batterie zu niedrig für Boost",
"stateReady": "Batterie Boost bereit"
},
"batteryUsage": "Hausbatterie",
"currents": "Ladestrom",
@ -1142,6 +1147,9 @@
"vehicleStatus": {
"awaitingAuthorization": "Warte auf Autorisierung.",
"batteryBoost": "Batterie Boost aktiv.",
"batteryBoostBelowLimit": "Batterie zu niedrig für Boost.",
"batteryBoostDisabled": "Batterie Boost deaktiviert.",
"batteryBoostEnabled": "Boost bis Batterie bei {limit}.",
"charging": "Ladevorgang aktiv …",
"cheapEnergyCharging": "Günstige Energie verfügbar.",
"cheapEnergyNextStart": "Günstige Energie in {duration}.",

View file

@ -961,7 +961,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Γρήγορη φόρτιση από οικιακή μπαταρία.",
"description": "Γρήγορη φόρτιση από οικιακή μπαταρία, μέχρι να εκφορτιστεί στο {limit}.",
"label": "Ενίσχυση από μπαταρία",
"mode": "Διαθέσιμο μόνο σε λειτουργία Φ/Β ή Ελαχ+Φ/Β.",
"once": "Ενεργή ενίσχυση για αυτή τη συνεδρία φόρτισης."

View file

@ -1033,10 +1033,15 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Fast charge from home battery.",
"description": "Allow fast charging from home battery until it's drained to {limit}.",
"descriptionDisabled": "Select a limit to allow fast charging from home battery.",
"disabled": "Disabled",
"label": "Battery Boost",
"mode": "Only available in solar and min+solar mode.",
"once": "Boost active for this charging session."
"once": "Boost active for this charging session.",
"stateActive": "Battery boost active",
"stateBelowLimit": "Battery too low for boost",
"stateReady": "Battery boost ready"
},
"batteryUsage": "Home Battery",
"currents": "Charging Current",
@ -1142,6 +1147,9 @@
"vehicleStatus": {
"awaitingAuthorization": "Waiting for authorization.",
"batteryBoost": "Battery boost active.",
"batteryBoostBelowLimit": "Battery too low for boost.",
"batteryBoostDisabled": "Battery boost disabled.",
"batteryBoostEnabled": "Boost until battery at {limit}.",
"charging": "Charging…",
"cheapEnergyCharging": "Cheap energy available.",
"cheapEnergyNextStart": "Cheap energy in {duration}.",

View file

@ -973,7 +973,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Carga rápida desde la batería doméstica.",
"description": "Carga rápida desde la batería doméstica hasta que se descargue a {limit}.",
"label": "Aumento de la batería",
"mode": "Solo disponible en modo solar y min+solar.",
"once": "Impulso activo para esta sesión de carga."

View file

@ -980,7 +980,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Nopea lataus kodin akusta.",
"description": "Nopea lataus kodin akusta, kunnes se purkautuu tasolle {limit}.",
"label": "Akun tehostaminen",
"mode": "Saatavilla vain aurinkosähkö(pv)- ja min + pv-tilassa.",
"once": "Tehostus on aktiivinen tälle latausistunnolle."

View file

@ -982,7 +982,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Chargement rapide à partir de la batterie domestique.",
"description": "Chargement rapide à partir de la batterie domestique jusqu'à ce qu'elle soit déchargée à {limit}.",
"label": "Boost Batterie",
"mode": "Uniquement disponible en mode solaire et min+solaire ».",
"once": "Boost actif pour cette session de charge."

View file

@ -962,7 +962,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Brzo punjenje pomoću kućne baterije.",
"description": "Brzo punjenje pomoću kućne baterije dok se ne isprazni na {limit}.",
"label": "Ubrzaj punjenje s baterijom",
"mode": "Dostupno samo u modusu 'Solarno' i 'Min+Solarno'.",
"once": "Ubrzano punjenje je aktivno za ovu sesiju punjenja."

View file

@ -961,7 +961,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Gyorstöltés otthoni akkumulátorról.",
"description": "Gyorstöltés otthoni akkumulátorról, amíg az le nem merül {limit} szintre.",
"label": "Akkumulátoros Rásegítés",
"mode": "Csak szolár és min+szolár módban elérhető.",
"once": "Rásegítés aktív ehhez a töltési folyamathoz."

View file

@ -956,7 +956,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Carica rapida dalla batteria di casa.",
"description": "Carica rapida dalla batteria di casa fino a quando non si scarica a {limit}.",
"label": "Boost da batteria",
"mode": "Disponibile solo in modalità solare e min+solare.",
"once": "Boost attivo per questa sessione di ricarica."

View file

@ -981,7 +981,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Schnell vun der Hausbatterie lueden.",
"description": "Schnell vun der Hausbatterie lueden, bis se op {limit} entlueden ass.",
"label": "Batterie Boost",
"mode": "Nëmmen am PV- a Min+PV-Modus verfügbar.",
"once": "Boost ass dir dës Luedsessioun aktivéiert."

View file

@ -983,7 +983,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Greitas įkrovimas iš namų kaupiklio.",
"description": "Greitas įkrovimas iš namų kaupiklio, kol jis išsikraus iki {limit}.",
"label": "Kaupikliu paspartintas įkrovimas",
"mode": "Galimas tik Saulė ir Min+Saulė režimuose.",
"once": "Paspartinimas aktyvuotas šiai įkrovimo sesijai."

View file

@ -982,7 +982,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Snelladen vanuit thuisbatterij.",
"description": "Snelladen vanuit thuisbatterij tot deze is ontladen naar {limit}.",
"label": "Batterij Boost",
"mode": "Enkel beschikbaar in PV en Min+PV modus.",
"once": "Boost geactiveerd voor deze laadsessie."

View file

@ -953,7 +953,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Hurtiglading fra hjemmebatteriet.",
"description": "Hurtiglading fra hjemmebatteriet til det er utladet til {limit}.",
"label": "Batteriboost",
"mode": "Kun tilgjengelig i solenergi- og min+solenergi-modus.",
"once": "Boost aktiv for denne ladingsøkten."

View file

@ -954,7 +954,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Szybkie ładowanie z domowego magazynu energii.",
"description": "Szybkie ładowanie z domowego magazynu energii, aż rozładuje się do {limit}.",
"label": "Doładowanie akumulatora",
"mode": "Dostępne tylko w trybie słonce i min+słońce.",
"once": "Doładowanie aktywne dla tej sesji ładowania."

View file

@ -981,7 +981,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Carga rápida da bateria de casa.",
"description": "Carga rápida da bateria de casa até que descarregue para {limit}.",
"label": "Boost bateria",
"mode": "Disponível apenas nos modos solar e min+solar.",
"once": "Boost ativo para esta sessão de carga."

View file

@ -953,7 +953,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Încărcare rapidă de la bateria de acasă.",
"description": "Încărcare rapidă de la bateria de acasă până când se descarcă la {limit}.",
"label": "Îmbunătățirea bateriei",
"mode": "Disponibil numai în modul solar și min+solar.",
"once": "Boost activ pentru această sesiune de încărcare."

View file

@ -952,7 +952,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Быстрая зарядка от домашней батареи.",
"description": "Быстрая зарядка от домашней батареи, пока она не разрядится до {limit}.",
"label": "Усиление батареи",
"mode": "Доступно только в режимах «Солнечный» и «Мини+солнечный».",
"once": "Усиление активно для этой сессии зарядки."

View file

@ -983,7 +983,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Hitro polnjenje iz domače baterije.",
"description": "Hitro polnjenje iz domače baterije, dokler se ne izprazni na {limit}.",
"label": "Pospeševanje baterije",
"mode": "Na voljo samo v solarnem in min+solarnem načinu.",
"once": "Pospeševanje z baterijo aktivno za to sejo polnjenja."

View file

@ -983,7 +983,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Snabbladdning från hembatteri.",
"description": "Snabbladdning från hembatteri tills det är urladdat till {limit}.",
"label": "Batteri Boost",
"mode": "Endast tillgänglig vid 'Sol' och 'Min+Sol' läge.",
"once": "Boost är aktiverat för denna laddning."

View file

@ -741,7 +741,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Fast மின்னூட்டு இருந்து வீடு battery.",
"description": "வீட்டு மின்கலத்திலிருந்து {limit} வரை இறங்கும் வரை விரைவு மின்னூட்டம்.",
"label": "மின்கலம் ஊட்டம்",
"mode": "ஞாயிறு மற்றும் குறை+ஞாயிறு பயன்முறையில் மட்டுமே கிடைக்கும்.",
"once": "Boost செயலில் க்கு this charging session."

View file

@ -961,7 +961,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Ev bataryasından hızlı doldur.",
"description": "Ev bataryasından {limit} seviyesine düşene kadar hızlı şarj.",
"label": "'Batarya Takviyesi”",
"mode": "Sadece GES ve Asg.+GES modunda kullanılabilir.",
"once": "Bu doldurma oturumu için takviye etkin."

View file

@ -965,7 +965,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "Швидка зарядка від домашнього акумулятора.",
"description": "Швидка зарядка від домашнього акумулятора, поки він не розрядиться до {limit}.",
"label": "Батарея Boost",
"mode": "Доступно лише в режимі сонячної батареї та режимі min+sonal.",
"once": "Boost активний для цього сеансу зарядки."

View file

@ -697,7 +697,7 @@
},
"loadpointSettings": {
"batteryBoost": {
"description": "使用住宅电池快速充电。",
"description": "使用住宅电池快速充电,直到电量降至 {limit}。",
"label": "电池快充",
"mode": "仅在太阳能和最小+太阳能模式下可用。",
"once": "本次充电会话已启用快充。"

View file

@ -215,6 +215,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) {
"smartFeedInPriorityDelete": {"DELETE", "/smartfeedinprioritylimit", floatPtrHandler(pass(lp.SetSmartFeedInPriorityLimit), lp.GetSmartFeedInPriorityLimit)},
"priority": {"POST", "/priority/{value:[0-9]+}", intHandler(pass(lp.SetPriority), lp.GetPriority)},
"batteryBoost": {"POST", "/batteryboost/{value:[01truefalse]+}", boolHandler(lp.SetBatteryBoost, func() bool { return lp.GetBatteryBoost() > 0 })},
"batteryBoostLimit": {"POST", "/batteryboostlimit/{value:[0-9]+}", intHandler(pass(lp.SetBatteryBoostLimit), lp.GetBatteryBoostLimit)},
}
for _, r := range routes {

View file

@ -42,6 +42,7 @@ func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
PlanEnergy: planEnergy,
PlanTime: planTime,
PlanStrategy: lp.GetPlanStrategy(),
BatteryBoostLimit: lp.GetBatteryBoostLimit(),
LimitEnergy: lp.GetLimitEnergy(),
LimitSoc: lp.GetLimitSoc(),
}

View file

@ -1089,7 +1089,7 @@
},
"/loadpoints/{id}/batteryboost/{enable}": {
"post": {
"description": "Enable or disable 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.",
"externalDocs": {
"url": "https://docs.evcc.io/en/docs/features/battery#battery-boost"
},
@ -1113,6 +1113,32 @@
]
}
},
"/loadpoints/{id}/batteryboostlimit/{soc}": {
"post": {
"description": "Set the SoC limit for battery boost. Home battery will be used to support charging up to this SoC level. A value of 100 (default) disabled the boost feature in UI and API.",
"externalDocs": {
"url": "https://docs.evcc.io/en/docs/features/battery#battery-boost"
},
"operationId": "setLoadpointBatteryBoostLimit",
"parameters": [
{
"$ref": "#/components/parameters/id"
},
{
"$ref": "#/components/parameters/soc"
}
],
"responses": {
"200": {
"$ref": "#/components/responses/IntegerResult"
}
},
"summary": "Set battery boost SoC limit",
"tags": [
"loadpoints"
]
}
},
"/loadpoints/{id}/disable/delay/{delay}": {
"post": {
"description": "Delay before charging stops in solar mode.",

View file

@ -482,7 +482,7 @@ call removeLoadpointVehicle {
## setLoadpointBatteryBoost
Enable or disable battery boost.
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.
**Tags:** loadpoints
@ -502,6 +502,28 @@ call setLoadpointBatteryBoost {
}
```
## setLoadpointBatteryBoostLimit
Set the SoC limit for battery boost. Home battery will be used to support charging up to this SoC level. A value of 100 (default) disabled the boost feature in UI and API.
**Tags:** loadpoints
**Arguments:**
| Name | Type | Description |
|------|------|-------------|
| id | integer | Loadpoint index starting at 1 |
| soc | number | SOC in % |
**Example call:**
```json
call setLoadpointBatteryBoostLimit {
"id": 123,
"soc": 123.45
}
```
## setLoadpointDisableDelay
Delay before charging stops in solar mode.

View file

@ -260,6 +260,7 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
{"smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))},
{"smartFeedInPriorityLimit", floatPtrSetter(pass(lp.SetSmartFeedInPriorityLimit))},
{"batteryBoost", boolSetter(lp.SetBatteryBoost)},
{"batteryBoostLimit", intSetter(pass(lp.SetBatteryBoostLimit))},
{"planStrategy", planStrategySetter(lp.SetPlanStrategy)},
{"planEnergy", planGoalSetter(lp.SetPlanEnergy)},
{"vehicle", func(payload string) error {

View file

@ -192,7 +192,7 @@ paths:
post:
operationId: setLoadpointBatteryBoost
summary: Set battery boost
description: "Enable or disable 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."
externalDocs:
url: https://docs.evcc.io/en/docs/features/battery#battery-boost
tags:
@ -203,6 +203,21 @@ paths:
responses:
"200":
$ref: "#/components/responses/BooleanResult"
/loadpoints/{id}/batteryboostlimit/{soc}:
post:
operationId: setLoadpointBatteryBoostLimit
summary: Set battery boost SoC limit
description: "Set the SoC limit for battery boost. Home battery will be used to support charging up to this SoC level. A value of 100 (default) disabled the boost feature in UI and API."
externalDocs:
url: https://docs.evcc.io/en/docs/features/battery#battery-boost
tags:
- loadpoints
parameters:
- $ref: "#/components/parameters/id"
- $ref: "#/components/parameters/soc"
responses:
"200":
$ref: "#/components/responses/IntegerResult"
/loadpoints/{id}/disable/delay/{delay}:
post:
operationId: setLoadpointDisableDelay

View file

@ -15,15 +15,9 @@ test.beforeEach(async ({ page }) => {
});
test.describe("boost", async () => {
test("not visible when not experimental", async ({ page }) => {
await expect(page.getByTestId("vehicle-status-batteryboost")).not.toBeVisible();
await page.getByTestId("loadpoint-settings-button").nth(1).click();
const modal = page.getByTestId("loadpoint-settings-modal");
await expect(modal.getByTestId("battery-boost")).not.toBeVisible();
});
test("activate boost in solar mode", async ({ page }) => {
await expect(page.getByTestId("vehicle-status-batteryboost")).not.toBeVisible();
test("activate and deactivate boost in solar mode", async ({ page }) => {
const boostButton = page.getByTestId("battery-boost-button");
await expect(boostButton).not.toBeVisible();
await page
.getByTestId("mode")
.first()
@ -31,22 +25,57 @@ test.describe("boost", async () => {
.click();
await page.getByTestId("loadpoint-settings-button").nth(1).click();
const modal = page.getByTestId("loadpoint-settings-modal");
await modal.getByTestId("battery-boost").getByTestId("battery-boost-checkbox").click();
await modal.getByTestId("battery-boost-limit").selectOption("20 %");
await expect(modal.getByTestId("battery-boost")).toContainText(
"Boost active for this charging session."
"Allow fast charging from home battery until it's drained to 20%."
);
await page.waitForLoadState("networkidle");
await modal.getByLabel("Close").click();
await expectModalHidden(modal);
await expect(page.getByTestId("vehicle-status-batteryboost")).toBeVisible();
await expect(boostButton).toHaveAttribute("aria-label", "Battery boost ready");
// activate boost
await boostButton.click();
await expect(boostButton).toHaveAttribute("aria-label", "Battery boost active");
// deactivate boost
await boostButton.click();
await expect(boostButton).toHaveAttribute("aria-label", "Battery boost ready");
});
test("disabled in fast mode", async ({ page }) => {
await page.getByTestId("mode").first().getByRole("button", { name: "Fast" }).click();
test("battery too low for boost when limit above soc", async ({ page }) => {
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");
await expect(modal.getByTestId("battery-boost-checkbox")).toBeDisabled();
await expect(modal.getByTestId("battery-boost")).toContainText(
"Only available in solar and min+solar mode."
);
await modal.getByTestId("battery-boost-limit").selectOption("90 %");
await expect(modal.getByTestId("battery-boost")).toContainText("drained to 90%");
await page.waitForLoadState("networkidle");
await modal.getByLabel("Close").click();
await expectModalHidden(modal);
// limit (90%) above battery soc (50%)
await expect(boostButton).toHaveAttribute("aria-label", "Battery too low for boost");
});
test("boost button disabled in fast mode", async ({ page }) => {
const boostButton = page.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 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 expect(boostButton).toBeDisabled();
});
});