Site: add experimental battery grid discharge setting (#32086)
This commit is contained in:
parent
f7af0bbe6a
commit
177ca558c9
16 changed files with 189 additions and 0 deletions
60
assets/js/components/Config/BatteryModal.vue
Normal file
60
assets/js/components/Config/BatteryModal.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<GenericModal
|
||||
id="batteryModal"
|
||||
:title="`${$t('config.battery.title')} 🧪`"
|
||||
config-modal-name="battery"
|
||||
data-testid="battery-modal"
|
||||
>
|
||||
<p>{{ $t("config.battery.description") }}</p>
|
||||
<ErrorMessage :error="error" />
|
||||
<div class="form-check form-switch my-3">
|
||||
<input
|
||||
id="batteryGridDischarge"
|
||||
:checked="gridDischarge"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
@change="change"
|
||||
/>
|
||||
<div class="form-check-label">
|
||||
<label for="batteryGridDischarge">
|
||||
{{ $t("config.battery.gridDischarge") }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import GenericModal from "../Helper/GenericModal.vue";
|
||||
import ErrorMessage from "../Helper/ErrorMessage.vue";
|
||||
import api from "@/api";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BatteryModal",
|
||||
components: { GenericModal, ErrorMessage },
|
||||
props: {
|
||||
gridDischarge: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: null as string | null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async change(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
try {
|
||||
this.error = null;
|
||||
await api.post(`batterygriddischarge/${target.checked}`);
|
||||
} catch (err) {
|
||||
target.checked = this.gridDischarge; // revert on failure to stay in sync with state
|
||||
const axiosErr = err as AxiosError<{ error: string }>;
|
||||
this.error = axiosErr.response?.data?.error || axiosErr.message;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
@ -29,6 +29,14 @@
|
|||
@edit="openModal('experimental')"
|
||||
/>
|
||||
|
||||
<GeneralConfigEntry
|
||||
v-if="experimental && batteryControllable"
|
||||
test-id="generalconfig-battery"
|
||||
:label="$t('config.battery.title')"
|
||||
:text="$t(`config.general.${batteryGridDischarge ? 'on' : 'off'}`)"
|
||||
@edit="openModal('battery')"
|
||||
/>
|
||||
|
||||
<GeneralConfigEntry
|
||||
test-id="generalconfig-sponsoring"
|
||||
:label="$t('config.sponsor.title')"
|
||||
|
|
@ -95,6 +103,12 @@ export default {
|
|||
telemetryEnabled() {
|
||||
return store.state?.telemetry === true;
|
||||
},
|
||||
batteryControllable() {
|
||||
return (store.state?.battery ?? []).some((b) => b.controllable);
|
||||
},
|
||||
batteryGridDischarge() {
|
||||
return store.state?.batteryGridDischarge === true;
|
||||
},
|
||||
networkStatus() {
|
||||
return `${store.state?.network?.port ?? ""}`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -262,6 +262,8 @@ export interface State {
|
|||
bufferStartSoc?: number;
|
||||
/** Home battery discharge is prevented during fast charging and planned charging. */
|
||||
batteryDischargeControl?: boolean;
|
||||
/** Home battery is allowed to discharge to the grid (experimental). */
|
||||
batteryGridDischarge?: boolean;
|
||||
solarAdjusted?: boolean;
|
||||
/** Price or emission limit for charging the home battery from grid. */
|
||||
batteryGridChargeLimit?: number | null;
|
||||
|
|
|
|||
|
|
@ -481,6 +481,7 @@
|
|||
<OptimizerModal :is-sponsor="isSponsor" />
|
||||
<McpModal />
|
||||
<ExperimentalModal :experimental="experimental" />
|
||||
<BatteryModal :grid-discharge="batteryGridDischarge" />
|
||||
<RemoteModal :remote="remote" :is-sponsor="isSponsor" :site-title="siteTitle" />
|
||||
<TitleModal @changed="loadDirty" />
|
||||
<ModbusProxyModal :is-sponsor="isSponsor" @changed="loadDirty" />
|
||||
|
|
@ -559,6 +560,7 @@ import TariffCard from "../components/Config/TariffCard.vue";
|
|||
import TariffModal from "../components/Config/TariffModal.vue";
|
||||
import TelemetryModal from "../components/Config/TelemetryModal.vue";
|
||||
import ExperimentalModal from "../components/Config/ExperimentalModal.vue";
|
||||
import BatteryModal from "../components/Config/BatteryModal.vue";
|
||||
import TitleModal from "../components/Config/TitleModal.vue";
|
||||
import Header from "../components/Top/Header.vue";
|
||||
import VehicleIcon from "../components/VehicleIcon";
|
||||
|
|
@ -648,6 +650,7 @@ export default defineComponent({
|
|||
TariffModal,
|
||||
TelemetryModal,
|
||||
ExperimentalModal,
|
||||
BatteryModal,
|
||||
TitleModal,
|
||||
TopHeader: Header,
|
||||
VehicleIcon,
|
||||
|
|
@ -922,6 +925,9 @@ export default defineComponent({
|
|||
experimental() {
|
||||
return store.state?.experimental;
|
||||
},
|
||||
batteryGridDischarge() {
|
||||
return store.state?.batteryGridDischarge === true;
|
||||
},
|
||||
eebus() {
|
||||
return store.state?.eebus;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const (
|
|||
BatteryDischargeControl = "batteryDischargeControl"
|
||||
BatteryGridChargeLimit = "batteryGridChargeLimit"
|
||||
BatteryGridChargeActive = "batteryGridChargeActive"
|
||||
BatteryGridDischarge = "batteryGridDischarge"
|
||||
BufferSoc = "bufferSoc"
|
||||
BufferStartSoc = "bufferStartSoc"
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ type Site struct {
|
|||
bufferStartSoc float64 // start charging on battery above this Soc
|
||||
batteryDischargeControl bool // prevent battery discharge for fast and planned charging
|
||||
batteryGridChargeLimit *float64 // grid charging limit
|
||||
batteryGridDischarge bool // allow battery discharge to grid (experimental)
|
||||
|
||||
// forecast settings
|
||||
solarAdjusted bool // adjust solar forecast to real production data
|
||||
|
|
@ -383,6 +384,11 @@ func (site *Site) restoreSettings() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if v, err := settings.Bool(keys.BatteryGridDischarge); err == nil {
|
||||
if err := site.SetBatteryGridDischarge(v); err != nil && !errors.Is(err, ErrBatteryControlNotAvailable) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if v, err := settings.Float(keys.ResidualPower); err == nil {
|
||||
if err := site.SetResidualPower(v); err != nil {
|
||||
return err
|
||||
|
|
@ -1210,6 +1216,7 @@ func (site *Site) prepare() {
|
|||
site.publish(keys.BufferStartSoc, site.bufferStartSoc)
|
||||
site.publish(keys.BatteryMode, site.batteryMode)
|
||||
site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
|
||||
site.publish(keys.BatteryGridDischarge, site.batteryGridDischarge)
|
||||
site.publish(keys.SolarAdjusted, site.solarAdjusted)
|
||||
site.publish(keys.ResidualPower, site.GetResidualPower())
|
||||
site.publish(keys.SmartCostAvailable, site.isDynamicTariff(api.TariffUsagePlanner))
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ type API interface {
|
|||
|
||||
GetBatteryDischargeControl() bool
|
||||
SetBatteryDischargeControl(bool) error
|
||||
GetBatteryGridDischarge() bool
|
||||
SetBatteryGridDischarge(bool) error
|
||||
|
||||
//
|
||||
// battery control external
|
||||
|
|
|
|||
|
|
@ -370,6 +370,33 @@ func (site *Site) SetBatteryDischargeControl(val bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetBatteryGridDischarge returns whether the battery may discharge to grid (experimental)
|
||||
func (site *Site) GetBatteryGridDischarge() bool {
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
return site.batteryGridDischarge
|
||||
}
|
||||
|
||||
// SetBatteryGridDischarge sets whether the battery may discharge to grid (experimental)
|
||||
func (site *Site) SetBatteryGridDischarge(val bool) error {
|
||||
site.log.DEBUG.Println("set battery grid discharge:", val)
|
||||
|
||||
if !site.hasBatteryControl() {
|
||||
return ErrBatteryControlNotAvailable
|
||||
}
|
||||
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
if site.batteryGridDischarge != val {
|
||||
site.batteryGridDischarge = val
|
||||
settings.SetBool(keys.BatteryGridDischarge, val)
|
||||
site.publish(keys.BatteryGridDischarge, val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSolarAdjusted returns if the solar forecast is adjusted to real production data
|
||||
func (site *Site) GetSolarAdjusted() bool {
|
||||
site.RLock()
|
||||
|
|
|
|||
|
|
@ -796,6 +796,7 @@ func (site *Site) batteryRequest(dev config.Device[api.Meter], b types.Measureme
|
|||
controllable := api.HasCap[api.BatteryController](instance)
|
||||
if controllable {
|
||||
bat.ChargeFromGrid = true
|
||||
bat.DischargeToGrid = site.GetBatteryGridDischarge()
|
||||
}
|
||||
|
||||
if m, ok := api.Cap[api.BatteryPowerLimiter](instance); ok {
|
||||
|
|
|
|||
|
|
@ -118,6 +118,9 @@
|
|||
"titleEdit": "Edit Self-Regulating Consumer"
|
||||
},
|
||||
"battery": {
|
||||
"description": "Experimental battery controls. These features are still being tested and may change or be removed in future versions.",
|
||||
"gridDischarge": "Allow battery to discharge to the grid",
|
||||
"title": "Battery",
|
||||
"titleAdd": "Add Battery",
|
||||
"titleEdit": "Edit Battery"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API) {
|
|||
"buffersoc": {"POST", "/buffersoc/{value:[0-9.]+}", floatHandler(site.SetBufferSoc, site.GetBufferSoc)},
|
||||
"bufferstartsoc": {"POST", "/bufferstartsoc/{value:[0-9.]+}", floatHandler(site.SetBufferStartSoc, site.GetBufferStartSoc)},
|
||||
"batterydischargecontrol": {"POST", "/batterydischargecontrol/{value:[01truefalse]+}", boolHandler(site.SetBatteryDischargeControl, site.GetBatteryDischargeControl)},
|
||||
"batterygriddischarge": {"POST", "/batterygriddischarge/{value:[01truefalse]+}", boolHandler(site.SetBatteryGridDischarge, site.GetBatteryGridDischarge)},
|
||||
"batterygridcharge": {"POST", "/batterygridchargelimit/{value:-?[0-9.]+}", floatPtrHandler(site.SetBatteryGridChargeLimit, site.GetBatteryGridChargeLimit)},
|
||||
"batterygridchargedelete": {"DELETE", "/batterygridchargelimit", floatPtrHandler(site.SetBatteryGridChargeLimit, site.GetBatteryGridChargeLimit)},
|
||||
"batterymode": {"POST", "/batterymode/{value:[a-z]+}", updateBatteryMode(site)},
|
||||
|
|
|
|||
|
|
@ -283,6 +283,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/batterygriddischarge/{enable}": {
|
||||
"post": {
|
||||
"operationId": "setBatteryGridDischarge",
|
||||
"summary": "Control battery grid discharge",
|
||||
"description": "Allow the home battery to discharge to the grid (experimental).",
|
||||
"externalDocs": {
|
||||
"url": "https://docs.evcc.io/en/features/battery"
|
||||
},
|
||||
"tags": [
|
||||
"battery"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/enable"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/BooleanResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/batterygridchargelimit": {
|
||||
"delete": {
|
||||
"operationId": "removeBatteryGridChargeLimit",
|
||||
|
|
@ -4958,6 +4981,10 @@
|
|||
"description": "Home battery discharge is prevented during fast charging and planned charging.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"batteryGridDischarge": {
|
||||
"description": "Home battery is allowed to discharge to the grid (experimental).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"solarAdjusted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -146,6 +146,26 @@ call setBatteryGridChargeLimit {
|
|||
}
|
||||
```
|
||||
|
||||
## setBatteryGridDischarge
|
||||
|
||||
Allow the home battery to discharge to the grid (experimental).
|
||||
|
||||
**Tags:** battery
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| enable | string | Charging mode. |
|
||||
|
||||
**Example call:**
|
||||
|
||||
```json
|
||||
call setBatteryGridDischarge {
|
||||
"enable": "true"
|
||||
}
|
||||
```
|
||||
|
||||
## setBufferSoc
|
||||
|
||||
Set battery buffer SoC.
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
|
|||
{"bufferSoc", floatSetter(site.SetBufferSoc)},
|
||||
{"bufferStartSoc", floatSetter(site.SetBufferStartSoc)},
|
||||
{"batteryDischargeControl", boolSetter(site.SetBatteryDischargeControl)},
|
||||
{"batteryGridDischarge", boolSetter(site.SetBatteryGridDischarge)},
|
||||
{"prioritySoc", floatSetter(site.SetPrioritySoc)},
|
||||
{"residualPower", floatSetter(site.SetResidualPower)},
|
||||
{"solarAdjusted", boolSetter(pass(site.SetSolarAdjusted))},
|
||||
|
|
|
|||
|
|
@ -193,6 +193,9 @@ components:
|
|||
batteryDischargeControl:
|
||||
description: Home battery discharge is prevented during fast charging and planned charging.
|
||||
type: boolean
|
||||
batteryGridDischarge:
|
||||
description: Home battery is allowed to discharge to the grid (experimental).
|
||||
type: boolean
|
||||
solarAdjusted:
|
||||
type: boolean
|
||||
batteryGridChargeLimit:
|
||||
|
|
|
|||
|
|
@ -181,6 +181,20 @@ paths:
|
|||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BooleanResult"
|
||||
/batterygriddischarge/{enable}:
|
||||
post:
|
||||
operationId: setBatteryGridDischarge
|
||||
summary: Control battery grid discharge
|
||||
description: "Allow the home battery to discharge to the grid (experimental)."
|
||||
externalDocs:
|
||||
url: https://docs.evcc.io/en/features/battery
|
||||
tags:
|
||||
- battery
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/enable"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BooleanResult"
|
||||
/batterygridchargelimit:
|
||||
delete:
|
||||
operationId: removeBatteryGridChargeLimit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue