refactor: remove unneeded eslint-disable
[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 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 { convertToDate, 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 cp.chargingSchedule.startSchedule = convertToDate(cp.chargingSchedule.startSchedule)
168 cp.validFrom = convertToDate(cp.validFrom)
169 cp.validTo = convertToDate(cp.validTo)
170 let cpReplaced = false
171 if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
173 for (const [index, chargingProfile] of chargingStation
174 .getConnectorStatus(connectorId)!
175 .chargingProfiles!.entries()) {
176 if (
177 chargingProfile.chargingProfileId === cp.chargingProfileId ||
178 (chargingProfile.stackLevel === cp.stackLevel &&
179 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
180 ) {
181 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
182 chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp
183 cpReplaced = true
184 }
185 }
186 }
187 !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp)
188 }
189
190 public static clearChargingProfiles = (
191 chargingStation: ChargingStation,
192 commandPayload: OCPP16ClearChargingProfileRequest,
193 chargingProfiles: OCPP16ChargingProfile[] | undefined
194 ): boolean => {
195 const { id, chargingProfilePurpose, stackLevel } = commandPayload
196 let clearedCP = false
197 if (isNotEmptyArray(chargingProfiles)) {
198 chargingProfiles.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
199 let clearCurrentCP = false
200 if (chargingProfile.chargingProfileId === id) {
201 clearCurrentCP = true
202 }
203 if (chargingProfilePurpose == null && chargingProfile.stackLevel === stackLevel) {
204 clearCurrentCP = true
205 }
206 if (
207 stackLevel == null &&
208 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
209 ) {
210 clearCurrentCP = true
211 }
212 if (
213 chargingProfile.stackLevel === stackLevel &&
214 chargingProfile.chargingProfilePurpose === chargingProfilePurpose
215 ) {
216 clearCurrentCP = true
217 }
218 if (clearCurrentCP) {
219 chargingProfiles.splice(index, 1)
220 logger.debug(
221 `${chargingStation.logPrefix()} Matching charging profile(s) cleared: %j`,
222 chargingProfile
223 )
224 clearedCP = true
225 }
226 })
227 }
228 return clearedCP
229 }
230
231 public static composeChargingSchedules = (
232 chargingScheduleHigher: OCPP16ChargingSchedule | undefined,
233 chargingScheduleLower: OCPP16ChargingSchedule | undefined,
234 compositeInterval: Interval
235 ): OCPP16ChargingSchedule | undefined => {
236 if (chargingScheduleHigher == null && chargingScheduleLower == null) {
237 return undefined
238 }
239 if (chargingScheduleHigher != null && chargingScheduleLower == null) {
240 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher, compositeInterval)
241 }
242 if (chargingScheduleHigher == null && chargingScheduleLower != null) {
243 return OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower, compositeInterval)
244 }
245 const compositeChargingScheduleHigher: OCPP16ChargingSchedule | undefined =
246 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
247 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleHigher!, compositeInterval)
248 const compositeChargingScheduleLower: OCPP16ChargingSchedule | undefined =
249 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
250 OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval)
251 const compositeChargingScheduleHigherInterval: Interval = {
252 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
253 start: compositeChargingScheduleHigher!.startSchedule!,
254 end: addSeconds(
255 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
256 compositeChargingScheduleHigher!.startSchedule!,
257 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
258 compositeChargingScheduleHigher!.duration!
259 )
260 }
261 const compositeChargingScheduleLowerInterval: Interval = {
262 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
263 start: compositeChargingScheduleLower!.startSchedule!,
264 end: addSeconds(
265 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
266 compositeChargingScheduleLower!.startSchedule!,
267 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
268 compositeChargingScheduleLower!.duration!
269 )
270 }
271 const higherFirst = isBefore(
272 compositeChargingScheduleHigherInterval.start,
273 compositeChargingScheduleLowerInterval.start
274 )
275 if (
276 !areIntervalsOverlapping(
277 compositeChargingScheduleHigherInterval,
278 compositeChargingScheduleLowerInterval
279 )
280 ) {
281 return {
282 ...compositeChargingScheduleLower,
283 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
284 ...compositeChargingScheduleHigher!,
285 startSchedule: higherFirst
286 ? (compositeChargingScheduleHigherInterval.start as Date)
287 : (compositeChargingScheduleLowerInterval.start as Date),
288 duration: higherFirst
289 ? differenceInSeconds(
290 compositeChargingScheduleLowerInterval.end,
291 compositeChargingScheduleHigherInterval.start
292 )
293 : differenceInSeconds(
294 compositeChargingScheduleHigherInterval.end,
295 compositeChargingScheduleLowerInterval.start
296 ),
297 chargingSchedulePeriod: [
298 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map(schedulePeriod => {
300 return {
301 ...schedulePeriod,
302 startPeriod: higherFirst
303 ? 0
304 : schedulePeriod.startPeriod +
305 differenceInSeconds(
306 compositeChargingScheduleHigherInterval.start,
307 compositeChargingScheduleLowerInterval.start
308 )
309 }
310 }),
311 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
312 ...compositeChargingScheduleLower!.chargingSchedulePeriod.map(schedulePeriod => {
313 return {
314 ...schedulePeriod,
315 startPeriod: higherFirst
316 ? schedulePeriod.startPeriod +
317 differenceInSeconds(
318 compositeChargingScheduleLowerInterval.start,
319 compositeChargingScheduleHigherInterval.start
320 )
321 : 0
322 }
323 })
324 ].sort((a, b) => a.startPeriod - b.startPeriod)
325 }
326 }
327 return {
328 ...compositeChargingScheduleLower,
329 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
330 ...compositeChargingScheduleHigher!,
331 startSchedule: higherFirst
332 ? (compositeChargingScheduleHigherInterval.start as Date)
333 : (compositeChargingScheduleLowerInterval.start as Date),
334 duration: higherFirst
335 ? differenceInSeconds(
336 compositeChargingScheduleLowerInterval.end,
337 compositeChargingScheduleHigherInterval.start
338 )
339 : differenceInSeconds(
340 compositeChargingScheduleHigherInterval.end,
341 compositeChargingScheduleLowerInterval.start
342 ),
343 chargingSchedulePeriod: [
344 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
345 ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map(schedulePeriod => {
346 return {
347 ...schedulePeriod,
348 startPeriod: higherFirst
349 ? 0
350 : schedulePeriod.startPeriod +
351 differenceInSeconds(
352 compositeChargingScheduleHigherInterval.start,
353 compositeChargingScheduleLowerInterval.start
354 )
355 }
356 }),
357 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
358 ...compositeChargingScheduleLower!.chargingSchedulePeriod
359 .filter((schedulePeriod, index) => {
360 if (
361 higherFirst &&
362 isWithinInterval(
363 addSeconds(
364 compositeChargingScheduleLowerInterval.start,
365 schedulePeriod.startPeriod
366 ),
367 {
368 start: compositeChargingScheduleLowerInterval.start,
369 end: compositeChargingScheduleHigherInterval.end
370 }
371 )
372 ) {
373 return false
374 }
375 if (
376 higherFirst &&
377 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
378 index < compositeChargingScheduleLower!.chargingSchedulePeriod.length - 1 &&
379 !isWithinInterval(
380 addSeconds(
381 compositeChargingScheduleLowerInterval.start,
382 schedulePeriod.startPeriod
383 ),
384 {
385 start: compositeChargingScheduleLowerInterval.start,
386 end: compositeChargingScheduleHigherInterval.end
387 }
388 ) &&
389 isWithinInterval(
390 addSeconds(
391 compositeChargingScheduleLowerInterval.start,
392 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
393 compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod
394 ),
395 {
396 start: compositeChargingScheduleLowerInterval.start,
397 end: compositeChargingScheduleHigherInterval.end
398 }
399 )
400 ) {
401 return false
402 }
403 if (
404 !higherFirst &&
405 isWithinInterval(
406 addSeconds(
407 compositeChargingScheduleLowerInterval.start,
408 schedulePeriod.startPeriod
409 ),
410 {
411 start: compositeChargingScheduleHigherInterval.start,
412 end: compositeChargingScheduleLowerInterval.end
413 }
414 )
415 ) {
416 return false
417 }
418 return true
419 })
420 .map((schedulePeriod, index) => {
421 if (index === 0 && schedulePeriod.startPeriod !== 0) {
422 schedulePeriod.startPeriod = 0
423 }
424 return {
425 ...schedulePeriod,
426 startPeriod: higherFirst
427 ? schedulePeriod.startPeriod +
428 differenceInSeconds(
429 compositeChargingScheduleLowerInterval.start,
430 compositeChargingScheduleHigherInterval.start
431 )
432 : 0
433 }
434 })
435 ].sort((a, b) => a.startPeriod - b.startPeriod)
436 }
437 }
438
439 public static isConfigurationKeyVisible (key: ConfigurationKey): boolean {
440 if (key.visible == null) {
441 return true
442 }
443 return key.visible
444 }
445
446 public static hasReservation = (
447 chargingStation: ChargingStation,
448 connectorId: number,
449 idTag: string
450 ): boolean => {
451 const connectorReservation = chargingStation.getReservationBy('connectorId', connectorId)
452 const chargingStationReservation = chargingStation.getReservationBy('connectorId', 0)
453 if (
454 (chargingStation.getConnectorStatus(connectorId)?.status ===
455 OCPP16ChargePointStatus.Reserved &&
456 connectorReservation != null &&
457 !hasReservationExpired(connectorReservation) &&
458 connectorReservation.idTag === idTag) ||
459 (chargingStation.getConnectorStatus(0)?.status === OCPP16ChargePointStatus.Reserved &&
460 chargingStationReservation != null &&
461 !hasReservationExpired(chargingStationReservation) &&
462 chargingStationReservation.idTag === idTag)
463 ) {
464 logger.debug(
465 `${chargingStation.logPrefix()} Connector id ${connectorId} has a valid reservation for idTag ${idTag}: %j`,
466 connectorReservation ?? chargingStationReservation
467 )
468 return true
469 }
470 return false
471 }
472
473 public static parseJsonSchemaFile<T extends JsonType>(
474 relativePath: string,
475 moduleName?: string,
476 methodName?: string
477 ): JSONSchemaType<T> {
478 return super.parseJsonSchemaFile<T>(
479 relativePath,
480 OCPPVersion.VERSION_16,
481 moduleName,
482 methodName
483 )
484 }
485
486 private static readonly composeChargingSchedule = (
487 chargingSchedule: OCPP16ChargingSchedule,
488 compositeInterval: Interval
489 ): OCPP16ChargingSchedule | undefined => {
490 const chargingScheduleInterval: Interval = {
491 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
492 start: chargingSchedule.startSchedule!,
493 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
494 end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
495 }
496 if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
497 chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod)
498 if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
499 return {
500 ...chargingSchedule,
501 startSchedule: compositeInterval.start as Date,
502 duration: differenceInSeconds(
503 chargingScheduleInterval.end,
504 compositeInterval.start as Date
505 ),
506 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod
507 .filter((schedulePeriod, index) => {
508 if (
509 isWithinInterval(
510 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
511 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
512 compositeInterval
513 )
514 ) {
515 return true
516 }
517 if (
518 index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
519 !isWithinInterval(
520 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
521 compositeInterval
522 ) &&
523 isWithinInterval(
524 addSeconds(
525 chargingScheduleInterval.start,
526 chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
527 ),
528 compositeInterval
529 )
530 ) {
531 return true
532 }
533 return false
534 })
535 .map((schedulePeriod, index) => {
536 if (index === 0 && schedulePeriod.startPeriod !== 0) {
537 schedulePeriod.startPeriod = 0
538 }
539 return schedulePeriod
540 })
541 }
542 }
543 if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
544 return {
545 ...chargingSchedule,
546 duration: differenceInSeconds(
547 compositeInterval.end as Date,
548 chargingScheduleInterval.start
549 ),
550 chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(schedulePeriod =>
551 isWithinInterval(
552 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
553 addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod)!,
554 compositeInterval
555 )
556 )
557 }
558 }
559 return chargingSchedule
560 }
561 }
562 }