evcc-io/assets/js/components/Battery/BatteryExperimental.vue

183 lines
5.1 KiB
Vue

<template>
<div v-if="batteryAvailable" data-testid="battery-experimental">
<BatteryStatusCards
class="mb-4 box-pull-out"
:battery="state.battery"
:battery-mode="state.batteryMode"
/>
<BatteryHistoryCard
class="mb-4 box-pull-out"
:batteries="chartBatteries"
:now="now"
:kwh-available="kWhAvailable"
@range-start="onRangeStart"
/>
<BatteryConfigCard
class="mb-4 box-pull-out"
:buffer-soc="state.bufferSoc"
:priority-soc="state.prioritySoc"
:buffer-start-soc="state.bufferStartSoc"
:battery-discharge-control="state.batteryDischargeControl"
:battery-grid-discharge="state.batteryGridDischarge"
:battery="state.battery"
:experimental="state.experimental"
/>
<Card
v-if="gridChargeVisible"
class="box-pull-out"
:title="$t('batterySettings.gridChargeTab')"
>
<SmartCostLimit v-bind="smartCostLimitProps" />
</Card>
</div>
<p v-else class="my-4 text-muted">{{ $t("batterySettings.noBattery") }}</p>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import store from "@/store";
import settings from "@/settings";
import api from "@/api";
import { SMART_COST_TYPE, CURRENCY, type BatteryMeter } from "@/types/evcc";
import Card from "../Helper/Card.vue";
import SmartCostLimit from "../Tariff/SmartCostLimit.vue";
import BatteryStatusCards from "./BatteryStatusCards.vue";
import BatteryConfigCard from "./BatteryConfigCard.vue";
import BatteryHistoryCard from "./BatteryHistoryCard.vue";
import { historyToSeries, forecastToSeries, buildChartBatteries } from "./history";
import type { BatteryHistorySeries, BatterySeries } from "./types";
const CHUNK_MS = 48 * 3600 * 1000; // 48h load/grow step
export default defineComponent({
name: "BatteryExperimental",
components: {
Card,
SmartCostLimit,
BatteryStatusCards,
BatteryConfigCard,
BatteryHistoryCard,
},
data() {
const now = new Date();
return {
now,
rangeStart: new Date(now.getTime() - CHUNK_MS), // earliest time the chart currently shows
loadedFrom: null as Date | null, // earliest time we have history for
loadedTo: null as Date | null, // latest time we have history for
loading: false,
rawSeries: [] as BatteryHistorySeries[],
};
},
computed: {
state() {
return store.state;
},
historyUpdated(): string | undefined {
return store.state.historyUpdated;
},
devices(): BatteryMeter[] {
return this.state.battery?.devices ?? [];
},
batteryAvailable(): boolean {
return this.devices.length > 0;
},
evopt() {
return this.state.evopt;
},
kWhAvailable(): boolean {
return this.batteryAvailable && this.devices.every((d) => d.capacity > 0);
},
// earliest history we want loaded: a chunk before the visible start (kept as a buffer),
// never later than what we already loaded so the window only grows
requestedFrom(): Date {
const wanted = new Date(this.rangeStart.getTime() - CHUNK_MS);
return this.loadedFrom && this.loadedFrom < wanted ? this.loadedFrom : wanted;
},
chartBatteries(): BatterySeries[] {
return buildChartBatteries(
this.devices,
historyToSeries(this.rawSeries),
forecastToSeries(this.evopt, this.now.getTime())
);
},
gridChargePossible(): boolean {
return (
this.devices.some(({ controllable }) => controllable) &&
!!this.state.smartCostAvailable
);
},
gridChargeLimit(): number | null {
return this.state.batteryGridChargeLimit ?? null;
},
gridChargeVisible(): boolean {
return this.gridChargePossible || this.gridChargeLimit !== null;
},
gridChargeTariff() {
const { co2, grid } = store.uiForecast.value;
return this.state.smartCostType === SMART_COST_TYPE.CO2 ? co2 : grid;
},
smartCostLimitProps() {
return {
currentLimit: this.gridChargeLimit,
lastLimit: settings.lastBatterySmartCostLimit,
smartCostType: this.state.smartCostType,
currency: this.state.currency || CURRENCY.EUR,
tariff: this.gridChargeTariff,
possible: this.gridChargePossible,
};
},
},
watch: {
// fetchHistory reloads only when the needed range is not already covered
requestedFrom: {
handler() {
this.fetchHistory();
},
immediate: true,
},
now() {
this.fetchHistory();
},
// bumped on each metrics persist; advance clock to reload recent history
historyUpdated() {
this.now = new Date();
},
},
methods: {
onRangeStart(start: Date) {
this.rangeStart = start;
},
async fetchHistory() {
if (this.loading || !this.batteryAvailable) return;
const from = this.requestedFrom;
const to = this.now;
const backLoaded = this.loadedFrom !== null && this.loadedFrom <= from;
const frontLoaded = this.loadedTo !== null && this.loadedTo >= to;
if (backLoaded && frontLoaded) return;
this.loading = true;
try {
const res = await api.get("history/energy", {
params: {
from: from.toISOString(),
to: to.toISOString(),
aggregate: "15m",
grouped: false,
group: "battery",
},
});
this.rawSeries = res.data || [];
this.loadedFrom = from;
this.loadedTo = to;
} catch (e) {
console.error("failed to load battery history", e);
} finally {
this.loading = false;
}
},
},
});
</script>