diff --git a/api/api.go b/api/api.go index 12f074841..4f87bd32b 100644 --- a/api/api.go +++ b/api/api.go @@ -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) diff --git a/core/site.go b/core/site.go index a82fa0f01..6b131aad9 100644 --- a/core/site.go +++ b/core/site.go @@ -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 diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 57a3ce4b0..4d13e9947 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -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: diff --git a/meter/meter.go b/meter/meter.go index c89225841..9b0c11982 100644 --- a/meter/meter.go +++ b/meter/meter.go @@ -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() +} diff --git a/meter/meter_decorators.go b/meter/meter_decorators.go index 389a1e87a..c0e453562 100644 --- a/meter/meter_decorators.go +++ b/meter/meter_decorators.go @@ -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) } diff --git a/meter/tesla.go b/meter/tesla.go index 0eb076b71..7c4b3d202 100644 --- a/meter/tesla.go +++ b/meter/tesla.go @@ -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 +} diff --git a/meter/tesla_decorators.go b/meter/tesla_decorators.go index 7a899a4b9..494aa5d7f 100644 --- a/meter/tesla_decorators.go +++ b/meter/tesla_decorators.go @@ -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) }