Planner: add continuous strategy (BC) (#24423)

This commit is contained in:
Ingo 2025-12-30 23:43:11 +01:00 • committed by GitHub
parent f93b15a8c1
commit 291898fea9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 2245 additions and 950 deletions

View file

@ -9,7 +9,6 @@ import (
"slices"
"strconv"
"strings"
"time"
)
// pass converts a simple api without return value to api with nil error return value
@ -29,21 +28,6 @@ func parseFloat(payload string) (float64, error) {
return f, err
}
// parseDuration parses a duration string as seconds
func parseDuration(payload string) (time.Duration, error) {
if payload == "" {
return 0, nil
}
v, err := strconv.Atoi(payload)
if err != nil {
return 0, err
}
if v < 0 {
return 0, fmt.Errorf("invalid duration: %s", payload)
}
return time.Duration(v) * time.Second, err
}
// jsonDecoder returns a json decoder with disallowed unknown fields
func jsonDecoder(r io.Reader) *json.Decoder {
dec := json.NewDecoder(r)

View file

@ -173,6 +173,7 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"plan": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc/{value:[0-9]+}/{time:[0-9TZ:.+-]+}", planSocHandler(site)},
"plan2": {"DELETE", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/soc", planSocRemoveHandler(site)},
"repeatingPlans": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/repeating", addRepeatingPlansHandler(site)},
"planStrategy": {"POST", "/vehicles/{name:[a-zA-Z0-9_.:-]+}/plan/strategy", updatePlanStrategyHandler(site)},
// config ui
// "mode": {"POST", "/mode/{value:[a-z]+}", chargeModeHandler(v)},
@ -199,9 +200,9 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"phases": {"POST", "/phases/{value:[0-9]+}", intHandler(lp.SetPhasesConfigured, lp.GetPhasesConfigured)},
"plan": {"GET", "/plan", planHandler(lp)},
"staticPlanPreview": {"GET", "/plan/static/preview/{type:(?:soc|energy)}/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", staticPlanPreviewHandler(lp)},
"repeatingPlanPreview": {"GET", "/plan/repeating/preview/{soc:[0-9]+}/{weekdays:[0-6,]+}/{time:[0-2][0-9]:[0-5][0-9]}/{tz:[a-zA-Z0-9_./:-]+}", repeatingPlanPreviewHandler(lp)},
"planenergy": {"POST", "/plan/energy/{value:[0-9.]+}/{time:[0-9TZ:.+-]+}", planEnergyHandler(lp)},
"planenergy2": {"DELETE", "/plan/energy", planRemoveHandler(lp)},
"planStrategy": {"POST", "/plan/strategy", planStrategyHandler(lp)},
"vehicle": {"POST", "/vehicle/{name:[a-zA-Z0-9_.:-]+}", vehicleSelectHandler(site, lp)},
"vehicle2": {"DELETE", "/vehicle", vehicleRemoveHandler(lp)},
"vehicleDetect": {"PATCH", "/vehicle", vehicleDetectHandler(lp)},

View file

@ -27,7 +27,7 @@ func getLoadpointStaticConfig(lp loadpoint.API) loadpoint.StaticConfig {
}
func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
planTime, planPrecondition, planEnergy := lp.GetPlanEnergy()
planTime, planEnergy := lp.GetPlanEnergy()
return loadpoint.DynamicConfig{
Title: lp.GetTitle(),
DefaultMode: string(lp.GetDefaultMode()),
@ -41,7 +41,7 @@ func getLoadpointDynamicConfig(lp loadpoint.API) loadpoint.DynamicConfig {
Soc: lp.GetSocConfig(),
PlanEnergy: planEnergy,
PlanTime: planTime,
PlanPrecondition: int64(planPrecondition.Seconds()),
PlanStrategy: lp.GetPlanStrategy(),
LimitEnergy: lp.GetLimitEnergy(),
LimitSoc: lp.GetLimitSoc(),
}

View file

@ -5,31 +5,31 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/core/site"
"github.com/evcc-io/evcc/util"
"github.com/gorilla/mux"
)
type PlanResponse struct {
PlanId int `json:"planId"`
PlanTime time.Time `json:"planTime"`
Duration int64 `json:"duration"`
Precondition int64 `json:"precondition"`
Plan api.Rates `json:"plan"`
Power float64 `json:"power"`
PlanId int `json:"planId"`
PlanTime time.Time `json:"planTime"`
Duration int64 `json:"duration"`
Plan api.Rates `json:"plan"`
Power float64 `json:"power"`
}
type PlanPreviewResponse struct {
PlanTime time.Time `json:"planTime"`
Duration int64 `json:"duration"`
Precondition int64 `json:"precondition"`
Plan api.Rates `json:"plan"`
Power float64 `json:"power"`
PlanTime time.Time `json:"planTime"`
Duration int64 `json:"duration"`
Plan api.Rates `json:"plan"`
Power float64 `json:"power"`
// TODO remove
Continuous bool `json:"continuous"`
Precondition int64 `json:"precondition"`
}
// planHandler returns the current plan
@ -40,17 +40,16 @@ func planHandler(lp loadpoint.API) http.HandlerFunc {
id := lp.EffectivePlanId()
goal, _ := lp.GetPlanGoal()
precondition := lp.GetPlanPreCondDuration()
requiredDuration := lp.GetPlanRequiredDuration(goal, maxPower)
plan := lp.GetPlan(planTime, requiredDuration, precondition)
strategy := lp.EffectivePlanStrategy()
plan := lp.GetPlan(planTime, requiredDuration, strategy.Precondition, strategy.Continuous)
res := PlanResponse{
PlanId: id,
PlanTime: planTime,
Duration: int64(requiredDuration.Seconds()),
Precondition: int64(precondition.Seconds()),
Plan: plan,
Power: maxPower,
PlanId: id,
PlanTime: planTime,
Duration: int64(requiredDuration.Seconds()),
Plan: plan,
Power: maxPower,
}
jsonWrite(w, res)
@ -61,7 +60,6 @@ func planHandler(lp loadpoint.API) http.HandlerFunc {
func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
query := r.URL.Query()
planTime, err := time.ParseInLocation(time.RFC3339, vars["time"], nil)
if err != nil {
@ -75,12 +73,6 @@ func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
return
}
precondition, err := parseDuration(query.Get("precondition"))
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
switch typ := vars["type"]; typ {
case "soc":
if !lp.SocBasedPlanning() {
@ -99,66 +91,19 @@ func staticPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
maxPower := lp.EffectiveMaxPower()
requiredDuration := lp.GetPlanRequiredDuration(goal, maxPower)
plan := lp.GetPlan(planTime, requiredDuration, precondition)
strategy := lp.EffectivePlanStrategy()
plan := lp.GetPlan(planTime, requiredDuration, strategy.Precondition, strategy.Continuous)
res := PlanPreviewResponse{
PlanTime: planTime,
Duration: int64(requiredDuration.Seconds()),
Precondition: int64(precondition.Seconds()),
Plan: plan,
Power: maxPower,
}
PlanTime: planTime,
Duration: int64(requiredDuration.Seconds()),
Plan: plan,
Power: maxPower,
jsonWrite(w, res)
}
}
func repeatingPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
query := r.URL.Query()
hourMinute := vars["time"]
tz := vars["tz"]
var weekdays []int
for weekdayStr := range strings.SplitSeq(vars["weekdays"], ",") {
weekday, err := strconv.Atoi(weekdayStr)
if err != nil {
jsonError(w, http.StatusBadRequest, fmt.Errorf("invalid weekdays format"))
return
}
weekdays = append(weekdays, weekday)
}
soc, err := strconv.ParseFloat(vars["soc"], 64)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
planTime, err := util.GetNextOccurrence(weekdays, hourMinute, tz)
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
precondition, err := parseDuration(query.Get("precondition"))
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
maxPower := lp.EffectiveMaxPower()
requiredDuration := lp.GetPlanRequiredDuration(soc, maxPower)
plan := lp.GetPlan(planTime, requiredDuration, precondition)
res := PlanPreviewResponse{
PlanTime: planTime,
Duration: int64(requiredDuration.Seconds()),
Precondition: int64(precondition.Seconds()),
Plan: plan,
Power: maxPower,
// TODO remove
Continuous: strategy.Continuous,
Precondition: int64(strategy.Precondition.Seconds()),
}
jsonWrite(w, res)
@ -169,7 +114,6 @@ func repeatingPlanPreviewHandler(lp loadpoint.API) http.HandlerFunc {
func planEnergyHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
query := r.URL.Query()
ts, err := time.ParseInLocation(time.RFC3339, vars["time"], nil)
if err != nil {
@ -183,27 +127,19 @@ func planEnergyHandler(lp loadpoint.API) http.HandlerFunc {
return
}
precondition, err := parseDuration(query.Get("precondition"))
if err != nil {
if err := lp.SetPlanEnergy(ts, val); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if err := lp.SetPlanEnergy(ts, precondition, val); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
ts, precondition, energy := lp.GetPlanEnergy()
ts, energy := lp.GetPlanEnergy()
res := struct {
Energy float64 `json:"energy"`
Precondition int64 `json:"precondition"`
Time time.Time `json:"time"`
Energy float64 `json:"energy"`
Time time.Time `json:"time"`
}{
Energy: energy,
Precondition: int64(precondition.Seconds()),
Time: ts,
Energy: energy,
Time: ts,
}
jsonWrite(w, res)
@ -213,13 +149,12 @@ func planEnergyHandler(lp loadpoint.API) http.HandlerFunc {
// planRemoveHandler removes plan time
func planRemoveHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := lp.SetPlanEnergy(time.Time{}, 0, 0); err != nil {
if err := lp.SetPlanEnergy(time.Time{}, 0); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res := struct{}{}
jsonWrite(w, res)
jsonWrite(w, struct{}{})
}
}
@ -251,8 +186,7 @@ func vehicleSelectHandler(site site.API, lp loadpoint.API) http.HandlerFunc {
func vehicleRemoveHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lp.SetVehicle(nil)
res := struct{}{}
jsonWrite(w, res)
jsonWrite(w, struct{}{})
}
}
@ -260,7 +194,20 @@ func vehicleRemoveHandler(lp loadpoint.API) http.HandlerFunc {
func vehicleDetectHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
lp.StartVehicleDetection()
res := struct{}{}
jsonWrite(w, struct{}{})
}
}
// planStrategyHandler updates plan strategy for loadpoint
func planStrategyHandler(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := planStrategyHandlerSetter(r, lp.SetPlanStrategy); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res := planStrategyPayloadFromApi(lp.GetPlanStrategy())
jsonWrite(w, res)
}
}

View file

@ -73,7 +73,6 @@ func limitSocHandler(site site.API) http.HandlerFunc {
func planSocHandler(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
query := r.URL.Query()
v, err := site.Vehicles().ByName(vars["name"])
if err != nil {
@ -93,28 +92,52 @@ func planSocHandler(site site.API) http.HandlerFunc {
return
}
precondition, err := parseDuration(query.Get("precondition"))
if err := v.SetPlanSoc(ts, soc); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
ts, soc = v.GetPlanSoc()
res := struct {
Soc int `json:"soc"`
Time time.Time `json:"time"`
}{
Soc: soc,
Time: ts,
}
jsonWrite(w, res)
}
}
func planStrategyHandlerSetter(r *http.Request, set func(api.PlanStrategy) error) error {
var planStrategy planStrategyPayload
if err := json.NewDecoder(r.Body).Decode(&planStrategy); err != nil {
return err
}
return set(api.PlanStrategy{
Continuous: planStrategy.Continuous,
Precondition: time.Duration(planStrategy.Precondition) * time.Second,
})
}
// updatePlanStrategyHandler updates plan strategy
func updatePlanStrategyHandler(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
v, err := site.Vehicles().ByName(vars["name"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
if err := v.SetPlanSoc(ts, precondition, soc); err != nil {
if err := planStrategyHandlerSetter(r, v.SetPlanStrategy); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
ts, precondition, soc = v.GetPlanSoc()
res := struct {
Soc int `json:"soc"`
Precondition int64 `json:"precondition"`
Time time.Time `json:"time"`
}{
Soc: soc,
Precondition: int64(precondition.Seconds()),
Time: ts,
}
res := planStrategyPayloadFromApi(v.GetPlanStrategy())
jsonWrite(w, res)
}
@ -157,12 +180,11 @@ func planSocRemoveHandler(site site.API) http.HandlerFunc {
return
}
if err := v.SetPlanSoc(time.Time{}, 0, 0); err != nil {
if err := v.SetPlanSoc(time.Time{}, 0); err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}
res := struct{}{}
jsonWrite(w, res)
jsonWrite(w, struct{}{})
}
}

View file

@ -594,6 +594,21 @@
},
"type": "object"
},
"PlanStrategy": {
"description": "Charging plan strategy configuration",
"properties": {
"continuous": {
"description": "Force continuous planning",
"type": "boolean"
},
"precondition": {
"description": "Precondition duration in seconds",
"minimum": 0,
"type": "integer"
}
},
"type": "object"
},
"Power": {
"description": "Power in W",
"example": 2500,
@ -638,9 +653,6 @@
"description": "Set plan active.",
"type": "boolean"
},
"precondition": {
"$ref": "#/components/schemas/Precondition"
},
"soc": {
"$ref": "#/components/schemas/Soc"
},
@ -675,9 +687,6 @@
"energy": {
"$ref": "#/components/schemas/Energy"
},
"precondition": {
"$ref": "#/components/schemas/Precondition"
},
"time": {
"$ref": "#/components/schemas/Timestamp"
}
@ -689,9 +698,6 @@
"url": "https://docs.evcc.io/en/docs/features/plans#create-charging-plan"
},
"properties": {
"precondition": {
"$ref": "#/components/schemas/Precondition"
},
"soc": {
"$ref": "#/components/schemas/Soc"
},
@ -1423,9 +1429,6 @@
"properties": {
"planId": {
"$ref": "#/components/schemas/Id"
},
"precondition": {
"$ref": "#/components/schemas/Precondition"
}
},
"type": "object"
@ -1606,6 +1609,51 @@
]
}
},
"/loadpoints/{id}/plan/strategy": {
"post": {
"description": "Updates the charging plan strategy for the loadpoint.",
"externalDocs": {
"url": "https://docs.evcc.io/en/docs/features/plans"
},
"operationId": "setLoadpointPlanStrategy",
"parameters": [
{
"$ref": "#/components/parameters/id"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlanStrategy"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/PlanStrategy"
}
},
"type": "object"
}
}
},
"description": "Success"
}
},
"summary": "Set plan strategy",
"tags": [
"loadpoints"
]
}
},
"/loadpoints/{id}/priority/{priority}": {
"post": {
"description": "Set loadpoint priority.",
@ -2502,13 +2550,6 @@
},
{
"$ref": "#/components/parameters/timestamp"
},
{
"in": "query",
"name": "precondition",
"schema": {
"$ref": "#/components/schemas/Precondition"
}
}
],
"responses": {
@ -2518,7 +2559,15 @@
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/StaticSocPlan"
"properties": {
"soc": {
"$ref": "#/components/schemas/Soc"
},
"time": {
"$ref": "#/components/schemas/Timestamp"
}
},
"type": "object"
}
},
"type": "object"
@ -2533,6 +2582,51 @@
"vehicles"
]
}
},
"/vehicles/{name}/plan/strategy": {
"post": {
"description": "Updates the charging plan strategy for the vehicle.",
"externalDocs": {
"url": "https://docs.evcc.io/en/docs/features/plans"
},
"operationId": "setVehiclePlanStrategy",
"parameters": [
{
"$ref": "#/components/parameters/vehicleName"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlanStrategy"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/PlanStrategy"
}
},
"type": "object"
}
}
},
"description": "Success"
}
},
"summary": "Set plan strategy",
"tags": [
"vehicles"
]
}
}
},
"tags": [

View file

@ -730,6 +730,28 @@ call setLoadpointPhases {
}
```
## setLoadpointPlanStrategy
Updates the charging plan strategy for the loadpoint.
**Tags:** loadpoints
**Arguments:**
| Name | Type | Description |
|------|------|-------------|
| id | integer | Loadpoint index starting at 1 |
| requestBody | object | The JSON request body. |
**Example call:**
```json
call setLoadpointPlanStrategy {
"id": 123,
"requestBody": "..."
}
```
## setLoadpointPriority
Set loadpoint priority.
@ -1054,6 +1076,28 @@ call setVehicleMinSoc {
}
```
## setVehiclePlanStrategy
Updates the charging plan strategy for the vehicle.
**Tags:** vehicles
**Arguments:**
| Name | Type | Description |
|------|------|-------------|
| name | string | Vehicle name |
| requestBody | object | The JSON request body. |
**Example call:**
```json
call setVehiclePlanStrategy {
"name": "example",
"requestBody": "..."
}
```
## setVehicleSocLimit
Charging will stop when this SoC is reached.
@ -1087,7 +1131,6 @@ Create charging plan with fixed time and SoC target.
| Name | Type | Description |
|------|------|-------------|
| name | string | Vehicle name |
| precondition | integer | Late charging duration in seconds. |
| soc | number | SOC in % |
| timestamp | string | Timestamp in RFC3339 format |
@ -1096,7 +1139,6 @@ Create charging plan with fixed time and SoC target.
```json
call setVehicleSocPlan {
"name": "example",
"precondition": 123,
"soc": 123.45,
"timestamp": "example"
}

View file

@ -1,7 +1,6 @@
package server
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
@ -252,18 +251,8 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
{"smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))},
{"smartFeedInPriorityLimit", floatPtrSetter(pass(lp.SetSmartFeedInPriorityLimit))},
{"batteryBoost", boolSetter(lp.SetBatteryBoost)},
{"planEnergy", func(payload string) error {
var plan struct {
Time time.Time `json:"time"`
Precondition int64 `json:"precondition"`
Value float64 `json:"value"`
}
err := json.Unmarshal([]byte(payload), &plan)
if err == nil {
err = lp.SetPlanEnergy(plan.Time, time.Duration(plan.Precondition)*time.Second, plan.Value)
}
return err
}},
{"planStrategy", planStrategySetter(lp.SetPlanStrategy)},
{"planEnergy", planGoalSetter(lp.SetPlanEnergy)},
{"vehicle", func(payload string) error {
// https://github.com/evcc-io/evcc/issues/11184 empty payload is swallowed by listener
if isEmpty(payload) {
@ -289,18 +278,8 @@ func (m *MQTT) listenVehicleSetters(topic string, v vehicle.API) error {
for _, s := range []setter{
{"limitSoc", intSetter(pass(v.SetLimitSoc))},
{"minSoc", intSetter(pass(v.SetMinSoc))},
{"planSoc", func(payload string) error {
var plan struct {
Time time.Time `json:"time"`
Precondition int64 `json:"precondition"`
Value int `json:"value"`
}
err := json.Unmarshal([]byte(payload), &plan)
if err == nil {
err = v.SetPlanSoc(plan.Time, time.Duration(plan.Precondition)*time.Second, plan.Value)
}
return err
}},
{"planStrategy", planStrategySetter(v.SetPlanStrategy)},
{"planSoc", planGoalSetter(v.SetPlanSoc)},
} {
if err := m.Handler.ListenSetter(topic+"/"+s.topic, s.fun); err != nil {
return err

View file

@ -1,10 +1,12 @@
package server
import (
"encoding/json"
"slices"
"strconv"
"time"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/util"
"github.com/spf13/cast"
)
@ -63,3 +65,28 @@ func boolSetter(set func(bool) error) func(string) error {
func durationSetter(set func(time.Duration) error) func(string) error {
return setterFunc(util.ParseDuration, set)
}
func planStrategySetter(set func(api.PlanStrategy) error) func(string) error {
return func(payload string) error {
var planStrategy planStrategyPayload
if err := json.Unmarshal([]byte(payload), &planStrategy); err != nil {
return err
}
return set(api.PlanStrategy{
Continuous: planStrategy.Continuous,
Precondition: time.Duration(planStrategy.Precondition) * time.Second,
})
}
}
func planGoalSetter[T any](set func(time.Time, T) error) func(string) error {
return func(payload string) error {
var plan planGoal[T]
if err := json.Unmarshal([]byte(payload), &plan); err != nil {
return err
}
return set(plan.Time, plan.Value)
}
}

View file

@ -398,8 +398,6 @@ paths:
properties:
planId:
$ref: "#/components/schemas/Id"
precondition:
$ref: "#/components/schemas/Precondition"
- $ref: "#/components/schemas/PlanRates"
/loadpoints/{id}/plan/energy:
delete:
@ -488,6 +486,33 @@ paths:
responses:
"200":
$ref: "#/components/responses/PlanRatesResult"
/loadpoints/{id}/plan/strategy:
post:
operationId: setLoadpointPlanStrategy
summary: Set plan strategy
description: "Updates the charging plan strategy for the loadpoint."
externalDocs:
url: https://docs.evcc.io/en/docs/features/plans
tags:
- loadpoints
parameters:
- $ref: "#/components/parameters/id"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PlanStrategy"
responses:
200:
description: Success
content:
application/json:
schema:
type: object
properties:
result:
$ref: "#/components/schemas/PlanStrategy"
/loadpoints/{id}/priority/{priority}:
post:
operationId: setLoadpointPriority
@ -1080,10 +1105,6 @@ paths:
- $ref: "#/components/parameters/vehicleName"
- $ref: "#/components/parameters/soc"
- $ref: "#/components/parameters/timestamp"
- in: query
name: precondition
schema:
$ref: "#/components/schemas/Precondition"
responses:
"200":
description: Success
@ -1093,7 +1114,39 @@ paths:
type: object
properties:
result:
$ref: "#/components/schemas/StaticSocPlan"
type: object
properties:
soc:
$ref: "#/components/schemas/Soc"
time:
$ref: "#/components/schemas/Timestamp"
/vehicles/{name}/plan/strategy:
post:
operationId: setVehiclePlanStrategy
summary: Set plan strategy
description: "Updates the charging plan strategy for the vehicle."
externalDocs:
url: https://docs.evcc.io/en/docs/features/plans
tags:
- vehicles
parameters:
- $ref: "#/components/parameters/vehicleName"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/PlanStrategy"
responses:
200:
description: Success
content:
application/json:
schema:
type: object
properties:
result:
$ref: "#/components/schemas/PlanStrategy"
components:
schemas:
BatteryMode:
@ -1304,6 +1357,17 @@ components:
type: array
items:
$ref: "#/components/schemas/Rate"
PlanStrategy:
description: Charging plan strategy configuration
type: object
properties:
continuous:
description: "Force continuous planning"
type: boolean
precondition:
description: "Precondition duration in seconds"
type: integer
minimum: 0
RepeatingPlan:
externalDocs:
url: https://docs.evcc.io/en/docs/features/plans#repeating-plans
@ -1312,8 +1376,6 @@ components:
active:
description: "Set plan active."
type: boolean
precondition:
$ref: "#/components/schemas/Precondition"
soc:
$ref: "#/components/schemas/Soc"
time:
@ -1338,8 +1400,6 @@ components:
properties:
energy:
$ref: "#/components/schemas/Energy"
precondition:
$ref: "#/components/schemas/Precondition"
time:
$ref: "#/components/schemas/Timestamp"
StaticSocPlan:
@ -1347,8 +1407,6 @@ components:
url: https://docs.evcc.io/en/docs/features/plans#create-charging-plan
type: object
properties:
precondition:
$ref: "#/components/schemas/Precondition"
soc:
$ref: "#/components/schemas/Soc"
time:

24
server/types.go Normal file
View file

@ -0,0 +1,24 @@
package server
import (
"time"
"github.com/evcc-io/evcc/api"
)
type planStrategyPayload struct {
Continuous bool `json:"continuous"`
Precondition int64 `json:"precondition"`
}
func planStrategyPayloadFromApi(ps api.PlanStrategy) planStrategyPayload {
return planStrategyPayload{
Continuous: ps.Continuous,
Precondition: int64(ps.Precondition.Seconds()),
}
}
type planGoal[T any] struct {
Time time.Time `json:"time"`
Value T `json:"value"`
}