evcc-io/assets/js/components/TargetCharge.vue
Michael Geers 7de484969a
Persist targetSoc/Energy, minSoc, targetTime per vehicle, add minSoc to UI (#8115)
* Plugins: make javascript return values more permissive

* Persist targetSoc/Energy, minSoc and target time, remove experimental from minSoc UI

* go generate

* Fix tests

* Update minSoc description

* Store settings by index. Show plan outside pv mode. Add help texts.

* remove slug

* go mock

* lint

* chore: simplify templates (#8304)

* Add e2e tests with playwright (#8123)

* Revert "1p3p: let charger handle session stop/restart (#7723)"

This reverts commit dd787ce52f.

* mazda2mqtt: document vin required (#8319)

* FoxESS: split H1/H3 devices (#7376)

Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>

* Translations update from Hosted Weblate (#8124)

* Translated using Weblate (Czech)

Currently translated at 37.3% (99 of 265 strings)

Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/cs/
Translation: evcc/evcc

* Translated using Weblate (Finnish)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/fi/
Translation: evcc/evcc

* Translated using Weblate (German)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: ThinkEV <claas@rootdir.de>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/de/
Translation: evcc/evcc

* Translated using Weblate (Croatian)

Currently translated at 100.0% (265 of 265 strings)

Translated using Weblate (Slovenian)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/hr/
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/sl/
Translation: evcc/evcc

* Translated using Weblate (Catalan)

Currently translated at 100.0% (265 of 265 strings)

Co-authored-by: Norbert Poch <npoch@yahoo.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/ca/
Translation: evcc/evcc

* Translated using Weblate (Dutch)

Currently translated at 76.6% (203 of 265 strings)

Translated using Weblate (Dutch)

Currently translated at 74.3% (197 of 265 strings)

Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evcc/evcc/nl/
Translation: evcc/evcc

* fix toml

---------

Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Co-authored-by: ThinkEV <claas@rootdir.de>
Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Co-authored-by: Norbert Poch <npoch@yahoo.com>
Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>

* Add OBO Betterman Ion (#8321)

Also aligns heartbeat implementations

* Enphase: add token auth (firmware D7.x.xxx) and grid (#8247)

* Mqtt: fix smartCostLimit topic case (#8328)

* Add ISO15118 vehicle template (#8302)

* Mqtt: simplify setters

* Mqtt: disable message ordering to improve performance

* chore: simplify random state generation

* Porsche: remove deprecated mobile api (#8349)

* Porsche: remove remaining mobile api types

* Cupra: add odometer (#8340)

* chore: refactor go-stylish

* chore: fix stale handler

* mazda2mqtt: longer timeout (#8364)

* chore: pre-process toml

* chore: prevent toml double quotes

* Audi: temporarily switch to etron (#8374)

* Fix nightly build (#8384)

* Update SunSpec templates (#8270)

* Revert "Fix nightly build (#8384)"

This reverts commit 536dbc936c.

* Revert "Add e2e tests with playwright (#8123)"

This reverts commit 0df815729b.

* Easee: update Observation IDs (#8391)

* Easee: handle smartCharging errors (#8389)

* TWC: add non-Tesla vehicle warning (#8329)

* TWC: allow loadpoint to wakeup vehicle (#8284)

* chore: use gridx consts

* Fix ISO15118 vehicle (#8402)

Such vehicles would be reported as `(Offline)` previously, as no Soc can be fetched and thus needs to behave as the `offline` template.

The SoC is fetched via the charger, so hardcode it to 0 in here

* Modbus: add coils  (#8385)

* OpenEVSE: fix api (#8415)

* chore: refactor offline vehicles (#8404)

* Easee: wait for api confirmation (#8307)

* Revert "Easee: wait for api confirmation (#8307)"

This reverts commit ce0e4277aa.

* automatically switch session log energy unit (#8371)

Show the charged energy in the session log table and details in Wh for
values below 1 kWh and in kWh for larger values

* Fix nightly/release + integration workflow  (#8450)

* Revert "Add e2e tests with playwright (#8123)"

This reverts commit 0df815729b.

* Revert "Revert "Add e2e tests with playwright (#8123)""

This reverts commit 2299306954.

* fix nightly/release

* fix nightly/release

* fix nightly/release

---------

Co-authored-by: andig <cpuidle@gmx.de>

* chore: no integration on nightly

* chore: no integration on nightly

* chore: no integration on nightly

* Sessions: filter, monthly sums, paging (#8278)

* fmt

* Update i18n/en.toml

Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com>

* Use exp/slices

* Use mux consistently

* add integration tests

---------

Co-authored-by: andig <cpuidle@gmx.de>
Co-authored-by: C64Axel <33828760+C64Axel@users.noreply.github.com>
Co-authored-by: premultiply <4681172+premultiply@users.noreply.github.com>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Dusan Suja <bc.suja.dusan@googlemail.com>
Co-authored-by: Arna Lepikkö <arna.lepikko@telemail.fi>
Co-authored-by: ThinkEV <claas@rootdir.de>
Co-authored-by: Žiga Deisinger <ziga@deisinger.si>
Co-authored-by: Norbert Poch <npoch@yahoo.com>
Co-authored-by: Ruben Van Boxem <vanboxem.ruben@gmail.com>
Co-authored-by: salz3n <79696598+salz3n@users.noreply.github.com>
Co-authored-by: Daniel Paschke <paschdan@gmail.com>
Co-authored-by: Andreas Linde <42185+DerAndereAndi@users.noreply.github.com>
Co-authored-by: lex777777 <84906358+lex777777@users.noreply.github.com>
Co-authored-by: Oscar <OAltr@users.noreply.github.com>
Co-authored-by: Michael Heß <GrimmiMeloni@users.noreply.github.com>
Co-authored-by: RTTTC <94727758+RTTTC@users.noreply.github.com>
Co-authored-by: kscholty <47229207+kscholty@users.noreply.github.com>
Co-authored-by: Jan Alexander <jan.alexander@posteo.de>
Co-authored-by: Simon Riepl <43091717+savus4@users.noreply.github.com>
2023-08-02 15:43:00 +02:00

314 lines
8.1 KiB
Vue

<template>
<form @submit.prevent="setTargetTime">
<div class="mt-4">
<div class="form-group d-lg-flex align-items-baseline mb-2 justify-content-between">
<!-- eslint-disable vue/no-v-html -->
<label :for="`targetTimeLabel${id}`" class="mb-3 me-3">
<span v-if="socBasedCharging">
{{
$t("main.targetCharge.descriptionSoc", {
targetSoc,
})
}}
</span>
<span v-else>
{{
$t("main.targetCharge.descriptionEnergy", {
targetEnergy: targetEnergyFormatted,
})
}}
</span>
</label>
<!-- eslint-enable vue/no-v-html -->
<div class="d-flex justify-content-between date-selection">
<select
:id="`targetTimeLabel${id}`"
v-model="selectedDay"
class="form-select me-2"
data-testid="target-day"
>
<option v-for="opt in dayOptions()" :key="opt.value" :value="opt.value">
{{ opt.name }}
</option>
</select>
<input
v-model="selectedTime"
type="time"
class="form-control ms-2 time-selection"
:step="60 * 5"
required
data-testid="target-time"
/>
</div>
</div>
<p class="mb-0">
<span v-if="timeInThePast" class="d-block text-danger mb-1">
{{ $t("main.targetCharge.targetIsInThePast") }}
</span>
<span v-if="!socBasedCharging && !targetEnergy" class="d-block text-danger mb-1">
{{ $t("main.targetCharge.targetEnergyRequired") }}
</span>
<span v-if="timeTooFarInTheFuture" class="d-block text-secondary mb-1">
{{ $t("main.targetCharge.targetIsTooFarInTheFuture") }}
</span>
<span v-if="costLimitExists" class="d-block text-secondary mb-1">
{{
$t("main.targetCharge.costLimitIgnore", {
limit: costLimitText,
})
}}
</span>
<span v-if="['off', 'now'].includes(mode)" class="d-block text-secondary mb-1">
{{ $t("main.targetCharge.onlyInPvMode") }}
</span>
&nbsp;
</p>
<TargetChargePlan v-if="targetChargePlanProps" v-bind="targetChargePlanProps" />
<div class="d-flex justify-content-between mt-3">
<button
type="button"
class="btn btn-outline-secondary"
:disabled="!targetTime"
@click="removeTargetTime"
>
{{ $t("main.targetCharge.remove") }}
</button>
<button type="submit" class="btn btn-primary" :disabled="timeInThePast">
<span v-if="targetTime">
{{ $t("main.targetCharge.update") }}
</span>
<span v-else>
{{ $t("main.targetCharge.activate") }}
</span>
</button>
</div>
</div>
</form>
</template>
<script>
import "@h2d2/shopicons/es/filled/plus";
import "@h2d2/shopicons/es/filled/edit";
import { CO2_TYPE } from "../units";
import TargetChargePlan from "./TargetChargePlan.vue";
import api from "../api";
import formatter from "../mixins/formatter";
const DEFAULT_TARGET_TIME = "7:00";
const LAST_TARGET_TIME_KEY = "last_target_time";
export default {
name: "TargetCharge",
components: { TargetChargePlan },
mixins: [formatter],
props: {
id: [String, Number],
planActive: Boolean,
targetTime: String,
targetSoc: Number,
targetEnergy: Number,
socBasedCharging: Boolean,
disabled: Boolean,
smartCostLimit: Number,
smartCostType: String,
currency: String,
mode: String,
},
emits: ["target-time-updated", "target-time-removed"],
data: function () {
return {
selectedDay: null,
selectedTime: null,
plan: {},
tariff: {},
activeTab: "time",
loading: false,
};
},
computed: {
timeTabActive: function () {
return this.activeTab === "time";
},
priceTabActive: function () {
return this.activeTab === "price";
},
targetChargeEnabled: function () {
return this.targetTime;
},
timeInThePast: function () {
const now = new Date();
return now >= this.selectedTargetTime;
},
timeTooFarInTheFuture: function () {
if (this.tariff?.rates) {
const lastRate = this.tariff.rates[this.tariff.rates.length - 1];
if (lastRate?.end) {
const end = new Date(lastRate.end);
return this.selectedTargetTime >= end;
}
}
return false;
},
selectedTargetTime: function () {
return new Date(`${this.selectedDay}T${this.selectedTime || "00:00"}`);
},
targetEnergyFormatted: function () {
return this.fmtKWh(this.targetEnergy * 1e3, true, true, 1);
},
targetChargePlanProps: function () {
const targetTime = this.selectedTargetTime;
const { rates } = this.tariff;
const { duration, plan } = this.plan;
const { currency, smartCostType } = this;
return rates ? { duration, rates, plan, targetTime, currency, smartCostType } : null;
},
tariffLowest: function () {
return this.tariff?.rates.reduce((res, slot) => {
return Math.min(res, slot.price);
}, Number.MAX_VALUE);
},
tariffHighest: function () {
return this.tariff?.rates.reduce((res, slot) => {
return Math.max(res, slot.price);
}, 0);
},
costLimitExists: function () {
return this.smartCostLimit !== 0;
},
costLimitText: function () {
if (this.isCo2) {
this.$t("main.targetCharge.co2Limit", {
co2: this.fmtCo2Short(this.smartCostLimit),
});
}
return this.$t("main.targetCharge.priceLimit", {
price: this.fmtPricePerKWh(this.smartCostLimit, this.currency, true),
});
},
isCo2() {
return this.smartCostType === CO2_TYPE;
},
},
watch: {
targetTime() {
this.initInputFields();
this.updatePlan();
},
selectedTargetTime() {
this.updatePlan();
},
targetSoc() {
this.updatePlan();
},
targetEnergy() {
this.updatePlan();
},
},
mounted() {
this.initInputFields();
this.updatePlan();
},
methods: {
updatePlan: async function () {
if (
!this.timeInThePast &&
(this.targetEnergy || this.targetSoc) &&
!isNaN(this.selectedTargetTime) &&
!this.loading
) {
try {
this.loading = true;
this.plan = (
await api.get(`loadpoints/${this.id}/target/plan`, {
params: { targetTime: this.selectedTargetTime },
})
).data.result;
const tariffRes = await api.get(`tariff/planner`, {
validateStatus: function (status) {
return status >= 200 && status < 500;
},
});
this.tariff = tariffRes.status === 404 ? { rates: [] } : tariffRes.data.result;
} catch (e) {
console.error(e);
} finally {
this.loading = false;
}
}
},
defaultDate: function () {
const [hours, minutes] = (
window.localStorage[LAST_TARGET_TIME_KEY] || DEFAULT_TARGET_TIME
).split(":");
const target = new Date();
target.setSeconds(0);
target.setMinutes(minutes);
target.setHours(hours);
// today or tomorrow?
const isInPast = target < new Date();
if (isInPast) {
target.setDate(target.getDate() + 1);
}
return target;
},
initInputFields: function () {
let date = this.defaultDate();
let targetTimeInTheFuture = new Date(this.targetTime) > new Date();
if (this.targetChargeEnabled && targetTimeInTheFuture) {
date = new Date(this.targetTime);
}
this.selectedDay = this.fmtDayString(date);
this.selectedTime = this.fmtTimeString(date);
},
dayOptions: function () {
const options = [];
const date = new Date();
const labels = [
this.$t("main.targetCharge.today"),
this.$t("main.targetCharge.tomorrow"),
];
for (let i = 0; i < 7; i++) {
const dayNumber = date.toLocaleDateString(this.$i18n.locale, {
month: "short",
day: "numeric",
});
const dayName =
labels[i] || date.toLocaleDateString(this.$i18n.locale, { weekday: "long" });
options.push({
value: this.fmtDayString(date),
name: `${dayNumber} (${dayName})`,
});
date.setDate(date.getDate() + 1);
}
return options;
},
setTargetTime: function () {
try {
const hours = this.selectedTargetTime.getHours();
const minutes = this.selectedTargetTime.getMinutes();
window.localStorage[LAST_TARGET_TIME_KEY] = `${hours}:${minutes}`;
} catch (e) {
console.warn(e);
}
this.$emit("target-time-updated", this.selectedTargetTime);
},
removeTargetTime: function () {
this.$emit("target-time-removed");
},
},
};
</script>
<style scoped>
@media (min-width: 992px) {
.date-selection {
width: 370px;
}
}
.time-selection {
flex-basis: 200px;
}
</style>