Skip to content

Commit

Permalink
Add Target Charge Plan Visualisation (evcc-io#5860)
Browse files Browse the repository at this point in the history
  • Loading branch information
naltatis authored Jan 29, 2023
1 parent edaffce commit f568cb8
Show file tree
Hide file tree
Showing 13 changed files with 1,217 additions and 40 deletions.
5 changes: 5 additions & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,8 @@ small {
.dark .form-switch .form-check-input:checked {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2328293e'/%3e%3c/svg%3e");
}
/* fix desktop safari formatting */
input::-webkit-datetime-edit {
display: block;
padding: 0;
}
90 changes: 63 additions & 27 deletions assets/js/components/TargetCharge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<div
:id="modalId"
ref="modal"
class="modal fade text-dark"
class="modal fade text-dark modal-xl"
data-bs-backdrop="true"
tabindex="-1"
role="dialog"
Expand All @@ -44,9 +44,11 @@
</div>
<form @submit.prevent="setTargetTime">
<div class="modal-body">
<div class="form-group mb-2">
<div
class="form-group d-lg-flex align-items-baseline mb-2 justify-content-between"
>
<!-- eslint-disable vue/no-v-html -->
<label for="targetTimeLabel" class="mb-3">
<label for="targetTimeLabel" class="mb-3 me-3">
<span v-if="socBasedCharging">
{{
$t("main.targetCharge.descriptionSoc", {
Expand All @@ -63,15 +65,8 @@
</span>
</label>
<!-- eslint-enable vue/no-v-html -->
<div
class="d-flex justify-content-between"
:style="{ 'max-width': '350px' }"
>
<select
v-model="selectedDay"
class="form-select me-2"
:style="{ 'flex-basis': '60%' }"
>
<div class="d-flex justify-content-between date-selection">
<select v-model="selectedDay" class="form-select me-2">
<option
v-for="opt in dayOptions()"
:key="opt.value"
Expand All @@ -83,17 +78,25 @@
<input
v-model="selectedTime"
type="time"
class="form-control ms-2"
:style="{ 'flex-basis': '40%' }"
class="form-control ms-2 time-selection"
:step="60 * 5"
required
/>
</div>
</div>
<p v-if="!selectedTargetTimeValid" class="text-danger mb-0">
{{ $t("main.targetCharge.targetIsInThePast") }}
<p class="mb-0">
<span v-if="timeInThePast" class="text-danger">
{{ $t("main.targetCharge.targetIsInThePast") }}
</span>
<span v-else-if="timeTooFarInTheFuture" class="text-secondary">
{{ $t("main.targetCharge.targetIsTooFarInTheFuture") }}
</span>
&nbsp;
</p>
<TargetChargePlanMinimal v-else-if="plan.duration" v-bind="plan" />
<TargetChargePlan
v-if="targetChargePlanProps"
v-bind="targetChargePlanProps"
/>
</div>
<div class="modal-footer d-flex justify-content-between">
<button
Expand All @@ -109,9 +112,14 @@
type="submit"
class="btn btn-primary"
data-bs-dismiss="modal"
:disabled="!selectedTargetTimeValid"
:disabled="timeInThePast"
>
{{ $t("main.targetCharge.activate") }}
<span v-if="targetTime">
{{ $t("main.targetCharge.update") }}
</span>
<span v-else>
{{ $t("main.targetCharge.activate") }}
</span>
</button>
</div>
</form>
Expand All @@ -127,7 +135,7 @@ import Modal from "bootstrap/js/dist/modal";
import "@h2d2/shopicons/es/filled/plus";
import "@h2d2/shopicons/es/filled/edit";
import LabelAndValue from "./LabelAndValue.vue";
import TargetChargePlanMinimal from "./TargetChargePlanMinimal.vue";
import TargetChargePlan from "./TargetChargePlan.vue";
import api from "../api";
import formatter from "../mixins/formatter";
Expand All @@ -137,7 +145,7 @@ const LAST_TARGET_TIME_KEY = "last_target_time";
export default {
name: "TargetCharge",
components: { LabelAndValue, TargetChargePlanMinimal },
components: { LabelAndValue, TargetChargePlan },
mixins: [formatter],
props: {
id: [String, Number],
Expand All @@ -154,6 +162,7 @@ export default {
selectedDay: null,
selectedTime: null,
plan: {},
tariff: {},
modal: null,
isModalVisible: false,
};
Expand All @@ -162,9 +171,19 @@ export default {
targetChargeEnabled: function () {
return this.targetTime;
},
selectedTargetTimeValid: function () {
timeInThePast: function () {
const now = new Date();
return now < this.selectedTargetTime;
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"}`);
Expand All @@ -175,6 +194,12 @@ export default {
targetEnergyFormatted: function () {
return this.fmtKWh(this.targetEnergy * 1e3, true, true, 1);
},
targetChargePlanProps: function () {
const targetTime = this.selectedTargetTime;
const { rates } = this.tariff;
const { duration, unit, plan } = this.plan;
return rates ? { duration, rates, plan, unit, targetTime } : null;
},
},
watch: {
targetTimeLabel: function () {
Expand Down Expand Up @@ -219,14 +244,17 @@ export default {
updatePlan: async function () {
if (
this.isModalVisible &&
this.selectedTargetTimeValid &&
!this.timeInThePast &&
(this.targetEnergy || this.targetSoc)
) {
try {
const response = await api.get(`/loadpoints/${this.id}/target/plan`, {
const opts = {
params: { targetTime: this.selectedTargetTime },
});
this.plan = response.data.result;
};
this.plan = (
await api.get(`/loadpoints/${this.id}/target/plan`, opts)
).data.result;
this.tariff = (await api.get(`/tariff/planner`)).data.result;
} catch (e) {
console.error(e);
}
Expand Down Expand Up @@ -323,4 +351,12 @@ export default {
.value:hover {
color: var(--bs-color-white);
}
@media (min-width: 992px) {
.date-selection {
width: 370px;
}
}
.time-selection {
flex-basis: 200px;
}
</style>
73 changes: 73 additions & 0 deletions assets/js/components/TargetChargePlan.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup>
import TargetChargePlan from "./TargetChargePlan.vue";
const now = new Date();
function createDate(hoursFromNow) {
const result = new Date(now.getTime());
result.setHours(result.getHours() + hoursFromNow);
return result;
}
function createRate(price, hoursFromNow, durationHours = 1) {
const start = new Date(now.getTime());
start.setHours(start.getHours() + hoursFromNow);
start.setMinutes(0);
start.setSeconds(0);
start.setMilliseconds(0);
const end = new Date(start.getTime());
end.setHours(start.getHours() + durationHours);
end.setMinutes(0);
end.setSeconds(0);
end.setMilliseconds(0);
return { start: start.toISOString(), end: end.toISOString(), price };
}
const co2 = {
rates: [
545, 518, 545, 518, 213, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336,
372, 400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336,
336, 336,
].map((price, i) => createRate(price, i)),
duration: 8695,
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
unit: "gCO2eq",
targetTime: createDate(14),
};
const fixed = {
rates: [createRate(0.442, 0, 50)],
duration: 8695,
plan: [createRate(0.442, 12, 3)],
unit: "EUR",
targetTime: createDate(14),
};
const zoned = {
rates: [
createRate(3.72, 0, 4),
createRate(2.39, 4, 12),
createRate(3.72, 16, 12),
createRate(2.39, 28, 12),
createRate(3.72, 40, 12),
],
duration: 8695,
plan: [createRate(2.39, 13, 3)],
unit: "DKK",
targetTime: createDate(17),
};
</script>

<template>
<Story title="TargetChargePlan">
<Variant title="co2">
<TargetChargePlan v-bind="co2" />
</Variant>
<Variant title="fixed">
<TargetChargePlan v-bind="fixed" />
</Variant>
<Variant title="zoned">
<TargetChargePlan v-bind="zoned" />
</Variant>
</Story>
</template>
Loading

0 comments on commit f568cb8

Please sign in to comment.