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