Refactor savings (#2226)
This commit is contained in:
parent
445bafcb8e
commit
8ba710ff5f
17 changed files with 413 additions and 243 deletions
|
|
@ -19,9 +19,9 @@ KeinUpdate.args = {
|
|||
version: { installed: "0.40" },
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 15231,
|
||||
chargedSelfConsumption: 12231,
|
||||
selfPercentage: 80.3,
|
||||
totalCharged: 15231,
|
||||
selfConsumptionCharged: 12231,
|
||||
selfConsumptionPercent: 80.3,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -32,9 +32,9 @@ Sponsor.args = {
|
|||
},
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 21000,
|
||||
chargedSelfConsumption: 12000,
|
||||
selfPercentage: 54,
|
||||
totalCharged: 21000,
|
||||
selfConsumptionCharged: 12000,
|
||||
selfConsumptionPercent: 54,
|
||||
},
|
||||
sponsor: "naltatis",
|
||||
};
|
||||
|
|
@ -48,9 +48,9 @@ UpdateVerfuegbar.args = {
|
|||
},
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 15231,
|
||||
chargedSelfConsumption: 15000,
|
||||
selfPercentage: 74,
|
||||
totalCharged: 15231,
|
||||
selfConsumptionCharged: 15000,
|
||||
selfConsumptionPercent: 74,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -64,9 +64,9 @@ Updater.args = {
|
|||
},
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 0,
|
||||
chargedSelfConsumption: 0,
|
||||
selfPercentage: 0,
|
||||
totalCharged: 0,
|
||||
selfConsumptionCharged: 0,
|
||||
selfConsumptionPercent: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -79,9 +79,9 @@ Upgrade.args = {
|
|||
},
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 0,
|
||||
chargedSelfConsumption: 0,
|
||||
selfPercentage: 0,
|
||||
totalCharged: 0,
|
||||
selfConsumptionCharged: 0,
|
||||
selfConsumptionPercent: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -94,8 +94,8 @@ Savings.args = {
|
|||
},
|
||||
savings: {
|
||||
since: 82800,
|
||||
chargedTotal: 15231,
|
||||
chargedSelfConsumption: 12231,
|
||||
selfPercentage: 80.3,
|
||||
totalCharged: 15231,
|
||||
selfConsumptionCharged: 12231,
|
||||
selfConsumptionPercent: 80.3,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(totalCharged * 1000, true, false),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="d-none d-sm-block">
|
||||
{{
|
||||
$t("footer.savings.modalTitleLong", {
|
||||
percent,
|
||||
total: fmtKw(totalCharged * 1000, true, false),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -45,7 +56,7 @@
|
|||
<fa-icon icon="square" class="text-evcc"></fa-icon>
|
||||
{{
|
||||
$t("footer.savings.modalChartSelf", {
|
||||
self: fmtKw(chargedSelfConsumption * 1000, true, false),
|
||||
self: fmtKw(selfConsumptionCharged * 1000, true, false),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
|
@ -53,7 +64,7 @@
|
|||
<fa-icon icon="square" class="text-grid"></fa-icon>
|
||||
{{
|
||||
$t("footer.savings.modalChartGrid", {
|
||||
grid: fmtKw(chargedGrid * 1000, true, false),
|
||||
grid: fmtKw(gridCharged * 1000, true, false),
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
|
@ -62,21 +73,21 @@
|
|||
class="chart d-flex justify-content-stretch mb-1 rounded overflow-hidden"
|
||||
>
|
||||
<div
|
||||
v-if="chargedTotal > 0"
|
||||
v-if="totalCharged > 0"
|
||||
class="chart-item chart-item--self d-flex justify-content-center text-white flex-shrink-1"
|
||||
:style="{ width: `${percent}%` }"
|
||||
>
|
||||
<span class="text-truncate"> {{ percent }}% </span>
|
||||
</div>
|
||||
<div
|
||||
v-if="chargedTotal > 0"
|
||||
v-if="totalCharged > 0"
|
||||
class="chart-item chart-item--grid d-flex justify-content-center text-white flex-shrink-1"
|
||||
:style="{ width: `${100 - percent}%` }"
|
||||
>
|
||||
<span class="text-truncate"> {{ 100 - percent }}% </span>
|
||||
</div>
|
||||
<div
|
||||
v-if="chargedTotal === 0"
|
||||
v-if="totalCharged === 0"
|
||||
class="chart-item chart-item--no-data d-flex justify-content-center text-white w-100"
|
||||
>
|
||||
<span>{{ $t("footer.savings.modalNoData") }}</span>
|
||||
|
|
@ -85,15 +96,15 @@
|
|||
</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">
|
||||
<a
|
||||
href="https://github.com/evcc-io/evcc/blob/master/README.md#energy-tariffs--savings-estimate"
|
||||
href="https://docs.evcc.io/docs/guides/setup/#ersparnisberechnung"
|
||||
target="_blank"
|
||||
class="text-muted"
|
||||
>
|
||||
|
|
@ -153,36 +164,21 @@ export default {
|
|||
components: { Sponsor },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
selfPercentage: Number,
|
||||
selfConsumptionPercent: Number,
|
||||
since: { type: Number, default: 0 },
|
||||
sponsor: String,
|
||||
chargedTotal: { type: Number, default: 0 },
|
||||
chargedSelfConsumption: { type: Number, default: 0 },
|
||||
gridPrice: { type: Number, default: 0.3 },
|
||||
feedInPrice: { type: Number, default: 0.08 },
|
||||
amount: { type: Number, default: 0 },
|
||||
effectivePrice: { type: Number, default: 0 },
|
||||
totalCharged: { type: Number, default: 0 },
|
||||
gridCharged: { type: Number, default: 0 },
|
||||
selfConsumptionCharged: { type: Number, default: 0 },
|
||||
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;
|
||||
return Math.round(this.selfConsumptionPercent) || 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -35,12 +36,12 @@ export default {
|
|||
experimentalText: "Unplausible Werte? Fragen zur Ansicht? Meld dich gerne in den",
|
||||
},
|
||||
sponsor: {
|
||||
thanks: "Danke für deine Unterstützung {sponsor}! Das hilft uns bei der Weiterentwicklung.",
|
||||
thanks: "Danke für deine Unterstützung, {sponsor}! Das hilft uns bei der Weiterentwicklung.",
|
||||
confetti: "Lust auf etwas Sponsor-Konfetti?",
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -35,12 +36,12 @@ export default {
|
|||
experimentalText: "Implausible values? Questions about this view? Feel free to join our ",
|
||||
},
|
||||
sponsor: {
|
||||
thanks: "Thanks for your support {sponsor}! It helps us with the further development.",
|
||||
thanks: "Thanks for your support, {sponsor}! It helps us with the further development.",
|
||||
confetti: "Ready for some sponsor confetti?",
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
maximumFractionDigits = 1;
|
||||
}
|
||||
return `${this.$n(value, { style: "decimal" })} ${unit}/kWh`;
|
||||
return `${this.$n(value, { style: "decimal", maximumFractionDigits })} ${unit}/kWh`;
|
||||
},
|
||||
fmtTimeAgo: function (elapsed) {
|
||||
const units = {
|
||||
|
|
|
|||
|
|
@ -103,9 +103,12 @@ export default {
|
|||
savings: function () {
|
||||
return {
|
||||
since: this.store.state.savingsSince,
|
||||
chargedTotal: this.store.state.savingsChargedTotal,
|
||||
chargedSelfConsumption: this.store.state.savingsChargedSelfConsumption,
|
||||
selfPercentage: this.store.state.savingsSelfPercentage,
|
||||
totalCharged: this.store.state.savingsTotalCharged,
|
||||
gridCharged: this.store.state.savingsGridCharged,
|
||||
selfConsumptionCharged: this.store.state.savingsSelfConsumptionCharged,
|
||||
amount: this.store.state.savingsAmount,
|
||||
effectivePrice: this.store.state.savingsEffectivePrice,
|
||||
selfConsumptionPercent: this.store.state.savingsSelfConsumptionPercent,
|
||||
gridPrice: this.store.state.tariffGrid,
|
||||
feedInPrice: this.store.state.tariffFeedIn,
|
||||
currency: this.store.state.currency,
|
||||
|
|
|
|||
131
core/savings.go
131
core/savings.go
|
|
@ -5,52 +5,76 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
// Site is the main configuration container. A site can host multiple loadpoints.
|
||||
type Savings struct {
|
||||
log *util.Logger
|
||||
const DefaultGridPrice = 0.30
|
||||
const DefaultFeedInPrice = 0.08
|
||||
|
||||
started time.Time // Boot time
|
||||
updated time.Time // Time of last charged value update
|
||||
chargedTotal float64 // Energy charged since startup (kWh)
|
||||
chargedSelfConsumption float64 // Self-produced energy charged since startup (kWh)
|
||||
Clock clock.Clock
|
||||
// publisher gives access to the site's publish function
|
||||
type publisher interface {
|
||||
publish(key string, val interface{})
|
||||
}
|
||||
|
||||
func NewSavings() Savings {
|
||||
// Site is the main configuration container. A site can host multiple loadpoints.
|
||||
type Savings struct {
|
||||
log *util.Logger
|
||||
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)
|
||||
selfConsumptionCharged float64 // Self-produced energy charged since startup (kWh)
|
||||
selfConsumptionCost float64 // Running total of charged self-produced energy cost (e.g. EUR)
|
||||
lastGridPrice float64 // Stores the last published grid price. Needed to detect price changes (Awattar, ..)
|
||||
}
|
||||
|
||||
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 {
|
||||
func (s *Savings) SelfConsumptionPercent() float64 {
|
||||
if s.TotalCharged() == 0 {
|
||||
return 0
|
||||
}
|
||||
return 100 / s.chargedTotal * s.chargedSelfConsumption
|
||||
return s.selfConsumptionCharged / s.TotalCharged() * 100
|
||||
}
|
||||
|
||||
func (s *Savings) ChargedTotal() float64 {
|
||||
return s.chargedTotal
|
||||
func (s *Savings) TotalCharged() float64 {
|
||||
return s.gridCharged + s.selfConsumptionCharged
|
||||
}
|
||||
|
||||
func (s *Savings) ChargedSelfConsumption() float64 {
|
||||
return s.chargedSelfConsumption
|
||||
func (s *Savings) CostTotal() float64 {
|
||||
return s.gridCost + s.selfConsumptionCost
|
||||
}
|
||||
|
||||
func (s *Savings) shareOfSelfProducedEnergy(gridPower float64, pvPower float64, batteryPower float64) float64 {
|
||||
func (s *Savings) EffectivePrice() float64 {
|
||||
if s.TotalCharged() == 0 {
|
||||
return s.currentGridPrice()
|
||||
}
|
||||
return s.CostTotal() / s.TotalCharged()
|
||||
}
|
||||
|
||||
func (s *Savings) SavingsAmount() float64 {
|
||||
return s.selfConsumptionCharged * (s.currentGridPrice() - s.currentFeedInPrice())
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -58,29 +82,60 @@ func (s *Savings) shareOfSelfProducedEnergy(gridPower float64, pvPower float64,
|
|||
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()
|
||||
|
||||
selfPercentage := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
|
||||
|
||||
updateDuration := now.Sub(s.updated)
|
||||
|
||||
// assuming the charge power was constant over the duration -> rough estimate
|
||||
addedEnergy := updateDuration.Hours() * chargePower / 1000
|
||||
|
||||
s.chargedTotal += addedEnergy
|
||||
s.chargedSelfConsumption += addedEnergy * (selfPercentage / 100)
|
||||
s.updated = now
|
||||
|
||||
s.log.DEBUG.Printf("%.1fkWh charged since %s", s.chargedTotal, time.Since(s.started).Round(time.Second))
|
||||
s.log.DEBUG.Printf("%.1fkWh own energy (%.1f%%)", s.chargedSelfConsumption, s.SelfPercentage())
|
||||
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) Update(p publisher, gridPower, pvPower, batteryPower, chargePower float64) {
|
||||
// assume charge power as constant over the duration -> rough kWh estimate
|
||||
energyAdded := s.clock.Since(s.updated).Hours() * chargePower / 1e3
|
||||
|
||||
// nothing meaningfull changed, no need to update
|
||||
if energyAdded == 0 && s.lastGridPrice == s.currentGridPrice() {
|
||||
return
|
||||
}
|
||||
|
||||
share := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
|
||||
|
||||
addedSelfConsumption := energyAdded * share
|
||||
addedGrid := energyAdded - addedSelfConsumption
|
||||
|
||||
s.gridCharged += addedGrid
|
||||
s.gridCost += addedGrid * s.currentGridPrice()
|
||||
s.selfConsumptionCharged += addedSelfConsumption
|
||||
s.selfConsumptionCost += addedSelfConsumption * s.currentFeedInPrice()
|
||||
s.lastGridPrice = s.currentGridPrice()
|
||||
s.updated = s.clock.Now()
|
||||
|
||||
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())
|
||||
p.publish("tariffGrid", s.currentGridPrice())
|
||||
p.publish("tariffFeedIn", s.currentFeedInPrice())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,24 @@ import (
|
|||
"github.com/evcc-io/evcc/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, s *Savings, total float64, self float64, percentage float64) {
|
||||
if !compareWithTolerane(s.ChargedTotal(), total) {
|
||||
t.Errorf("ChargedTotal was incorrect, got: %.3f, want: %.3f.", s.ChargedTotal(), total)
|
||||
func assertEnergy(t *testing.T, s *Savings, total, self, percentage float64) {
|
||||
if !compareWithTolerane(s.TotalCharged(), total) {
|
||||
t.Errorf("TotalCharged was incorrect, got: %.3f, want: %.3f.", s.TotalCharged(), total)
|
||||
}
|
||||
if !compareWithTolerane(s.ChargedSelfConsumption(), self) {
|
||||
t.Errorf("ChargedSelfConsumption was incorrect, got: %.3f, want: %.3f.", s.ChargedSelfConsumption(), self)
|
||||
if !compareWithTolerane(s.selfConsumptionCharged, self) {
|
||||
t.Errorf("ChargedSelfConsumption was incorrect, got: %.3f, want: %.3f.", s.selfConsumptionCharged, self)
|
||||
}
|
||||
if int(s.SelfPercentage()) != int(percentage) {
|
||||
t.Errorf("SelfPercentage was incorrect, got: %.1f, want: %.1f.", s.SelfPercentage(), percentage)
|
||||
if int(s.SelfConsumptionPercent()) != int(percentage) {
|
||||
t.Errorf("SelfConsumptionPercent was incorrect, got: %.1f, want: %.1f.", s.SelfConsumptionPercent(), percentage)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPrices(t *testing.T, s *Savings, effectivePrice, savingsAmount float64) {
|
||||
if !compareWithTolerane(s.EffectivePrice(), effectivePrice) {
|
||||
t.Errorf("EffectivePrice was incorrect, got: %.3f, want: %.3f.", s.EffectivePrice(), effectivePrice)
|
||||
}
|
||||
if !compareWithTolerane(s.SavingsAmount(), savingsAmount) {
|
||||
t.Errorf("SavingsAmount was incorrect, got: %.3f, want: %.3f.", s.SavingsAmount(), savingsAmount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,94 +36,189 @@ func compareWithTolerane(a, b float64) bool {
|
|||
return diff < tolerance
|
||||
}
|
||||
|
||||
type StubPublisher struct{}
|
||||
|
||||
func (p StubPublisher) publish(key string, val interface{}) {}
|
||||
|
||||
func TestSavingsWithChangingEnergySources(t *testing.T) {
|
||||
mockClock := clock.NewMock()
|
||||
p := StubPublisher{}
|
||||
|
||||
clck := clock.NewMock()
|
||||
s := &Savings{
|
||||
log: util.NewLogger("savings"),
|
||||
started: mockClock.Now(),
|
||||
updated: mockClock.Now(),
|
||||
Clock: mockClock,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
started: clck.Now(),
|
||||
updated: clck.Now(),
|
||||
}
|
||||
|
||||
s.Update(0, 0, 0, 0)
|
||||
tc := []struct {
|
||||
title string
|
||||
grid, pv, battery, charge float64
|
||||
total, self, percentage float64
|
||||
}{
|
||||
{"half grid, half pv",
|
||||
2500, 2500, 0, 5000,
|
||||
5, 2.5, 50},
|
||||
{"full pv",
|
||||
0, 5000, 0, 5000,
|
||||
10, 7.5, 75},
|
||||
{"full grid",
|
||||
5000, 0, 0, 5000,
|
||||
15, 7.5, 50},
|
||||
{"half grid, half battery",
|
||||
2500, 0, 2500, 5000,
|
||||
20, 10, 50},
|
||||
{"full pv, pv export",
|
||||
-5000, 10000, 0, 5000,
|
||||
25, 15, 60},
|
||||
{"full pv, pv export, battery charge",
|
||||
-2500, 10000, -2500, 5000,
|
||||
30, 20, 66},
|
||||
{"double charge speed, full grid",
|
||||
10000, 0, 0, 10000,
|
||||
40, 20, 50},
|
||||
}
|
||||
|
||||
// half grid, half pv
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(2500, 2500, 0, 5000)
|
||||
assert(t, s, 5, 2.5, 50)
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
|
||||
// full pv
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(0, 5000, 0, 5000)
|
||||
assert(t, s, 10, 7.5, 75)
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
// full grid
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(5000, 0, 0, 5000)
|
||||
assert(t, s, 15, 7.5, 50)
|
||||
|
||||
// half grid, half battery
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(2500, 0, 2500, 5000)
|
||||
assert(t, s, 20, 10, 50)
|
||||
|
||||
// full pv, pv export
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(-5000, 10000, 0, 5000)
|
||||
assert(t, s, 25, 15, 60)
|
||||
|
||||
// full pv, pv export, battery charge
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(-2500, 10000, -2500, 5000)
|
||||
assert(t, s, 30, 20, 66)
|
||||
|
||||
// double charge speed, full grid
|
||||
mockClock.Add(time.Hour)
|
||||
s.Update(10000, 0, 0, 10000)
|
||||
assert(t, s, 40, 20, 50)
|
||||
clck.Add(time.Hour)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
assertEnergy(t, s, tc.total, tc.self, tc.percentage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavingsWithDifferentTimespans(t *testing.T) {
|
||||
mockClock := clock.NewMock()
|
||||
p := StubPublisher{}
|
||||
|
||||
clck := clock.NewMock()
|
||||
s := &Savings{
|
||||
log: util.NewLogger("savings"),
|
||||
started: mockClock.Now(),
|
||||
updated: mockClock.Now(),
|
||||
Clock: mockClock,
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
started: clck.Now(),
|
||||
updated: clck.Now(),
|
||||
}
|
||||
|
||||
s.Update(0, 0, 0, 0)
|
||||
type tcStep = struct {
|
||||
dt time.Duration
|
||||
grid, pv, battery, charge float64
|
||||
}
|
||||
|
||||
// 10 second 11kW charging, full grid
|
||||
mockClock.Add(10 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
assert(t, s, 0.030556, 0, 0) // 30,555Wh
|
||||
tc := []struct {
|
||||
title string
|
||||
steps []tcStep
|
||||
total, self, percentage float64
|
||||
}{
|
||||
{"10 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{10 * time.Second, 0, 0, 0, 11000},
|
||||
},
|
||||
0.030556, 0, 0, // 30,555Wh
|
||||
},
|
||||
{"10 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{10 * time.Second, 0, 0, 0, 11000},
|
||||
},
|
||||
0.061111, 0, 0, // 61,111Wh
|
||||
},
|
||||
{"5x 2 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
},
|
||||
0.092, 0, 0, // 91,666Wh
|
||||
},
|
||||
{"30 min 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{30 * time.Minute, 0, 0, 0, 11000},
|
||||
},
|
||||
5.592, 0, 0, // 5561,111Wh
|
||||
},
|
||||
{"4 hours 11kW charging, full pv",
|
||||
[]tcStep{
|
||||
{4 * time.Hour, 0, 11000, 0, 11000},
|
||||
},
|
||||
49.592, 44, 88,
|
||||
},
|
||||
}
|
||||
|
||||
// 10 second 11kW charging, full grid
|
||||
mockClock.Add(10 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
assert(t, s, 0.061111, 0, 0) // 61,111Wh
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
|
||||
// 5x 2 second 11kW charging, full grid
|
||||
mockClock.Add(2 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
mockClock.Add(2 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
mockClock.Add(2 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
mockClock.Add(2 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
mockClock.Add(2 * time.Second)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
assert(t, s, 0.092, 0, 0) // 91,666Wh
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
// 30 min 11kW charging, full grid
|
||||
mockClock.Add(30 * time.Minute)
|
||||
s.Update(11000, 0, 0, 11000)
|
||||
assert(t, s, 5.592, 0, 0) // 5561,111Wh
|
||||
for _, tc := range tc.steps {
|
||||
clck.Add(tc.dt)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
}
|
||||
|
||||
// 4 hours 11kW charging, full pv
|
||||
mockClock.Add(4 * time.Hour)
|
||||
s.Update(0, 11000, 0, 11000)
|
||||
assert(t, s, 49.592, 44, 88)
|
||||
assertEnergy(t, s, tc.total, tc.self, tc.percentage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveEnergyPriceAndSavingsAmount(t *testing.T) {
|
||||
p := StubPublisher{}
|
||||
|
||||
clck := clock.NewMock()
|
||||
|
||||
type tcStep = struct {
|
||||
dt time.Duration
|
||||
grid, pv, battery, charge float64
|
||||
}
|
||||
|
||||
tc := []struct {
|
||||
title string
|
||||
steps []tcStep
|
||||
effectivePrice, savingsAmount float64
|
||||
}{
|
||||
{"1 hour, 10kW, full grid",
|
||||
[]tcStep{
|
||||
{time.Hour, 10000, 0, 0, 10000},
|
||||
},
|
||||
0.3, 0,
|
||||
},
|
||||
{"1 hour, 10kW, full pv",
|
||||
[]tcStep{
|
||||
{time.Hour, 0, 10000, 0, 10000},
|
||||
},
|
||||
0.08, 2.2,
|
||||
},
|
||||
{"1 hour, 10kW, full battery",
|
||||
[]tcStep{
|
||||
{time.Hour, 0, 0, 10000, 10000},
|
||||
},
|
||||
0.08, 2.2,
|
||||
},
|
||||
|
||||
{"1 hour, 10kW, half grid, half pv",
|
||||
[]tcStep{
|
||||
{time.Hour, 5000, 0, 5000, 10000},
|
||||
},
|
||||
0.19, 1.1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
s := &Savings{
|
||||
log: util.NewLogger("foo"),
|
||||
clock: clck,
|
||||
started: clck.Now(),
|
||||
updated: clck.Now(),
|
||||
}
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
|
||||
for _, tc := range tc.steps {
|
||||
clck.Add(tc.dt)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
}
|
||||
|
||||
assertPrices(t, s, tc.effectivePrice, tc.savingsAmount)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
core/site.go
38
core/site.go
|
|
@ -45,7 +45,7 @@ type Site struct {
|
|||
|
||||
tariffs tariff.Tariffs // Tariff
|
||||
loadpoints []*LoadPoint // Loadpoints
|
||||
savings Savings // Savings
|
||||
savings *Savings // Savings
|
||||
|
||||
// cached state
|
||||
gridPower float64 // Grid power
|
||||
|
|
@ -77,9 +77,9 @@ func NewSiteFromConfig(
|
|||
}
|
||||
|
||||
Voltage = site.Voltage
|
||||
site.tariffs = tariffs
|
||||
site.loadpoints = loadpoints
|
||||
site.savings = NewSavings()
|
||||
site.tariffs = tariffs
|
||||
site.savings = NewSavings(tariffs)
|
||||
|
||||
if site.Meters.GridMeterRef != "" {
|
||||
site.gridMeter = cp.Meter(site.Meters.GridMeterRef)
|
||||
|
|
@ -393,24 +393,6 @@ func (site *Site) sitePower() (float64, error) {
|
|||
sitePower := sitePower(site.gridPower, batteryPower, site.ResidualPower)
|
||||
site.log.DEBUG.Printf("site power: %.0fW", sitePower)
|
||||
|
||||
site.publish("savingsChargedTotal", site.savings.ChargedTotal())
|
||||
site.publish("savingsChargedSelfConsumption", site.savings.ChargedSelfConsumption())
|
||||
site.publish("savingsSelfPercentage", site.savings.SelfPercentage())
|
||||
site.publish("savingsSince", site.savings.Since())
|
||||
|
||||
site.publish("currency", site.tariffs.Currency.String())
|
||||
|
||||
if site.tariffs.Grid != nil {
|
||||
if gridPrice, err := site.tariffs.Grid.CurrentPrice(); err == nil {
|
||||
site.publish("tariffGrid", gridPrice)
|
||||
}
|
||||
}
|
||||
if site.tariffs.FeedIn != nil {
|
||||
if feedInPrice, err := site.tariffs.FeedIn.CurrentPrice(); err == nil {
|
||||
site.publish("tariffFeedIn", feedInPrice)
|
||||
}
|
||||
}
|
||||
|
||||
return sitePower, nil
|
||||
}
|
||||
|
||||
|
|
@ -441,12 +423,20 @@ func (site *Site) update(lp Updater) {
|
|||
}
|
||||
|
||||
// update savings
|
||||
|
||||
// TODO: use a proper interface, use meter readings instead of current power for better results
|
||||
var totalChargePower = 0.0
|
||||
var totalChargePower float64
|
||||
for _, lp := range site.loadpoints {
|
||||
totalChargePower += lp.chargePower
|
||||
}
|
||||
site.savings.Update(site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
|
||||
|
||||
site.savings.Update(site, site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
|
||||
}
|
||||
|
||||
// prepare publishes initial values
|
||||
func (site *Site) prepare() {
|
||||
site.publish("currency", site.tariffs.Currency.String())
|
||||
site.publish("savingsSince", site.savings.Since())
|
||||
}
|
||||
|
||||
// Prepare attaches communication channels to site and loadpoints
|
||||
|
|
@ -454,6 +444,8 @@ func (site *Site) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Event)
|
|||
site.uiChan = uiChan
|
||||
site.lpUpdateChan = make(chan *LoadPoint, 1) // 1 capacity to avoid deadlock
|
||||
|
||||
site.prepare()
|
||||
|
||||
for id, lp := range site.loadpoints {
|
||||
lpUIChan := make(chan util.Param)
|
||||
lpPushChan := make(chan push.Event)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.a26a8c8b.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.b0ecc7d3.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.a26a8c8b.css" rel="stylesheet"></head><body><script>window.evcc = {
|
||||
<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="EV Charge Controller"><meta name="author" content="andig"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-status-bar-style" content="black"><link rel="apple-touch-icon" sizes="180x180" href="ico/apple-touch-icon.png?[[.Version]]"><link rel="icon" type="image/png" sizes="32x32" href="ico/favicon-32x32.png?[[.Version]]"><link rel="icon" type="image/png" sizes="16x16" href="ico/favicon-16x16.png?[[.Version]]"><link rel="manifest" href="ico/site.webmanifest"><link rel="mask-icon" href="ico/safari-pinned-tab.svg?[[.Version]]" color="#18191a"><link rel="shortcut icon" href="ico/favicon.ico?[[.Version]]"><meta name="apple-mobile-web-app-title" content="evcc"><meta name="application-name" content="evcc"><meta name="msapplication-TileColor" content="#18191a"><meta name="msapplication-config" content="ico/browserconfig.xml"><meta name="theme-color" content="#18191a"><title>evcc</title><link href="css/chunk-vendors.4692b1e2.css" rel="preload" as="style"><link href="css/index.77f18cb6.css" rel="preload" as="style"><link href="js/chunk-vendors.323cc329.js" rel="preload" as="script"><link href="js/index.2b1f2a0a.js" rel="preload" as="script"><link href="css/chunk-vendors.4692b1e2.css" rel="stylesheet"><link href="css/index.77f18cb6.css" rel="stylesheet"></head><body><script>window.evcc = {
|
||||
version: "[[.Version]]",
|
||||
configured: "[[.Configured]]",
|
||||
commit: "[[.Commit]]",
|
||||
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.b0ecc7d3.js"></script></body></html>
|
||||
};</script><div id="app"></div><script src="js/chunk-vendors.323cc329.js"></script><script src="js/index.2b1f2a0a.js"></script></body></html>
|
||||
2
dist/js/index.2b1f2a0a.js
vendored
Normal file
2
dist/js/index.2b1f2a0a.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/index.2b1f2a0a.js.map
vendored
Normal file
1
dist/js/index.2b1f2a0a.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/js/index.b0ecc7d3.js
vendored
2
dist/js/index.b0ecc7d3.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/index.b0ecc7d3.js.map
vendored
1
dist/js/index.b0ecc7d3.js.map
vendored
File diff suppressed because one or more lines are too long
63
package-lock.json
generated
63
package-lock.json
generated
|
|
@ -34290,7 +34290,8 @@
|
|||
"@fortawesome/vue-fontawesome": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.6.tgz",
|
||||
"integrity": "sha512-V3vT3flY15AKbUS31aZOP12awQI3aAzkr2B1KnqcHLmwrmy51DW3pwyBczKdypV8QxBZ8U68Hl2XxK2nudTxpg=="
|
||||
"integrity": "sha512-V3vT3flY15AKbUS31aZOP12awQI3aAzkr2B1KnqcHLmwrmy51DW3pwyBczKdypV8QxBZ8U68Hl2XxK2nudTxpg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@gar/promisify": {
|
||||
"version": "1.1.2",
|
||||
|
|
@ -36267,7 +36268,8 @@
|
|||
"version": "1.6.22",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz",
|
||||
"integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@mdx-js/util": {
|
||||
"version": "1.6.22",
|
||||
|
|
@ -38322,7 +38324,8 @@
|
|||
"version": "4.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.15.tgz",
|
||||
"integrity": "sha512-fqap+4HN+w+InDxlA3hZTOGE0tzBTgXhKLoDydhywqgmhQ1D9JA6Feh94ze6tG8DsWX58/ujYUqA8jAz17FJtg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/cli-service": {
|
||||
"version": "4.5.15",
|
||||
|
|
@ -38950,7 +38953,8 @@
|
|||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
||||
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@vue/reactivity-transform": {
|
||||
"version": "3.2.26",
|
||||
|
|
@ -39231,7 +39235,8 @@
|
|||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"acorn-walk": {
|
||||
"version": "7.2.0",
|
||||
|
|
@ -39296,13 +39301,15 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
||||
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"alphanum-sort": {
|
||||
"version": "1.0.2",
|
||||
|
|
@ -39791,7 +39798,8 @@
|
|||
"version": "7.0.0-bridge.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
|
||||
"integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "24.9.0",
|
||||
|
|
@ -40801,7 +40809,8 @@
|
|||
"bootstrap": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
|
||||
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q=="
|
||||
"integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"boxen": {
|
||||
"version": "5.1.2",
|
||||
|
|
@ -44070,13 +44079,15 @@
|
|||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz",
|
||||
"integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-config-standard": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz",
|
||||
"integrity": "sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
"version": "0.3.6",
|
||||
|
|
@ -44303,13 +44314,15 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz",
|
||||
"integrity": "sha512-SftLb1pUG01QYq2A/hGAWfDRXqYD82zE7j7TopDOyNdU+7SvvoXREls/+PRTY17vUXzXnZA/zfnyKgRH6x4JJw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-standard": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz",
|
||||
"integrity": "sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "7.20.0",
|
||||
|
|
@ -48008,7 +48021,8 @@
|
|||
"version": "7.5.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
|
||||
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -48462,7 +48476,8 @@
|
|||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||
"integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "26.0.0",
|
||||
|
|
@ -50426,7 +50441,8 @@
|
|||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.5.tgz",
|
||||
"integrity": "sha512-YQEMMMCX3PYOWtUAQu8Fmz5/sH09s17eyQnDubwaAo8sWmnRTT1og96EFv1vL59l4nWfmtF3L91pqkuheVqRlA==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.5",
|
||||
|
|
@ -53328,7 +53344,8 @@
|
|||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.5.1.tgz",
|
||||
"integrity": "sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.14.0",
|
||||
|
|
@ -57043,13 +57060,15 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.2.1.tgz",
|
||||
"integrity": "sha512-6+X1FLlIcjvFMAeAD/hcxDT8tmyrWnbSPMU0EnxQuDLIxokuFzWliXBiYZuGIx+mrAMLBw0WFfCkaPw8ebzAhw==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"use-isomorphic-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"use-latest": {
|
||||
"version": "1.2.0",
|
||||
|
|
@ -58461,7 +58480,8 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-filter-warnings-plugin/-/webpack-filter-warnings-plugin-1.2.1.tgz",
|
||||
"integrity": "sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"webpack-hot-middleware": {
|
||||
"version": "2.25.1",
|
||||
|
|
@ -58723,7 +58743,8 @@
|
|||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.4.0.tgz",
|
||||
"integrity": "sha512-IHVsKe2pjajSUIl4KYMQOdlyliovpEPquKkqbwswulszzI7r0SfQrxnXdWAEqOlDCLrVSJzo+O1hAwdog2sKSQ==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"x-default-browser": {
|
||||
"version": "0.4.0",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue