Ersparnisanzeige (#2105)

This commit is contained in:
Michael Geers 2022-01-02 22:58:13 +01:00 • committed by GitHub
parent 1a66593512
commit 68cfebd2cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 968 additions and 199 deletions

View file

@ -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
```

View file

@ -181,5 +181,6 @@ type VehicleStopCharge interface {
}
type Tariff interface {
IsCheap() bool
IsCheap() (bool, error)
CurrentPrice() (float64, error) // EUR/kWh, CHF/kWh, ...
}

View file

@ -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;

View file

@ -27,6 +27,7 @@
window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
commit: "[[.Commit]]",
};
</script>

View file

@ -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,
},
};

View file

@ -4,7 +4,7 @@
<div class="d-flex justify-content-between">
<Version v-bind="version" />
<!-- Please don't mess with the sponsor status. You risk loosing your mojo. -->
<Sponsor :sponsor="sponsor" />
<Savings v-bind="savings" :sponsor="sponsor" />
</div>
</div>
</footer>
@ -12,14 +12,15 @@
<script>
import Version from "./Version";
import Sponsor from "./Sponsor";
import Savings from "./Savings";
export default {
name: "Footer",
components: { Version, Sponsor },
components: { Version, Savings },
props: {
version: Object,
sponsor: String,
savings: Object,
},
};
</script>

View file

@ -37,7 +37,7 @@
class="d-flex justify-content-end mt-3"
:title="fmtAbsoluteDate(msg.time)"
>
{{ fmtTimeAgo(msg.time) }}
{{ fmtTimeAgo(msg.time - new Date()) }}
</small>
<p class="d-flex align-items-baseline">
<fa-icon

View file

@ -0,0 +1,231 @@
<template>
<div>
<button
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap"
data-bs-toggle="modal"
data-bs-target="#savingsModal"
>
<span class="d-inline d-sm-none">{{
$t("footer.savings.footerShort", { percent })
}}</span
><span class="d-none d-sm-inline">{{
$t("footer.savings.footerLong", { percent })
}}</span
><fa-icon icon="sun" class="icon ms-2 text-evcc"></fa-icon>
</button>
<div
id="savingsModal"
ref="modal"
class="modal fade"
tabindex="-1"
role="dialog"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{{
$t("footer.savings.modalTitle", {
total: fmtKw(chargedTotal * 1000, true, false),
})
}}
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body py-4">
<div class="chart-container mb-3">
<div class="chart-legend d-flex flex-wrap justify-content-between mb-1">
<div class="text-nowrap">
<fa-icon icon="square" class="text-evcc"></fa-icon>
{{
$t("footer.savings.modalChartSelf", {
self: fmtKw(chargedSelfConsumption * 1000, true, false),
})
}}
</div>
<div class="text-nowrap">
<fa-icon icon="square" class="text-grid"></fa-icon>
{{
$t("footer.savings.modalChartGrid", {
grid: fmtKw(chargedGrid * 1000, true, false),
})
}}
</div>
</div>
<div
class="chart d-flex justify-content-stretch mb-1 rounded overflow-hidden"
>
<div
v-if="chargedTotal > 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"
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"
class="chart-item chart-item--no-data d-flex justify-content-center text-white w-100"
>
<span>{{ $t("footer.savings.modalNoData") }}</span>
</div>
</div>
</div>
<p class="mb-3">
{{ $t("footer.savings.modalSavingsPrice") }}:
<strong>{{ pricePerKWh }}</strong>
<br />
{{ $t("footer.savings.modalSavingsTotal") }}:
<strong>{{ savingAmount }}</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"
target="_blank"
class="text-muted"
>
{{ $t("footer.savings.modalExplaination") }}</a
>:
<span class="text-nowrap">
{{
$t("footer.savings.modalExplainationGrid", {
gridPrice: fmtPricePerKWh(gridPrice, currency),
})
}}</span
>,
<span class="text-nowrap">
{{
$t("footer.savings.modalExplainationFeedIn", {
feedInPrice: fmtPricePerKWh(feedInPrice, currency),
})
}}
</span>
<br />
{{
$t("footer.savings.modalServerStart", {
since: fmtTimeAgo(since * -1000),
})
}}
</p>
<hr class="mb-4" />
<Sponsor :sponsor="sponsor" class="mb-4" />
<p class="small text-muted mb-0">
<strong class="text-primary">
<fa-icon icon="flask"></fa-icon>
{{ $t("footer.savings.experimentalLabel") }}:
</strong>
{{ $t("footer.savings.experimentalText") }}
<a
href="https://github.com/evcc-io/evcc/discussions/2104"
target="_blank"
>GitHub Discussions</a
>.
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import formatter from "../mixins/formatter";
import Sponsor from "./Sponsor.vue";
export default {
name: "Savings",
components: { Sponsor },
mixins: [formatter],
props: {
selfPercentage: Number,
since: { type: Number, default: 0 },
sponsor: String,
chargedTotal: { type: Number, default: 0 },
chargedSelfConsumption: { type: Number, default: 0 },
gridPrice: { type: Number, default: 30 },
feedInPrice: { type: Number, default: 8 },
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;
},
},
};
</script>
<style scoped>
/* make modal a bottom drawer on small screens */
@media (max-width: 575px) {
.modal-dialog.modal-dialog-centered {
align-items: flex-end;
margin-bottom: 0;
}
.modal.fade .modal-dialog {
transition: transform 0.4s ease;
transform: translate(0, 150px);
}
.modal.show .modal-dialog {
transform: none;
}
.modal-dialog-scrollable {
height: calc(100% - 0.5rem);
}
.modal-content {
border-radius: 1rem 1rem 0 0;
}
}
.chart {
height: 1.6rem;
}
.chart-item--self {
background-color: var(--evcc-self);
}
.chart-item--grid {
background-color: var(--evcc-grid);
}
.chart-item--no-data {
background-color: var(--bs-gray);
}
.chart-item {
transition-property: width;
transition-duration: 500ms;
transition-timing-function: linear;
}
</style>

View file

@ -1,27 +1,45 @@
<template>
<div
v-if="sponsor"
ref="sponsor"
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap sponsor-button"
@click.stop.prevent="surprise"
>
<span class="d-inline d-sm-none">{{ $t("footer.sponsor.sponsoredShort") }}</span>
<span class="d-none d-sm-inline">{{
$t("footer.sponsor.sponsoredLong", { sponsor })
}}</span>
<fa-icon :icon="['fas', 'heart']" class="icon ms-1"></fa-icon>
<div v-if="sponsor">
<p class="mb-3">
{{ $t("footer.sponsor.thanks", { sponsor }) }}
</p>
<div class="d-flex justify-content-center flex-column">
<button
ref="confetti"
class="btn btn-primary mb-2 confetti-button bg-evcc"
@click="surprise"
>
<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"
>
{{ $t("footer.sponsor.sticker") }}
</a>
</div>
</div>
<div v-else>
<p class="mb-3">
{{ $t("footer.sponsor.supportUs") }}
</p>
<div class="d-flex justify-content-center flex-column">
<a
target="_blank"
href="https://github.com/sponsors/andig"
class="btn btn-outline-primary mb-2"
>
<fa-icon :icon="['far', 'heart']" class="icon me-1"></fa-icon>
{{ $t("footer.sponsor.becomeSponsor") }}
</a>
<div class="small text-muted text-center">
{{ $t("footer.sponsor.confettiPromise") }}
</div>
</div>
</div>
<a
v-else
href="https://github.com/sponsors/andig"
target="_blank"
class="btn btn-link pe-0 text-decoration-none link-dark text-nowrap support-button"
>
<span class="d-inline d-sm-none">{{ $t("footer.sponsor.supportProjectShort") }}</span>
<span class="d-none d-sm-inline">{{ $t("footer.sponsor.supportProjectLong") }}</span>
<fa-icon :icon="['far', 'heart']" class="icon ms-1 outline"></fa-icon>
<fa-icon :icon="['fas', 'heart']" class="icon ms-1 solid"></fa-icon>
</a>
</template>
<script>
@ -34,18 +52,23 @@ export default {
},
methods: {
surprise: function () {
const { top, height, left, width } = this.$refs.sponsor.getBoundingClientRect();
const $el = this.$refs.confetti;
const angle = 45 + Math.random() * 90;
const drift = 0;
const { top, height, left, width } = $el.getBoundingClientRect();
const x = (left + width / 2) / window.innerWidth;
const y = (top + height / 2) / window.innerHeight;
const origin = { x, y };
confetti({
origin,
angle: 90 + Math.random() * 35,
angle,
particleCount: 75 + Math.random() * 50,
spread: 50 + Math.random() * 50,
drift: -0.5,
drift,
scalar: 1.3,
zIndex: 1056, // Bootstrap Modal is 1055
colors: [
"#0d6efd",
"#0fdd42",
@ -65,22 +88,9 @@ export default {
</script>
<style scoped>
.icon {
color: #0fdd42;
display: inline-block;
}
.sponsor-button {
.confetti-button {
/* prevent double-tap zoom */
touch-action: none;
user-select: none;
}
.support-button .solid {
display: none;
}
.support-button:hover .solid {
display: inline-block;
}
.support-button:hover .outline {
display: none;
}
</style>

View file

@ -7,10 +7,13 @@
data-bs-target="#updateModal"
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
>
<fa-icon icon="gift" class="icon me-1"></fa-icon>
<span class="d-none d-sm-inline"> {{ $t("footer.version.availableLong") }}: </span>
<span class="d-inline d-sm-none"> {{ $t("footer.version.availableShort") }}: </span>
{{ available }}
<fa-icon icon="gift" class="icon me-2"></fa-icon>v{{ installed }}
<span class="d-none d-xs-inline d-sm-none text-muted text-decoration-underline">
{{ $t("footer.version.availableShort") }}
</span>
<span class="d-none d-sm-inline text-muted text-decoration-underline">
{{ $t("footer.version.availableLong") }}
</span>
</button>
<a
v-else
@ -18,7 +21,12 @@
target="_blank"
class="btn btn-link ps-0 text-decoration-none link-dark text-nowrap"
>
{{ $t("footer.version.version") }} {{ installed }}
<span class="d-inline d-xs-none d-sm-none">{{
$t("footer.version.versionShort", { installed })
}}</span>
<span class="d-none d-xs-inline d-sm-inline">{{
$t("footer.version.versionLong", { installed })
}}</span>
</a>
<div id="updateModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
@ -110,6 +118,7 @@ export default {
installed: String,
available: String,
releaseNotes: String,
commit: String,
hasUpdater: Boolean,
uploadMessage: String,
uploadProgress: Number,
@ -121,6 +130,9 @@ export default {
};
},
computed: {
githubHashUrl: function () {
return `https://github.com/evcc-io/evcc/commit/${this.commit}`;
},
newVersionAvailable: function () {
return (
this.available && // available version already computed?
@ -149,6 +161,6 @@ export default {
<style scoped>
.icon {
color: #0fdd42;
color: var(--evcc-dark-green);
}
</style>

View file

@ -2,8 +2,9 @@ export default {
header: { docs: "Dokumentation", blog: "Blog", github: "GitHub" },
footer: {
version: {
version: "Version",
availableShort: "verfügbar",
versionShort: "v{installed}",
versionLong: "Version {installed}",
availableShort: "Update",
availableLong: "Update verfügbar",
modalTitle: "Update verfügbar",
modalUpdateStarted: "Nach der Aktualisierung wird evcc neu gestartet.",
@ -17,17 +18,37 @@ export default {
modalUpdateStatusStart: "Aktualisierung gestartet: ",
modalUpdateStatusFailed: "Aktualisierung nicht möglich: ",
},
savings: {
footerShort: "{percent}% Sonne",
footerLong: "{percent}% Sonnenstrom ",
modalTitle: "{total} kWh Strom geladen",
modalChartGrid: "Netzstrom {grid} kWh",
modalChartSelf: "Sonnenstrom {self} kWh",
modalSavingsPrice: "Effektiver Strompreis",
modalSavingsTotal: "Ersparnis gegenüber Netzbezug",
modalExplaination: "Berechnung",
modalExplainationGrid: "Netzstrom {gridPrice}",
modalExplainationFeedIn: "Einspeisung {feedInPrice}",
modalServerStart: "Seit Serverstart {since}.",
modalNoData: "noch nicht geladen",
experimentalLabel: "experimentell",
experimentalText: "Unplausible Werte? Fragen zur Ansicht? Meld dich gerne in den",
},
sponsor: {
sponsoredShort: "Danke",
sponsoredLong: "Danke {sponsor}",
supportProjectShort: "unterstützen",
supportProjectLong: "Projekt unterstützen",
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 ;)",
becomeSponsor: "Werde jetzt GitHub Sponsor",
},
},
notifications: {
modalTitle: "Meldungen",
dismissAll: "Meldungen entfernen",
},
main: {
energyflow: {
noEnergy: "Kein Energiefluss",

View file

@ -2,8 +2,9 @@ export default {
header: { docs: "Documentation", blog: "Blog", github: "GitHub" },
footer: {
version: {
version: "version",
availableShort: "available",
versionShort: "v{installed}",
versionLong: "Version {installed}",
availableShort: "update",
availableLong: "update available",
modalTitle: "Update available",
modalUpdateStarted: "Evcc will restart after the update..",
@ -17,11 +18,30 @@ export default {
modalUpdateStatusStart: "Update started: ",
modalUpdateStatusFailed: "Update failed: ",
},
savings: {
footerShort: "{percent}% solar",
footerLong: "{percent}% solar energy",
modalTitle: "{total} kWh solar energy charged",
modalChartGrid: "Grid energy {grid} kWh",
modalChartSelf: "Solar energy {self} kWh",
modalSavingsPrice: "Effective energy price",
modalSavingsTotal: "Savings compared to grid",
modalExplaination: "Calculation",
modalExplainationGrid: "grid tariff {gridPrice}",
modalExplainationFeedIn: "feed-in rate {feedInPrice}",
modalServerStart: "since server start {since}.",
modalNoData: "nothing charged yet",
experimentalLabel: "experimental",
experimentalText: "Implausible values? Questions about this view? Feel free to join our ",
},
sponsor: {
sponsoredShort: "thanks",
sponsoredLong: "thanks {sponsor}",
supportProjectShort: "support",
supportProjectLong: "support the project",
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 ;)",
becomeSponsor: "Become a GitHub Sponsor",
},
},
notifications: {

View file

@ -3,7 +3,7 @@ export default {
footer: {
version: {
version: "versione",
availableShort: "disponibile",
availableShort: "aggiornamento",
availableLong: "aggiornamento disponibile",
modalTitle: "Aggiornamento disponibile",
modalUpdateStarted: "Evcc ripartirà dopo l'aggiornamento..",

View file

@ -12,6 +12,8 @@ import { faAngleDown } from "@fortawesome/free-solid-svg-icons/faAngleDown";
import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle";
import { faSun } from "@fortawesome/free-solid-svg-icons/faSun";
import { faSun as farSun } from "@fortawesome/free-regular-svg-icons/faSun";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
import { faFlask } from "@fortawesome/free-solid-svg-icons/faFlask";
import { faTemperatureHigh } from "@fortawesome/free-solid-svg-icons/faTemperatureHigh";
import { faTemperatureLow } from "@fortawesome/free-solid-svg-icons/faTemperatureLow";
@ -21,6 +23,7 @@ import { faHeart as fasHeart } from "@fortawesome/free-solid-svg-icons/faHeart";
import { faGift } from "@fortawesome/free-solid-svg-icons/faGift";
import { faBox } from "@fortawesome/free-solid-svg-icons/faBox";
import { faHome } from "@fortawesome/free-solid-svg-icons/faHome";
import { faWrench } from "@fortawesome/free-solid-svg-icons/faWrench";
import { faCar } from "@fortawesome/free-solid-svg-icons/faCar";
import { faSquare } from "@fortawesome/free-solid-svg-icons/faSquare";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@ -44,10 +47,13 @@ library.add(
faExclamationTriangle,
faGift,
faHome,
faWrench,
farHeart,
fasHeart,
faSquare,
faSun,
farSun,
faInfoCircle,
faFlask,
faTemperatureHigh,
faTemperatureLow,

View file

@ -89,24 +89,33 @@ export default {
return `${HH}:${mm}`;
},
fmtAbsoluteDate: function (date) {
return new Intl.DateTimeFormat("default", {
return new Intl.DateTimeFormat(this.$i18n.locale, {
weekday: "short",
hour: "numeric",
minute: "numeric",
}).format(date);
},
fmtTimeAgo: function (date) {
fmtMoney: function (amout = 0, currency = "EUR") {
return this.$n(amout, { style: "currency", currency });
},
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
let unit = currency;
let value = amout;
if (["EUR", "USD"].includes(currency)) {
value *= 100;
unit = "ct";
}
return `${this.$n(value, { style: "decimal" })} ${unit}/kWh`;
},
fmtTimeAgo: function (elapsed) {
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" });
const elapsed = date - new Date();
const rtf = new Intl.RelativeTimeFormat(this.$i18n.locale, { numeric: "auto" });
// "Math.abs" accounts for both "past" & "future" scenarios
for (var u in units)

View file

@ -57,7 +57,7 @@
class="flex-grow-1 d-flex flex-column justify-content-stretch"
></router-view>
</div>
<Footer :version="version" :sponsor="sponsor"></Footer>
<Footer :version="version" :sponsor="sponsor" :savings="savings"></Footer>
</div>
</template>
@ -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);

View file

@ -65,7 +65,9 @@ type messagingConfig struct {
}
type tariffConfig struct {
Grid typedConfig
Currency string
Grid typedConfig
FeedIn typedConfig
}
// ConfigProvider provides configuration items

View file

@ -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)
}

86
core/savings.go Normal file
View file

@ -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())
}

120
core/savings_test.go Normal file
View file

@ -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)
}

View file

@ -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

File diff suppressed because one or more lines are too long

1
dist/css/index.e1913a59.css vendored Normal file

File diff suppressed because one or more lines are too long

5
dist/index.html vendored
View file

@ -1,4 +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.a7b690cd.css" rel="preload" as="style"><link href="css/index.8cd7e991.css" rel="preload" as="style"><link href="js/chunk-vendors.7d6dc381.js" rel="preload" as="script"><link href="js/index.8a9e5172.js" rel="preload" as="script"><link href="css/chunk-vendors.a7b690cd.css" rel="stylesheet"><link href="css/index.8cd7e991.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.a7b690cd.css" rel="preload" as="style"><link href="css/index.e1913a59.css" rel="preload" as="style"><link href="js/chunk-vendors.42761c73.js" rel="preload" as="script"><link href="js/index.36278bf9.js" rel="preload" as="script"><link href="css/chunk-vendors.a7b690cd.css" rel="stylesheet"><link href="css/index.e1913a59.css" rel="stylesheet"></head><body><script>window.evcc = {
version: "[[.Version]]",
configured: "[[.Configured]]",
};</script><div id="app"></div><script src="js/chunk-vendors.7d6dc381.js"></script><script src="js/index.8a9e5172.js"></script></body></html>
commit: "[[.Commit]]",
};</script><div id="app"></div><script src="js/chunk-vendors.42761c73.js"></script><script src="js/index.36278bf9.js"></script></body></html>

File diff suppressed because one or more lines are too long

1
dist/js/chunk-vendors.42761c73.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/js/index.36278bf9.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/index.36278bf9.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -105,19 +105,28 @@ loadpoints:
maxCurrent: 16 # maximum charge current (default 16A)
# tariffs are the fixed or variable tariffs
# cheap can be used to define a tariff rate considered cheap enough for charging
# cheap (tibber/awattar) can be used to define a tariff rate considered cheap enough for charging
tariffs:
currency: EUR # three letter ISO-4217 currency code (default EUR)
grid:
# # either
# either static grid price
type: fixed
price: 0.294 # EUR/kWh
# # or variable via tibber
# 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
# # or variable via awattar
# type: awattar
# cheap: 20 # ct/kWh
# cheap: 0.2 # EUR/kWh
# region: de # optional, choose at for Austria
feedin:
# rate for feeding excess (pv) energy to the grid
type: fixed
price: 0.08 # EUR/kWh
# mqtt message broker
mqtt:

204
package-lock.json generated
View file

@ -4487,6 +4487,15 @@
"nan": "^2.12.1"
}
},
"is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"requires": {
"is-docker": "^2.0.0"
}
},
"istanbul-lib-coverage": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz",
@ -4581,7 +4590,27 @@
}
},
"node-notifier": {
"version": ">=8.0.1"
"version": ">=8.0.1",
"dev": true,
"requires": {
"growly": "^1.3.0",
"is-wsl": "^2.2.0",
"semver": "^7.3.5",
"shellwords": "^0.1.1",
"uuid": "^8.3.2",
"which": "^2.0.2"
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"normalize-path": {
"version": "2.1.1",
@ -4682,6 +4711,21 @@
"require-main-filename": "^2.0.0"
}
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
}
},
"write-file-atomic": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz",
@ -8688,17 +8732,16 @@
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
"is-extglob": "^2.1.1"
}
}
}
@ -11570,7 +11613,22 @@
},
"dependencies": {
"glob-parent": {
"version": ">=5.1.2"
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
}
}
}
}
},
@ -12349,17 +12407,16 @@
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
"is-extglob": "^2.1.1"
}
}
}
@ -12572,23 +12629,32 @@
},
"dependencies": {
"glob-parent": {
"version": ">=5.1.2"
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
}
}
}
}
},
"glob-parent": {
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
}
@ -14191,7 +14257,18 @@
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
}
}
},
"globals": {
@ -15111,7 +15188,20 @@
},
"dependencies": {
"glob-parent": {
"version": ">=5.1.2"
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^4.0.3"
}
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"micromatch": {
"version": "4.0.4",
@ -15791,13 +15881,6 @@
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"glob-promise": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz",
@ -16598,10 +16681,6 @@
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
},
"immer": {
"version": ">=9.0.6",
"dev": true
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@ -20692,25 +20771,6 @@
}
}
},
"node-notifier": {
"version": ">=8.0.1",
"dev": true,
"requires": {
"growly": "^1.3.0",
"is-wsl": "^1.1.0",
"semver": "^5.5.0",
"shellwords": "^0.1.1",
"which": "^1.3.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
}
}
},
"node-releases": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz",
@ -21341,12 +21401,6 @@
"integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
"dev": true
},
"path-dirname": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz",
"integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=",
"dev": true
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -22916,7 +22970,8 @@
}
},
"immer": {
"version": ">=9.0.6"
"version": ">=9.0.6",
"dev": true
},
"node-releases": {
"version": "1.1.77",
@ -23478,7 +23533,8 @@
},
"dependencies": {
"trim": {
"version": ">=1.0.1"
"version": ">=1.0.1",
"dev": true
}
}
},
@ -25401,10 +25457,6 @@
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
"dev": true
},
"trim": {
"version": ">=1.0.1",
"dev": true
},
"trim-trailing-lines": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz",
@ -26689,18 +26741,17 @@
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"optional": true,
"requires": {
"is-extglob": "^2.1.0"
"is-extglob": "^2.1.1"
}
}
}
@ -27103,17 +27154,16 @@
"version": ">=5.1.2",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
"is-glob": "^4.0.3"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
"is-extglob": "^2.1.1"
}
}
}

View file

@ -1,6 +1,7 @@
package tariff
import (
"errors"
"fmt"
"strings"
"sync"
@ -61,7 +62,7 @@ func (t *Awattar) Run() {
}
}
func (t *Awattar) IsCheap() bool {
func (t *Awattar) CurrentPrice() (float64, error) {
t.mux.Lock()
defer t.mux.Unlock()
@ -69,9 +70,14 @@ func (t *Awattar) IsCheap() bool {
pi := t.data[i]
if pi.StartTimestamp.Before(time.Now()) && pi.EndTimestamp.After(time.Now()) {
return pi.Marketprice/10 <= t.cheap // Eur/MWh conversion
return pi.Marketprice / 1000, nil // convert EUR/MWh to EUR/KWh
}
}
return false
return 0, errors.New("unable to find current awattar price")
}
func (t *Awattar) IsCheap() (bool, error) {
price, err := t.CurrentPrice()
return price <= t.cheap, err
}

View file

@ -21,6 +21,10 @@ func NewFixed(other map[string]interface{}) (*Fixed, error) {
return &cc, nil
}
func (t *Fixed) IsCheap() bool {
return false
func (t *Fixed) CurrentPrice() (float64, error) {
return t.Price, nil
}
func (t *Fixed) IsCheap() (bool, error) {
return false, nil
}

22
tariff/tariffs.go Normal file
View file

@ -0,0 +1,22 @@
package tariff
import (
"github.com/evcc-io/evcc/api"
"golang.org/x/text/currency"
)
type Tariffs struct {
Currency currency.Unit
Grid api.Tariff
FeedIn api.Tariff
}
var _ api.Tariff = (*Fixed)(nil)
func NewTariffs(currency currency.Unit, grid api.Tariff, feedin api.Tariff) *Tariffs {
t := Tariffs{}
t.Currency = currency
t.Grid = grid
t.FeedIn = feedin
return &t
}

View file

@ -2,6 +2,7 @@ package tariff
import (
"context"
"errors"
"fmt"
"sync"
"time"
@ -97,7 +98,7 @@ func (t *Tibber) Run() {
}
}
func (t *Tibber) IsCheap() bool {
func (t *Tibber) CurrentPrice() (float64, error) {
t.mux.Lock()
defer t.mux.Unlock()
@ -105,9 +106,13 @@ func (t *Tibber) IsCheap() bool {
pi := t.data[i]
if pi.StartsAt.Before(time.Now()) {
return pi.Total <= t.Cheap
return pi.Total, nil
}
}
return false
return 0, errors.New("unable to find current tibber price")
}
func (t *Tibber) IsCheap() (bool, error) {
price, err := t.CurrentPrice()
return price <= t.Cheap, err
}