Experimental: add telemetry for virtual power plant (#4343)

This commit is contained in:
andig 2022-09-19 17:09:04 +02:00 • committed by GitHub
parent b272f24d24
commit f57f9edeaa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 15 deletions

View file

@ -28,6 +28,7 @@ type config struct {
Network networkConfig
Log string
SponsorToken string
Telemetry bool
Metrics bool
Profile bool
Levels map[string]string

View file

@ -21,6 +21,7 @@ import (
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/pipe"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/evcc-io/evcc/util/telemetry"
"github.com/samber/lo"
"github.com/spf13/viper"
"golang.org/x/text/currency"
@ -51,6 +52,10 @@ func configureEnvironment(conf config) (err error) {
err = sponsor.ConfigureSponsorship(conf.SponsorToken)
}
if err == nil && conf.Telemetry {
err = telemetry.Create(sponsor.Token)
}
// setup mqtt client listener
if err == nil && conf.Mqtt.Broker != "" {
err = configureMQTT(conf.Mqtt)

View file

@ -125,27 +125,28 @@ func (s *Savings) updatePrices(p publisher) (float64, float64) {
return gridPrice, feedinPrice
}
func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePower float64) {
// Update savings calculation and return grid/green energy added since last update
func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePower float64) (float64, float64) {
gridPrice, feedinPrice := s.updatePrices(p)
defer func() { s.updated = s.clock.Now() }()
// no charging, no need to update
if chargePower == 0 {
return
return 0, 0
}
// assume charge power as constant over the duration -> rough kWh estimate
energyAdded := s.clock.Since(s.updated).Hours() * chargePower / 1e3
deltaCharged := s.clock.Since(s.updated).Hours() * chargePower / 1e3
share := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
addedSelfConsumption := energyAdded * share
addedGrid := energyAdded - addedSelfConsumption
deltaSelf := deltaCharged * share
deltaGrid := deltaCharged - deltaSelf
s.gridCharged += addedGrid
s.gridCost += addedGrid * gridPrice
s.gridSavedCost += addedSelfConsumption * (gridPrice - feedinPrice)
s.selfConsumptionCharged += addedSelfConsumption
s.selfConsumptionCost += addedSelfConsumption * feedinPrice
s.gridCharged += deltaGrid
s.gridCost += deltaGrid * gridPrice
s.gridSavedCost += deltaSelf * (gridPrice - feedinPrice)
s.selfConsumptionCharged += deltaSelf
s.selfConsumptionCost += deltaSelf * feedinPrice
p.publish("savingsTotalCharged", s.TotalCharged())
p.publish("savingsGridCharged", s.gridCharged)
@ -153,4 +154,6 @@ func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePo
p.publish("savingsSelfConsumptionPercent", s.SelfConsumptionPercent())
p.publish("savingsEffectivePrice", s.EffectivePrice())
p.publish("savingsAmount", s.SavingsAmount())
return deltaCharged, deltaSelf
}

View file

@ -14,6 +14,7 @@ import (
"github.com/evcc-io/evcc/push"
"github.com/evcc-io/evcc/tariff"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/telemetry"
)
// Updater abstracts the LoadPoint implementation for testing
@ -325,7 +326,7 @@ func (site *Site) updateMeters() error {
if err == nil {
site.batteryPower += power
} else {
site.log.ERROR.Println(fmt.Errorf("battery meter %d: %v", id, err))
site.log.ERROR.Printf("battery meter %d: %v", id, err)
}
}
@ -342,7 +343,7 @@ func (site *Site) updateMeters() error {
site.log.DEBUG.Printf("grid currents: %.3gA", []float64{i1, i2, i3})
site.publish("gridCurrents", []float64{i1, i2, i3})
} else {
site.log.ERROR.Println(fmt.Errorf("grid meter currents: %v", err))
site.log.ERROR.Printf("grid meter currents: %v", err)
}
}
@ -371,7 +372,7 @@ func (site *Site) sitePower(totalChargePower float64) (float64, error) {
site.gridPower = totalChargePower - site.pvPower
}
// allow using Grid and charge as estimate for pv power
// allow using grid and charge as estimate for pv power
if site.pvMeters == nil {
site.pvPower = totalChargePower - site.gridPower + site.ResidualPower
if site.pvPower < 0 {
@ -448,9 +449,12 @@ func (site *Site) update(lp Updater) {
site.Health.Update()
}
// update savings
// update savings and aggregate telemetry
// TODO: use energy instead of current power for better results
site.savings.Update(site, site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
deltaCharged, deltaSelf := site.savings.Update(site, site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
if totalChargePower > 0 {
go telemetry.ChargeProgress(site.log, totalChargePower, deltaCharged, deltaSelf)
}
}
// prepare publishes initial values

View file

@ -15,6 +15,19 @@ interval: 10s # control cycle interval
# sponsor token enables optional features (request at https://cloud.evcc.io)
# sponsortoken:
# telemetry enables aggregated statistics
#
# Telemetry allows collecting usage data (grid and green energy, charge power).
# Data is aggregated, no individual charging sessions are tracked. The collected,
# anonymous data can be retrieved using https://api.evcc.io.
#
# See https://github.com/evcc-io/evcc/pull/4343 or details.
#
# For time being, this is only available to sponsors, hence data is associated with
# the sponsor token's identity.
#
# telemetry: true
# log settings
log: info
levels:

View file

@ -0,0 +1,61 @@
package telemetry
import (
"errors"
"fmt"
"net/http"
"github.com/evcc-io/evcc/util"
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/sponsor"
)
var (
api = "https://api.evcc.io"
enabled bool
)
func Create(token string) error {
if token == "" {
return errors.New("community requires sponsorship")
}
enabled = true
return nil
}
func ChargeProgress(log *util.Logger, power, deltaCharged, deltaGreen float64) {
log.DEBUG.Printf("community: Δ%.0f/%.0fkWh @ %.0fW", deltaGreen, deltaCharged, power)
if !enabled {
return
}
data := struct {
ChargePower float64 `json:"chargePower"`
ChargeEnergy float64 `json:"chargeEnergy"`
GreenEnergy float64 `json:"greenEnergy"`
}{
ChargePower: power,
ChargeEnergy: deltaCharged,
GreenEnergy: deltaGreen,
}
uri := fmt.Sprintf("%s/charged/%s", api, sponsor.Token)
req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data))
var res struct {
Error string
}
if err == nil {
client := request.NewHelper(log)
if err = client.DoJSON(req, &res); err == nil && res.Error != "" {
err = errors.New(res.Error)
}
}
if err != nil {
log.ERROR.Printf("community api: %v", err)
}
}