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 commit dd787ce52f.

* 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 commit 536dbc936c.

* Revert "Add e2e tests with playwright (#8123)"

This reverts commit 0df815729b.

* 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 commit ce0e4277aa.

* 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 commit 0df815729b.

* Revert "Revert "Add e2e tests with playwright (#8123)""

This reverts commit 2299306954.

* 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:
Michael Geers 2023-08-02 15:43:00 +02:00 • committed by GitHub
parent e6b88f7b58
commit 7de484969a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 437 additions and 52 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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>
&nbsp;
</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;
}
}
},

View file

@ -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>

View file

@ -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>

View file

@ -1,5 +1,5 @@
<template>
<div class="d-block evcc-gray">{{ message }}&nbsp;</div>
<div class="d-block evcc-gray" data-testid="vehicle-status">{{ message }}&nbsp;</div>
</template>
<script>

View file

@ -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'"

View file

@ -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)
}

View file

@ -8,4 +8,5 @@ type API interface {
Acquire(api.Vehicle)
Release(api.Vehicle)
IdentifyVehicleByStatus() api.Vehicle
GetVehicleIndex(api.Vehicle) int
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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.

View file

@ -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"

View file

@ -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"

View file

@ -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);

View 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

View 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

View 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%");
});
});