Vehicle: add charge mode in arrival tab (#31330)
This commit is contained in:
parent
d495d3e442
commit
cc5ba51c31
17 changed files with 401 additions and 75 deletions
|
|
@ -1,70 +1,80 @@
|
|||
<template>
|
||||
<div class="mt-4 container">
|
||||
<div class="row">
|
||||
<div class="col-6 col-lg-3 col-form-label">
|
||||
<label :for="formId('minsoc')">
|
||||
<div class="row mb-4">
|
||||
<label :for="formId('mode')" class="col-12 col-md-4 col-lg-3 col-form-label">
|
||||
{{ $t("config.loadpoint.defaultModeLabel") }}
|
||||
</label>
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
<select
|
||||
:id="formId('mode')"
|
||||
v-model="selectedMode"
|
||||
class="form-select"
|
||||
@change="changeMode"
|
||||
>
|
||||
<option v-for="opt in modeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.name }}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ modeHelp }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="socSettingsVisible">
|
||||
<div class="row mb-4">
|
||||
<label :for="formId('minsoc')" class="col-12 col-md-4 col-lg-3 col-form-label">
|
||||
{{ $t("main.loadpointSettings.minSoc.label") }}
|
||||
</label>
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
<select
|
||||
:id="formId('minsoc')"
|
||||
v-model.number="selectedMinSoc"
|
||||
class="form-select"
|
||||
@change="changeMinSoc"
|
||||
>
|
||||
<option v-for="soc in minSocOptions" :key="soc.value" :value="soc.value">
|
||||
{{ soc.name }}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
{{
|
||||
$t("main.loadpointSettings.minSoc.description", [
|
||||
selectedMinSoc ? fmtPercentage(selectedMinSoc) : "x",
|
||||
])
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<select
|
||||
:id="formId('minsoc')"
|
||||
v-model.number="selectedMinSoc"
|
||||
class="form-select mb-2"
|
||||
:disabled="arrivalDisabled"
|
||||
@change="changeMinSoc"
|
||||
>
|
||||
<option v-for="soc in minSocOptions" :key="soc.value" :value="soc.value">
|
||||
{{ soc.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="col-12 col-lg-6 ps-lg-4 col-form-label mb-4">
|
||||
{{
|
||||
$t("main.loadpointSettings.minSoc.description", [
|
||||
selectedMinSoc ? fmtPercentage(selectedMinSoc) : "x",
|
||||
])
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 col-lg-3 col-form-label">
|
||||
<label :for="formId('limitsoc')">
|
||||
<div class="row mb-4">
|
||||
<label :for="formId('limitsoc')" class="col-12 col-md-4 col-lg-3 col-form-label">
|
||||
{{ $t("main.loadpointSettings.limitSoc.label") }}
|
||||
</label>
|
||||
<div class="col-12 col-md-8 col-lg-6">
|
||||
<select
|
||||
:id="formId('limitsoc')"
|
||||
v-model.number="selectedLimitSoc"
|
||||
class="form-select"
|
||||
@change="changeLimitSoc"
|
||||
>
|
||||
<option v-for="soc in limitSocOptions" :key="soc.value" :value="soc.value">
|
||||
{{ soc.name }}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
{{ $t("main.loadpointSettings.limitSoc.description") }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<select
|
||||
:id="formId('limitsoc')"
|
||||
v-model.number="selectedLimitSoc"
|
||||
class="form-select mb-2"
|
||||
:disabled="arrivalDisabled"
|
||||
@change="changeLimitSoc"
|
||||
>
|
||||
<option v-for="soc in limitSocOptions" :key="soc.value" :value="soc.value">
|
||||
{{ soc.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="col-12 col-lg-6 ps-lg-4 col-form-label mb-4">
|
||||
{{ $t("main.loadpointSettings.limitSoc.description") }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="arrivalDisabled" class="mx-2 small text-muted">
|
||||
<strong class="text-evcc">
|
||||
{{ $t("main.loadpointSettings.disclaimerHint") }}
|
||||
</strong>
|
||||
{{ $t("main.loadpointSettings.onlyForSocBasedCharging") }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { distanceUnit } from "@/units";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import type { SelectOption } from "@/types/evcc";
|
||||
import { CHARGE_MODE, type SelectOption } from "@/types/evcc";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
const { OFF, PV, MINPV, NOW } = CHARGE_MODE;
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChargingPlanArrival",
|
||||
mixins: [formatter],
|
||||
|
|
@ -72,16 +82,35 @@ export default defineComponent({
|
|||
id: [String, Number],
|
||||
minSoc: { type: Number, default: 0 },
|
||||
limitSoc: { type: Number, default: 0 },
|
||||
mode: { type: String, default: "" },
|
||||
vehicleName: String,
|
||||
vehicleNotReachable: Boolean,
|
||||
socBasedCharging: Boolean,
|
||||
rangePerSoc: Number,
|
||||
},
|
||||
emits: ["minsoc-updated", "limitsoc-updated"],
|
||||
emits: ["minsoc-updated", "limitsoc-updated", "mode-updated"],
|
||||
data() {
|
||||
return { selectedMinSoc: this.minSoc, selectedLimitSoc: this.limitSoc };
|
||||
return {
|
||||
selectedMinSoc: this.minSoc,
|
||||
selectedLimitSoc: this.limitSoc,
|
||||
selectedMode: this.mode,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
modeOptions(): SelectOption<string>[] {
|
||||
return [
|
||||
{ value: "", name: "---" },
|
||||
...[OFF, PV, MINPV, NOW].map((mode) => ({
|
||||
value: mode,
|
||||
name: this.$t(`main.mode.${mode}`),
|
||||
})),
|
||||
];
|
||||
},
|
||||
modeHelp(): string {
|
||||
return this.selectedMode === ""
|
||||
? this.$t("config.loadpoint.defaultModeHelpKeep")
|
||||
: this.$t("config.loadpoint.defaultModeHelp.charging");
|
||||
},
|
||||
minSocOptions(): SelectOption<number>[] {
|
||||
// a list of entries from 0 to 95 with a step of 5
|
||||
return Array.from(Array(20).keys())
|
||||
|
|
@ -94,8 +123,8 @@ export default defineComponent({
|
|||
.map((i) => i * 5)
|
||||
.map(this.socOption);
|
||||
},
|
||||
arrivalDisabled(): boolean {
|
||||
return !(this.socBasedCharging || this.vehicleNotReachable);
|
||||
socSettingsVisible(): boolean {
|
||||
return this.socBasedCharging || this.vehicleNotReachable;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -105,6 +134,9 @@ export default defineComponent({
|
|||
limitSoc(value: number): void {
|
||||
this.selectedLimitSoc = value;
|
||||
},
|
||||
mode(value: string): void {
|
||||
this.selectedMode = value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
socOption(soc: number): SelectOption<number> {
|
||||
|
|
@ -122,6 +154,9 @@ export default defineComponent({
|
|||
changeLimitSoc(): void {
|
||||
this.$emit("limitsoc-updated", this.selectedLimitSoc);
|
||||
},
|
||||
changeMode(): void {
|
||||
this.$emit("mode-updated", this.selectedMode);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
@closed="modalInvisible"
|
||||
>
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs">
|
||||
<ul v-if="vehicle" class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
{{ $t("main.chargingPlan.departureTab") }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<li v-if="vehicle" class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
:class="{ active: arrivalTabActive }"
|
||||
|
|
@ -65,12 +65,14 @@
|
|||
:id="id"
|
||||
:minSoc="vehicle?.minSoc"
|
||||
:limitSoc="vehicle?.limitSoc"
|
||||
:mode="vehicle?.mode"
|
||||
:vehicleName="vehicle?.name"
|
||||
:vehicleNotReachable="loadpoint?.vehicleNotReachable"
|
||||
:socBasedCharging="loadpoint?.socBasedCharging"
|
||||
:rangePerSoc="loadpoint?.rangePerSoc"
|
||||
@minsoc-updated="setMinSoc"
|
||||
@limitsoc-updated="setLimitSoc"
|
||||
@mode-updated="setMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -91,7 +93,14 @@ import type {
|
|||
StaticPlan,
|
||||
StaticSocPlan,
|
||||
} from "./types";
|
||||
import type { CURRENCY, Forecast, SMART_COST_TYPE, UiLoadpoint, Vehicle } from "@/types/evcc";
|
||||
import type {
|
||||
CHARGE_MODE,
|
||||
CURRENCY,
|
||||
Forecast,
|
||||
SMART_COST_TYPE,
|
||||
UiLoadpoint,
|
||||
Vehicle,
|
||||
} from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChargingPlanModal",
|
||||
|
|
@ -142,16 +151,17 @@ export default defineComponent({
|
|||
},
|
||||
modalTitle(): string {
|
||||
const baseTitle = this.$t("main.chargingPlan.modalTitle");
|
||||
if (this.loadpoint?.socBasedPlanning && this.vehicle) {
|
||||
const withVehicle = this.arrivalTabActive || this.loadpoint?.socBasedPlanning;
|
||||
if (withVehicle && this.vehicle) {
|
||||
return `${baseTitle}: ${this.vehicle.title}`;
|
||||
}
|
||||
return baseTitle;
|
||||
},
|
||||
departureTabActive(): boolean {
|
||||
return this.activeTab === "departure";
|
||||
return this.activeTab === "departure" || !this.vehicle;
|
||||
},
|
||||
arrivalTabActive(): boolean {
|
||||
return this.activeTab === "arrival";
|
||||
return this.activeTab === "arrival" && !!this.vehicle;
|
||||
},
|
||||
apiVehicle(): string {
|
||||
return `vehicles/${this.vehicle?.name}/`;
|
||||
|
|
@ -188,6 +198,13 @@ export default defineComponent({
|
|||
setLimitSoc(soc: number): void {
|
||||
api.post(`${this.apiVehicle}limitsoc/${soc}`);
|
||||
},
|
||||
setMode(mode: CHARGE_MODE | ""): void {
|
||||
if (mode === "") {
|
||||
api.delete(`${this.apiVehicle}mode`);
|
||||
} else {
|
||||
api.post(`${this.apiVehicle}mode/${mode}`);
|
||||
}
|
||||
},
|
||||
updateStaticPlan(plan: StaticPlan): void {
|
||||
const timeISO = plan.time.toISOString();
|
||||
if (this.loadpoint?.socBasedPlanning) {
|
||||
|
|
|
|||
|
|
@ -677,6 +677,7 @@ export interface BatteryMeter extends Meter {
|
|||
|
||||
export interface Vehicle {
|
||||
name: string;
|
||||
mode?: CHARGE_MODE | "";
|
||||
minSoc?: number;
|
||||
limitSoc?: number;
|
||||
plan?: StaticPlan;
|
||||
|
|
|
|||
|
|
@ -148,7 +148,12 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {
|
|||
lp.publish(keys.VehicleName, vehicle.Settings(lp.log, v).Name())
|
||||
lp.publish(keys.VehicleTitle, v.GetTitle())
|
||||
|
||||
if mode, ok := v.OnIdentified().GetMode(); ok {
|
||||
// vehicle mode overrides the yaml onIdentify action
|
||||
mode, ok := v.OnIdentified().GetMode()
|
||||
if m := vehicle.Settings(lp.log, v).GetMode(); m != "" {
|
||||
mode, ok = m, true
|
||||
}
|
||||
if ok && mode != "" {
|
||||
lp.SetMode(mode)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type vehicleStruct struct {
|
|||
Icon string `json:"icon,omitempty"`
|
||||
Capacity float64 `json:"capacity,omitempty"`
|
||||
Phases int `json:"phases,omitempty"`
|
||||
Mode api.ChargeMode `json:"mode,omitempty"`
|
||||
MinSoc int `json:"minSoc,omitempty"`
|
||||
LimitSoc int `json:"limitSoc,omitempty"`
|
||||
MinCurrent float64 `json:"minCurrent,omitempty"`
|
||||
|
|
@ -59,6 +60,7 @@ func (site *Site) publishVehicles() {
|
|||
Icon: instance.Icon(),
|
||||
Capacity: instance.Capacity(),
|
||||
Phases: instance.Phases(),
|
||||
Mode: v.GetMode(),
|
||||
MinSoc: v.GetMinSoc(),
|
||||
LimitSoc: v.GetLimitSoc(),
|
||||
MinCurrent: ac.MinCurrent,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,21 @@ func (v *adapter) Name() string {
|
|||
return v.name
|
||||
}
|
||||
|
||||
// GetMode returns the charge mode
|
||||
func (v *adapter) GetMode() api.ChargeMode {
|
||||
if s, err := settings.String(v.key() + keys.Mode); err == nil {
|
||||
return api.ChargeMode(s)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetMode sets the charge mode
|
||||
func (v *adapter) SetMode(mode api.ChargeMode) {
|
||||
v.log.DEBUG.Printf("set %s mode: %s", v.name, mode)
|
||||
settings.SetString(v.key()+keys.Mode, string(mode))
|
||||
v.publish()
|
||||
}
|
||||
|
||||
// GetMinSoc returns the min soc
|
||||
func (v *adapter) GetMinSoc() int {
|
||||
if v, err := settings.Int(v.key() + keys.MinSoc); err == nil {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ type API interface {
|
|||
// Name returns the vehicle name
|
||||
Name() string
|
||||
|
||||
// GetMode returns the charge mode
|
||||
GetMode() api.ChargeMode
|
||||
// SetMode sets the charge mode
|
||||
SetMode(api.ChargeMode)
|
||||
|
||||
// GetMinSoc returns the min soc
|
||||
GetMinSoc() int
|
||||
// SetMinSoc sets the min soc
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ func (v *dummy) Name() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// GetMode returns the charge mode
|
||||
func (v *dummy) GetMode() api.ChargeMode {
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetMode sets the charge mode
|
||||
func (v *dummy) SetMode(mode api.ChargeMode) {
|
||||
}
|
||||
|
||||
// GetMinSoc returns the min soc
|
||||
func (v *dummy) GetMinSoc() int {
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -69,6 +69,20 @@ func (mr *MockAPIMockRecorder) GetMinSoc() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinSoc", reflect.TypeOf((*MockAPI)(nil).GetMinSoc))
|
||||
}
|
||||
|
||||
// GetMode mocks base method.
|
||||
func (m *MockAPI) GetMode() api.ChargeMode {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetMode")
|
||||
ret0, _ := ret[0].(api.ChargeMode)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetMode indicates an expected call of GetMode.
|
||||
func (mr *MockAPIMockRecorder) GetMode() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMode", reflect.TypeOf((*MockAPI)(nil).GetMode))
|
||||
}
|
||||
|
||||
// GetPlanSoc mocks base method.
|
||||
func (m *MockAPI) GetPlanSoc() (time.Time, int) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
@ -164,6 +178,18 @@ func (mr *MockAPIMockRecorder) SetMinSoc(soc any) *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMinSoc", reflect.TypeOf((*MockAPI)(nil).SetMinSoc), soc)
|
||||
}
|
||||
|
||||
// SetMode mocks base method.
|
||||
func (m *MockAPI) SetMode(arg0 api.ChargeMode) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "SetMode", arg0)
|
||||
}
|
||||
|
||||
// SetMode indicates an expected call of SetMode.
|
||||
func (mr *MockAPIMockRecorder) SetMode(arg0 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMode", reflect.TypeOf((*MockAPI)(nil).SetMode), arg0)
|
||||
}
|
||||
|
||||
// SetPlanSoc mocks base method.
|
||||
func (m *MockAPI) SetPlanSoc(arg0 time.Time, arg1 int) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
|||
|
|
@ -1365,7 +1365,6 @@
|
|||
"batteryUsage": "Hausbatterie",
|
||||
"currents": "Ladestrom",
|
||||
"default": "default",
|
||||
"disclaimerHint": "Hinweis:",
|
||||
"limitSoc": {
|
||||
"description": "Dieses Ladelimit wird verwendet, wenn das Fahrzeug angeschlossen wird.",
|
||||
"label": "Standard Ladelimit"
|
||||
|
|
@ -1380,7 +1379,6 @@
|
|||
"description": "Fahrzeug wird im PV-Modus „Schnell“ auf {0} geladen. Danach weiter mit PV-Überschuss. Nützlich, um auch an dunklen Tagen eine Mindestreichweite zu gewährleisten.",
|
||||
"label": "Min. Ladung %"
|
||||
},
|
||||
"onlyForSocBasedCharging": "Diese Optionen sind nur für Fahrzeuge mit bekanntem Ladestand verfügbar.",
|
||||
"phasesConfigured": {
|
||||
"label": "Phasen",
|
||||
"no1p3pSupport": "Wie ist deine Wallbox angeschlossen?",
|
||||
|
|
|
|||
|
|
@ -1365,7 +1365,6 @@
|
|||
"batteryUsage": "Home Battery",
|
||||
"currents": "Charging Current",
|
||||
"default": "default",
|
||||
"disclaimerHint": "Note:",
|
||||
"limitSoc": {
|
||||
"description": "Charging limit that is used when this vehicle is connected.",
|
||||
"label": "Default limit"
|
||||
|
|
@ -1380,7 +1379,6 @@
|
|||
"description": "The vehicle gets „fast” charged to {0} in solar mode. Then continues with solar surplus. Useful to ensure a minimum range even for darker days.",
|
||||
"label": "Min. charge %"
|
||||
},
|
||||
"onlyForSocBasedCharging": "These options are only available for vehicles with known charging level.",
|
||||
"phasesConfigured": {
|
||||
"label": "Phases",
|
||||
"no1p3pSupport": "How is your charger connected?",
|
||||
|
|
|
|||
|
|
@ -173,6 +173,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) {
|
|||
|
||||
// vehicle api
|
||||
vehicles := map[string]route{
|
||||
"mode": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/mode/{value:[a-z]+}", vehicleModeHandler(site)},
|
||||
"modeDelete": {"DELETE", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/mode", vehicleModeHandler(site)},
|
||||
"minsoc": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/minsoc/{value:[0-9]+}", minSocHandler(site)},
|
||||
"limitsoc": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/limitsoc/{value:[0-9]+}", limitSocHandler(site)},
|
||||
"plan": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc/{value:[0-9]+}/{time:[0-9TZ:.+-]+}", planSocHandler(site)},
|
||||
|
|
|
|||
|
|
@ -69,6 +69,35 @@ func limitSocHandler(site site.API) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// vehicleModeHandler updates the vehicle charge mode (empty value clears it)
|
||||
func vehicleModeHandler(site site.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
v, err := site.Vehicles().ByName(vars["name"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
mode, err := api.ChargeModeString(vars["value"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
v.SetMode(mode)
|
||||
|
||||
res := struct {
|
||||
Mode api.ChargeMode `json:"mode"`
|
||||
}{
|
||||
Mode: v.GetMode(),
|
||||
}
|
||||
|
||||
jsonWrite(w, res)
|
||||
}
|
||||
}
|
||||
|
||||
// planSocHandler updates plan soc and time
|
||||
func planSocHandler(site site.API) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -2864,6 +2864,67 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/vehicles/{name}/mode": {
|
||||
"delete": {
|
||||
"description": "Resets the vehicle charge mode to keep the last selected mode.",
|
||||
"externalDocs": {
|
||||
"url": "https://docs.evcc.io/en/docs/features/solar-charging"
|
||||
},
|
||||
"operationId": "deleteVehicleMode",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/vehicleName"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/NullResult"
|
||||
}
|
||||
},
|
||||
"summary": "Reset charge mode",
|
||||
"tags": [
|
||||
"vehicles"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/vehicles/{name}/mode/{mode}": {
|
||||
"post": {
|
||||
"description": "Sets the charge mode applied when this vehicle becomes active on a loadpoint.",
|
||||
"externalDocs": {
|
||||
"url": "https://docs.evcc.io/en/docs/features/solar-charging"
|
||||
},
|
||||
"operationId": "setVehicleMode",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/vehicleName"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/mode"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"properties": {
|
||||
"mode": {
|
||||
"$ref": "#/components/schemas/Mode"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Success"
|
||||
}
|
||||
},
|
||||
"summary": "Set charge mode",
|
||||
"tags": [
|
||||
"vehicles"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/vehicles/{name}/plan/repeating": {
|
||||
"post": {
|
||||
"description": "Updates the repeating charging plan.",
|
||||
|
|
|
|||
|
|
@ -1192,6 +1192,26 @@ call getTariffInfo {
|
|||
}
|
||||
```
|
||||
|
||||
## deleteVehicleMode
|
||||
|
||||
Resets the vehicle charge mode to keep the last selected mode.
|
||||
|
||||
**Tags:** vehicles
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| name | string | Vehicle name |
|
||||
|
||||
**Example call:**
|
||||
|
||||
```json
|
||||
call deleteVehicleMode {
|
||||
"name": "vehicle_1"
|
||||
}
|
||||
```
|
||||
|
||||
## deleteVehicleSocPlan
|
||||
|
||||
Delete the charging plan
|
||||
|
|
@ -1234,6 +1254,28 @@ call setVehicleMinSoc {
|
|||
}
|
||||
```
|
||||
|
||||
## setVehicleMode
|
||||
|
||||
Sets the charge mode applied when this vehicle becomes active on a loadpoint.
|
||||
|
||||
**Tags:** vehicles
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| mode | string | Charging mode. |
|
||||
| name | string | Vehicle name |
|
||||
|
||||
**Example call:**
|
||||
|
||||
```json
|
||||
call setVehicleMode {
|
||||
"mode": "off",
|
||||
"name": "vehicle_1"
|
||||
}
|
||||
```
|
||||
|
||||
## setVehiclePlanStrategy
|
||||
|
||||
Updates the charging plan strategy for the vehicle.
|
||||
|
|
|
|||
|
|
@ -1121,6 +1121,42 @@ paths:
|
|||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/SocResult"
|
||||
/vehicles/{name}/mode:
|
||||
delete:
|
||||
operationId: deleteVehicleMode
|
||||
summary: Reset charge mode
|
||||
description: "Resets the vehicle charge mode to keep the last selected mode."
|
||||
externalDocs:
|
||||
url: https://docs.evcc.io/en/docs/features/solar-charging
|
||||
tags:
|
||||
- vehicles
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/vehicleName"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/NullResult"
|
||||
/vehicles/{name}/mode/{mode}:
|
||||
post:
|
||||
operationId: setVehicleMode
|
||||
summary: Set charge mode
|
||||
description: "Sets the charge mode applied when this vehicle becomes active on a loadpoint."
|
||||
externalDocs:
|
||||
url: https://docs.evcc.io/en/docs/features/solar-charging
|
||||
tags:
|
||||
- vehicles
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/vehicleName"
|
||||
- $ref: "#/components/parameters/mode"
|
||||
responses:
|
||||
"200":
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
mode:
|
||||
$ref: "#/components/schemas/Mode"
|
||||
/vehicles/{name}/plan/repeating:
|
||||
post:
|
||||
operationId: updateVehicleRepeatingPlans
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { start, stop, restart, baseUrl } from "./evcc";
|
||||
import { expectModalVisible, expectModalHidden } from "./utils";
|
||||
import {
|
||||
|
|
@ -104,7 +104,7 @@ test.describe("limitSoc", async () => {
|
|||
});
|
||||
|
||||
test.describe("minSoc and limitSoc", async () => {
|
||||
test("disabled for offline vehicles", async ({ page }) => {
|
||||
test("hidden for offline vehicles", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// switch to offline vehicle
|
||||
|
|
@ -112,19 +112,64 @@ test.describe("minSoc and limitSoc", async () => {
|
|||
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
await expect(page.getByRole("combobox", { name: "Min. charge %" })).toBeDisabled();
|
||||
await expect(page.getByRole("combobox", { name: "Default limit" })).toBeDisabled();
|
||||
await expect(page.getByRole("combobox", { name: "Min. charge %" })).not.toBeVisible();
|
||||
await expect(page.getByRole("combobox", { name: "Default limit" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("disabled for guest vehicles", async ({ page }) => {
|
||||
test("no tabs for guest vehicles", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// switch to offline vehicle
|
||||
// switch to guest vehicle (no known vehicle)
|
||||
await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
|
||||
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
await expect(page.getByRole("combobox", { name: "Min. charge %" })).toBeDisabled();
|
||||
await expect(page.getByRole("combobox", { name: "Default limit" })).toBeDisabled();
|
||||
const modal = page.getByTestId("charging-plan-modal");
|
||||
await expectModalVisible(modal);
|
||||
await expect(modal.getByRole("link", { name: "Departure" })).toHaveCount(0);
|
||||
await expect(modal.getByRole("link", { name: "Arrival" })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("default mode", async () => {
|
||||
async function setDefaultMode(page: Page, option: string) {
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
const modal = page.getByTestId("charging-plan-modal");
|
||||
await expectModalVisible(modal);
|
||||
await modal.getByRole("link", { name: "Arrival" }).click();
|
||||
await modal.getByRole("combobox", { name: "Default mode" }).selectOption(option);
|
||||
await modal.getByRole("button", { name: "Close" }).click();
|
||||
await expectModalHidden(modal);
|
||||
}
|
||||
|
||||
test("apply and restart", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await setDefaultMode(page, "Fast");
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await restart(simulatorConfig());
|
||||
await page.reload();
|
||||
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
await expect(page.getByRole("combobox", { name: "Default mode" })).toHaveValue("now");
|
||||
});
|
||||
|
||||
test("applied on vehicle switch", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const mode = page.getByTestId("loadpoint").first().getByTestId("mode");
|
||||
const changeVehicle = page.getByTestId("change-vehicle").locator("select");
|
||||
|
||||
// per-vehicle default modes
|
||||
await setDefaultMode(page, "Solar");
|
||||
await changeVehicle.selectOption("grüner Honda e");
|
||||
await setDefaultMode(page, "Fast");
|
||||
|
||||
// switching vehicles applies the vehicle's default mode
|
||||
await changeVehicle.selectOption("blauer e-Golf");
|
||||
await expect(mode.getByRole("button", { name: "Solar", exact: true })).toHaveClass(/active/);
|
||||
|
||||
await changeVehicle.selectOption("grüner Honda e");
|
||||
await expect(mode.getByRole("button", { name: "Fast" })).toHaveClass(/active/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue