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