diff --git a/cmd/config.go b/cmd/config.go index 5da073c7c..f2edff8bf 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -28,6 +28,7 @@ type config struct { Network networkConfig Log string SponsorToken string + Telemetry bool Metrics bool Profile bool Levels map[string]string diff --git a/cmd/setup.go b/cmd/setup.go index 4e9000aed..83a90f584 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -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) diff --git a/core/savings.go b/core/savings.go index dd7faade6..0db42c18f 100644 --- a/core/savings.go +++ b/core/savings.go @@ -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 } diff --git a/core/site.go b/core/site.go index a4f2bc69e..301f9ba32 100644 --- a/core/site.go +++ b/core/site.go @@ -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 diff --git a/evcc.dist.yaml b/evcc.dist.yaml index fd20eef04..8f47b21d2 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -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: diff --git a/util/telemetry/community.go b/util/telemetry/community.go new file mode 100644 index 000000000..ce6032c1f --- /dev/null +++ b/util/telemetry/community.go @@ -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) + } +}