diff --git a/README.md b/README.md
index 0aa6db590..c42993df1 100644
--- a/README.md
+++ b/README.md
@@ -335,22 +335,44 @@ to the configuration. The EVCC loadpoints can then be added to the SHM configura
Sunny-Portal via the "Optional energy demand" slider. When the amount of configured PV is not available, charging suspends like in **PV** mode. So, pushing the slider completely
to the left makes **Min+PV** behave as described above. Pushing completely to the right makes **Min+PV** mode behave like **PV** mode.
+### Energy Tariffs & Savings Estimate
+
+You can specify your energy tariffs and grid feed-in rates. Evcc uses these values for a rough savings calculation shown in the web UI.
+
+```yaml
+tariffs:
+ currency: EUR # (default EUR)
+ grid:
+ # static grid price
+ type: fixed
+ price: 0.294 # EUR/kWh
+
+ feedin:
+ # rate for feeding excess (pv) energy to the grid
+ type: fixed
+ price: 0.08 # EUR/kWh
+```
+
+For the savings estimate calculation evcc roughly records the total amount of energy charged and the used energy sources (grid, battery, pv). PV direct usage and energy provided by the battery system are treated equally in this calculation. Battery losses due to conversion are not taken into account. The algorithm distinguishes between grid energy and self-produced solar energy (pv, battery). The cost advantage of your solar energy is the difference between you grid import price (e.g. 0.294 EUR/kWh) and your feedin rate (e.g. 0.08 EUR/kWh). If you are not compensated for feedin energy to the grid you can set the feedin price to 0.
+If at one time your pulling energy from multiple sources (e.g. 50% grid, 50% pv) the algorithm will distribute the energy sources evenly. It assumes that your house consumption and your active loadpoints are each using a 50/50 energy mix.
+At the moment the algorithm only uses the current energy price. Flexible prices (see below) or day/night-tariffs are not taken into account. The energy amounts and savings are currently not persistant and will be reset after a service restart.
+
### Flexible Energy Tariffs
-EVCC supports flexible energy tariffs as offered by [Awattar](https://www.awattar.de) or [Tibber](https://tibber.com). Configuration allows to define a "cheap" rate at which charging from grid is enabled at highest possible rate even when not enough PV power is locally available:
+Evcc supports flexible energy tariffs as offered by [Awattar](https://www.awattar.de) or [Tibber](https://tibber.com). Configuration allows to define a "cheap" rate at which charging from grid is enabled at highest possible rate even when not enough PV power is locally available:
```yaml
tariffs:
grid:
# either
type: tibber
- cheap: 20 # ct/kWh
+ cheap: 0.2 # EUR/kWh
token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4" # access token
homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c" # optional if multiple homes associated to account
# or
type: awattar
- cheap: 20 # ct/kWh
+ cheap: 0.2 # EUR/kWh
region: de # optional, choose at for Austria
```
diff --git a/api/api.go b/api/api.go
index ddaf2eb62..767e0b04c 100644
--- a/api/api.go
+++ b/api/api.go
@@ -181,5 +181,6 @@ type VehicleStopCharge interface {
}
type Tariff interface {
- IsCheap() bool
+ IsCheap() (bool, error)
+ CurrentPrice() (float64, error) // EUR/kWh, CHF/kWh, ...
}
diff --git a/assets/css/app.css b/assets/css/app.css
index 02abf19d6..6d357dec6 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -27,14 +27,40 @@ a {
opacity: 0.25;
}
-.btn-primary {
+.btn-primary,
+.btn-primary:focus {
background-color: var(--bs-gray-dark);
border-color: var(--bs-gray-dark);
}
-.btn-primary:hover {
+.btn-primary:hover,
+.btn-primary:active {
background-color: var(--evcc-dark-green);
border-color: var(--evcc-dark-green);
}
+.btn-outline-primary {
+ color: var(--bs-gray-dark);
+ border-color: var(--bs-gray-dark);
+}
+.btn-outline-primary:hover {
+ background-color: var(--evcc-dark-green);
+ border-color: var(--evcc-dark-green);
+}
+.text-evcc {
+ color: var(--evcc-dark-green);
+}
+
+.text-grid {
+ color: var(--evcc-grid);
+}
+
+@media screen and (min-width: 400px) and (max-width: 574px) {
+ .d-xs-none {
+ display: none !important;
+ }
+ .d-xs-inline {
+ display: inline !important;
+ }
+}
.cursor-pointer {
cursor: pointer;
diff --git a/assets/index.html b/assets/index.html
index 2af672b7d..7ba8be19b 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -27,6 +27,7 @@
window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
+ commit: "[[.Commit]]",
};
diff --git a/assets/js/components/Footer.stories.js b/assets/js/components/Footer.stories.js
index b7af49992..7c9eee08d 100644
--- a/assets/js/components/Footer.stories.js
+++ b/assets/js/components/Footer.stories.js
@@ -17,6 +17,26 @@ const Template = (args, { argTypes }) => ({
export const KeinUpdate = Template.bind({});
KeinUpdate.args = {
version: { installed: "0.40" },
+ savings: {
+ since: 82800,
+ chargedTotal: 15231,
+ chargedSelfConsumption: 12231,
+ selfPercentage: 80.3,
+ },
+};
+
+export const Sponsor = Template.bind({});
+Sponsor.args = {
+ version: {
+ installed: "0.36",
+ },
+ savings: {
+ since: 82800,
+ chargedTotal: 21000,
+ chargedSelfConsumption: 12000,
+ selfPercentage: 54,
+ },
+ sponsor: "naltatis",
};
export const UpdateVerfuegbar = Template.bind({});
@@ -26,15 +46,12 @@ UpdateVerfuegbar.args = {
available: "0.40",
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
},
-};
-
-export const Sponsor = Template.bind({});
-Sponsor.args = {
- version: {
- installed: "0.36",
- available: "0.40",
+ savings: {
+ since: 82800,
+ chargedTotal: 15231,
+ chargedSelfConsumption: 15000,
+ selfPercentage: 74,
},
- sponsor: "naltatis",
};
export const Updater = Template.bind({});
@@ -45,6 +62,12 @@ Updater.args = {
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
hasUpdater: true,
},
+ savings: {
+ since: 82800,
+ chargedTotal: 0,
+ chargedSelfConsumption: 0,
+ selfPercentage: 0,
+ },
};
export const Upgrade = Template.bind({});
@@ -54,4 +77,25 @@ Upgrade.args = {
available: "0.40",
hasUpdater: true,
},
+ savings: {
+ since: 82800,
+ chargedTotal: 0,
+ chargedSelfConsumption: 0,
+ selfPercentage: 0,
+ },
+};
+
+export const Savings = Template.bind({});
+Savings.args = {
+ version: {
+ installed: "0.36",
+ available: "0.40",
+ hasUpdater: true,
+ },
+ savings: {
+ since: 82800,
+ chargedTotal: 15231,
+ chargedSelfConsumption: 12231,
+ selfPercentage: 80.3,
+ },
};
diff --git a/assets/js/components/Footer.vue b/assets/js/components/Footer.vue
index 5ef2e004f..ad51104c8 100644
--- a/assets/js/components/Footer.vue
+++ b/assets/js/components/Footer.vue
@@ -4,7 +4,7 @@
-
+
@@ -12,14 +12,15 @@
diff --git a/assets/js/components/Notifications.vue b/assets/js/components/Notifications.vue
index 4adefbebe..d28834e81 100644
--- a/assets/js/components/Notifications.vue
+++ b/assets/js/components/Notifications.vue
@@ -37,7 +37,7 @@
class="d-flex justify-content-end mt-3"
:title="fmtAbsoluteDate(msg.time)"
>
- {{ fmtTimeAgo(msg.time) }}
+ {{ fmtTimeAgo(msg.time - new Date()) }}
+
+
+ {{
+ $t("footer.savings.footerShort", { percent })
+ }} {{
+ $t("footer.savings.footerLong", { percent })
+ }}
+
+
+
+
+
+
+
diff --git a/assets/js/components/Sponsor.vue b/assets/js/components/Sponsor.vue
index c91225186..dc540ee01 100644
--- a/assets/js/components/Sponsor.vue
+++ b/assets/js/components/Sponsor.vue
@@ -1,27 +1,45 @@
-
@@ -80,6 +80,7 @@ export default {
compact: false,
store: this.$root.$data.store,
installedVersion: window.evcc.version,
+ commit: window.evcc.commit,
};
},
computed: {
@@ -99,6 +100,17 @@ export default {
sponsor: function () {
return this.store.state.sponsor;
},
+ savings: function () {
+ return {
+ since: this.store.state.savingsSince,
+ chargedTotal: this.store.state.savingsChargedTotal,
+ chargedSelfConsumption: this.store.state.savingsChargedSelfConsumption,
+ selfPercentage: this.store.state.savingsSelfPercentage,
+ gridPrice: this.store.state.tariffGrid,
+ feedInPrice: this.store.state.tariffFeedIn,
+ currency: this.store.state.currency,
+ };
+ },
},
created: function () {
const urlParams = new URLSearchParams(window.location.search);
diff --git a/cmd/config.go b/cmd/config.go
index b794cb760..9831ad363 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -65,7 +65,9 @@ type messagingConfig struct {
}
type tariffConfig struct {
- Grid typedConfig
+ Currency string
+ Grid typedConfig
+ FeedIn typedConfig
}
// ConfigProvider provides configuration items
diff --git a/cmd/setup.go b/cmd/setup.go
index 9bece6170..4fb3c10bb 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -24,6 +24,7 @@ import (
"github.com/evcc-io/evcc/util/pipe"
"github.com/evcc-io/evcc/util/sponsor"
"github.com/spf13/viper"
+ "golang.org/x/text/currency"
)
func init() {
@@ -180,16 +181,30 @@ func configureMessengers(conf messagingConfig, cache *util.Cache) chan push.Even
return notificationChan
}
-func configureTariffs(conf tariffConfig) (t api.Tariff, err error) {
+func configureTariffs(conf tariffConfig) (tariff.Tariffs, error) {
+ var grid, feedin api.Tariff
+ var currencyCode currency.Unit = currency.EUR
+ var err error
+
+ if conf.Currency != "" {
+ currencyCode = currency.MustParseISO(conf.Currency)
+ }
+
if conf.Grid.Type != "" {
- t, err = tariff.NewFromConfig(conf.Grid.Type, conf.Grid.Other)
+ grid, err = tariff.NewFromConfig(conf.Grid.Type, conf.Grid.Other)
+ }
+
+ if err == nil && conf.FeedIn.Type != "" {
+ feedin, err = tariff.NewFromConfig(conf.FeedIn.Type, conf.FeedIn.Other)
}
if err != nil {
err = fmt.Errorf("failed configuring tariff: %w", err)
}
- return t, err
+ tariffs := tariff.NewTariffs(currencyCode, grid, feedin)
+
+ return *tariffs, err
}
func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) {
@@ -197,21 +212,21 @@ func configureSiteAndLoadpoints(conf config) (site *core.Site, err error) {
var loadPoints []*core.LoadPoint
loadPoints, err = configureLoadPoints(conf, cp)
- var tariff api.Tariff
+ var tariffs tariff.Tariffs
if err == nil {
- tariff, err = configureTariffs(conf.Tariffs)
+ tariffs, err = configureTariffs(conf.Tariffs)
}
if err == nil {
- site, err = configureSite(conf.Site, cp, loadPoints, tariff)
+ site, err = configureSite(conf.Site, cp, loadPoints, tariffs)
}
}
return site, err
}
-func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, tariff api.Tariff) (*core.Site, error) {
- site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, tariff)
+func configureSite(conf map[string]interface{}, cp *ConfigProvider, loadPoints []*core.LoadPoint, tariffs tariff.Tariffs) (*core.Site, error) {
+ site, err := core.NewSiteFromConfig(log, cp, conf, loadPoints, tariffs)
if err != nil {
return nil, fmt.Errorf("failed configuring site: %w", err)
}
diff --git a/core/savings.go b/core/savings.go
new file mode 100644
index 000000000..41b767d81
--- /dev/null
+++ b/core/savings.go
@@ -0,0 +1,86 @@
+package core
+
+import (
+ "math"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ "github.com/evcc-io/evcc/util"
+)
+
+// Site is the main configuration container. A site can host multiple loadpoints.
+type Savings struct {
+ log *util.Logger
+
+ 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
+}
+
+func NewSavings() Savings {
+ clock := clock.New()
+ savings := &Savings{
+ log: util.NewLogger("savings"),
+ started: clock.Now(),
+ updated: clock.Now(),
+ Clock: clock,
+ }
+
+ return *savings
+}
+
+func (s *Savings) Since() time.Duration {
+ return time.Since(s.started)
+}
+
+func (s *Savings) SelfPercentage() float64 {
+ if s.chargedTotal == 0 {
+ return 0
+ }
+ return 100 / s.chargedTotal * s.chargedSelfConsumption
+}
+
+func (s *Savings) ChargedTotal() float64 {
+ return s.chargedTotal
+}
+
+func (s *Savings) ChargedSelfConsumption() float64 {
+ return s.chargedSelfConsumption
+}
+
+func (s *Savings) shareOfSelfProducedEnergy(gridPower float64, pvPower float64, 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
+
+ if math.IsNaN(selfPercentage) {
+ return 0
+ }
+
+ return selfPercentage
+}
+
+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())
+}
diff --git a/core/savings_test.go b/core/savings_test.go
new file mode 100644
index 000000000..7ac02aa83
--- /dev/null
+++ b/core/savings_test.go
@@ -0,0 +1,120 @@
+package core
+
+import (
+ "math"
+ "testing"
+ "time"
+
+ "github.com/benbjohnson/clock"
+ "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)
+ }
+ if !compareWithTolerane(s.ChargedSelfConsumption(), self) {
+ t.Errorf("ChargedSelfConsumption was incorrect, got: %.3f, want: %.3f.", s.ChargedSelfConsumption(), self)
+ }
+ if int(s.SelfPercentage()) != int(percentage) {
+ t.Errorf("SelfPercentage was incorrect, got: %.1f, want: %.1f.", s.SelfPercentage(), percentage)
+ }
+}
+
+func compareWithTolerane(a, b float64) bool {
+ tolerance := 0.001
+ diff := math.Abs(a - b)
+ return diff < tolerance
+}
+
+func TestSavingsWithChangingEnergySources(t *testing.T) {
+ mockClock := clock.NewMock()
+ s := &Savings{
+ log: util.NewLogger("savings"),
+ started: mockClock.Now(),
+ updated: mockClock.Now(),
+ Clock: mockClock,
+ }
+
+ s.Update(0, 0, 0, 0)
+
+ // half grid, half pv
+ mockClock.Add(time.Hour)
+ s.Update(2500, 2500, 0, 5000)
+ assert(t, s, 5, 2.5, 50)
+
+ // full pv
+ mockClock.Add(time.Hour)
+ s.Update(0, 5000, 0, 5000)
+ assert(t, s, 10, 7.5, 75)
+
+ // 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)
+}
+
+func TestSavingsWithDifferentTimespans(t *testing.T) {
+ mockClock := clock.NewMock()
+ s := &Savings{
+ log: util.NewLogger("savings"),
+ started: mockClock.Now(),
+ updated: mockClock.Now(),
+ Clock: mockClock,
+ }
+
+ s.Update(0, 0, 0, 0)
+
+ // 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
+
+ // 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
+
+ // 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
+
+ // 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
+
+ // 4 hours 11kW charging, full pv
+ mockClock.Add(4 * time.Hour)
+ s.Update(0, 11000, 0, 11000)
+ assert(t, s, 49.592, 44, 88)
+}
diff --git a/core/site.go b/core/site.go
index 1223a9e9e..661d7d4ee 100644
--- a/core/site.go
+++ b/core/site.go
@@ -11,6 +11,7 @@ import (
"github.com/evcc-io/evcc/api"
"github.com/evcc-io/evcc/core/loadpoint"
"github.com/evcc-io/evcc/push"
+ "github.com/evcc-io/evcc/tariff"
"github.com/evcc-io/evcc/util"
)
@@ -42,8 +43,9 @@ type Site struct {
pvMeters []api.Meter // PV generation meters
batteryMeters []api.Meter // Battery charging meters
- tariff api.Tariff // Tariff
- loadpoints []*LoadPoint // Loadpoints
+ tariffs tariff.Tariffs // Tariff
+ loadpoints []*LoadPoint // Loadpoints
+ savings Savings // Savings
// cached state
gridPower float64 // Grid power
@@ -67,7 +69,7 @@ func NewSiteFromConfig(
cp configProvider,
other map[string]interface{},
loadpoints []*LoadPoint,
- tariff api.Tariff,
+ tariffs tariff.Tariffs,
) (*Site, error) {
site := NewSite()
if err := util.DecodeOther(other, &site); err != nil {
@@ -75,8 +77,9 @@ func NewSiteFromConfig(
}
Voltage = site.Voltage
- site.tariff = tariff
+ site.tariffs = tariffs
site.loadpoints = loadpoints
+ site.savings = NewSavings()
if site.Meters.GridMeterRef != "" {
site.gridMeter = cp.Meter(site.Meters.GridMeterRef)
@@ -390,6 +393,24 @@ 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
}
@@ -397,8 +418,12 @@ func (site *Site) update(lp Updater) {
site.log.DEBUG.Println("----")
var cheap bool
- if site.tariff != nil {
- cheap = site.tariff.IsCheap()
+ var err error
+ if site.tariffs.Grid != nil {
+ cheap, err = site.tariffs.Grid.IsCheap()
+ if err != nil {
+ cheap = false
+ }
}
if sitePower, err := site.sitePower(); err == nil {
@@ -414,6 +439,14 @@ func (site *Site) update(lp Updater) {
site.Health.Update()
}
+
+ // update savings
+ // TODO: use a proper interface, use meter readings instead of current power for better results
+ var totalChargePower = 0.0
+ for _, lp := range site.loadpoints {
+ totalChargePower += lp.chargePower
+ }
+ site.savings.Update(site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
}
// Prepare attaches communication channels to site and loadpoints
diff --git a/dist/css/index.8cd7e991.css b/dist/css/index.8cd7e991.css
deleted file mode 100644
index bd7568158..000000000
--- a/dist/css/index.8cd7e991.css
+++ /dev/null
@@ -1 +0,0 @@
-:root{--evcc-green:#66d85a;--evcc-dark-green:#3aba2c;--evcc-yellow:#ffe000;--evcc-grid:var(--bs-gray-dark);--evcc-self:var(--evcc-dark-green);--evcc-export:var(--evcc-yellow);--bs-primary:var(--evcc-dark-green);--bs-primary-rgb:58,186,44}.bg-primary{background-color:var(--evcc-dark-green)!important}a{color:var(--evcc-dark-green)}.progress-bar-animated{animation-direction:reverse}.bg-muted{opacity:.25}.btn-primary{background-color:var(--bs-gray-dark);border-color:var(--bs-gray-dark)}.btn-primary:hover{background-color:var(--evcc-dark-green);border-color:var(--evcc-dark-green)}.cursor-pointer{cursor:pointer}.v-popper__inner{margin:0 12px!important}.icon[data-v-d8b311e0]{color:#0fdd42}.icon[data-v-e5cf2622]{color:#0fdd42;display:inline-block}.sponsor-button[data-v-e5cf2622]{touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.support-button .solid[data-v-e5cf2622]{display:none}.support-button:hover .solid[data-v-e5cf2622]{display:inline-block}.support-button:hover .outline[data-v-e5cf2622]{display:none}.logo[data-v-4479596c]{width:85px}.app[data-v-4479596c]{min-height:100vh}.title[data-v-4479596c]{position:relative;top:.1rem}.battery[data-v-2dcca16c]{transform:translateX(.35rem) rotate(-90deg);transition-property:transform;transition-duration:.25s;transition-timing-function:ease}.power--in .battery[data-v-2dcca16c]{transform:translateX(.7rem) rotate(-90deg)}.power--out .battery[data-v-2dcca16c]{transform:translateX(0) rotate(-90deg)}.arrow[data-v-2dcca16c]{margin-left:-.2rem;opacity:0;transform:translateX(-.5rem);transition-property:opacity,transform;transition-duration:.25s;transition-timing-function:ease}.power--in .arrow[data-v-2dcca16c]{opacity:1;transform:translateX(-1rem) scaleX(1)}.power--out .arrow[data-v-2dcca16c]{opacity:1;transform:translateX(0) scaleX(1)}.grid[data-v-c8e84f48]{transform:translateX(.35rem);transition-property:transform;transition-duration:.25s;transition-timing-function:ease}.power-export .grid[data-v-c8e84f48]{transform:translateX(.9rem)}.power-import .grid[data-v-c8e84f48]{transform:translateX(-.1rem)}.arrow[data-v-c8e84f48]{margin-left:-.2rem;opacity:0;transform:translateX(-.5rem);transition-property:opacity,transform;transition-duration:.25s;transition-timing-function:ease}.power-export .arrow[data-v-c8e84f48]{opacity:1;transform:translateX(-.9rem) scaleX(1)}.power-import .arrow[data-v-c8e84f48]{opacity:1;transform:translateX(.1em) scaleX(1)}.label-bar[data-v-17da7e54]{width:0;margin:0;height:1.7rem;padding:.6rem 0;opacity:1;overflow:hidden}.label-bar-scale--hidden[data-v-17da7e54]{opacity:0}.label-bar-scale[data-v-17da7e54]{border:1px solid var(--bs-gray);height:.5rem;background:none;display:flex;justify-content:center;align-items:center;white-space:nowrap;border-radius:0;transition:border-radius .5s linear}.label-bar--top .label-bar-scale[data-v-17da7e54]{border-bottom:none}.label-bar--bottom .label-bar-scale[data-v-17da7e54]{border-top:none}.label-bar--top.label-bar--first .label-bar-scale[data-v-17da7e54]{border-top-left-radius:4px}.label-bar--top.label-bar--last .label-bar-scale[data-v-17da7e54]{border-top-right-radius:4px}.label-bar--bottom.label-bar--first .label-bar-scale[data-v-17da7e54]{border-bottom-left-radius:4px}.label-bar--bottom.label-bar--last .label-bar-scale[data-v-17da7e54]{border-bottom-right-radius:4px}.label-bar:not(.label-bar--first) .label-bar-scale[data-v-17da7e54]{border-left:none}.label-bar-icon[data-v-17da7e54]{background-color:#fff;color:var(--bs-gray);padding:0 .3rem;opacity:1}.label-bar--top .label-bar-icon[data-v-17da7e54]{margin-top:-6px}.label-bar--bottom .label-bar-icon[data-v-17da7e54]{margin-top:6px}.label-bar--hidden[data-v-17da7e54],.label-bar--hide-icon .label-bar-icon[data-v-17da7e54]{opacity:0}.site-progress[data-v-0c035ffc]{--height:38px;height:var(--height);margin:.25rem 0;border-radius:5px;display:flex;overflow:hidden}.site-progress-bar[data-v-0c035ffc]{display:flex;justify-content:center;align-items:center;overflow:hidden;position:relative;width:0}.visualization--ready .site-progress-bar[data-v-0c035ffc]{transition-property:width;transition-duration:.5s;transition-timing-function:linear}.grid-import[data-v-0c035ffc]{background-color:var(--evcc-grid);color:var(--bs-white)}.self-consumption[data-v-0c035ffc]{background-color:var(--evcc-self);color:var(--bs-white)}.pv-export[data-v-0c035ffc]{background-color:var(--evcc-export);color:var(--bs-dark)}.power[data-v-0c035ffc]{display:block;margin:0 .2rem;white-space:nowrap;overflow:hidden}.visualization--ready[data-v-0c035ffc] .label-bar{transition-property:width,opacity;transition-duration:.5s,.25s;transition-timing-function:linear,ease}.visualization--ready[data-v-0c035ffc] .label-bar-icon{transition:opacity .25s ease-in}.energyflow[data-v-c2b4f05e]{cursor:pointer}.color-grid[data-v-c2b4f05e]{color:var(--evcc-grid)}.color-self[data-v-c2b4f05e]{color:var(--evcc-self)}.color-export[data-v-c2b4f05e]{color:var(--evcc-export)}.details-icon[data-v-c2b4f05e]{text-align:center;width:30px;margin-right:.25rem;white-space:nowrap;flex-shrink:0}.btn[data-v-1ff39d22]{flex-basis:0;white-space:nowrap}.vehicle-soc[data-v-66f68a62]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-66f68a62]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-66f68a62]{color:var(--white)}.bg-light[data-v-66f68a62]{color:var(--bs-gray-dark)}.target-label[data-v-66f68a62]{width:3em;margin-left:-1.5em;height:var(--label-height);position:absolute;top:calc(var(--label-height)*-1 + var(--thumb-overlap)*-1);text-align:center;color:var(--bs-gray-dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-66f68a62]{-webkit-appearance:none;position:absolute;top:calc(var(--thumb-overlap)*-1);left:calc(var(--thumb-horizontal-padding)*-1);height:calc(100% + var(--thumb-overlap)*2);width:calc(100% + var(--thumb-horizontal-padding)*2);background:transparent}.target-slider[data-v-66f68a62]:focus{outline:none}.target-slider[data-v-66f68a62]::-webkit-slider-runnable-track{position:relative;background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-66f68a62]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-66f68a62]::-webkit-slider-thumb{-webkit-appearance:none;position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:-webkit-grab;cursor:grab;border:none;opacity:1;-webkit-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s;box-shadow:none}.target-slider[data-v-66f68a62]::-moz-range-thumb{position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:0 var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .5s ease 1s;transition:opacity .5s ease 1s}.target--slider-hidden .target-label[data-v-66f68a62],.target--slider-hidden .target-slider[data-v-66f68a62]::-webkit-slider-thumb{opacity:0}.target--slider-hidden .target-label[data-v-66f68a62],.target--slider-hidden .target-slider[data-v-66f68a62]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-66f68a62],.target:hover .target-slider[data-v-66f68a62]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-66f68a62],.target:hover .target-slider[data-v-66f68a62]::-moz-range-thumb{opacity:1;transition-delay:0s}.dots[data-v-60c12baf]{height:1em;width:1em}.dots-back[data-v-60c12baf],.dots-front[data-v-60c12baf]{display:block}.dots-back[data-v-60c12baf]{opacity:.25}.dots-front[data-v-60c12baf]{opacity:1}.direction-up .dots-front[data-v-60c12baf]{-webkit-animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite normal;animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite normal}.direction-down .dots-front[data-v-60c12baf]{animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite reverse}@-webkit-keyframes dots-vertical-data-v-60c12baf{25%{-webkit-clip-path:inset(100% 0 0 0);clip-path:inset(100% 0 0 0)}75%{-webkit-clip-path:inset(0 0 0 0);clip-path:inset(0 0 0 0)}}@keyframes dots-vertical-data-v-60c12baf{25%{-webkit-clip-path:inset(100% 0 0 0);clip-path:inset(100% 0 0 0)}75%{-webkit-clip-path:inset(0 0 0 0);clip-path:inset(0 0 0 0)}}.root[data-v-60c12baf] svg{display:block}
\ No newline at end of file
diff --git a/dist/css/index.e1913a59.css b/dist/css/index.e1913a59.css
new file mode 100644
index 000000000..eab98c14b
--- /dev/null
+++ b/dist/css/index.e1913a59.css
@@ -0,0 +1 @@
+:root{--evcc-green:#66d85a;--evcc-dark-green:#3aba2c;--evcc-yellow:#ffe000;--evcc-grid:var(--bs-gray-dark);--evcc-self:var(--evcc-dark-green);--evcc-export:var(--evcc-yellow);--bs-primary:var(--evcc-dark-green);--bs-primary-rgb:58,186,44}.bg-primary{background-color:var(--evcc-dark-green)!important}a{color:var(--evcc-dark-green)}.progress-bar-animated{animation-direction:reverse}.bg-muted{opacity:.25}.btn-primary,.btn-primary:focus{background-color:var(--bs-gray-dark);border-color:var(--bs-gray-dark)}.btn-primary:active,.btn-primary:hover{background-color:var(--evcc-dark-green);border-color:var(--evcc-dark-green)}.btn-outline-primary{color:var(--bs-gray-dark);border-color:var(--bs-gray-dark)}.btn-outline-primary:hover{background-color:var(--evcc-dark-green);border-color:var(--evcc-dark-green)}.text-evcc{color:var(--evcc-dark-green)}.text-grid{color:var(--evcc-grid)}@media screen and (min-width:400px) and (max-width:574px){.d-xs-none{display:none!important}.d-xs-inline{display:inline!important}}.cursor-pointer{cursor:pointer}.v-popper__inner{margin:0 12px!important}.icon[data-v-7872ae76]{color:var(--evcc-dark-green)}.confetti-button[data-v-bcb1af48]{touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media (max-width:575px){.modal-dialog.modal-dialog-centered[data-v-0be46c78]{align-items:flex-end;margin-bottom:0}.modal.fade .modal-dialog[data-v-0be46c78]{transition:transform .4s ease;transform:translateY(150px)}.modal.show .modal-dialog[data-v-0be46c78]{transform:none}.modal-dialog-scrollable[data-v-0be46c78]{height:calc(100% - .5rem)}.modal-content[data-v-0be46c78]{border-radius:1rem 1rem 0 0}}.chart[data-v-0be46c78]{height:1.6rem}.chart-item--self[data-v-0be46c78]{background-color:var(--evcc-self)}.chart-item--grid[data-v-0be46c78]{background-color:var(--evcc-grid)}.chart-item--no-data[data-v-0be46c78]{background-color:var(--bs-gray)}.chart-item[data-v-0be46c78]{transition-property:width;transition-duration:.5s;transition-timing-function:linear}.logo[data-v-69de83c0]{width:85px}.app[data-v-69de83c0]{min-height:100vh}.title[data-v-69de83c0]{position:relative;top:.1rem}.battery[data-v-2dcca16c]{transform:translateX(.35rem) rotate(-90deg);transition-property:transform;transition-duration:.25s;transition-timing-function:ease}.power--in .battery[data-v-2dcca16c]{transform:translateX(.7rem) rotate(-90deg)}.power--out .battery[data-v-2dcca16c]{transform:translateX(0) rotate(-90deg)}.arrow[data-v-2dcca16c]{margin-left:-.2rem;opacity:0;transform:translateX(-.5rem);transition-property:opacity,transform;transition-duration:.25s;transition-timing-function:ease}.power--in .arrow[data-v-2dcca16c]{opacity:1;transform:translateX(-1rem) scaleX(1)}.power--out .arrow[data-v-2dcca16c]{opacity:1;transform:translateX(0) scaleX(1)}.grid[data-v-c8e84f48]{transform:translateX(.35rem);transition-property:transform;transition-duration:.25s;transition-timing-function:ease}.power-export .grid[data-v-c8e84f48]{transform:translateX(.9rem)}.power-import .grid[data-v-c8e84f48]{transform:translateX(-.1rem)}.arrow[data-v-c8e84f48]{margin-left:-.2rem;opacity:0;transform:translateX(-.5rem);transition-property:opacity,transform;transition-duration:.25s;transition-timing-function:ease}.power-export .arrow[data-v-c8e84f48]{opacity:1;transform:translateX(-.9rem) scaleX(1)}.power-import .arrow[data-v-c8e84f48]{opacity:1;transform:translateX(.1em) scaleX(1)}.label-bar[data-v-17da7e54]{width:0;margin:0;height:1.7rem;padding:.6rem 0;opacity:1;overflow:hidden}.label-bar-scale--hidden[data-v-17da7e54]{opacity:0}.label-bar-scale[data-v-17da7e54]{border:1px solid var(--bs-gray);height:.5rem;background:none;display:flex;justify-content:center;align-items:center;white-space:nowrap;border-radius:0;transition:border-radius .5s linear}.label-bar--top .label-bar-scale[data-v-17da7e54]{border-bottom:none}.label-bar--bottom .label-bar-scale[data-v-17da7e54]{border-top:none}.label-bar--top.label-bar--first .label-bar-scale[data-v-17da7e54]{border-top-left-radius:4px}.label-bar--top.label-bar--last .label-bar-scale[data-v-17da7e54]{border-top-right-radius:4px}.label-bar--bottom.label-bar--first .label-bar-scale[data-v-17da7e54]{border-bottom-left-radius:4px}.label-bar--bottom.label-bar--last .label-bar-scale[data-v-17da7e54]{border-bottom-right-radius:4px}.label-bar:not(.label-bar--first) .label-bar-scale[data-v-17da7e54]{border-left:none}.label-bar-icon[data-v-17da7e54]{background-color:#fff;color:var(--bs-gray);padding:0 .3rem;opacity:1}.label-bar--top .label-bar-icon[data-v-17da7e54]{margin-top:-6px}.label-bar--bottom .label-bar-icon[data-v-17da7e54]{margin-top:6px}.label-bar--hidden[data-v-17da7e54],.label-bar--hide-icon .label-bar-icon[data-v-17da7e54]{opacity:0}.site-progress[data-v-0c035ffc]{--height:38px;height:var(--height);margin:.25rem 0;border-radius:5px;display:flex;overflow:hidden}.site-progress-bar[data-v-0c035ffc]{display:flex;justify-content:center;align-items:center;overflow:hidden;position:relative;width:0}.visualization--ready .site-progress-bar[data-v-0c035ffc]{transition-property:width;transition-duration:.5s;transition-timing-function:linear}.grid-import[data-v-0c035ffc]{background-color:var(--evcc-grid);color:var(--bs-white)}.self-consumption[data-v-0c035ffc]{background-color:var(--evcc-self);color:var(--bs-white)}.pv-export[data-v-0c035ffc]{background-color:var(--evcc-export);color:var(--bs-dark)}.power[data-v-0c035ffc]{display:block;margin:0 .2rem;white-space:nowrap;overflow:hidden}.visualization--ready[data-v-0c035ffc] .label-bar{transition-property:width,opacity;transition-duration:.5s,.25s;transition-timing-function:linear,ease}.visualization--ready[data-v-0c035ffc] .label-bar-icon{transition:opacity .25s ease-in}.energyflow[data-v-c2b4f05e]{cursor:pointer}.color-grid[data-v-c2b4f05e]{color:var(--evcc-grid)}.color-self[data-v-c2b4f05e]{color:var(--evcc-self)}.color-export[data-v-c2b4f05e]{color:var(--evcc-export)}.details-icon[data-v-c2b4f05e]{text-align:center;width:30px;margin-right:.25rem;white-space:nowrap;flex-shrink:0}.btn[data-v-1ff39d22]{flex-basis:0;white-space:nowrap}.vehicle-soc[data-v-66f68a62]{--height:38px;--thumb-overlap:3px;--thumb-width:3px;--thumb-horizontal-padding:15px;--label-height:26px;position:relative;height:var(--height)}.progress[data-v-66f68a62]{height:100%;font-size:1rem}.progress-bar.bg-muted[data-v-66f68a62]{color:var(--white)}.bg-light[data-v-66f68a62]{color:var(--bs-gray-dark)}.target-label[data-v-66f68a62]{width:3em;margin-left:-1.5em;height:var(--label-height);position:absolute;top:calc(var(--label-height)*-1 + var(--thumb-overlap)*-1);text-align:center;color:var(--bs-gray-dark);font-size:1rem;opacity:1;transition:opacity .2s ease 1s}.target-slider[data-v-66f68a62]{-webkit-appearance:none;position:absolute;top:calc(var(--thumb-overlap)*-1);left:calc(var(--thumb-horizontal-padding)*-1);height:calc(100% + var(--thumb-overlap)*2);width:calc(100% + var(--thumb-horizontal-padding)*2);background:transparent}.target-slider[data-v-66f68a62]:focus{outline:none}.target-slider[data-v-66f68a62]::-webkit-slider-runnable-track{position:relative;background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-66f68a62]::-moz-range-track{background:transparent;border:none;height:100%;cursor:pointer}.target-slider[data-v-66f68a62]::-webkit-slider-thumb{-webkit-appearance:none;position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:var(--label-height) var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:-webkit-grab;cursor:grab;border:none;opacity:1;-webkit-transition:opacity .2s ease 1s;transition:opacity .2s ease 1s;box-shadow:none}.target-slider[data-v-66f68a62]::-moz-range-thumb{position:relative;top:calc(var(--label-height)*-1);height:100%;width:var(--thumb-width);padding:0 var(--thumb-horizontal-padding) 0;box-sizing:content-box;background-clip:content-box;background-color:var(--bs-gray-dark);cursor:grab;border:none;opacity:1;-moz-transition:opacity .5s ease 1s;transition:opacity .5s ease 1s}.target--slider-hidden .target-label[data-v-66f68a62],.target--slider-hidden .target-slider[data-v-66f68a62]::-webkit-slider-thumb{opacity:0}.target--slider-hidden .target-label[data-v-66f68a62],.target--slider-hidden .target-slider[data-v-66f68a62]::-moz-range-thumb{opacity:0}.target:hover .target-label[data-v-66f68a62],.target:hover .target-slider[data-v-66f68a62]::-webkit-slider-thumb{opacity:1;transition-delay:0s}.target:hover .target-label[data-v-66f68a62],.target:hover .target-slider[data-v-66f68a62]::-moz-range-thumb{opacity:1;transition-delay:0s}.dots[data-v-60c12baf]{height:1em;width:1em}.dots-back[data-v-60c12baf],.dots-front[data-v-60c12baf]{display:block}.dots-back[data-v-60c12baf]{opacity:.25}.dots-front[data-v-60c12baf]{opacity:1}.direction-up .dots-front[data-v-60c12baf]{-webkit-animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite normal;animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite normal}.direction-down .dots-front[data-v-60c12baf]{animation:dots-vertical-data-v-60c12baf 2s steps(4,jump-none) infinite reverse}@-webkit-keyframes dots-vertical-data-v-60c12baf{25%{-webkit-clip-path:inset(100% 0 0 0);clip-path:inset(100% 0 0 0)}75%{-webkit-clip-path:inset(0 0 0 0);clip-path:inset(0 0 0 0)}}@keyframes dots-vertical-data-v-60c12baf{25%{-webkit-clip-path:inset(100% 0 0 0);clip-path:inset(100% 0 0 0)}75%{-webkit-clip-path:inset(0 0 0 0);clip-path:inset(0 0 0 0)}}.root[data-v-60c12baf] svg{display:block}
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index 920d231a3..451344b67 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -1,4 +1,5 @@
-evcc
\ No newline at end of file
+ commit: "[[.Commit]]",
+ };