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