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