159 lines
4.5 KiB
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
|
|
}
|