e4d1ebdf1dc0e027925b16d54c7d82fb9d1934c8
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
2
3 import type { JSONSchemaType } from 'ajv';
4 import {
5 type Interval,
6 addSeconds,
7 areIntervalsOverlapping,
8 differenceInSeconds,
9 isAfter,
10 isBefore,
11 isWithinInterval,
12 } from 'date-fns';
13
14 import { OCPP16Constants } from './OCPP16Constants.js';
15 import {
16 type ChargingStation,
17 hasFeatureProfile,
18 hasReservationExpired,
19 } from '../../../charging-station/index.js';
20 import {
21 type GenericResponse,
22 type JsonType,
23 OCPP16AuthorizationStatus,
24 OCPP16AvailabilityType,
25 type OCPP16ChangeAvailabilityResponse,
26 OCPP16ChargePointStatus,
27 type OCPP16ChargingProfile,
28 type OCPP16ChargingSchedule,
29 type OCPP16ClearChargingProfileRequest,
30 type OCPP16IncomingRequestCommand,
31 type OCPP16MeterValue,
32 OCPP16MeterValueContext,
33 OCPP16MeterValueUnit,
34 OCPP16RequestCommand,
35 OCPP16StandardParametersKey,
36 OCPP16StopTransactionReason,
37 type OCPP16SupportedFeatureProfiles,
38 OCPPVersion,
39 } from '../../../types/index.js';
40 import { isNotEmptyArray, isNullOrUndefined, logger, roundTo } from '../../../utils/index.js';
41 import { OCPPServiceUtils } from '../OCPPServiceUtils.js';
42
43 export class OCPP16ServiceUtils extends OCPPServiceUtils {
44 public static checkFeatureProfile(
45 chargingStation: ChargingStation,
46 featureProfile: OCPP16SupportedFeatureProfiles,
47 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand,
48 ): boolean {
49 if (!hasFeatureProfile(chargingStation, featureProfile)) {
50 logger.warn(
51 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
52 OCPP16StandardParametersKey.SupportedFeatureProfiles
53 } in configuration`,
54 );
55 return false;
56 }
57 return true;
58 }
59
60 public static buildTransactionBeginMeterValue(
61 chargingStation: ChargingStation,
62 connectorId: number,
63 meterStart: number,
64 ): OCPP16MeterValue {
65 const meterValue: OCPP16MeterValue = {
66 timestamp: new Date(),
67 sampledValue: [],
68 };
69 // Energy.Active.Import.Register measurand (default)
70 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
71 chargingStation,
72 connectorId,
73 );
74 const unitDivider =
75 sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
76 meterValue.sampledValue.push(
77 OCPP16ServiceUtils.buildSampledValue(
78 sampledValueTemplate!,
79 roundTo((meterStart ?? 0) / unitDivider, 4),
80 OCPP16MeterValueContext.TRANSACTION_BEGIN,
81 ),
82 );
83 return meterValue;
84 }
85
86 public static buildTransactionDataMeterValues(
87 transactionBeginMeterValue: OCPP16MeterValue,
88 transactionEndMeterValue: OCPP16MeterValue,
89 ): OCPP16MeterValue[] {
90 const meterValues: OCPP16MeterValue[] = [];
91 meterValues.push(transactionBeginMeterValue);
92 meterValues.push(transactionEndMeterValue);
93 return meterValues;
94 }
95
96 public static remoteStopTransaction = async (
97 chargingStation: ChargingStation,
98 connectorId: number,
99 ): Promise<GenericResponse> => {
100 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
101 chargingStation,
102 connectorId,
103 OCPP16ChargePointStatus.Finishing,
104 );
105 const stopResponse = await chargingStation.stopTransactionOnConnector(
106 connectorId,
107 OCPP16StopTransactionReason.REMOTE,
108 );
109 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
110 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED;
111 }
112 return OCPP16Constants.OCPP_RESPONSE_REJECTED;
113 };
114
115 public static changeAvailability = async (
116 chargingStation: ChargingStation,
117 connectorIds: number[],
118 chargePointStatus: OCPP16ChargePointStatus,
119 availabilityType: OCPP16AvailabilityType,
120 ): Promise<OCPP16ChangeAvailabilityResponse> => {
121 const responses: OCPP16ChangeAvailabilityResponse[] = [];
122 for (const connectorId of connectorIds) {
123 let response: OCPP16ChangeAvailabilityResponse =
124 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
125 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!;
126 if (connectorStatus?.transactionStarted === true) {
127 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
128 }
129 connectorStatus.availability = availabilityType;
130 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
131 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
132 chargingStation,
133 connectorId,
134 chargePointStatus,
135 );
136 }
137 responses.push(response);
138 }
139 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
140 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
141 }
142 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
143 };
144
145 public static setChargingProfile(
146 chargingStation: ChargingStation,
147 connectorId: number,
148 cp: OCPP16ChargingProfile,
149 ): void {
150 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
151 logger.error(
152 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
153 );
154 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
155 }
156 if (
157 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
158 ) {
159 logger.error(
160 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`,
161 );
162 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
163 }
164 let cpReplaced = false;
165 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
166 chargingStation
167 .getConnectorStatus(connectorId)
168 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
169 if (
170 chargingProfile.chargingProfileId === cp.chargingProfileId ||
171 (chargingProfile.stackLevel === cp.stackLevel &&
172 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
173 ) {
174 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
175 cpReplaced = true;
176 }
177 });
178 }
179 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
180 }
181
182 public static clearChargingProfiles = (
183 chargingStation: ChargingStation,
184 commandPayload: OCPP16ClearChargingProfileRequest,
185 chargingProfiles: OCPP16ChargingProfile[] | undefined,
186 ): boolean => {
187 const { id, chargingProfilePurpose, stackLevel } = commandPayload;
188 let clearedCP = false;
189 if (isNotEmptyArray(chargingProfiles)) {
190 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
191 let clearCurrentCP = false;
192 if (chargingProfile.chargingProfileId === id) {
193 clearCurrentCP = true;
194 }
195 if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
196 clearCurrentCP = true;
197 }
198 if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
199 clearCurrentCP = true;
200 }
201 if (
202 chargingProfile.stackLevel === stackLevel &&
203 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
204 ) {
205 clearCurrentCP = true;
206 }
207 if (clearCurrentCP) {
208 chargingProfiles.splice(index, 1);
209 logger.debug(
210 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
211 chargingProfile,
212 );
213 clearedCP = true;
214 }
215 });
216 }
217 return clearedCP;
218 };
219
220 public static composeChargingSchedules = (
221 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
222 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
223 compositeInterval: Interval,
224 ): OCPP16ChargingSchedule | undefined => {
225 if (!chargingScheduleHigher && !chargingScheduleLower) {
226 return undefined;
227 }
228 if (chargingScheduleHigher && !chargingScheduleLower) {
229 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
230 }
231 if (!chargingScheduleHigher && chargingScheduleLower) {
232 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
233 }
234 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
235 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
236 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
237 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
238 const compositeChargingScheduleHigherInterval: Interval = {
239 start: compositeChargingScheduleHigher!.startSchedule!,
240 end: addSeconds(
241 compositeChargingScheduleHigher!.startSchedule!,
242 compositeChargingScheduleHigher!.duration!,
243 ),
244 };
245 const compositeChargingScheduleLowerInterval: Interval = {
246 start: compositeChargingScheduleLower!.startSchedule!,
247 end: addSeconds(
248 compositeChargingScheduleLower!.startSchedule!,
249 compositeChargingScheduleLower!.duration!,
250 ),
251 };
252 const higherFirst = isBefore(
253 compositeChargingScheduleHigherInterval.start,
254 compositeChargingScheduleLowerInterval.start,
255 );
256 if (
257 !areIntervalsOverlapping(
258 compositeChargingScheduleHigherInterval,
259 compositeChargingScheduleLowerInterval,
260 )
261 ) {
262 return {
263 ...compositeChargingScheduleLower,
264 ...compositeChargingScheduleHigher!,
265 startSchedule: higherFirst
266 ? (compositeChargingScheduleHigherInterval.start as Date)
267 : (compositeChargingScheduleLowerInterval.start as Date),
268 duration: higherFirst
269 ? differenceInSeconds(
270 compositeChargingScheduleLowerInterval.end,
271 compositeChargingScheduleHigherInterval.start,
272 )
273 : differenceInSeconds(
274 compositeChargingScheduleHigherInterval.end,
275 compositeChargingScheduleLowerInterval.start,
276 ),
277 chargingSchedulePeriod: [
278 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
279 return {
280 ...schedulePeriod,
281 startPeriod: higherFirst
282 ? 0
283 : schedulePeriod.startPeriod +
284 differenceInSeconds(
285 compositeChargingScheduleHigherInterval.start,
286 compositeChargingScheduleLowerInterval.start,
287 ),
288 };
289 }),
290 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
291 return {
292 ...schedulePeriod,
293 startPeriod: higherFirst
294 ? schedulePeriod.startPeriod +
295 differenceInSeconds(
296 compositeChargingScheduleLowerInterval.start,
297 compositeChargingScheduleHigherInterval.start,
298 )
299 : 0,
300 };
301 }),
302 ].sort((a, b) => a.startPeriod - b.startPeriod),
303 };
304 }
305 return {
306 ...compositeChargingScheduleLower,
307 ...compositeChargingScheduleHigher!,
308 startSchedule: higherFirst
309 ? (compositeChargingScheduleHigherInterval.start as Date)
310 : (compositeChargingScheduleLowerInterval.start as Date),
311 duration: higherFirst
312 ? differenceInSeconds(
313 compositeChargingScheduleLowerInterval.end,
314 compositeChargingScheduleHigherInterval.start,
315 )
316 : differenceInSeconds(
317 compositeChargingScheduleHigherInterval.end,
318 compositeChargingScheduleLowerInterval.start,
319 ),
320 chargingSchedulePeriod: [
321 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
322 return {
323 ...schedulePeriod,
324 startPeriod: higherFirst
325 ? 0
326 : schedulePeriod.startPeriod +
327 differenceInSeconds(
328 compositeChargingScheduleHigherInterval.start,
329 compositeChargingScheduleLowerInterval.start,
330 ),
331 };
332 }),
333 ...compositeChargingScheduleLower!.chargingSchedulePeriod
334 .filter((schedulePeriod, index) => {
335 if (
336 higherFirst &&
337 isWithinInterval(
338 addSeconds(
339 compositeChargingScheduleLowerInterval.start,
340 schedulePeriod.startPeriod,
341 ),
342 {
343 start: compositeChargingScheduleLowerInterval.start,
344 end: compositeChargingScheduleHigherInterval.end,
345 },
346 )
347 ) {
348 return false;
349 }
350 if (
351 higherFirst &&
352 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
353 !isWithinInterval(
354 addSeconds(
355 compositeChargingScheduleLowerInterval.start,
356 schedulePeriod.startPeriod,
357 ),
358 {
359 start: compositeChargingScheduleLowerInterval.start,
360 end: compositeChargingScheduleHigherInterval.end,
361 },
362 ) &&
363 isWithinInterval(
364 addSeconds(
365 compositeChargingScheduleLowerInterval.start,
366 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod,
367 ),
368 {
369 start: compositeChargingScheduleLowerInterval.start,
370 end: compositeChargingScheduleHigherInterval.end,
371 },
372 )
373 ) {
374 return false;
375 }
376 if (
377 !higherFirst &&
378 isWithinInterval(
379 addSeconds(
380 compositeChargingScheduleLowerInterval.start,
381 schedulePeriod.startPeriod,
382 ),
383 {
384 start: compositeChargingScheduleHigherInterval.start,
385 end: compositeChargingScheduleLowerInterval.end,
386 },
387 )
388 ) {
389 return false;
390 }
391 return true;
392 })
393 .map((schedulePeriod, index) => {
394 if (index === 0 && schedulePeriod.startPeriod !== 0) {
395 schedulePeriod.startPeriod = 0;
396 }
397 return {
398 ...schedulePeriod,
399 startPeriod: higherFirst
400 ? schedulePeriod.startPeriod +
401 differenceInSeconds(
402 compositeChargingScheduleLowerInterval.start,
403 compositeChargingScheduleHigherInterval.start,
404 )
405 : 0,
406 };
407 }),
408 ].sort((a, b) => a.startPeriod - b.startPeriod),
409 };
410 };
411
412 public static hasReservation = (
413 chargingStation: ChargingStation,
414 connectorId: number,
415 idTag: string,
416 ): boolean => {
417 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId);
418 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0);
419 if (
420 (chargingStation.getConnectorStatus(connectorId)?.status ===
421 OCPP16ChargePointStatus.Reserved &&
422 connectorReservation &&
423 !hasReservationExpired(connectorReservation) &&
424 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
425 connectorReservation?.idTag === idTag) ||
426 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
427 chargingStationReservation &&
428 !hasReservationExpired(chargingStationReservation) &&
429 chargingStationReservation?.idTag === idTag)
430 ) {
431 logger.debug(
432 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
433 connectorReservation ?? chargingStationReservation,
434 );
435 return true;
436 }
437 return false;
438 };
439
440 public static parseJsonSchemaFile<T extends JsonType>(
441 relativePath: string,
442 moduleName?: string,
443 methodName?: string,
444 ): JSONSchemaType<T> {
445 return super.parseJsonSchemaFile<T>(
446 relativePath,
447 OCPPVersion.VERSION_16,
448 moduleName,
449 methodName,
450 );
451 }
452
453 private static composeChargingSchedule = (
454 chargingSchedule: OCPP16ChargingSchedule,
455 compositeInterval: Interval,
456 ): OCPP16ChargingSchedule | undefined => {
457 const chargingScheduleInterval: Interval = {
458 start: chargingSchedule.startSchedule!,
459 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
460 };
461 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
462 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
463 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
464 return {
465 ...chargingSchedule,
466 startSchedule: compositeInterval.start as Date,
467 duration: differenceInSeconds(
468 chargingScheduleInterval.end,
469 compositeInterval.start as Date,
470 ),
471 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
472 .filter((schedulePeriod, index) => {
473 if (
474 isWithinInterval(
475 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
476 compositeInterval,
477 )
478 ) {
479 return true;
480 }
481 if (
482 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
483 !isWithinInterval(
484 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
485 compositeInterval,
486 ) &&
487 isWithinInterval(
488 addSeconds(
489 chargingScheduleInterval.start,
490 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
491 ),
492 compositeInterval,
493 )
494 ) {
495 return true;
496 }
497 return false;
498 })
499 .map((schedulePeriod, index) => {
500 if (index === 0 && schedulePeriod.startPeriod !== 0) {
501 schedulePeriod.startPeriod = 0;
502 }
503 return schedulePeriod;
504 }),
505 };
506 }
507 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
508 return {
509 ...chargingSchedule,
510 duration: differenceInSeconds(
511 compositeInterval.end as Date,
512 chargingScheduleInterval.start,
513 ),
514 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
515 isWithinInterval(
516 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
517 compositeInterval,
518 ),
519 ),
520 };
521 }
522 return chargingSchedule;
523 }
524 };
525 }