Tariffs: rename price to value (BC) (#19182)

This commit is contained in:
andig 2025-04-06 19:05:35 +02:00 • committed by GitHub
parent e945ed10fb
commit 48d84356e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 271 additions and 249 deletions

View file

@ -10,12 +10,12 @@ import (
type Rate struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Price float64 `json:"price"`
Value float64 `json:"value"`
}
// IsZero returns is the rate is the zero value
func (r Rate) IsZero() bool {
return r.Start.IsZero() && r.End.IsZero() && r.Price == 0
return r.Start.IsZero() && r.End.IsZero() && r.Value == 0
}
// Rates is a slice of (future) tariff rates

View file

@ -14,7 +14,7 @@ func TestRates(t *testing.T) {
return Rate{
Start: clock.Now().Add(time.Duration(start) * time.Hour),
End: clock.Now().Add(time.Duration(start+1) * time.Hour),
Price: val,
Value: val,
}
}
@ -26,11 +26,11 @@ func TestRates(t *testing.T) {
for i := 1; i <= 4; i++ {
r, err := rr.At(clock.Now().Add(time.Duration(i) * time.Hour))
assert.NoError(t, err)
assert.Equal(t, float64(i), r.Price)
assert.Equal(t, float64(i), r.Value)
r, err = rr.At(clock.Now().Add(time.Duration(i)*time.Hour + 30*time.Minute))
assert.NoError(t, err)
assert.Equal(t, float64(i), r.Price)
assert.Equal(t, float64(i), r.Value)
}
_, err = rr.At(clock.Now().Add(5 * time.Hour))

View file

@ -75,9 +75,9 @@ import collector from "../../mixins/collector.js";
import api, { allowClientError } from "../../api.js";
import CustomSelect from "../Helper/CustomSelect.vue";
import deepEqual from "../../utils/deepEqual.js";
import convertRates from "../../utils/convertRates";
import { defineComponent, type PropType } from "vue";
import type { Vehicle, PartialBy, Timeout, Tariff, SelectOption, CURRENCY } from "../../types/evcc";
import type {
StaticPlan,
RepeatingPlan,
@ -316,10 +316,10 @@ export default defineComponent({
return;
}
const tariffRes = await api.get(`tariff/planner`, allowClientError);
const res = await api.get(`tariff/planner`, allowClientError);
this.tariff = {
rates: tariffRes.status === 404 ? [] : tariffRes.data.result.rates,
rates: res.status === 404 ? [] : convertRates(res.data.result.rates),
lastUpdate: new Date(),
};
},

View file

@ -10,7 +10,7 @@ function createDate(hoursFromNow: number) {
return result;
}
function createRate(price: number, hoursFromNow: number, durationHours = 1): Rate {
function createRate(value: number, hoursFromNow: number, durationHours = 1): Rate {
const start = new Date(now.getTime());
start.setHours(start.getHours() + hoursFromNow);
start.setMinutes(0);
@ -21,7 +21,7 @@ function createRate(price: number, hoursFromNow: number, durationHours = 1): Rat
end.setMinutes(0);
end.setSeconds(0);
end.setMilliseconds(0);
return { start, end, price };
return { start, end, value };
}
// Scenario data
@ -30,7 +30,7 @@ const co2Data = {
545, 518, 545, 518, 0, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336, 372,
400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336, 336,
336,
].map((price, i) => createRate(price, i)),
].map((value, i) => createRate(value, i)),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
smartCostType: "co2",
@ -72,7 +72,7 @@ const unknownData = {
const dynamicData = {
rates: [
0.12, 0.15, 0, -0.05, -0.11, -0.24, -0.08, 0.12, 0.25, 0.29, 0.22, 0.31, 0.31, 0.33,
].map((price, i) => createRate(price, i)),
].map((value, i) => createRate(value, i)),
duration: 8695,
plan: [createRate(0.23, 2, 5)],
smartCostType: "price",

View file

@ -1,7 +1,7 @@
import { mount, config } from "@vue/test-utils";
import { beforeAll, describe, expect, test } from "vitest";
import Preview from "./Preview.vue";
import type { Slot } from "./types";
import type { Slot } from "../../types/evcc.js";
config.global.mocks["$i18n"] = { locale: "de-DE" };
config.global.mocks["$t"] = (a: any) => a;
@ -13,14 +13,14 @@ describe("basics", () => {
{
start: new Date("2023-01-11T11:00:00+01:00"),
end: new Date("2023-01-22T00:00:00+01:00"),
price: 0.4,
value: 0.4,
},
];
const PLAN = [
{
start: new Date("2023-01-11T12:00:00+01:00"),
end: new Date("2023-01-11T13:00:00+01:00"),
price: 0.2,
value: 0.2,
},
];
@ -75,8 +75,8 @@ describe("basics", () => {
expect(result[3].charging).eq(false);
});
test("all slots have the same fixed price", () => {
result.forEach((slot) => expect(slot.price).eq(0.4));
test("all slots have the same fixed value", () => {
result.forEach((slot) => expect(slot.value).eq(0.4));
});
});
@ -87,24 +87,24 @@ describe("zoned tariffs", () => {
{
start: new Date("2023-01-11T11:00:00+01:00"),
end: new Date("2023-01-11T12:00:00+01:00"),
price: 0.2,
value: 0.2,
},
{
start: new Date("2023-01-11T12:00:00+01:00"),
end: new Date("2023-01-22T00:00:00+01:00"),
price: 0.4,
value: 0.4,
},
];
const PLAN = [
{
start: new Date("2023-01-11T11:30:00+01:00"),
end: new Date("2023-01-11T13:00:00+01:00"),
price: 0.3,
value: 0.3,
},
{
start: new Date("2023-01-11T14:30:00+01:00"),
end: new Date("2023-01-11T16:00:00+01:00"),
price: 0.2,
value: 0.2,
},
];
@ -134,7 +134,7 @@ describe("zoned tariffs", () => {
test("first slot is cheap, others are expensive", () => {
const [first, ...others] = result;
expect(first.price).eq(0.2);
others.forEach((slot) => expect(slot.price).eq(0.4));
expect(first.value).eq(0.2);
others.forEach((slot) => expect(slot.value).eq(0.4));
});
});

View file

@ -21,7 +21,7 @@
<span v-else>{{ $t("main.targetChargePlan.priceLabel") }}</span>
</div>
<div class="value text-primary">
{{ fmtAvgPrice }}
{{ fmtAvgValue }}
</div>
</div>
</div>
@ -39,8 +39,7 @@ import { defineComponent, type PropType } from "vue";
import formatter from "../../mixins/formatter.js";
import { CO2_TYPE } from "../../units.js";
import TariffChart from "../Tariff/TariffChart.vue";
import type { CURRENCY, Rate } from "assets/js/types/evcc.js";
import type { Slot } from "./types.js";
import type { CURRENCY, Rate, Slot } from "../../types/evcc.js";
export default defineComponent({
name: "ChargingPlanPreview",
@ -87,29 +86,29 @@ export default defineComponent({
hasTariff(): boolean {
return (this.rates?.length || 0) > 1;
},
avgPrice(): number | undefined {
avgValue(): number | undefined {
let hourSum = 0;
let priceSum = 0;
let valueSum = 0;
this.convertDates(this.plan).forEach((slot) => {
const hours = (slot.end.getTime() - slot.start.getTime()) / 3600000;
if (slot.price) {
if (slot.value) {
hourSum += hours;
priceSum += hours * slot.price;
valueSum += hours * slot.value;
}
});
return hourSum ? priceSum / hourSum : undefined;
return hourSum ? valueSum / hourSum : undefined;
},
fmtAvgPrice(): string {
fmtAvgValue(): string {
if (this.duration === 0) {
return "—";
}
const price = this.activeSlot ? this.activeSlot.price : this.avgPrice;
if (price === undefined) {
const value = this.activeSlot ? this.activeSlot.value : this.avgValue;
if (value === undefined) {
return this.$t("main.targetChargePlan.unknownPrice");
}
return this.isCo2
? this.fmtCo2Medium(price)
: this.fmtPricePerKWh(price, this.currency);
? this.fmtCo2Medium(value)
: this.fmtPricePerKWh(value, this.currency);
},
activeSlot(): Slot | null {
return this.activeIndex ? this.slots[this.activeIndex] : null;
@ -122,10 +121,8 @@ export default defineComponent({
}
return null;
},
targetHourOffset(): number | null {
if (!this.targetTime) {
return null;
}
targetHourOffset(): number | undefined {
if (!this.targetTime) return;
const start = new Date(this.startTime);
start.setMinutes(0);
start.setSeconds(0);
@ -155,7 +152,7 @@ export default defineComponent({
const day = this.weekdayShort(start);
const toLate = this.targetTime && this.targetTime <= start;
// TODO: handle multiple matching time slots
const price = this.findSlotInRange(start, end, rates)?.price;
const value = this.findSlotInRange(start, end, rates)?.value;
const isTarget =
this.targetTime && start <= this.targetTime && end > this.targetTime;
const charging = this.findSlotInRange(start, end, plan) != null;
@ -167,7 +164,7 @@ export default defineComponent({
this.targetTime < this.endTime;
result.push({
day,
price,
value,
startHour,
endHour,
charging,
@ -193,7 +190,7 @@ export default defineComponent({
return {
start: new Date(item.start),
end: new Date(item.end),
price: item.price,
value: item.value,
};
});
},

View file

@ -32,14 +32,3 @@ export interface StaticEnergyPlan {
energy: number;
time: Date;
}
export interface Slot {
day: string;
price?: number;
startHour: number;
endHour: number;
charging: boolean;
toLate?: boolean | null;
warning?: boolean | null;
isTarget?: boolean | null;
}

View file

@ -11,32 +11,32 @@
<script lang="ts">
import { defineComponent } from "vue";
import type { PropType } from "vue";
import { isPriceSlot, type PriceSlot, type TimeseriesEntry } from "../../utils/forecast.ts";
import formatter from "../../mixins/formatter.ts";
import { isForecastSlot, type ForecastSlot, type TimeseriesEntry } from "../../utils/forecast";
import formatter from "../../mixins/formatter";
export default defineComponent({
name: "ForecastActiveSlot",
mixins: [formatter],
props: {
activeSlot: { type: Object as PropType<PriceSlot | TimeseriesEntry | null> },
activeSlot: { type: Object as PropType<ForecastSlot | TimeseriesEntry | null> },
},
computed: {
isSlot() {
return this.activeSlot !== null && isPriceSlot(this.activeSlot);
return this.activeSlot !== null && isForecastSlot(this.activeSlot);
},
isTimeseries() {
return this.activeSlot !== null && !isPriceSlot(this.activeSlot);
return this.activeSlot !== null && !isForecastSlot(this.activeSlot);
},
day() {
const startDate = new Date((this.activeSlot! as PriceSlot).start);
const startDate = new Date((this.activeSlot! as ForecastSlot).start);
return this.weekdayShort(startDate);
},
start() {
const startDate = new Date((this.activeSlot! as PriceSlot).start);
const startDate = new Date((this.activeSlot! as ForecastSlot).start);
return this.hourShort(startDate);
},
end() {
const endDate = new Date((this.activeSlot! as PriceSlot).end);
const endDate = new Date((this.activeSlot! as ForecastSlot).end);
return this.hourShort(endDate);
},
time() {

View file

@ -38,7 +38,7 @@ import colors, { lighterColor } from "../../colors.ts";
import {
highestSlotIndexByDay,
ForecastType,
type PriceSlot,
type ForecastSlot,
type SolarDetails,
type TimeseriesEntry,
} from "../../utils/forecast.ts";
@ -63,9 +63,9 @@ export default defineComponent({
components: { Bar },
mixins: [formatter],
props: {
grid: { type: Array as PropType<PriceSlot[]> },
grid: { type: Array as PropType<ForecastSlot[]> },
solar: { type: Object as PropType<SolarDetails> },
co2: { type: Array as PropType<PriceSlot[]> },
co2: { type: Array as PropType<ForecastSlot[]> },
currency: { type: String as PropType<CURRENCY> },
selected: { type: String as PropType<ForecastType> },
},
@ -174,7 +174,7 @@ export default defineComponent({
datasets.push({
label: ForecastType.Price,
data: this.gridSlots.map((slot, index) => ({
y: slot.price,
y: slot.value,
x: new Date(slot.start),
highlight:
active &&
@ -202,7 +202,7 @@ export default defineComponent({
? this.selectedIndex === index
: index === this.maxCo2Index || index === this.minCo2Index);
return {
y: slot.price,
y: slot.value,
x: new Date(slot.start),
highlight: dataActive,
active: dataActive,
@ -422,7 +422,7 @@ export default defineComponent({
now.setMilliseconds(0);
this.startDate = now;
},
filterSlots(slots: PriceSlot[] = []) {
filterSlots(slots: ForecastSlot[] = []) {
return slots.filter(
(slot) =>
new Date(slot.end) >= this.startDate && new Date(slot.start) <= this.endDate
@ -462,7 +462,7 @@ export default defineComponent({
}, 100);
}
},
yMax(slots: PriceSlot[] = []): number | undefined {
yMax(slots: ForecastSlot[] = []): number | undefined {
const value = this.maxValue(slots);
return value ? value * 1.15 : undefined;
},
@ -472,18 +472,18 @@ export default defineComponent({
// use scale and unscaled to determine max scale
return Math.max(maxValue * scale, maxValue) * 1.15;
},
maxIndex(slots: PriceSlot[] = []) {
maxIndex(slots: ForecastSlot[] = []) {
return slots.reduce((max, slot, index) => {
return slot.price > slots[max].price ? index : max;
return slot.value > slots[max].value ? index : max;
}, 0);
},
minIndex(slots: PriceSlot[] = []) {
minIndex(slots: ForecastSlot[] = []) {
return slots.reduce((min, slot, index) => {
return slot.price < slots[min].price ? index : min;
return slot.value < slots[min].value ? index : min;
}, 0);
},
maxValue(slots: PriceSlot[] = []) {
return slots[this.maxIndex(slots)]?.price || null;
maxValue(slots: ForecastSlot[] = []) {
return slots[this.maxIndex(slots)]?.value || null;
},
maxEntryValue(entries: TimeseriesEntry[] = []) {
return entries[this.maxEntryIndex(entries)]?.val || null;

View file

@ -64,7 +64,7 @@
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import { ForecastType, type PriceSlot, type SolarDetails } from "../../utils/forecast.ts";
import { ForecastType, type ForecastSlot, type SolarDetails } from "../../utils/forecast.ts";
import formatter, { POWER_UNIT } from "../../mixins/formatter.ts";
import AnimatedNumber from "../Helper/AnimatedNumber.vue";
import type { CURRENCY, Timeout } from "assets/js/types/evcc.ts";
@ -89,8 +89,8 @@ export default defineComponent({
mixins: [formatter],
props: {
type: { type: String as () => ForecastType, required: true },
grid: { type: Array as PropType<PriceSlot[]> },
co2: { type: Array as PropType<PriceSlot[]> },
grid: { type: Array as PropType<ForecastSlot[]> },
co2: { type: Array as PropType<ForecastSlot[]> },
solar: { type: Object as PropType<SolarDetails> },
currency: { type: String as PropType<CURRENCY> },
},
@ -107,7 +107,7 @@ export default defineComponent({
isPrice() {
return this.type === ForecastType.Price;
},
upcomingSlots(): PriceSlot[] {
upcomingSlots(): ForecastSlot[] {
const now = this.now;
const slots = this.isPrice ? this.grid || [] : this.co2 || [];
return slots.filter((slot) => new Date(slot.end) > now).slice(0, 48);
@ -115,21 +115,21 @@ export default defineComponent({
averagePrice() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const price = slots.reduce((acc, slot) => acc + slot.price, 0) / slots.length;
const price = slots.reduce((acc, slot) => acc + slot.value, 0) / slots.length;
return this.fmtValue(price, true);
},
priceRange() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const min = Math.min(...slots.map((slot) => slot.price));
const max = Math.max(...slots.map((slot) => slot.price));
const min = Math.min(...slots.map((slot) => slot.value));
const max = Math.max(...slots.map((slot) => slot.value));
return `${this.fmtValue(min, false)} – ${this.fmtValue(max, true)}`;
},
lowestPriceHour() {
if (this.isSolar) return "";
const slots = this.upcomingSlots;
const min = Math.min(...slots.map((slot) => slot.price));
const slot = slots.find((slot) => slot.price === min);
const min = Math.min(...slots.map((slot) => slot.value));
const slot = slots.find((slot) => slot.value === min);
if (!slot) return "";
const start = new Date(slot.start);
const end = new Date(slot.end);

View file

@ -59,7 +59,7 @@ import TypeSelect from "./TypeSelect.vue";
import Details from "./Details.vue";
import ActiveSlot from "./ActiveSlot.vue";
import {
type PriceSlot,
type ForecastSlot,
type TimeseriesEntry,
type Forecast,
ForecastType,
@ -85,7 +85,7 @@ export default defineComponent({
data(): {
isModalVisible: boolean;
selectedType: ForecastType;
selectedSlot: PriceSlot | TimeseriesEntry | null;
selectedSlot: ForecastSlot | TimeseriesEntry | null;
} {
return {
isModalVisible: false,
@ -134,7 +134,7 @@ export default defineComponent({
modalInvisible() {
this.isModalVisible = false;
},
updateSlot(slot: PriceSlot | TimeseriesEntry | null) {
updateSlot(slot: ForecastSlot | TimeseriesEntry | null) {
this.selectedSlot = slot;
},
updateSelectedType() {

View file

@ -325,7 +325,7 @@ export default {
const { rates } = res.data.result;
this.referenceGrid =
rates.reduce((acc, slot) => {
return acc + slot.price;
return acc + slot.value;
}, 0) / rates.length;
} catch (e) {
this.referenceGrid = undefined;

View file

@ -82,47 +82,53 @@
</div>
</template>
<script>
<script lang="ts">
import formatter from "../../mixins/formatter.js";
import TariffChart from "./TariffChart.vue";
import { CO2_TYPE } from "../../units.js";
import api, { allowClientError } from "../../api.js";
import convertRates from "../../utils/convertRates";
import type { PropType } from "vue";
import type { Tariff, CURRENCY, Rate, SelectOption, Slot } from "../../types/evcc";
export default {
name: "SmartCostLimit",
components: { TariffChart },
mixins: [formatter],
props: {
smartCostLimit: Number,
smartCostLimit: {
type: [Number, null] as PropType<number | null>,
required: true,
},
smartCostType: String,
tariffGrid: Number,
currency: String,
currency: String as PropType<CURRENCY>,
loadpointId: Number,
multipleLoadpoints: Boolean,
possible: Boolean,
},
data() {
return {
selectedSmartCostLimit: null,
tariff: null,
startTime: null,
activeIndex: null,
selectedSmartCostLimit: null as number | null,
tariff: null as Tariff | null,
startTime: null as Date | null,
activeIndex: null as number | null,
applyToAllVisible: false,
};
},
computed: {
isCo2() {
isCo2(): boolean {
return this.smartCostType === CO2_TYPE;
},
costOptions() {
costOptions(): SelectOption<number>[] {
const { max } = this.costRange(this.totalSlots);
const values = [];
const values = [] as number[];
const stepSize = this.optionStepSize;
for (let i = 1; i <= 100; i++) {
const value = this.optionStartValue + stepSize * i;
if (value > max + stepSize) break;
values.push(this.roundLimit(value));
if (max !== undefined && value > max + stepSize) break;
values.push(this.roundLimit(value) as number);
}
// add special entry if currently selected value is not in the scale
const selected = this.selectedSmartCostLimit;
@ -140,7 +146,7 @@ export default {
const stepSize = this.optionStepSize;
// always show some negative values for price
const start = this.isCo2 ? 0 : stepSize * -11;
const minValue = Math.min(start, min);
const minValue = min !== undefined ? Math.min(start, min) : start;
return Math.floor(minValue / stepSize) * stepSize;
},
optionStepSize() {
@ -148,6 +154,9 @@ export default {
return 1;
}
const { min, max } = this.costRange(this.totalSlots);
if (min === undefined || max === undefined) {
return 1;
}
const baseSteps = [0.001, 0.002, 0.005];
const range = max - Math.min(0, min);
@ -159,11 +168,11 @@ export default {
}
return 1;
},
slots() {
const result = [];
if (!this.tariff?.rates) return result;
slots(): Slot[] {
const result: Slot[] = [];
if (!this.tariff?.rates || !this.startTime) return result;
const rates = this.convertDates(this.tariff.rates);
const { rates } = this.tariff;
const oneHour = 60 * 60 * 1000;
for (let i = 0; i < 42; i++) {
@ -177,17 +186,19 @@ export default {
const endHour = end.getHours();
const day = this.weekdayShort(start);
// TODO: handle multiple matching time slots
const price = this.findSlotInRange(start, end, rates)?.price;
const value = this.findRateInRange(start, end, rates)?.value;
const charging =
price <= this.selectedSmartCostLimit && this.selectedSmartCostLimit !== null;
const selectable = price !== undefined;
result.push({ day, price, startHour, endHour, charging, selectable });
this.selectedSmartCostLimit !== null &&
value !== undefined &&
value <= this.selectedSmartCostLimit;
const selectable = value !== undefined;
result.push({ day, value, startHour, endHour, charging, selectable });
}
return result;
},
totalSlots() {
return this.slots.filter((s) => s.price !== undefined);
return this.slots.filter((s) => s.value !== undefined);
},
chargingSlots() {
return this.totalSlots.filter((s) => s.charging);
@ -198,8 +209,8 @@ export default {
chargingCostRange() {
return this.fmtCostRange(this.costRange(this.chargingSlots));
},
activeSlot() {
return this.slots[this.activeIndex];
activeSlot(): Slot | null {
return this.activeIndex !== null ? this.slots[this.activeIndex] : null;
},
activeSlotName() {
if (this.activeSlot) {
@ -210,14 +221,14 @@ export default {
return null;
},
activeSlotCost() {
const price = this.activeSlot?.price;
if (price === undefined) {
const value = this.activeSlot?.value;
if (value === undefined) {
return this.$t("main.targetChargePlan.unknownPrice");
}
if (this.isCo2) {
return this.fmtCo2Medium(price);
return this.fmtCo2Medium(value);
}
return this.fmtPricePerKWh(price, this.currency);
return this.fmtPricePerKWh(value, this.currency);
},
title() {
return this.$t(`smartCost.${this.isCo2 ? "clean" : "cheap"}Title`);
@ -245,10 +256,10 @@ export default {
this.selectedSmartCostLimit = this.roundLimit(this.smartCostLimit);
},
methods: {
roundLimit(limit) {
roundLimit(limit: number | null): number | null {
return limit === null ? null : Math.round(limit * 1000) / 1000;
},
fmtSmartCostLimit(limit) {
fmtSmartCostLimit(limit: number | null): string {
if (limit === null) {
return this.$t("smartCost.none");
}
@ -258,45 +269,38 @@ export default {
},
async updateTariff() {
try {
const tariffRes = await api.get(`tariff/planner`, allowClientError);
if (tariffRes.status === 200) {
this.tariff = tariffRes.data.result;
const res = await api.get(`tariff/planner`, allowClientError);
if (res.status === 200) {
this.tariff = {
rates: convertRates(res.data.result.rates),
lastUpdate: new Date(),
};
this.startTime = new Date();
}
} catch (e) {
console.error(e);
}
},
convertDates(list) {
if (!list?.length) {
return [];
findRateInRange(start: Date, end: Date, rates: Rate[]) {
return rates.find((r) => {
if (r.start.getTime() < start.getTime()) {
return r.end.getTime() > start.getTime();
}
return list.map((item) => {
return {
start: new Date(item.start),
end: new Date(item.end),
price: item.price,
};
return r.start.getTime() < end.getTime();
});
},
findSlotInRange(start, end, slots) {
return slots.find((s) => {
if (s.start.getTime() < start.getTime()) {
return s.end.getTime() > start.getTime();
}
return s.start.getTime() < end.getTime();
});
},
costRange(slots) {
let min = undefined;
let max = undefined;
costRange(slots: Slot[]): { min: number | undefined; max: number | undefined } {
let min = undefined as number | undefined;
let max = undefined as number | undefined;
slots.forEach((slot) => {
min = min === undefined ? slot.price : Math.min(min, slot.price);
max = max === undefined ? slot.price : Math.max(max, slot.price);
if (slot.value === undefined) return;
min = min === undefined ? slot.value : Math.min(min, slot.value);
max = max === undefined ? slot.value : Math.max(max, slot.value);
});
return { min, max };
},
fmtCostRange({ min, max }) {
fmtCostRange({ min, max }: { min: number | undefined; max: number | undefined }): string {
if (min === undefined || max === undefined) return "";
const fmtMin = this.isCo2
? this.fmtCo2Short(min)
: this.fmtPricePerKWh(min, this.currency, true);
@ -305,21 +309,22 @@ export default {
: this.fmtPricePerKWh(max, this.currency, true);
return `${fmtMin} – ${fmtMax}`;
},
slotHovered(index) {
slotHovered(index: number) {
this.activeIndex = index;
},
slotSelected(index) {
const price = this.slots[index].price;
if (price !== undefined) {
slotSelected(index: number) {
const value = this.slots[index].value;
if (value !== undefined) {
// 3 decimal precision
const priceRounded = Math.ceil(price * 1000) / 1000;
this.saveSmartCostLimit(priceRounded);
const valueRounded = Math.ceil(value * 1000) / 1000;
this.saveSmartCostLimit(`${valueRounded}`);
}
},
changeSmartCostLimit($event) {
this.saveSmartCostLimit($event.target.value);
changeSmartCostLimit($event: Event) {
const value = ($event.target as HTMLSelectElement).value;
this.saveSmartCostLimit(value);
},
async saveSmartCostLimit(limit) {
async saveSmartCostLimit(limit: string) {
const url = this.isLoadpoint
? `loadpoints/${this.loadpointId}/smartcostlimit`
: "batterygridchargelimit";

View file

@ -3,7 +3,7 @@
<div class="chart position-relative">
<div
v-for="(slot, index) in slots"
:key="slot.start"
:key="`${slot.day}-${slot.startHour}`"
:data-index="index"
class="slot user-select-none"
:class="{
@ -22,8 +22,8 @@
@mouseup="hoverSlot(null)"
@click="selectSlot(index)"
>
<div class="slot-bar" :style="priceStyle(slot.price)">
<span v-if="slot.price === undefined && avgPrice" class="unknown">?</span>
<div class="slot-bar" :style="valueStyle(slot.value)">
<span v-if="slot.value === undefined && avgValue" class="unknown">?</span>
</div>
<div class="slot-label">
<span v-if="!slot.isTarget || targetNearlyOutOfRange">{{
@ -52,11 +52,12 @@
</div>
</template>
<script>
<script lang="ts">
import type { PropType } from "vue";
import "@h2d2/shopicons/es/regular/arrowright";
import PlanEndIcon from "../MaterialIcon/PlanEnd.vue";
import formatter from "../../mixins/formatter.js";
import { CO2_TYPE } from "../../units.js";
import type { Slot } from "../../types/evcc.ts";
const BAR_WIDTH = 20;
@ -67,29 +68,29 @@ export default {
},
mixins: [formatter],
props: {
slots: Array,
slots: { type: Array as PropType<Slot[]>, default: () => [] },
targetText: [String, null],
targetOffset: [Number, null],
targetOffset: { type: Number, default: 0 },
},
emits: ["slot-hovered", "slot-selected"],
data() {
return {
activeIndex: null,
activeIndex: null as number | null,
startTime: new Date(),
longPressTimer: null,
longPressTimer: undefined as number | undefined,
isLongPress: false,
};
},
computed: {
priceInfo() {
valueInfo() {
let max = Number.MIN_VALUE;
let min = 0;
this.slots
.map((s) => s.price)
.filter((price) => price !== undefined)
.forEach((price) => {
max = Math.max(max, price);
min = Math.min(min, price);
.map((s) => s.value)
.filter((value) => value !== undefined)
.forEach((value) => {
max = Math.max(max, value);
min = Math.min(min, value);
});
return { min, range: max - min };
},
@ -104,30 +105,27 @@ export default {
targetOutOfRange() {
return this.targetOffset > this.slots.length;
},
avgPrice() {
avgValue() {
let sum = 0;
let count = 0;
this.slots.forEach((s) => {
if (s.price !== undefined) {
sum += s.price;
if (s.value !== undefined) {
sum += s.value;
count++;
}
});
return sum / count;
},
isCo2() {
return this.unit === CO2_TYPE;
},
activeSlot() {
return this.slots[this.activeIndex];
return this.activeIndex !== null ? this.slots[this.activeIndex] : null;
},
},
methods: {
hoverSlot(index) {
hoverSlot(index: number | null) {
this.activeIndex = index;
this.$emit("slot-hovered", index);
},
selectSlot(index) {
selectSlot(index: number) {
if (this.isLongPress) {
this.isLongPress = false;
return;
@ -136,7 +134,7 @@ export default {
this.$emit("slot-selected", index);
}
},
showWeekday(index) {
showWeekday(index: number) {
const slot = this.slots[index];
if (!slot) {
return false;
@ -151,15 +149,15 @@ export default {
}
return false;
},
priceStyle(price) {
const value = price === undefined ? this.avgPrice : price;
valueStyle(value: number | undefined) {
const val = value === undefined ? this.avgValue : value;
const height =
value !== undefined && !isNaN(value)
? `${10 + (90 / this.priceInfo.range) * (value - this.priceInfo.min)}%`
value !== undefined && !isNaN(val)
? `${10 + (90 / this.valueInfo.range) * (val - this.valueInfo.min)}%`
: "75%";
return { height };
},
startLongPress(index) {
startLongPress(index: number) {
this.longPressTimer = setTimeout(() => {
this.isLongPress = true;
this.hoverSlot(index);

View file

@ -35,10 +35,29 @@ export interface Tariff {
lastUpdate: Date;
}
// data from api with string-based timestamps
export interface RateRaw {
start: string;
end: string;
value: number;
}
export interface Rate {
start: Date;
end: Date;
price: number;
value: number;
}
export interface Slot {
day: string;
value?: number;
startHour: number;
endHour: number;
charging: boolean;
toLate?: boolean | null;
warning?: boolean | null;
isTarget?: boolean | null;
selectable?: boolean | null;
}
export interface SelectOption<T> {

View file

@ -0,0 +1,14 @@
import type { RateRaw, Rate } from "../types/evcc";
function convertRate(rate: RateRaw): Rate {
return {
start: new Date(rate.start),
end: new Date(rate.end),
value: rate.value,
};
}
export default function convertRates(rates: RateRaw[] | null): Rate[] {
if (!rates) return [];
return rates.map(convertRate);
}

View file

@ -5,14 +5,14 @@ export interface TimeseriesEntry {
ts: string;
}
export interface PriceSlot {
export interface ForecastSlot {
start: string;
end: string;
price: number;
value: number;
}
export function isPriceSlot(obj?: TimeseriesEntry | PriceSlot): obj is PriceSlot {
return (obj as PriceSlot).start !== undefined;
export function isForecastSlot(obj?: TimeseriesEntry | ForecastSlot): obj is ForecastSlot {
return (obj as ForecastSlot).start !== undefined;
}
export interface EnergyByDay {
@ -29,8 +29,8 @@ export interface SolarDetails {
}
export interface Forecast {
grid?: PriceSlot[];
co2?: PriceSlot[];
grid?: ForecastSlot[];
co2?: ForecastSlot[];
solar?: SolarDetails;
}
@ -81,7 +81,7 @@ export function adjustedSolar(solar?: SolarDetails): SolarDetails | undefined {
if (!solar?.scale) return solar;
const { scale } = solar;
let result = deepCopy(solar);
const result = deepCopy(solar);
if (result.today) result.today.energy *= scale;
if (result.tomorrow) result.tomorrow.energy *= scale;

View file

@ -47,14 +47,14 @@ import Chart from "../components/Forecast/Chart.vue";
import IconSelectItem from "../components/Helper/IconSelectItem.vue";
import IconSelectGroup from "../components/Helper/IconSelectGroup.vue";
import DynamicPriceIcon from "../components/MaterialIcon/DynamicPrice.vue";
import { ForecastType, type PriceSlot, type SolarDetails } from "../utils/forecast";
import { ForecastType, type ForecastSlot, type SolarDetails } from "../utils/forecast";
import formatter from "../mixins/formatter.ts";
import store from "../store.ts";
interface Forecast {
grid?: PriceSlot[];
grid?: ForecastSlot[];
solar?: SolarDetails;
co2?: PriceSlot[];
co2?: ForecastSlot[];
}
export default defineComponent({

View file

@ -84,7 +84,7 @@ func runTariff(cmd *cobra.Command, args []string) {
const format = "2006-01-02 15:04:05"
for _, r := range rates {
fmt.Fprintf(tw, "%s\t%s\t%.3f\n", r.Start.Local().Format(format), r.End.Local().Format(format), r.Price)
fmt.Fprintf(tw, "%s\t%s\t%.3f\n", r.Start.Local().Format(format), r.End.Local().Format(format), r.Value)
}
tw.Flush()

View file

@ -1866,7 +1866,7 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, rates api.Rate
// cheap tariff
if smartCostActive {
rate, _ := rates.At(time.Now())
lp.log.DEBUG.Printf("smart cost active: %.2f", rate.Price)
lp.log.DEBUG.Printf("smart cost active: %.2f", rate.Value)
err = lp.fastCharging()
lp.resetPhaseTimer()
lp.elapsePVTimer() // let PV mode disable immediately afterwards

View file

@ -151,7 +151,7 @@ func (lp *Loadpoint) plannerActive() (active bool) {
// log plan
for _, slot := range plan {
lp.log.TRACE.Printf(" slot from: %v to %v cost %.3f", slot.Start.Round(time.Second).Local(), slot.End.Round(time.Second).Local(), slot.Price)
lp.log.TRACE.Printf(" slot from: %v to %v cost %.3f", slot.Start.Round(time.Second).Local(), slot.End.Round(time.Second).Local(), slot.Value)
}
activeSlot := planner.SlotAt(lp.clock.Now(), plan)

View file

@ -9,7 +9,7 @@ import (
func (lp *Loadpoint) smartCostActive(rates api.Rates) bool {
rate, err := rates.At(time.Now())
limit := lp.GetSmartCostLimit()
return err == nil && limit != nil && rate.Price <= *limit
return err == nil && limit != nil && rate.Value <= *limit
}
// smartCostNextStart returns the next start time for a smart cost rate below the limit
@ -21,7 +21,7 @@ func (lp *Loadpoint) smartCostNextStart(rates api.Rates) time.Time {
now := time.Now()
for _, slot := range rates {
if slot.Start.After(now) && slot.Price <= *limit {
if slot.Start.After(now) && slot.Value <= *limit {
return slot.Start
}
}

View file

@ -44,7 +44,7 @@ func AverageCost(plan api.Rates) float64 {
for _, slot := range plan {
slotDuration := slot.End.Sub(slot.Start)
duration += slotDuration
cost += float64(slotDuration) * slot.Price
cost += float64(slotDuration) * slot.Value
}
return cost / float64(duration)
}

View file

@ -21,7 +21,7 @@ func rates(prices []float64, start time.Time, slotDuration time.Duration) api.Ra
ar := api.Rate{
Start: slotStart,
End: slotStart.Add(slotDuration),
Price: v,
Value: v,
}
res = append(res, ar)
}

View file

@ -5,9 +5,9 @@ import "github.com/evcc-io/evcc/api"
// sortByCost is a sortFunc for slices.Sort
func sortByCost(i, j api.Rate) int {
switch {
case i.Price < j.Price:
case i.Value < j.Value:
return -1
case i.Price > j.Price:
case i.Value > j.Value:
return +1
default:
return j.Start.Compare(i.Start)

View file

@ -13,15 +13,15 @@ import (
func testRates(clock clock.Clock) api.Rates {
return api.Rates{
{
Price: 2,
Value: 2,
Start: clock.Now().Add(2 * time.Hour),
},
{
Price: 2,
Value: 2,
Start: clock.Now(),
},
{
Price: 1,
Value: 1,
Start: clock.Now().Add(time.Hour),
},
}

View file

@ -93,12 +93,12 @@ func (site *Site) plannerRates() (api.Rates, error) {
func (site *Site) smartCostActive(lp loadpoint.API, rate api.Rate) bool {
limit := lp.GetSmartCostLimit()
return limit != nil && !rate.IsZero() && rate.Price <= *limit
return limit != nil && !rate.IsZero() && rate.Value <= *limit
}
func (site *Site) batteryGridChargeActive(rate api.Rate) bool {
limit := site.GetBatteryGridChargeLimit()
return limit != nil && !rate.IsZero() && rate.Price <= *limit
return limit != nil && !rate.IsZero() && rate.Value <= *limit
}
func (site *Site) dischargeControlActive(rate api.Rate) bool {

View file

@ -89,7 +89,7 @@ func timestampSeries(rr api.Rates) timeseries {
return lo.Map(rr, func(r api.Rate, _ int) tsval {
return tsval{
Timestamp: r.Start,
Value: r.Price,
Value: r.Value,
}
})
}

View file

@ -105,10 +105,10 @@ func (t *Amber) run(done chan error) {
ar := api.Rate{
Start: startTime.Local(),
End: endTime.Local(),
Price: r.PerKwh / 1e2,
Value: r.PerKwh / 1e2,
}
if r.AdvancedPrice != nil {
ar.Price = r.AdvancedPrice.Predicted / 1e2
ar.Value = r.AdvancedPrice.Predicted / 1e2
}
data = append(data, ar)
}

View file

@ -85,7 +85,7 @@ func (t *Awattar) run(done chan error) {
ar := api.Rate{
Start: r.StartTimestamp.Local(),
End: r.EndTimestamp.Local(),
Price: t.totalPrice(r.Marketprice/1e3, r.StartTimestamp),
Value: t.totalPrice(r.Marketprice/1e3, r.StartTimestamp),
}
data = append(data, ar)
}

View file

@ -54,7 +54,7 @@ func (t *combined) Rates() (api.Rates, error) {
return nil, errors.New("combined tariffs must have the same period length")
}
rate.Price += r.Price
rate.Value += r.Value
}
res = append(res, rate)

View file

@ -28,7 +28,7 @@ func TestCombined(t *testing.T) {
return api.Rate{
Start: clock.Now().Add(time.Duration(start) * time.Hour),
End: clock.Now().Add(time.Duration(start+1) * time.Hour),
Price: val,
Value: val,
}
}

View file

@ -140,7 +140,7 @@ func (t *EdfTempo) run(done chan error) {
ar := api.Rate{
Start: ts,
End: ts.Add(time.Hour),
Price: t.totalPrice(t.prices[strings.ToLower(r.Value)], ts),
Value: t.totalPrice(t.prices[strings.ToLower(r.Value)], ts),
}
data = append(data, ar)
}

View file

@ -103,7 +103,7 @@ func (t *ElectricityMaps) run(done chan error) {
ar := api.Rate{
Start: r.Datetime.Local(),
End: r.Datetime.Add(time.Hour).Local(),
Price: r.CarbonIntensity,
Value: r.CarbonIntensity,
}
data = append(data, ar)
}

View file

@ -89,7 +89,7 @@ func (t *Elering) run(done chan error) {
ar := api.Rate{
Start: ts,
End: ts.Add(time.Hour),
Price: t.totalPrice(r.Price/1e3, ts),
Value: t.totalPrice(r.Price/1e3, ts),
}
data = append(data, ar)
}

View file

@ -88,7 +88,7 @@ func (t *Energinet) run(done chan error) {
ar := api.Rate{
Start: ts.Local(),
End: ts.Add(time.Hour).Local(),
Price: t.totalPrice(r.SpotPriceDKK/1e3, ts),
Value: t.totalPrice(r.SpotPriceDKK/1e3, ts),
}
data = append(data, ar)
}

View file

@ -154,7 +154,7 @@ func (t *Entsoe) run(done chan error) {
ar := api.Rate{
Start: r.Start.Local(),
End: r.End.Local(),
Price: t.totalPrice(r.Value, r.Start),
Value: t.totalPrice(r.Value, r.Start),
}
data = append(data, ar)
}

View file

@ -118,9 +118,9 @@ func (t *Fixed) Rates() (api.Rates, error) {
}
rate := api.Rate{
Price: zone.Price,
Start: ts,
End: end,
Value: zone.Price,
}
res = append(res, rate)

View file

@ -26,7 +26,7 @@ func TestFixed(t *testing.T) {
for hour := 0; hour < 24; hour++ {
expect = append(expect, api.Rate{
Price: 0.3,
Value: 0.3,
Start: dayStart.Add(time.Hour * time.Duration(hour)),
End: dayStart.Add(time.Hour * time.Duration(hour+1)),
})
@ -60,7 +60,7 @@ func TestFixedSplitZones(t *testing.T) {
// 00:00-05:00 0.1
for hour := 0; hour < 5; hour++ {
expect = append(expect, api.Rate{
Price: 0.1,
Value: 0.1,
Start: dayStart.Add(time.Hour * time.Duration(hour)),
End: dayStart.Add(time.Hour * time.Duration(hour+1)),
})
@ -68,14 +68,14 @@ func TestFixedSplitZones(t *testing.T) {
// 05:00-05:30 0.1
expect = append(expect, api.Rate{
Price: 0.1,
Value: 0.1,
Start: dayStart.Add(5 * time.Hour),
End: dayStart.Add(5*time.Hour + 30*time.Minute),
})
// 05:30-06:00 0.5
expect = append(expect, api.Rate{
Price: 0.5,
Value: 0.5,
Start: dayStart.Add(5*time.Hour + 30*time.Minute),
End: dayStart.Add(6 * time.Hour),
})
@ -83,7 +83,7 @@ func TestFixedSplitZones(t *testing.T) {
// 06:00-21:00 0.5
for hour := 6; hour < 21; hour++ {
expect = append(expect, api.Rate{
Price: 0.5,
Value: 0.5,
Start: dayStart.Add(time.Hour * time.Duration(hour)),
End: dayStart.Add(time.Hour * time.Duration(hour+1)),
})
@ -92,7 +92,7 @@ func TestFixedSplitZones(t *testing.T) {
// 21:00-00:00 0.1
for hour := 21; hour < 24; hour++ {
expect = append(expect, api.Rate{
Price: 0.1,
Value: 0.1,
Start: dayStart.Add(time.Hour * time.Duration(hour)),
End: dayStart.Add(time.Hour * time.Duration(hour+1)),
})

View file

@ -66,7 +66,7 @@ func (t *GroupeE) run(done chan error) {
ar := api.Rate{
Start: r.StartTimestamp.Local(),
End: r.EndTimestamp.Local(),
Price: r.VarioPlus / 1e2, // Rp/kWh
Value: r.VarioPlus / 1e2, // Rp/kWh
}
data = append(data, ar)
}

View file

@ -88,9 +88,9 @@ func (t *GrünStromIndex) run(done chan error) {
data := make(api.Rates, 0, len(res.Forecast))
for _, r := range res.Forecast {
data = append(data, api.Rate{
Price: float64(r.Co2GStandard),
Start: time.UnixMilli(r.Timeframe.Start).Local(),
End: time.UnixMilli(r.Timeframe.End).Local(),
Value: float64(r.Co2GStandard),
})
}

View file

@ -20,7 +20,7 @@ func TestMergeRatesAfter(t *testing.T) {
return api.Rate{
Start: clock.Now().Add(time.Duration(start) * time.Hour),
End: clock.Now().Add(time.Duration(start+1) * time.Hour),
Price: val,
Value: val,
}
}

View file

@ -91,7 +91,7 @@ func (t *Ngeso) run(done chan error) {
Start: r.ValidityStart.Time,
End: r.ValidityEnd.Time,
// Use the forecasted rate, as the actual rate is only available for historical data
Price: r.Intensity.Forecast,
Value: r.Intensity.Forecast,
}
data = append(data, ar)
}

View file

@ -145,7 +145,7 @@ func (t *Octopus) run(done chan error) {
Start: r.ValidityStart,
End: rateEnd,
// UnitRates are supplied inclusive of tax, though this could be flipped easily with a config flag.
Price: r.PriceInclusiveTax / 1e2,
Value: r.PriceInclusiveTax / 1e2,
}
data = append(data, ar)
}

View file

@ -190,7 +190,7 @@ func (t *Ostrom) runStatic(done chan error) {
data[i] = api.Rate{
Start: ts,
End: ts.Add(time.Hour),
Price: price / 100.0,
Value: price / 100.0,
}
}
@ -232,7 +232,7 @@ func (t *Ostrom) run(done chan error) {
data = append(data, api.Rate{
Start: ts,
End: ts.Add(time.Hour),
Price: (val.Marketprice + val.AdditionalCost) / 100.0, // Both values include VAT
Value: (val.Marketprice + val.AdditionalCost) / 100.0, // Both values include VAT
})
}

View file

@ -205,7 +205,7 @@ func (t *Pun) getData(day time.Time) (api.Rates, error) {
ar := api.Rate{
Start: ts,
End: ts.Add(time.Hour),
Price: t.totalPrice(price/1e3, ts),
Value: t.totalPrice(price/1e3, ts),
}
data = append(data, ar)
}

View file

@ -75,7 +75,7 @@ func (t *SmartEnergy) run(done chan error) {
ar := api.Rate{
Start: r.Date.Local(),
End: r.Date.Add(time.Hour).Local(),
Price: t.totalPrice(r.Value/100, r.Date),
Value: t.totalPrice(r.Value/100, r.Date),
}
data = append(data, ar)
}

View file

@ -95,12 +95,12 @@ func (t *Solcast) run(interval time.Duration, done chan error) {
rr := api.Rate{
Start: start,
End: start.Add(time.Hour),
Price: r.PvEstimate * 1e3,
Value: r.PvEstimate * 1e3,
}
if r.Period.Duration() != time.Hour {
for i, r := range data {
if r.Start.Equal(rr.Start) {
data[i].Price = (r.Price + rr.Price) / 2
data[i].Value = (r.Value + rr.Value) / 2
continue NEXT
}
}

View file

@ -99,7 +99,7 @@ func (t *Tariff) run(forecastG func() (string, error), done chan error, interval
}
for i, r := range data {
data[i] = api.Rate{
Price: t.totalPrice(r.Price, r.Start),
Value: t.totalPrice(r.Value, r.Start),
Start: r.Start.Local(),
End: r.End.Local(),
}
@ -145,7 +145,7 @@ func (t *Tariff) priceRates() (api.Rates, error) {
res[i] = api.Rate{
Start: slot,
End: slot.Add(time.Hour),
Price: t.totalPrice(price, slot),
Value: t.totalPrice(price, slot),
}
}

View file

@ -28,7 +28,7 @@ func At(t api.Tariff, ts time.Time) (api.Rate, error) {
// Now returns the price/cost/value at the given time
func Now(t api.Tariff) (float64, error) {
r, err := At(t, time.Now())
return r.Price, err
return r.Value, err
}
func Forecast(t api.Tariff) api.Rates {

View file

@ -120,7 +120,7 @@ func (t *Tibber) rates(pi []tibber.Price) api.Rates {
ar := api.Rate{
Start: r.StartsAt.Local(),
End: r.StartsAt.Add(time.Hour).Local(),
Price: price,
Value: price,
}
data = append(data, ar)
}

View file

@ -18,5 +18,5 @@ render: |
[.timestamps, .prices] | transpose | map({
"start": .[0] | strptime("%FT%T.%f%z") | strftime("%FT%TZ"),
"end": .[0] | strptime("%FT%T.%f%z") | mktime + 3600 | strftime("%FT%TZ"),
"price": .[1]
"value": .[1]
}) | tostring

View file

@ -46,5 +46,5 @@ render: |
[.unix_seconds, .price] | transpose | map({
"start": .[0] | todate,
"end": .[0]+3600 | todate,
"price": .[1]/1000
"value": .[1]/1000
}) | tostring

View file

@ -56,7 +56,7 @@ render: |
{
"start": .datum | strptime("%F %T") | strflocaltime("%FT%T%:z"),
"end": .datum | strptime("%F %T") | strflocaltime("%FT%T%:z") | strptime("%FT%T%z") | mktime + 3600 | strflocaltime("%FT%T%:z"),
"price": .prijs{{ .provider }} | tonumber
"value": .prijs{{ .provider }} | tonumber
}
] | tostring
- name: tomorrow
@ -69,7 +69,7 @@ render: |
{
"start": .datum | strptime("%F %T") | strflocaltime("%FT%T%:z"),
"end": .datum | strptime("%F %T") | strflocaltime("%FT%T%:z") | strptime("%FT%T%z") | mktime + 3600 | strflocaltime("%FT%T%:z"),
"price": .prijs{{ .provider }} | tonumber
"value": .prijs{{ .provider }} | tonumber
}
] | tostring
interval: {{ .interval }}

View file

@ -45,6 +45,6 @@ render: |
[ .result.watts | to_entries.[] | {
"start": (.key | strptime("%FT%T%z") | strftime("%FT%TZ")),
"end": (.key | strptime("%FT%T%z") | mktime+3600 | strftime("%FT%TZ")),
"price": .value
"value": .value
} ] | tostring
interval: {{ .interval }}

View file

@ -62,7 +62,7 @@ render: |
{
"start": .deliveryStart,
"end": .deliveryEnd,
"price": .entryPerArea.{{ .region }} / 1000
"value": .entryPerArea.{{ .region }} / 1000
}
] | tostring
- name: tomorrow
@ -76,7 +76,7 @@ render: |
{
"start": .deliveryStart,
"end": .deliveryEnd,
"price": .entryPerArea.{{ .region }} / 1000
"value": .entryPerArea.{{ .region }} / 1000
}
] | tostring
{{ include "tariff-base" . }}

View file

@ -31,5 +31,5 @@ render: |
[ .[] | {
start: .from,
end: (.from | fromdate + 3600 | todate),
price: (.price/100)
value: (.price/100)
} ] | tostring