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