Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forecast: add adjusted forecast #18867

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions core/keys/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
SmartCostType = "smartCostType"
Statistics = "statistics"
Forecast = "forecast"
SolarForecasted = "solarForecasted"
SolarYieldToday = "solarYieldToday"
TariffCo2 = "tariffCo2"
TariffCo2Home = "tariffCo2Home"
TariffCo2Loadpoints = "tariffCo2Loadpoints"
Expand Down
62 changes: 62 additions & 0 deletions core/meterenergy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package core

import (
"time"

"github.com/benbjohnson/clock"
"github.com/jinzhu/now"
"github.com/samber/lo"
)

type meterEnergy struct {
clock clock.Clock
updated time.Time
startFunc func(time.Time) time.Time
energy *float64 // kWh
acc float64 // kWh
}

// reset previous period
func (m *meterEnergy) resetPeriod() {
sod := m.startFunc(m.clock.Now())
if m.startFunc != nil && m.updated.Before(sod) {
m.updated = time.Time{}
m.energy = nil
m.acc = 0
}
}

func (m *meterEnergy) AccumulatedEnergy() float64 {
m.resetPeriod()
return m.acc
}

func (m *meterEnergy) AddTotalEnergy(v float64) {
m.resetPeriod()
defer func() {
m.updated = m.clock.Now()
m.energy = lo.ToPtr(v)
}()

if m.energy == nil {
return
}

m.acc += v - *m.energy
}

func (m *meterEnergy) AddPower(v float64) {
m.resetPeriod()
defer func() { m.updated = m.clock.Now() }()

if m.updated.IsZero() {
return
}

d := m.clock.Since(m.updated)
m.acc += v * d.Hours() / 1e3
}

func beginningOfDay(t time.Time) time.Time {
return now.With(t).BeginningOfDay()
}
44 changes: 44 additions & 0 deletions core/meterenergy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package core

import (
"testing"
"time"

"github.com/benbjohnson/clock"
"github.com/stretchr/testify/assert"
)

func TestMeterEnergy(t *testing.T) {
clock := clock.NewMock()
me := &meterEnergy{
clock: clock,
startFunc: beginningOfDay,
}

me.AddTotalEnergy(10)
assert.Equal(t, 0.0, me.AccumulatedEnergy())
me.AddTotalEnergy(11)
assert.Equal(t, 1.0, me.AccumulatedEnergy())
me.AddTotalEnergy(11)
assert.Equal(t, 1.0, me.AccumulatedEnergy())

me.AddPower(1e3)
assert.Equal(t, 1.0, me.AccumulatedEnergy())

clock.Add(30 * time.Minute)
me.AddPower(1e3)
assert.Equal(t, 1.5, me.AccumulatedEnergy())

clock.Add(23*time.Hour + 30*time.Minute)
me.AddPower(1e3)
assert.Equal(t, 0.0, me.AccumulatedEnergy())

clock.Add(1 * time.Hour)
me.AddPower(1e3)
assert.Equal(t, 1.0, me.AccumulatedEnergy())

me.AddTotalEnergy(12)
assert.Equal(t, 1.0, me.AccumulatedEnergy())
me.AddTotalEnergy(13)
assert.Equal(t, 2.0, me.AccumulatedEnergy())
}
121 changes: 25 additions & 96 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/benbjohnson/clock"
"github.com/cenkalti/backoff/v4"
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/cmd/shutdown"
Expand Down Expand Up @@ -95,15 +96,17 @@ type Site struct {
coordinator *coordinator.Coordinator // Vehicles
prioritizer *prioritizer.Prioritizer // Power budgets
stats *Stats // Stats
pvEnergy meterEnergy

// cached state
gridPower float64 // Grid power
pvPower float64 // PV power
excessDCPower float64 // PV excess DC charge power (hybrid only)
auxPower float64 // Aux power
batteryPower float64 // Battery power (charge negative, discharge positive)
batterySoc float64 // Battery soc
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
gridPower float64 // Grid power
pvPower float64 // PV power
excessDCPower float64 // PV excess DC charge power (hybrid only)
auxPower float64 // Aux power
batteryPower float64 // Battery power (charge negative, discharge positive)
batterySoc float64 // Battery soc
batteryCapacity float64 // Battery capacity
batteryMode api.BatteryMode // Battery mode (runtime only, not persisted)
}

// MetersConfig contains the site's meter configuration
Expand Down Expand Up @@ -241,12 +244,16 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif

// NewSite creates a Site with sane defaults
func NewSite() *Site {
lp := &Site{
site := &Site{
log: util.NewLogger("site"),
Voltage: 230, // V
pvEnergy: meterEnergy{
clock: clock.New(),
startFunc: beginningOfDay,
},
}

return lp
return site
}

// restoreMetersAndTitle restores site meter configuration
Expand Down Expand Up @@ -501,6 +508,13 @@ func (site *Site) updatePvMeters() {
site.publish(keys.PvPower, site.pvPower)
site.publish(keys.PvEnergy, totalEnergy)
site.publish(keys.Pv, mm)

// update statistics
if totalEnergy > 0 {
site.pvEnergy.AddTotalEnergy(totalEnergy)
} else {
site.pvEnergy.AddPower(site.pvPower)
}
}

// updateBatteryMeters updates battery meters
Expand Down Expand Up @@ -552,6 +566,7 @@ func (site *Site) updateBatteryMeters() {
totalCapacity = float64(len(site.batteryMeters))
}
site.batterySoc = batterySocAcc / totalCapacity
site.batteryCapacity = totalCapacity

site.batteryPower = lo.SumBy(mm, func(m measurement) float64 {
return m.Power
Expand All @@ -565,7 +580,7 @@ func (site *Site) updateBatteryMeters() {
site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc))
}

site.publish(keys.BatteryCapacity, totalCapacity)
site.publish(keys.BatteryCapacity, site.batteryCapacity)
site.publish(keys.BatterySoc, site.batterySoc)

site.publish(keys.BatteryPower, site.batteryPower)
Expand Down Expand Up @@ -734,92 +749,6 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
return sitePower, batteryBuffered, batteryStart, nil
}

// greenShare returns
// - the current green share, calculated for the part of the consumption between powerFrom and powerTo
// the consumption below powerFrom will get the available green power first
func (site *Site) greenShare(powerFrom float64, powerTo float64) float64 {
greenPower := math.Max(0, site.pvPower) + math.Max(0, site.batteryPower)
greenPowerAvailable := math.Max(0, greenPower-powerFrom)

power := powerTo - powerFrom
share := math.Min(greenPowerAvailable, power) / power

if math.IsNaN(share) {
if greenPowerAvailable > 0 {
share = 1
} else {
share = 0
}
}

return share
}

// effectivePrice calculates the real energy price based on self-produced and grid-imported energy.
func (site *Site) effectivePrice(greenShare float64) *float64 {
if grid, err := tariff.Now(site.GetTariff(api.TariffUsageGrid)); err == nil {
feedin, err := tariff.Now(site.GetTariff(api.TariffUsageFeedIn))
if err != nil {
feedin = 0
}
effPrice := grid*(1-greenShare) + feedin*greenShare
return &effPrice
}
return nil
}

// effectiveCo2 calculates the amount of emitted co2 based on self-produced and grid-imported energy.
func (site *Site) effectiveCo2(greenShare float64) *float64 {
if co2, err := tariff.Now(site.GetTariff(api.TariffUsageCo2)); err == nil {
effCo2 := co2 * (1 - greenShare)
return &effCo2
}
return nil
}

func (site *Site) publishTariffs(greenShareHome float64, greenShareLoadpoints float64) {
site.publish(keys.GreenShareHome, greenShareHome)
site.publish(keys.GreenShareLoadpoints, greenShareLoadpoints)

if v, err := tariff.Now(site.GetTariff(api.TariffUsageGrid)); err == nil {
site.publish(keys.TariffGrid, v)
}
if v, err := tariff.Now(site.GetTariff(api.TariffUsageFeedIn)); err == nil {
site.publish(keys.TariffFeedIn, v)
}
if v, err := tariff.Now(site.GetTariff(api.TariffUsageCo2)); err == nil {
site.publish(keys.TariffCo2, v)
}
if v, err := tariff.Now(site.GetTariff(api.TariffUsageSolar)); err == nil {
site.publish(keys.TariffSolar, v)
}
if v := site.effectivePrice(greenShareHome); v != nil {
site.publish(keys.TariffPriceHome, v)
}
if v := site.effectiveCo2(greenShareHome); v != nil {
site.publish(keys.TariffCo2Home, v)
}
if v := site.effectivePrice(greenShareLoadpoints); v != nil {
site.publish(keys.TariffPriceLoadpoints, v)
}
if v := site.effectiveCo2(greenShareLoadpoints); v != nil {
site.publish(keys.TariffCo2Loadpoints, v)
}

// forecast
site.publish(keys.Forecast, struct {
Co2 api.Rates `json:"co2,omitempty"`
FeedIn api.Rates `json:"feedin,omitempty"`
Grid api.Rates `json:"grid,omitempty"`
Solar api.Rates `json:"solar,omitempty"`
}{
Co2: tariff.Forecast(site.GetTariff(api.TariffUsageCo2)),
FeedIn: tariff.Forecast(site.GetTariff(api.TariffUsageFeedIn)),
Grid: tariff.Forecast(site.GetTariff(api.TariffUsageGrid)),
Solar: tariff.Forecast(site.GetTariff(api.TariffUsageSolar)),
})
}

// updateLoadpoints updates all loadpoints' charge power
func (site *Site) updateLoadpoints() float64 {
var (
Expand Down
Loading
Loading