Battery Boost Button & Limit (BC) (#27392)
This commit is contained in:
parent
7f772bad73
commit
0046661568
54 changed files with 805 additions and 146 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
259
assets/js/components/Loadpoints/BatteryBoostButton.vue
Normal file
259
assets/js/components/Loadpoints/BatteryBoostButton.vue
Normal 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>
|
||||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type API interface {
|
|||
// battery
|
||||
//
|
||||
|
||||
GetBatterySoc() float64
|
||||
GetPrioritySoc() float64
|
||||
SetPrioritySoc(float64) error
|
||||
GetBufferSoc() float64
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -956,7 +956,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "Бързо зареждане от домашна батерия.",
|
||||
"description": "Бързо зареждане от домашна батерия, докато се изтощи до {limit}.",
|
||||
"label": "Батериен бууст",
|
||||
"mode": "Достъпно само в режим „слънчев“ и „мин+слънчев“.",
|
||||
"once": "Усилване активно за тази сесия на зареждане."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
12
i18n/de.json
12
i18n/de.json
|
|
@ -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}.",
|
||||
|
|
|
|||
|
|
@ -961,7 +961,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "Γρήγορη φόρτιση από οικιακή μπαταρία.",
|
||||
"description": "Γρήγορη φόρτιση από οικιακή μπαταρία, μέχρι να εκφορτιστεί στο {limit}.",
|
||||
"label": "Ενίσχυση από μπαταρία",
|
||||
"mode": "Διαθέσιμο μόνο σε λειτουργία Φ/Β ή Ελαχ+Φ/Β.",
|
||||
"once": "Ενεργή ενίσχυση για αυτή τη συνεδρία φόρτισης."
|
||||
|
|
|
|||
12
i18n/en.json
12
i18n/en.json
|
|
@ -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}.",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "Быстрая зарядка от домашней батареи.",
|
||||
"description": "Быстрая зарядка от домашней батареи, пока она не разрядится до {limit}.",
|
||||
"label": "Усиление батареи",
|
||||
"mode": "Доступно только в режимах «Солнечный» и «Мини+солнечный».",
|
||||
"once": "Усиление активно для этой сессии зарядки."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -741,7 +741,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "Fast மின்னூட்டு இருந்து வீடு battery.",
|
||||
"description": "வீட்டு மின்கலத்திலிருந்து {limit} வரை இறங்கும் வரை விரைவு மின்னூட்டம்.",
|
||||
"label": "மின்கலம் ஊட்டம்",
|
||||
"mode": "ஞாயிறு மற்றும் குறை+ஞாயிறு பயன்முறையில் மட்டுமே கிடைக்கும்.",
|
||||
"once": "Boost செயலில் க்கு this charging session."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -965,7 +965,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "Швидка зарядка від домашнього акумулятора.",
|
||||
"description": "Швидка зарядка від домашнього акумулятора, поки він не розрядиться до {limit}.",
|
||||
"label": "Батарея Boost",
|
||||
"mode": "Доступно лише в режимі сонячної батареї та режимі min+sonal.",
|
||||
"once": "Boost активний для цього сеансу зарядки."
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@
|
|||
},
|
||||
"loadpointSettings": {
|
||||
"batteryBoost": {
|
||||
"description": "使用住宅电池快速充电。",
|
||||
"description": "使用住宅电池快速充电,直到电量降至 {limit}。",
|
||||
"label": "电池快充",
|
||||
"mode": "仅在太阳能和最小+太阳能模式下可用。",
|
||||
"once": "本次充电会话已启用快充。"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue