Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add battery grid charging #14855

Merged
merged 35 commits into from
Aug 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f69a4eb
Add grid charge limit
andig Jul 12, 2024
2d3c022
Update server/http.go
andig Jul 12, 2024
e95305f
Apply suggestions from code review
andig Jul 12, 2024
2dff437
Refactor to reduce warnings
andig Jul 13, 2024
87a6174
Simplify
andig Jul 13, 2024
3305042
Optimize
andig Jul 13, 2024
74b5dcb
Minimize diff
andig Jul 13, 2024
1b61696
wip
andig Jul 13, 2024
8f76c6a
Add pointer handler
andig Jul 13, 2024
12bc851
wip
andig Jul 13, 2024
4882706
wip
andig Jul 13, 2024
df6624d
Update core/site_api.go
andig Jul 13, 2024
fe31d88
Fix printing
andig Jul 14, 2024
58a895d
Refactor updating battery mode
andig Jul 14, 2024
95dedf3
wip
andig Jul 14, 2024
d2a5002
wip
andig Jul 14, 2024
adf5be4
wip
andig Jul 14, 2024
588f400
wip
andig Jul 14, 2024
f64eaf5
Lazy- update battery mode
andig Jul 14, 2024
1b45c79
Fix tests
andig Jul 14, 2024
47830ab
Merge branch 'master' into feat/grid-charge
andig Jul 15, 2024
2357955
Fix pointer comparison
andig Jul 21, 2024
14c22f6
wip
andig Jul 21, 2024
6300be7
wip
andig Jul 21, 2024
6f07007
add ui, rename to batterygridcharge
naltatis Jul 21, 2024
80a8163
Merge branch 'master' into feat/grid-charge
naltatis Jul 21, 2024
ec7cc82
Fix tests
andig Jul 22, 2024
a67d3d6
Rename setting
andig Jul 22, 2024
20a4457
Merge branch 'master' into feat/grid-charge
naltatis Jul 22, 2024
807f3c4
Merge branch 'master' into feat/grid-charge
naltatis Jul 31, 2024
0c23185
layout update
naltatis Jul 31, 2024
e4972a0
hide tabs when no grid charge possible
naltatis Jul 31, 2024
b68f093
,
naltatis Jul 31, 2024
9c11892
add e2e tests
naltatis Jul 31, 2024
1539c16
Merge branch 'master' into feat/grid-charge
naltatis Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/keys/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const (
BufferStartSoc = "bufferStartSoc"
MaxGridSupplyWhileBatteryCharging = "maxGridSupplyWhileBatteryCharging"

// battery charging
GridChargeLimit = "gridChargeLimit"
GridChargeActive = "gridChargeActive"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich bin noch nicht überzeugt vom Namen. Hier gehts ja "nur" ums Batterie-Laden. Das Smart Cost Limit am LP ist ja auch ein Grid-Charging, aber halt fürs Fahrzeug. Für Symmetrie würd ich batteryCostLimit vorschlagen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich find den eigentlich gut. Energie von außen ist günstig und das Feature heisst landläufig Netzladen.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ich hätt gerne die Batterie da mit drin. Gerade weil es ja "nur" die Batterie betrifft. Sonst wird nicht klar, dass es hier nicht um unser "normales laden" geht.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wie wär's mit batteryGridChargeLimit?


// battery status
Battery = "battery"
BatteryEnergy = "batteryEnergy"
Expand Down
22 changes: 14 additions & 8 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,12 @@ type Loadpoint struct {
MinCurrent_ float64 `mapstructure:"minCurrent"`
MaxCurrent_ float64 `mapstructure:"maxCurrent"`

minCurrent float64 // PV mode: start current Min+PV mode: min current
maxCurrent float64 // Max allowed current. Physically ensured by the charger
configuredPhases int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit float64 // always charge if cost is below this value
minCurrent float64 // PV mode: start current Min+PV mode: min current
maxCurrent float64 // Max allowed current. Physically ensured by the charger
configuredPhases int // Charger configured phase mode 0/1/3
limitSoc int // Session limit for soc
limitEnergy float64 // Session limit for energy
smartCostLimit *float64 // always charge if cost is below this value

mode api.ChargeMode
enabled bool // Charger enabled state
Expand Down Expand Up @@ -335,9 +335,15 @@ func (lp *Loadpoint) restoreSettings() {
if v, err := lp.settings.Float(keys.LimitEnergy); err == nil && v > 0 {
lp.setLimitEnergy(v)
}
if v, err := lp.settings.Float(keys.SmartCostLimit); err == nil {
lp.SetSmartCostLimit(v)

if v, err := lp.settings.String(keys.SmartCostLimit); err == nil {
if v == "" {
lp.SetSmartCostLimit(nil)
} else if v, err := lp.settings.Float(keys.SmartCostLimit); err == nil {
lp.SetSmartCostLimit(&v)
}
}

t, err1 := lp.settings.Time(keys.PlanTime)
v, err2 := lp.settings.Float(keys.PlanEnergy)
if err1 == nil && err2 == nil {
Expand Down
4 changes: 2 additions & 2 deletions core/loadpoint/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ type API interface {
//

// GetSmartChargingActive determines if smart charging is active
GetSmartCostLimit() float64
GetSmartCostLimit() *float64
// SetSmartCostLimit sets the smart cost limit
SetSmartCostLimit(limit float64)
SetSmartCostLimit(limit *float64)

//
// power and energy
Expand Down
6 changes: 3 additions & 3 deletions core/loadpoint/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions core/loadpoint_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/evcc-io/evcc/core/keys"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/core/wrapper"
"github.com/evcc-io/evcc/server/db/settings"
andig marked this conversation as resolved.
Show resolved Hide resolved
)

var _ loadpoint.API = (*Loadpoint)(nil)
Expand Down Expand Up @@ -489,23 +490,29 @@ func (lp *Loadpoint) StartVehicleDetection() {
}

// GetSmartCostLimit gets the smart cost limit
func (lp *Loadpoint) GetSmartCostLimit() float64 {
func (lp *Loadpoint) GetSmartCostLimit() *float64 {
lp.RLock()
defer lp.RUnlock()
return lp.smartCostLimit
}

// SetSmartCostLimit sets the smart cost limit
func (lp *Loadpoint) SetSmartCostLimit(val float64) {
func (lp *Loadpoint) SetSmartCostLimit(val *float64) {
lp.Lock()
defer lp.Unlock()

lp.log.DEBUG.Println("set smart cost limit:", val)

if lp.smartCostLimit != val {
lp.smartCostLimit = val
lp.settings.SetFloat(keys.SmartCostLimit, lp.smartCostLimit)
lp.publish(keys.SmartCostLimit, lp.smartCostLimit)

if val == nil {
settings.SetString(keys.SmartCostLimit, "")
andig marked this conversation as resolved.
Show resolved Hide resolved
lp.publish(keys.SmartCostLimit, "")
} else {
settings.SetFloat(keys.SmartCostLimit, *val)
andig marked this conversation as resolved.
Show resolved Hide resolved
lp.publish(keys.SmartCostLimit, *val)
}
}
}

Expand Down
30 changes: 25 additions & 5 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@
auxMeters []api.Meter // Auxiliary meters

// battery settings
prioritySoc float64 // prefer battery up to this Soc
bufferSoc float64 // continue charging on battery above this Soc
bufferStartSoc float64 // start charging on battery above this Soc
batteryDischargeControl bool // prevent battery discharge for fast and planned charging
prioritySoc float64 // prefer battery up to this Soc
bufferSoc float64 // continue charging on battery above this Soc
bufferStartSoc float64 // start charging on battery above this Soc
batteryDischargeControl bool // prevent battery discharge for fast and planned charging
gridChargeLimit *float64 // grid charging limit

loadpoints []*Loadpoint // Loadpoints
tariffs *tariff.Tariffs // Tariffs
Expand Down Expand Up @@ -295,6 +296,15 @@
return err
}
}

if v, err := settings.String(keys.GridChargeLimit); err == nil {
if v == "" {
site.SetGridChargeLimit(nil)
} else if v, err := settings.Float(keys.GridChargeLimit); err == nil {
site.SetGridChargeLimit(&v)
}
}

return nil
}

Expand Down Expand Up @@ -792,8 +802,18 @@
var smartCostActive bool
if rate, err := site.plannerRate(); err == nil {
smartCostActive = site.smartCostActive(lp, rate)

gridChargeActive := site.gridChargeActive(rate)
site.publish(keys.GridChargeActive, gridChargeActive)

if gridChargeActive {
site.SetBatteryMode(api.BatteryCharge)
} else if mode := site.GetBatteryMode(); mode != api.BatteryNormal {
site.SetBatteryMode(api.BatteryNormal)
}

} else {

Check failure on line 815 in core/site.go

View workflow job for this annotation

GitHub Actions / Lint

unnecessary trailing newline (whitespace)
site.log.WARN.Println("smartCostActive:", err)
site.log.WARN.Println("smartCost:", err)
}

var smartCostNextStart time.Time
Expand Down
5 changes: 5 additions & 0 deletions core/site/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type API interface {
GetMaxGridSupplyWhileBatteryCharging() float64
SetMaxGridSupplyWhileBatteryCharging(float64) error

// GetGridChargeLimit get the grid charge limit
GetGridChargeLimit() *float64
// SetGridChargeLimit sets the grid charge limit
SetGridChargeLimit(limit *float64)

//
// power and energy
//
Expand Down
25 changes: 25 additions & 0 deletions core/site_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,28 @@ func (site *Site) SetBatteryDischargeControl(val bool) error {

return nil
}

func (site *Site) GetGridChargeLimit() *float64 {
site.RLock()
defer site.RUnlock()
return site.gridChargeLimit
}

func (site *Site) SetGridChargeLimit(val *float64) {
site.log.DEBUG.Println("set smart cost limit:", val)
andig marked this conversation as resolved.
Show resolved Hide resolved

site.Lock()
defer site.Unlock()

if site.gridChargeLimit != val {
site.gridChargeLimit = val

if val == nil {
settings.SetString(keys.GridChargeLimit, "")
site.publish(keys.GridChargeLimit, "")
} else {
settings.SetFloat(keys.GridChargeLimit, *val)
site.publish(keys.GridChargeLimit, *val)
}
}
}
11 changes: 8 additions & 3 deletions core/site_battery.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,25 +80,30 @@ func (site *Site) plannerRate() (*api.Rate, error) {

func (site *Site) smartCostActive(lp loadpoint.API, rate *api.Rate) bool {
limit := lp.GetSmartCostLimit()
return limit != 0 && rate != nil && rate.Price <= limit
return limit != nil && rate != nil && rate.Price <= *limit
}

func (site *Site) smartCostNextStart(lp loadpoint.API, rate api.Rates) time.Time {
limit := lp.GetSmartCostLimit()
if limit == 0 || rate == nil {
if limit == nil || rate == nil {
return time.Time{}
}

now := time.Now()
for _, slot := range rate {
if slot.Start.After(now) && slot.Price <= limit {
if slot.Start.After(now) && slot.Price <= *limit {
return slot.Start
}
}

return time.Time{}
}

func (site *Site) gridChargeActive(rate *api.Rate) bool {
limit := site.GetGridChargeLimit()
return limit != nil && rate != nil && rate.Price <= *limit
}

func (site *Site) updateBatteryMode() {
mode := api.BatteryNormal

Expand Down
8 changes: 6 additions & 2 deletions server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"maxgridsupply": {"POST", "/maxgridsupply/{value:[0-9.]+}", floatHandler(site.SetMaxGridSupplyWhileBatteryCharging, site.GetMaxGridSupplyWhileBatteryCharging)},
"prioritysoc": {"POST", "/prioritysoc/{value:[0-9.]+}", floatHandler(site.SetPrioritySoc, site.GetPrioritySoc)},
"residualpower": {"POST", "/residualpower/{value:-?[0-9.]+}", floatHandler(site.SetResidualPower, site.GetResidualPower)},
"smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(site)},
"smartcost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateGlobalSmartCostLimit(site)},
"smartcostdelete": {"DELETE", "/smartcostlimit/{value:-?[0-9.]+}", updateGlobalSmartCostLimit(site)},
andig marked this conversation as resolved.
Show resolved Hide resolved
"gridcharge": {"POST", "/gridchargelimit/{value:-?[0-9.]+}", updateGridChargeLimit(site)},
"gridchargedelete": {"DELETE", "/gridchargelimit", updateGridChargeLimit(site)},
"tariff": {"GET", "/tariff/{tariff:[a-z]+}", tariffHandler(site)},
"sessions": {"GET", "/sessions", sessionHandler},
"updatesession": {"PUT", "/session/{id:[0-9]+}", updateSessionHandler},
Expand Down Expand Up @@ -173,7 +176,8 @@ func (s *HTTPd) RegisterSiteHandlers(site site.API, valueChan chan<- util.Param)
"remotedemand": {"POST", "/remotedemand/{demand:[a-z]+}/{source:[0-9a-zA-Z_-]+}", remoteDemandHandler(lp)},
"enableThreshold": {"POST", "/enable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetEnableThreshold), lp.GetEnableThreshold)},
"disableThreshold": {"POST", "/disable/threshold/{value:-?[0-9.]+}", floatHandler(pass(lp.SetDisableThreshold), lp.GetDisableThreshold)},
"smartCostLimit": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", floatHandler(pass(lp.SetSmartCostLimit), lp.GetSmartCostLimit)},
"smartCost": {"POST", "/smartcostlimit/{value:-?[0-9.]+}", updateSmartCostLimit(lp)},
"smartCostDelete": {"DELETE", "/smartcostlimit", updateSmartCostLimit(lp)},
// "priority": {"POST", "/priority/{value:[0-9.]+}", floatHandler(pass(lp.SetPriority), lp.GetPriority)},
}

Expand Down
23 changes: 23 additions & 0 deletions server/http_loadpoint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,26 @@ func vehicleDetectHandler(lp loadpoint.API) http.HandlerFunc {
jsonResult(w, res)
}
}

// updateSmartCostLimit sets the smart cost limit
func updateSmartCostLimit(lp loadpoint.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var valP *float64

if r.Method != http.MethodDelete {
vars := mux.Vars(r)

val, err := parseFloat(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}

valP = &val
}

lp.SetSmartCostLimit(valP)

jsonResult(w, valP)
}
}
47 changes: 38 additions & 9 deletions server/http_site_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,22 +148,51 @@ func boolGetHandler(get func() bool) http.HandlerFunc {
}
}

// updateSmartCostLimit sets the smart cost limit globally
func updateSmartCostLimit(site site.API) http.HandlerFunc {
// updateGlobalSmartCostLimit sets the smart cost limit globally
func updateGlobalSmartCostLimit(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
var valP *float64

val, err := parseFloat(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
if r.Method != http.MethodDelete {
vars := mux.Vars(r)

val, err := parseFloat(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}

valP = &val
}

for _, lp := range site.Loadpoints() {
lp.SetSmartCostLimit(val)
lp.SetSmartCostLimit(valP)
}

jsonResult(w, valP)
}
}

// updateGridChargeLimit sets the smart cost limit globally
func updateGridChargeLimit(site site.API) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var valP *float64

if r.Method != http.MethodDelete {
vars := mux.Vars(r)

val, err := parseFloat(vars["value"])
if err != nil {
jsonError(w, http.StatusBadRequest, err)
return
}

valP = &val
}

jsonResult(w, val)
site.SetGridChargeLimit(valP)

jsonResult(w, valP)
}
}

Expand Down
10 changes: 6 additions & 4 deletions server/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,14 @@ func (m *MQTT) listenSiteSetters(topic string, site site.API) error {
{"/batteryDischargeControl", boolSetter(site.SetBatteryDischargeControl)},
{"/prioritySoc", floatSetter(site.SetPrioritySoc)},
{"/residualPower", floatSetter(site.SetResidualPower)},
{"/smartCostLimit", floatSetter(func(limit float64) error {
{"/smartCostLimit", floatPtrSetter(pass(func(limit *float64) {
for _, lp := range site.Loadpoints() {
lp.SetSmartCostLimit(limit)
}
return nil
})},
}))},
{"/gridChargeLimit", floatPtrSetter(pass(func(limit *float64) {
site.SetGridChargeLimit(limit)
}))},
} {
if err := m.Handler.ListenSetter(topic+s.topic, s.fun); err != nil {
return err
Expand All @@ -203,7 +205,7 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
{"/limitEnergy", floatSetter(pass(lp.SetLimitEnergy))},
{"/enableThreshold", floatSetter(pass(lp.SetEnableThreshold))},
{"/disableThreshold", floatSetter(pass(lp.SetDisableThreshold))},
{"/smartCostLimit", floatSetter(pass(lp.SetSmartCostLimit))},
{"/smartCostLimit", floatPtrSetter(pass(lp.SetSmartCostLimit))},
{"/planEnergy", func(payload string) error {
var plan struct {
Time time.Time `json:"time"`
Expand Down
Loading
Loading