Planner: add continuous strategy (BC) (#24423)
This commit is contained in:
parent
f93b15a8c1
commit
291898fea9
68 changed files with 2245 additions and 950 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}{})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
24
server/types.go
Normal 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"`
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue