Allow giving home battery charge priority up to defined soc (#369)

Add batteryPriority setting
Add battery soc reading to PowerWall
Add battery soc reading to generic meter
Publish batterySoC vaue when read
This commit is contained in:
andig 2020-10-03 15:45:40 +02:00 • committed by GitHub
parent 8a5bd8e119
commit d3ee35272d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 24 deletions

View file

@ -52,6 +52,11 @@ type MeterCurrent interface {
Currents() (float64, float64, float64, error)
}
// Battery is able to provide battery SoC in %
type Battery interface {
SoC() (float64, error)
}
// Charger is able to provide current charging status and to enable/disabler charging
type Charger interface {
Status() (ChargeStatus, error)

View file

@ -31,6 +31,7 @@ type Site struct {
Voltage float64 `mapstructure:"voltage"` // Operating voltage. 230V for Germany.
ResidualPower float64 `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
Meters MetersConfig // Meter references
PrioritySoC float64 `mapstructure:"prioritySoC"` // prefer battery up to this SoC
// meters
gridMeter api.Meter // Grid usage meter
@ -311,7 +312,23 @@ func (site *Site) sitePower() (float64, error) {
return 0, err
}
sitePower := sitePower(site.gridPower, site.batteryPower, site.ResidualPower)
// honour battery priority
batteryPower := site.batteryPower
if battery, ok := site.batteryMeter.(api.Battery); ok && site.PrioritySoC > 0 {
soc, err := battery.SoC()
if err != nil {
site.log.ERROR.Printf("updating battery soc: %v", err)
} else {
site.publish("batterySoC", soc)
if soc < site.PrioritySoC {
site.log.DEBUG.Printf("giving priority to battery at soc: %.0f", soc)
batteryPower = 0
}
}
}
sitePower := sitePower(site.gridPower, batteryPower, site.ResidualPower)
site.log.DEBUG.Printf("site power: %.0fW", sitePower)
return sitePower, nil

View file

@ -57,6 +57,7 @@ site:
grid: grid # grid meter
pv: pv # pv meter
battery: battery # battery meter
prioritySoC: 60 # give home battery priority up to this soc (0 to disable)
# loadpoint describes the charger, charge meter and connected vehicle
loadpoints:

View file

@ -13,13 +13,14 @@ func init() {
registry.Add("default", NewConfigurableFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -p meter -f decorateMeter -b api.Meter -o meter_decorators -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)"
//go:generate go run ../cmd/tools/decorate.go -p meter -f decorateMeter -b api.Meter -o meter_decorators -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.Battery,SoC,func() (float64, error)"
// NewConfigurableFromConfig creates api.Meter from config
func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
Power provider.Config
Energy *provider.Config // optional
SoC *provider.Config // optional
Currents []provider.Config // optional
}{}
@ -69,7 +70,17 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error)
currents = m.currents
}
res := decorateMeter(m, totalEnergy, currents)
// decorate Meter with BatterySoC
var batterySoC func() (float64, error)
if cc.SoC != nil {
m.batterySoCG, err = provider.NewFloatGetterFromConfig(*cc.SoC)
if err != nil {
return nil, err
}
batterySoC = m.batterySoC
}
res := decorateMeter(m, totalEnergy, currents, batterySoC)
return res, nil
}
@ -87,6 +98,7 @@ type Meter struct {
currentPowerG func() (float64, error)
totalEnergyG func() (float64, error)
currentsG []func() (float64, error)
batterySoCG func() (float64, error)
}
// CurrentPower implements the Meter.CurrentPower interface
@ -113,3 +125,8 @@ func (m *Meter) currents() (float64, float64, float64, error) {
return currents[0], currents[1], currents[2], nil
}
// batterySoC implements the Battery.SoC interface
func (m *Meter) batterySoC() (float64, error) {
return m.batterySoCG()
}

View file

@ -6,12 +6,12 @@ import (
"github.com/andig/evcc/api"
)
func decorateMeter(base api.Meter, meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error)) api.Meter {
func decorateMeter(base api.Meter, meterEnergy func() (float64, error), meterCurrent func() (float64, float64, float64, error), battery func() (float64, error)) api.Meter {
switch {
case meterCurrent == nil && meterEnergy == nil:
case battery == nil && meterCurrent == nil && meterEnergy == nil:
return base
case meterCurrent == nil && meterEnergy != nil:
case battery == nil && meterCurrent == nil && meterEnergy != nil:
return &struct {
api.Meter
api.MeterEnergy
@ -22,7 +22,7 @@ func decorateMeter(base api.Meter, meterEnergy func() (float64, error), meterCur
},
}
case meterCurrent != nil && meterEnergy == nil:
case battery == nil && meterCurrent != nil && meterEnergy == nil:
return &struct {
api.Meter
api.MeterCurrent
@ -33,7 +33,7 @@ func decorateMeter(base api.Meter, meterEnergy func() (float64, error), meterCur
},
}
case meterCurrent != nil && meterEnergy != nil:
case battery == nil && meterCurrent != nil && meterEnergy != nil:
return &struct {
api.Meter
api.MeterCurrent
@ -47,11 +47,79 @@ func decorateMeter(base api.Meter, meterEnergy func() (float64, error), meterCur
meterEnergy: meterEnergy,
},
}
case battery != nil && meterCurrent == nil && meterEnergy == nil:
return &struct {
api.Meter
api.Battery
}{
Meter: base,
Battery: &decorateMeterBatteryImpl{
battery: battery,
},
}
case battery != nil && meterCurrent == nil && meterEnergy != nil:
return &struct {
api.Meter
api.Battery
api.MeterEnergy
}{
Meter: base,
Battery: &decorateMeterBatteryImpl{
battery: battery,
},
MeterEnergy: &decorateMeterMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
case battery != nil && meterCurrent != nil && meterEnergy == nil:
return &struct {
api.Meter
api.Battery
api.MeterCurrent
}{
Meter: base,
Battery: &decorateMeterBatteryImpl{
battery: battery,
},
MeterCurrent: &decorateMeterMeterCurrentImpl{
meterCurrent: meterCurrent,
},
}
case battery != nil && meterCurrent != nil && meterEnergy != nil:
return &struct {
api.Meter
api.Battery
api.MeterCurrent
api.MeterEnergy
}{
Meter: base,
Battery: &decorateMeterBatteryImpl{
battery: battery,
},
MeterCurrent: &decorateMeterMeterCurrentImpl{
meterCurrent: meterCurrent,
},
MeterEnergy: &decorateMeterMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
}
return nil
}
type decorateMeterBatteryImpl struct {
battery func() (float64, error)
}
func (impl *decorateMeterBatteryImpl) SoC() (float64, error) {
return impl.battery()
}
type decorateMeterMeterCurrentImpl struct {
meterCurrent func() (float64, float64, float64, error)
}

View file

@ -14,7 +14,12 @@ import (
// credits to https://github.com/vloschiavo/powerwall2
type teslaResponse map[string]struct {
const (
teslaMeterURI = "/api/meters/aggregates"
teslaBatteryURI = "/api/system_status/soe"
)
type teslaMeterResponse map[string]struct {
LastCommunicationTime string `json:"last_communication_time"`
InstantPower float64 `json:"instant_power"`
InstantReactivePower float64 `json:"instant_reactive_power"`
@ -29,6 +34,10 @@ type teslaResponse map[string]struct {
ICCurrent float64 `json:"i_c_current"`
}
type teslaBatteryResponse struct {
Percentage float64 `json:"percentage"`
}
// Tesla is the tesla powerwall meter
type Tesla struct {
*request.Helper
@ -39,7 +48,7 @@ func init() {
registry.Add("tesla", NewTeslaFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -p meter -f decorateTesla -b api.Meter -o tesla_decorators -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -p meter -f decorateTesla -b api.Meter -o tesla_decorators -t "api.MeterEnergy,TotalEnergy,func() (float64, error)" -t "api.Battery,SoC,func() (float64, error)"
// NewTeslaFromConfig creates a Tesla Powerwall Meter from generic config
func NewTeslaFromConfig(other map[string]interface{}) (api.Meter, error) {
@ -60,12 +69,12 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Meter, error) {
return nil, fmt.Errorf("invalid uri %s", cc.URI)
}
if url.Path == "" {
url.Path = "api/meters/aggregates"
cc.URI = url.String()
uri := "https://" + url.Hostname()
if url.Port() != "" {
uri += ":" + url.Port()
}
return NewTesla(cc.URI, cc.Usage)
return NewTesla(uri, cc.Usage)
}
// NewTesla creates a Tesla Meter
@ -85,16 +94,22 @@ func NewTesla(uri, usage string) (api.Meter, error) {
totalEnergy = m.totalEnergy
}
return decorateTesla(m, totalEnergy), nil
// decorate api.BatterySoC
var batterySoC func() (float64, error)
if usage == "battery" {
batterySoC = m.batterySoC
}
return decorateTesla(m, totalEnergy, batterySoC), nil
}
// CurrentPower implements the Meter.CurrentPower interface
func (m *Tesla) CurrentPower() (float64, error) {
var tr teslaResponse
err := m.GetJSON(m.uri, &tr)
var res teslaMeterResponse
err := m.GetJSON(m.uri+teslaMeterURI, &res)
if err == nil {
if o, ok := tr[m.usage]; ok {
if o, ok := res[m.usage]; ok {
return o.InstantPower, nil
}
}
@ -104,11 +119,11 @@ func (m *Tesla) CurrentPower() (float64, error) {
// totalEnergy implements the api.MeterEnergy interface
func (m *Tesla) totalEnergy() (float64, error) {
var tr teslaResponse
err := m.GetJSON(m.uri, &tr)
var res teslaMeterResponse
err := m.GetJSON(m.uri+teslaMeterURI, &res)
if err == nil {
if o, ok := tr[m.usage]; ok {
if o, ok := res[m.usage]; ok {
if m.usage == "load" {
return o.EnergyImported, nil
}
@ -120,3 +135,11 @@ func (m *Tesla) totalEnergy() (float64, error) {
return 0, fmt.Errorf("invalid usage: %s", m.usage)
}
// batterySoC implements the api.Battery interface
func (m *Tesla) batterySoC() (float64, error) {
var res teslaBatteryResponse
err := m.GetJSON(m.uri+teslaBatteryURI, &res)
return res.Percentage, err
}

View file

@ -6,12 +6,12 @@ import (
"github.com/andig/evcc/api"
)
func decorateTesla(base api.Meter, meterEnergy func() (float64, error)) api.Meter {
func decorateTesla(base api.Meter, meterEnergy func() (float64, error), battery func() (float64, error)) api.Meter {
switch {
case meterEnergy == nil:
case battery == nil && meterEnergy == nil:
return base
case meterEnergy != nil:
case battery == nil && meterEnergy != nil:
return &struct {
api.Meter
api.MeterEnergy
@ -21,11 +21,45 @@ func decorateTesla(base api.Meter, meterEnergy func() (float64, error)) api.Mete
meterEnergy: meterEnergy,
},
}
case battery != nil && meterEnergy == nil:
return &struct {
api.Meter
api.Battery
}{
Meter: base,
Battery: &decorateTeslaBatteryImpl{
battery: battery,
},
}
case battery != nil && meterEnergy != nil:
return &struct {
api.Meter
api.Battery
api.MeterEnergy
}{
Meter: base,
Battery: &decorateTeslaBatteryImpl{
battery: battery,
},
MeterEnergy: &decorateTeslaMeterEnergyImpl{
meterEnergy: meterEnergy,
},
}
}
return nil
}
type decorateTeslaBatteryImpl struct {
battery func() (float64, error)
}
func (impl *decorateTeslaBatteryImpl) SoC() (float64, error) {
return impl.battery()
}
type decorateTeslaMeterEnergyImpl struct {
meterEnergy func() (float64, error)
}