diff --git a/api/rates.go b/api/rates.go index afe05fd380..3b5c216d73 100644 --- a/api/rates.go +++ b/api/rates.go @@ -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 +} diff --git a/core/energy_metrics.go b/core/energy_metrics.go index 68934ed43d..cd33170d02 100644 --- a/core/energy_metrics.go +++ b/core/energy_metrics.go @@ -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 { @@ -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. @@ -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 } @@ -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 { @@ -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()) } diff --git a/core/energy_metrics_test.go b/core/energy_metrics_test.go index 65ac760b78..b7c907cc2e 100644 --- a/core/energy_metrics_test.go +++ b/core/energy_metrics_test.go @@ -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) } @@ -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 { diff --git a/core/loadpoint.go b/core/loadpoint.go index ade1ae39ed..922502d030 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1573,7 +1573,7 @@ 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() @@ -1581,7 +1581,7 @@ func (lp *Loadpoint) Update(sitePower float64, autoCharge, batteryBuffered, batt 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) diff --git a/core/loadpoint_session.go b/core/loadpoint_session.go index f72b2f0910..76d76e178e 100644 --- a/core/loadpoint_session.go +++ b/core/loadpoint_session.go @@ -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 diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go index 92ecbaf1a3..303e847189 100644 --- a/core/loadpoint_test.go +++ b/core/loadpoint_test.go @@ -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() } @@ -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") @@ -437,7 +437,7 @@ 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") @@ -445,14 +445,14 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) { 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") @@ -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() } @@ -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) @@ -566,14 +566,14 @@ 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") @@ -581,7 +581,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -589,7 +589,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -597,7 +597,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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") @@ -605,7 +605,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) { 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() diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go index 927293de06..d352b6cf97 100644 --- a/core/loadpoint_vehicle_test.go +++ b/core/loadpoint_vehicle_test.go @@ -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 @@ -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 diff --git a/core/session/session.go b/core/session/session.go index cba27ef4a8..0aabbb71fe 100644 --- a/core/session/session.go +++ b/core/session/session.go @@ -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 diff --git a/core/site.go b/core/site.go index bad0f92104..3d705e7566 100644 --- a/core/site.go +++ b/core/site.go @@ -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 @@ -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 { @@ -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() diff --git a/tariff/tariffs.go b/tariff/tariffs.go index ffeb024cf2..27857567e3 100644 --- a/tariff/tariffs.go +++ b/tariff/tariffs.go @@ -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) @@ -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) }