Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 89 additions & 98 deletions assets/js/components/Battery/BatteryGridChargeOnce.vue
Original file line number Diff line number Diff line change
@@ -1,90 +1,90 @@
<template>
<div class="once border-top pt-3 mt-3" data-testid="battery-grid-charge-once">
<div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<h6 class="mb-0">{{ $t("battery.gridChargeOnce.title") }}</h6>
<!-- power button: outlined to start, filled while running, tap again stops -->
<button
v-if="running"
type="button"
class="power btn btn-primary"
data-testid="battery-grid-charge-once-cancel"
:aria-label="$t('battery.gridChargeOnce.cancel')"
:title="$t('battery.gridChargeOnce.cancel')"
<h6 class="mb-2">{{ $t("battery.gridChargeOnce.title") }}</h6>

<!-- standard switch in front of the sentence: on starts, off stops -->
<div class="form-check form-switch mb-0">
<input
id="batteryGridChargeOnceSwitch"
:checked="running"
class="form-check-input"
type="checkbox"
role="switch"
data-testid="battery-grid-charge-once-switch"
:aria-label="
running
? $t('battery.gridChargeOnce.cancel')
: $t('battery.gridChargeOnce.start')
"
:disabled="busy"
@click="cancel"
>
<PowerIcon />
</button>
<button
@change="toggle"
/>
<div v-if="running" class="form-check-label">
<span data-testid="battery-grid-charge-once-status">
{{ $t("battery.gridChargeOnce.running", { soc: fmtSoc(once.target) }) }} ·
{{ whenText }} ·
<span :class="once.active ? 'text-primary' : 'evcc-gray'">{{
once.active
? $t("battery.gridChargeOnce.active")
: $t("battery.gridChargeOnce.waiting")
}}</span>
</span>
</div>

<form
v-else
type="submit"
form="batteryGridChargeOnceForm"
class="power btn btn-outline-primary"
data-testid="battery-grid-charge-once-start"
:aria-label="$t('battery.gridChargeOnce.start')"
:title="$t('battery.gridChargeOnce.start')"
:disabled="busy"
id="batteryGridChargeOnceForm"
class="form-check-label"
@submit.prevent="start"
>
<PowerIcon />
</button>
</div>

<div v-if="running">
<span data-testid="battery-grid-charge-once-status">
{{ $t("battery.gridChargeOnce.running", { soc: fmtSoc(once.target) }) }} ·
{{ whenText }} ·
<span :class="once.active ? 'text-primary' : 'evcc-gray'">{{
once.active
? $t("battery.gridChargeOnce.active")
: $t("battery.gridChargeOnce.waiting")
}}</span>
</span>
</div>

<form v-else id="batteryGridChargeOnceForm" @submit.prevent="start">
<i18n-t keypath="battery.gridChargeOnce.sentence" tag="span" scope="global">
<template #soc>
<InlineSocSelect
id="batteryGridChargeOnceSoc"
:options="socOptions"
:selected="target"
:label="fmtSoc(target)"
nowrap
@change="target = parseInt(($event.target as HTMLInputElement).value, 10)"
/>
</template>
<template #when>
<CustomSelect
id="batteryGridChargeOnceWhen"
:options="whenOptions"
:selected="when"
:aria-label="$t('battery.gridChargeOnce.whenLabel')"
data-testid="battery-grid-charge-once-when"
inline
@change="when = ($event.target as HTMLSelectElement).value"
>
<span
v-if="when === 'now'"
class="text-decoration-underline fw-bold text-nowrap"
>
{{ $t("battery.gridChargeOnce.now") }}
</span>
<!-- "bis" stays with the time when the sentence wraps -->
<i18n-t
v-else
keypath="battery.gridChargeOnce.until"
tag="span"
class="text-nowrap"
scope="global"
<i18n-t keypath="battery.gridChargeOnce.sentence" tag="span" scope="global">
<template #soc>
<InlineSocSelect
id="batteryGridChargeOnceSoc"
:options="socOptions"
:selected="target"
:label="fmtSoc(target)"
nowrap
@change="
target = parseInt(($event.target as HTMLInputElement).value, 10)
"
/>
</template>
<template #when>
<CustomSelect
id="batteryGridChargeOnceWhen"
:options="whenOptions"
:selected="when"
:aria-label="$t('battery.gridChargeOnce.whenLabel')"
data-testid="battery-grid-charge-once-when"
inline
@change="when = ($event.target as HTMLSelectElement).value"
>
<template #time>
<span class="text-decoration-underline fw-bold">{{ when }}</span>
</template>
</i18n-t>
</CustomSelect>
</template>
</i18n-t>
</form>
<span
v-if="when === 'now'"
class="text-decoration-underline fw-bold text-nowrap"
>
{{ $t("battery.gridChargeOnce.now") }}
</span>
<!-- "bis" stays with the time when the sentence wraps -->
<i18n-t
v-else
keypath="battery.gridChargeOnce.until"
tag="span"
class="text-nowrap"
scope="global"
>
<template #time>
<span class="text-decoration-underline fw-bold">{{
when
}}</span>
</template>
</i18n-t>
</CustomSelect>
</template>
</i18n-t>
</form>
</div>

<p v-if="error" class="text-danger small mt-2 mb-0">{{ error }}</p>
</div>
Expand All @@ -97,7 +97,6 @@ import api from "@/api";
import store from "@/store";
import CustomSelect from "../Helper/CustomSelect.vue";
import InlineSocSelect from "./InlineSocSelect.vue";
import PowerIcon from "./PowerIcon.vue";

interface GridChargeOnce {
target: number;
Expand All @@ -109,7 +108,7 @@ interface GridChargeOnce {
// of day at the cheapest slots, see core/site_lm_once.go
export default defineComponent({
name: "BatteryGridChargeOnce",
components: { CustomSelect, InlineSocSelect, PowerIcon },
components: { CustomSelect, InlineSocSelect },
mixins: [formatter],
data() {
// when: "now" or a time of day HH:MM
Expand Down Expand Up @@ -157,6 +156,13 @@ export default defineComponent({
fmtSoc(soc: number) {
return this.fmtPercentage(soc);
},
toggle(e: Event) {
const on = (e.target as HTMLInputElement).checked;
// the switch follows the state from the server, not the click
(e.target as HTMLInputElement).checked = this.running;
if (on) this.start();
else this.cancel();
},
async start() {
this.busy = true;
this.error = "";
Expand Down Expand Up @@ -184,18 +190,3 @@ export default defineComponent({
},
});
</script>

<style scoped>
/* round, colours from the primary button so dark mode follows evcc */
.power {
width: 36px;
height: 36px;
padding: 0;
border-width: 2px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
</style>
14 changes: 0 additions & 14 deletions assets/js/components/Battery/BatteryPeakShavingCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,6 @@
</i18n-t>
</div>

<!-- custom: the load management circuit takes the peak limit, see core/site_lm_switch.go -->
<p
v-if="alsoCircuit"
class="small evcc-gray mt-3 mb-0"
data-testid="battery-peak-shaving-circuit"
>
{{ $t("battery.peakShaving.alsoCircuit", { name: alsoCircuit }) }}
</p>

<div v-if="error" class="alert alert-danger mt-3 mb-0 py-2 small">{{ error }}</div>

<div
Expand Down Expand Up @@ -80,7 +71,6 @@ import api from "@/api";
import store from "@/store";
import { socSteps, MARSTEK_MIN_RESERVE } from "@/utils/socSteps";
import { peakShavingSetUp } from "@/utils/peakShaving";
import { peakShavingCircuitTitle } from "@/utils/circuitFromPeak";
import Card from "../Helper/Card.vue";
import InlineSocSelect from "./InlineSocSelect.vue";

Expand Down Expand Up @@ -132,10 +122,6 @@ export default defineComponent({
entityMissing(): boolean {
return !peakShavingSetUp({ ...store.state, peakShavingEntity: this.entity });
},
// the circuit whose power limit is this limit right now, empty = none
alsoCircuit(): string {
return peakShavingCircuitTitle(store.state);
},
followOn(): boolean {
return !!this.follow?.enabled;
},
Expand Down
24 changes: 0 additions & 24 deletions assets/js/components/Battery/PowerIcon.vue

This file was deleted.

25 changes: 1 addition & 24 deletions assets/js/utils/circuitFromPeak.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vite-plus/test";
import { circuitPeakHintLimit, peakShavingCircuitTitle } from "./circuitFromPeak";
import { circuitPeakHintLimit } from "./circuitFromPeak";

describe("circuitPeakHintLimit", () => {
const on = {
Expand All @@ -25,26 +25,3 @@ describe("circuitPeakHintLimit", () => {
expect(circuitPeakHintLimit({}, "main")).toBeUndefined();
});
});

describe("peakShavingCircuitTitle", () => {
const on = {
lmOff: { circuit: "main", fromPeak: true },
peakShaving: true,
circuits: { main: { title: "Hausanschluss" } },
};

test("names the circuit whose limit is the peak limit right now", () => {
expect(peakShavingCircuitTitle(on)).toBe("Hausanschluss");
expect(peakShavingCircuitTitle({ ...on, circuits: {} })).toBe("main");
});

test("empty otherwise", () => {
expect(peakShavingCircuitTitle({ ...on, lmOff: { circuit: "main", fromPeak: false } })).toBe(
""
);
expect(peakShavingCircuitTitle({ ...on, lmOff: { fromPeak: true } })).toBe("");
expect(peakShavingCircuitTitle({ ...on, peakShaving: false })).toBe("");
expect(peakShavingCircuitTitle(undefined)).toBe("");
expect(peakShavingCircuitTitle({})).toBe("");
});
});
8 changes: 0 additions & 8 deletions assets/js/utils/circuitFromPeak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,3 @@ export function circuitPeakHintLimit(
if (!state.lmAdvanced?.circuitFromPeak) return undefined;
return state.peakShavingLimit || 0;
}

// The circuit the peak shaving card names: the one whose limit is the peak limit
// right now, empty otherwise.
export function peakShavingCircuitTitle(state: CircuitFromPeakState | undefined): string {
const name = state?.lmOff?.circuit;
if (!name || !state?.lmOff?.fromPeak || !state.peakShaving) return "";
return state.circuits?.[name]?.title || name;
}
2 changes: 1 addition & 1 deletion core/lm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ Every change in an evcc file. Check these when merging a new evcc version.
| snow on pv | `core/site_snow.go`, `core/site_snow_auto.go`, `core/testdata/open-meteo-snow-tirol.json` (recorded answer, added with `git add -f` as `*.json` is ignored), `assets/js/components/Forecast/SnowCoverSwitch.vue` |
| optimizer | `core/site_optimizer_lm.go`, `core/site_optimizer_reserve_pass.go`, `core/site_optimizer_soc_pass.go`, `core/site_lm_export_forecast.go`, `tariff/homeassistant_forecast.go`, `templates/definition/tariff/homeassistant-forecast.yaml` |
| api, keys | `core/site/api_custom.go`, `server/http_custom.go`, `core/keys/site_custom.go` |
| ui | `assets/js/types/evcc-lm.ts`, `assets/js/utils/lmPriorityOrder.ts`, `assets/js/utils/socSteps.ts`, `assets/js/components/LoadManagement/`, `assets/js/components/PeakShaving/`, the battery cards in `assets/js/components/Battery/` (`BatterySocGridChargeCard`, `BatteryGridChargeOnce`, `PowerIcon`, `BatteryPeakShavingCard`, `BatteryProfileCard`, `ProfileIcon`), the config components in `assets/js/components/Config/` (`PeakShavingConfig`, `LmConfigModals` and its dialogs, `FeedInEegSummary`, `PhaseSwitchFields`, `CircuitPeakHint`), `assets/js/utils/circuitFromPeak.ts` |
| ui | `assets/js/types/evcc-lm.ts`, `assets/js/utils/lmPriorityOrder.ts`, `assets/js/utils/socSteps.ts`, `assets/js/components/LoadManagement/`, `assets/js/components/PeakShaving/`, the battery cards in `assets/js/components/Battery/` (`BatterySocGridChargeCard`, `BatteryGridChargeOnce`, `BatteryPeakShavingCard`, `BatteryProfileCard`, `ProfileIcon`), the config components in `assets/js/components/Config/` (`PeakShavingConfig`, `LmConfigModals` and its dialogs, `FeedInEegSummary`, `PhaseSwitchFields`, `CircuitPeakHint`), `assets/js/utils/circuitFromPeak.ts` |
| build | `.github/workflows/custom-image.yml` |
| start page | `.github/README.md` (GitHub shows it instead of the evcc `README.md`; its feature list follows part 1) |

Expand Down
1 change: 0 additions & 1 deletion i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"suggestion": "Vorschlag"
},
"peakShaving": {
"alsoCircuit": "Gilt auch für Stromkreis {name}",
"description": "Unter {reserve} deckt die Batterie nur, was das 15-Minuten-Mittel über {limit} heben würde. Darüber normaler Eigenverbrauch.",
"enable": "Untere Batteriereserve für Lastspitzen zurückhalten.",
"noEntity": "Keine Zielentität hinterlegt (Konfiguration → Lastmanagement-Details → Peak Shaving).",
Expand Down
1 change: 0 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
"suggestion": "Suggestion"
},
"peakShaving": {
"alsoCircuit": "Also applies to circuit {name}",
"description": "Below {reserve}, the battery only covers what would lift the 15 minute average above {limit}. Above it, normal self-consumption.",
"enable": "Hold the lower battery range back for demand peaks.",
"noEntity": "No target entity set (Configuration → Load management details → Peak Shaving).",
Expand Down
Loading