From 3b5ec989a29816e1a2eac36beeee64776ce46555 Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 27 Sep 2020 10:06:24 +0200 Subject: [PATCH] Follow SMA Application Note EVCharger (#356) --- core/loadpoint.go | 3 ++ core/wrapper/socestimator.go | 14 ++++++++- hems/semp/messages.go | 52 +++++++++++-------------------- hems/semp/semp.go | 59 +++++++++++++++++------------------- 4 files changed, 61 insertions(+), 67 deletions(-) diff --git a/core/loadpoint.go b/core/loadpoint.go index 3c17d08e7..161d1eb4d 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -597,6 +597,9 @@ func (lp *LoadPoint) publishSoC() { } lp.publish("chargeEstimate", chargeEstimate) + chargeRemainingEnergy := 1e3 * lp.socEstimator.RemainingChargeEnergy(lp.TargetSoC) + lp.publish("chargeRemainingEnergy", chargeRemainingEnergy) + return } diff --git a/core/wrapper/socestimator.go b/core/wrapper/socestimator.go index 62a8c8c01..d06d82d80 100644 --- a/core/wrapper/socestimator.go +++ b/core/wrapper/socestimator.go @@ -74,6 +74,18 @@ func (s *SocEstimator) RemainingChargeDuration(chargePower float64, targetSoC in return -1 } +// RemainingChargeEnergy returns the remaining charge energy in kWh +func (s *SocEstimator) RemainingChargeEnergy(targetSoC int) float64 { + percentRemaining := float64(targetSoC) - s.socCharge + if percentRemaining <= 0 { + return 0 + } + + // estimate remaining energy + whRemaining := percentRemaining / 100 * s.virtualCapacity + return whRemaining / 1e3 +} + // SoC implements Vehicle.ChargeState with addition of given charged energy func (s *SocEstimator) SoC(chargedEnergy float64) (float64, error) { f, err := s.vehicle.ChargeState() @@ -99,7 +111,7 @@ func (s *SocEstimator) SoC(chargedEnergy float64) (float64, error) { if socDelta > 1 && energyDelta > 0 && s.prevSoC > 0 { s.energyPerSocStep = energyDelta / socDelta s.virtualCapacity = s.energyPerSocStep * 100 - s.log.TRACE.Printf("soc gradient updated: energyPerSocStep: %0.0fWh, virtualCapacity: %0.1fkWh", s.energyPerSocStep, s.virtualCapacity) + s.log.TRACE.Printf("soc gradient updated: energyPerSocStep: %0.0fWh, virtualCapacity: %0.0fWh", s.energyPerSocStep, s.virtualCapacity) } // sample charged energy at soc change, reset energy delta diff --git a/hems/semp/messages.go b/hems/semp/messages.go index a04c733e8..be019a4ed 100644 --- a/hems/semp/messages.go +++ b/hems/semp/messages.go @@ -76,32 +76,16 @@ type Characteristics struct { MinOffTime int `xml:",omitempty"` } -type Capabilities struct { - CurrentPower CurrentPower - Timestamps Timestamps - Interruptions Interruptions - Requests Requests -} - const ( MethodMeasurement = "Measurement" MethodEstimation = "Estimation" ) -type CurrentPower struct { - Method string -} - -type Timestamps struct { - AbsoluteTimestamps bool -} - -type Interruptions struct { - InterruptionsAllowed bool -} - -type Requests struct { - OptionalEnergy bool +type Capabilities struct { + CurrentPowerMethod string `xml:"CurrentPower>Method"` + AbsoluteTimestamps bool `xml:"Timestamps>AbsoluteTimestamps"` + InterruptionsAllowed bool `xml:"Interruptions>InterruptionsAllowed"` + OptionalEnergy bool `xml:"Requests>OptionalEnergy"` } const ( @@ -113,11 +97,7 @@ type DeviceStatus struct { DeviceID string `xml:"DeviceId"` EMSignalsAccepted bool Status string - PowerConsumption PowerConsumption -} - -type PowerConsumption struct { - PowerInfo PowerInfo + PowerInfo PowerInfo `xml:"PowerConsumption>PowerInfo"` } type PowerInfo struct { @@ -127,28 +107,30 @@ type PowerInfo struct { } type PlanningRequest struct { - Timeframe Timeframe + Timeframe []Timeframe } type Timeframe struct { DeviceID string `xml:"DeviceId"` EarliestStart int LatestEnd int - MinRunningTime int - MaxRunningTime int `xml:",omitempty"` + MinRunningTime *int `xml:",omitempty"` + MaxRunningTime *int `xml:",omitempty"` + MinEnergy *int `xml:",omitempty"` // AN EVCharger + MaxEnergy *int `xml:",omitempty"` // AN EVCharger } // EM2Device is the EM to device message type EM2Device struct { - Xmlns string `xml:"xmlns,attr"` - DeviceControl []DeviceControl `xml:",omitempty"` - PlanningRequest []PlanningRequest `xml:",omitempty"` + Xmlns string `xml:"xmlns,attr"` + DeviceControl []DeviceControl `xml:",omitempty"` } type DeviceControl struct { - DeviceID string `xml:"DeviceId"` - On bool - Timestamp int + DeviceID string `xml:"DeviceId"` + On bool + RecommendedPowerConsumption int // AN EVCharger + Timestamp int } // Device2EMMsg is the XML message container diff --git a/hems/semp/semp.go b/hems/semp/semp.go index 117d5540c..a50cccdbf 100644 --- a/hems/semp/semp.go +++ b/hems/semp/semp.go @@ -286,7 +286,7 @@ func (s *SEMP) devicePlanningQuery(w http.ResponseWriter, r *http.Request) { continue } - if pr := s.planningRequest(id, lp); pr.Timeframe.DeviceID != "" { + if pr := s.planningRequest(id, lp); len(pr.Timeframe) > 0 { msg.PlanningRequest = append(msg.PlanningRequest, pr) } } @@ -331,15 +331,9 @@ func (s *SEMP) deviceInfo(id int, lp *core.LoadPoint) DeviceInfo { DeviceVendor: "github.com/andig/evcc", }, Capabilities: Capabilities{ - CurrentPower: CurrentPower{ - Method: method, - }, - Interruptions: Interruptions{ - InterruptionsAllowed: true, - }, - Requests: Requests{ - OptionalEnergy: true, - }, + CurrentPowerMethod: method, + InterruptionsAllowed: true, + OptionalEnergy: true, }, Characteristics: Characteristics{ MinPowerConsumption: 230 * int(lp.MinCurrent), @@ -374,11 +368,9 @@ func (s *SEMP) deviceStatus(id int, lp *core.LoadPoint) DeviceStatus { res := DeviceStatus{ DeviceID: s.deviceID(id), EMSignalsAccepted: true, - PowerConsumption: PowerConsumption{ - PowerInfo: PowerInfo{ - AveragePower: int(chargePower), - AveragingInterval: 60, - }, + PowerInfo: PowerInfo{ + AveragePower: int(chargePower), + AveragingInterval: 60, }, Status: status, } @@ -410,30 +402,35 @@ func (s *SEMP) planningRequest(id int, lp *core.LoadPoint) (res PlanningRequest) chargeEstimate = chargeEstimateP.Val.(time.Duration) } - maxDuration := int(chargeEstimate / time.Second) - if chargeEstimate <= 0 { - maxDuration = 10 * 60 // 10min + latestEnd := int(chargeEstimate / time.Second) + if mode == api.ModePV || latestEnd <= 0 { + latestEnd = 24 * 3600 } - minDuration := maxDuration - if mode == api.ModePV { - minDuration = 0 + var maxEnergy int + if chargeRemainingEnergyP, err := s.cacheGet(id, "chargeRemainingEnergy"); err == nil { + maxEnergy = int(chargeRemainingEnergyP.Val.(float64)) } - latestEnd := maxDuration + // add 1kWh in case we're charging but battery claims full + if maxEnergy == 0 { + maxEnergy = 1e3 // 1kWh + } + + minEnergy := maxEnergy if mode == api.ModePV { - latestEnd = 2 * maxDuration + minEnergy = 0 } if charging { res = PlanningRequest{ - Timeframe: Timeframe{ - DeviceID: s.deviceID(id), - EarliestStart: 0, - LatestEnd: latestEnd, - MinRunningTime: minDuration, - MaxRunningTime: maxDuration, - }, + Timeframe: []Timeframe{Timeframe{ + DeviceID: s.deviceID(id), + EarliestStart: 0, + LatestEnd: latestEnd, + MinEnergy: &minEnergy, + MaxEnergy: &maxEnergy, + }}, } } @@ -442,7 +439,7 @@ func (s *SEMP) planningRequest(id int, lp *core.LoadPoint) (res PlanningRequest) func (s *SEMP) allPlanningRequest() (res []PlanningRequest) { for id, lp := range s.site.LoadPoints() { - if pr := s.planningRequest(id, lp); pr.Timeframe.DeviceID != "" { + if pr := s.planningRequest(id, lp); len(pr.Timeframe) > 0 { res = append(res, pr) } }