build: switch to NodeNext module resolution
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
c8eeb62b 2
130783a7 3import type { JSONSchemaType } from 'ajv';
ef9e3b33 4import {
f1e3871b 5 type Interval,
ef9e3b33
JB
6 addSeconds,
7 areIntervalsOverlapping,
8 differenceInSeconds,
9 isAfter,
10 isBefore,
11 isWithinInterval,
12} from 'date-fns';
130783a7 13
a6ef1ece 14import { OCPP16Constants } from './OCPP16Constants.js';
90aceaf6
JB
15import {
16 type ChargingStation,
17 hasFeatureProfile,
18 hasReservationExpired,
a6ef1ece 19} from '../../../charging-station/index.js';
e7aeea18 20import {
d19b10a8 21 type GenericResponse,
268a74bb 22 type JsonType,
d19b10a8 23 OCPP16AuthorizationStatus,
366f75f6
JB
24 OCPP16AvailabilityType,
25 type OCPP16ChangeAvailabilityResponse,
26 OCPP16ChargePointStatus,
268a74bb 27 type OCPP16ChargingProfile,
ef9e3b33 28 type OCPP16ChargingSchedule,
41f3983a 29 type OCPP16ClearChargingProfileRequest,
268a74bb 30 type OCPP16IncomingRequestCommand,
27782dbc 31 type OCPP16MeterValue,
41f3983a
JB
32 OCPP16MeterValueContext,
33 OCPP16MeterValueUnit,
370ae4ee 34 OCPP16RequestCommand,
268a74bb 35 OCPP16StandardParametersKey,
d19b10a8 36 OCPP16StopTransactionReason,
268a74bb
JB
37 type OCPP16SupportedFeatureProfiles,
38 OCPPVersion,
a6ef1ece
JB
39} from '../../../types/index.js';
40import { isNotEmptyArray, isNullOrUndefined, logger, roundTo } from '../../../utils/index.js';
41import { OCPPServiceUtils } from '../OCPPServiceUtils.js';
6ed92bc1 42
7bc31f9c 43export class OCPP16ServiceUtils extends OCPPServiceUtils {
370ae4ee
JB
44 public static checkFeatureProfile(
45 chargingStation: ChargingStation,
46 featureProfile: OCPP16SupportedFeatureProfiles,
5edd8ba0 47 command: OCPP16RequestCommand | OCPP16IncomingRequestCommand,
370ae4ee 48 ): boolean {
d8093be1 49 if (!hasFeatureProfile(chargingStation, featureProfile)) {
370ae4ee
JB
50 logger.warn(
51 `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
52 OCPP16StandardParametersKey.SupportedFeatureProfiles
5edd8ba0 53 } in configuration`,
370ae4ee
JB
54 );
55 return false;
56 }
57 return true;
58 }
59
e7aeea18
JB
60 public static buildTransactionBeginMeterValue(
61 chargingStation: ChargingStation,
62 connectorId: number,
5edd8ba0 63 meterStart: number,
e7aeea18 64 ): OCPP16MeterValue {
fd0c36fa 65 const meterValue: OCPP16MeterValue = {
c38f0ced 66 timestamp: new Date(),
fd0c36fa
JB
67 sampledValue: [],
68 };
9ccca265 69 // Energy.Active.Import.Register measurand (default)
ed3d2808 70 const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
492cf6ab 71 chargingStation,
5edd8ba0 72 connectorId,
492cf6ab 73 );
41f3983a
JB
74 const unitDivider =
75 sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1;
e7aeea18
JB
76 meterValue.sampledValue.push(
77 OCPP16ServiceUtils.buildSampledValue(
e1d9a0f4 78 sampledValueTemplate!,
9bf0ef23 79 roundTo((meterStart ?? 0) / unitDivider, 4),
41f3983a 80 OCPP16MeterValueContext.TRANSACTION_BEGIN,
5edd8ba0 81 ),
e7aeea18 82 );
fd0c36fa
JB
83 return meterValue;
84 }
85
e7aeea18
JB
86 public static buildTransactionDataMeterValues(
87 transactionBeginMeterValue: OCPP16MeterValue,
5edd8ba0 88 transactionEndMeterValue: OCPP16MeterValue,
e7aeea18 89 ): OCPP16MeterValue[] {
fd0c36fa
JB
90 const meterValues: OCPP16MeterValue[] = [];
91 meterValues.push(transactionBeginMeterValue);
92 meterValues.push(transactionEndMeterValue);
93 return meterValues;
94 }
7bc31f9c 95
d19b10a8
JB
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
366f75f6
JB
115 public static changeAvailability = async (
116 chargingStation: ChargingStation,
225e32b0 117 connectorIds: number[],
366f75f6
JB
118 chargePointStatus: OCPP16ChargePointStatus,
119 availabilityType: OCPP16AvailabilityType,
120 ): Promise<OCPP16ChangeAvailabilityResponse> => {
225e32b0
JB
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);
366f75f6 138 }
3b0ed034 139 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
225e32b0 140 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
366f75f6 141 }
225e32b0 142 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
366f75f6
JB
143 };
144
ed3d2808
JB
145 public static setChargingProfile(
146 chargingStation: ChargingStation,
147 connectorId: number,
5edd8ba0 148 cp: OCPP16ChargingProfile,
ed3d2808 149 ): void {
9bf0ef23 150 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808 151 logger.error(
5edd8ba0 152 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`,
ed3d2808 153 );
e1d9a0f4 154 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808 155 }
72092cfc
JB
156 if (
157 Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
158 ) {
ed3d2808 159 logger.error(
bbb55ee4 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`,
ed3d2808 161 );
e1d9a0f4 162 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = [];
ed3d2808
JB
163 }
164 let cpReplaced = false;
9bf0ef23 165 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
ed3d2808
JB
166 chargingStation
167 .getConnectorStatus(connectorId)
72092cfc 168 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
ed3d2808
JB
169 if (
170 chargingProfile.chargingProfileId === cp.chargingProfileId ||
171 (chargingProfile.stackLevel === cp.stackLevel &&
172 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
173 ) {
e1d9a0f4 174 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp;
ed3d2808
JB
175 cpReplaced = true;
176 }
177 });
178 }
72092cfc 179 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
ed3d2808
JB
180 }
181
73d87be1
JB
182 public static clearChargingProfiles = (
183 chargingStation: ChargingStation,
41f3983a 184 commandPayload: OCPP16ClearChargingProfileRequest,
73d87be1
JB
185 chargingProfiles: OCPP16ChargingProfile[] | undefined,
186 ): boolean => {
0d1f33ba 187 const { id, chargingProfilePurpose, stackLevel } = commandPayload;
73d87be1
JB
188 let clearedCP = false;
189 if (isNotEmptyArray(chargingProfiles)) {
190 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
191 let clearCurrentCP = false;
0d1f33ba 192 if (chargingProfile.chargingProfileId === id) {
73d87be1
JB
193 clearCurrentCP = true;
194 }
0d1f33ba 195 if (!chargingProfilePurpose && chargingProfile.stackLevel === stackLevel) {
73d87be1
JB
196 clearCurrentCP = true;
197 }
0d1f33ba 198 if (!stackLevel && chargingProfile.chargingProfilePurpose === chargingProfilePurpose) {
73d87be1
JB
199 clearCurrentCP = true;
200 }
201 if (
0d1f33ba
JB
202 chargingProfile.stackLevel === stackLevel &&
203 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
73d87be1
JB
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
ef9e3b33 220 public static composeChargingSchedules = (
4abf6441
JB
221 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
222 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
d632062f 223 compositeInterval: Interval,
ef9e3b33 224 ): OCPP16ChargingSchedule | undefined => {
4abf6441 225 if (!chargingScheduleHigher && !chargingScheduleLower) {
ef9e3b33
JB
226 return undefined;
227 }
4abf6441 228 if (chargingScheduleHigher && !chargingScheduleLower) {
d632062f 229 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval);
ef9e3b33 230 }
4abf6441 231 if (!chargingScheduleHigher && chargingScheduleLower) {
d632062f 232 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval);
ef9e3b33 233 }
4abf6441 234 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
d632062f 235 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval);
4abf6441 236 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
d632062f 237 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval);
4abf6441
JB
238 const compositeChargingScheduleHigherInterval: Interval = {
239 start: compositeChargingScheduleHigher!.startSchedule!,
ef9e3b33 240 end: addSeconds(
4abf6441
JB
241 compositeChargingScheduleHigher!.startSchedule!,
242 compositeChargingScheduleHigher!.duration!,
ef9e3b33
JB
243 ),
244 };
4abf6441
JB
245 const compositeChargingScheduleLowerInterval: Interval = {
246 start: compositeChargingScheduleLower!.startSchedule!,
ef9e3b33 247 end: addSeconds(
4abf6441
JB
248 compositeChargingScheduleLower!.startSchedule!,
249 compositeChargingScheduleLower!.duration!,
ef9e3b33
JB
250 ),
251 };
4abf6441
JB
252 const higherFirst = isBefore(
253 compositeChargingScheduleHigherInterval.start,
254 compositeChargingScheduleLowerInterval.start,
255 );
ef9e3b33
JB
256 if (
257 !areIntervalsOverlapping(
4abf6441
JB
258 compositeChargingScheduleHigherInterval,
259 compositeChargingScheduleLowerInterval,
ef9e3b33
JB
260 )
261 ) {
262 return {
4abf6441
JB
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),
ef9e3b33
JB
303 };
304 }
4abf6441
JB
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
c4ab56ba 334 .filter((schedulePeriod, index) => {
4abf6441
JB
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 }
c4ab56ba
JB
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 ) {
c4ab56ba
JB
374 return false;
375 }
4abf6441
JB
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 })
0e14e1d4
JB
393 .map((schedulePeriod, index) => {
394 if (index === 0 && schedulePeriod.startPeriod !== 0) {
395 schedulePeriod.startPeriod = 0;
396 }
4abf6441
JB
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 };
ef9e3b33
JB
410 };
411
90aceaf6
JB
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 &&
56563a3c 423 !hasReservationExpired(connectorReservation) &&
90aceaf6 424 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
56563a3c 425 connectorReservation?.idTag === idTag) ||
90aceaf6
JB
426 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
427 chargingStationReservation &&
56563a3c
JB
428 !hasReservationExpired(chargingStationReservation) &&
429 chargingStationReservation?.idTag === idTag)
90aceaf6 430 ) {
88499f52
JB
431 logger.debug(
432 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
433 connectorReservation ?? chargingStationReservation,
434 );
56563a3c 435 return true;
90aceaf6 436 }
56563a3c 437 return false;
90aceaf6
JB
438 };
439
1b271a54
JB
440 public static parseJsonSchemaFile<T extends JsonType>(
441 relativePath: string,
442 moduleName?: string,
5edd8ba0 443 methodName?: string,
1b271a54 444 ): JSONSchemaType<T> {
7164966d 445 return super.parseJsonSchemaFile<T>(
51022aa0 446 relativePath,
1b271a54
JB
447 OCPPVersion.VERSION_16,
448 moduleName,
5edd8ba0 449 methodName,
7164966d 450 );
130783a7
JB
451 }
452
ef9e3b33
JB
453 private static composeChargingSchedule = (
454 chargingSchedule: OCPP16ChargingSchedule,
d632062f 455 compositeInterval: Interval,
ef9e3b33
JB
456 ): OCPP16ChargingSchedule | undefined => {
457 const chargingScheduleInterval: Interval = {
458 start: chargingSchedule.startSchedule!,
459 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
460 };
d632062f 461 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
ef9e3b33 462 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod);
d632062f 463 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
ef9e3b33
JB
464 return {
465 ...chargingSchedule,
d632062f
JB
466 startSchedule: compositeInterval.start as Date,
467 duration: differenceInSeconds(
468 chargingScheduleInterval.end,
469 compositeInterval.start as Date,
470 ),
0e14e1d4
JB
471 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
472 .filter((schedulePeriod, index) => {
ef9e3b33
JB
473 if (
474 isWithinInterval(
475 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 476 compositeInterval,
ef9e3b33
JB
477 )
478 ) {
479 return true;
480 }
481 if (
482 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
483 !isWithinInterval(
484 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
d632062f 485 compositeInterval,
ef9e3b33
JB
486 ) &&
487 isWithinInterval(
488 addSeconds(
489 chargingScheduleInterval.start,
490 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod,
491 ),
d632062f 492 compositeInterval,
ef9e3b33
JB
493 )
494 ) {
ef9e3b33
JB
495 return true;
496 }
497 return false;
0e14e1d4
JB
498 })
499 .map((schedulePeriod, index) => {
500 if (index === 0 && schedulePeriod.startPeriod !== 0) {
501 schedulePeriod.startPeriod = 0;
502 }
503 return schedulePeriod;
504 }),
ef9e3b33
JB
505 };
506 }
d632062f 507 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
ef9e3b33
JB
508 return {
509 ...chargingSchedule,
d632062f
JB
510 duration: differenceInSeconds(
511 compositeInterval.end as Date,
512 chargingScheduleInterval.start,
513 ),
ef9e3b33
JB
514 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
515 isWithinInterval(
516 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
d632062f 517 compositeInterval,
ef9e3b33
JB
518 ),
519 ),
520 };
521 }
522 return chargingSchedule;
523 }
524 };
6ed92bc1 525}