Skip to content

Commit a35a705

Browse files
authored
Add schedule for planner (#16091)
1 parent 35c9452 commit a35a705

33 files changed

+1771
-421
lines changed

api/plans.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package api
2+
3+
type RepeatingPlanStruct struct {
4+
Weekdays []int `json:"weekdays"` // 0-6 (Sunday-Saturday)
5+
Time string `json:"time"` // HH:MM
6+
Tz string `json:"tz"` // timezone in IANA format
7+
Soc int `json:"soc"`
8+
Active bool `json:"active"`
9+
}

assets/css/app.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,3 +632,7 @@ html.app .modal-dialog {
632632
.alert-danger code {
633633
color: var(--evcc-darkest-green);
634634
}
635+
636+
input::-webkit-date-and-time-value {
637+
text-align: left;
638+
}

assets/js/components/ChargingPlan.vue

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
data-testid="charging-plan"
88
>
99
<div class="value m-0 d-block align-items-baseline justify-content-center">
10-
<button class="value-button p-0" :class="buttonColor" @click="openModal">
10+
<button
11+
class="value-button p-0"
12+
:class="buttonColor"
13+
data-testid="charging-plan-button"
14+
@click="openModal"
15+
>
1116
<strong v-if="enabled">
1217
<span class="targetTimeLabel"> {{ targetTimeLabel }}</span>
1318
<div
@@ -33,6 +38,7 @@
3338
tabindex="-1"
3439
role="dialog"
3540
aria-hidden="true"
41+
data-testid="charging-plan-modal"
3642
>
3743
<div class="modal-dialog modal-dialog-centered" role="document">
3844
<div class="modal-content">
@@ -74,11 +80,12 @@
7480
</li>
7581
</ul>
7682
<div v-if="isModalVisible">
77-
<ChargingPlanSettings
83+
<ChargingPlansSettings
7884
v-if="departureTabActive"
79-
v-bind="chargingPlanSettingsProps"
80-
@plan-updated="updatePlan"
81-
@plan-removed="removePlan"
85+
v-bind="chargingPlansSettingsProps"
86+
@static-plan-updated="updateStaticPlan"
87+
@static-plan-removed="removeStaticPlan"
88+
@repeating-plans-updated="updateRepeatingPlans"
8289
/>
8390
<ChargingPlanArrival
8491
v-if="arrivalTabActive"
@@ -98,7 +105,7 @@
98105
<script>
99106
import Modal from "bootstrap/js/dist/modal";
100107
import LabelAndValue from "./LabelAndValue.vue";
101-
import ChargingPlanSettings from "./ChargingPlanSettings.vue";
108+
import ChargingPlansSettings from "./ChargingPlansSettings.vue";
102109
import ChargingPlanArrival from "./ChargingPlanArrival.vue";
103110
104111
import formatter from "../mixins/formatter";
@@ -110,7 +117,7 @@ const ONE_MINUTE = 60 * 1000;
110117
111118
export default {
112119
name: "ChargingPlan",
113-
components: { LabelAndValue, ChargingPlanSettings, ChargingPlanArrival },
120+
components: { LabelAndValue, ChargingPlansSettings, ChargingPlanArrival },
114121
mixins: [formatter, collector],
115122
props: {
116123
currency: String,
@@ -162,12 +169,18 @@ export default {
162169
limitSoc: function () {
163170
return this.vehicle?.limitSoc;
164171
},
165-
plans: function () {
172+
staticPlan: function () {
166173
if (this.socBasedPlanning) {
167-
return this.vehicle?.plans || [];
174+
return this.vehicle?.plan;
168175
}
169176
if (this.planEnergy && this.planTime) {
170-
return [{ energy: this.planEnergy, time: this.planTime }];
177+
return { energy: this.planEnergy, time: this.planTime };
178+
}
179+
return null;
180+
},
181+
repeatingPlans: function () {
182+
if (this.vehicle?.repeatingPlans.length > 0) {
183+
return [...this.vehicle.repeatingPlans];
171184
}
172185
return [];
173186
},
@@ -183,8 +196,8 @@ export default {
183196
arrivalTabActive: function () {
184197
return this.activeTab === "arrival";
185198
},
186-
chargingPlanSettingsProps: function () {
187-
return this.collectProps(ChargingPlanSettings);
199+
chargingPlansSettingsProps: function () {
200+
return this.collectProps(ChargingPlansSettings);
188201
},
189202
chargingPlanArrival: function () {
190203
return this.collectProps(ChargingPlanArrival);
@@ -211,6 +224,11 @@ export default {
211224
effectivePlanTime() {
212225
this.updateTargetTimeLabel();
213226
},
227+
"$i18n.locale": {
228+
handler() {
229+
this.updateTargetTimeLabel();
230+
},
231+
},
214232
},
215233
mounted() {
216234
this.modal = Modal.getOrCreateInstance(this.$refs.modal);
@@ -263,21 +281,24 @@ export default {
263281
showArrivalTab: function () {
264282
this.activeTab = "arrival";
265283
},
266-
updatePlan: function ({ soc, time, energy }) {
284+
updateStaticPlan: function ({ soc, time, energy }) {
267285
const timeISO = time.toISOString();
268286
if (this.socBasedPlanning) {
269287
api.post(`${this.apiVehicle}plan/soc/${soc}/${timeISO}`);
270288
} else {
271289
api.post(`${this.apiLoadpoint}plan/energy/${energy}/${timeISO}`);
272290
}
273291
},
274-
removePlan: function () {
292+
removeStaticPlan: function () {
275293
if (this.socBasedPlanning) {
276294
api.delete(`${this.apiVehicle}plan/soc`);
277295
} else {
278296
api.delete(`${this.apiLoadpoint}plan/energy`);
279297
}
280298
},
299+
updateRepeatingPlans: function (plans) {
300+
api.post(`${this.apiVehicle}plan/repeating`, { plans });
301+
},
281302
setMinSoc: function (soc) {
282303
api.post(`${this.apiVehicle}minsoc/${soc}`);
283304
},

assets/js/components/ChargingPlanPreview.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ describe("basics", () => {
3737
result = wrapper.vm.slots;
3838
});
3939

40-
test("should return 42 slots", () => {
41-
expect(result.length).eq(42);
40+
test("should return 39 slots", () => {
41+
expect(result.length).eq(39);
4242
});
4343

4444
test("slots should be an hour apart", () => {

assets/js/components/ChargingPlanPreview.vue

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525
</div>
2626
</div>
2727
</div>
28-
<TariffChart :slots="slots" @slot-hovered="slotHovered" />
28+
<TariffChart
29+
:slots="slots"
30+
:target-text="targetText"
31+
:target-offset="targetHourOffset"
32+
@slot-hovered="slotHovered"
33+
/>
2934
</div>
3035
</template>
3136

@@ -114,12 +119,28 @@ export default {
114119
}
115120
return null;
116121
},
122+
targetHourOffset() {
123+
if (!this.targetTime) {
124+
return null;
125+
}
126+
const start = new Date(this.startTime);
127+
start.setMinutes(0);
128+
start.setSeconds(0);
129+
start.setMilliseconds(0);
130+
return (this.targetTime.getTime() - start.getTime()) / (60 * 60 * 1000);
131+
},
132+
targetText() {
133+
if (!this.targetTime) {
134+
return null;
135+
}
136+
return this.fmtWeekdayTime(this.targetTime);
137+
},
117138
slots() {
118139
const result = [];
119140
const rates = this.convertDates(this.rates);
120141
const plan = this.convertDates(this.plan);
121142
const oneHour = 60 * 60 * 1000;
122-
for (let i = 0; i < 42; i++) {
143+
for (let i = 0; i < 39; i++) {
123144
const start = new Date(this.startTime.getTime() + oneHour * i);
124145
const startHour = start.getHours();
125146
start.setMinutes(0);
@@ -132,13 +153,23 @@ export default {
132153
const toLate = this.targetTime && this.targetTime <= start;
133154
// TODO: handle multiple matching time slots
134155
const price = this.findSlotInRange(start, end, rates)?.price;
156+
const isTarget = start <= this.targetTime && end > this.targetTime;
135157
const charging = this.findSlotInRange(start, end, plan) != null;
136158
const warning =
137159
charging &&
138160
this.targetTime &&
139161
end > this.targetTime &&
140162
this.targetTime < this.endTime;
141-
result.push({ day, price, startHour, endHour, charging, toLate, warning });
163+
result.push({
164+
day,
165+
price,
166+
startHour,
167+
endHour,
168+
charging,
169+
toLate,
170+
warning,
171+
isTarget,
172+
});
142173
}
143174
return result;
144175
},

0 commit comments

Comments
 (0)