223 lines
6.3 KiB
Vue
223 lines
6.3 KiB
Vue
<template>
|
|
<div class="container px-4 safe-area-inset">
|
|
<TopHeader title="Optimize Debug 🧪" />
|
|
<OptimizeHeader
|
|
class="mt-4 mb-5"
|
|
:updated="evopt?.updated"
|
|
:status="evopt?.res?.status"
|
|
:net-cost="netCost"
|
|
:horizon-hours="horizonHours"
|
|
:currency="currency"
|
|
:charging-strategies="chargingStrategies"
|
|
:selected-strategy="optimizerChargingStrategy"
|
|
:pending="pending"
|
|
@optimize="optimizeNow"
|
|
@change-strategy="changeChargingStrategy"
|
|
/>
|
|
<div class="row">
|
|
<main class="col-12">
|
|
<div v-if="evopt">
|
|
<!-- Optimizer Plan -->
|
|
<section class="mb-5">
|
|
<h3 class="fw-normal mb-4">Result: Charging Plan</h3>
|
|
<ChargeChart
|
|
:evopt="evopt"
|
|
:battery-details="evopt.details.batteryDetails"
|
|
:timestamp="evopt.details.timestamp[0]"
|
|
:currency="currency"
|
|
:battery-colors="batteryColors"
|
|
:device-colors="deviceColors"
|
|
/>
|
|
|
|
<h3 class="fw-normal mb-4">Result: SoC Projection</h3>
|
|
<SocChart
|
|
:evopt="evopt"
|
|
:battery-details="evopt.details.batteryDetails"
|
|
:timestamp="evopt.details.timestamp[0]"
|
|
:currency="currency"
|
|
:battery-colors="batteryColors"
|
|
/>
|
|
</section>
|
|
|
|
<!-- Input Parameters -->
|
|
<section class="mb-5">
|
|
<h3 class="fw-normal mb-4">Input: Grid Prices</h3>
|
|
<PriceChart
|
|
:evopt="evopt"
|
|
:timestamp="evopt.details.timestamp[0]"
|
|
:currency="currency"
|
|
/>
|
|
|
|
<h3
|
|
class="fw-normal d-flex gap-3 flex-wrap d-flex align-items-baseline overflow-hidden mb-4"
|
|
>
|
|
<span class="d-block no-wrap text-truncate"> Input: Battery </span>
|
|
<small class="d-block no-wrap text-truncate">
|
|
{{ fmtPercentage((evopt.req.eta_c || 1) * 100, 1) }} charge
|
|
efficiency ・
|
|
{{ fmtPercentage((evopt.req.eta_d || 1) * 100, 1) }} discharge
|
|
efficiency
|
|
</small>
|
|
</h3>
|
|
|
|
<BatteryConfigurationTable
|
|
:batteries="evopt.req.batteries"
|
|
:battery-details="evopt.details.batteryDetails"
|
|
:currency="currency"
|
|
/>
|
|
</section>
|
|
|
|
<hr class="my-5" />
|
|
|
|
<!-- Debugging -->
|
|
<section class="mb-5">
|
|
<h3 class="fw-normal mb-4">Time Series</h3>
|
|
|
|
<TimeSeriesDataTable
|
|
:evopt="evopt"
|
|
:battery-details="evopt.details.batteryDetails"
|
|
:timestamps="evopt.details.timestamp"
|
|
:currency="currency"
|
|
:battery-colors="batteryColors"
|
|
:dimmed-battery-colors="dimmedBatteryColors"
|
|
/>
|
|
|
|
<h3 class="fw-normal mb-4">Raw Data</h3>
|
|
|
|
<div class="mb-4">
|
|
<p class="mb-2">Request:</p>
|
|
<div class="position-relative">
|
|
<pre
|
|
class="p-3 rounded border overflow-auto"
|
|
style="background-color: var(--evcc-box)"
|
|
>{{ formattedRequest }}</pre
|
|
>
|
|
<CopyButton :content="formattedRequest" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<p class="mb-2">Response:</p>
|
|
<div class="position-relative">
|
|
<pre
|
|
class="p-3 rounded border overflow-auto"
|
|
style="background-color: var(--evcc-box)"
|
|
>{{ formattedResponse }}</pre
|
|
>
|
|
<CopyButton :content="formattedResponse" />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
<div v-else>
|
|
<p>nothing to see here</p>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { defineComponent } from "vue";
|
|
import Header from "../components/Top/Header.vue";
|
|
import OptimizeHeader from "../components/Optimize/OptimizeHeader.vue";
|
|
import BatteryConfigurationTable from "../components/Optimize/BatteryConfigurationTable.vue";
|
|
import SocChart from "../components/Optimize/SocChart.vue";
|
|
import ChargeChart from "../components/Optimize/ChargeChart.vue";
|
|
import PriceChart from "../components/Optimize/PriceChart.vue";
|
|
import TimeSeriesDataTable from "../components/Optimize/TimeSeriesDataTable.vue";
|
|
import CopyButton from "../components/Optimize/CopyButton.vue";
|
|
import { formatCompactJson } from "../components/Optimize/compactJson";
|
|
import api from "../api";
|
|
import store from "../store";
|
|
import formatter from "../mixins/formatter";
|
|
import { resolveColors, deviceColorMap } from "../colors";
|
|
import { CURRENCY } from "../types/evcc";
|
|
|
|
export default defineComponent({
|
|
name: "Optimize",
|
|
components: {
|
|
TopHeader: Header,
|
|
OptimizeHeader,
|
|
BatteryConfigurationTable,
|
|
SocChart,
|
|
ChargeChart,
|
|
PriceChart,
|
|
TimeSeriesDataTable,
|
|
CopyButton,
|
|
},
|
|
mixins: [formatter],
|
|
data() {
|
|
return {
|
|
pending: false,
|
|
};
|
|
},
|
|
head() {
|
|
return { title: "Optimize Debug" };
|
|
},
|
|
computed: {
|
|
evopt() {
|
|
return store.state.evopt;
|
|
},
|
|
currency() {
|
|
return store.state.currency || CURRENCY.EUR;
|
|
},
|
|
chargingStrategies(): string[] {
|
|
return store.state.optimizerChargingStrategies || [];
|
|
},
|
|
optimizerChargingStrategy(): string {
|
|
return store.state.optimizerChargingStrategy || "";
|
|
},
|
|
netCost(): number {
|
|
return (this.evopt?.res?.objective_value || 0) * -1;
|
|
},
|
|
horizonHours(): number {
|
|
const dt = this.evopt?.req?.time_series?.dt;
|
|
if (!dt?.length) return 0;
|
|
return Math.round(dt.reduce((sum, s) => sum + s, 0) / 3600);
|
|
},
|
|
deviceColors() {
|
|
return deviceColorMap(store.state.deviceColors);
|
|
},
|
|
batteryTitles(): string[] {
|
|
const details = this.evopt?.details?.batteryDetails || [];
|
|
return (this.evopt?.res.batteries || []).map(
|
|
(_, i) => details[i]?.title || details[i]?.name || `battery-${i}`
|
|
);
|
|
},
|
|
batteryColors() {
|
|
if (!this.evopt?.res.batteries) return [];
|
|
const palette = resolveColors(this.batteryTitles, this.deviceColors);
|
|
return this.batteryTitles.map((t) => palette[t] || "");
|
|
},
|
|
dimmedBatteryColors() {
|
|
return (this.batteryColors || []).map((color) => this.dimColorBy25Percent(color));
|
|
},
|
|
formattedRequest() {
|
|
return this.evopt?.req ? formatCompactJson(this.evopt.req) : "";
|
|
},
|
|
formattedResponse() {
|
|
return this.evopt?.res ? formatCompactJson(this.evopt.res) : "";
|
|
},
|
|
},
|
|
watch: {
|
|
"evopt.updated"() {
|
|
// re-enable the refresh action once a fresh optimizer run lands
|
|
this.pending = false;
|
|
},
|
|
},
|
|
methods: {
|
|
optimizeNow() {
|
|
this.pending = true;
|
|
api.post("optimize");
|
|
},
|
|
changeChargingStrategy(value: string) {
|
|
api.post(`optimizerchargingstrategy/${value}`);
|
|
},
|
|
dimColorBy25Percent(color: string): string {
|
|
// Convert color to 25% opacity (40 in hex = 25% of 255)
|
|
return color?.toLowerCase().replace(/ff$/, "40") || color;
|
|
},
|
|
},
|
|
});
|
|
</script>
|