Add option to show current price and co2 info (#6048)
This commit is contained in:
parent
d01985402f
commit
4ff0b9a5fd
23 changed files with 430 additions and 159 deletions
|
|
@ -27,6 +27,10 @@ import Energyflow from "./Energyflow.vue";
|
|||
:batteryPower="800"
|
||||
:batterySoc="77"
|
||||
siteTitle="Home"
|
||||
:battery="[
|
||||
{ soc: 44, capacity: 13.3 },
|
||||
{ soc: 82, capacity: 21 },
|
||||
]"
|
||||
/>
|
||||
</Variant>
|
||||
<Variant title="battery charging">
|
||||
|
|
|
|||
|
|
@ -65,12 +65,17 @@
|
|||
:soc="batterySoc"
|
||||
:power="batteryDischarge"
|
||||
:valuesInKw="valuesInKw"
|
||||
:tooltip="batteryTooltip"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
:name="$t('main.energyflow.gridImport')"
|
||||
icon="powersupply"
|
||||
:power="gridImport"
|
||||
:valuesInKw="valuesInKw"
|
||||
:price="tariffGrid"
|
||||
:currency="currency"
|
||||
:co2="tariffCo2"
|
||||
:tooltip="detailsTooltip(tariffGrid, tariffCo2)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -89,6 +94,10 @@
|
|||
icon="home"
|
||||
:power="homePower"
|
||||
:valuesInKw="valuesInKw"
|
||||
:price="tariffEffectivePrice"
|
||||
:currency="currency"
|
||||
:co2="tariffEffectiveCo2"
|
||||
:tooltip="detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
:name="
|
||||
|
|
@ -100,6 +109,10 @@
|
|||
:vehicleIcons="vehicleIcons"
|
||||
:power="loadpointsPower"
|
||||
:valuesInKw="valuesInKw"
|
||||
:price="tariffEffectivePrice"
|
||||
:currency="currency"
|
||||
:co2="tariffEffectiveCo2"
|
||||
:tooltip="detailsTooltip(tariffEffectivePrice, tariffEffectiveCo2)"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
v-if="batteryConfigured"
|
||||
|
|
@ -108,12 +121,16 @@
|
|||
:soc="batterySoc"
|
||||
:power="batteryCharge"
|
||||
:valuesInKw="valuesInKw"
|
||||
:tooltip="batteryTooltip"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
:name="$t('main.energyflow.pvExport')"
|
||||
icon="powersupply"
|
||||
:power="pvExport"
|
||||
:valuesInKw="valuesInKw"
|
||||
:price="-tariffFeedIn"
|
||||
:currency="currency"
|
||||
:tooltip="detailsTooltip(-tariffFeedIn)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -143,9 +160,16 @@ export default {
|
|||
loadpointsPower: { type: Number, default: 0 },
|
||||
activeLoadpointsCount: { type: Number, default: 0 },
|
||||
batteryConfigured: Boolean,
|
||||
battery: { type: Array },
|
||||
batteryPower: { type: Number, default: 0 },
|
||||
batterySoc: { type: Number, default: 0 },
|
||||
vehicleIcons: { type: Array },
|
||||
tariffGrid: { type: Number },
|
||||
tariffFeedIn: { type: Number },
|
||||
tariffEffectivePrice: { type: Number },
|
||||
tariffCo2: { type: Number },
|
||||
tariffEffectiveCo2: { type: Number },
|
||||
currency: { type: String },
|
||||
},
|
||||
data: () => {
|
||||
return { detailsOpen: false, detailsCompleteHeight: null };
|
||||
|
|
@ -187,6 +211,16 @@ export default {
|
|||
detailsHeight: function () {
|
||||
return this.detailsOpen ? this.detailsCompleteHeight + "px" : 0;
|
||||
},
|
||||
batteryTooltip() {
|
||||
if (!Array.isArray(this.battery)) {
|
||||
return;
|
||||
}
|
||||
return this.battery.map(({ soc, capacity }) => {
|
||||
const energy = this.fmtKWh((capacity / 100) * soc * 1e3, true, false, 1);
|
||||
const total = this.fmtKWh(capacity * 1e3, true, true, 1);
|
||||
return this.$t("main.energyflow.batteryTooltip", { energy, total, soc });
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("resize", this.updateHeight);
|
||||
|
|
@ -199,6 +233,16 @@ export default {
|
|||
window.removeEventListener("resize", this.updateHeight);
|
||||
},
|
||||
methods: {
|
||||
detailsTooltip(price, co2) {
|
||||
const result = [];
|
||||
if (co2 !== undefined) {
|
||||
result.push(`${this.fmtCo2Long(co2)}`);
|
||||
}
|
||||
if (price !== undefined) {
|
||||
result.push(`${this.fmtPricePerKWh(price, this.currency)}`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
kw: function (watt) {
|
||||
return this.fmtKw(watt, this.valuesInKw);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,10 +5,22 @@
|
|||
<VehicleIcon v-else-if="isVehicle" :names="vehicleIcons" />
|
||||
<component :is="`shopicon-regular-${icon}`" v-else></component>
|
||||
</span>
|
||||
<span class="text-nowrap flex-grow-1 ms-3">{{ name }}</span>
|
||||
<span class="text-end text-nowrap ps-1 fw-bold"
|
||||
><span v-if="hasSoc">{{ soc }}% / </span>
|
||||
<AnimatedNumber :to="power" :format="kw" />
|
||||
<span class="text-nowrap flex-grow-1 ms-3">
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="text-end text-nowrap ps-1 fw-bold d-flex">
|
||||
<div
|
||||
ref="details"
|
||||
class="evcc-gray fw-normal"
|
||||
data-bs-toggle="tooltip"
|
||||
title=" "
|
||||
@click.stop=""
|
||||
>
|
||||
<span v-if="showPrice()"><AnimatedNumber :to="price" :format="fmtPrice" /></span>
|
||||
<span v-if="showCo2()"><AnimatedNumber :to="co2" :format="fmtCo2Short" /></span>
|
||||
<span v-if="hasSoc">{{ soc }}%</span>
|
||||
</div>
|
||||
<AnimatedNumber :to="power" :format="kw" class="power" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -17,10 +29,12 @@
|
|||
import "@h2d2/shopicons/es/regular/powersupply";
|
||||
import "@h2d2/shopicons/es/regular/sun";
|
||||
import "@h2d2/shopicons/es/regular/home";
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
import BatteryIcon from "./BatteryIcon.vue";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import AnimatedNumber from "../AnimatedNumber.vue";
|
||||
import VehicleIcon from "../VehicleIcon";
|
||||
import { showGridPrice, showGridCo2 } from "../../gridDetails";
|
||||
|
||||
export default {
|
||||
name: "EnergyflowEntry",
|
||||
|
|
@ -31,8 +45,15 @@ export default {
|
|||
icon: { type: String },
|
||||
power: { type: Number },
|
||||
soc: { type: Number },
|
||||
price: { type: Number },
|
||||
co2: { type: Number },
|
||||
valuesInKw: { type: Boolean },
|
||||
vehicleIcons: { type: Array },
|
||||
currency: { type: String },
|
||||
tooltip: { type: Array },
|
||||
},
|
||||
data() {
|
||||
return { tooltipInstance: null };
|
||||
},
|
||||
computed: {
|
||||
active: function () {
|
||||
|
|
@ -48,10 +69,39 @@ export default {
|
|||
return this.isBattery && !isNaN(this.soc);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tooltip(newVal, oldVal) {
|
||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
this.updateTooltip();
|
||||
},
|
||||
methods: {
|
||||
showPrice() {
|
||||
return showGridPrice() && !isNaN(this.price);
|
||||
},
|
||||
showCo2() {
|
||||
return showGridCo2() && !isNaN(this.co2);
|
||||
},
|
||||
kw: function (watt) {
|
||||
return this.fmtKw(watt, this.valuesInKw);
|
||||
},
|
||||
fmtPrice: function (price) {
|
||||
return this.fmtPricePerKWh(price, this.currency, true);
|
||||
},
|
||||
updateTooltip: function () {
|
||||
if (!Array.isArray(this.tooltip) || !this.tooltip.length) {
|
||||
return;
|
||||
}
|
||||
if (!this.tooltipInstance) {
|
||||
this.tooltipInstance = new Tooltip(this.$refs.details, { html: true });
|
||||
}
|
||||
const html = `<div class="text-end">${this.tooltip.join("<br/>")}</div>`;
|
||||
this.tooltipInstance.setContent({ ".tooltip-inner": html });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -59,4 +109,7 @@ export default {
|
|||
.entry {
|
||||
transition: color var(--evcc-transition-medium) linear;
|
||||
}
|
||||
.power {
|
||||
min-width: 75px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,23 @@
|
|||
"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
id="settingsGridDetails"
|
||||
:label="$t('settings.gridDetails.label')"
|
||||
>
|
||||
<SelectGroup
|
||||
id="settingsGridDetails"
|
||||
v-model="gridDetails"
|
||||
class="w-100"
|
||||
:options="
|
||||
GRID_DETAILS.map((value) => ({
|
||||
value,
|
||||
name: $t(`settings.gridDetails.${value}`),
|
||||
disabled: isDisabled(value),
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</FormRow>
|
||||
<FormRow id="telemetryEnabled" :label="$t('settings.telemetry.label')">
|
||||
<TelemetrySettings :sponsor="sponsor" class="mt-1 mb-0" />
|
||||
</FormRow>
|
||||
|
|
@ -81,20 +98,25 @@ import SelectGroup from "./SelectGroup.vue";
|
|||
import { getLocalePreference, setLocalePreference, LOCALES, removeLocalePreference } from "../i18n";
|
||||
import { getThemePreference, setThemePreference, THEMES } from "../theme";
|
||||
import { getUnits, setUnits, UNITS } from "../units";
|
||||
import { getGridDetails, setGridDetails, GRID_DETAILS } from "../gridDetails";
|
||||
|
||||
export default {
|
||||
name: "GlobalSettingsModal",
|
||||
components: { TelemetrySettings, FormRow, SelectGroup },
|
||||
props: {
|
||||
sponsor: String,
|
||||
hasPrice: Boolean,
|
||||
hasCo2: Boolean,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
theme: getThemePreference(),
|
||||
language: getLocalePreference() || "",
|
||||
unit: getUnits(),
|
||||
gridDetails: getGridDetails(),
|
||||
THEMES,
|
||||
UNITS,
|
||||
GRID_DETAILS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -111,6 +133,9 @@ export default {
|
|||
unit(value) {
|
||||
setUnits(value);
|
||||
},
|
||||
gridDetails(value) {
|
||||
setGridDetails(value);
|
||||
},
|
||||
theme(value) {
|
||||
setThemePreference(value);
|
||||
},
|
||||
|
|
@ -123,6 +148,11 @@ export default {
|
|||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isDisabled(option) {
|
||||
return (option === "co2" && !this.hasCo2) || (option === "price" && !this.hasPrice);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
type="button"
|
||||
class="btn btn-sm flex-grow-1 flex-shrink-1"
|
||||
:class="{ active: option.value === modelValue }"
|
||||
:disabled="option.disabled"
|
||||
@click="$emit('update:modelValue', option.value)"
|
||||
>
|
||||
{{ option.name }}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export default {
|
|||
batteryConfigured: Boolean,
|
||||
batteryPower: Number,
|
||||
batterySoc: Number,
|
||||
battery: Array,
|
||||
gridCurrents: Array,
|
||||
prioritySoc: Number,
|
||||
siteTitle: String,
|
||||
|
|
@ -68,7 +69,6 @@ export default {
|
|||
|
||||
auth: Object,
|
||||
|
||||
// footer
|
||||
currency: String,
|
||||
savingsAmount: Number,
|
||||
savingsEffectivePrice: Number,
|
||||
|
|
@ -77,8 +77,12 @@ export default {
|
|||
savingsSelfConsumptionPercent: Number,
|
||||
savingsSince: String,
|
||||
savingsTotalCharged: Number,
|
||||
greenShare: Number,
|
||||
tariffFeedIn: Number,
|
||||
tariffGrid: Number,
|
||||
tariffEffectivePrice: Number,
|
||||
tariffCo2: Number,
|
||||
tariffEffectiveCo2: Number,
|
||||
|
||||
availableVersion: String,
|
||||
releaseNotes: String,
|
||||
|
|
@ -113,6 +117,12 @@ export default {
|
|||
const vehicleLogins = this.auth ? this.auth.vehicles : {};
|
||||
return { vehicleLogins, ...this.collectProps(TopNavigation) };
|
||||
},
|
||||
hasPrice: function () {
|
||||
return !isNaN(this.tariffGrid);
|
||||
},
|
||||
hasCo2: function () {
|
||||
return !isNaN(this.tariffCo2);
|
||||
},
|
||||
showParkingLot: function () {
|
||||
// work in progess
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -94,9 +94,7 @@ export default {
|
|||
if (price === undefined) {
|
||||
return this.$t("main.targetChargePlan.unknownPrice");
|
||||
}
|
||||
return this.isCo2
|
||||
? `${Math.round(price)} g/kWh`
|
||||
: this.fmtPricePerKWh(price, this.unit);
|
||||
return this.isCo2 ? this.fmtCo2Medium(price) : this.fmtPricePerKWh(price, this.unit);
|
||||
},
|
||||
activeSlot() {
|
||||
return this.slots[this.activeIndex];
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<GlobalSettingsModal :sponsor="sponsor" />
|
||||
<GlobalSettingsModal v-bind="globalSettingsModalProps" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -104,12 +104,14 @@ import "@h2d2/shopicons/es/regular/moonstars";
|
|||
import "@h2d2/shopicons/es/regular/menu";
|
||||
import "@h2d2/shopicons/es/regular/newtab";
|
||||
import GlobalSettingsModal from "./GlobalSettingsModal.vue";
|
||||
import collector from "../mixins/collector";
|
||||
|
||||
import baseAPI from "../baseapi";
|
||||
|
||||
export default {
|
||||
name: "TopNavigation",
|
||||
components: { GlobalSettingsModal },
|
||||
mixins: [collector],
|
||||
props: {
|
||||
vehicleLogins: {
|
||||
type: Object,
|
||||
|
|
@ -118,8 +120,13 @@ export default {
|
|||
},
|
||||
},
|
||||
sponsor: String,
|
||||
hasPrice: Boolean,
|
||||
hasCo2: Boolean,
|
||||
},
|
||||
computed: {
|
||||
globalSettingsModalProps: function () {
|
||||
return this.collectProps(GlobalSettingsModal);
|
||||
},
|
||||
logoutCount() {
|
||||
return this.providerLogins.filter((login) => !login.loggedIn).length;
|
||||
},
|
||||
|
|
|
|||
22
assets/js/gridDetails.js
Normal file
22
assets/js/gridDetails.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import settings from "./settings";
|
||||
|
||||
const NONE = "none";
|
||||
const PRICE = "price";
|
||||
const CO2 = "co2";
|
||||
export const GRID_DETAILS = [NONE, PRICE, CO2];
|
||||
|
||||
export function getGridDetails() {
|
||||
return GRID_DETAILS.includes(settings.gridDetails) ? settings.gridDetails : NONE;
|
||||
}
|
||||
|
||||
export function showGridPrice() {
|
||||
return settings.gridDetails === PRICE;
|
||||
}
|
||||
|
||||
export function showGridCo2() {
|
||||
return settings.gridDetails === CO2;
|
||||
}
|
||||
|
||||
export function setGridDetails(value) {
|
||||
settings.gridDetails = value;
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ export default {
|
|||
}).format(value)}${unit}`;
|
||||
},
|
||||
fmtKWh: function (watt, kw, withUnit, digits) {
|
||||
return this.fmtKw(watt, kw, withUnit, digits) + "h";
|
||||
return this.fmtKw(watt, kw, withUnit, digits) + (withUnit ? "h" : "");
|
||||
},
|
||||
fmtNumber: function (number, decimals) {
|
||||
return new Intl.NumberFormat(this.$i18n.locale, {
|
||||
|
|
@ -42,6 +42,15 @@ export default {
|
|||
maximumFractionDigits: decimals,
|
||||
}).format(number);
|
||||
},
|
||||
fmtCo2Short: function (gramms) {
|
||||
return `${this.fmtNumber(gramms, 0)} g`;
|
||||
},
|
||||
fmtCo2Medium: function (gramms) {
|
||||
return `${this.fmtNumber(gramms, 0)} g/kWh`;
|
||||
},
|
||||
fmtCo2Long: function (gramms) {
|
||||
return `${this.fmtNumber(gramms, 0)} gCO₂e/kWh`;
|
||||
},
|
||||
fmtUnit: function (val) {
|
||||
return Math.abs(val) >= this.fmtLimit ? "k" : "";
|
||||
},
|
||||
|
|
@ -182,7 +191,7 @@ export default {
|
|||
const symbols = { EUR: "€", USD: "$" };
|
||||
return symbols[currency] || currency;
|
||||
},
|
||||
fmtPricePerKWh: function (amout = 0, currency = "EUR") {
|
||||
fmtPricePerKWh: function (amout = 0, currency = "EUR", short = false) {
|
||||
let unit = currency;
|
||||
let value = amout;
|
||||
let maximumFractionDigits = 3;
|
||||
|
|
@ -194,7 +203,7 @@ export default {
|
|||
return `${new Intl.NumberFormat(this.$i18n.locale, {
|
||||
style: "decimal",
|
||||
maximumFractionDigits,
|
||||
}).format(value)} ${unit}/kWh`;
|
||||
}).format(value)} ${unit}${short ? "" : "/kWh"}`;
|
||||
},
|
||||
fmtTimeAgo: function (elapsed) {
|
||||
const units = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const SETTINGS_LOCALE = "settings_locale";
|
|||
const SETTINGS_THEME = "settings_theme";
|
||||
const SETTINGS_UNIT = "settings_unit";
|
||||
const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
|
||||
const SETTINGS_GRID_DETAILS = "settings_grid_details";
|
||||
|
||||
function read(key) {
|
||||
return window.localStorage[key];
|
||||
|
|
@ -38,12 +39,14 @@ const settings = reactive({
|
|||
locale: read(SETTINGS_LOCALE),
|
||||
theme: read(SETTINGS_THEME),
|
||||
unit: read(SETTINGS_UNIT),
|
||||
gridDetails: read(SETTINGS_GRID_DETAILS),
|
||||
energyflowDetails: readBool(SETTINGS_ENERGYFLOW_DETAILS),
|
||||
});
|
||||
|
||||
watch(() => settings.locale, save(SETTINGS_LOCALE));
|
||||
watch(() => settings.theme, save(SETTINGS_THEME));
|
||||
watch(() => settings.unit, save(SETTINGS_UNIT));
|
||||
watch(() => settings.gridDetails, save(SETTINGS_GRID_DETAILS));
|
||||
watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
|
||||
|
||||
export default settings;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ meters:
|
|||
if (state.batterySoc < 10) state.batterySoc = 90;
|
||||
if (state.batterySoc > 90) state.batterySoc = 10;
|
||||
state.batterySoc;
|
||||
capacity: 13.4
|
||||
|
||||
- name: meter_charger_1
|
||||
type: custom
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/evcc-io/evcc/api"
|
||||
"github.com/evcc-io/evcc/server/db/settings"
|
||||
"github.com/evcc-io/evcc/tariff"
|
||||
)
|
||||
|
|
@ -22,17 +20,16 @@ type publisher interface {
|
|||
|
||||
// Site is the main configuration container. A site can host multiple loadpoints.
|
||||
type Savings struct {
|
||||
clock clock.Clock
|
||||
tariffs tariff.Tariffs
|
||||
started time.Time // Boot time
|
||||
updated time.Time // Time of last charged value update
|
||||
gridCharged float64 // Grid energy charged since startup (kWh)
|
||||
gridCost float64 // Running total of charged grid energy cost (e.g. EUR)
|
||||
gridSavedCost float64 // Running total of saved cost from self consumption (e.g. EUR)
|
||||
selfConsumptionCharged float64 // Self-produced energy charged since startup (kWh)
|
||||
selfConsumptionCost float64 // Running total of charged self-produced energy cost (e.g. EUR)
|
||||
lastGridPrice, lastFeedInPrice float64 // Stores the last published grid price. Needed to detect price changes (Awattar, ..)
|
||||
hasPublished bool // Has initial publish happened?
|
||||
clock clock.Clock
|
||||
tariffs tariff.Tariffs
|
||||
started time.Time // Boot time
|
||||
updated time.Time // Time of last charged value update
|
||||
gridCharged float64 // Grid energy charged since startup (kWh)
|
||||
gridCost float64 // Running total of charged grid energy cost (e.g. EUR)
|
||||
gridSavedCost float64 // Running total of saved cost from self consumption (e.g. EUR)
|
||||
selfConsumptionCharged float64 // Self-produced energy charged since startup (kWh)
|
||||
selfConsumptionCost float64 // Running total of charged self-produced energy cost (e.g. EUR)
|
||||
hasPublished bool // Has initial publish happened?
|
||||
}
|
||||
|
||||
func NewSavings(tariffs tariff.Tariffs) *Savings {
|
||||
|
|
@ -97,80 +94,45 @@ func (s *Savings) SavingsAmount() float64 {
|
|||
return s.gridSavedCost
|
||||
}
|
||||
|
||||
func (s *Savings) shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower float64) float64 {
|
||||
batteryDischarge := math.Max(0, batteryPower)
|
||||
batteryCharge := math.Min(0, batteryPower) * -1
|
||||
pvConsumption := math.Min(pvPower, pvPower+gridPower-batteryCharge)
|
||||
|
||||
gridImport := math.Max(0, gridPower)
|
||||
selfConsumption := math.Max(0, batteryDischarge+pvConsumption+batteryCharge)
|
||||
|
||||
share := selfConsumption / (gridImport + selfConsumption)
|
||||
|
||||
if math.IsNaN(share) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return share
|
||||
}
|
||||
|
||||
func currentPrice(t api.Tariff, dfltPrice float64) float64 {
|
||||
if t != nil {
|
||||
if rr, err := t.Rates(); err == nil {
|
||||
if r, err := rr.Current(time.Now()); err == nil {
|
||||
return r.Price
|
||||
}
|
||||
}
|
||||
}
|
||||
return dfltPrice
|
||||
}
|
||||
|
||||
func (s *Savings) currentGridPrice() float64 {
|
||||
return currentPrice(s.tariffs.Grid, DefaultGridPrice)
|
||||
price, err := s.tariffs.CurrentGridPrice()
|
||||
if err != nil {
|
||||
price = DefaultGridPrice
|
||||
}
|
||||
return price
|
||||
}
|
||||
|
||||
func (s *Savings) currentFeedInPrice() float64 {
|
||||
return currentPrice(s.tariffs.FeedIn, DefaultFeedInPrice)
|
||||
}
|
||||
|
||||
func (s *Savings) updatePrices(p publisher) (float64, float64) {
|
||||
gridPrice := s.currentGridPrice()
|
||||
if gridPrice != s.lastGridPrice {
|
||||
s.lastGridPrice = gridPrice
|
||||
p.publish("tariffGrid", gridPrice)
|
||||
price, err := s.tariffs.CurrentFeedInPrice()
|
||||
if err != nil {
|
||||
price = DefaultFeedInPrice
|
||||
}
|
||||
|
||||
feedinPrice := s.currentFeedInPrice()
|
||||
if feedinPrice != s.lastFeedInPrice {
|
||||
s.lastFeedInPrice = feedinPrice
|
||||
p.publish("tariffFeedIn", feedinPrice)
|
||||
}
|
||||
|
||||
return gridPrice, feedinPrice
|
||||
return price
|
||||
}
|
||||
|
||||
// Update savings calculation and return grid/green energy added since last update
|
||||
func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePower float64) (float64, float64) {
|
||||
gridPrice, feedinPrice := s.updatePrices(p)
|
||||
func (s *Savings) Update(p publisher, greenShare, chargePower float64) float64 {
|
||||
defer func() { s.updated = s.clock.Now() }()
|
||||
|
||||
// no charging, no need to update
|
||||
if chargePower == 0 && s.hasPublished {
|
||||
return 0, 0
|
||||
return 0
|
||||
}
|
||||
|
||||
// assume charge power as constant over the duration -> rough kWh estimate
|
||||
deltaCharged := s.clock.Since(s.updated).Hours() * chargePower / 1e3
|
||||
share := s.shareOfSelfProducedEnergy(gridPower, pvPower, batteryPower)
|
||||
|
||||
deltaSelf := deltaCharged * share
|
||||
deltaSelf := deltaCharged * greenShare
|
||||
deltaGrid := deltaCharged - deltaSelf
|
||||
|
||||
gridPrice := s.currentGridPrice()
|
||||
feedInPrice := s.currentFeedInPrice()
|
||||
|
||||
s.gridCharged += deltaGrid
|
||||
s.gridCost += deltaGrid * gridPrice
|
||||
s.gridSavedCost += deltaSelf * (gridPrice - feedinPrice)
|
||||
s.gridSavedCost += deltaSelf * (gridPrice - feedInPrice)
|
||||
s.selfConsumptionCharged += deltaSelf
|
||||
s.selfConsumptionCost += deltaSelf * feedinPrice
|
||||
s.selfConsumptionCost += deltaSelf * feedInPrice
|
||||
|
||||
p.publish("savingsTotalCharged", s.TotalCharged())
|
||||
p.publish("savingsGridCharged", s.gridCharged)
|
||||
|
|
@ -178,9 +140,10 @@ func (s *Savings) Update(p publisher, gridPower, pvPower, batteryPower, chargePo
|
|||
p.publish("savingsSelfConsumptionPercent", s.SelfConsumptionPercent())
|
||||
p.publish("savingsEffectivePrice", s.EffectivePrice())
|
||||
p.publish("savingsAmount", s.SavingsAmount())
|
||||
|
||||
s.hasPublished = true
|
||||
|
||||
s.save()
|
||||
|
||||
return deltaCharged, deltaSelf
|
||||
return deltaCharged
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,55 +39,6 @@ type StubPublisher struct{}
|
|||
|
||||
func (p StubPublisher) publish(key string, val interface{}) {}
|
||||
|
||||
func TestSavingsWithChangingEnergySources(t *testing.T) {
|
||||
p := StubPublisher{}
|
||||
|
||||
clck := clock.NewMock()
|
||||
s := &Savings{
|
||||
clock: clck,
|
||||
started: clck.Now(),
|
||||
updated: clck.Now(),
|
||||
}
|
||||
|
||||
tc := []struct {
|
||||
title string
|
||||
grid, pv, battery, charge float64
|
||||
total, self, percentage float64
|
||||
}{
|
||||
{"half grid, half pv",
|
||||
2500, 2500, 0, 5000,
|
||||
5, 2.5, 50},
|
||||
{"full pv",
|
||||
0, 5000, 0, 5000,
|
||||
10, 7.5, 75},
|
||||
{"full grid",
|
||||
5000, 0, 0, 5000,
|
||||
15, 7.5, 50},
|
||||
{"half grid, half battery",
|
||||
2500, 0, 2500, 5000,
|
||||
20, 10, 50},
|
||||
{"full pv, pv export",
|
||||
-5000, 10000, 0, 5000,
|
||||
25, 15, 60},
|
||||
{"full pv, pv export, battery charge",
|
||||
-2500, 10000, -2500, 5000,
|
||||
30, 20, 66},
|
||||
{"double charge speed, full grid",
|
||||
10000, 0, 0, 10000,
|
||||
40, 20, 50},
|
||||
}
|
||||
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
clck.Add(time.Hour)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
assertEnergy(t, s, tc.total, tc.self, tc.percentage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavingsWithDifferentTimespans(t *testing.T) {
|
||||
p := StubPublisher{}
|
||||
|
||||
|
|
@ -99,8 +50,8 @@ func TestSavingsWithDifferentTimespans(t *testing.T) {
|
|||
}
|
||||
|
||||
type tcStep = struct {
|
||||
dt time.Duration
|
||||
grid, pv, battery, charge float64
|
||||
dt time.Duration
|
||||
greenShare, charge float64
|
||||
}
|
||||
|
||||
tc := []struct {
|
||||
|
|
@ -110,54 +61,54 @@ func TestSavingsWithDifferentTimespans(t *testing.T) {
|
|||
}{
|
||||
{"10 second not charging, full grid",
|
||||
[]tcStep{
|
||||
{10 * time.Second, 1000, 0, 0, 0},
|
||||
{10 * time.Second, 0, 0},
|
||||
},
|
||||
0, 0, 0, // 0Wh
|
||||
},
|
||||
{"10 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{10 * time.Second, 0, 0, 0, 11000},
|
||||
{10 * time.Second, 0, 11000},
|
||||
},
|
||||
0.030556, 0, 0, // 30,555Wh
|
||||
},
|
||||
{"10 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{10 * time.Second, 0, 0, 0, 11000},
|
||||
{10 * time.Second, 0, 11000},
|
||||
},
|
||||
0.061111, 0, 0, // 61,111Wh
|
||||
},
|
||||
{"5x 2 second 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 0, 0, 11000},
|
||||
{2 * time.Second, 0, 11000},
|
||||
{2 * time.Second, 0, 11000},
|
||||
{2 * time.Second, 0, 11000},
|
||||
{2 * time.Second, 0, 11000},
|
||||
{2 * time.Second, 0, 11000},
|
||||
},
|
||||
0.092, 0, 0, // 91,666Wh
|
||||
},
|
||||
{"30 min 11kW charging, full grid",
|
||||
[]tcStep{
|
||||
{30 * time.Minute, 0, 0, 0, 11000},
|
||||
{30 * time.Minute, 0, 11000},
|
||||
},
|
||||
5.592, 0, 0, // 5561,111Wh
|
||||
},
|
||||
{"4 hours 11kW charging, full pv",
|
||||
[]tcStep{
|
||||
{4 * time.Hour, 0, 11000, 0, 11000},
|
||||
{4 * time.Hour, 1, 11000},
|
||||
},
|
||||
49.592, 44, 88,
|
||||
},
|
||||
}
|
||||
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
s.Update(p, 0, 0)
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
for _, tc := range tc.steps {
|
||||
clck.Add(tc.dt)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
s.Update(p, tc.greenShare, tc.charge)
|
||||
}
|
||||
|
||||
assertEnergy(t, s, tc.total, tc.self, tc.percentage)
|
||||
|
|
@ -170,8 +121,8 @@ func TestEffectiveEnergyPriceAndSavingsAmount(t *testing.T) {
|
|||
clck := clock.NewMock()
|
||||
|
||||
type tcStep = struct {
|
||||
dt time.Duration
|
||||
grid, pv, battery, charge float64
|
||||
dt time.Duration
|
||||
greenShare, charge float64
|
||||
}
|
||||
|
||||
tc := []struct {
|
||||
|
|
@ -181,26 +132,26 @@ func TestEffectiveEnergyPriceAndSavingsAmount(t *testing.T) {
|
|||
}{
|
||||
{"1 hour, 10kW, full grid",
|
||||
[]tcStep{
|
||||
{time.Hour, 10000, 0, 0, 10000},
|
||||
{time.Hour, 0, 10000},
|
||||
},
|
||||
0.3, 0,
|
||||
},
|
||||
{"1 hour, 10kW, full pv",
|
||||
[]tcStep{
|
||||
{time.Hour, 0, 10000, 0, 10000},
|
||||
{time.Hour, 1, 10000},
|
||||
},
|
||||
0.08, 2.2,
|
||||
},
|
||||
{"1 hour, 10kW, full battery",
|
||||
[]tcStep{
|
||||
{time.Hour, 0, 0, 10000, 10000},
|
||||
{time.Hour, 1, 10000},
|
||||
},
|
||||
0.08, 2.2,
|
||||
},
|
||||
|
||||
{"1 hour, 10kW, half grid, half pv",
|
||||
[]tcStep{
|
||||
{time.Hour, 5000, 0, 5000, 10000},
|
||||
{time.Hour, 0.5, 10000},
|
||||
},
|
||||
0.19, 1.1,
|
||||
},
|
||||
|
|
@ -214,11 +165,11 @@ func TestEffectiveEnergyPriceAndSavingsAmount(t *testing.T) {
|
|||
started: clck.Now(),
|
||||
updated: clck.Now(),
|
||||
}
|
||||
s.Update(p, 0, 0, 0, 0)
|
||||
s.Update(p, 0, 0)
|
||||
|
||||
for _, tc := range tc.steps {
|
||||
clck.Add(tc.dt)
|
||||
s.Update(p, tc.grid, tc.pv, tc.battery, tc.charge)
|
||||
s.Update(p, tc.greenShare, tc.charge)
|
||||
}
|
||||
|
||||
assertPrices(t, s, tc.effectivePrice, tc.savingsAmount)
|
||||
|
|
|
|||
84
core/site.go
84
core/site.go
|
|
@ -75,6 +75,8 @@ type Site struct {
|
|||
batteryPower float64 // Battery charge power
|
||||
batterySoc float64 // Battery soc
|
||||
batteryBuffered bool // Battery buffer active
|
||||
|
||||
publishCache map[string]any // store last published values to avoid unnecessary republishing
|
||||
}
|
||||
|
||||
// MetersConfig contains the loadpoint's meter configuration
|
||||
|
|
@ -207,8 +209,9 @@ func NewSiteFromConfig(
|
|||
// NewSite creates a Site with sane defaults
|
||||
func NewSite() *Site {
|
||||
lp := &Site{
|
||||
log: util.NewLogger("site"),
|
||||
Voltage: 230, // V
|
||||
log: util.NewLogger("site"),
|
||||
publishCache: make(map[string]any),
|
||||
Voltage: 230, // V
|
||||
}
|
||||
|
||||
return lp
|
||||
|
|
@ -331,6 +334,16 @@ func (site *Site) publish(key string, val interface{}) {
|
|||
}
|
||||
}
|
||||
|
||||
// publishDelta deduplicates messages before publishing
|
||||
func (site *Site) publishDelta(key string, val interface{}) {
|
||||
if v, ok := site.publishCache[key]; ok && v == val {
|
||||
return
|
||||
}
|
||||
|
||||
site.publishCache[key] = val
|
||||
site.publish(key, val)
|
||||
}
|
||||
|
||||
// updateMeter updates and publishes single meter
|
||||
func (site *Site) updateMeter(meter api.Meter, power *float64) func() error {
|
||||
return func() error {
|
||||
|
|
@ -544,6 +557,65 @@ func (site *Site) sitePower(totalChargePower float64) (float64, error) {
|
|||
return sitePower, nil
|
||||
}
|
||||
|
||||
func (site *Site) greenShare() float64 {
|
||||
batteryDischarge := math.Max(0, site.batteryPower)
|
||||
batteryCharge := -math.Min(0, site.batteryPower)
|
||||
pvConsumption := math.Min(site.pvPower, site.pvPower+site.gridPower-batteryCharge)
|
||||
|
||||
gridImport := math.Max(0, site.gridPower)
|
||||
selfConsumption := math.Max(0, batteryDischarge+pvConsumption+batteryCharge)
|
||||
|
||||
share := selfConsumption / (gridImport + selfConsumption)
|
||||
|
||||
if math.IsNaN(share) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return share
|
||||
}
|
||||
|
||||
// effectivePrice calculates the real energy price based on self-produced and grid-imported energy.
|
||||
func (s *Site) effectivePrice(greenShare float64) (float64, error) {
|
||||
if grid, err := s.tariffs.CurrentGridPrice(); err == nil {
|
||||
feedin, err := s.tariffs.CurrentFeedInPrice()
|
||||
if err != nil {
|
||||
feedin = 0
|
||||
}
|
||||
return grid*(1-greenShare) + feedin*greenShare, nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
// effectiveCo2 calculates the amount of emitted co2 based on self-produced and grid-imported energy.
|
||||
func (s *Site) effectiveCo2(greenShare float64) (float64, error) {
|
||||
if co2, err := s.tariffs.CurrentCo2(); err == nil {
|
||||
return co2 * (1 - greenShare), nil
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
func (s *Site) publishTariffs() {
|
||||
greenShare := s.greenShare()
|
||||
|
||||
s.publish("greenShare", greenShare)
|
||||
|
||||
if gridPrice, err := s.tariffs.CurrentGridPrice(); err == nil {
|
||||
s.publishDelta("tariffGrid", gridPrice)
|
||||
}
|
||||
if feedInPrice, err := s.tariffs.CurrentFeedInPrice(); err == nil {
|
||||
s.publishDelta("tariffFeedIn", feedInPrice)
|
||||
}
|
||||
if co2, err := s.tariffs.CurrentCo2(); err == nil {
|
||||
s.publishDelta("tariffCo2", co2)
|
||||
}
|
||||
if price, err := s.effectivePrice(greenShare); err == nil {
|
||||
s.publish("tariffEffectivePrice", price)
|
||||
}
|
||||
if co2, err := s.effectiveCo2(greenShare); err == nil {
|
||||
s.publish("tariffEffectiveCo2", co2)
|
||||
}
|
||||
}
|
||||
|
||||
func (site *Site) update(lp Updater) {
|
||||
site.log.DEBUG.Println("----")
|
||||
|
||||
|
|
@ -565,11 +637,13 @@ func (site *Site) update(lp Updater) {
|
|||
site.Health.Update()
|
||||
}
|
||||
|
||||
// update savings and aggregate telemetry
|
||||
site.publishTariffs()
|
||||
greenShare := site.greenShare()
|
||||
|
||||
// TODO: use energy instead of current power for better results
|
||||
deltaCharged, deltaSelf := site.savings.Update(site, site.gridPower, site.pvPower, site.batteryPower, totalChargePower)
|
||||
deltaCharged := site.savings.Update(site, greenShare, totalChargePower)
|
||||
if telemetry.Enabled() && totalChargePower > standbyPower {
|
||||
go telemetry.UpdateChargeProgress(site.log, totalChargePower, deltaCharged, deltaSelf)
|
||||
go telemetry.UpdateChargeProgress(site.log, totalChargePower, deltaCharged, greenShare)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,3 +30,48 @@ func TestSitePower(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGreenShare(t *testing.T) {
|
||||
tc := []struct {
|
||||
title string
|
||||
grid, pv, battery float64
|
||||
share float64
|
||||
}{
|
||||
{"half grid, half pv",
|
||||
2500, 2500, 0,
|
||||
0.5},
|
||||
{"full pv",
|
||||
0, 5000, 0,
|
||||
1},
|
||||
{"full grid",
|
||||
5000, 0, 0,
|
||||
0},
|
||||
{"half grid, half battery",
|
||||
2500, 0, 2500,
|
||||
0.5},
|
||||
{"full pv, pv export",
|
||||
-5000, 10000, 0,
|
||||
1},
|
||||
{"full pv, pv export, battery charge",
|
||||
-2500, 10000, -2500,
|
||||
1},
|
||||
{"double charge speed, full grid",
|
||||
10000, 0, 0,
|
||||
0},
|
||||
}
|
||||
|
||||
for _, tc := range tc {
|
||||
t.Logf("%+v", tc)
|
||||
|
||||
s := &Site{
|
||||
gridPower: tc.grid,
|
||||
pvPower: tc.pv,
|
||||
batteryPower: tc.battery,
|
||||
}
|
||||
|
||||
share := s.greenShare()
|
||||
if share != tc.share {
|
||||
t.Errorf("greenShare wanted %.f, got %.f", tc.share, share)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,16 @@ export default defineConfig({
|
|||
setupFile: "./histoire.setup.js",
|
||||
viteNodeInlineDeps: [/!axios/],
|
||||
routerMode: "hash",
|
||||
backgroundPresets: [
|
||||
{
|
||||
label: "default",
|
||||
color: "var(--evcc-background)",
|
||||
contrastColor: "var(--evcc-default-text)",
|
||||
},
|
||||
{
|
||||
label: "box",
|
||||
color: "var(--evcc-box)",
|
||||
contrastColor: "var(--evcc-default-text)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -229,6 +229,12 @@ vehicle = "Fahrzeug"
|
|||
[settings]
|
||||
title = "Einstellungen"
|
||||
|
||||
[settings.gridDetails]
|
||||
co2 = "CO₂"
|
||||
label = "Netztarifdetails"
|
||||
none = "keine"
|
||||
price = "Preis"
|
||||
|
||||
[settings.language]
|
||||
auto = "Automatisch"
|
||||
label = "Sprache"
|
||||
|
|
|
|||
|
|
@ -229,6 +229,12 @@ vehicle = "Vehicle"
|
|||
[settings]
|
||||
title = "Settings"
|
||||
|
||||
[settings.gridDetails]
|
||||
co2 = "CO₂"
|
||||
label = "Grid details"
|
||||
none = "none"
|
||||
price = "price"
|
||||
|
||||
[settings.language]
|
||||
auto = "Automatic"
|
||||
label = "Language"
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func (t *ElectricityMaps) run(done chan error) {
|
|||
|
||||
// Unit implements the api.Tariff interface
|
||||
func (t *ElectricityMaps) Unit() string {
|
||||
return "gCO2eq"
|
||||
return Co2Equivalent
|
||||
}
|
||||
|
||||
func (t *ElectricityMaps) Rates() (api.Rates, error) {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func (t *GrünStromIndex) run(done chan error) {
|
|||
|
||||
// Unit implements the api.Tariff interface
|
||||
func (t *GrünStromIndex) Unit() string {
|
||||
return "gCO2eq"
|
||||
return Co2Equivalent
|
||||
}
|
||||
|
||||
// Rates implements the api.Tariff interface
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import (
|
|||
"golang.org/x/text/currency"
|
||||
)
|
||||
|
||||
// Co2Equivalent is the unit for co2 emissions
|
||||
const Co2Equivalent = "gCO2eq"
|
||||
|
||||
type Tariffs struct {
|
||||
Currency currency.Unit
|
||||
Grid, FeedIn, Planner api.Tariff
|
||||
|
|
@ -25,6 +28,35 @@ func NewTariffs(currency currency.Unit, grid, feedin, planner api.Tariff) *Tarif
|
|||
}
|
||||
}
|
||||
|
||||
func currentPrice(t api.Tariff) (float64, error) {
|
||||
if t != nil {
|
||||
if rr, err := t.Rates(); err == nil {
|
||||
if r, err := rr.Current(time.Now()); err == nil {
|
||||
return r.Price, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
// CurrentGridPrice returns the current grid price.
|
||||
func (t *Tariffs) CurrentGridPrice() (float64, error) {
|
||||
return currentPrice(t.Grid)
|
||||
}
|
||||
|
||||
// CurrentFeedInPrice returns the current feed-in price.
|
||||
func (t *Tariffs) CurrentFeedInPrice() (float64, error) {
|
||||
return currentPrice(t.FeedIn)
|
||||
}
|
||||
|
||||
// CurrentCo2 determines the grids co2 emission.
|
||||
func (t *Tariffs) CurrentCo2() (float64, error) {
|
||||
if t.Planner != nil && t.Planner.Unit() == Co2Equivalent {
|
||||
return currentPrice(t.Planner)
|
||||
}
|
||||
return 0, api.ErrNotAvailable
|
||||
}
|
||||
|
||||
// outdatedError returns api.ErrOutdated if t is older than 2*d
|
||||
func outdatedError(t time.Time, d time.Duration) error {
|
||||
if time.Since(t) > 2*d {
|
||||
|
|
|
|||
|
|
@ -58,19 +58,19 @@ func Create(machineID string) {
|
|||
|
||||
// UpdateChargeProgress accumulates the charge delta and uploads at given interval.
|
||||
// This interval must be smaller that the apis expiry interval for treating power values as current.
|
||||
func UpdateChargeProgress(log *util.Logger, power, deltaCharged, deltaGreen float64) {
|
||||
func UpdateChargeProgress(log *util.Logger, power, deltaCharged, greenShare float64) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// cache
|
||||
accChargeEnergy += deltaCharged
|
||||
accGreenEnergy += deltaGreen
|
||||
accGreenEnergy += deltaCharged * greenShare
|
||||
|
||||
if time.Since(updated) < 30*time.Second {
|
||||
return
|
||||
}
|
||||
|
||||
if err := upload(log, power, power*deltaGreen/deltaCharged); err != nil {
|
||||
if err := upload(log, power, power*greenShare); err != nil {
|
||||
log.ERROR.Printf("telemetry: upload failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue