evcc-io/core/savings.go

159 lines
4.5 KiB
Go

package core
import (
"math"
"time"
"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/tariff"
)
const (
DefaultGridPrice = 0.30
DefaultFeedInPrice = 0.08
)
// publisher gives access to the site's publish function
type publisher interface {
publish(key string, val interface{})
}
// Site is the main configuration container. A site can host multiple loadpoints.
type Savings struct {
clock clock.Clock
tariffs tariff.Tariffs
started time.Time // Boot time
updated time.Time // Time of last charged value update
gridCharged float64 // Grid energy charged since startup (kWh)
gridCost float64 // Running total of charged grid energy cost (e.g. EUR)
gridSavedCost float64 // Running total of saved cost from self consumption (e.g. EUR)
selfConsumptionCharged float64 // Self-produced energy charged since startup (kWh)
selfConsumptionCost float64 // Running total of charged self-produced energy cost (e.g. EUR)
lastGridPrice, lastFeedInPrice float64 // Stores the last published grid price. Needed to detect price changes (Awattar, ..)
}
func NewSavings(tariffs tariff.Tariffs) *Savings {
clock := clock.New()
savings := &Savings{
clock: clock,
tariffs: tariffs,
started: clock.Now(),
updated: clock.Now(),
}
return savings
}
func (s *Savings) Since() time.Time {
return s.started
}
func (s *Savings) SelfConsumptionPercent() float64 {
if s.TotalCharged() == 0 {
return 0
}
return s.selfConsumptionCharged / s.TotalCharged() * 100
}
func (s *Savings) TotalCharged() float64 {
return s.gridCharged + s.selfConsumptionCharged
}
func (s *Savings) CostTotal() float64 {
return s.gridCost + s.selfConsumptionCost
}
func (s *Savings) EffectivePrice() float64 {
if s.TotalCharged() == 0 {
return s.currentGridPrice()
}
return s.CostTotal() / s.TotalCharged()
}
func (s *Savings) SavingsAmount() float64 {
return s.gridSavedCost
}
func (s *Savings) shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower float64) float64 {
batteryDischarge := math.Max(0, batteryPower)
batteryCharge := math.Min(0, batteryPower) * -1
pvConsumption := math.Min(pvPower, pvPower+gridPower-batteryCharge)
gridImport := math.Max(0, gridPower)
selfConsumption := math.Max(0, batteryDischarge+pvConsumption+batteryCharge)
share := selfConsumption / (gridImport + selfConsumption)
if math.IsNaN(share) {
return 0
}
return share
}
func (s *Savings) currentGridPrice() float64 {
if s.tariffs.Grid != nil {
if gridPrice, err := s.tariffs.Grid.CurrentPrice(); err == nil {
return gridPrice
}
}
return DefaultGridPrice
}
func (s *Savings) currentFeedInPrice() float64 {
if s.tariffs.FeedIn != nil {
if gridPrice, err := s.tariffs.FeedIn.CurrentPrice(); err == nil {
return gridPrice
}
}
return DefaultFeedInPrice
}
func (s *Savings) updatePrices(p publisher) (float64, float64) {
gridPrice := s.currentGridPrice()
if gridPrice != s.lastGridPrice {
s.lastGridPrice = gridPrice
p.publish("tariffGrid", gridPrice)
}
feedinPrice := s.currentFeedInPrice()
if feedinPrice != s.lastFeedInPrice {
s.lastFeedInPrice = feedinPrice
p.publish("tariffFeedIn", feedinPrice)
}
return gridPrice, feedinPrice
}
// 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 0, 0
}
// assume charge power as constant over the duration -> rough kWh estimate
deltaCharged := s.clock.Since(s.updated).Hours() * chargePower / 1e3
share := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
deltaSelf := deltaCharged * share
deltaGrid := deltaCharged - deltaSelf
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)
p.publish("savingsSelfConsumptionCharged", s.selfConsumptionCharged)
p.publish("savingsSelfConsumptionPercent", s.SelfConsumptionPercent())
p.publish("savingsEffectivePrice", s.EffectivePrice())
p.publish("savingsAmount", s.SavingsAmount())
return deltaCharged, deltaSelf
}