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

Sessions: add price/co2 savings #13950

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/rates.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,17 @@ func (r Rates) Current(now time.Time) (Rate, error) {

return Rate{}, errors.New("no matching rate")
}

// Average return the average price of all currently known rates
func (r Rates) Average() (float64, error) {
if len(r) == 0 {
return 0, errors.New("no rates available")
}

var sum float64
for _, rr := range r {
sum += rr.Price
}

return sum / float64(len(r)), nil
}
47 changes: 35 additions & 12 deletions core/energy_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ type EnergyMetrics struct {
totalKWh float64 // Total amount of energy used (kWh)
solarKWh float64 // Self-produced energy (kWh)
price *float64 // Total cost (Currency)
priceSaved *float64 // Total saved cost (Currency)
co2 *float64 // Amount of emitted CO2 (gCO2eq)
co2Saved *float64 // Amount of saved CO2 (gCO2eq)
currentGreenShare float64 // Current share of solar energy of site (0-1)
currentPrice *float64 // Current price per kWh
referencePrice *float64 // Reference price per kWh
currentCo2 *float64 // Current co2 emissions
referenceCo2 *float64 // Reference co2 emissions
}

func NewEnergyMetrics() *EnergyMetrics {
Expand All @@ -19,10 +23,12 @@ func NewEnergyMetrics() *EnergyMetrics {
}

// SetEnvironment updates site information like solar share, price, co2 for use in later calculations
func (em *EnergyMetrics) SetEnvironment(greenShare float64, effPrice, effCo2 *float64) {
func (em *EnergyMetrics) SetEnvironment(greenShare float64, effPrice, refPrice, effCo2, refCo2 *float64) {
em.currentGreenShare = greenShare
em.currentPrice = effPrice
em.referencePrice = refPrice
em.currentCo2 = effCo2
em.referenceCo2 = refCo2
}

// Update sets the a new value for the total amount of charged energy and updated metrics based on environment values.
Expand All @@ -36,22 +42,21 @@ func (em *EnergyMetrics) Update(chargedKWh float64) (float64, float64) {
em.totalKWh = chargedKWh
addedGreen := added * em.currentGreenShare
em.solarKWh += addedGreen
// optional values
if em.currentPrice != nil {
addedPrice := *em.currentPrice * added
newPrice := addedPrice
if em.price != nil {
newPrice = *em.price + newPrice
if em.price == nil {
em.price = new(float64)
em.priceSaved = new(float64)
}
em.price = &newPrice
*em.price += *em.currentPrice * added
*em.priceSaved = (*em.referencePrice - *em.currentPrice) * added
}
if em.currentCo2 != nil {
addedCo2 := *em.currentCo2 * added
newCo2 := addedCo2
if em.co2 != nil {
newCo2 = *em.co2 + newCo2
if em.co2 == nil {
em.co2 = new(float64)
em.co2Saved = new(float64)
}
em.co2 = &newCo2
*em.co2 += *em.currentCo2 * added
*em.co2Saved = (*em.referenceCo2 - *em.currentCo2) * added
}
return added, addedGreen
}
Expand Down Expand Up @@ -85,6 +90,14 @@ func (em *EnergyMetrics) Price() *float64 {
return em.price
}

// PriceSaved return the total amount of saved cost in Currency
func (em *EnergyMetrics) PriceSaved() *float64 {
if em.totalKWh == 0 || em.priceSaved == nil {
return nil
}
return em.priceSaved
}

// PricePerKWh returns the average energy price in Currency
func (em *EnergyMetrics) PricePerKWh() *float64 {
if em.totalKWh == 0 || em.price == nil {
Expand All @@ -103,11 +116,21 @@ func (em *EnergyMetrics) Co2PerKWh() *float64 {
return &co2
}

// Co2Saved returns the total amount of saved co2 emissions in gCO2eq
func (em *EnergyMetrics) Co2Saved() *float64 {
if em.totalKWh == 0 || em.co2Saved == nil {
return nil
}
return em.co2Saved
}

// Publish publishes metrics with a given prefix
func (em *EnergyMetrics) Publish(prefix string, p publisher) {
p.publish(prefix+"Energy", em.TotalWh())
p.publish(prefix+"SolarPercentage", em.SolarPercentage())
p.publish(prefix+"PricePerKWh", em.PricePerKWh())
p.publish(prefix+"Price", em.Price())
p.publish(prefix+"PriceSaved", em.PriceSaved())
p.publish(prefix+"Co2PerKWh", em.Co2PerKWh())
p.publish(prefix+"Co2Saved", em.Co2Saved())
}
4 changes: 2 additions & 2 deletions core/energy_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func TestEnergyMetrics(t *testing.T) {
s := NewEnergyMetrics()

for _, tc := range tc.steps {
s.SetEnvironment(tc.greenShare, tc.effPrice, tc.effCo2)
s.SetEnvironment(tc.greenShare, tc.effPrice, nil, tc.effCo2, nil)
s.Update(tc.kWh)
}

Expand All @@ -146,7 +146,7 @@ func TestEnergyMetrics(t *testing.T) {

// reset
s := NewEnergyMetrics()
s.SetEnvironment(1, f(1), f(1))
s.SetEnvironment(1, f(1), f(1), f(1), f(1))
s.Update(1)
s.Reset()
if s.TotalWh() != 0 || s.SolarPercentage() != 0 || s.Co2PerKWh() != nil || s.Price() != nil || s.PricePerKWh() != nil {
Expand Down
4 changes: 2 additions & 2 deletions core/loadpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1573,15 +1573,15 @@ func (lp *Loadpoint) phaseSwitchCompleted() bool {
}

// Update is the main control function. It reevaluates meters and charger state
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, effCo2 *float64) {
func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effPrice, refPrice, effCo2, refCo2 *float64) {
lp.publish(keys.SmartCostActive, autoCharge)
lp.processTasks()

// read and publish meters first- charge power and currents have already been updated by the site
lp.updateChargeVoltages()
lp.phasesFromChargeCurrents()

lp.sessionEnergy.SetEnvironment(greenShare, effPrice, effCo2)
lp.sessionEnergy.SetEnvironment(greenShare, effPrice, refPrice, effCo2, refCo2)

// update ChargeRater here to make sure initial meter update is caught
lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
Expand Down
2 changes: 2 additions & 0 deletions core/loadpoint_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ func (lp *Loadpoint) stopSession() {
solarPerc := lp.sessionEnergy.SolarPercentage()
s.SolarPercentage = &solarPerc
s.Price = lp.sessionEnergy.Price()
s.PriceSaved = lp.sessionEnergy.PriceSaved()
s.PricePerKWh = lp.sessionEnergy.PricePerKWh()
s.Co2PerKWh = lp.sessionEnergy.Co2PerKWh()
s.Co2Saved = lp.sessionEnergy.Co2Saved()
s.ChargedEnergy = lp.sessionEnergy.TotalWh() / 1e3
s.ChargeDuration = &lp.chargeDuration

Expand Down
28 changes: 14 additions & 14 deletions core/loadpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func TestUpdatePowerZero(t *testing.T) {
}

lp.mode = tc.mode
lp.Update(0, false, false, false, 0, nil, nil) // false,sitePower false,0
lp.Update(0, false, false, false, 0, nil, nil, nil, nil) // false,sitePower false,0

ctrl.Finish()
}
Expand Down Expand Up @@ -428,7 +428,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500, false, false, false, 0, nil, nil)
lp.Update(500, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

t.Log("charging above target - soc deactivates charger")
Expand All @@ -437,22 +437,22 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(500, false, false, false, 0, nil, nil)
lp.Update(500, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

t.Log("deactivated charger changes status to B")
clock.Add(5 * time.Minute)
vehicle.EXPECT().Soc().Return(95.0, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-5000, false, false, false, 0, nil, nil)
lp.Update(-5000, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

t.Log("soc has fallen below target - soc update prevented by timer")
clock.Add(5 * time.Minute)
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
lp.Update(-5000, false, false, false, 0, nil, nil)
lp.Update(-5000, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

t.Log("soc has fallen below target - soc update timer expired")
Expand All @@ -461,7 +461,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
charger.EXPECT().Status().Return(api.StatusB, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Enable(true).Return(nil)
lp.Update(-5000, false, false, false, 0, nil, nil)
lp.Update(-5000, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()
}

Expand Down Expand Up @@ -496,14 +496,14 @@ func TestSetModeAndSocAtDisconnect(t *testing.T) {
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
charger.EXPECT().MaxCurrent(int64(maxA)).Return(nil)
lp.Update(500, false, false, false, 0, nil, nil)
lp.Update(500, false, false, false, 0, nil, nil, nil, nil)

t.Log("switch off when disconnected")
clock.Add(5 * time.Minute)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusA, nil)
charger.EXPECT().Enable(false).Return(nil)
lp.Update(-3000, false, false, false, 0, nil, nil)
lp.Update(-3000, false, false, false, 0, nil, nil, nil, nil)

if mode := lp.GetMode(); mode != api.ModeOff {
t.Error("unexpected mode", mode)
Expand Down Expand Up @@ -566,46 +566,46 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
rater.EXPECT().ChargedEnergy().Return(0.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)

t.Log("at 1:00h charging at 5 kWh")
clock.Add(time.Hour)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h stop charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:00h restart charging at 5 kWh")
clock.Add(time.Second)
rater.EXPECT().ChargedEnergy().Return(5.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)
expectCache("chargedEnergy", 5000.0)

t.Log("at 1:30h continue charging at 7.5 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(7.5, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusC, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)
expectCache("chargedEnergy", 7500.0)

t.Log("at 2:00h stop charging at 10 kWh")
clock.Add(30 * time.Minute)
rater.EXPECT().ChargedEnergy().Return(10.0, nil)
charger.EXPECT().Enabled().Return(lp.enabled, nil)
charger.EXPECT().Status().Return(api.StatusB, nil)
lp.Update(-1, false, false, false, 0, nil, nil)
lp.Update(-1, false, false, false, 0, nil, nil, nil, nil)
expectCache("chargedEnergy", 10000.0)

ctrl.Finish()
Expand Down
4 changes: 2 additions & 2 deletions core/loadpoint_vehicle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusA, nil)

lp.Update(0, false, false, false, 0, nil, nil)
lp.Update(0, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

// detection started
Expand All @@ -284,7 +284,7 @@ func TestReconnectVehicle(t *testing.T) {
// vehicle not updated yet
vehicle.MockChargeState.EXPECT().Status().Return(api.StatusB, nil)

lp.Update(0, false, false, false, 0, nil, nil)
lp.Update(0, false, false, false, 0, nil, nil, nil, nil)
ctrl.Finish()

// vehicle detected
Expand Down
2 changes: 2 additions & 0 deletions core/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ type Session struct {
ChargeDuration *time.Duration `json:"chargeDuration" csv:"Charge Duration" gorm:"column:charge_duration"`
SolarPercentage *float64 `json:"solarPercentage" csv:"Solar (%)" gorm:"column:solar_percentage"`
Price *float64 `json:"price" csv:"Price" gorm:"column:price"`
PriceSaved *float64 `json:"priceSaved" csv:"Price Saved" gorm:"column:price_saved"`
PricePerKWh *float64 `json:"pricePerKWh" csv:"Price/kWh" gorm:"column:price_per_kwh"`
Co2PerKWh *float64 `json:"co2PerKWh" csv:"CO2/kWh (gCO2eq)" gorm:"column:co2_per_kwh"`
Co2Saved *float64 `json:"co2Saved" csv:"CO2 Saved (gCO2eq)" gorm:"column:co2_saved"`
}

// Sessions is a list of sessions
Expand Down
20 changes: 18 additions & 2 deletions core/site.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const standbyPower = 10 // consider less than 10W as charger in standby
// updater abstracts the Loadpoint implementation for testing
type updater interface {
loadpoint.API
Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
Update(availablePower float64, autoCharge, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, referencePrice, effectiveCo2, referenceCo2 *float64)
}

// meterMeasurement is used as slice element for publishing structured data
Expand Down Expand Up @@ -732,6 +732,22 @@ func (site *Site) effectivePrice(greenShare float64) *float64 {
return nil
}

// referencePrice calculates the reference grid price from all currently known rates
func (site *Site) referencePrice() *float64 {
if price, err := site.tariffs.AverageGridPrice(); err == nil {
return &price
}
return nil
}

// referenceCo2 calculates the reference co2 from all currently known rates
func (site *Site) referenceCo2() *float64 {
if co2, err := site.tariffs.AverageCo2(); err == nil {
return &co2
}
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 := site.tariffs.CurrentCo2(); err == nil {
Expand Down Expand Up @@ -814,7 +830,7 @@ func (site *Site) update(lp updater) {
greenShareHome := site.greenShare(0, homePower)
greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)

lp.Update(sitePower, smartCostActive, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints))
lp.Update(sitePower, smartCostActive, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.referencePrice(), site.effectiveCo2(greenShareLoadpoints), site.referenceCo2())

site.Health.Update()

Expand Down
24 changes: 20 additions & 4 deletions tariff/tariffs.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ func currentPrice(t api.Tariff) (float64, error) {
return 0, api.ErrNotAvailable
}

func averagePrice(t api.Tariff) (float64, error) {
if t != nil {
if rr, err := t.Rates(); err == nil {
if avg, err := rr.Average(); err == nil {
return avg, nil
}
}
}
return 0, api.ErrNotAvailable
}

// CurrentGridPrice returns the current grid price.
func (t *Tariffs) CurrentGridPrice() (float64, error) {
return currentPrice(t.Grid)
Expand All @@ -35,8 +46,13 @@ func (t *Tariffs) CurrentFeedInPrice() (float64, error) {

// CurrentCo2 determines the grids co2 emission.
func (t *Tariffs) CurrentCo2() (float64, error) {
if t.Co2 != nil {
return currentPrice(t.Co2)
}
return 0, api.ErrNotAvailable
return currentPrice(t.Co2)
}

func (t *Tariffs) AverageGridPrice() (float64, error) {
return averagePrice(t.Grid)
}

func (t *Tariffs) AverageCo2() (float64, error) {
return averagePrice(t.Co2)
}
Loading