Add battery settings UI (#7802)
This commit is contained in:
parent
1e6da3670a
commit
9b282e6e4c
16 changed files with 725 additions and 67 deletions
|
|
@ -121,6 +121,18 @@ h5 {
|
|||
background-color: var(--evcc-dark-green) !important;
|
||||
}
|
||||
|
||||
.bg-dark-green {
|
||||
background-color: var(--evcc-dark-green) !important;
|
||||
}
|
||||
|
||||
.bg-darker-green {
|
||||
background-color: var(--evcc-darker-green) !important;
|
||||
}
|
||||
|
||||
.bg-darkest-green {
|
||||
background-color: var(--evcc-darkest-green) !important;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--bs-primary) !important;
|
||||
}
|
||||
|
|
@ -237,6 +249,9 @@ a:hover {
|
|||
.text-dark {
|
||||
color: var(--bs-gray-dark);
|
||||
}
|
||||
.text-light {
|
||||
color: var(--bs-gray-bright);
|
||||
}
|
||||
.text-gray {
|
||||
color: var(--bs-gray-medium);
|
||||
}
|
||||
|
|
|
|||
522
assets/js/components/BatterySettingsModal.vue
Normal file
522
assets/js/components/BatterySettingsModal.vue
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
id="batterySettingsModal"
|
||||
ref="modal"
|
||||
class="modal fade text-dark"
|
||||
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("batterySettings.modalTitle") }}
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div
|
||||
class="modal-body 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="batteryLimits">
|
||||
<label
|
||||
class="bufferSoc p-2 end-0"
|
||||
role="button"
|
||||
:class="{
|
||||
'bufferSoc--hidden':
|
||||
selectedBufferSoc === selectedPrioritySoc,
|
||||
}"
|
||||
:style="{ top: `${topHeight}%` }"
|
||||
>
|
||||
<select
|
||||
:value="selectedBufferSoc"
|
||||
class="custom-select"
|
||||
@change="changeBufferSoc"
|
||||
>
|
||||
<option
|
||||
v-for="{ value, name, disabled } in bufferOptions"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-decoration-underline text-nowrap pe-none">
|
||||
{{ fmtSoc(selectedBufferSoc) }}
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="prioritySoc p-2 end-0"
|
||||
role="button"
|
||||
:style="{ top: `${100 - bottomHeight}%` }"
|
||||
>
|
||||
<select
|
||||
:value="selectedPrioritySoc"
|
||||
class="custom-select"
|
||||
@change="changePrioritySoc"
|
||||
>
|
||||
<option
|
||||
v-for="{ value, name, disabled } in priorityOptions"
|
||||
:key="value"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
>
|
||||
{{ name }}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-decoration-underline text-nowrap pe-none">
|
||||
{{ fmtSoc(selectedPrioritySoc) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div
|
||||
class="bg-dark-green progress-bar text-light align-items-center"
|
||||
role="button"
|
||||
:style="{ height: `${topHeight}%` }"
|
||||
@click="toggleBufferStart"
|
||||
>
|
||||
<shopicon-regular-lightning
|
||||
size="m"
|
||||
class="icon"
|
||||
:style="iconStyle(topHeight)"
|
||||
></shopicon-regular-lightning>
|
||||
</div>
|
||||
<div
|
||||
class="bg-darker-green progress-bar text-light align-items-center"
|
||||
:style="{ height: `${middleHeight}%` }"
|
||||
>
|
||||
<shopicon-regular-car3
|
||||
size="m"
|
||||
class="icon"
|
||||
:style="iconStyle(middleHeight)"
|
||||
></shopicon-regular-car3>
|
||||
</div>
|
||||
<div
|
||||
class="bg-darkest-green progress-bar text-light align-items-center"
|
||||
:style="{ height: `${bottomHeight}%` }"
|
||||
>
|
||||
<shopicon-regular-home
|
||||
size="m"
|
||||
class="icon"
|
||||
:style="iconStyle(bottomHeight)"
|
||||
></shopicon-regular-home>
|
||||
</div>
|
||||
<div
|
||||
class="batterySoc ps-0 bg-white pe-none"
|
||||
:style="{ top: `${100 - batterySoc}%` }"
|
||||
></div>
|
||||
<div
|
||||
class="bufferStartIndicator pe-none"
|
||||
:class="{
|
||||
'bufferStartIndicator--hidden':
|
||||
!selectedBufferStartSoc || selectedBufferSoc === 100,
|
||||
}"
|
||||
:style="{ top: `${bufferStartTop}%` }"
|
||||
>
|
||||
<div class="bufferStartIndicator__left"></div>
|
||||
<div class="bufferStartIndicator__right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="me-sm-4">
|
||||
<p>
|
||||
{{ $t("batterySettings.batteryLevel") }}:
|
||||
<strong>{{ fmtSoc(batterySoc) }}</strong>
|
||||
<small
|
||||
v-for="(line, index) in batteryDetails"
|
||||
:key="index"
|
||||
class="d-block"
|
||||
>
|
||||
{{ line }}
|
||||
</small>
|
||||
</p>
|
||||
<p>{{ $t("batterySettings.legendTitle") }}</p>
|
||||
<p class="d-flex">
|
||||
<shopicon-regular-lightning
|
||||
size="s"
|
||||
class="flex-shrink-0 me-2"
|
||||
></shopicon-regular-lightning>
|
||||
<span class="d-block">
|
||||
{{ $t("batterySettings.legendTopName") }}
|
||||
<small v-if="selectedBufferSoc == 100" class="d-block">
|
||||
{{ $t("batterySettings.legendTopSubline") }}
|
||||
</small>
|
||||
<small v-else class="d-block">
|
||||
{{ $t("batterySettings.legendTopAutostart") }}
|
||||
<label
|
||||
for="bufferStartSelect"
|
||||
class="position-relative d-block"
|
||||
>
|
||||
<select
|
||||
id="bufferStartSelect"
|
||||
class="custom-select"
|
||||
@change="changeBufferStart"
|
||||
>
|
||||
<option
|
||||
v-for="option in [
|
||||
'never',
|
||||
'full',
|
||||
'medium',
|
||||
'low',
|
||||
]"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{
|
||||
$t(`batterySettings.bufferStart.${option}`)
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<span class="text-decoration-underline">
|
||||
{{
|
||||
$t(
|
||||
`batterySettings.bufferStart.${bufferStartOption}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</small>
|
||||
</span>
|
||||
</p>
|
||||
<p class="d-flex">
|
||||
<shopicon-regular-car3
|
||||
size="s"
|
||||
class="flex-shrink-0 me-2"
|
||||
></shopicon-regular-car3>
|
||||
<span class="d-block">
|
||||
{{ $t("batterySettings.legendMiddleName") }}
|
||||
<small class="d-block">
|
||||
{{ $t("batterySettings.legendMiddleSubline") }}
|
||||
</small>
|
||||
</span>
|
||||
</p>
|
||||
<p class="d-flex">
|
||||
<shopicon-regular-home
|
||||
size="s"
|
||||
class="flex-shrink-0 me-2"
|
||||
></shopicon-regular-home>
|
||||
<span class="d-block">
|
||||
{{ $t("batterySettings.legendBottomName") }}
|
||||
<small class="d-block">
|
||||
{{ $t("batterySettings.legendBottomSubline") }}
|
||||
</small>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<small>
|
||||
{{ $t("batterySettings.note") }}
|
||||
</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "@h2d2/shopicons/es/regular/lightning";
|
||||
import "@h2d2/shopicons/es/regular/car3";
|
||||
import "@h2d2/shopicons/es/regular/home";
|
||||
import formatter from "../mixins/formatter";
|
||||
|
||||
import api from "../api";
|
||||
|
||||
export default {
|
||||
name: "BatterySettingsModal",
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
bufferSoc: Number,
|
||||
prioritySoc: Number,
|
||||
batterySoc: Number,
|
||||
bufferStartSoc: Number,
|
||||
battery: { type: Array },
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
selectedBufferSoc: 100,
|
||||
selectedPrioritySoc: 0,
|
||||
selectedBufferStartSoc: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
priorityOptions() {
|
||||
const options = [];
|
||||
for (let i = 100; i >= 0; i -= 5) {
|
||||
// avoid intersection with buffer soc; allow everything if they touch
|
||||
const disabled =
|
||||
i > this.selectedBufferSoc &&
|
||||
!(this.selectedBufferSoc == this.selectedPrioritySoc);
|
||||
options.push({ value: i, name: this.fmtSoc(i), disabled });
|
||||
}
|
||||
return options;
|
||||
},
|
||||
bufferOptions() {
|
||||
const options = [];
|
||||
for (let i = 100; i >= 5; i -= 5) {
|
||||
options.push({
|
||||
value: i,
|
||||
name: this.fmtSoc(i),
|
||||
disabled: i < this.selectedPrioritySoc,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
},
|
||||
bufferStartTop() {
|
||||
if (!this.selectedBufferStartSoc) return 0;
|
||||
return 100 - this.selectedBufferStartSoc;
|
||||
},
|
||||
topHeight() {
|
||||
return 100 - (this.bufferSoc || 100);
|
||||
},
|
||||
middleHeight() {
|
||||
return 100 - this.topHeight - this.bottomHeight;
|
||||
},
|
||||
bottomHeight() {
|
||||
return this.prioritySoc;
|
||||
},
|
||||
bufferStartOption() {
|
||||
if (!this.selectedBufferStartSoc) return "never";
|
||||
if (this.selectedBufferStartSoc >= this.bufferStartOptionToSoc("full")) return "full";
|
||||
if (this.selectedBufferStartSoc >= this.bufferStartOptionToSoc("medium"))
|
||||
return "medium";
|
||||
return "low";
|
||||
},
|
||||
batteryDetails() {
|
||||
if (!Array.isArray(this.battery)) {
|
||||
return;
|
||||
}
|
||||
return this.battery.map(({ soc, capacity }) => {
|
||||
const multipleBatteries = this.battery.length > 1;
|
||||
const energy = this.fmtKWh(
|
||||
(capacity / 100) * soc * 1e3,
|
||||
true,
|
||||
!multipleBatteries,
|
||||
1
|
||||
);
|
||||
const total = this.fmtKWh(capacity * 1e3, true, true, 1);
|
||||
const name = multipleBatteries ? "↳ " : "";
|
||||
const formattedSoc = multipleBatteries ? ` (${this.fmtSoc(soc)})` : "";
|
||||
const formattedEnergy = this.$t("batterySettings.capacity", {
|
||||
energy,
|
||||
total,
|
||||
});
|
||||
return `${name}${formattedEnergy}${formattedSoc}`;
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
prioritySoc(soc) {
|
||||
this.selectedPrioritySoc = soc;
|
||||
},
|
||||
bufferSoc(soc) {
|
||||
this.selectedBufferSoc = soc || 100;
|
||||
},
|
||||
bufferStartSoc(soc) {
|
||||
this.selectedBufferStartSoc = soc;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedBufferSoc = this.bufferSoc || 100;
|
||||
this.selectedPrioritySoc = this.prioritySoc;
|
||||
this.selectedBufferSoc = this.bufferStart;
|
||||
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: {
|
||||
modalVisible: function () {
|
||||
this.isModalVisible = true;
|
||||
},
|
||||
modalInvisible: function () {
|
||||
this.isModalVisible = false;
|
||||
},
|
||||
changeBufferStart($event) {
|
||||
this.setBufferStart($event.target.value);
|
||||
},
|
||||
changePrioritySoc($event) {
|
||||
const value = parseInt($event.target.value, 10);
|
||||
if (value > this.bufferSoc) {
|
||||
this.saveBufferSoc(value);
|
||||
} else {
|
||||
this.savePrioritySoc(value);
|
||||
}
|
||||
},
|
||||
bufferStartOptionToSoc(option) {
|
||||
const bufferSoc = this.selectedBufferSoc;
|
||||
const bufferHeight = 100 - bufferSoc;
|
||||
console.log("bufferStartOptionToSoc", { bufferSoc, bufferHeight, option });
|
||||
switch (option) {
|
||||
case "low":
|
||||
return bufferSoc + bufferHeight * 0.2;
|
||||
case "medium":
|
||||
return bufferSoc + bufferHeight * 0.5;
|
||||
case "full":
|
||||
return bufferSoc + bufferHeight * 0.8;
|
||||
case "never":
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
toggleBufferStart() {
|
||||
const next = {
|
||||
never: "low",
|
||||
low: "medium",
|
||||
medium: "full",
|
||||
full: "never",
|
||||
};
|
||||
const nextOption = next[this.bufferStartOption];
|
||||
if (nextOption) {
|
||||
this.setBufferStart(nextOption);
|
||||
}
|
||||
},
|
||||
setBufferStart(option) {
|
||||
this.selectedBufferStartSoc = this.bufferStartOptionToSoc(option);
|
||||
this.saveBufferStartSoc(this.selectedBufferStartSoc);
|
||||
},
|
||||
changeBufferSoc($event) {
|
||||
const startOption = this.bufferStartOption;
|
||||
this.saveBufferSoc(parseInt($event.target.value, 10));
|
||||
this.setBufferStart(startOption);
|
||||
},
|
||||
async savePrioritySoc(soc) {
|
||||
this.selectedPrioritySoc = soc;
|
||||
try {
|
||||
await api.post(`prioritysoc/${encodeURIComponent(soc)}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
async saveBufferSoc(soc) {
|
||||
this.selectedBufferSoc = soc;
|
||||
try {
|
||||
await api.post(`buffersoc/${encodeURIComponent(soc)}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
async saveBufferStartSoc(soc) {
|
||||
try {
|
||||
await api.post(`bufferstartsoc/${encodeURIComponent(soc)}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
iconStyle(height) {
|
||||
let scale = 1;
|
||||
if (height <= 10) scale = 0.75;
|
||||
if (height <= 5) scale = 0;
|
||||
return { transform: `scale(${scale})` };
|
||||
},
|
||||
fmtSoc(soc) {
|
||||
return `${Math.round(soc)}%`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.battery {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
}
|
||||
.batteryLimits {
|
||||
width: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bufferStart,
|
||||
.bufferSoc,
|
||||
.prioritySoc {
|
||||
position: absolute;
|
||||
transform: translateY(-50%);
|
||||
transition-property: top, opacity;
|
||||
transition-timing-function: linear;
|
||||
transition-duration: var(--evcc-transition-fast);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bufferStart--hidden,
|
||||
.bufferSoc--hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.batterySoc,
|
||||
.bufferStartIndicator {
|
||||
position: absolute;
|
||||
transition-property: top, opacity;
|
||||
transition-timing-function: linear;
|
||||
transition-duration: var(--evcc-transition-fast);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.batterySoc {
|
||||
border-radius: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
height: 0.5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.bufferStartIndicator {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.bufferStartIndicator--hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.bufferStartIndicator__left,
|
||||
.bufferStartIndicator__right {
|
||||
height: 0.5rem;
|
||||
width: 0.5rem;
|
||||
background-color: var(--evcc-box);
|
||||
}
|
||||
.bufferStartIndicator__left {
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
.bufferStartIndicator__right {
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
.progress {
|
||||
height: 100%;
|
||||
width: 100px;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
background-color: var(--evcc-box) !important;
|
||||
}
|
||||
.progress-bar {
|
||||
transition: height var(--evcc-transition-fast) linear;
|
||||
}
|
||||
.icon {
|
||||
transition: transform var(--evcc-transition-fast) linear;
|
||||
z-index: 1;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.custom-select {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
-webkit-appearance: menulist-button;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -68,7 +68,8 @@
|
|||
:soc="batterySoc"
|
||||
:details="batterySoc"
|
||||
:detailsFmt="batteryFmt"
|
||||
:detailsTooltip="batteryTooltip"
|
||||
detailsClickable
|
||||
@details-clicked="openBatterySettingsModal"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
:name="$t('main.energyflow.gridImport')"
|
||||
|
|
@ -133,7 +134,8 @@
|
|||
:soc="batterySoc"
|
||||
:details="batterySoc"
|
||||
:detailsFmt="batteryFmt"
|
||||
:detailsTooltip="batteryTooltip"
|
||||
detailsClickable
|
||||
@details-clicked="openBatterySettingsModal"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
:name="$t('main.energyflow.pvExport')"
|
||||
|
|
@ -149,6 +151,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<GridSettingsModal v-bind="gridSettings" />
|
||||
<BatterySettingsModal v-bind="batterySettings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -163,10 +166,17 @@ import AnimatedNumber from "../AnimatedNumber.vue";
|
|||
import settings from "../../settings";
|
||||
import { CO2_UNIT } from "../../units";
|
||||
import collector from "../../mixins/collector";
|
||||
import BatterySettingsModal from "../BatterySettingsModal.vue";
|
||||
|
||||
export default {
|
||||
name: "Energyflow",
|
||||
components: { Visualization, EnergyflowEntry, AnimatedNumber, GridSettingsModal },
|
||||
components: {
|
||||
Visualization,
|
||||
EnergyflowEntry,
|
||||
AnimatedNumber,
|
||||
GridSettingsModal,
|
||||
BatterySettingsModal,
|
||||
},
|
||||
mixins: [formatter, collector],
|
||||
props: {
|
||||
gridConfigured: Boolean,
|
||||
|
|
@ -191,6 +201,9 @@ export default {
|
|||
smartCostLimit: { type: Number },
|
||||
smartCostUnit: { type: String },
|
||||
currency: { type: String },
|
||||
prioritySoc: { type: Number },
|
||||
bufferSoc: { type: Number },
|
||||
bufferStartSoc: { type: Number },
|
||||
},
|
||||
data: () => {
|
||||
return { detailsOpen: false, detailsCompleteHeight: null, gridSettingsModal: null };
|
||||
|
|
@ -234,27 +247,15 @@ export default {
|
|||
}
|
||||
return this.pv.map(({ power }) => this.fmtKw(power, this.powerInKw));
|
||||
},
|
||||
batteryTooltip() {
|
||||
if (!Array.isArray(this.battery)) {
|
||||
return;
|
||||
}
|
||||
return this.battery.map(({ soc, capacity }) => {
|
||||
const energy = this.fmtKWh((capacity / 100) * soc * 1e3, true, false, 1);
|
||||
const total = this.fmtKWh(capacity * 1e3, true, true, 1);
|
||||
const formattedSoc = this.batteryFmt(soc);
|
||||
return this.$t("main.energyflow.batteryTooltip", {
|
||||
energy,
|
||||
total,
|
||||
soc: formattedSoc,
|
||||
});
|
||||
});
|
||||
},
|
||||
batteryFmt() {
|
||||
return (soc) => `${Math.round(soc)}%`;
|
||||
},
|
||||
gridSettings() {
|
||||
return this.collectProps(GridSettingsModal);
|
||||
},
|
||||
batterySettings() {
|
||||
return this.collectProps(BatterySettingsModal);
|
||||
},
|
||||
co2Available() {
|
||||
return this.smartCostUnit === CO2_UNIT;
|
||||
},
|
||||
|
|
@ -263,6 +264,9 @@ export default {
|
|||
this.gridSettingsModal = Modal.getOrCreateInstance(
|
||||
document.querySelector("#gridSettingsModal")
|
||||
);
|
||||
this.batterySettingsModal = Modal.getOrCreateInstance(
|
||||
document.querySelector("#batterySettingsModal")
|
||||
);
|
||||
window.addEventListener("resize", this.updateHeight);
|
||||
// height must be calculated in case of initially open details
|
||||
if (settings.energyflowDetails) {
|
||||
|
|
@ -309,6 +313,9 @@ export default {
|
|||
openGridSettingsModal() {
|
||||
this.gridSettingsModal.show();
|
||||
},
|
||||
openBatterySettingsModal() {
|
||||
this.batterySettingsModal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ export default {
|
|||
battery: Array,
|
||||
gridCurrents: Array,
|
||||
prioritySoc: Number,
|
||||
bufferSoc: Number,
|
||||
bufferStartSoc: Number,
|
||||
siteTitle: String,
|
||||
vehicles: Array,
|
||||
|
||||
|
|
|
|||
|
|
@ -1070,7 +1070,7 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin
|
|||
}
|
||||
|
||||
// pvMaxCurrent calculates the maximum target current for PV mode
|
||||
func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered bool) float64 {
|
||||
func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batteryBuffered, batteryStart bool) float64 {
|
||||
// read only once to simplify testing
|
||||
minCurrent := lp.GetMinCurrent()
|
||||
maxCurrent := lp.GetMaxCurrent()
|
||||
|
|
@ -1427,7 +1427,7 @@ func (lp *Loadpoint) guardGracePeriodElapsed() bool {
|
|||
}
|
||||
|
||||
// Update is the main control function. It reevaluates meters and charger state
|
||||
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered bool, greenShare float64, effPrice, effCo2 *float64) {
|
||||
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
|
||||
lp.processTasks()
|
||||
|
||||
mode := lp.GetMode()
|
||||
|
|
@ -1526,7 +1526,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered bool,
|
|||
break
|
||||
}
|
||||
|
||||
targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered)
|
||||
targetCurrent := lp.pvMaxCurrent(mode, sitePower, batteryBuffered, batteryStart)
|
||||
|
||||
var required bool // false
|
||||
if targetCurrent == 0 && lp.vehicleClimateActive() {
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ func TestUpdatePowerZero(t *testing.T) {
|
|||
}
|
||||
|
||||
lp.Mode = tc.mode
|
||||
lp.Update(0, false, false, 0, nil, nil) // sitePower false,0
|
||||
lp.Update(0, false, false, false, 0, nil, nil) // false,sitePower false,0
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
|
@ -338,7 +338,7 @@ func TestPVHysteresis(t *testing.T) {
|
|||
// charger.EXPECT().Enabled().Return(tc.enabled, nil)
|
||||
|
||||
lp.enabled = tc.enabled
|
||||
current := lp.pvMaxCurrent(api.ModePV, se.site, false)
|
||||
current := lp.pvMaxCurrent(api.ModePV, se.site, false, false)
|
||||
|
||||
if current != se.current {
|
||||
t.Errorf("step %d: wanted %.1f, got %.1f", step, se.current, current)
|
||||
|
|
@ -371,7 +371,7 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
|
|||
|
||||
// maxCurrent will read enabled state in PV mode
|
||||
sitePower := -float64(phases)*minA*Voltage + 1 // 1W below min power
|
||||
current := lp.pvMaxCurrent(api.ModePV, sitePower, false)
|
||||
current := lp.pvMaxCurrent(api.ModePV, sitePower, false, false)
|
||||
|
||||
if current != 0 {
|
||||
t.Errorf("PV mode could not disable charger as expected. Expected 0, got %.f", current)
|
||||
|
|
@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
|
||||
lp.Update(500, false, false, 0, nil, nil)
|
||||
lp.Update(500, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("charging above target - soc deactivates charger")
|
||||
clock.Add(5 * time.Minute)
|
||||
|
|
@ -436,20 +436,20 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Enable(false).Return(nil)
|
||||
lp.Update(500, false, false, 0, nil, nil)
|
||||
lp.Update(500, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("deactivated charger changes status to B")
|
||||
clock.Add(5 * time.Minute)
|
||||
vehicle.EXPECT().Soc().Return(95.0, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
lp.Update(-5000, false, false, 0, nil, nil)
|
||||
lp.Update(-5000, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("soc has fallen below target - soc update prevented by timer")
|
||||
clock.Add(5 * time.Minute)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
lp.Update(-5000, false, false, 0, nil, nil)
|
||||
lp.Update(-5000, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("soc has fallen below target - soc update timer expired")
|
||||
clock.Add(pollInterval)
|
||||
|
|
@ -457,7 +457,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
|
|||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Enable(true).Return(nil)
|
||||
lp.Update(-5000, false, false, 0, nil, nil)
|
||||
lp.Update(-5000, false, false, false, 0, nil, nil)
|
||||
|
||||
ctrl.Finish()
|
||||
}
|
||||
|
|
@ -498,14 +498,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
|
|||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
|
||||
lp.Update(500, false, false, 0, nil, nil)
|
||||
lp.Update(500, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("switch off when disconnected")
|
||||
clock.Add(5 * time.Minute)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusA, nil)
|
||||
charger.EXPECT().Enable(false).Return(nil)
|
||||
lp.Update(-3000, false, false, 0, nil, nil)
|
||||
lp.Update(-3000, false, false, false, 0, nil, nil)
|
||||
|
||||
if lp.Mode != api.ModeOff {
|
||||
t.Error("unexpected mode", lp.Mode)
|
||||
|
|
@ -568,14 +568,14 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
|
|||
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
|
||||
t.Log("at 1:00h charging at 5 kWh")
|
||||
clock.Add(time.Hour)
|
||||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:00h stop charging at 5 kWh")
|
||||
|
|
@ -583,7 +583,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
|
|||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:00h restart charging at 5 kWh")
|
||||
|
|
@ -591,7 +591,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
|
|||
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 5000.0)
|
||||
|
||||
t.Log("at 1:30h continue charging at 7.5 kWh")
|
||||
|
|
@ -599,7 +599,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
|
|||
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusC, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 7500.0)
|
||||
|
||||
t.Log("at 2:00h stop charging at 10 kWh")
|
||||
|
|
@ -607,7 +607,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
|
|||
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
|
||||
charger.EXPECT().Enabled().Return(lp.enabled, nil)
|
||||
charger.EXPECT().Status().Return(api.StatusB, nil)
|
||||
lp.Update(-1, false, false, 0, nil, nil)
|
||||
lp.Update(-1, false, false, false, 0, nil, nil)
|
||||
expectCache("chargedEnergy", 10000.0)
|
||||
|
||||
ctrl.Finish()
|
||||
|
|
|
|||
|
|
@ -361,7 +361,7 @@ func TestReconnectVehicle(t *testing.T) {
|
|||
// vehicle not updated yet
|
||||
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)
|
||||
|
||||
lp.Update(0, false, false, 0, nil, nil)
|
||||
lp.Update(0, false, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
// detection started
|
||||
|
|
@ -375,7 +375,7 @@ func TestReconnectVehicle(t *testing.T) {
|
|||
// vehicle not updated yet
|
||||
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)
|
||||
|
||||
lp.Update(0, false, false, 0, nil, nil)
|
||||
lp.Update(0, false, false, false, 0, nil, nil)
|
||||
ctrl.Finish()
|
||||
|
||||
// vehicle detected
|
||||
|
|
|
|||
48
core/site.go
48
core/site.go
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/evcc-io/evcc/core/prioritizer"
|
||||
"github.com/evcc-io/evcc/push"
|
||||
serverdb "github.com/evcc-io/evcc/server/db"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/telemetry"
|
||||
|
|
@ -27,7 +28,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
|
|||
// Updater abstracts the Loadpoint implementation for testing
|
||||
type Updater interface {
|
||||
loadpoint.API
|
||||
Update(availablePower float64, autoCharge, batteryBuffered bool, greenShare float64, effectivePrice *float64, effectiveCo2 *float64)
|
||||
Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
|
||||
}
|
||||
|
||||
// meterMeasurement is used as slice element for publishing structured data
|
||||
|
|
@ -58,7 +59,8 @@ type Site struct {
|
|||
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
|
||||
Meters MetersConfig // Meter references
|
||||
PrioritySoc float64 `mapstructure:"prioritySoc"` // prefer battery up to this Soc
|
||||
BufferSoc float64 `mapstructure:"bufferSoc"` // ignore battery above this Soc
|
||||
BufferSoc float64 `mapstructure:"bufferSoc"` // continue charging on battery above this Soc
|
||||
BufferStartSoc float64 `mapstructure:"bufferStartSoc"` // start charging on battery above this Soc
|
||||
MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
|
||||
SmartCostLimit float64 `mapstructure:"smartCostLimit"` // always charge if cost is below this value
|
||||
|
||||
|
|
@ -114,6 +116,8 @@ func NewSiteFromConfig(
|
|||
site.prioritizer = prioritizer.New()
|
||||
site.savings = NewSavings(tariffs)
|
||||
|
||||
site.restoreSettings()
|
||||
|
||||
// upload telemetry on shutdown
|
||||
if telemetry.Enabled() {
|
||||
shutdown.Register(func() {
|
||||
|
|
@ -179,6 +183,14 @@ func NewSiteFromConfig(
|
|||
return nil, errors.New("missing either grid or pv meter")
|
||||
}
|
||||
|
||||
if site.BufferStartSoc <= site.BufferSoc {
|
||||
site.log.WARN.Println("bufferStartSoc must be larger than bufferSoc")
|
||||
}
|
||||
|
||||
if site.PrioritySoc > site.BufferSoc {
|
||||
site.log.WARN.Println("bufferSoc must be larger than prioritySoc")
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +214,21 @@ func (site *Site) Loadpoints() []loadpoint.API {
|
|||
return res
|
||||
}
|
||||
|
||||
func (site *Site) restoreSettings() {
|
||||
if v, err := settings.Float("site.bufferSoc"); err == nil {
|
||||
site.BufferSoc = v
|
||||
}
|
||||
if v, err := settings.Float("site.bufferStartSoc"); err == nil {
|
||||
site.BufferStartSoc = v
|
||||
}
|
||||
if v, err := settings.Float("site.prioritySoc"); err == nil {
|
||||
site.PrioritySoc = v
|
||||
}
|
||||
if v, err := settings.Float("site.smartCostLimit"); err == nil {
|
||||
site.SmartCostLimit = v
|
||||
}
|
||||
}
|
||||
|
||||
func meterCapabilities(name string, meter interface{}) string {
|
||||
_, power := meter.(api.Meter)
|
||||
_, energy := meter.(api.MeterEnergy)
|
||||
|
|
@ -491,9 +518,9 @@ func (site *Site) updateMeters() error {
|
|||
// - the net power exported by the site minus a residual margin
|
||||
// (negative values mean grid: export, battery: charging
|
||||
// - if battery buffer can be used for charging
|
||||
func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, error) {
|
||||
func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, bool, bool, error) {
|
||||
if err := site.updateMeters(); err != nil {
|
||||
return 0, false, err
|
||||
return 0, false, false, err
|
||||
}
|
||||
|
||||
// allow using PV as estimate for grid power
|
||||
|
|
@ -515,7 +542,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
batteryPower := site.batteryPower
|
||||
|
||||
// handed to loadpoint
|
||||
var batteryBuffered bool
|
||||
var batteryBuffered, batteryStart bool
|
||||
|
||||
if len(site.batteryMeters) > 0 {
|
||||
site.Lock()
|
||||
|
|
@ -528,6 +555,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
} else {
|
||||
// if battery is above bufferSoc allow using it for charging
|
||||
batteryBuffered = site.BufferSoc > 0 && site.batterySoc > site.BufferSoc
|
||||
batteryStart = site.BufferStartSoc > 0 && site.batterySoc > site.BufferStartSoc
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -564,7 +592,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
|
|||
|
||||
site.log.DEBUG.Printf("site power: %.0fW", sitePower)
|
||||
|
||||
return sitePower, batteryBuffered, nil
|
||||
return sitePower, batteryBuffered, batteryStart, nil
|
||||
}
|
||||
|
||||
func (site *Site) greenShare() float64 {
|
||||
|
|
@ -663,9 +691,9 @@ func (site *Site) update(lp Updater) {
|
|||
}
|
||||
}
|
||||
|
||||
if sitePower, batteryBuffered, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
|
||||
if sitePower, batteryBuffered, batteryStart, err := site.sitePower(totalChargePower, flexiblePower); err == nil {
|
||||
greenShare := site.greenShare()
|
||||
lp.Update(sitePower, autoCharge, batteryBuffered, greenShare, site.effectivePrice(greenShare), site.effectiveCo2(greenShare))
|
||||
lp.Update(sitePower, autoCharge, batteryBuffered, batteryStart, greenShare, site.effectivePrice(greenShare), site.effectiveCo2(greenShare))
|
||||
|
||||
// ignore negative pvPower values as that means it is not an energy source but consumption
|
||||
homePower := site.gridPower + math.Max(0, site.pvPower) + site.batteryPower - totalChargePower
|
||||
|
|
@ -693,12 +721,16 @@ func (site *Site) prepare() {
|
|||
site.publish("pvConfigured", len(site.pvMeters) > 0)
|
||||
site.publish("batteryConfigured", len(site.batteryMeters) > 0)
|
||||
site.publish("bufferSoc", site.BufferSoc)
|
||||
site.publish("bufferStartSoc", site.BufferStartSoc)
|
||||
site.publish("prioritySoc", site.PrioritySoc)
|
||||
site.publish("residualPower", site.ResidualPower)
|
||||
site.publish("smartCostLimit", site.SmartCostLimit)
|
||||
if tariff := site.GetTariff(PlannerTariff); tariff != nil {
|
||||
site.publish("smartCostUnit", tariff.Unit())
|
||||
site.publish("smartCostAvailable", tariff.IsDynamic())
|
||||
} else {
|
||||
site.publish("smartCostUnit", nil)
|
||||
site.publish("smartCostAvailable", nil)
|
||||
}
|
||||
site.publish("currency", site.tariffs.Currency.String())
|
||||
site.publish("savingsSince", site.savings.Since())
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ type API interface {
|
|||
|
||||
GetBufferSoc() float64
|
||||
SetBufferSoc(float64) error
|
||||
GetBufferStartSoc() float64
|
||||
SetBufferStartSoc(float64) error
|
||||
GetPrioritySoc() float64
|
||||
SetPrioritySoc(float64) error
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/core/site"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
)
|
||||
|
||||
var _ site.API = (*Site)(nil)
|
||||
|
|
@ -32,6 +33,7 @@ func (site *Site) SetPrioritySoc(soc float64) error {
|
|||
}
|
||||
|
||||
site.PrioritySoc = soc
|
||||
settings.SetFloat("site.prioritySoc", site.PrioritySoc)
|
||||
site.publish("prioritySoc", site.PrioritySoc)
|
||||
|
||||
return nil
|
||||
|
|
@ -54,11 +56,35 @@ func (site *Site) SetBufferSoc(soc float64) error {
|
|||
}
|
||||
|
||||
site.BufferSoc = soc
|
||||
settings.SetFloat("site.bufferSoc", site.BufferSoc)
|
||||
site.publish("bufferSoc", site.BufferSoc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBufferStartSoc returns the BufferStartSoc
|
||||
func (site *Site) GetBufferStartSoc() float64 {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
return site.BufferStartSoc
|
||||
}
|
||||
|
||||
// SetBufferStartSoc sets the BufferStartSoc
|
||||
func (site *Site) SetBufferStartSoc(soc float64) error {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
if len(site.batteryMeters) == 0 {
|
||||
return errors.New("battery not configured")
|
||||
}
|
||||
|
||||
site.BufferStartSoc = soc
|
||||
settings.SetFloat("site.bufferStartSoc", site.BufferStartSoc)
|
||||
site.publish("bufferStartSoc", site.BufferStartSoc)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetResidualPower returns the ResidualPower
|
||||
func (site *Site) GetResidualPower() float64 {
|
||||
site.Lock()
|
||||
|
|
@ -90,6 +116,7 @@ func (site *Site) SetSmartCostLimit(val float64) error {
|
|||
defer site.Unlock()
|
||||
|
||||
site.SmartCostLimit = val
|
||||
settings.SetFloat("site.smartCostLimit", site.SmartCostLimit)
|
||||
site.publish("smartCostLimit", site.SmartCostLimit)
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -109,11 +109,12 @@ site:
|
|||
- battery # list of battery meters
|
||||
aux:
|
||||
- aux # list of auxiliary meters for adjusting grid operating point
|
||||
residualPower: # additional household usage margin
|
||||
prioritySoc: # give home battery priority up to this soc (empty to disable)
|
||||
bufferSoc: # ignore home battery discharge above soc (empty to disable)
|
||||
maxGridSupplyWhileBatteryCharging: # ignore battery charging if AC consumption is above this value
|
||||
smartCostLimit: # set cost limit for automatic charging in PV mode
|
||||
residualPower: 0 # additional household usage margin
|
||||
prioritySoc: 0 # give home battery priority up to this soc (empty to disable)
|
||||
bufferSoc: 0 # continue charging on battery above soc (0 to disable)
|
||||
bufferStartSoc: 0 # start charging on battery above soc (0 to disable)
|
||||
maxGridSupplyWhileBatteryCharging: 0 # ignore battery charging if AC consumption is above this value
|
||||
smartCostLimit: 0 # set cost limit for automatic charging in PV mode
|
||||
|
||||
# loadpoint describes the charger, charge meter and connected vehicle
|
||||
loadpoints:
|
||||
|
|
|
|||
20
i18n/de.toml
20
i18n/de.toml
|
|
@ -1,3 +1,23 @@
|
|||
[batterySettings]
|
||||
batteryLevel = "Ladestand"
|
||||
capacity = "{energy} von {total}"
|
||||
legendBottomName = "Haus hat Priorität"
|
||||
legendBottomSubline = "nicht zum Laden verwenden"
|
||||
legendMiddleName = "Fahrzeug zuerst"
|
||||
legendMiddleSubline = "dann das Haus"
|
||||
legendTitle = "Wie soll Sonnenstrom genutzt werden?"
|
||||
legendTopAutostart = "startet automatisch"
|
||||
legendTopName = "batterieunterstütztes Laden"
|
||||
legendTopSubline = "ohne Unterbrechungen"
|
||||
modalTitle = "Batterie Einstellungen"
|
||||
note = "Hinweis: Nur relevant im PV Modus. Das Ladeverhalten wird entsprechend angepasst."
|
||||
|
||||
[batterySettings.bufferStart]
|
||||
full = "wenn fast voll"
|
||||
low = "wenn etwas voll"
|
||||
medium = "wenn halbvoll"
|
||||
never = "nur mit genug PV-Überschuss"
|
||||
|
||||
[footer]
|
||||
|
||||
[footer.community]
|
||||
|
|
|
|||
20
i18n/en.toml
20
i18n/en.toml
|
|
@ -1,3 +1,23 @@
|
|||
[batterySettings]
|
||||
batteryLevel = "Battery level"
|
||||
capacity = "{energy} of {total}"
|
||||
legendBottomName = "home priority"
|
||||
legendBottomSubline = "not used for charging"
|
||||
legendMiddleName = "vehicle first"
|
||||
legendMiddleSubline = "home second"
|
||||
legendTitle = "How should solar energy be used?"
|
||||
legendTopAutostart = "starts automatically"
|
||||
legendTopName = "battery supported charging"
|
||||
legendTopSubline = "without interruptions"
|
||||
modalTitle = "Battery settings"
|
||||
note = "Note: These settings only affect the solar mode. Charging behaviour is adjusted accordingly."
|
||||
|
||||
[batterySettings.bufferStart]
|
||||
full = "when nearly full"
|
||||
low = "when somewhat full"
|
||||
medium = "when half full"
|
||||
never = "only with enough surplus"
|
||||
|
||||
[footer]
|
||||
|
||||
[footer.community]
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@
|
|||
"bufferSoc": {
|
||||
"type": "number"
|
||||
},
|
||||
"bufferStartSoc": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxGridSupplyWhileBatteryCharging": {
|
||||
"type": "number"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -95,21 +95,22 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, cache *util.Cache) {
|
|||
|
||||
// site api
|
||||
routes := map[string]route{
|
||||
"health": {[]string{"GET"}, "/health", healthHandler(site)},
|
||||
"state": {[]string{"GET"}, "/state", stateHandler(cache)},
|
||||
"config": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler},
|
||||
"products": {[]string{"GET"}, "/config/products/{class:[a-z]+}", productsHandler},
|
||||
"test": {[]string{"POST"}, "/config/test/{class:[a-z]+}", testHandler},
|
||||
"buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
|
||||
"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)},
|
||||
"tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
|
||||
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
|
||||
"session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler},
|
||||
"session2": {[]string{"DELETE"}, "/session/{id:[0-9]+}", deleteSessionHandler},
|
||||
"telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
|
||||
"telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
|
||||
"health": {[]string{"GET"}, "/health", healthHandler(site)},
|
||||
"state": {[]string{"GET"}, "/state", stateHandler(cache)},
|
||||
"config": {[]string{"GET"}, "/config/templates/{class:[a-z]+}", templatesHandler},
|
||||
"products": {[]string{"GET"}, "/config/products/{class:[a-z]+}", productsHandler},
|
||||
"test": {[]string{"POST"}, "/config/test/{class:[a-z]+}", testHandler},
|
||||
"buffersoc": {[]string{"POST", "OPTIONS"}, "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
|
||||
"bufferstartsoc": {[]string{"POST", "OPTIONS"}, "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)},
|
||||
"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)},
|
||||
"tariff": {[]string{"GET"}, "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
|
||||
"sessions": {[]string{"GET"}, "/sessions", sessionHandler},
|
||||
"session1": {[]string{"PUT"}, "/session/{id:[0-9]+}", updateSessionHandler},
|
||||
"session2": {[]string{"DELETE"}, "/session/{id:[0-9]+}", deleteSessionHandler},
|
||||
"telemetry": {[]string{"GET"}, "/settings/telemetry", boolGetHandler(telemetry.Enabled)},
|
||||
"telemetry2": {[]string{"POST", "OPTIONS"}, "/settings/telemetry/{value:[a-z]+}", boolHandler(telemetry.Enable, telemetry.Enabled)},
|
||||
}
|
||||
|
||||
for _, r := range routes {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,12 @@ func (m *MQTT) Run(site site.API, in <-chan util.Param) {
|
|||
}
|
||||
})
|
||||
|
||||
m.Handler.ListenSetter(fmt.Sprintf("%s/site/bufferStartSoc/set", m.root), func(payload string) {
|
||||
if val, err := strconv.ParseFloat(payload, 64); err == nil {
|
||||
_ = site.SetBufferStartSoc(val)
|
||||
}
|
||||
})
|
||||
|
||||
m.Handler.ListenSetter(fmt.Sprintf("%s/site/residualPower/set", m.root), func(payload string) {
|
||||
if val, err := strconv.ParseFloat(payload, 64); err == nil {
|
||||
_ = site.SetResidualPower(val)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue