From d68e5e58093613d3d2813572e96ee657bbf0ad8b Mon Sep 17 00:00:00 2001 From: Daniel Martin Date: Fri, 17 Jul 2026 14:54:56 +0200 Subject: [PATCH] Tariffs: add outdoor temperature type and OpenMeteo (#27780) Co-authored-by: Michael Geers --- api/globalconfig/types.go | 30 +++---- api/tariff.go | 2 + api/tarifftype_enumer.go | 12 ++- api/tariffusage_enumer.go | 12 ++- assets/css/app.css | 5 ++ assets/js/colors.ts | 3 + assets/js/components/Config/DeviceTags.vue | 6 +- assets/js/components/Config/TariffCard.vue | 4 +- assets/js/components/Config/TariffModal.vue | 11 ++- .../Config/defaultYaml/tariffTemperature.yaml | 28 +++++++ .../Forecast/{Co2Chart.vue => ValueChart.vue} | 42 +++++++--- .../{Co2Details.vue => ValueDetails.vue} | 30 ++++--- assets/js/types/evcc.ts | 4 +- assets/js/views/Config.vue | 27 +++++- assets/js/views/Forecast.vue | 83 ++++++++++--------- cmd/demo.yaml | 3 + cmd/refs.go | 14 +--- cmd/setup.go | 1 + core/keys/site.go | 1 + core/metrics/collector.go | 24 +++--- core/metrics/db_history.go | 2 +- core/site.go | 7 ++ core/site_tariffs.go | 29 +++++-- i18n/de.json | 22 ++++- i18n/en.json | 22 ++++- server/http_config_device_handler.go | 2 +- server/http_config_helper.go | 3 + tariff/tariff.go | 4 + tariff/tariffs.go | 7 +- .../tariff/demo-temperature-forecast.yaml | 61 ++++++++++++++ .../tariff/open-meteo-temperature.yaml | 34 ++++++++ tests/config-tariffs.spec.ts | 30 ++++++- tests/config-with-tariffs.evcc.yaml | 16 ++++ util/templates/defaults.yaml | 3 + 34 files changed, 449 insertions(+), 135 deletions(-) create mode 100644 assets/js/components/Config/defaultYaml/tariffTemperature.yaml rename assets/js/components/Forecast/{Co2Chart.vue => ValueChart.vue} (69%) rename assets/js/components/Forecast/{Co2Details.vue => ValueDetails.vue} (54%) create mode 100644 templates/definition/tariff/demo-temperature-forecast.yaml create mode 100644 templates/definition/tariff/open-meteo-temperature.yaml diff --git a/api/globalconfig/types.go b/api/globalconfig/types.go index 2d4fd7bf5..631b4e4e2 100644 --- a/api/globalconfig/types.go +++ b/api/globalconfig/types.go @@ -166,33 +166,35 @@ func (c Messaging) IsConfigured() bool { } type Tariffs struct { - Currency string - Grid config.Typed - FeedIn config.Typed - Co2 config.Typed - Planner config.Typed - Solar []config.Typed + Currency string + Grid config.Typed + FeedIn config.Typed + Co2 config.Typed + Planner config.Typed + Solar []config.Typed + Temperature config.Typed } func (c Tariffs) IsConfigured() bool { - return c.Currency != "" || c.Grid.Type != "" || c.FeedIn.Type != "" || c.Co2.Type != "" || c.Planner.Type != "" || len(c.Solar) > 0 + return c.Currency != "" || c.Grid.Type != "" || c.FeedIn.Type != "" || c.Co2.Type != "" || c.Planner.Type != "" || len(c.Solar) > 0 || c.Temperature.Type != "" } type TariffRefs struct { - Grid string `json:"grid"` - FeedIn string `json:"feedIn"` - Co2 string `json:"co2"` - Planner string `json:"planner"` - Solar []string `json:"solar"` + Grid string `json:"grid"` + FeedIn string `json:"feedIn"` + Co2 string `json:"co2"` + Planner string `json:"planner"` + Solar []string `json:"solar"` + Temperature string `json:"temperature"` } func (refs TariffRefs) IsConfigured() bool { - return refs.Grid != "" || refs.FeedIn != "" || refs.Co2 != "" || refs.Planner != "" || len(refs.Solar) > 0 + return refs.Grid != "" || refs.FeedIn != "" || refs.Co2 != "" || refs.Planner != "" || len(refs.Solar) > 0 || refs.Temperature != "" } func (refs TariffRefs) Used() iter.Seq[string] { return func(yield func(string) bool) { - for _, ref := range append([]string{refs.Grid, refs.FeedIn, refs.Co2, refs.Planner}, refs.Solar...) { + for _, ref := range append([]string{refs.Grid, refs.FeedIn, refs.Co2, refs.Planner, refs.Temperature}, refs.Solar...) { if ref != "" { if !yield(ref) { return diff --git a/api/tariff.go b/api/tariff.go index ca6063b68..641e53b38 100644 --- a/api/tariff.go +++ b/api/tariff.go @@ -12,6 +12,7 @@ const ( TariffTypePriceForecast TariffTypeCo2 TariffTypeSolar + TariffTypeTemperature // outdoor temperature forecast in °C ) type TariffUsage int @@ -23,4 +24,5 @@ const ( TariffUsageGrid TariffUsagePlanner TariffUsageSolar + TariffUsageTemperature // outdoor temperature forecast ) diff --git a/api/tarifftype_enumer.go b/api/tarifftype_enumer.go index 12272d9bc..093d309de 100644 --- a/api/tarifftype_enumer.go +++ b/api/tarifftype_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2solar" +const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2solartemperature" -var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39, 44} +var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39, 44, 55} -const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2solar" +const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2solartemperature" func (i TariffType) String() string { i -= 1 @@ -30,9 +30,10 @@ func _TariffTypeNoOp() { _ = x[TariffTypePriceForecast-(3)] _ = x[TariffTypeCo2-(4)] _ = x[TariffTypeSolar-(5)] + _ = x[TariffTypeTemperature-(6)] } -var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2, TariffTypeSolar} +var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2, TariffTypeSolar, TariffTypeTemperature} var _TariffTypeNameToValueMap = map[string]TariffType{ _TariffTypeName[0:11]: TariffTypePriceStatic, @@ -45,6 +46,8 @@ var _TariffTypeNameToValueMap = map[string]TariffType{ _TariffTypeLowerName[36:39]: TariffTypeCo2, _TariffTypeName[39:44]: TariffTypeSolar, _TariffTypeLowerName[39:44]: TariffTypeSolar, + _TariffTypeName[44:55]: TariffTypeTemperature, + _TariffTypeLowerName[44:55]: TariffTypeTemperature, } var _TariffTypeNames = []string{ @@ -53,6 +56,7 @@ var _TariffTypeNames = []string{ _TariffTypeName[23:36], _TariffTypeName[36:39], _TariffTypeName[39:44], + _TariffTypeName[44:55], } // TariffTypeString retrieves an enum value from the enum constants string name. diff --git a/api/tariffusage_enumer.go b/api/tariffusage_enumer.go index 2eadfda90..f82a9c3e3 100644 --- a/api/tariffusage_enumer.go +++ b/api/tariffusage_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _TariffUsageName = "co2feedingridplannersolar" +const _TariffUsageName = "co2feedingridplannersolartemperature" -var _TariffUsageIndex = [...]uint8{0, 3, 9, 13, 20, 25} +var _TariffUsageIndex = [...]uint8{0, 3, 9, 13, 20, 25, 36} -const _TariffUsageLowerName = "co2feedingridplannersolar" +const _TariffUsageLowerName = "co2feedingridplannersolartemperature" func (i TariffUsage) String() string { i -= 1 @@ -30,9 +30,10 @@ func _TariffUsageNoOp() { _ = x[TariffUsageGrid-(3)] _ = x[TariffUsagePlanner-(4)] _ = x[TariffUsageSolar-(5)] + _ = x[TariffUsageTemperature-(6)] } -var _TariffUsageValues = []TariffUsage{TariffUsageCo2, TariffUsageFeedIn, TariffUsageGrid, TariffUsagePlanner, TariffUsageSolar} +var _TariffUsageValues = []TariffUsage{TariffUsageCo2, TariffUsageFeedIn, TariffUsageGrid, TariffUsagePlanner, TariffUsageSolar, TariffUsageTemperature} var _TariffUsageNameToValueMap = map[string]TariffUsage{ _TariffUsageName[0:3]: TariffUsageCo2, @@ -45,6 +46,8 @@ var _TariffUsageNameToValueMap = map[string]TariffUsage{ _TariffUsageLowerName[13:20]: TariffUsagePlanner, _TariffUsageName[20:25]: TariffUsageSolar, _TariffUsageLowerName[20:25]: TariffUsageSolar, + _TariffUsageName[25:36]: TariffUsageTemperature, + _TariffUsageLowerName[25:36]: TariffUsageTemperature, } var _TariffUsageNames = []string{ @@ -53,6 +56,7 @@ var _TariffUsageNames = []string{ _TariffUsageName[9:13], _TariffUsageName[13:20], _TariffUsageName[20:25], + _TariffUsageName[25:36], } // TariffUsageString retrieves an enum value from the enum constants string name. diff --git a/assets/css/app.css b/assets/css/app.css index 82ed3fb92..bcedf8a4e 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -42,6 +42,7 @@ --evcc-export-contrast: var(--evcc-dark-yellow); --evcc-price: #ff912fff; --evcc-co2: #00916eff; + --evcc-temperature: #1b6fba; --evcc-background: var(--bs-gray-bright); --evcc-box: var(--bs-white); --evcc-box-border: var(--bs-gray-brighter); @@ -198,6 +199,10 @@ h5 { color: var(--evcc-co2) !important; } +.text-temperature { + color: var(--evcc-temperature) !important; +} + .text-export { color: var(--evcc-export-contrast) !important; } diff --git a/assets/js/colors.ts b/assets/js/colors.ts index 53a52b8a4..d9425d322 100644 --- a/assets/js/colors.ts +++ b/assets/js/colors.ts @@ -24,6 +24,7 @@ const colors: { pricePerKWh: string | null; price: string | null; co2: string | null; + temperature: string | null; export: string | null; background: string | null; light: string | null; @@ -40,6 +41,7 @@ const colors: { pricePerKWh: null, price: null, co2: null, + temperature: null, export: null, background: null, light: null, @@ -147,6 +149,7 @@ export function updateCssColors() { colors.grid = style.getPropertyValue("--evcc-grid"); colors.price = style.getPropertyValue("--evcc-price"); colors.co2 = style.getPropertyValue("--evcc-co2"); + colors.temperature = style.getPropertyValue("--evcc-temperature"); colors.export = style.getPropertyValue("--evcc-export-contrast"); colors.background = style.getPropertyValue("--evcc-background"); colors.pricePerKWh = style.getPropertyValue("--bs-gray-medium"); diff --git a/assets/js/components/Config/DeviceTags.vue b/assets/js/components/Config/DeviceTags.vue index 779f9f3a2..e8973baf3 100644 --- a/assets/js/components/Config/DeviceTags.vue +++ b/assets/js/components/Config/DeviceTags.vue @@ -76,7 +76,7 @@ const HIDDEN_TAGS = ["icon", "heating", "integratedDevice"]; const PHASE_TAGS = ["phaseCurrents", "phaseVoltages", "phasePowers"]; -const FORECAST_TAGS = ["priceRates", "co2Rates", "solarRates"]; +const FORECAST_TAGS = ["priceRates", "co2Rates", "solarRates", "temperatureRates"]; export default { name: "DeviceTags", @@ -122,6 +122,7 @@ export default { priceRates: "price", co2Rates: "co2", solarRates: "solar", + temperatureRates: "temperature", }; // Find which forecast tag is present @@ -220,6 +221,7 @@ export default { return this.fmtPercentage(value, 1); case "temp": case "heaterTempLimit": + case "outdoorTemp": return this.fmtTemperature(value); case "odometer": case "range": @@ -290,6 +292,8 @@ export default { return short ? this.fmtCo2Short(value) : this.fmtCo2Medium(value); case "solar": return this.fmtW(value); + case "temperature": + return this.fmtTemperature(value); default: return value; } diff --git a/assets/js/components/Config/TariffCard.vue b/assets/js/components/Config/TariffCard.vue index 21bdbbee2..f43f1735b 100644 --- a/assets/js/components/Config/TariffCard.vue +++ b/assets/js/components/Config/TariffCard.vue @@ -23,6 +23,7 @@ import "@h2d2/shopicons/es/regular/receivepayment"; import "@h2d2/shopicons/es/regular/eco1"; import "@h2d2/shopicons/es/regular/clock"; import "@h2d2/shopicons/es/regular/sun"; +import "@h2d2/shopicons/es/regular/thermometerhalf"; import { type PropType } from "vue"; import type { TariffType, CURRENCY } from "@/types/evcc"; import DeviceCard from "./DeviceCard.vue"; @@ -49,7 +50,7 @@ export default { hasError: { type: Boolean, default: false }, title: String, tags: { type: Object, default: () => ({}) }, - currency: { type: String as PropType, required: true }, + currency: { type: String as PropType }, }, emits: ["edit"], computed: { @@ -69,6 +70,7 @@ export default { co2: "shopicon-regular-eco1", planner: "shopicon-regular-clock", solar: "shopicon-regular-sun", + temperature: "shopicon-regular-thermometerhalf", }; return iconMap[this.tariffType]; }, diff --git a/assets/js/components/Config/TariffModal.vue b/assets/js/components/Config/TariffModal.vue index d6cc13a3c..d96c19e30 100644 --- a/assets/js/components/Config/TariffModal.vue +++ b/assets/js/components/Config/TariffModal.vue @@ -65,6 +65,7 @@ import { getModal } from "@/configModal"; import tariffPriceYaml from "./defaultYaml/tariffPrice.yaml?raw"; import tariffCo2Yaml from "./defaultYaml/tariffCo2.yaml?raw"; import tariffSolarYaml from "./defaultYaml/tariffSolar.yaml?raw"; +import tariffTemperatureYaml from "./defaultYaml/tariffTemperature.yaml?raw"; const initialValues = { type: ConfigType.Template, @@ -129,7 +130,9 @@ export default defineComponent({ methods: { provideTemplateOptions(products: Product[]): TemplateGroup[] { // Use different custom option text for tariffs vs forecasts - const isForecast = ["co2", "planner", "solar"].includes(this.tariffType || ""); + const isForecast = ["co2", "planner", "solar", "temperature"].includes( + this.tariffType || "" + ); const customLabel = isForecast ? this.$t("config.tariff.customForecast") : this.$t("config.tariff.customTariff"); @@ -141,6 +144,7 @@ export default defineComponent({ "demo-co2-forecast", "demo-dynamic-grid", "demo-solar-forecast", + "demo-temperature-forecast", "energy-charts-api", ]; @@ -162,9 +166,11 @@ export default defineComponent({ const priceProducts = filterByGroup("price"); const co2Products = filterByGroup("co2"); const solarProducts = filterByGroup("solar"); + const temperatureProducts = filterByGroup("temperature"); const priceGeneric = extractGeneric("price"); const co2Generic = extractGeneric("co2"); const solarGeneric = extractGeneric("solar"); + const temperatureGeneric = extractGeneric("temperature"); // Special handling for planner: show price + co2 services if (this.tariffType === "planner") { @@ -194,6 +200,7 @@ export default defineComponent({ feedIn: { service: priceProducts, generic: priceGeneric }, co2: { service: co2Products, generic: co2Generic }, solar: { service: solarProducts, generic: solarGeneric }, + temperature: { service: temperatureProducts, generic: temperatureGeneric }, }; const mapped = (this.tariffType && groupMap[this.tariffType]) || { service: [], @@ -225,6 +232,8 @@ export default defineComponent({ values.yaml = tariffCo2Yaml; } else if (this.tariffType === "solar") { values.yaml = tariffSolarYaml; + } else if (this.tariffType === "temperature") { + values.yaml = tariffTemperatureYaml; } } }, diff --git a/assets/js/components/Config/defaultYaml/tariffTemperature.yaml b/assets/js/components/Config/defaultYaml/tariffTemperature.yaml new file mode 100644 index 000000000..bec2331d8 --- /dev/null +++ b/assets/js/components/Config/defaultYaml/tariffTemperature.yaml @@ -0,0 +1,28 @@ +## Outdoor temperature forecast +tariff: temperature +forecast: # hourly outdoor temperature forecast (°C) + source: js + script: | + var rates = []; + var now = new Date(); + for (var i = 0; i < 48; i++) { + var start = new Date(now.getTime() + i * 3600000); + rates.push({ + start: start.toISOString(), + end: new Date(start.getTime() + 3600000).toISOString(), + value: 15 // °C + }); + } + JSON.stringify(rates); + +## HTTP example (uncomment to use) + +#forecast: # hourly outdoor temperature forecast (°C) +# source: http +# uri: https://example.com/api/temperature +# jq: | +# map({ +# "start": .start, +# "end": .end, +# "value": .temperature +# }) | tostring diff --git a/assets/js/components/Forecast/Co2Chart.vue b/assets/js/components/Forecast/ValueChart.vue similarity index 69% rename from assets/js/components/Forecast/Co2Chart.vue rename to assets/js/components/Forecast/ValueChart.vue index 2806ce2c0..cc076933b 100644 --- a/assets/js/components/Forecast/Co2Chart.vue +++ b/assets/js/components/Forecast/ValueChart.vue @@ -24,15 +24,26 @@ import formatter from "@/mixins/formatter"; import chartMixin from "./chartMixin"; import type { ForecastSlot } from "./types"; +export type ValueChartType = "co2" | "temperature"; + export default defineComponent({ - name: "Co2Chart", + name: "ValueChart", mixins: [formatter, chartMixin], props: { - co2: { type: Array as PropType, required: true }, + type: { type: String as PropType, required: true }, + rates: { type: Array as PropType, required: true }, }, computed: { + color(): string { + return (this.type === "co2" ? colors.co2 : colors.temperature) || ""; + }, slots(): ForecastSlot[] { - return filterForecastSlots(this.co2, this.startDate, this.endDate); + return filterForecastSlots(this.rates, this.startDate, this.endDate); + }, + yMin(): number { + // co2 is never negative, temperature may be + if (this.type === "co2" || !this.slots.length) return 0; + return Math.min(0, Math.floor(Math.min(...this.slots.map((s) => s.value)))); }, markPoints(): { coord: [string, number]; @@ -51,20 +62,20 @@ export default defineComponent({ if (slots[minIdx]) { points.push({ coord: [clampStart(slots[minIdx]!.start, this.startDate), slots[minIdx]!.value], - value: this.fmtGrams(slots[minIdx]!.value), + value: this.fmtShort(slots[minIdx]!.value), label: { position: "bottom", offset: [0, 2] }, }); } if (maxIdx !== minIdx && slots[maxIdx]) { points.push({ coord: [clampStart(slots[maxIdx]!.start, this.startDate), slots[maxIdx]!.value], - value: this.fmtGrams(slots[maxIdx]!.value), + value: this.fmtShort(slots[maxIdx]!.value), }); } return points; }, chartOption(): Record { - const co2Color = colors.co2 || ""; + const color = this.color; // eslint-disable-next-line @typescript-eslint/no-this-alias const vThis = this; @@ -75,13 +86,13 @@ export default defineComponent({ tooltip: { trigger: "axis", axisPointer: { type: "line", snap: true, lineStyle: { color: "transparent" } }, - ...tooltipStyle(co2Color, () => this.chart), + ...tooltipStyle(color, () => this.chart), formatter(params: { value: [string, number] }[]) { const p = params[0]; if (!p) return ""; const d = new Date(p.value[0]); const time = `${vThis.weekdayShort(d)} ${vThis.fmtHourMinute(d)}`; - return tooltipTable(time, [{ values: [vThis.fmtCo2Medium(p.value[1])] }]); + return tooltipTable(time, [{ values: [vThis.fmtLong(p.value[1])] }]); }, }, xAxis: forecastXAxes( @@ -91,6 +102,7 @@ export default defineComponent({ this.weekdayShort ), yAxis: forecastYAxis({ + min: this.yMin, splitNumber: 2, axisLabel: { color: colors.muted, @@ -105,14 +117,14 @@ export default defineComponent({ symbol: "circle", symbolSize: 6, showSymbol: false, - lineStyle: { color: co2Color, width: 3 }, + lineStyle: { color, width: 3 }, emphasis: { disabled: false, scale: false, - itemStyle: { color: co2Color, borderColor: co2Color, borderWidth: 2 }, + itemStyle: { color, borderColor: color, borderWidth: 2 }, }, markPoint: markPointLabel( - co2Color, + color, this.tooltipVisible ? [] : this.markPoints, this.startDate, this.endDate @@ -122,5 +134,13 @@ export default defineComponent({ }; }, }, + methods: { + fmtShort(value: number): string { + return this.type === "co2" ? this.fmtGrams(value) : this.fmtTemperature(value); + }, + fmtLong(value: number): string { + return this.type === "co2" ? this.fmtCo2Medium(value) : this.fmtTemperature(value); + }, + }, }); diff --git a/assets/js/components/Forecast/Co2Details.vue b/assets/js/components/Forecast/ValueDetails.vue similarity index 54% rename from assets/js/components/Forecast/Co2Details.vue rename to assets/js/components/Forecast/ValueDetails.vue index 3358f0b5d..ef3d95fa7 100644 --- a/assets/js/components/Forecast/Co2Details.vue +++ b/assets/js/components/Forecast/ValueDetails.vue @@ -2,16 +2,16 @@
- {{ $t("forecast.co2.range") }} + {{ $t(`forecast.${type}.range`) }}
- {{ range }} + {{ range }}
- {{ $t("forecast.co2.average") }} + {{ $t(`forecast.${type}.average`) }}
- {{ average }} + {{ average }}
@@ -21,21 +21,23 @@ import { defineComponent, type PropType } from "vue"; import formatter from "@/mixins/formatter"; import type { ForecastSlot } from "./types"; +import type { ValueChartType } from "./ValueChart.vue"; const MAX_HOURS = 96; const SLOTS_PER_HOUR = 4; export default defineComponent({ - name: "Co2Details", + name: "ValueDetails", mixins: [formatter], props: { - co2: { type: Array as PropType }, + type: { type: String as PropType, required: true }, + rates: { type: Array as PropType }, }, computed: { upcomingSlots(): ForecastSlot[] { - if (!Array.isArray(this.co2)) return []; + if (!Array.isArray(this.rates)) return []; const now = new Date(); - return this.co2 + return this.rates .filter((slot) => new Date(slot.end) > now) .slice(0, MAX_HOURS * SLOTS_PER_HOUR); }, @@ -43,14 +45,22 @@ export default defineComponent({ if (this.upcomingSlots.length === 0) return ""; const avg = this.upcomingSlots.reduce((a, s) => a + s.value, 0) / this.upcomingSlots.length; - return this.fmtCo2Medium(avg); + return this.fmtValue(avg); }, range(): string { if (this.upcomingSlots.length === 0) return ""; const values = this.upcomingSlots.map((s) => s.value); const min = Math.min(...values); const max = Math.max(...values); - return `${this.fmtNumber(min, 0)} – ${this.fmtCo2Medium(max)}`; + if (this.type === "co2") { + return `${this.fmtNumber(min, 0)} – ${this.fmtCo2Medium(max)}`; + } + return `${this.fmtValue(min)} – ${this.fmtValue(max)}`; + }, + }, + methods: { + fmtValue(value: number): string { + return this.type === "co2" ? this.fmtCo2Medium(value) : this.fmtTemperature(value); }, }, }); diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts index 7c65d7aef..63be2ff29 100644 --- a/assets/js/types/evcc.ts +++ b/assets/js/types/evcc.ts @@ -100,6 +100,7 @@ export interface State { tariffFeedIn?: number; tariffCo2?: number; tariffSolar?: number; + tariffTemperature?: number; mqtt?: MqttConfig; influx?: InfluxConfig; hems?: ConfigStatus; @@ -744,6 +745,7 @@ export interface Forecast { solar?: SolarDetails; planner?: ForecastSlot[]; feedin?: ForecastSlot[]; + temperature?: ForecastSlot[]; } export interface SelectOption { @@ -763,7 +765,7 @@ export type DeviceType = | "hems"; export type MeterType = "grid" | "pv" | "battery" | "charge" | "aux" | "ext" | "consumer"; export type MeterTemplateUsage = "grid" | "pv" | "battery" | "charge" | "aux"; -export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar"; +export type TariffType = "grid" | "feedIn" | "co2" | "planner" | "solar" | "temperature"; // see https://stackoverflow.com/a/54178819 type Omit = Pick>; diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue index 5ce87f1e8..20d6fbbf8 100644 --- a/assets/js/views/Config.vue +++ b/assets/js/views/Config.vue @@ -222,7 +222,6 @@ tariff-type="co2" :has-error="hasDeviceError('tariff', co2Tariff.name)" :tags="deviceTags('tariff', co2Tariff.name)" - :currency="currency" @edit="openModal('tariff', { type: 'co2', id: co2Tariff.id })" /> + t.name === name) : null; }, + temperatureTariff() { + const name = this.tariffRefs?.temperature; + return name ? this.tariffs.find((t) => t.name === name) : null; + }, solarTariffs() { const names = this.tariffRefs?.solar || []; return names.map((name) => this.tariffs.find((t) => t.name === name)).filter(Boolean); @@ -769,16 +783,19 @@ export default defineComponent({ const types: TariffType[] = []; if (!this.co2Tariff) types.push("co2"); types.push("solar"); // Solar can have multiple + if (!this.temperatureTariff) types.push("temperature"); if (!this.plannerTariff) types.push("planner"); return types; }, tariffTags(): DeviceTags { - const { tariffGrid, tariffFeedIn, tariffCo2, tariffSolar } = store.state; + const { tariffGrid, tariffFeedIn, tariffCo2, tariffSolar, tariffTemperature } = + store.state; if ( tariffGrid === undefined && tariffFeedIn === undefined && tariffCo2 === undefined && - tariffSolar === undefined + tariffSolar === undefined && + tariffTemperature === undefined ) { return { configured: { value: false } }; } @@ -787,6 +804,7 @@ export default defineComponent({ feedinPrice: {}, co2: {}, solarForecast: {}, + outdoorTemp: {}, }; if (tariffGrid) { tags.gridPrice = { value: tariffGrid }; @@ -800,6 +818,9 @@ export default defineComponent({ if (tariffSolar) { tags.solarForecast = { value: tariffSolar }; } + if (tariffTemperature !== undefined) { + tags.outdoorTemp = { value: tariffTemperature }; + } return tags; }, mqttTags(): DeviceTags { diff --git a/assets/js/views/Forecast.vue b/assets/js/views/Forecast.vue index fbfe6bea0..5527b5780 100644 --- a/assets/js/views/Forecast.vue +++ b/assets/js/views/Forecast.vue @@ -7,31 +7,15 @@
    -
  • - +
  • +
    - {{ $t("forecast.type.solar") }} -
    {{ $t("forecast.empty.solar") }}
    -
    -
  • -
  • - -
    - {{ $t("forecast.type.price") }} -
    {{ $t("forecast.empty.price") }}
    -
    -
  • -
  • - -
    - {{ $t("forecast.type.co2") }} -
    {{ $t("forecast.empty.co2") }}
    + {{ $t(`forecast.type.${type}`) }} +
    {{ $t(`forecast.empty.${type}`) }}
@@ -121,21 +105,23 @@
-
- +
@@ -145,7 +131,8 @@