chore: switch coding style to JS standard
[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 type 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 type 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) === false) {
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 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79 sampledValueTemplate!,
80 roundTo((meterStart ?? 0) / unitDivider, 4),
81 OCPP16MeterValueContext.TRANSACTION_BEGIN
82 )
83 )
84 return meterValue
85 }
86
87 public static buildTransactionDataMeterValues (
88 transactionBeginMeterValue: OCPP16MeterValue,
89 transactionEndMeterValue: OCPP16MeterValue
90 ): OCPP16MeterValue[] {
91 const meterValues: OCPP16MeterValue[] = []
92 meterValues.push(transactionBeginMeterValue)
93 meterValues.push(transactionEndMeterValue)
94 return meterValues
95 }
96
97 public static remoteStopTransaction = async (
98 chargingStation: ChargingStation,
99 connectorId: number
100 ): Promise<GenericResponse> => {
101 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
102 chargingStation,
103 connectorId,
104 OCPP16ChargePointStatus.Finishing
105 )
106 const stopResponse = await chargingStation.stopTransactionOnConnector(
107 connectorId,
108 OCPP16StopTransactionReason.REMOTE
109 )
110 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
111 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
112 }
113 return OCPP16Constants.OCPP_RESPONSE_REJECTED
114 }
115
116 public static changeAvailability = async (
117 chargingStation: ChargingStation,
118 connectorIds: number[],
119 chargePointStatus: OCPP16ChargePointStatus,
120 availabilityType: OCPP16AvailabilityType
121 ): Promise<OCPP16ChangeAvailabilityResponse> => {
122 const responses: OCPP16ChangeAvailabilityResponse[] = []
123 for (const connectorId of connectorIds) {
124 let response: OCPP16ChangeAvailabilityResponse =
125 OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
126 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
127 const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
128 if (connectorStatus?.transactionStarted === true) {
129 response = OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
130 }
131 connectorStatus.availability = availabilityType
132 if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
133 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
134 chargingStation,
135 connectorId,
136 chargePointStatus
137 )
138 }
139 responses.push(response)
140 }
141 if (responses.includes(OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED)) {
142 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
143 }
144 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
145 }
146
147 public static setChargingProfile (
148 chargingStation: ChargingStation,
149 connectorId: number,
150 cp: OCPP16ChargingProfile
151 ): void {
152 if (isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
153 logger.error(
154 `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
155 )
156 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
157 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
158 }
159 if (!Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
160 logger.error(
161 `${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`
162 )
163 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
164 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
165 }
166 let cpReplaced = false
167 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
168 chargingStation
169 .getConnectorStatus(connectorId)
170 ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
171 if (
172 chargingProfile.chargingProfileId === cp.chargingProfileId ||
173 (chargingProfile.stackLevel === cp.stackLevel &&
174 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
175 ) {
176 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
177 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp
178 cpReplaced = true
179 }
180 })
181 }
182 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp)
183 }
184
185 public static clearChargingProfiles = (
186 chargingStation: ChargingStation,
187 commandPayload: OCPP16ClearChargingProfileRequest,
188 chargingProfiles: OCPP16ChargingProfile[] | undefined
189 ): boolean => {
190 const { id, chargingProfilePurpose, stackLevel } = commandPayload
191 let clearedCP = false
192 if (isNotEmptyArray(chargingProfiles)) {
193 chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
194 let clearCurrentCP = false
195 if (chargingProfile.chargingProfileId === id) {
196 clearCurrentCP = true
197 }
198 if (chargingProfilePurpose == null && chargingProfile.stackLevel === stackLevel) {
199 clearCurrentCP = true
200 }
201 if (
202 stackLevel == null &&
203 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
204 ) {
205 clearCurrentCP = true
206 }
207 if (
208 chargingProfile.stackLevel === stackLevel &&
209 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
210 ) {
211 clearCurrentCP = true
212 }
213 if (clearCurrentCP) {
214 chargingProfiles.splice(index, 1)
215 logger.debug(
216 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
217 chargingProfile
218 )
219 clearedCP = true
220 }
221 })
222 }
223 return clearedCP
224 }
225
226 public static composeChargingSchedules = (
227 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
228 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
229 compositeInterval: Interval
230 ): OCPP16ChargingSchedule | undefined => {
231 if (chargingScheduleHigher == null && chargingScheduleLower == null) {
232 return undefined
233 }
234 if (chargingScheduleHigher != null && chargingScheduleLower == null) {
235 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval)
236 }
237 if (chargingScheduleHigher == null && chargingScheduleLower != null) {
238 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval)
239 }
240 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
241 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
242 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval)
243 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
244 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
245 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval)
246 const compositeChargingScheduleHigherInterval: Interval = {
247 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
248 start: compositeChargingScheduleHigher!.startSchedule!,
249 end: addSeconds(
250 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
251 compositeChargingScheduleHigher!.startSchedule!,
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 compositeChargingScheduleHigher!.duration!
254 )
255 }
256 const compositeChargingScheduleLowerInterval: Interval = {
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 start: compositeChargingScheduleLower!.startSchedule!,
259 end: addSeconds(
260 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
261 compositeChargingScheduleLower!.startSchedule!,
262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
263 compositeChargingScheduleLower!.duration!
264 )
265 }
266 const higherFirst = isBefore(
267 compositeChargingScheduleHigherInterval.start,
268 compositeChargingScheduleLowerInterval.start
269 )
270 if (
271 !areIntervalsOverlapping(
272 compositeChargingScheduleHigherInterval,
273 compositeChargingScheduleLowerInterval
274 )
275 ) {
276 return {
277 ...compositeChargingScheduleLower,
278 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
279 ...compositeChargingScheduleHigher!,
280 startSchedule: higherFirst
281 ? (compositeChargingScheduleHigherInterval.start as Date)
282 : (compositeChargingScheduleLowerInterval.start as Date),
283 duration: higherFirst
284 ? differenceInSeconds(
285 compositeChargingScheduleLowerInterval.end,
286 compositeChargingScheduleHigherInterval.start
287 )
288 : differenceInSeconds(
289 compositeChargingScheduleHigherInterval.end,
290 compositeChargingScheduleLowerInterval.start
291 ),
292 chargingSchedulePeriod: [
293 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
294 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
295 return {
296 ...schedulePeriod,
297 startPeriod: higherFirst
298 ? 0
299 : schedulePeriod.startPeriod +
300 differenceInSeconds(
301 compositeChargingScheduleHigherInterval.start,
302 compositeChargingScheduleLowerInterval.start
303 )
304 }
305 }),
306 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
307 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map((schedulePeriod) => {
308 return {
309 ...schedulePeriod,
310 startPeriod: higherFirst
311 ? schedulePeriod.startPeriod +
312 differenceInSeconds(
313 compositeChargingScheduleLowerInterval.start,
314 compositeChargingScheduleHigherInterval.start
315 )
316 : 0
317 }
318 })
319 ].sort((a, b) => a.startPeriod - b.startPeriod)
320 }
321 }
322 return {
323 ...compositeChargingScheduleLower,
324 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
325 ...compositeChargingScheduleHigher!,
326 startSchedule: higherFirst
327 ? (compositeChargingScheduleHigherInterval.start as Date)
328 : (compositeChargingScheduleLowerInterval.start as Date),
329 duration: higherFirst
330 ? differenceInSeconds(
331 compositeChargingScheduleLowerInterval.end,
332 compositeChargingScheduleHigherInterval.start
333 )
334 : differenceInSeconds(
335 compositeChargingScheduleHigherInterval.end,
336 compositeChargingScheduleLowerInterval.start
337 ),
338 chargingSchedulePeriod: [
339 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
340 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map((schedulePeriod) => {
341 return {
342 ...schedulePeriod,
343 startPeriod: higherFirst
344 ? 0
345 : schedulePeriod.startPeriod +
346 differenceInSeconds(
347 compositeChargingScheduleHigherInterval.start,
348 compositeChargingScheduleLowerInterval.start
349 )
350 }
351 }),
352 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353 ...compositeChargingScheduleLower!.chargingSchedulePeriod
354 .filter((schedulePeriod, index) => {
355 if (
356 higherFirst &&
357 isWithinInterval(
358 addSeconds(
359 compositeChargingScheduleLowerInterval.start,
360 schedulePeriod.startPeriod
361 ),
362 {
363 start: compositeChargingScheduleLowerInterval.start,
364 end: compositeChargingScheduleHigherInterval.end
365 }
366 )
367 ) {
368 return false
369 }
370 if (
371 higherFirst &&
372 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
373 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
374 !isWithinInterval(
375 addSeconds(
376 compositeChargingScheduleLowerInterval.start,
377 schedulePeriod.startPeriod
378 ),
379 {
380 start: compositeChargingScheduleLowerInterval.start,
381 end: compositeChargingScheduleHigherInterval.end
382 }
383 ) &&
384 isWithinInterval(
385 addSeconds(
386 compositeChargingScheduleLowerInterval.start,
387 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
388 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod
389 ),
390 {
391 start: compositeChargingScheduleLowerInterval.start,
392 end: compositeChargingScheduleHigherInterval.end
393 }
394 )
395 ) {
396 return false
397 }
398 if (
399 !higherFirst &&
400 isWithinInterval(
401 addSeconds(
402 compositeChargingScheduleLowerInterval.start,
403 schedulePeriod.startPeriod
404 ),
405 {
406 start: compositeChargingScheduleHigherInterval.start,
407 end: compositeChargingScheduleLowerInterval.end
408 }
409 )
410 ) {
411 return false
412 }
413 return true
414 })
415 .map((schedulePeriod, index) => {
416 if (index === 0 && schedulePeriod.startPeriod !== 0) {
417 schedulePeriod.startPeriod = 0
418 }
419 return {
420 ...schedulePeriod,
421 startPeriod: higherFirst
422 ? schedulePeriod.startPeriod +
423 differenceInSeconds(
424 compositeChargingScheduleLowerInterval.start,
425 compositeChargingScheduleHigherInterval.start
426 )
427 : 0
428 }
429 })
430 ].sort((a, b) => a.startPeriod - b.startPeriod)
431 }
432 }
433
434 public static hasReservation = (
435 chargingStation: ChargingStation,
436 connectorId: number,
437 idTag: string
438 ): boolean => {
439 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId)
440 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0)
441 if (
442 (chargingStation.getConnectorStatus(connectorId)?.status ===
443 OCPP16ChargePointStatus.Reserved &&
444 connectorReservation != null &&
445 !hasReservationExpired(connectorReservation) &&
446 connectorReservation?.idTag === idTag) ||
447 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
448 chargingStationReservation != null &&
449 !hasReservationExpired(chargingStationReservation) &&
450 chargingStationReservation?.idTag === idTag)
451 ) {
452 logger.debug(
453 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
454 connectorReservation ?? chargingStationReservation
455 )
456 return true
457 }
458 return false
459 }
460
461 public static parseJsonSchemaFile<T extends JsonType>(
462 relativePath: string,
463 moduleName?: string,
464 methodName?: string
465 ): JSONSchemaType<T> {
466 return super.parseJsonSchemaFile<T>(
467 relativePath,
468 OCPPVersion.VERSION_16,
469 moduleName,
470 methodName
471 )
472 }
473
474 private static readonly composeChargingSchedule = (
475 chargingSchedule: OCPP16ChargingSchedule,
476 compositeInterval: Interval
477 ): OCPP16ChargingSchedule | undefined => {
478 const chargingScheduleInterval: Interval = {
479 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
480 start: chargingSchedule.startSchedule!,
481 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
482 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
483 }
484 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
485 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod)
486 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
487 return {
488 ...chargingSchedule,
489 startSchedule: compositeInterval.start as Date,
490 duration: differenceInSeconds(
491 chargingScheduleInterval.end,
492 compositeInterval.start as Date
493 ),
494 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
495 .filter((schedulePeriod, index) => {
496 if (
497 isWithinInterval(
498 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
499 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
500 compositeInterval
501 )
502 ) {
503 return true
504 }
505 if (
506 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
507 !isWithinInterval(
508 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
509 compositeInterval
510 ) &&
511 isWithinInterval(
512 addSeconds(
513 chargingScheduleInterval.start,
514 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
515 ),
516 compositeInterval
517 )
518 ) {
519 return true
520 }
521 return false
522 })
523 .map((schedulePeriod, index) => {
524 if (index === 0 && schedulePeriod.startPeriod !== 0) {
525 schedulePeriod.startPeriod = 0
526 }
527 return schedulePeriod
528 })
529 }
530 }
531 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
532 return {
533 ...chargingSchedule,
534 duration: differenceInSeconds(
535 compositeInterval.end as Date,
536 chargingScheduleInterval.start
537 ),
538 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter((schedulePeriod) =>
539 isWithinInterval(
540 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
541 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
542 compositeInterval
543 )
544 )
545 }
546 }
547 return chargingSchedule
548 }
549 }
550 }