Skip to content

Commit f568cb8

Browse files
authored
Add Target Charge Plan Visualisation (evcc-io#5860)
1 parent edaffce commit f568cb8

File tree

13 files changed

+1217
-40
lines changed

13 files changed

+1217
-40
lines changed

assets/css/app.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,8 @@ small {
347347
.dark .form-switch .form-check-input:checked {
348348
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");
349349
}
350+
/* fix desktop safari formatting */
351+
input::-webkit-datetime-edit {
352+
display: block;
353+
padding: 0;
354+
}

assets/js/components/TargetCharge.vue

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<div
2121
:id="modalId"
2222
ref="modal"
23-
class="modal fade text-dark"
23+
class="modal fade text-dark modal-xl"
2424
data-bs-backdrop="true"
2525
tabindex="-1"
2626
role="dialog"
@@ -44,9 +44,11 @@
4444
</div>
4545
<form @submit.prevent="setTargetTime">
4646
<div class="modal-body">
47-
<div class="form-group mb-2">
47+
<div
48+
class="form-group d-lg-flex align-items-baseline mb-2 justify-content-between"
49+
>
4850
<!-- eslint-disable vue/no-v-html -->
49-
<label for="targetTimeLabel" class="mb-3">
51+
<label for="targetTimeLabel" class="mb-3 me-3">
5052
<span v-if="socBasedCharging">
5153
{{
5254
$t("main.targetCharge.descriptionSoc", {
@@ -63,15 +65,8 @@
6365
</span>
6466
</label>
6567
<!-- eslint-enable vue/no-v-html -->
66-
<div
67-
class="d-flex justify-content-between"
68-
:style="{ 'max-width': '350px' }"
69-
>
70-
<select
71-
v-model="selectedDay"
72-
class="form-select me-2"
73-
:style="{ 'flex-basis': '60%' }"
74-
>
68+
<div class="d-flex justify-content-between date-selection">
69+
<select v-model="selectedDay" class="form-select me-2">
7570
<option
7671
v-for="opt in dayOptions()"
7772
:key="opt.value"
@@ -83,17 +78,25 @@
8378
<input
8479
v-model="selectedTime"
8580
type="time"
86-
class="form-control ms-2"
87-
:style="{ 'flex-basis': '40%' }"
81+
class="form-control ms-2 time-selection"
8882
:step="60 * 5"
8983
required
9084
/>
9185
</div>
9286
</div>
93-
<p v-if="!selectedTargetTimeValid" class="text-danger mb-0">
94-
{{ $t("main.targetCharge.targetIsInThePast") }}
87+
<p class="mb-0">
88+
<span v-if="timeInThePast" class="text-danger">
89+
{{ $t("main.targetCharge.targetIsInThePast") }}
90+
</span>
91+
<span v-else-if="timeTooFarInTheFuture" class="text-secondary">
92+
{{ $t("main.targetCharge.targetIsTooFarInTheFuture") }}
93+
</span>
94+
&nbsp;
9595
</p>
96-
<TargetChargePlanMinimal v-else-if="plan.duration" v-bind="plan" />
96+
<TargetChargePlan
97+
v-if="targetChargePlanProps"
98+
v-bind="targetChargePlanProps"
99+
/>
97100
</div>
98101
<div class="modal-footer d-flex justify-content-between">
99102
<button
@@ -109,9 +112,14 @@
109112
type="submit"
110113
class="btn btn-primary"
111114
data-bs-dismiss="modal"
112-
:disabled="!selectedTargetTimeValid"
115+
:disabled="timeInThePast"
113116
>
114-
{{ $t("main.targetCharge.activate") }}
117+
<span v-if="targetTime">
118+
{{ $t("main.targetCharge.update") }}
119+
</span>
120+
<span v-else>
121+
{{ $t("main.targetCharge.activate") }}
122+
</span>
115123
</button>
116124
</div>
117125
</form>
@@ -127,7 +135,7 @@ import Modal from "bootstrap/js/dist/modal";
127135
import "@h2d2/shopicons/es/filled/plus";
128136
import "@h2d2/shopicons/es/filled/edit";
129137
import LabelAndValue from "./LabelAndValue.vue";
130-
import TargetChargePlanMinimal from "./TargetChargePlanMinimal.vue";
138+
import TargetChargePlan from "./TargetChargePlan.vue";
131139
import api from "../api";
132140
133141
import formatter from "../mixins/formatter";
@@ -137,7 +145,7 @@ const LAST_TARGET_TIME_KEY = "last_target_time";
137145
138146
export default {
139147
name: "TargetCharge",
140-
components: { LabelAndValue, TargetChargePlanMinimal },
148+
components: { LabelAndValue, TargetChargePlan },
141149
mixins: [formatter],
142150
props: {
143151
id: [String, Number],
@@ -154,6 +162,7 @@ export default {
154162
selectedDay: null,
155163
selectedTime: null,
156164
plan: {},
165+
tariff: {},
157166
modal: null,
158167
isModalVisible: false,
159168
};
@@ -162,9 +171,19 @@ export default {
162171
targetChargeEnabled: function () {
163172
return this.targetTime;
164173
},
165-
selectedTargetTimeValid: function () {
174+
timeInThePast: function () {
166175
const now = new Date();
167-
return now < this.selectedTargetTime;
176+
return now >= this.selectedTargetTime;
177+
},
178+
timeTooFarInTheFuture: function () {
179+
if (this.tariff?.rates) {
180+
const lastRate = this.tariff.rates[this.tariff.rates.length - 1];
181+
if (lastRate.end) {
182+
const end = new Date(lastRate.end);
183+
return this.selectedTargetTime >= end;
184+
}
185+
}
186+
return false;
168187
},
169188
selectedTargetTime: function () {
170189
return new Date(`${this.selectedDay}T${this.selectedTime || "00:00"}`);
@@ -175,6 +194,12 @@ export default {
175194
targetEnergyFormatted: function () {
176195
return this.fmtKWh(this.targetEnergy * 1e3, true, true, 1);
177196
},
197+
targetChargePlanProps: function () {
198+
const targetTime = this.selectedTargetTime;
199+
const { rates } = this.tariff;
200+
const { duration, unit, plan } = this.plan;
201+
return rates ? { duration, rates, plan, unit, targetTime } : null;
202+
},
178203
},
179204
watch: {
180205
targetTimeLabel: function () {
@@ -219,14 +244,17 @@ export default {
219244
updatePlan: async function () {
220245
if (
221246
this.isModalVisible &&
222-
this.selectedTargetTimeValid &&
247+
!this.timeInThePast &&
223248
(this.targetEnergy || this.targetSoc)
224249
) {
225250
try {
226-
const response = await api.get(`/loadpoints/${this.id}/target/plan`, {
251+
const opts = {
227252
params: { targetTime: this.selectedTargetTime },
228-
});
229-
this.plan = response.data.result;
253+
};
254+
this.plan = (
255+
await api.get(`/loadpoints/${this.id}/target/plan`, opts)
256+
).data.result;
257+
this.tariff = (await api.get(`/tariff/planner`)).data.result;
230258
} catch (e) {
231259
console.error(e);
232260
}
@@ -323,4 +351,12 @@ export default {
323351
.value:hover {
324352
color: var(--bs-color-white);
325353
}
354+
@media (min-width: 992px) {
355+
.date-selection {
356+
width: 370px;
357+
}
358+
}
359+
.time-selection {
360+
flex-basis: 200px;
361+
}
326362
</style>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup>
2+
import TargetChargePlan from "./TargetChargePlan.vue";
3+
4+
const now = new Date();
5+
6+
function createDate(hoursFromNow) {
7+
const result = new Date(now.getTime());
8+
result.setHours(result.getHours() + hoursFromNow);
9+
return result;
10+
}
11+
12+
function createRate(price, hoursFromNow, durationHours = 1) {
13+
const start = new Date(now.getTime());
14+
start.setHours(start.getHours() + hoursFromNow);
15+
start.setMinutes(0);
16+
start.setSeconds(0);
17+
start.setMilliseconds(0);
18+
const end = new Date(start.getTime());
19+
end.setHours(start.getHours() + durationHours);
20+
end.setMinutes(0);
21+
end.setSeconds(0);
22+
end.setMilliseconds(0);
23+
return { start: start.toISOString(), end: end.toISOString(), price };
24+
}
25+
26+
const co2 = {
27+
rates: [
28+
545, 518, 545, 518, 213, 545, 527, 527, 536, 518, 400, 336, 336, 339, 344, 336, 336, 336,
29+
372, 400, 555, 555, 545, 555, 564, 545, 555, 545, 536, 545, 527, 536, 518, 545, 509, 336,
30+
336, 336,
31+
].map((price, i) => createRate(price, i)),
32+
duration: 8695,
33+
plan: [createRate(213, 4), createRate(336, 11), createRate(336, 12)],
34+
unit: "gCO2eq",
35+
targetTime: createDate(14),
36+
};
37+
38+
const fixed = {
39+
rates: [createRate(0.442, 0, 50)],
40+
duration: 8695,
41+
plan: [createRate(0.442, 12, 3)],
42+
unit: "EUR",
43+
targetTime: createDate(14),
44+
};
45+
46+
const zoned = {
47+
rates: [
48+
createRate(3.72, 0, 4),
49+
createRate(2.39, 4, 12),
50+
createRate(3.72, 16, 12),
51+
createRate(2.39, 28, 12),
52+
createRate(3.72, 40, 12),
53+
],
54+
duration: 8695,
55+
plan: [createRate(2.39, 13, 3)],
56+
unit: "DKK",
57+
targetTime: createDate(17),
58+
};
59+
</script>
60+
61+
<template>
62+
<Story title="TargetChargePlan">
63+
<Variant title="co2">
64+
<TargetChargePlan v-bind="co2" />
65+
</Variant>
66+
<Variant title="fixed">
67+
<TargetChargePlan v-bind="fixed" />
68+
</Variant>
69+
<Variant title="zoned">
70+
<TargetChargePlan v-bind="zoned" />
71+
</Variant>
72+
</Story>
73+
</template>

0 commit comments

Comments
 (0)