Ersparnisanzeige (#2105)
This commit is contained in:
parent
1a66593512
commit
68cfebd2cd
37 changed files with 968 additions and 199 deletions
28
README.md
28
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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -181,5 +181,6 @@ type VehicleStopCharge interface {
|
|||
}
|
||||
|
||||
type Tariff interface {
|
||||
IsCheap() bool
|
||||
IsCheap() (bool, error)
|
||||
CurrentPrice() (float64, error) // EUR/kWh, CHF/kWh, ...
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
window.evcc = {
|
||||
version: "[[.Version]]",
|
||||
configured: "[[.Configured]]",
|
||||
commit: "[[.Commit]]",
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
231
assets/js/components/Savings.vue
Normal file
231
assets/js/components/Savings.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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..",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -65,7 +65,9 @@ type messagingConfig struct {
|
|||
}
|
||||
|
||||
type tariffConfig struct {
|
||||
Grid typedConfig
|
||||
Currency string
|
||||
Grid typedConfig
|
||||
FeedIn typedConfig
|
||||
}
|
||||
|
||||
// ConfigProvider provides configuration items
|
||||
|
|
|
|||
31
cmd/setup.go
31
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)
|
||||
}
|
||||
|
|
|
|||
86
core/savings.go
Normal file
86
core/savings.go
Normal 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
120
core/savings_test.go
Normal 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)
|
||||
}
|
||||
45
core/site.go
45
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
|
||||
|
|
|
|||
1
dist/css/index.8cd7e991.css
vendored
1
dist/css/index.8cd7e991.css
vendored
File diff suppressed because one or more lines are too long
1
dist/css/index.e1913a59.css
vendored
Normal file
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
5
dist/index.html
vendored
|
|
@ -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
1
dist/js/chunk-vendors.42761c73.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/js/chunk-vendors.7d6dc381.js.map
vendored
1
dist/js/chunk-vendors.7d6dc381.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/js/index.36278bf9.js
vendored
Normal file
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
1
dist/js/index.36278bf9.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/js/index.8a9e5172.js
vendored
2
dist/js/index.8a9e5172.js
vendored
File diff suppressed because one or more lines are too long
1
dist/js/index.8a9e5172.js.map
vendored
1
dist/js/index.8a9e5172.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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
204
package-lock.json
generated
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
22
tariff/tariffs.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue