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

Savings updates #2226

Merged
merged 22 commits into from
Jan 9, 2022
Merged
Show file tree
Hide file tree
Changes from 16 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
50 changes: 23 additions & 27 deletions assets/js/components/Savings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,22 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{
$t("footer.savings.modalTitle", {
total: fmtKw(chargedTotal * 1000, true, false),
})
}}
<span class="d-block d-sm-none">
{{
$t("footer.savings.modalTitleShort", {
percent,
total: fmtKw(chargedTotal * 1000, true, false),
})
}}
</span>
<span class="d-none d-sm-block">
{{
$t("footer.savings.modalTitleLong", {
percent,
total: fmtKw(chargedTotal * 1000, true, false),
})
}}
</span>
</h5>
<button
type="button"
Expand Down Expand Up @@ -85,10 +96,10 @@
</div>
<p class="mb-3">
{{ $t("footer.savings.modalSavingsPrice") }}:
<strong>{{ pricePerKWh }}</strong>
<strong>{{ fmtPricePerKWh(effectivePrice, currency) }}</strong>
<br />
{{ $t("footer.savings.modalSavingsTotal") }}:
<strong>{{ savingAmount }}</strong>
<strong>{{ fmtMoney(amount, currency) }}</strong>
</p>

<p class="small text-muted mb-3">
Expand Down Expand Up @@ -156,31 +167,16 @@ export default {
selfPercentage: Number,
since: { type: Number, default: 0 },
sponsor: String,
amount: { type: Number, default: 0 },
effectivePrice: { type: Number, default: 0 },
chargedTotal: { type: Number, default: 0 },
chargedGrid: { type: Number, default: 0 },
chargedSelfConsumption: { type: Number, default: 0 },
gridPrice: { type: Number, default: 0.3 },
feedInPrice: { type: Number, default: 0.08 },
gridPrice: { type: Number },
feedInPrice: { type: Number },
currency: String,
},
computed: {
chargedGrid() {
return this.chargedTotal - this.chargedSelfConsumption;
},
defaultPrices() {
const { gridPrice, feedInPrice } = this.$options.propsData;
return gridPrice === undefined || feedInPrice === undefined;
},
savingAmount() {
const priceDiff = (this.gridPrice - this.feedInPrice) / 100;
const saving = this.chargedSelfConsumption * priceDiff;
return this.fmtMoney(saving, this.currency);
},
pricePerKWh() {
const total =
this.chargedGrid * this.gridPrice + this.chargedSelfConsumption * this.feedInPrice;
const perKWh = total / this.chargedTotal;
return this.fmtPricePerKWh(perKWh || this.gridPrice, this.currency);
},
percent() {
return Math.round(this.selfPercentage) || 0;
},
Expand Down
7 changes: 1 addition & 6 deletions assets/js/components/Sponsor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@
<fa-icon :icon="['fas', 'heart']" class="icon me-1 solid"></fa-icon>
{{ $t("footer.sponsor.confetti") }}
</button>
<a
v-if="false"
href="https://evcc.io/sticker"
target="_blank"
class="small text-muted text-center"
>
<a href="https://evcc.io/sticker" target="_blank" class="small text-muted text-center">
{{ $t("footer.sponsor.sticker") }}
</a>
</div>
Expand Down
15 changes: 8 additions & 7 deletions assets/js/i18n/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ export default {
},
savings: {
footerShort: "{percent}% Sonne",
footerLong: "{percent}% Sonnenstrom ",
modalTitle: "{total} kWh Strom geladen",
modalChartGrid: "Netzstrom {grid} kWh",
modalChartSelf: "Sonnenstrom {self} kWh",
modalSavingsPrice: "Effektiver Strompreis",
footerLong: "{percent}% Sonnenenergie",
modalTitleShort: "{total} kWh geladen · {percent}% Sonne",
modalTitleLong: "{total} kWh geladen · {percent}% Sonnenenergie",
modalChartGrid: "Netz {grid} kWh",
modalChartSelf: "Sonne {self} kWh",
modalSavingsPrice: "Effektiver Energiepreis",
modalSavingsTotal: "Ersparnis gegenüber Netzbezug",
modalExplaination: "Berechnung",
modalExplainationGrid: "Netzstrom {gridPrice}",
modalExplainationGrid: "Netz {gridPrice}",
modalExplainationFeedIn: "Einspeisung {feedInPrice}",
modalServerStart: "Seit Serverstart {since}.",
modalNoData: "noch nicht geladen",
Expand All @@ -40,7 +41,7 @@ export default {
supportUs:
"Wir möchten effizientes Zuhause-Laden zum Standard für möglichst viele Menschen machen. Helfe uns indem du die Weiterentwicklung und Pflege des Projekts unterstützt.",
sticker: "...oder evcc Sticker?",
confettiPromise: "Es gibt auch Konfetti ;)",
confettiPromise: "Es gibt auch Sticker und digitales Konfetti ;)",
becomeSponsor: "Werde jetzt GitHub Sponsor",
},
},
Expand Down
5 changes: 3 additions & 2 deletions assets/js/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export default {
savings: {
footerShort: "{percent}% solar",
footerLong: "{percent}% solar energy",
modalTitle: "{total} kWh solar energy charged",
modalTitleShort: "{total} kWh charged · {percent}% solar",
modalTitleLong: "{total} kWh charged · {percent}% solar energy",
modalChartGrid: "Grid energy {grid} kWh",
modalChartSelf: "Solar energy {self} kWh",
modalSavingsPrice: "Effective energy price",
Expand All @@ -40,7 +41,7 @@ export default {
supportUs:
"We want to make efficient home charging the standard for as many people as possible. Help us by supporting the further development and maintenance of the project.",
sticker: "...or evcc stickers?",
confettiPromise: "There will be confetti ;)",
confettiPromise: "There will be stickers and digital confetti ;)",
becomeSponsor: "Become a GitHub Sponsor",
},
},
Expand Down
4 changes: 3 additions & 1 deletion assets/js/mixins/formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ export default {
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
let unit = currency;
let value = amout;
let maximumFractionDigits = 3;
if (["EUR", "USD"].includes(currency)) {
value *= 100;
unit = "ct";
naltatis marked this conversation as resolved.
Show resolved Hide resolved
maximumFractionDigits = 1;
}
return `${this.$n(value, { style: "decimal" })} ${unit}/kWh`;
return `${this.$n(value, { style: "decimal", maximumFractionDigits })} ${unit}/kWh`;
},
fmtTimeAgo: function (elapsed) {
const units = {
Expand Down
3 changes: 3 additions & 0 deletions assets/js/views/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ export default {
return {
since: this.store.state.savingsSince,
chargedTotal: this.store.state.savingsChargedTotal,
chargedGrid: this.store.state.savingsChargedGrid,
chargedSelfConsumption: this.store.state.savingsChargedSelfConsumption,
amount: this.store.state.savingsAmount,
effectivePrice: this.store.state.savingsEffectivePrice,
selfPercentage: this.store.state.savingsSelfPercentage,
gridPrice: this.store.state.tariffGrid,
feedInPrice: this.store.state.tariffFeedIn,
Expand Down
100 changes: 74 additions & 26 deletions core/savings.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,82 +5,130 @@ import (
"time"

"github.com/benbjohnson/clock"
"github.com/evcc-io/evcc/tariff"
"github.com/evcc-io/evcc/util"
)

// 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 {
log *util.Logger

log *util.Logger
clock clock.Clock
tariffs tariff.Tariffs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

das muss ein Pointer sein, sonst doppelt sich das mit dem Tariff aus der Config- da schau ich nochmal rein

started time.Time // Boot time
updated time.Time // Time of last charged value update
chargedTotal float64 // Energy charged since startup (kWh)
chargedGrid float64 // Grid energy charged since startup (kWh)
naltatis marked this conversation as resolved.
Show resolved Hide resolved
chargedSelfConsumption float64 // Self-produced energy charged since startup (kWh)
naltatis marked this conversation as resolved.
Show resolved Hide resolved
Clock clock.Clock
costGrid float64 // Cost of charged grid energy (e.g. EUR)
naltatis marked this conversation as resolved.
Show resolved Hide resolved
costSelfConsumption float64 // Cost of charged self-produced energy (e.g. EUR)
}

func NewSavings() Savings {
func NewSavings(tariffs tariff.Tariffs) *Savings {
clock := clock.New()
savings := &Savings{
log: util.NewLogger("savings"),
clock: clock,
tariffs: tariffs,
started: clock.Now(),
updated: clock.Now(),
Clock: clock,
}

return *savings
return savings
}

func (s *Savings) Since() time.Duration {
return time.Since(s.started)
}

func (s *Savings) SelfPercentage() float64 {
if s.chargedTotal == 0 {
if s.ChargedTotal() == 0 {
return 0
}
return 100 / s.chargedTotal * s.chargedSelfConsumption
return s.chargedSelfConsumption / s.ChargedTotal() * 100
}

func (s *Savings) ChargedTotal() float64 {
return s.chargedTotal
return s.chargedGrid + s.chargedSelfConsumption
}

func (s *Savings) CostTotal() float64 {
return s.costGrid + s.costSelfConsumption
}

func (s *Savings) EffectivePrice() float64 {
if s.ChargedTotal() == 0 {
return s.currentGridPrice()
}
return s.CostTotal() / s.ChargedTotal()
}

func (s *Savings) ChargedSelfConsumption() float64 {
return s.chargedSelfConsumption
func (s *Savings) SavingsAmount() float64 {
return s.chargedSelfConsumption * (s.currentGridPrice() - s.currentFeedInPrice())
}

func (s *Savings) shareOfSelfProducedEnergy(gridPower float64, pvPower float64, batteryPower float64) float64 {
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)

selfPercentage := 100 / (gridImport + selfConsumption) * selfConsumption
share := selfConsumption / (gridImport + selfConsumption)

if math.IsNaN(selfPercentage) {
if math.IsNaN(share) {
return 0
}

return selfPercentage
return share
}

func (s *Savings) Update(gridPower float64, pvPower float64, batteryPower float64, chargePower float64) {
now := s.Clock.Now()
func (s *Savings) currentGridPrice() float64 {
if s.tariffs.Grid != nil {
if gridPrice, err := s.tariffs.Grid.CurrentPrice(); err == nil {
return gridPrice
}
}
return 0.3
naltatis marked this conversation as resolved.
Show resolved Hide resolved
}

selfPercentage := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
func (s *Savings) currentFeedInPrice() float64 {
if s.tariffs.FeedIn != nil {
if gridPrice, err := s.tariffs.FeedIn.CurrentPrice(); err == nil {
return gridPrice
}
}
return 0.08
}

func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePower float64) {
// assume charge power as constant over the duration -> rough kWh estimate
addedEnergy := s.clock.Since(s.updated).Hours() * chargePower / 1e3
share := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)

updateDuration := now.Sub(s.updated)
addedSelfConsumption := addedEnergy * share
addedGrid := addedEnergy - addedSelfConsumption

// assuming the charge power was constant over the duration -> rough estimate
addedEnergy := updateDuration.Hours() * chargePower / 1000
s.chargedGrid += addedGrid
s.costGrid += addedGrid * s.currentGridPrice()
s.chargedSelfConsumption += addedSelfConsumption
s.costSelfConsumption += addedSelfConsumption * s.currentFeedInPrice()

s.chargedTotal += addedEnergy
s.chargedSelfConsumption += addedEnergy * (selfPercentage / 100)
s.updated = now
s.updated = s.clock.Now()

s.log.DEBUG.Printf("%.1fkWh charged since %s", s.chargedTotal, time.Since(s.started).Round(time.Second))
s.log.DEBUG.Printf("%.1fkWh charged since %s", s.ChargedTotal(), time.Since(s.started).Round(time.Second))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

können wir die Zeilen noch in 1 kombinieren? Brauchen wir das jetzt nach der Debugphase überhaupt noch? Weniger Loglines = viel einfachere Fehleranalyse.

Mein Vorschlag: raus, Rest ist im Cache sichtbar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jo, brauchen wir nicht mehr. ist raus.

s.log.DEBUG.Printf("%.1fkWh own energy (%.1f%%)", s.chargedSelfConsumption, s.SelfPercentage())

p.publish("savingsChargedTotal", s.ChargedTotal())
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die Funktion braucht ein early exit wenn sich nichts geändert hat, z.b. weil wir gar nicht laden. Spart jede Menge log lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ist ein bisschen komplizierter. Nun wird publiziert wenn geladen wird oder sich der Grid-Preis geändert hat.

p.publish("savingsChargedGrid", s.chargedGrid)
p.publish("savingsChargedSelfConsumption", s.chargedSelfConsumption)
p.publish("savingsSelfPercentage", s.SelfPercentage())
p.publish("savingsEffectivePrice", s.EffectivePrice())
p.publish("savingsAmount", s.SavingsAmount())
p.publish("tariffGrid", s.currentGridPrice())
p.publish("tariffFeedIn", s.currentFeedInPrice())
}
Loading