Persist targetSoc/Energy, minSoc, targetTime per vehicle, add minSoc to UI (#8115)
* Plugins: make javascript return values more permissive * Persist targetSoc/Energy, minSoc and target time, remove experimental from minSoc UI * go generate * Fix tests * Update minSoc description * Store settings by index. Show plan outside pv mode. Add help texts. * remove slug * go mock * lint * chore: simplify templates (#8304) * Add e2e tests with playwright (#8123) * Revert "1p3p: let charger handle session stop/restart (#7723)" This reverts commitdd787ce52f. * mazda2mqtt: document vin required (#8319) * FoxESS: split H1/H3 devices (#7376) Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> * Translations update from Hosted Weblate (#8124) * Translated using Weblate (Czech) Currently translated at 37.3% (99 of 265 strings) Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/cs/ Translation: evcc/evcc * Translated using Weblate (Finnish) Currently translated at 100.0% (265 of 265 strings) Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fi/ Translation: evcc/evcc * Translated using Weblate (German) Currently translated at 100.0% (265 of 265 strings) Co-authored-by: ThinkEV <claas@rootdir.de> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/de/ Translation: evcc/evcc * Translated using Weblate (Croatian) Currently translated at 100.0% (265 of 265 strings) Translated using Weblate (Slovenian) Currently translated at 100.0% (265 of 265 strings) Co-authored-by: Žiga Deisinger <ziga@deisinger.si> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hr/ Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sl/ Translation: evcc/evcc * Translated using Weblate (Catalan) Currently translated at 100.0% (265 of 265 strings) Co-authored-by: Norbert Poch <npoch@yahoo.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/ca/ Translation: evcc/evcc * Translated using Weblate (Dutch) Currently translated at 76.6% (203 of 265 strings) Translated using Weblate (Dutch) Currently translated at 74.3% (197 of 265 strings) Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com> Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/ Translation: evcc/evcc * fix toml --------- Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com> Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi> Co-authored-by: ThinkEV <claas@rootdir.de> Co-authored-by: Žiga Deisinger <ziga@deisinger.si> Co-authored-by: Norbert Poch <npoch@yahoo.com> Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com> Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> * Add OBO Betterman Ion (#8321) Also aligns heartbeat implementations * Enphase: add token auth (firmware D7.x.xxx) and grid (#8247) * Mqtt: fix smartCostLimit topic case (#8328) * Add ISO15118 vehicle template (#8302) * Mqtt: simplify setters * Mqtt: disable message ordering to improve performance * chore: simplify random state generation * Porsche: remove deprecated mobile api (#8349) * Porsche: remove remaining mobile api types * Cupra: add odometer (#8340) * chore: refactor go-stylish * chore: fix stale handler * mazda2mqtt: longer timeout (#8364) * chore: pre-process toml * chore: prevent toml double quotes * Audi: temporarily switch to etron (#8374) * Fix nightly build (#8384) * Update SunSpec templates (#8270) * Revert "Fix nightly build (#8384)" This reverts commit536dbc936c. * Revert "Add e2e tests with playwright (#8123)" This reverts commit0df815729b. * Easee: update Observation IDs (#8391) * Easee: handle smartCharging errors (#8389) * TWC: add non-Tesla vehicle warning (#8329) * TWC: allow loadpoint to wakeup vehicle (#8284) * chore: use gridx consts * Fix ISO15118 vehicle (#8402) Such vehicles would be reported as `(Offline)` previously, as no Soc can be fetched and thus needs to behave as the `offline` template. The SoC is fetched via the charger, so hardcode it to 0 in here * Modbus: add coils (#8385) * OpenEVSE: fix api (#8415) * chore: refactor offline vehicles (#8404) * Easee: wait for api confirmation (#8307) * Revert "Easee: wait for api confirmation (#8307)" This reverts commitce0e4277aa. * automatically switch session log energy unit (#8371) Show the charged energy in the session log table and details in Wh for values below 1 kWh and in kWh for larger values * Fix nightly/release + integration workflow (#8450) * Revert "Add e2e tests with playwright (#8123)" This reverts commit0df815729b. * Revert "Revert "Add e2e tests with playwright (#8123)"" This reverts commit2299306954. * fix nightly/release * fix nightly/release * fix nightly/release --------- Co-authored-by: andig <cpuidle@gmx.de> * chore: no integration on nightly * chore: no integration on nightly * chore: no integration on nightly * Sessions: filter, monthly sums, paging (#8278) * fmt * Update i18n/en.toml Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com> * Use exp/slices * Use mux consistently * add integration tests --------- Co-authored-by: andig <cpuidle@gmx.de> Co-authored-by: C64Axel <33828760+C64Axel@users.noreply.github.com> Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com> Co-authored-by: Weblate (bot) <hosted@weblate.org> Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com> Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi> Co-authored-by: ThinkEV <claas@rootdir.de> Co-authored-by: Žiga Deisinger <ziga@deisinger.si> Co-authored-by: Norbert Poch <npoch@yahoo.com> Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com> Co-authored-by: salz3n <79696598+salz3n@users.noreply.github.com> Co-authored-by: Daniel Paschke <paschdan@gmail.com> Co-authored-by: Andreas Linde <42185+DerAndereAndi@users.noreply.github.com> Co-authored-by: lex777777 <84906358+lex777777@users.noreply.github.com> Co-authored-by: Oscar <OAltr@users.noreply.github.com> Co-authored-by: Michael Heß <GrimmiMeloni@users.noreply.github.com> Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com> Co-authored-by: kscholty <47229207+kscholty@users.noreply.github.com> Co-authored-by: Jan Alexander <jan.alexander@posteo.de> Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com>
This commit is contained in:
parent
e6b88f7b58
commit
7de484969a
19 changed files with 437 additions and 52 deletions
|
|
@ -3,13 +3,13 @@
|
|||
<LabelAndValue
|
||||
class="root flex-grow-1"
|
||||
:label="title"
|
||||
:class="disabled ? 'opacity-0' : 'opacity-1'"
|
||||
:class="disabled ? 'opacity-25' : 'opacity-100'"
|
||||
data-testid="charging-plan"
|
||||
>
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline justify-content-center">
|
||||
<button
|
||||
class="value-button p-0"
|
||||
:class="enabled ? 'evcc-default-text' : 'text-gray'"
|
||||
:disabled="disabled"
|
||||
@click="openModal"
|
||||
>
|
||||
<strong v-if="minSocEnabled">{{ minSocLabel }}</strong>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<ul v-if="showTabs" class="nav nav-tabs">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
href="#"
|
||||
@click.prevent="showArrivalTab"
|
||||
>
|
||||
{{ $t("main.chargingPlan.arrivalTab") }} 🧪
|
||||
{{ $t("main.chargingPlan.arrivalTab") }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -113,6 +113,7 @@ export default {
|
|||
smartCostLimit: Number,
|
||||
smartCostType: String,
|
||||
currency: String,
|
||||
mode: String,
|
||||
},
|
||||
emits: ["target-time-updated", "target-time-removed", "minsoc-updated"],
|
||||
data: function () {
|
||||
|
|
@ -145,7 +146,7 @@ export default {
|
|||
return this.$t("main.chargingPlan.title");
|
||||
},
|
||||
minSocEnabled: function () {
|
||||
return this.minSoc >= this.vehicleSoc && this.$hiddenFeatures();
|
||||
return this.minSoc > this.vehicleSoc;
|
||||
},
|
||||
departureTabActive: function () {
|
||||
return this.activeTab === "departure";
|
||||
|
|
@ -159,9 +160,6 @@ export default {
|
|||
chargingPlanArrival: function () {
|
||||
return this.collectProps(ChargingPlanArrival);
|
||||
},
|
||||
showTabs: function () {
|
||||
return this.$hiddenFeatures();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = Modal.getOrCreateInstance(this.$refs.modal);
|
||||
|
|
@ -180,12 +178,10 @@ export default {
|
|||
this.isModalVisible = false;
|
||||
},
|
||||
openModal() {
|
||||
this.showDeatureTab();
|
||||
if (this.minSocEnabled) {
|
||||
this.showArrivalTab();
|
||||
}
|
||||
if (this.targetChargeEnabled) {
|
||||
this.showDeatureTab();
|
||||
}
|
||||
this.modal.show();
|
||||
},
|
||||
// not computed because it needs to update over time
|
||||
|
|
|
|||
|
|
@ -1,27 +1,34 @@
|
|||
<template>
|
||||
<div class="root mt-4">
|
||||
<div class="d-flex justify-content-between align-items-baseline mb-2">
|
||||
<label :for="formId('minsoc')" class="pt-1">
|
||||
{{ $t("main.loadpointSettings.minSoc.label") }}
|
||||
</label>
|
||||
<select
|
||||
:id="formId('minsoc')"
|
||||
v-model.number="selectedMinSoc"
|
||||
class="minSocSelect form-select mb-2"
|
||||
@change="changeMinSoc"
|
||||
>
|
||||
<option
|
||||
v-for="soc in [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]"
|
||||
:key="soc"
|
||||
:value="soc"
|
||||
<div class="mt-4 container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div
|
||||
class="minsoc d-flex justify-content-between align-items-baseline mb-2 me-sm-4"
|
||||
>
|
||||
{{ soc ? `${soc}%` : "--" }}
|
||||
</option>
|
||||
</select>
|
||||
<label :for="formId('minsoc')" class="pt-1">
|
||||
{{ $t("main.loadpointSettings.minSoc.label") }}
|
||||
</label>
|
||||
<select
|
||||
:id="formId('minsoc')"
|
||||
v-model.number="selectedMinSoc"
|
||||
class="minSocSelect form-select mb-2"
|
||||
:disabled="!socBasedCharging"
|
||||
@change="changeMinSoc"
|
||||
>
|
||||
<option
|
||||
v-for="soc in [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]"
|
||||
:key="soc"
|
||||
:value="soc"
|
||||
>
|
||||
{{ soc ? `${soc}%` : "--" }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<small class="col-12 col-md-6 ps-md-4">
|
||||
{{ $t("main.loadpointSettings.minSoc.description", [selectedMinSoc || "x"]) }}
|
||||
</small>
|
||||
</div>
|
||||
<small>
|
||||
{{ $t("main.loadpointSettings.minSoc.description", [selectedMinSoc || "x"]) }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -32,6 +39,7 @@ export default {
|
|||
id: [String, Number],
|
||||
minSoc: Number,
|
||||
vehicleName: String,
|
||||
socBasedCharging: Boolean,
|
||||
},
|
||||
emits: ["minsoc-updated"],
|
||||
data: function () {
|
||||
|
|
@ -54,7 +62,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.root {
|
||||
.minsoc {
|
||||
max-width: 300px;
|
||||
}
|
||||
.minSocSelect {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="mt-4">
|
||||
<div class="form-group d-lg-flex align-items-baseline mb-2 justify-content-between">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<label for="targetTimeLabel" class="mb-3 me-3">
|
||||
<label :for="`targetTimeLabel${id}`" class="mb-3 me-3">
|
||||
<span v-if="socBasedCharging">
|
||||
{{
|
||||
$t("main.targetCharge.descriptionSoc", {
|
||||
|
|
@ -21,7 +21,12 @@
|
|||
</label>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<div class="d-flex justify-content-between date-selection">
|
||||
<select v-model="selectedDay" class="form-select me-2">
|
||||
<select
|
||||
:id="`targetTimeLabel${id}`"
|
||||
v-model="selectedDay"
|
||||
class="form-select me-2"
|
||||
data-testid="target-day"
|
||||
>
|
||||
<option v-for="opt in dayOptions()" :key="opt.value" :value="opt.value">
|
||||
{{ opt.name }}
|
||||
</option>
|
||||
|
|
@ -32,23 +37,30 @@
|
|||
class="form-control ms-2 time-selection"
|
||||
:step="60 * 5"
|
||||
required
|
||||
data-testid="target-time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">
|
||||
<span v-if="timeInThePast" class="d-block text-danger">
|
||||
<span v-if="timeInThePast" class="d-block text-danger mb-1">
|
||||
{{ $t("main.targetCharge.targetIsInThePast") }}
|
||||
</span>
|
||||
<span v-else-if="timeTooFarInTheFuture" class="d-block text-secondary">
|
||||
<span v-if="!socBasedCharging && !targetEnergy" class="d-block text-danger mb-1">
|
||||
{{ $t("main.targetCharge.targetEnergyRequired") }}
|
||||
</span>
|
||||
<span v-if="timeTooFarInTheFuture" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.targetIsTooFarInTheFuture") }}
|
||||
</span>
|
||||
<span v-if="costLimitExists" class="d-block text-secondary">
|
||||
<span v-if="costLimitExists" class="d-block text-secondary mb-1">
|
||||
{{
|
||||
$t("main.targetCharge.costLimitIgnore", {
|
||||
limit: costLimitText,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span v-if="['off', 'now'].includes(mode)" class="d-block text-secondary mb-1">
|
||||
{{ $t("main.targetCharge.onlyInPvMode") }}
|
||||
</span>
|
||||
|
||||
</p>
|
||||
<TargetChargePlan v-if="targetChargePlanProps" v-bind="targetChargePlanProps" />
|
||||
|
|
@ -101,6 +113,7 @@ export default {
|
|||
smartCostLimit: Number,
|
||||
smartCostType: String,
|
||||
currency: String,
|
||||
mode: String,
|
||||
},
|
||||
emits: ["target-time-updated", "target-time-removed"],
|
||||
data: function () {
|
||||
|
|
@ -110,6 +123,7 @@ export default {
|
|||
plan: {},
|
||||
tariff: {},
|
||||
activeTab: "time",
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -200,9 +214,11 @@ export default {
|
|||
if (
|
||||
!this.timeInThePast &&
|
||||
(this.targetEnergy || this.targetSoc) &&
|
||||
!isNaN(this.selectedTargetTime)
|
||||
!isNaN(this.selectedTargetTime) &&
|
||||
!this.loading
|
||||
) {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.plan = (
|
||||
await api.get(`loadpoints/${this.id}/target/plan`, {
|
||||
params: { targetTime: this.selectedTargetTime },
|
||||
|
|
@ -217,6 +233,8 @@ export default {
|
|||
this.tariff = tariffRes.status === 404 ? { rates: [] } : tariffRes.data.result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<LabelAndValue class="flex-grow-1" :label="$t('main.targetEnergy.label')" align="end">
|
||||
<LabelAndValue
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.targetEnergy.label')"
|
||||
align="end"
|
||||
data-testid="target-energy"
|
||||
>
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline justify-content-end">
|
||||
<label class="position-relative">
|
||||
<select :value="targetEnergy" class="custom-select" @change="change">
|
||||
|
|
@ -15,6 +20,7 @@
|
|||
<span
|
||||
class="text-decoration-underline"
|
||||
:class="{ 'text-gray fw-normal': !targetEnergy }"
|
||||
data-testid="target-energy-value"
|
||||
>
|
||||
<AnimatedNumber :to="targetEnergy" :format="fmtEnergy" />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<LabelAndValue class="flex-grow-1" :label="$t('main.vehicle.targetSoc')" align="end">
|
||||
<LabelAndValue
|
||||
class="flex-grow-1"
|
||||
:label="$t('main.vehicle.targetSoc')"
|
||||
align="end"
|
||||
data-testid="target-soc"
|
||||
>
|
||||
<h3 class="value m-0 d-block d-sm-flex align-items-baseline justify-content-end">
|
||||
<label class="position-relative">
|
||||
<select :value="targetSoc" class="custom-select" @change="change">
|
||||
|
|
@ -7,7 +12,7 @@
|
|||
{{ text }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-decoration-underline">
|
||||
<span class="text-decoration-underline" data-testid="target-soc-value">
|
||||
<AnimatedNumber :to="targetSoc" :format="formatSoc" />
|
||||
</span>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="d-block evcc-gray">{{ message }} </div>
|
||||
<div class="d-block evcc-gray" data-testid="vehicle-status">{{ message }} </div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="d-flex justify-content-between mb-3 align-items-center">
|
||||
<div class="d-flex justify-content-between mb-3 align-items-center" data-testid="vehicle-title">
|
||||
<h4 class="d-flex align-items-center m-0 flex-grow-1 overflow-hidden">
|
||||
<shopicon-regular-refresh
|
||||
v-if="icon === 'refresh'"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package coordinator
|
|||
import (
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/loadpoint"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type adapter struct {
|
||||
|
|
@ -23,6 +24,10 @@ func (a *adapter) GetVehicles() []api.Vehicle {
|
|||
return a.c.GetVehicles()
|
||||
}
|
||||
|
||||
func (a *adapter) GetVehicleIndex(v api.Vehicle) int {
|
||||
return slices.Index(a.c.vehicles, v)
|
||||
}
|
||||
|
||||
func (a *adapter) Acquire(v api.Vehicle) {
|
||||
a.c.acquire(a.lp, v)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ type API interface {
|
|||
Acquire(api.Vehicle)
|
||||
Release(api.Vehicle)
|
||||
IdentifyVehicleByStatus() api.Vehicle
|
||||
GetVehicleIndex(api.Vehicle) int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ func New(log *util.Logger, vehicles []api.Vehicle) *Coordinator {
|
|||
}
|
||||
}
|
||||
|
||||
// GetVehicles returns the list of all vehicles
|
||||
func (c *Coordinator) GetVehicles() []api.Vehicle {
|
||||
return c.vehicles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,7 @@ func (a *dummy) Release(v api.Vehicle) {}
|
|||
func (a *dummy) IdentifyVehicleByStatus() api.Vehicle {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *dummy) GetVehicleIndex(v api.Vehicle) int {
|
||||
return -1
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ func (lp *Loadpoint) SetTargetEnergy(energy float64) {
|
|||
if lp.targetEnergy != energy {
|
||||
lp.setTargetEnergy(energy)
|
||||
lp.requestUpdate()
|
||||
lp.persistVehicleSettings()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +142,7 @@ func (lp *Loadpoint) SetTargetSoc(soc int) {
|
|||
if lp.Soc.target != soc {
|
||||
lp.setTargetSoc(soc)
|
||||
lp.requestUpdate()
|
||||
lp.persistVehicleSettings()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +170,7 @@ func (lp *Loadpoint) SetMinSoc(soc int) {
|
|||
if lp.Soc.min != soc {
|
||||
lp.setMinSoc(soc)
|
||||
lp.requestUpdate()
|
||||
lp.persistVehicleSettings()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -219,6 +222,7 @@ func (lp *Loadpoint) SetTargetTime(finishAt time.Time) error {
|
|||
lp.Lock()
|
||||
defer lp.Unlock()
|
||||
lp.setTargetTime(finishAt)
|
||||
lp.persistVehicleSettings()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package core
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/db"
|
||||
"github.com/evcc-io/evcc/core/soc"
|
||||
"github.com/evcc-io/evcc/provider"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
|
|
@ -148,6 +150,7 @@ func (lp *Loadpoint) setActiveVehicle(vehicle api.Vehicle) {
|
|||
lp.publish(vehicleTitle, vehicle.Title())
|
||||
lp.publish(vehicleIcon, vehicle.Icon())
|
||||
lp.publish(vehicleCapacity, vehicle.Capacity())
|
||||
lp.restoreVehicleSettings()
|
||||
|
||||
lp.applyAction(vehicle.OnIdentified())
|
||||
lp.addTask(lp.vehicleOdometer)
|
||||
|
|
@ -223,6 +226,40 @@ func (lp *Loadpoint) publishVehicleFeature(f api.Feature) {
|
|||
lp.publish("vehicleFeature"+f.String(), lp.vehicleHasFeature(f))
|
||||
}
|
||||
|
||||
// persistVehicleSettings stores user configuration (via UI/API) for the current vehicle
|
||||
func (lp *Loadpoint) persistVehicleSettings() {
|
||||
idx := lp.coordinator.GetVehicleIndex(lp.GetVehicle())
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
settings.SetInt(fmt.Sprintf("vehicle.%d.targetSoc", idx), int64(lp.Soc.target))
|
||||
settings.SetFloat(fmt.Sprintf("vehicle.%d.targetEnergy", idx), lp.targetEnergy)
|
||||
settings.SetInt(fmt.Sprintf("vehicle.%d.minSoc", idx), int64(lp.Soc.min))
|
||||
settings.SetTime(fmt.Sprintf("vehicle.%d.targetTime", idx), lp.targetTime)
|
||||
}
|
||||
|
||||
// restoreVehicleSettings restores user configuration (via UI/API) for the current vehicle
|
||||
func (lp *Loadpoint) restoreVehicleSettings() {
|
||||
idx := lp.coordinator.GetVehicleIndex(lp.GetVehicle())
|
||||
if idx == -1 {
|
||||
return
|
||||
}
|
||||
if v, err := settings.Int(fmt.Sprintf("vehicle.%d.targetSoc", idx)); err == nil {
|
||||
lp.setTargetSoc(int(v))
|
||||
}
|
||||
if v, err := settings.Float(fmt.Sprintf("vehicle.%d.targetEnergy", idx)); err == nil {
|
||||
lp.setTargetEnergy(v)
|
||||
}
|
||||
if v, err := settings.Int(fmt.Sprintf("vehicle.%d.minSoc", idx)); err == nil {
|
||||
lp.setMinSoc(int(v))
|
||||
}
|
||||
if v, err := settings.Time(fmt.Sprintf("vehicle.%d.targetTime", idx)); err == nil {
|
||||
if v.After(time.Now()) {
|
||||
lp.setTargetTime(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// vehicleUnidentified returns true if there are associated vehicles and detection is running.
|
||||
// It will also reset the api cache at regular intervals.
|
||||
// Detection is stopped after maximum duration and the "guest vehicle" message dispatched.
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ label = "Max. Ladestrom"
|
|||
label = "Min. Ladestrom"
|
||||
|
||||
[main.loadpointSettings.minSoc]
|
||||
description = "Reichweite für Notfälle. Fahrzeug wird im PV-Modus „schnell“ auf {0}% geladen. Danach weiter mit PV-Überschuss."
|
||||
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 %"
|
||||
|
||||
[main.loadpointSettings.phasesConfigured]
|
||||
|
|
@ -197,6 +197,7 @@ costLimitIgnore = "Die eingestellte {limit} wird in diesem Zeitraum ignoriert."
|
|||
descriptionEnergy = "Bis wann sollen {targetEnergy} ins Fahrzeug geladen sein?"
|
||||
descriptionSoc = "Wann soll das Fahrzeug auf {targetSoc}% geladen sein?"
|
||||
inactiveLabel = "Zielzeit"
|
||||
onlyInPvMode = "Ladeplanung ist nur im PV-Modus aktiv."
|
||||
planDuration = "Ladezeit"
|
||||
planPeriodLabel = "Zeitraum"
|
||||
planPeriodValue = "{start} bis {end}"
|
||||
|
|
@ -204,6 +205,7 @@ planUnknown = "noch unbekannt"
|
|||
priceLimit = "Preisgrenze von {price}"
|
||||
remove = "Entfernen"
|
||||
setTargetTime = "keine"
|
||||
targetEnergyRequired = "Damit die Ladeplanung funktioniert muss ein Ladeziel gesetzt sein."
|
||||
targetIsInThePast = "Wähle einen Zeitpunkt in der Zukunft, Marty."
|
||||
targetIsTooFarInTheFuture = "Wir passen den Plan an, sobald wir mehr über die Zukunft wissen."
|
||||
title = "Zielzeit"
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ label = "Max. Current"
|
|||
label = "Min. Current"
|
||||
|
||||
[main.loadpointSettings.minSoc]
|
||||
description = "For emergencies. The vehicle gets „fast” charged to {0}% from all available solar, and then continues with only the solar surplus."
|
||||
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 %"
|
||||
|
||||
[main.loadpointSettings.phasesConfigured]
|
||||
|
|
@ -197,6 +197,7 @@ costLimitIgnore = "The configured {limit} will be ignored during this period."
|
|||
descriptionEnergy = "Until when should {targetEnergy} be loaded into the vehicle?"
|
||||
descriptionSoc = "When should the vehicle be charged to {targetSoc}%?"
|
||||
inactiveLabel = "Target time"
|
||||
onlyInPvMode = "Charging plan only works in solar mode."
|
||||
planDuration = "Charging time"
|
||||
planPeriodLabel = "Period"
|
||||
planPeriodValue = "{start} to {end}"
|
||||
|
|
@ -204,6 +205,7 @@ planUnknown = "not known yet"
|
|||
priceLimit = "price limit of {price}"
|
||||
remove = "Remove"
|
||||
setTargetTime = "none"
|
||||
targetEnergyRequired = "You also need to set an energy limit."
|
||||
targetIsInThePast = "Pick a time in the future, Marty."
|
||||
targetIsTooFarInTheFuture = "We will adjust the plan as soon as we know more about the future."
|
||||
title = "Target Time"
|
||||
|
|
|
|||
|
|
@ -10,11 +10,35 @@ const DB_PATH = "./evcc.db";
|
|||
const BINARY = "./evcc";
|
||||
|
||||
export async function start(config, database) {
|
||||
clean();
|
||||
await _clean();
|
||||
if (database) {
|
||||
console.log("loading database", { database });
|
||||
execSync(`sqlite3 ${DB_PATH} < tests/${database}`);
|
||||
await _restoreDatabase(database);
|
||||
}
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
await _stop();
|
||||
await _clean();
|
||||
}
|
||||
|
||||
export async function restart(config) {
|
||||
await _stop();
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
export async function cleanRestart(config) {
|
||||
await _stop();
|
||||
await _clean();
|
||||
await _start(config);
|
||||
}
|
||||
|
||||
async function _restoreDatabase(database) {
|
||||
console.log("loading database", { database });
|
||||
execSync(`sqlite3 ${DB_PATH} < tests/${database}`);
|
||||
}
|
||||
|
||||
async function _start(config) {
|
||||
console.log("starting evcc", { config });
|
||||
const instance = exec(`EVCC_DATABASE_DSN=${DB_PATH} ${BINARY} --config tests/${config}`);
|
||||
instance.stdout.pipe(process.stdout);
|
||||
|
|
@ -26,13 +50,12 @@ export async function start(config, database) {
|
|||
await waitOn({ resources: [BASE_URL] });
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
async function _stop() {
|
||||
console.log("shutting down evcc");
|
||||
await axios.post(BASE_URL + "/api/shutdown");
|
||||
clean();
|
||||
}
|
||||
|
||||
function clean() {
|
||||
async function _clean() {
|
||||
if (fs.existsSync(DB_PATH)) {
|
||||
console.log("delete database", DB_PATH);
|
||||
fs.unlinkSync(DB_PATH);
|
||||
|
|
|
|||
58
tests/persist-target-minsoc-connected.evcc.yaml
Executable file
58
tests/persist-target-minsoc-connected.evcc.yaml
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
site:
|
||||
title: Hello World
|
||||
meters:
|
||||
grid: grid
|
||||
|
||||
meters:
|
||||
- name: grid
|
||||
type: custom
|
||||
power:
|
||||
source: js
|
||||
script: |
|
||||
1000
|
||||
|
||||
loadpoints:
|
||||
- title: Carport
|
||||
charger: charger
|
||||
mode: pv
|
||||
vehicle: golf
|
||||
|
||||
chargers:
|
||||
- name: charger
|
||||
type: custom
|
||||
enable:
|
||||
source: js
|
||||
script:
|
||||
enabled:
|
||||
source: js
|
||||
script: |
|
||||
false
|
||||
status:
|
||||
source: js
|
||||
script: |
|
||||
"B"
|
||||
maxcurrent:
|
||||
source: js
|
||||
script:
|
||||
|
||||
vehicles:
|
||||
- name: golf
|
||||
title: blauer e-Golf
|
||||
type: custom
|
||||
soc:
|
||||
source: js
|
||||
vm: shared
|
||||
script: |
|
||||
35
|
||||
capacity: 20
|
||||
- name: honda
|
||||
type: template
|
||||
template: offline
|
||||
title: grüner Honda e
|
||||
capacity: 28.5
|
||||
|
||||
tariffs:
|
||||
currency: EUR
|
||||
grid:
|
||||
type: fixed
|
||||
price: 0.20
|
||||
58
tests/persist-target-minsoc-disconnected.evcc.yaml
Executable file
58
tests/persist-target-minsoc-disconnected.evcc.yaml
Executable file
|
|
@ -0,0 +1,58 @@
|
|||
site:
|
||||
title: Hello World
|
||||
meters:
|
||||
grid: grid
|
||||
|
||||
meters:
|
||||
- name: grid
|
||||
type: custom
|
||||
power:
|
||||
source: js
|
||||
script: |
|
||||
1000
|
||||
|
||||
loadpoints:
|
||||
- title: Carport
|
||||
charger: charger
|
||||
mode: pv
|
||||
vehicle: golf
|
||||
|
||||
chargers:
|
||||
- name: charger
|
||||
type: custom
|
||||
enable:
|
||||
source: js
|
||||
script:
|
||||
enabled:
|
||||
source: js
|
||||
script: |
|
||||
false
|
||||
status:
|
||||
source: js
|
||||
script: |
|
||||
"A"
|
||||
maxcurrent:
|
||||
source: js
|
||||
script:
|
||||
|
||||
vehicles:
|
||||
- name: golf
|
||||
title: blauer e-Golf
|
||||
type: custom
|
||||
soc:
|
||||
source: js
|
||||
vm: shared
|
||||
script: |
|
||||
35
|
||||
capacity: 20
|
||||
- name: honda
|
||||
type: template
|
||||
template: offline
|
||||
title: grüner Honda e
|
||||
capacity: 28.5
|
||||
|
||||
tariffs:
|
||||
currency: EUR
|
||||
grid:
|
||||
type: fixed
|
||||
price: 0.20
|
||||
157
tests/persist-target-minsoc.spec.js
Normal file
157
tests/persist-target-minsoc.spec.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
const { start, stop, restart } = require("./evcc");
|
||||
|
||||
const CONFIG_CONNECTED = "persist-target-minsoc-connected.evcc.yaml";
|
||||
const CONFIG_DISCONNECTED = "persist-target-minsoc-disconnected.evcc.yaml";
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("targetSoc", async () => {
|
||||
test("survives a restart", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("100%");
|
||||
await page.getByTestId("target-soc").getByRole("combobox").selectOption("50%");
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("50%");
|
||||
|
||||
await restart(CONFIG_CONNECTED);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("50%");
|
||||
});
|
||||
|
||||
test("can be set even if vehicle isn't connected yet", async ({ page }) => {
|
||||
await start(CONFIG_DISCONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("vehicle-title")).toContainText("blauer e-Golf");
|
||||
await expect(page.getByTestId("vehicle-status")).toHaveText("Disconnected.");
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("100%");
|
||||
await page.getByTestId("target-soc").getByRole("combobox").selectOption("50%");
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("50%");
|
||||
|
||||
await restart(CONFIG_CONNECTED);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("50%");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("targetEnergy", async () => {
|
||||
test("survives a restart", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("button", { name: "blauer e-Golf" }).click();
|
||||
await page.getByRole("button", { name: "grüner Honda e" }).click();
|
||||
|
||||
await expect(page.getByTestId("target-energy-value")).toHaveText("none");
|
||||
await page.getByTestId("target-energy").getByRole("combobox").selectOption("10 kWh (+35%)");
|
||||
await expect(page.getByTestId("target-energy-value")).toHaveText("10 kWh");
|
||||
|
||||
await restart(CONFIG_CONNECTED);
|
||||
await page.reload();
|
||||
|
||||
await page.getByRole("button", { name: "blauer e-Golf" }).click();
|
||||
await page.getByRole("button", { name: "grüner Honda e" }).click();
|
||||
|
||||
await expect(page.getByTestId("target-energy-value")).toHaveText("10 kWh");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("minSoc", async () => {
|
||||
test("survives a restart", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
|
||||
await expect(page.getByText("charged to x% in solar mode")).toBeVisible();
|
||||
await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("20%");
|
||||
await expect(page.getByText("charged to 20% in solar mode")).toBeVisible();
|
||||
|
||||
await restart(CONFIG_CONNECTED);
|
||||
await page.reload();
|
||||
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
await expect(page.getByText("charged to 20% in solar mode")).toBeVisible();
|
||||
});
|
||||
|
||||
test("show minsoc instead of plan when minsoc is active", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("charging-plan")).toContainText("Plan");
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
await page.getByRole("link", { name: "Arrival" }).click();
|
||||
await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("50%");
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
await expect(page.getByTestId("charging-plan")).toContainText("Min charge");
|
||||
await expect(page.getByTestId("vehicle-status")).toHaveText("Minimum charging to 50%.");
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "50%" }).click();
|
||||
|
||||
await page.getByRole("combobox", { name: "Min. charge %" }).selectOption("--");
|
||||
await page.getByRole("button", { name: "Close" }).click();
|
||||
await expect(page.getByTestId("charging-plan")).toContainText("Plan");
|
||||
await expect(page.getByTestId("charging-plan").getByRole("button")).toHaveText("none");
|
||||
});
|
||||
|
||||
test("disabled for offline vehicles", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
// switch to offline vehicle
|
||||
await page.getByRole("button", { name: "blauer e-Golf" }).click();
|
||||
await page.getByRole("button", { name: "grüner Honda e" }).click();
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
test("disabled for guest vehicles", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
// switch to offline vehicle
|
||||
await page.getByRole("button", { name: "blauer e-Golf" }).click();
|
||||
await page.getByRole("button", { name: "Guest vehicle" }).click();
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("targetTime", async () => {
|
||||
test("survives a restart", async ({ page }) => {
|
||||
await start(CONFIG_CONNECTED);
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("target-soc").getByRole("combobox").selectOption("90%");
|
||||
await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
|
||||
|
||||
await page.getByTestId("target-day").selectOption({ index: 1 });
|
||||
await page.getByTestId("target-time").fill("09:30");
|
||||
|
||||
await page.getByRole("button", { name: "Activate" }).click();
|
||||
await expect(page.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 9:30 AM"
|
||||
);
|
||||
|
||||
await restart(CONFIG_CONNECTED);
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("vehicle-status")).toContainText("Target charging starts at");
|
||||
await expect(page.getByTestId("charging-plan").getByRole("button")).toHaveText(
|
||||
"tomorrow 9:30 AM"
|
||||
);
|
||||
await expect(page.getByTestId("target-soc-value")).toHaveText("90%");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue