Smart grid charging per loadpoint (BC) (#12135)

This commit is contained in:
Michael Geers 2024-02-24 17:52:15 +01:00 • committed by GitHub
parent 974cc85b6c
commit 93a1faf755
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 615 additions and 598 deletions

View file

@ -460,3 +460,17 @@ input[type="time"]::-webkit-calendar-picker-indicator {
width: calc(1.2rem + 0.75rem);
border-radius: 3rem;
}
.form-switch .form-check-input + .form-check-label {
margin-top: 0.1rem;
padding-left: 0.5rem;
}
@media (min-width: 576px) {
.w-sm-100 {
width: 100% !important;
}
.w-sm-50 {
width: 50% !important;
}
}

View file

@ -3,7 +3,7 @@
<div
id="batterySettingsModal"
ref="modal"
class="modal fade text-dark"
class="modal fade text-dark modal-xl"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
@ -26,7 +26,7 @@
<div
class="d-flex justify-content-between align-items-center align-items-sm-start pb-2 flex-column flex-sm-row-reverse"
>
<div class="battery mb-5 mb-sm-3 me-5 me-sm-0">
<div class="battery mb-5 mb-sm-3 me-5 me-sm-0 w-sm-50">
<div class="batteryLimits">
<label
class="bufferSoc p-2 end-0"
@ -130,7 +130,7 @@
</div>
</div>
</div>
<div class="me-sm-4">
<div class="legend me-sm-4 align-self-start w-sm-50">
<p>
{{ $t("batterySettings.batteryLevel") }}:
<strong>{{ fmtSoc(batterySoc) }}</strong>
@ -212,12 +212,8 @@
</p>
</div>
</div>
<FormRow
v-if="controllable"
id="batteryDischargeControl"
:label="`${$t('batterySettings.control')}`"
>
<div class="form-check form-switch col-form-label">
<div v-if="!controllable">
<div class="form-check form-switch">
<input
id="batteryDischargeControl"
:checked="batteryDischargeControl"
@ -232,7 +228,7 @@
</label>
</div>
</div>
</FormRow>
</div>
</div>
</div>
</div>
@ -245,14 +241,12 @@ import "@h2d2/shopicons/es/regular/lightning";
import "@h2d2/shopicons/es/regular/car3";
import "@h2d2/shopicons/es/regular/home";
import formatter from "../mixins/formatter";
import FormRow from "./FormRow.vue";
import collector from "../mixins/collector";
import api from "../api";
export default {
name: "BatterySettingsModal",
components: { FormRow },
mixins: [formatter],
mixins: [formatter, collector],
props: {
bufferSoc: Number,
prioritySoc: Number,
@ -460,6 +454,11 @@ export default {
height: 300px;
display: flex;
}
.legend {
min-width: 260px;
}
.batteryLimits {
width: 50px;
position: relative;
@ -521,11 +520,13 @@ export default {
border-radius: 0.5rem 0 0 0.5rem;
}
.progress {
flex: 1;
height: 100%;
width: 100px;
min-width: 100px;
max-width: 150px;
flex-direction: column;
position: relative;
border-radius: 10px;
border-radius: 1rem;
background-color: var(--evcc-box) !important;
}
.progress-bar {

View file

@ -89,10 +89,8 @@
:powerInKw="powerInKw"
:details="detailsValue(tariffGrid, tariffCo2)"
:detailsFmt="detailsFmt"
:detailsClickable="gridModalAvailable"
:detailsTooltip="detailsTooltip(tariffGrid, tariffCo2)"
data-testid="energyflow-entry-gridimport"
@details-clicked="openGridSettingsModal"
/>
</div>
</div>
@ -173,7 +171,6 @@ import AnimatedNumber from "../AnimatedNumber.vue";
import settings from "../../settings";
import { CO2_TYPE } from "../../units";
import collector from "../../mixins/collector";
import gridModalAvailable from "../../utils/gridModalAvailable";
export default {
name: "Energyflow",
@ -204,7 +201,6 @@ export default {
tariffCo2Home: { type: Number },
tariffPriceLoadpoints: { type: Number },
tariffCo2Loadpoints: { type: Number },
smartCostLimit: { type: Number },
smartCostType: { type: String },
currency: { type: String },
prioritySoc: { type: Number },
@ -212,12 +208,9 @@ export default {
bufferStartSoc: { type: Number },
},
data: () => {
return { detailsOpen: false, detailsCompleteHeight: null, gridSettingsModal: null };
return { detailsOpen: false, detailsCompleteHeight: null };
},
computed: {
gridModalAvailable: function () {
return gridModalAvailable(this.smartCostType);
},
gridImport: function () {
return Math.max(0, this.gridPower);
},
@ -334,10 +327,6 @@ export default {
updateHeight: function () {
this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
},
openGridSettingsModal() {
const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
modal.show();
},
openBatterySettingsModal() {
const modal = Modal.getOrCreateInstance(
document.getElementById("batterySettingsModal")

View file

@ -188,6 +188,7 @@ export default {
tariffGrid: Number,
tariffCo2: Number,
currency: String,
multipleLoadpoints: Boolean,
},
data() {
return {

View file

@ -2,11 +2,13 @@
<Teleport to="body">
<div
:id="`loadpointSettingsModal_${id}`"
class="modal fade text-dark"
class="modal fade text-dark modal-xl"
ref="modal"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
aria-hidden="true"
data-testid="loadpoint-settings-modal"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
@ -23,7 +25,12 @@
</div>
<div class="modal-body">
<div class="container">
<h4 class="d-flex align-items-center mb-3 mt-4 text-evcc">
<SmartCostLimit
v-if="isModalVisible && smartCostAvailable"
v-bind="smartCostLimitProps"
class="mt-2"
/>
<h4 class="d-flex align-items-center mb-3 mt-5 text-evcc">
{{ $t("main.loadpointSettings.currents") }}
</h4>
<div v-if="phasesOptions.length" class="mb-3 row">
@ -142,7 +149,10 @@
</template>
<script>
import collector from "../mixins/collector";
import formatter from "../mixins/formatter";
import SmartCostLimit from "./SmartCostLimit.vue";
import smartCostAvailable from "../utils/smartCostAvailable";
const V = 230;
@ -161,7 +171,8 @@ const insertSorted = (arr, num) => {
export default {
name: "LoadpointSettingsModal",
mixins: [formatter],
mixins: [formatter, collector],
components: { SmartCostLimit },
props: {
id: [String, Number],
phasesConfigured: Number,
@ -172,6 +183,11 @@ export default {
maxCurrent: Number,
minCurrent: Number,
title: String,
smartCostLimit: Number,
smartCostType: String,
tariffGrid: Number,
currency: String,
multipleLoadpoints: Boolean,
},
emits: ["phasesconfigured-updated", "maxcurrent-updated", "mincurrent-updated"],
data: function () {
@ -179,6 +195,7 @@ export default {
selectedMaxCurrent: this.maxCurrent,
selectedMinCurrent: this.minCurrent,
selectedPhases: this.phasesConfigured,
isModalVisible: false,
};
},
computed: {
@ -228,6 +245,15 @@ export default {
const opt2 = insertSorted(opt1, this.maxCurrent);
return opt2.map((value) => this.currentOption(value, value === 16));
},
smartCostLimitProps: function () {
return this.collectProps(SmartCostLimit);
},
loadpointId: function () {
return this.id;
},
smartCostAvailable() {
return smartCostAvailable(this.smartCostType);
},
},
watch: {
maxCurrent: function (value) {
@ -243,6 +269,14 @@ export default {
this.selectedMinSoc = value;
},
},
mounted() {
this.$refs.modal.addEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal.addEventListener("hidden.bs.modal", this.modalInvisible);
},
unmounted() {
this.$refs.modal?.removeEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal?.removeEventListener("hidden.bs.modal", this.modalInvisible);
},
methods: {
maxPowerPhases: function (phases) {
return this.fmtKw(this.maxCurrent * V * phases);
@ -269,6 +303,12 @@ export default {
}
return { value, name };
},
modalVisible: function () {
this.isModalVisible = true;
},
modalInvisible: function () {
this.isModalVisible = false;
},
},
};
</script>

View file

@ -13,12 +13,11 @@
:id="index + 1"
data-testid="loadpoint"
:vehicles="vehicles"
:smartCostLimit="smartCostLimit"
:smartCostType="smartCostType"
:smartCostActive="smartCostActive"
:tariffGrid="tariffGrid"
:tariffCo2="tariffCo2"
:currency="currency"
:multiple-loadpoints="loadpoints.length > 1"
class="h-100"
:class="{ 'loadpoint-unselected': !selected(index) }"
@click="scrollTo(index)"

View file

@ -70,6 +70,7 @@ export default {
batteryPower: Number,
batterySoc: Number,
batteryDischargeControl: Boolean,
batterySmartCostLimit: Number,
batteryMode: String,
battery: Array,
gridCurrents: Array,

View file

@ -1,104 +1,77 @@
<template>
<Teleport to="body">
<div
id="gridSettingsModal"
ref="modal"
class="modal fade text-dark modal-xl"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{ $t("gridSettings.modalTitle") }}
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="smartCostLimit mb-4">
<div class="d-flex justify-content-between align-items-baseline mb-2">
<label for="smartCostLimit" class="pt-1">
{{
isCo2
? $t("gridSettings.co2Limit")
: $t("gridSettings.priceLimit")
}}
</label>
<select
id="smartCostLimit"
v-model.number="selectedSmartCostLimit"
class="form-select smartCostLimitSelect"
@change="changeSmartCostLimit"
>
<option value="0">{{ $t("gridSettings.none") }}</option>
<option
v-for="{ value, name } in costOptions"
:key="value"
:value="value"
>
{{ name }}
</option>
</select>
</div>
<small v-if="selectedSmartCostLimit !== 0">
{{ $t("gridSettings.costLimitDescription") }}
</small>
</div>
<div class="justify-content-between mb-2 d-flex justify-content-between">
<div class="text-start">
<div class="label">
{{ $t("gridSettings.activeHoursLabel") }}
</div>
<div
class="value"
:class="
chargingSlots.length ? 'text-primary' : 'value-inactive'
"
>
{{
$t("gridSettings.activeHours", {
total: totalSlots.length,
charging: chargingSlots.length,
})
}}
</div>
</div>
<div class="text-end">
<div class="label">
<span v-if="activeSlot">{{ activeSlotName }}</span>
<span v-else-if="isCo2">{{ $t("gridSettings.co2Label") }}</span>
<span v-else>{{ $t("gridSettings.priceLabel") }}</span>
</div>
<div v-if="activeSlot" class="value text-primary">
{{ activeSlotCost }}
</div>
<div v-else-if="chargingSlots.length" class="value text-primary">
{{ chargingCostRange }}
</div>
<div v-else class="value value-inactive">
{{ totalCostRange }}
</div>
</div>
</div>
<TariffChart
v-if="tariff"
:slots="slots"
@slot-hovered="slotHovered"
@slot-selected="slotSelected"
/>
</div>
<div>
<h4 class="text-evcc mb-3">{{ title }}</h4>
<p>
{{ description }}
</p>
<div class="row mb-3">
<label :for="formId" class="col-sm-4 col-form-label pt-0 pt-sm-2">
{{ isCo2 ? $t("smartCost.co2Limit") : $t("smartCost.priceLimit") }}
</label>
<div class="col-sm-8 col-lg-4 pe-0">
<select
:id="formId"
v-model.number="selectedSmartCostLimit"
class="form-select form-select-sm mb-1"
@change="changeSmartCostLimit"
>
<option value="0">{{ $t("smartCost.none") }}</option>
<option v-for="{ value, name } in costOptions" :key="value" :value="value">
{{ name }}
</option>
</select>
</div>
<div
v-if="applyToAllVisible"
class="col-sm-8 offset-sm-4 offset-lg-0 col-lg-4 d-flex align-items-baseline"
>
<div class="text-primary">{{ $t("smartCost.saved") }}</div>
<button class="ms-1 btn btn-sm btn-link text-muted p-0" @click="applyToAll">
{{ $t("smartCost.applyToAll") }}
</button>
</div>
</div>
<div class="justify-content-between mb-2 d-flex justify-content-between">
<div class="text-start">
<div class="label">
{{ $t("smartCost.activeHoursLabel") }}
</div>
<div
class="value"
:class="chargingSlots.length ? 'text-primary' : 'value-inactive'"
>
{{
$t("smartCost.activeHours", {
total: totalSlots.length,
charging: chargingSlots.length,
})
}}
</div>
</div>
<div class="text-end">
<div class="label">
<span v-if="activeSlot">{{ activeSlotName }}</span>
<span v-else-if="isCo2">{{ $t("smartCost.co2Label") }}</span>
<span v-else>{{ $t("smartCost.priceLabel") }}</span>
</div>
<div v-if="activeSlot" class="value text-primary">
{{ activeSlotCost }}
</div>
<div v-else-if="chargingSlots.length" class="value text-primary">
{{ chargingCostRange }}
</div>
<div v-else class="value value-inactive">
{{ totalCostRange }}
</div>
</div>
</div>
</Teleport>
<TariffChart
v-if="tariff"
:slots="slots"
@slot-hovered="slotHovered"
@slot-selected="slotSelected"
/>
</div>
</template>
<script>
@ -108,22 +81,24 @@ import { CO2_TYPE } from "../units";
import api from "../api";
export default {
name: "GridSettingsModal",
name: "SmartCostLimit",
components: { TariffChart },
mixins: [formatter],
props: {
smartCostLimit: Number,
smartCostLimit: { type: Number, default: 0 },
smartCostType: String,
tariffGrid: Number,
currency: String,
loadpointId: Number,
multipleLoadpoints: Boolean,
},
data: function () {
return {
isModalVisible: false,
selectedSmartCostLimit: 0,
tariff: null,
startTime: null,
activeIndex: null,
applyToAllVisible: false,
};
},
computed: {
@ -235,30 +210,27 @@ export default {
}
return this.fmtPricePerKWh(price, this.currency);
},
title() {
return this.$t(`smartCost.${this.isCo2 ? "clean" : "cheap"}Title`);
},
description() {
return this.$t(`smartCost.${this.loadpointId ? "loadpoint" : "battery"}Description`);
},
formId() {
return `smartCostLimit-${this.loadpointId || "battery"}`;
},
},
watch: {
isModalVisible(visible) {
if (visible) {
this.updateTariff();
}
},
tariffGrid() {
if (this.isModalVisible) {
this.updateTariff();
}
this.updateTariff();
},
smartCostLimit(limit) {
this.selectedSmartCostLimit = limit;
},
},
mounted() {
this.updateTariff();
this.selectedSmartCostLimit = this.smartCostLimit;
this.$refs.modal.addEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal.addEventListener("hidden.bs.modal", this.modalInvisible);
},
unmounted() {
this.$refs.modal?.removeEventListener("show.bs.modal", this.modalVisible);
this.$refs.modal?.removeEventListener("hidden.bs.modal", this.modalInvisible);
},
methods: {
updateTariff: async function () {
@ -269,12 +241,6 @@ export default {
console.error(e);
}
},
modalVisible: function () {
this.isModalVisible = true;
},
modalInvisible: function () {
this.isModalVisible = false;
},
convertDates(list) {
if (!list?.length) {
return [];
@ -337,8 +303,23 @@ export default {
this.saveSmartCostLimit($event.target.value);
},
async saveSmartCostLimit(limit) {
const isLoadpoint = !!this.loadpointId;
const url = isLoadpoint
? `loadpoints/${this.loadpointId}/smartcostlimit`
: "batterysmartcostlimit"; // currently not implemented
try {
await api.post(`smartcostlimit/${encodeURIComponent(limit)}`);
await api.post(`${url}/${encodeURIComponent(limit)}`);
if (isLoadpoint && this.multipleLoadpoints) {
this.applyToAllVisible = true;
}
} catch (err) {
console.error(err);
}
},
async applyToAll() {
try {
await api.post(`smartcostlimit/${encodeURIComponent(this.selectedSmartCostLimit)}`);
this.applyToAllVisible = false;
} catch (err) {
console.error(err);
}
@ -348,12 +329,6 @@ export default {
</script>
<style scoped>
.smartCostLimit {
max-width: 400px;
}
.smartCostLimitSelect {
width: auto;
}
.value {
font-size: 18px;
font-weight: bold;

View file

@ -113,7 +113,6 @@ import "@h2d2/shopicons/es/regular/moonstars";
import "@h2d2/shopicons/es/regular/menu";
import "@h2d2/shopicons/es/regular/newtab";
import collector from "../mixins/collector";
import gridModalAvailable from "../utils/gridModalAvailable";
import baseAPI from "../baseapi";
@ -130,7 +129,6 @@ export default {
sponsor: String,
sponsorTokenExpires: Number,
batteryConfigured: Boolean,
smartCostType: String,
},
computed: {
logoutCount() {
@ -153,9 +151,6 @@ export default {
batteryModalAvailable() {
return this.batteryConfigured;
},
gridModalAvailable: function () {
return gridModalAvailable(this.smartCostType);
},
},
mounted() {
const $el = document.getElementById("topNavigatonDropdown");
@ -191,10 +186,6 @@ export default {
);
modal.show();
},
openGridSettingsModal() {
const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
modal.show();
},
},
};
</script>

View file

@ -4,7 +4,6 @@
<GlobalSettingsModal v-bind="globalSettingsProps" />
<BatterySettingsModal v-if="batteryModalAvailabe" v-bind="batterySettingsProps" />
<GridSettingsModal v-if="gridModalAvailable" v-bind="gridSettingsProps" />
<HelpModal />
</div>
</template>
@ -13,14 +12,12 @@
import store from "../store";
import GlobalSettingsModal from "../components/GlobalSettingsModal.vue";
import BatterySettingsModal from "../components/BatterySettingsModal.vue";
import GridSettingsModal from "../components/GridSettingsModal.vue";
import HelpModal from "../components/HelpModal.vue";
import collector from "../mixins/collector";
import gridModalAvailable from "../utils/gridModalAvailable";
export default {
name: "App",
components: { GlobalSettingsModal, HelpModal, BatterySettingsModal, GridSettingsModal },
components: { GlobalSettingsModal, HelpModal, BatterySettingsModal },
mixins: [collector],
props: {
notifications: Array,
@ -34,9 +31,6 @@ export default {
return { title: siteTitle ? `${siteTitle} | evcc` : "evcc" };
},
computed: {
gridModalAvailable: function () {
return gridModalAvailable(store.state.smartCostType);
},
batteryModalAvailabe: function () {
return store.state.batteryConfigured;
},
@ -46,9 +40,6 @@ export default {
batterySettingsProps() {
return this.collectProps(BatterySettingsModal, store.state);
},
gridSettingsProps() {
return this.collectProps(GridSettingsModal, store.state);
},
},
mounted: function () {
this.connect();

View file

@ -27,6 +27,10 @@ const (
Connected = "connected" // connected
Charging = "charging" // charging
// smart charging
SmartCostActive = "smartCostActive" // smart cost active
SmartCostLimit = "smartCostLimit" // smart cost limit
// effective values
EffectivePriority = "effectivePriority" // effective priority
EffectivePlanTime = "effectivePlanTime" // effective plan time

View file

@ -18,8 +18,6 @@ const (
PvPower = "pvPower"
ResidualPower = "residualPower"
SiteTitle = "siteTitle"
SmartCostActive = "smartCostActive"
SmartCostLimit = "smartCostLimit"
SmartCostType = "smartCostType"
Statistics = "statistics"
TariffCo2 = "tariffCo2"

View file

@ -124,6 +124,7 @@ type Loadpoint struct {
configuredPhases int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit float64 // always charge if cost is below this value
mode api.ChargeMode
enabled bool // Charger enabled state
@ -324,7 +325,9 @@ func (lp *Loadpoint) restoreSettings() {
if v, err := lp.settings.Float(keys.LimitEnergy); err == nil && v > 0 {
lp.setLimitEnergy(v)
}
if v, err := lp.settings.Float(keys.SmartCostLimit); err == nil {
lp.SetSmartCostLimit(v)
}
t, err1 := lp.settings.Time(keys.PlanTime)
v, err2 := lp.settings.Float(keys.PlanEnergy)
if err1 == nil && err2 == nil {
@ -1495,6 +1498,7 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
// Update is the main control function. It reevaluates meters and charger state
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
lp.publish(keys.SmartCostActive, autoCharge)
lp.processTasks()
// read and publish meters first- charge power has already been updated by the site

View file

@ -105,6 +105,15 @@ type API interface {
// RemoteControl sets remote status demand
RemoteControl(string, RemoteDemand)
//
// smart grid charging
//
// GetSmartChargingActive determines if smart charging is active
GetSmartCostLimit() float64
// SetSmartCostLimit sets the smart cost limit
SetSmartCostLimit(limit float64)
//
// power and energy
//

View file

@ -351,6 +351,20 @@ func (mr *MockAPIMockRecorder) GetRemainingEnergy() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemainingEnergy", reflect.TypeOf((*MockAPI)(nil).GetRemainingEnergy))
}
// GetSmartCostLimit mocks base method.
func (m *MockAPI) GetSmartCostLimit() float64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSmartCostLimit")
ret0, _ := ret[0].(float64)
return ret0
}
// GetSmartCostLimit indicates an expected call of GetSmartCostLimit.
func (mr *MockAPIMockRecorder) GetSmartCostLimit() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).GetSmartCostLimit))
}
// GetStatus mocks base method.
func (m *MockAPI) GetStatus() api.ChargeStatus {
m.ctrl.T.Helper()
@ -559,6 +573,18 @@ func (mr *MockAPIMockRecorder) SetPriority(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPriority", reflect.TypeOf((*MockAPI)(nil).SetPriority), arg0)
}
// SetSmartCostLimit mocks base method.
func (m *MockAPI) SetSmartCostLimit(arg0 float64) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetSmartCostLimit", arg0)
}
// SetSmartCostLimit indicates an expected call of SetSmartCostLimit.
func (mr *MockAPIMockRecorder) SetSmartCostLimit(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSmartCostLimit", reflect.TypeOf((*MockAPI)(nil).SetSmartCostLimit), arg0)
}
// SetVehicle mocks base method.
func (m *MockAPI) SetVehicle(arg0 api.Vehicle) {
m.ctrl.T.Helper()

View file

@ -477,3 +477,24 @@ func (lp *Loadpoint) StartVehicleDetection() {
// start auto-detect
lp.startVehicleDetection()
}
// GetSmartCostLimit gets the smart cost limit
func (lp *Loadpoint) GetSmartCostLimit() float64 {
lp.RLock()
defer lp.RUnlock()
return lp.smartCostLimit
}
// SetSmartCostLimit sets the smart cost limit
func (lp *Loadpoint) SetSmartCostLimit(val float64) {
lp.Lock()
defer lp.Unlock()
lp.log.DEBUG.Println("set smart cost limit:", val)
if lp.smartCostLimit != val {
lp.smartCostLimit = val
lp.settings.SetFloat(keys.SmartCostLimit, lp.smartCostLimit)
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)
}
}

View file

@ -33,8 +33,8 @@ import (
const standbyPower = 10 // consider less than 10W as charger in standby
// Updater abstracts the Loadpoint implementation for testing
type Updater interface {
// updater abstracts the Loadpoint implementation for testing
type updater interface {
loadpoint.API
Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
}
@ -77,9 +77,6 @@ type Site struct {
batteryMeters []api.Meter // Battery charging meters
auxMeters []api.Meter // Auxiliary meters
// cost settings
smartCostLimit float64 // always charge if cost is below this value
// battery settings
prioritySoc float64 // prefer battery up to this Soc
bufferSoc float64 // continue charging on battery above this Soc
@ -270,11 +267,6 @@ func (site *Site) restoreSettings() error {
return err
}
}
if v, err := settings.Float(keys.SmartCostLimit); err == nil {
if err := site.SetSmartCostLimit(v); err != nil {
return err
}
}
if v, err := settings.Float(keys.PrioritySoc); err == nil {
if err := site.SetPrioritySoc(v); err != nil {
return err
@ -771,7 +763,7 @@ func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints fl
}
}
func (site *Site) update(lp Updater) {
func (site *Site) update(lp updater) {
site.log.DEBUG.Println("----")
// update all loadpoint's charge power
@ -799,9 +791,8 @@ func (site *Site) update(lp Updater) {
}
if err == nil {
limit := site.GetSmartCostLimit()
limit := lp.GetSmartCostLimit()
smartCostActive = limit != 0 && rate.Price <= limit
site.publish(keys.SmartCostActive, smartCostActive)
} else {
site.log.ERROR.Println("smartCost:", err)
}
@ -862,8 +853,6 @@ func (site *Site) prepare() {
site.publish(keys.ResidualPower, site.ResidualPower)
site.publish(keys.Currency, site.tariffs.Currency)
site.publish(keys.SmartCostActive, false)
site.publish(keys.SmartCostLimit, site.smartCostLimit)
if tariff := site.GetTariff(PlannerTariff); tariff != nil {
site.publish(keys.SmartCostType, tariff.Type())
} else {
@ -919,7 +908,7 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
}
// loopLoadpoints keeps iterating across loadpoints sending the next to the given channel
func (site *Site) loopLoadpoints(next chan<- Updater) {
func (site *Site) loopLoadpoints(next chan<- updater) {
for {
for _, lp := range site.loadpoints {
next <- lp
@ -936,7 +925,7 @@ func (site *Site) Run(stopC chan struct{}, interval time.Duration) {
site.log.WARN.Printf("interval <%.0fs can lead to unexpected behavior, see https://docs.evcc.io/docs/reference/configuration/interval", max.Seconds())
}
loadpointChan := make(chan Updater)
loadpointChan := make(chan updater)
go site.loopLoadpoints(loadpointChan)
ticker := time.NewTicker(interval)

View file

@ -47,8 +47,6 @@ type API interface {
// GetTariff returns the respective tariff
GetTariff(string) api.Tariff
GetSmartCostLimit() float64
SetSmartCostLimit(float64) error
//
// battery control

View file

@ -255,29 +255,6 @@ func (site *Site) SetResidualPower(power float64) error {
return nil
}
// GetSmartCostLimit returns the smartCostLimit
func (site *Site) GetSmartCostLimit() float64 {
site.RLock()
defer site.RUnlock()
return site.smartCostLimit
}
// SetSmartCostLimit sets the smartCostLimit
func (site *Site) SetSmartCostLimit(val float64) error {
site.Lock()
defer site.Unlock()
site.log.DEBUG.Println("set smart cost limit:", val)
if site.smartCostLimit != val {
site.smartCostLimit = val
settings.SetFloat(keys.SmartCostLimit, site.smartCostLimit)
site.publish(keys.SmartCostLimit, site.smartCostLimit)
}
return nil
}
// GetTariff returns the respective tariff if configured or nil
func (site *Site) GetTariff(tariff string) api.Tariff {
site.RLock()

View file

@ -108,5 +108,5 @@ modalUpdateNow = "Инсталирай сега"
modalUpdateStarted = "Стартирай новата версия на evcc..."
modalUpdateStatusStart = "Инсталацията започна:"
[gridSettings]
[smartCost]
activeHours = "{charging} от {total}"

View file

@ -75,17 +75,6 @@ modalUpdateNow = "Instal·lar ara"
modalUpdateStarted = "Després de l'actualització, evcc es reiniciarà."
modalUpdateStatusStart = "Instal·lació iniciada:"
[gridSettings]
activeHours = "{charging} de {total}"
activeHoursLabel = "Hores funcionant"
co2Label = "Emissions de CO₂"
co2Limit = "Límit de CO₂"
costLimitDescription = "Habilita temporalment la càrrega ràpida en mode solar"
modalTitle = "Xarxa intel·ligent de càrrega"
none = "cap"
priceLabel = "Preu de l'energia"
priceLimit = "Límit de preu"
[header]
about = "Sobre"
blog = "Blog"
@ -327,6 +316,17 @@ km = "km"
label = "Unitats"
mi = "milles"
[smartCost]
activeHours = "{charging} de {total}"
activeHoursLabel = "Hores funcionant"
co2Label = "Emissions de CO₂"
co2Limit = "Límit de CO₂"
loadpointDescription = "Habilita temporalment la càrrega ràpida en mode solar"
modalTitle = "Xarxa intel·ligent de càrrega"
none = "cap"
priceLabel = "Preu de l'energia"
priceLimit = "Límit de preu"
[startupError]
configFile = "Arxiu de configuració utilitzat:"
configuration = "Configuració"

View file

@ -100,17 +100,6 @@ modalUpdateNow = "Instalovat nyní"
modalUpdateStarted = "Spouštím novou verzi evcc..."
modalUpdateStatusStart = "Instalace spuštěna:"
[gridSettings]
activeHours = "{charging} z {total}"
activeHoursLabel = "Aktivně hodin"
co2Label = "CO₂ emise"
co2Limit = "CO₂ limit"
costLimitDescription = "Umožňuje dočasné rychlé dobíjení v solárním režimu"
modalTitle = "Smart Grid Nabíjení"
none = "žádné"
priceLabel = "Cena energie"
priceLimit = "Cenový strop"
[header]
about = "O"
blog = "Blog"
@ -188,3 +177,14 @@ label = "Min. Proud"
[main.loadpointSettings.minSoc]
description = "Vozidlo se “rychle” nabije na {0} % v solárním režimu. Poté pokračuje solárním přebytkem. Užitečné pro zajištění minimálního dojezdu i pro temnější dny."
[smartCost]
activeHours = "{charging} z {total}"
activeHoursLabel = "Aktivně hodin"
co2Label = "CO₂ emise"
co2Limit = "CO₂ limit"
loadpointDescription = "Umožňuje dočasné rychlé dobíjení v solárním režimu"
modalTitle = "Smart Grid Nabíjení"
none = "žádné"
priceLabel = "Cena energie"
priceLimit = "Cenový strop"

View file

@ -166,17 +166,6 @@ modalUpdateNow = "Installer nu"
modalUpdateStarted = "Starter den nye version af evcc..."
modalUpdateStatusStart = "Installation startede:"
[gridSettings]
activeHours = "{charging} af {total}"
activeHoursLabel = "Aktive timer"
co2Label = "CO₂ udledning"
co2Limit = "CO₂ grænse"
costLimitDescription = "Aktiverer midlertidig hurtig ladning i solcelle tilstand"
modalTitle = "Smart net opladning"
none = "ingen"
priceLabel = "Energi pris"
priceLimit = "Pris grænse"
[header]
about = "Om"
blog = "Blog"
@ -467,6 +456,17 @@ km = "km"
label = "Enheder"
mi = "mil"
[smartCost]
activeHours = "{charging} af {total}"
activeHoursLabel = "Aktive timer"
co2Label = "CO₂ udledning"
co2Limit = "CO₂ grænse"
loadpointDescription = "Aktiverer midlertidig hurtig ladning i solcelle tilstand"
modalTitle = "Smart net opladning"
none = "ingen"
priceLabel = "Energi pris"
priceLimit = "Pris grænse"
[startupError]
configFile = "Konfigurationsfil brugt:"
configuration = "Opsætning"

View file

@ -164,17 +164,6 @@ modalUpdateNow = "Jetzt installieren"
modalUpdateStarted = "Starte die neue Version von evcc ..."
modalUpdateStatusStart = "Installation gestartet:"
[gridSettings]
activeHours = "{charging} von {total}"
activeHoursLabel = "Aktive Stunden"
co2Label = "CO₂-Emission"
co2Limit = "CO₂-Grenze"
costLimitDescription = "Aktiviert vorübergehendes Schnellladen im PV-Modus."
modalTitle = "Smartes Netzladen"
none = "keine"
priceLabel = "Energiepreis"
priceLimit = "Preisgrenze"
[header]
about = "Über"
blog = "Blog"
@ -460,6 +449,22 @@ km = "Kilometer"
label = "Einheit"
mi = "Meilen"
[smartCost]
activeHours = "{charging} von {total}"
activeHoursLabel = "Aktive Stunden"
applyToAll = "Überall anwenden?"
batteryDescription = "Lädt die Hausbatterie aus dem Netz."
cheapTitle = "Günstiges Netzladen"
cleanTitle = "Sauberes Netzladen"
co2Label = "CO₂-Emission"
co2Limit = "CO₂-Grenze"
loadpointDescription = "Aktiviert vorübergehendes Schnellladen im PV-Modus."
modalTitle = "Smartes Netzladen"
none = "keine"
priceLabel = "Energiepreis"
priceLimit = "Preisgrenze"
saved = "Gepeichert."
[startupError]
configFile = "Verwendete Konfigurationsdatei:"
configuration = "Konfiguration"

View file

@ -161,17 +161,6 @@ modalUpdateNow = "Install now"
modalUpdateStarted = "Starting the new version of evcc…"
modalUpdateStatusStart = "Installation started:"
[gridSettings]
activeHours = "{charging} of {total}"
activeHoursLabel = "Active hours"
co2Label = "CO₂ emission"
co2Limit = "CO₂ limit"
costLimitDescription = "Enables temporary fast-charging in solar mode."
modalTitle = "Smart Grid Charging"
none = "none"
priceLabel = "Energy price"
priceLimit = "Price limit"
[header]
about = "About"
blog = "Blog"
@ -251,10 +240,12 @@ remoteDisabledSoft = "{source}: turned off adaptive solar-charging"
solar = "Solar"
[main.loadpointSettings]
currents = "Charging current"
currents = "Charging Current"
default = "default"
disclaimerHint = "Note:"
onlyForSocBasedCharging = "These options are only available for vehicles with known charging level."
smartCostCheap = "Cheap Grid Charging"
smartCostClean = "Clean Grid Charging"
title = "Settings {0}"
vehicle = "Vehicle"
@ -457,6 +448,22 @@ km = "km"
label = "Units"
mi = "miles"
[smartCost]
activeHours = "{charging} of {total}"
activeHoursLabel = "Active hours"
applyToAll = "Apply everywhere?"
batteryDescription = "Charges the home battery with energy from the grid."
cheapTitle = "Cheap Grid Charging"
cleanTitle = "Clean Grid Charging"
co2Label = "CO₂ emission"
co2Limit = "CO₂ limit"
loadpointDescription = "Enables temporary fast-charging in solar mode."
modalTitle = "Smart Grid Charging"
none = "none"
priceLabel = "Energy price"
priceLimit = "Price limit"
saved = "Saved."
[startupError]
configFile = "Configuration file used:"
configuration = "Config"

View file

@ -166,17 +166,6 @@ modalUpdateNow = "Instalar ahora"
modalUpdateStarted = "Después de la actualización, evcc se reiniciará."
modalUpdateStatusStart = "Instalación iniciada:"
[gridSettings]
activeHours = "{charging} de {total}"
activeHoursLabel = "Horas funcionando"
co2Label = "Emisiones de CO₂"
co2Limit = "Límite de CO₂"
costLimitDescription = "Habilita temporalmente la carga rápida en modo solar"
modalTitle = "Red inteligente de carga"
none = "ninguno"
priceLabel = "Precio de la energía"
priceLimit = "Límite de precio"
[header]
about = "Sobre"
blog = "Blog"
@ -478,6 +467,17 @@ km = "km"
label = "Unidades"
mi = "millas"
[smartCost]
activeHours = "{charging} de {total}"
activeHoursLabel = "Horas funcionando"
co2Label = "Emisiones de CO₂"
co2Limit = "Límite de CO₂"
loadpointDescription = "Habilita temporalmente la carga rápida en modo solar"
modalTitle = "Red inteligente de carga"
none = "ninguno"
priceLabel = "Precio de la energía"
priceLimit = "Límite de precio"
[startupError]
configFile = "Archivo de configuración utilizado:"
configuration = "Configuración"

View file

@ -120,17 +120,6 @@ modalUpdateNow = "Asenna nyt"
modalUpdateStarted = "Käynnistetään evcc:n uusin versio..."
modalUpdateStatusStart = "Asennus aloitettu:"
[gridSettings]
activeHours = "{charging} / {total}"
activeHoursLabel = "Aktiiviset tunnit"
co2Label = "CO₂ päästö"
co2Limit = "CO₂ raja"
costLimitDescription = "Mahdollistaa väliaikaisesti latauksen nopeasti PV tilassa."
modalTitle = "Älykäs lataus verkosta"
none = "ei mitään"
priceLabel = "Energian hinta"
priceLimit = "Hintaraja"
[header]
about = "Tietoa"
blog = "Blogi"
@ -396,6 +385,17 @@ km = "km"
label = "Yksiköt"
mi = "mailit"
[smartCost]
activeHours = "{charging} / {total}"
activeHoursLabel = "Aktiiviset tunnit"
co2Label = "CO₂ päästö"
co2Limit = "CO₂ raja"
loadpointDescription = "Mahdollistaa väliaikaisesti latauksen nopeasti PV tilassa."
modalTitle = "Älykäs lataus verkosta"
none = "ei mitään"
priceLabel = "Energian hinta"
priceLimit = "Hintaraja"
[startupError]
configFile = "Käytetty määritystiedostoa:"
configuration = "Määritys"

View file

@ -122,17 +122,6 @@ modalUpdateNow = "Installer maintenant"
modalUpdateStarted = "Démarrage de la nouvelle version de evcc..."
modalUpdateStatusStart = "L'installation a commencé:"
[gridSettings]
activeHours = "{charging} sur {total}"
activeHoursLabel = "Heures actives"
co2Label = "Émissions de CO₂"
co2Limit = "Limite CO₂"
costLimitDescription = "Autorise la charge rapide temporaire en mode solaire."
modalTitle = "Recharge réseau intelligente"
none = "aucune"
priceLabel = "Prix de l'énergie"
priceLimit = "Limite de prix"
[header]
about = "À propos"
blog = "Blog"
@ -418,6 +407,17 @@ km = "km"
label = "Unités"
mi = "miles"
[smartCost]
activeHours = "{charging} sur {total}"
activeHoursLabel = "Heures actives"
co2Label = "Émissions de CO₂"
co2Limit = "Limite CO₂"
loadpointDescription = "Autorise la charge rapide temporaire en mode solaire."
modalTitle = "Recharge réseau intelligente"
none = "aucune"
priceLabel = "Prix de l'énergie"
priceLimit = "Limite de prix"
[startupError]
configFile = "Fichier de configuration utilisé:"
configuration = "Configuration"

View file

@ -123,17 +123,6 @@ modalUpdateNow = "Instaliraj sada"
modalUpdateStarted = "Pokretanje nove evcc verzije …"
modalUpdateStatusStart = "Instalacija je pokrenuta:"
[gridSettings]
activeHours = "{charging} od {total}"
activeHoursLabel = "Aktivno radno vrijeme"
co2Label = "Co₂ emisija"
co2Limit = "Ograničenje CO₂"
costLimitDescription = "Omogućuje privremeno brzo punjenje u solarnom modusu."
modalTitle = "Smart Grid napajanje"
none = "bez"
priceLabel = "Cijena energije"
priceLimit = "Ograničenje cijene"
[header]
about = "O"
blog = "Blog"
@ -426,6 +415,17 @@ km = "km"
label = "Jedinice"
mi = "milje"
[smartCost]
activeHours = "{charging} od {total}"
activeHoursLabel = "Aktivno radno vrijeme"
co2Label = "Co₂ emisija"
co2Limit = "Ograničenje CO₂"
loadpointDescription = "Omogućuje privremeno brzo punjenje u solarnom modusu."
modalTitle = "Smart Grid napajanje"
none = "bez"
priceLabel = "Cijena energije"
priceLimit = "Ograničenje cijene"
[startupError]
configFile = "Korištena konfiguracijska datoteka:"
configuration = "Konfiguracija"

View file

@ -166,17 +166,6 @@ modalUpdateNow = "Instaliuoti dabar"
modalUpdateStarted = "Startuoja nauja evcc versija..."
modalUpdateStatusStart = "Instaliavimas pradėtas:"
[gridSettings]
activeHours = "{charging} iš {total}"
activeHoursLabel = "Aktyvumo valandos"
co2Label = "CO₂ emisijos"
co2Limit = "CO₂ limitas"
costLimitDescription = "Laikinai aktyvuoja greitą įkrovimą (režime Saulė)."
modalTitle = "Išmanaus tinklo įkrovimas"
none = "nėra"
priceLabel = "Energijos kaina"
priceLimit = "Kainos limitas"
[header]
about = "Apie"
blog = "Tinklaraštis"
@ -478,6 +467,17 @@ km = "km"
label = "Vienetai"
mi = "mylios"
[smartCost]
activeHours = "{charging} iš {total}"
activeHoursLabel = "Aktyvumo valandos"
co2Label = "CO₂ emisijos"
co2Limit = "CO₂ limitas"
loadpointDescription = "Laikinai aktyvuoja greitą įkrovimą (režime Saulė)."
modalTitle = "Išmanaus tinklo įkrovimas"
none = "nėra"
priceLabel = "Energijos kaina"
priceLimit = "Kainos limitas"
[startupError]
configFile = "Naudojamas konfiguracijos failas:"
configuration = "Konfiguracija"

View file

@ -166,17 +166,6 @@ modalUpdateNow = "Nu installeren"
modalUpdateStarted = "Starten van de nieuwe versie van evcc..."
modalUpdateStatusStart = "Installatie gestart:"
[gridSettings]
activeHours = "{charging} van {total}"
activeHoursLabel = "Actieve uren"
co2Label = "CO₂ uitstoot"
co2Limit = "CO₂ limiet"
costLimitDescription = "Zet tijdelijk snel-laden in solar mode aan"
modalTitle = "Smart Grid laden"
none = "geen"
priceLabel = "Energieprijs"
priceLimit = "Prijslimiet"
[header]
about = "Over"
blog = "Blog"
@ -475,6 +464,17 @@ km = "km"
label = "Eenheden"
mi = "mijlen"
[smartCost]
activeHours = "{charging} van {total}"
activeHoursLabel = "Actieve uren"
co2Label = "CO₂ uitstoot"
co2Limit = "CO₂ limiet"
loadpointDescription = "Zet tijdelijk snel-laden in solar mode aan"
modalTitle = "Smart Grid laden"
none = "geen"
priceLabel = "Energieprijs"
priceLimit = "Prijslimiet"
[startupError]
configFile = "Gebruikt configuratiebestand:"
configuration = "Configuratie"

View file

@ -108,17 +108,6 @@ modalUpdateNow = "Installer nå"
modalUpdateStarted = "Starter den nye versjonen av evcc..."
modalUpdateStatusStart = "Installasjonen startet:"
[gridSettings]
activeHours = "{charging} av {total}"
activeHoursLabel = "Aktive timer"
co2Label = "CO₂-utslipp"
co2Limit = "CO₂-grense"
costLimitDescription = "Slår på midlertidig hurtiglading i solcellemodus."
modalTitle = "Smart lysnettlading"
none = "ingen"
priceLabel = "Energipris"
priceLimit = "Prisgrense"
[header]
about = "Om"
blog = "Blogg"
@ -381,6 +370,17 @@ km = "km"
label = "Enheter"
mi = "mil"
[smartCost]
activeHours = "{charging} av {total}"
activeHoursLabel = "Aktive timer"
co2Label = "CO₂-utslipp"
co2Limit = "CO₂-grense"
loadpointDescription = "Slår på midlertidig hurtiglading i solcellemodus."
modalTitle = "Smart lysnettlading"
none = "ingen"
priceLabel = "Energipris"
priceLimit = "Prisgrense"
[startupError]
configFile = "Brukt oppsettsfil:"
configuration = "Oppsett"

View file

@ -123,17 +123,6 @@ modalUpdateNow = "Instaluj teraz"
modalUpdateStarted = "Nowa wersja evcc uruchomi się..."
modalUpdateStatusStart = "Instalacja rozpoczęta:"
[gridSettings]
activeHours = "{charging} z {total}"
activeHoursLabel = "Aktywne godziny"
co2Label = "Emisja CO₂"
co2Limit = "Limit CO₂"
costLimitDescription = "Umożliwia tymczasowe szybkie ładowanie w trybie solarnym."
modalTitle = "Ładowanie z inteligentnej sieci"
none = "nie ma"
priceLabel = "Cena energii"
priceLimit = "Limit ceny"
[header]
about = "O evcc"
blog = "Blog"
@ -425,6 +414,17 @@ km = "km"
label = "Jednostki"
mi = "mile"
[smartCost]
activeHours = "{charging} z {total}"
activeHoursLabel = "Aktywne godziny"
co2Label = "Emisja CO₂"
co2Limit = "Limit CO₂"
loadpointDescription = "Umożliwia tymczasowe szybkie ładowanie w trybie solarnym."
modalTitle = "Ładowanie z inteligentnej sieci"
none = "nie ma"
priceLabel = "Cena energii"
priceLimit = "Limit ceny"
[startupError]
configFile = "Używany plik konfiguracyjny:"
configuration = "Konfig"

View file

@ -122,17 +122,6 @@ modalUpdateNow = "Instalar agora"
modalUpdateStarted = "Iniciando a nova versão do evcc..."
modalUpdateStatusStart = "Instalação iniciada:"
[gridSettings]
activeHours = "{charging} de {total}"
activeHoursLabel = "Horas ativas"
co2Label = "Emissão de CO₂"
co2Limit = "Limite de CO₂"
costLimitDescription = "Permite carregamento rápido temporário no modo solar."
modalTitle = "Carregamento de Rede Inteligente"
none = "sem"
priceLabel = "Preço da energia"
priceLimit = "Limite de preço"
[header]
about = "Sobre"
blog = "Blog"
@ -427,6 +416,17 @@ km = "km"
label = "Unidades"
mi = "milhas"
[smartCost]
activeHours = "{charging} de {total}"
activeHoursLabel = "Horas ativas"
co2Label = "Emissão de CO₂"
co2Limit = "Limite de CO₂"
loadpointDescription = "Permite carregamento rápido temporário no modo solar."
modalTitle = "Carregamento de Rede Inteligente"
none = "sem"
priceLabel = "Preço da energia"
priceLimit = "Limite de preço"
[startupError]
configFile = "Ficheiro de configuração em uso:"
configuration = "Configuração"

View file

@ -130,17 +130,6 @@ modalUpdateNow = "Instalează acum"
modalUpdateStarted = "Pornesc noua versiune evcc..."
modalUpdateStatusStart = "Instalare pornita:"
[gridSettings]
activeHours = "{charging} din {total}"
activeHoursLabel = "Ore de functionare"
co2Label = "emisii CO₂"
co2Limit = "limita CO₂"
costLimitDescription = "Activeaza temporar Incarcarea Rapida in mod Solar."
modalTitle = "Incarcare Smart Grid"
none = "nimic"
priceLabel = "Pretul energiei"
priceLimit = "Limita de pret"
[header]
about = "Despre"
blog = "Blog"
@ -371,6 +360,17 @@ km = "km"
label = "Unități de măsura"
mi = "mile"
[smartCost]
activeHours = "{charging} din {total}"
activeHoursLabel = "Ore de functionare"
co2Label = "emisii CO₂"
co2Limit = "limita CO₂"
loadpointDescription = "Activeaza temporar Incarcarea Rapida in mod Solar."
modalTitle = "Incarcare Smart Grid"
none = "nimic"
priceLabel = "Pretul energiei"
priceLimit = "Limita de pret"
[startupError]
configFile = "Fișier configurare folosit:"
configuration = "Configurare"

View file

@ -122,17 +122,6 @@ modalUpdateNow = "Namesti zdaj"
modalUpdateStarted = "Zagon nove različice evcc…"
modalUpdateStatusStart = "Namestitev se je začela:"
[gridSettings]
activeHours = "{charging} od {total}"
activeHoursLabel = "Aktivne ure"
co2Label = "Emisije CO₂"
co2Limit = "Omejitev CO₂"
costLimitDescription = "Omogoči začasno hitro polnjenje v solarnem načinu."
modalTitle = "Smart Grid Polnjenje"
none = "brez"
priceLabel = "Cena energije"
priceLimit = "Omejitev cene"
[header]
about = "Informacije"
blog = "Blog"
@ -426,6 +415,17 @@ km = "km"
label = "Enote"
mi = "milje"
[smartCost]
activeHours = "{charging} od {total}"
activeHoursLabel = "Aktivne ure"
co2Label = "Emisije CO₂"
co2Limit = "Omejitev CO₂"
loadpointDescription = "Omogoči začasno hitro polnjenje v solarnem načinu."
modalTitle = "Smart Grid Polnjenje"
none = "brez"
priceLabel = "Cena energije"
priceLimit = "Omejitev cene"
[startupError]
configFile = "Uporabljena konfiguracijska datoteka:"
configuration = "Konfiguracija"

View file

@ -166,17 +166,6 @@ modalUpdateNow = "Installera nu"
modalUpdateStarted = "Starta den nya versionen av evcc..."
modalUpdateStatusStart = "Installationen har börjat:"
[gridSettings]
activeHours = "{charging} av {total}"
activeHoursLabel = "Aktiva timmar"
co2Label = "CO₂ utsläpp"
co2Limit = "CO₂ gräns"
costLimitDescription = "Ladda när priset är lågt (sol-läge)"
modalTitle = "Smartladdning elnät"
none = "ingen"
priceLabel = "Energipris"
priceLimit = "Prisgräns"
[header]
about = "Om"
blog = "Blogg"
@ -473,6 +462,17 @@ km = "km"
label = "Enheter"
mi = "miles"
[smartCost]
activeHours = "{charging} av {total}"
activeHoursLabel = "Aktiva timmar"
co2Label = "CO₂ utsläpp"
co2Limit = "CO₂ gräns"
loadpointDescription = "Ladda när priset är lågt (sol-läge)"
modalTitle = "Smartladdning elnät"
none = "ingen"
priceLabel = "Energipris"
priceLimit = "Prisgräns"
[startupError]
configFile = "Konfigurationsfil som används:"
configuration = "Konfiguration"

View file

@ -109,17 +109,6 @@ modalUpdateNow = "Встановити зараз"
modalUpdateStarted = "Початок нової версії evcc…"
modalUpdateStatusStart = "Встановлення розпочато:"
[gridSettings]
activeHours = "{charging} зі {total}"
activeHoursLabel = "Активні години"
co2Label = "Викиди CO₂"
co2Limit = "Ліміт CO₂"
costLimitDescription = "Вмикає тимчасову швидку зарядку в сонячному режимі."
modalTitle = "Розумна зарядка"
none = "немає"
priceLabel = "Ціна енергії"
priceLimit = "Ліміт ціни"
[header]
about = "Про evcc"
blog = "Блог"
@ -384,6 +373,17 @@ km = "км"
label = "Одиниці вимірювання"
mi = "милі"
[smartCost]
activeHours = "{charging} зі {total}"
activeHoursLabel = "Активні години"
co2Label = "Викиди CO₂"
co2Limit = "Ліміт CO₂"
loadpointDescription = "Вмикає тимчасову швидку зарядку в сонячному режимі."
modalTitle = "Розумна зарядка"
none = "немає"
priceLabel = "Ціна енергії"
priceLimit = "Ліміт ціни"
[startupError]
configFile = "Використаний файл конфігурації:"
configuration = "Конфігурація"

View file

@ -119,17 +119,6 @@ modalUpdateNow = "现在安装"
modalUpdateStarted = "启动新版本的evcc..."
modalUpdateStatusStart = "安装已开始:"
[gridSettings]
activeHours = "{charging} / {total}"
activeHoursLabel = "运行小时"
co2Label = "CO₂ 排放"
co2Limit = "CO₂ 限制"
costLimitDescription = "在太阳能模式下实现临时快速充电."
modalTitle = "智能电网充电"
none = "无"
priceLabel = "电价"
priceLimit = "限价"
[header]
about = "关于"
blog = "博客"
@ -389,6 +378,17 @@ km = "km"
label = "单位"
mi = "英里"
[smartCost]
activeHours = "{charging} / {total}"
activeHoursLabel = "运行小时"
co2Label = "CO₂ 排放"
co2Limit = "CO₂ 限制"
loadpointDescription = "在太阳能模式下实现临时快速充电."
modalTitle = "智能电网充电"
none = "无"
priceLabel = "电价"
priceLimit = "限价"
[startupError]
configFile = "使用的配置文件:"
configuration = "配置"

24
server/helper.go Normal file
View file

@ -0,0 +1,24 @@
package server
import (
"fmt"
"math"
"strconv"
)
// pass converts a simple api without return value to api with nil error return value
func pass[T any](f func(T)) func(T) error {
return func(v T) error {
f(v)
return nil
}
}
// parseFloat rejects NaN and Inf values
func parseFloat(payload string) (float64, error) {
f, err := strconv.ParseFloat(payload, 64)
if err == nil && (math.IsNaN(f) || math.IsInf(f, 0)) {
err = fmt.Errorf("invalid float value: %s", payload)
}
return f, err
}

View file

@ -5,6 +5,7 @@ import (
"net/http"
"time"
eapi "github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/server/assets"
"github.com/evcc-io/evcc/util"
@ -99,7 +100,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
"batterydischargecontrol": {[]string{"POST", "OPTIONS"}, "/batterydischargecontrol/{value:[a-z]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)},
"prioritysoc": {[]string{"POST", "OPTIONS"}, "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
"residualpower": {[]string{"POST", "OPTIONS"}, "/residualpower/{value:[-0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
"smartcost": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", floatHandler(site.SetSmartCostLimit, site.GetSmartCostLimit)},
"smartcost": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[-0-9.]+}", updateSmartCostLimit(site)},
"tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
"updatesession": {[]string{"PUT", "OPTIONS"}, "/session/{id:[0-9]+}", updateSessionHandler},
@ -135,12 +136,12 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
api := api.PathPrefix(fmt.Sprintf("/loadpoints/%d", id+1)).Subrouter()
routes := map[string]route{
"mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", chargeModeHandler(lp)},
"mode": {[]string{"POST", "OPTIONS"}, "/mode/{value:[a-z]+}", handler(eapi.ChargeModeString, pass(lp.SetMode), lp.GetMode)},
"limitsoc": {[]string{"POST", "OPTIONS"}, "/limitsoc/{value:[0-9]+}", intHandler(pass(lp.SetLimitSoc), lp.GetLimitSoc)},
"limitenergy": {[]string{"POST", "OPTIONS"}, "/limitenergy/{value:[0-9.]+}", floatHandler(pass(lp.SetLimitEnergy), lp.GetLimitEnergy)},
"mincurrent": {[]string{"POST", "OPTIONS"}, "/mincurrent/{value:[0-9.]+}", floatHandler(lp.SetMinCurrent, lp.GetMinCurrent)},
"maxcurrent": {[]string{"POST", "OPTIONS"}, "/maxcurrent/{value:[0-9.]+}", floatHandler(lp.SetMaxCurrent, lp.GetMaxCurrent)},
"phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", phasesHandler(lp)},
"phases": {[]string{"POST", "OPTIONS"}, "/phases/{value:[0-9]+}", intHandler(lp.SetPhases, lp.GetPhases)},
"plan": {[]string{"GET"}, "/plan", planHandler(lp)},
"planpreview": {[]string{"GET"}, "/plan/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planPreviewHandler(lp)},
"planenergy": {[]string{"POST", "OPTIONS"}, "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.-]+}", planEnergyHandler(lp)},
@ -151,6 +152,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
"remotedemand": {[]string{"POST", "OPTIONS"}, "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
"enableThreshold": {[]string{"POST", "OPTIONS"}, "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
"disableThreshold": {[]string{"POST", "OPTIONS"}, "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
"smartCostLimit": {[]string{"POST", "OPTIONS"}, "/smartcostlimit/{value:[0-9.]+}", floatHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
// "priority": {[]string{"POST", "OPTIONS"}, "/priority/{value:[0-9.]+}", floatHandler(pass(lp.SetPriority), lp.GetPriority)},
}

View file

@ -13,52 +13,6 @@ import (
"github.com/gorilla/mux"
)
type moder interface {
GetMode() api.ChargeMode
SetMode(api.ChargeMode)
}
// chargeModeHandler updates charge mode
func chargeModeHandler(lp moder) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
mode, err := api.ChargeModeString(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
lp.SetMode(mode)
jsonResult(w, lp.GetMode())
}
}
type phaser interface {
GetPhases() int
SetPhases(int) error
}
// phasesHandler updates minimum soc
func phasesHandler(lp phaser) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
phases, err := strconv.Atoi(vars["value"])
if err == nil {
err = lp.SetPhases(phases)
}
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonResult(w, lp.GetPhases())
}
}
// remoteDemandHandler updates minimum soc
func remoteDemandHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {

View file

@ -86,69 +86,37 @@ func jsonError(w http.ResponseWriter, status int, err error) {
jsonWrite(w, map[string]interface{}{"error": err.Error()})
}
// pass converts a simple api without return value to api with nil error return value
func pass[T any](f func(T)) func(T) error {
return func(v T) error {
f(v)
return nil
func handler[T any](conv func(string) (T, error), set func(T) error, get func() T) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
val, err := conv(vars["value"])
if err == nil {
err = set(val)
}
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonResult(w, get())
}
}
// floatHandler updates float-param api
func floatHandler(set func(float64) error, get func() float64) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
val, err := strconv.ParseFloat(vars["value"], 64)
if err == nil {
err = set(val)
}
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonResult(w, get())
}
return handler(parseFloat, set, get)
}
// intHandler updates int-param api
func intHandler(set func(int) error, get func() int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
val, err := strconv.Atoi(vars["value"])
if err == nil {
err = set(val)
}
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonResult(w, get())
}
return handler(strconv.Atoi, set, get)
}
// boolHandler updates bool-param api
func boolHandler(set func(bool) error, get func() bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
val, err := strconv.ParseBool(vars["value"])
if err == nil {
err = set(val)
}
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
jsonResult(w, get())
}
return handler(strconv.ParseBool, set, get)
}
// boolGetHandler retrieves bool api values
@ -171,6 +139,25 @@ func encodeFloats(data map[string]any) {
}
}
// updateSmartCostLimit sets the smart cost limit globally
func updateSmartCostLimit(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
val, err := parseFloat(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
for _, lp := range site.Loadpoints() {
lp.SetSmartCostLimit(val)
}
jsonResult(w, val)
}
}
// stateHandler returns the combined state
func stateHandler(cache *util.Cache) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {

View file

@ -3,7 +3,6 @@ package server
import (
"encoding/json"
"fmt"
"math"
"reflect"
"strconv"
"strings"
@ -170,11 +169,10 @@ func (m *MQTT) Listen(site site.API) error {
func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
for _, s := range []setter{
{"/prioritySoc", floatSetterErr(site.SetPrioritySoc)},
{"/bufferSoc", floatSetterErr(site.SetBufferSoc)},
{"/bufferStartSoc", floatSetterErr(site.SetBufferStartSoc)},
{"/residualPower", floatSetterErr(site.SetResidualPower)},
{"/smartCostLimit", floatSetterErr(site.SetSmartCostLimit)},
{"/prioritySoc", floatSetter(site.SetPrioritySoc)},
{"/bufferSoc", floatSetter(site.SetBufferSoc)},
{"/bufferStartSoc", floatSetter(site.SetBufferStartSoc)},
{"/residualPower", floatSetter(site.SetResidualPower)},
} {
if err := m.Handler.ListenSetter(topic+s.topic, s.fun); err != nil {
return err
@ -186,14 +184,15 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.API) error {
for _, s := range []setter{
{"/mode", setterFunc(api.ChargeModeString, lp.SetMode)},
{"/phases", intSetterErr(lp.SetPhases)},
{"/limitSoc", intSetter(lp.SetLimitSoc)},
{"/minCurrent", floatSetterErr(lp.SetMinCurrent)},
{"/maxCurrent", floatSetterErr(lp.SetMaxCurrent)},
{"/limitEnergy", floatSetter(lp.SetLimitEnergy)},
{"/enableThreshold", floatSetter(lp.SetEnableThreshold)},
{"/disableThreshold", floatSetter(lp.SetDisableThreshold)},
{"/mode", setterFunc(api.ChargeModeString, pass(lp.SetMode))},
{"/phases", intSetter(lp.SetPhases)},
{"/limitSoc", intSetter(pass(lp.SetLimitSoc))},
{"/minCurrent", floatSetter(lp.SetMinCurrent)},
{"/maxCurrent", floatSetter(lp.SetMaxCurrent)},
{"/limitEnergy", floatSetter(pass(lp.SetLimitEnergy))},
{"/enableThreshold", floatSetter(pass(lp.SetEnableThreshold))},
{"/disableThreshold", floatSetter(pass(lp.SetDisableThreshold))},
{"/smartCostLimit", floatSetter(pass(lp.SetSmartCostLimit))},
{"/planEnergy", func(payload string) error {
var plan struct {
Time time.Time `json:"time"`
@ -228,8 +227,8 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
func (m *MQTT) listenVehicleSetters(topic string, v vehicle.API) error {
for _, s := range []setter{
{topic + "/limitSoc", intSetter(v.SetLimitSoc)},
{topic + "/minSoc", intSetter(v.SetMinSoc)},
{topic + "/limitSoc", intSetter(pass(v.SetLimitSoc))},
{topic + "/minSoc", intSetter(pass(v.SetMinSoc))},
{topic + "/planSoc", func(payload string) error {
var plan struct {
Time time.Time `json:"time"`
@ -303,12 +302,3 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) {
m.publish(topic, true, p.Val)
}
}
// parseFloat rejects NaN and Inf values
func parseFloat(payload string) (float64, error) {
f, err := strconv.ParseFloat(payload, 64)
if err == nil && (math.IsNaN(f) || math.IsInf(f, 0)) {
err = fmt.Errorf("invalid float value: %s", payload)
}
return f, err
}

View file

@ -9,7 +9,7 @@ type setter struct {
fun func(string) error
}
func setterFuncErr[T any](conv func(string) (T, error), set func(T) error) func(string) error {
func setterFunc[T any](conv func(string) (T, error), set func(T) error) func(string) error {
return func(payload string) error {
val, err := conv(payload)
if err == nil {
@ -19,25 +19,10 @@ func setterFuncErr[T any](conv func(string) (T, error), set func(T) error) func(
}
}
func setterFunc[T any](conv func(string) (T, error), set func(T)) func(string) error {
return setterFuncErr(conv, func(val T) error {
set(val)
return nil
})
}
func floatSetterErr(set func(float64) error) func(string) error {
return setterFuncErr(parseFloat, set)
}
func floatSetter(set func(float64)) func(string) error {
func floatSetter(set func(float64) error) func(string) error {
return setterFunc(parseFloat, set)
}
func intSetterErr(set func(int) error) func(string) error {
return setterFuncErr(strconv.Atoi, set)
}
func intSetter(set func(int)) func(string) error {
func intSetter(set func(int) error) func(string) error {
return setterFunc(strconv.Atoi, set)
}

View file

@ -23,7 +23,6 @@ test.describe("Basics", async () => {
await page.getByTestId("topnavigation-button").click();
await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
await expect(page.getByRole("button", { name: "Battery Settings" })).not.toBeVisible();
await expect(page.getByRole("button", { name: "Smart Grid Charging" })).not.toBeVisible();
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
}
});
@ -64,7 +63,6 @@ test.describe("Advanced", async () => {
await page.getByTestId("topnavigation-button").click();
await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
await expect(page.getByRole("button", { name: "Battery Settings" })).toBeVisible();
await expect(page.getByRole("button", { name: "Smart Grid Charging" })).toBeVisible();
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
}
});
@ -87,23 +85,4 @@ test.describe("Advanced", async () => {
await expect(page.getByRole("heading", { name: "Battery Settings" })).toBeVisible();
});
test("Smart Grid Charging from top navigation", async ({ page }) => {
await page.goto("/");
await page.getByTestId("topnavigation-button").click();
await page.getByRole("button", { name: "Smart Grid Charging" }).click();
await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
});
test("Smart Grid Charging from energyflow", async ({ page }) => {
await page.goto("/");
await page.getByTestId("energyflow").click();
await page
.getByTestId("energyflow-entry-gridimport")
.getByTestId("energyflow-entry-details")
.click();
await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
});
});

56
tests/smart-cost.spec.js Normal file
View file

@ -0,0 +1,56 @@
const { test, expect } = require("@playwright/test");
const { start, stop } = require("./evcc");
const { startSimulator, stopSimulator, SIMULATOR_URL } = require("./simulator");
const CONFIG = "simulator.evcc.yaml";
test.beforeAll(async () => {
await start(CONFIG);
await startSimulator();
});
test.afterAll(async () => {
await stopSimulator();
await stop();
});
test.beforeEach(async ({ page }) => {
await page.goto(SIMULATOR_URL);
await page.getByLabel("PV Power").fill("6000");
await page.getByTestId("loadpoint0").getByLabel("Power").fill("6000");
await page.getByTestId("loadpoint0").getByText("C (charging)").click();
await page.getByTestId("loadpoint0").getByText("Enabled").check();
await page.getByRole("button", { name: "Apply changes" }).click();
});
test.describe("smart cost limit", async () => {
test("no limit, normal charging", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("vehicle-status")).toHaveText("Charging…");
});
test("price below limit", async ({ page }) => {
await page.goto("/");
await page.getByTestId("loadpoint-settings-button").nth(1).click();
await expect(page.getByTestId("loadpoint-settings-modal")).toBeVisible();
await page
.getByTestId("loadpoint-settings-modal")
.getByLabel("Price limit")
.selectOption("≤ 50.0 ct/kWh");
await page.getByTestId("loadpoint-settings-modal").getByLabel("Close").click();
await expect(page.getByTestId("loadpoint-settings-modal")).not.toBeVisible();
await expect(page.getByTestId("vehicle-status")).toHaveText(
"Charging cheap energy: 40.0 ct (limit 50.0 ct)"
);
});
test("price above limit", async ({ page }) => {
await page.goto("/");
await page.getByTestId("loadpoint-settings-button").nth(1).click();
await expect(page.getByTestId("loadpoint-settings-modal")).toBeVisible();
await page
.getByTestId("loadpoint-settings-modal")
.getByLabel("Price limit")
.selectOption("≤ 10.0 ct/kWh");
await page.getByTestId("loadpoint-settings-modal").getByLabel("Close").click();
await expect(page.getByTestId("loadpoint-settings-modal")).not.toBeVisible();
await expect(page.getByTestId("vehicle-status")).toHaveText("Charging…");
});
});