fix: ensure remote start transaction triggers a delayed start transaction
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import { createWriteStream, readdirSync } from 'node:fs'
4 import { dirname, join, resolve } from 'node:path'
5 import { URL, fileURLToPath } from 'node:url'
6
7 import type { ValidateFunction } from 'ajv'
8 import { Client, type FTPResponse } from 'basic-ftp'
9 import {
10 type Interval,
11 addSeconds,
12 differenceInSeconds,
13 isDate,
14 secondsToMilliseconds
15 } from 'date-fns'
16 import { maxTime } from 'date-fns/constants'
17 import { create } from 'tar'
18
19 import { OCPP16Constants } from './OCPP16Constants.js'
20 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
21 import {
22 type ChargingStation,
23 canProceedChargingProfile,
24 checkChargingStation,
25 getConfigurationKey,
26 getConnectorChargingProfiles,
27 prepareChargingProfileKind,
28 removeExpiredReservations,
29 setConfigurationKeyValue
30 } from '../../../charging-station/index.js'
31 import { OCPPError } from '../../../exception/index.js'
32 import {
33 type ChangeConfigurationRequest,
34 type ChangeConfigurationResponse,
35 ErrorType,
36 type GenericResponse,
37 GenericStatus,
38 type GetConfigurationRequest,
39 type GetConfigurationResponse,
40 type GetDiagnosticsRequest,
41 type GetDiagnosticsResponse,
42 type IncomingRequestHandler,
43 type JsonType,
44 OCPP16AuthorizationStatus,
45 OCPP16AvailabilityType,
46 type OCPP16BootNotificationRequest,
47 type OCPP16BootNotificationResponse,
48 type OCPP16CancelReservationRequest,
49 type OCPP16ChangeAvailabilityRequest,
50 type OCPP16ChangeAvailabilityResponse,
51 OCPP16ChargePointErrorCode,
52 OCPP16ChargePointStatus,
53 type OCPP16ChargingProfile,
54 OCPP16ChargingProfilePurposeType,
55 type OCPP16ChargingSchedule,
56 type OCPP16ClearCacheRequest,
57 type OCPP16ClearChargingProfileRequest,
58 type OCPP16ClearChargingProfileResponse,
59 type OCPP16DataTransferRequest,
60 type OCPP16DataTransferResponse,
61 OCPP16DataTransferVendorId,
62 OCPP16DiagnosticsStatus,
63 type OCPP16DiagnosticsStatusNotificationRequest,
64 type OCPP16DiagnosticsStatusNotificationResponse,
65 OCPP16FirmwareStatus,
66 type OCPP16FirmwareStatusNotificationRequest,
67 type OCPP16FirmwareStatusNotificationResponse,
68 type OCPP16GetCompositeScheduleRequest,
69 type OCPP16GetCompositeScheduleResponse,
70 type OCPP16HeartbeatRequest,
71 type OCPP16HeartbeatResponse,
72 OCPP16IncomingRequestCommand,
73 OCPP16MessageTrigger,
74 OCPP16RequestCommand,
75 type OCPP16ReserveNowRequest,
76 type OCPP16ReserveNowResponse,
77 OCPP16StandardParametersKey,
78 type OCPP16StartTransactionRequest,
79 type OCPP16StartTransactionResponse,
80 type OCPP16StatusNotificationRequest,
81 type OCPP16StatusNotificationResponse,
82 OCPP16StopTransactionReason,
83 OCPP16SupportedFeatureProfiles,
84 type OCPP16TriggerMessageRequest,
85 type OCPP16TriggerMessageResponse,
86 type OCPP16UpdateFirmwareRequest,
87 type OCPP16UpdateFirmwareResponse,
88 type OCPPConfigurationKey,
89 OCPPVersion,
90 type RemoteStartTransactionRequest,
91 type RemoteStopTransactionRequest,
92 ReservationTerminationReason,
93 type ResetRequest,
94 type SetChargingProfileRequest,
95 type SetChargingProfileResponse,
96 type UnlockConnectorRequest,
97 type UnlockConnectorResponse
98 } from '../../../types/index.js'
99 import {
100 Constants,
101 convertToDate,
102 convertToInt,
103 formatDurationMilliSeconds,
104 getRandomInteger,
105 isEmptyArray,
106 isNotEmptyArray,
107 isNotEmptyString,
108 logger,
109 sleep
110 } from '../../../utils/index.js'
111 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
112
113 const moduleName = 'OCPP16IncomingRequestService'
114
115 export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
116 protected payloadValidateFunctions: Map<OCPP16IncomingRequestCommand, ValidateFunction<JsonType>>
117
118 private readonly incomingRequestHandlers: Map<
119 OCPP16IncomingRequestCommand,
120 IncomingRequestHandler
121 >
122
123 public constructor () {
124 // if (new.target.name === moduleName) {
125 // throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
126 // }
127 super(OCPPVersion.VERSION_16)
128 this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
129 [
130 OCPP16IncomingRequestCommand.RESET,
131 this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler
132 ],
133 [
134 OCPP16IncomingRequestCommand.CLEAR_CACHE,
135 this.handleRequestClearCache.bind(this) as IncomingRequestHandler
136 ],
137 [
138 OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
139 this.handleRequestUnlockConnector.bind(this) as unknown as IncomingRequestHandler
140 ],
141 [
142 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
143 this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler
144 ],
145 [
146 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
147 this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler
148 ],
149 [
150 OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
151 this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler
152 ],
153 [
154 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
155 this.handleRequestSetChargingProfile.bind(this) as unknown as IncomingRequestHandler
156 ],
157 [
158 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
159 this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler
160 ],
161 [
162 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
163 this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler
164 ],
165 [
166 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
167 this.handleRequestRemoteStartTransaction.bind(this) as unknown as IncomingRequestHandler
168 ],
169 [
170 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
171 this.handleRequestRemoteStopTransaction.bind(this) as unknown as IncomingRequestHandler
172 ],
173 [
174 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
175 this.handleRequestGetDiagnostics.bind(this) as IncomingRequestHandler
176 ],
177 [
178 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
179 this.handleRequestTriggerMessage.bind(this) as unknown as IncomingRequestHandler
180 ],
181 [
182 OCPP16IncomingRequestCommand.DATA_TRANSFER,
183 this.handleRequestDataTransfer.bind(this) as unknown as IncomingRequestHandler
184 ],
185 [
186 OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
187 this.handleRequestUpdateFirmware.bind(this) as unknown as IncomingRequestHandler
188 ],
189 [
190 OCPP16IncomingRequestCommand.RESERVE_NOW,
191 this.handleRequestReserveNow.bind(this) as unknown as IncomingRequestHandler
192 ],
193 [
194 OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
195 this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler
196 ]
197 ])
198 this.payloadValidateFunctions = new Map<
199 OCPP16IncomingRequestCommand,
200 ValidateFunction<JsonType>
201 >([
202 [
203 OCPP16IncomingRequestCommand.RESET,
204 this.ajv
205 .compile(
206 OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
207 'assets/json-schemas/ocpp/1.6/Reset.json',
208 moduleName,
209 'constructor'
210 )
211 )
212 .bind(this)
213 ],
214 [
215 OCPP16IncomingRequestCommand.CLEAR_CACHE,
216 this.ajv
217 .compile(
218 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
219 'assets/json-schemas/ocpp/1.6/ClearCache.json',
220 moduleName,
221 'constructor'
222 )
223 )
224 .bind(this)
225 ],
226 [
227 OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
228 this.ajv
229 .compile(
230 OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
231 'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
232 moduleName,
233 'constructor'
234 )
235 )
236 .bind(this)
237 ],
238 [
239 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
240 this.ajv
241 .compile(
242 OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
243 'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
244 moduleName,
245 'constructor'
246 )
247 )
248 .bind(this)
249 ],
250 [
251 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
252 this.ajv
253 .compile(
254 OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
255 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
256 moduleName,
257 'constructor'
258 )
259 )
260 .bind(this)
261 ],
262 [
263 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
264 this.ajv
265 .compile(
266 OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
267 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
268 moduleName,
269 'constructor'
270 )
271 )
272 .bind(this)
273 ],
274 [
275 OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
276 this.ajv
277 .compile(
278 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16GetCompositeScheduleRequest>(
279 'assets/json-schemas/ocpp/1.6/GetCompositeSchedule.json',
280 moduleName,
281 'constructor'
282 )
283 )
284 .bind(this)
285 ],
286 [
287 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
288 this.ajv
289 .compile(
290 OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
291 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
292 moduleName,
293 'constructor'
294 )
295 )
296 .bind(this)
297 ],
298 [
299 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
300 this.ajv
301 .compile(
302 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileRequest>(
303 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
304 moduleName,
305 'constructor'
306 )
307 )
308 .bind(this)
309 ],
310 [
311 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
312 this.ajv
313 .compile(
314 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityRequest>(
315 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
316 moduleName,
317 'constructor'
318 )
319 )
320 .bind(this)
321 ],
322 [
323 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
324 this.ajv
325 .compile(
326 OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
327 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
328 moduleName,
329 'constructor'
330 )
331 )
332 .bind(this)
333 ],
334 [
335 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
336 this.ajv
337 .compile(
338 OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
339 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
340 moduleName,
341 'constructor'
342 )
343 )
344 .bind(this)
345 ],
346 [
347 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
348 this.ajv
349 .compile(
350 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
351 'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
352 moduleName,
353 'constructor'
354 )
355 )
356 .bind(this)
357 ],
358 [
359 OCPP16IncomingRequestCommand.DATA_TRANSFER,
360 this.ajv
361 .compile(
362 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
363 'assets/json-schemas/ocpp/1.6/DataTransfer.json',
364 moduleName,
365 'constructor'
366 )
367 )
368 .bind(this)
369 ],
370 [
371 OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
372 this.ajv
373 .compile(
374 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
375 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
376 moduleName,
377 'constructor'
378 )
379 )
380 .bind(this)
381 ],
382 [
383 OCPP16IncomingRequestCommand.RESERVE_NOW,
384 this.ajv
385 .compile(
386 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
387 'assets/json-schemas/ocpp/1.6/ReserveNow.json',
388 moduleName,
389 'constructor'
390 )
391 )
392 .bind(this)
393 ],
394 [
395 OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
396 this.ajv
397 .compile(
398 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
399 'assets/json-schemas/ocpp/1.6/CancelReservation.json',
400 moduleName,
401 'constructor'
402 )
403 )
404 .bind(this)
405 ]
406 ])
407 this.on(
408 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
409 (chargingStation: ChargingStation, connectorId: number, idTag: string) => {
410 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
411 chargingStation.getConnectorStatus(connectorId)!.transactionRemoteStarted = true
412 chargingStation.ocppRequestService
413 .requestHandler<OCPP16StartTransactionRequest, OCPP16StartTransactionResponse>(
414 chargingStation,
415 OCPP16RequestCommand.START_TRANSACTION,
416 {
417 connectorId,
418 idTag
419 }
420 )
421 .then(response => {
422 if (response.status === OCPP16AuthorizationStatus.ACCEPTED) {
423 logger.debug(`
424 ${chargingStation.logPrefix()} Remote start transaction ACCEPTED on ${
425 chargingStation.stationInfo?.chargingStationId
426 }#${connectorId} for idTag '${idTag}'`)
427 } else {
428 logger.debug(`
429 ${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
430 chargingStation.stationInfo?.chargingStationId
431 }#${connectorId} for idTag '${idTag}'`)
432 }
433 })
434 .catch(error => {
435 logger.error(
436 `${chargingStation.logPrefix()} ${moduleName}.constructor: Remote start transaction error:`,
437 error
438 )
439 })
440 }
441 )
442 this.validatePayload = this.validatePayload.bind(this)
443 }
444
445 public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
446 chargingStation: ChargingStation,
447 messageId: string,
448 commandName: OCPP16IncomingRequestCommand,
449 commandPayload: ReqType
450 ): Promise<void> {
451 let response: ResType
452 if (
453 chargingStation.stationInfo?.ocppStrictCompliance === true &&
454 chargingStation.inPendingState() &&
455 (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
456 commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
457 ) {
458 throw new OCPPError(
459 ErrorType.SECURITY_ERROR,
460 `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
461 commandPayload,
462 undefined,
463 2
464 )} while the charging station is in pending state on the central server`,
465 commandName,
466 commandPayload
467 )
468 }
469 if (
470 chargingStation.isRegistered() ||
471 (chargingStation.stationInfo?.ocppStrictCompliance === false &&
472 chargingStation.inUnknownState())
473 ) {
474 if (
475 this.incomingRequestHandlers.has(commandName) &&
476 OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
477 ) {
478 try {
479 this.validatePayload(chargingStation, commandName, commandPayload)
480 // Call the method to build the response
481 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
482 response = (await this.incomingRequestHandlers.get(commandName)!(
483 chargingStation,
484 commandPayload
485 )) as ResType
486 } catch (error) {
487 // Log
488 logger.error(
489 `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
490 error
491 )
492 throw error
493 }
494 } else {
495 // Throw exception
496 throw new OCPPError(
497 ErrorType.NOT_IMPLEMENTED,
498 `${commandName} is not implemented to handle request PDU ${JSON.stringify(
499 commandPayload,
500 undefined,
501 2
502 )}`,
503 commandName,
504 commandPayload
505 )
506 }
507 } else {
508 throw new OCPPError(
509 ErrorType.SECURITY_ERROR,
510 `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
511 commandPayload,
512 undefined,
513 2
514 )} while the charging station is not registered on the central server.`,
515 commandName,
516 commandPayload
517 )
518 }
519 // Send the built response
520 await chargingStation.ocppRequestService.sendResponse(
521 chargingStation,
522 messageId,
523 response,
524 commandName
525 )
526 }
527
528 private validatePayload (
529 chargingStation: ChargingStation,
530 commandName: OCPP16IncomingRequestCommand,
531 commandPayload: JsonType
532 ): boolean {
533 if (this.payloadValidateFunctions.has(commandName)) {
534 return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
535 }
536 logger.warn(
537 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
538 )
539 return false
540 }
541
542 // Simulate charging station restart
543 private handleRequestReset (
544 chargingStation: ChargingStation,
545 commandPayload: ResetRequest
546 ): GenericResponse {
547 const { type } = commandPayload
548 chargingStation
549 .reset(`${type}Reset` as OCPP16StopTransactionReason)
550 .catch(Constants.EMPTY_FUNCTION)
551 logger.info(
552 `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
553 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
554 chargingStation.stationInfo!.resetTime!
555 )}`
556 )
557 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
558 }
559
560 private async handleRequestUnlockConnector (
561 chargingStation: ChargingStation,
562 commandPayload: UnlockConnectorRequest
563 ): Promise<UnlockConnectorResponse> {
564 const { connectorId } = commandPayload
565 if (!chargingStation.hasConnector(connectorId)) {
566 logger.error(
567 `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId}`
568 )
569 return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
570 }
571 if (connectorId === 0) {
572 logger.error(`${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId}`)
573 return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
574 }
575 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
576 const stopResponse = await chargingStation.stopTransactionOnConnector(
577 connectorId,
578 OCPP16StopTransactionReason.UNLOCK_COMMAND
579 )
580 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
581 return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
582 }
583 return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
584 }
585 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
586 chargingStation,
587 connectorId,
588 OCPP16ChargePointStatus.Available
589 )
590 return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
591 }
592
593 private handleRequestGetConfiguration (
594 chargingStation: ChargingStation,
595 commandPayload: GetConfigurationRequest
596 ): GetConfigurationResponse {
597 const { key } = commandPayload
598 const configurationKey: OCPPConfigurationKey[] = []
599 const unknownKey: string[] = []
600 if (key == null) {
601 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
602 for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) {
603 if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) {
604 continue
605 }
606 configurationKey.push({
607 key: configKey.key,
608 readonly: configKey.readonly,
609 value: configKey.value
610 })
611 }
612 } else if (isNotEmptyArray(key)) {
613 for (const k of key) {
614 const keyFound = getConfigurationKey(chargingStation, k, true)
615 if (keyFound != null) {
616 if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) {
617 continue
618 }
619 configurationKey.push({
620 key: keyFound.key,
621 readonly: keyFound.readonly,
622 value: keyFound.value
623 })
624 } else {
625 unknownKey.push(k)
626 }
627 }
628 }
629 return {
630 configurationKey,
631 unknownKey
632 }
633 }
634
635 private handleRequestChangeConfiguration (
636 chargingStation: ChargingStation,
637 commandPayload: ChangeConfigurationRequest
638 ): ChangeConfigurationResponse {
639 const { key, value } = commandPayload
640 const keyToChange = getConfigurationKey(chargingStation, key, true)
641 if (keyToChange?.readonly === true) {
642 return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED
643 } else if (keyToChange?.readonly === false) {
644 let valueChanged = false
645 if (keyToChange.value !== value) {
646 setConfigurationKeyValue(chargingStation, key, value, true)
647 valueChanged = true
648 }
649 let triggerHeartbeatRestart = false
650 if (
651 (keyToChange.key as OCPP16StandardParametersKey) ===
652 OCPP16StandardParametersKey.HeartBeatInterval &&
653 valueChanged
654 ) {
655 setConfigurationKeyValue(
656 chargingStation,
657 OCPP16StandardParametersKey.HeartbeatInterval,
658 value
659 )
660 triggerHeartbeatRestart = true
661 }
662 if (
663 (keyToChange.key as OCPP16StandardParametersKey) ===
664 OCPP16StandardParametersKey.HeartbeatInterval &&
665 valueChanged
666 ) {
667 setConfigurationKeyValue(
668 chargingStation,
669 OCPP16StandardParametersKey.HeartBeatInterval,
670 value
671 )
672 triggerHeartbeatRestart = true
673 }
674 if (triggerHeartbeatRestart) {
675 chargingStation.restartHeartbeat()
676 }
677 if (
678 (keyToChange.key as OCPP16StandardParametersKey) ===
679 OCPP16StandardParametersKey.WebSocketPingInterval &&
680 valueChanged
681 ) {
682 chargingStation.restartWebSocketPing()
683 }
684 if (keyToChange.reboot === true) {
685 return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED
686 }
687 return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED
688 }
689 return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED
690 }
691
692 private handleRequestSetChargingProfile (
693 chargingStation: ChargingStation,
694 commandPayload: SetChargingProfileRequest
695 ): SetChargingProfileResponse {
696 if (
697 !OCPP16ServiceUtils.checkFeatureProfile(
698 chargingStation,
699 OCPP16SupportedFeatureProfiles.SmartCharging,
700 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
701 )
702 ) {
703 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED
704 }
705 const { connectorId, csChargingProfiles } = commandPayload
706 if (!chargingStation.hasConnector(connectorId)) {
707 logger.error(
708 `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId}`
709 )
710 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
711 }
712 if (
713 csChargingProfiles.chargingProfilePurpose ===
714 OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
715 connectorId !== 0
716 ) {
717 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
718 }
719 if (
720 csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
721 connectorId === 0
722 ) {
723 logger.error(
724 `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId}`
725 )
726 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
727 }
728 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
729 if (
730 csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
731 connectorId > 0 &&
732 connectorStatus?.transactionStarted === false
733 ) {
734 logger.error(
735 `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId} without a started transaction`
736 )
737 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
738 }
739 if (
740 csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
741 connectorId > 0 &&
742 connectorStatus?.transactionStarted === true &&
743 csChargingProfiles.transactionId !== connectorStatus.transactionId
744 ) {
745 logger.error(
746 `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId} with a different transaction id ${
747 csChargingProfiles.transactionId
748 } than the started transaction id ${connectorStatus.transactionId}`
749 )
750 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
751 }
752 OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles)
753 logger.debug(
754 `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId}: %j`,
755 csChargingProfiles
756 )
757 return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED
758 }
759
760 private handleRequestGetCompositeSchedule (
761 chargingStation: ChargingStation,
762 commandPayload: OCPP16GetCompositeScheduleRequest
763 ): OCPP16GetCompositeScheduleResponse {
764 if (
765 !OCPP16ServiceUtils.checkFeatureProfile(
766 chargingStation,
767 OCPP16SupportedFeatureProfiles.SmartCharging,
768 OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE
769 )
770 ) {
771 return OCPP16Constants.OCPP_RESPONSE_REJECTED
772 }
773 const { connectorId, duration, chargingRateUnit } = commandPayload
774 if (!chargingStation.hasConnector(connectorId)) {
775 logger.error(
776 `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${connectorId}`
777 )
778 return OCPP16Constants.OCPP_RESPONSE_REJECTED
779 }
780 if (connectorId === 0) {
781 logger.error(
782 `${chargingStation.logPrefix()} Get composite schedule on connector id ${connectorId} is not yet supported`
783 )
784 return OCPP16Constants.OCPP_RESPONSE_REJECTED
785 }
786 if (chargingRateUnit != null) {
787 logger.warn(
788 `${chargingStation.logPrefix()} Get composite schedule with a specified rate unit is not yet supported, no conversion will be done`
789 )
790 }
791 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
792 if (
793 isEmptyArray(connectorStatus?.chargingProfiles) &&
794 isEmptyArray(chargingStation.getConnectorStatus(0)?.chargingProfiles)
795 ) {
796 return OCPP16Constants.OCPP_RESPONSE_REJECTED
797 }
798 const currentDate = new Date()
799 const compositeScheduleInterval: Interval = {
800 start: currentDate,
801 end: addSeconds(currentDate, duration)
802 }
803 // Get charging profiles sorted by connector id then stack level
804 const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
805 chargingStation,
806 connectorId
807 )
808 let previousCompositeSchedule: OCPP16ChargingSchedule | undefined
809 let compositeSchedule: OCPP16ChargingSchedule | undefined
810 for (const chargingProfile of chargingProfiles) {
811 if (chargingProfile.chargingSchedule.startSchedule == null) {
812 logger.debug(
813 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
814 chargingProfile.chargingProfileId
815 } has no startSchedule defined. Trying to set it to the connector current transaction start date`
816 )
817 // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
818 chargingProfile.chargingSchedule.startSchedule = connectorStatus?.transactionStart
819 }
820 if (!isDate(chargingProfile.chargingSchedule.startSchedule)) {
821 logger.warn(
822 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
823 chargingProfile.chargingProfileId
824 } startSchedule property is not a Date instance. Trying to convert it to a Date instance`
825 )
826 chargingProfile.chargingSchedule.startSchedule = convertToDate(
827 chargingProfile.chargingSchedule.startSchedule
828 )
829 }
830 if (chargingProfile.chargingSchedule.duration == null) {
831 logger.debug(
832 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetCompositeSchedule: Charging profile id ${
833 chargingProfile.chargingProfileId
834 } has no duration defined and will be set to the maximum time allowed`
835 )
836 // OCPP specifies that if duration is not defined, it should be infinite
837 chargingProfile.chargingSchedule.duration = differenceInSeconds(
838 maxTime,
839 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
840 chargingProfile.chargingSchedule.startSchedule!
841 )
842 }
843 if (
844 !prepareChargingProfileKind(
845 connectorStatus,
846 chargingProfile,
847 compositeScheduleInterval.start,
848 chargingStation.logPrefix()
849 )
850 ) {
851 continue
852 }
853 if (
854 !canProceedChargingProfile(
855 chargingProfile,
856 compositeScheduleInterval.start,
857 chargingStation.logPrefix()
858 )
859 ) {
860 continue
861 }
862 compositeSchedule = OCPP16ServiceUtils.composeChargingSchedules(
863 previousCompositeSchedule,
864 chargingProfile.chargingSchedule,
865 compositeScheduleInterval
866 )
867 previousCompositeSchedule = compositeSchedule
868 }
869 if (compositeSchedule != null) {
870 return {
871 status: GenericStatus.Accepted,
872 scheduleStart: compositeSchedule.startSchedule,
873 connectorId,
874 chargingSchedule: compositeSchedule
875 }
876 }
877 return OCPP16Constants.OCPP_RESPONSE_REJECTED
878 }
879
880 private handleRequestClearChargingProfile (
881 chargingStation: ChargingStation,
882 commandPayload: OCPP16ClearChargingProfileRequest
883 ): OCPP16ClearChargingProfileResponse {
884 if (
885 !OCPP16ServiceUtils.checkFeatureProfile(
886 chargingStation,
887 OCPP16SupportedFeatureProfiles.SmartCharging,
888 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
889 )
890 ) {
891 return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
892 }
893 const { connectorId } = commandPayload
894 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
895 if (!chargingStation.hasConnector(connectorId!)) {
896 logger.error(
897 `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId}`
898 )
899 return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
900 }
901 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
902 const connectorStatus = chargingStation.getConnectorStatus(connectorId!)
903 if (connectorId != null && isNotEmptyArray(connectorStatus?.chargingProfiles)) {
904 connectorStatus.chargingProfiles = []
905 logger.debug(
906 `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId}`
907 )
908 return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
909 }
910 if (connectorId == null) {
911 let clearedCP = false
912 if (chargingStation.hasEvses) {
913 for (const evseStatus of chargingStation.evses.values()) {
914 for (const status of evseStatus.connectors.values()) {
915 clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
916 chargingStation,
917 commandPayload,
918 status.chargingProfiles
919 )
920 }
921 }
922 } else {
923 for (const id of chargingStation.connectors.keys()) {
924 clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
925 chargingStation,
926 commandPayload,
927 chargingStation.getConnectorStatus(id)?.chargingProfiles
928 )
929 }
930 }
931 if (clearedCP) {
932 return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
933 }
934 }
935 return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
936 }
937
938 private async handleRequestChangeAvailability (
939 chargingStation: ChargingStation,
940 commandPayload: OCPP16ChangeAvailabilityRequest
941 ): Promise<OCPP16ChangeAvailabilityResponse> {
942 const { connectorId, type } = commandPayload
943 if (!chargingStation.hasConnector(connectorId)) {
944 logger.error(
945 `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId}`
946 )
947 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
948 }
949 const chargePointStatus: OCPP16ChargePointStatus =
950 type === OCPP16AvailabilityType.Operative
951 ? OCPP16ChargePointStatus.Available
952 : OCPP16ChargePointStatus.Unavailable
953 if (connectorId === 0) {
954 let response: OCPP16ChangeAvailabilityResponse | undefined
955 if (chargingStation.hasEvses) {
956 for (const evseStatus of chargingStation.evses.values()) {
957 response = await OCPP16ServiceUtils.changeAvailability(
958 chargingStation,
959 [...evseStatus.connectors.keys()],
960 chargePointStatus,
961 type
962 )
963 }
964 } else {
965 response = await OCPP16ServiceUtils.changeAvailability(
966 chargingStation,
967 [...chargingStation.connectors.keys()],
968 chargePointStatus,
969 type
970 )
971 }
972 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
973 return response!
974 } else if (
975 connectorId > 0 &&
976 (chargingStation.isChargingStationAvailable() ||
977 (!chargingStation.isChargingStationAvailable() &&
978 type === OCPP16AvailabilityType.Inoperative))
979 ) {
980 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
981 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
982 chargingStation.getConnectorStatus(connectorId)!.availability = type
983 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
984 }
985 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
986 chargingStation.getConnectorStatus(connectorId)!.availability = type
987 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
988 chargingStation,
989 connectorId,
990 chargePointStatus
991 )
992 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
993 }
994 return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
995 }
996
997 private async handleRequestRemoteStartTransaction (
998 chargingStation: ChargingStation,
999 commandPayload: RemoteStartTransactionRequest
1000 ): Promise<GenericResponse> {
1001 const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload
1002 if (!chargingStation.hasConnector(transactionConnectorId)) {
1003 return await this.notifyRemoteStartTransactionRejected(
1004 chargingStation,
1005 transactionConnectorId,
1006 idTag
1007 )
1008 }
1009 if (
1010 !chargingStation.isChargingStationAvailable() ||
1011 !chargingStation.isConnectorAvailable(transactionConnectorId)
1012 ) {
1013 return await this.notifyRemoteStartTransactionRejected(
1014 chargingStation,
1015 transactionConnectorId,
1016 idTag
1017 )
1018 }
1019 // idTag authorization check required
1020 if (
1021 chargingStation.getAuthorizeRemoteTxRequests() &&
1022 !(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, transactionConnectorId, idTag))
1023 ) {
1024 return await this.notifyRemoteStartTransactionRejected(
1025 chargingStation,
1026 transactionConnectorId,
1027 idTag
1028 )
1029 }
1030 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1031 chargingStation,
1032 transactionConnectorId,
1033 OCPP16ChargePointStatus.Preparing
1034 )
1035 if (
1036 chargingProfile != null &&
1037 !this.setRemoteStartTransactionChargingProfile(
1038 chargingStation,
1039 transactionConnectorId,
1040 chargingProfile
1041 )
1042 ) {
1043 return await this.notifyRemoteStartTransactionRejected(
1044 chargingStation,
1045 transactionConnectorId,
1046 idTag
1047 )
1048 }
1049 this.emit(
1050 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
1051 chargingStation,
1052 transactionConnectorId,
1053 idTag
1054 )
1055 return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
1056 }
1057
1058 private async notifyRemoteStartTransactionRejected (
1059 chargingStation: ChargingStation,
1060 connectorId: number,
1061 idTag: string
1062 ): Promise<GenericResponse> {
1063 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
1064 if (connectorStatus?.status !== OCPP16ChargePointStatus.Available) {
1065 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1066 chargingStation,
1067 connectorId,
1068 OCPP16ChargePointStatus.Available
1069 )
1070 }
1071 logger.warn(
1072 `${chargingStation.logPrefix()} Remote start transaction REJECTED on connector id ${connectorId}, idTag '${idTag}', availability '${connectorStatus?.availability}', status '${connectorStatus?.status}'`
1073 )
1074 return OCPP16Constants.OCPP_RESPONSE_REJECTED
1075 }
1076
1077 private setRemoteStartTransactionChargingProfile (
1078 chargingStation: ChargingStation,
1079 connectorId: number,
1080 chargingProfile: OCPP16ChargingProfile
1081 ): boolean {
1082 if (chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE) {
1083 OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile)
1084 logger.debug(
1085 `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}: %j`,
1086 chargingProfile
1087 )
1088 return true
1089 }
1090 logger.warn(
1091 `${chargingStation.logPrefix()} Not allowed to set ${
1092 chargingProfile.chargingProfilePurpose
1093 } charging profile(s) at remote start transaction`
1094 )
1095 return false
1096 }
1097
1098 private async handleRequestRemoteStopTransaction (
1099 chargingStation: ChargingStation,
1100 commandPayload: RemoteStopTransactionRequest
1101 ): Promise<GenericResponse> {
1102 const { transactionId } = commandPayload
1103 if (chargingStation.hasEvses) {
1104 for (const [evseId, evseStatus] of chargingStation.evses) {
1105 if (evseId > 0) {
1106 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1107 if (connectorStatus.transactionId === transactionId) {
1108 return await OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId)
1109 }
1110 }
1111 }
1112 }
1113 } else {
1114 for (const connectorId of chargingStation.connectors.keys()) {
1115 if (
1116 connectorId > 0 &&
1117 chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId
1118 ) {
1119 return await OCPP16ServiceUtils.remoteStopTransaction(chargingStation, connectorId)
1120 }
1121 }
1122 }
1123 logger.warn(
1124 `${chargingStation.logPrefix()} Trying to remote stop a non existing transaction with id ${transactionId}`
1125 )
1126 return OCPP16Constants.OCPP_RESPONSE_REJECTED
1127 }
1128
1129 private handleRequestUpdateFirmware (
1130 chargingStation: ChargingStation,
1131 commandPayload: OCPP16UpdateFirmwareRequest
1132 ): OCPP16UpdateFirmwareResponse {
1133 if (
1134 !OCPP16ServiceUtils.checkFeatureProfile(
1135 chargingStation,
1136 OCPP16SupportedFeatureProfiles.FirmwareManagement,
1137 OCPP16IncomingRequestCommand.UPDATE_FIRMWARE
1138 )
1139 ) {
1140 logger.warn(
1141 `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
1142 )
1143 return OCPP16Constants.OCPP_RESPONSE_EMPTY
1144 }
1145 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1146 commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
1147 const { retrieveDate } = commandPayload
1148 if (chargingStation.stationInfo?.firmwareStatus !== OCPP16FirmwareStatus.Installed) {
1149 logger.warn(
1150 `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
1151 )
1152 return OCPP16Constants.OCPP_RESPONSE_EMPTY
1153 }
1154 const now = Date.now()
1155 if (retrieveDate.getTime() <= now) {
1156 this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
1157 } else {
1158 setTimeout(() => {
1159 this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
1160 }, retrieveDate.getTime() - now)
1161 }
1162 return OCPP16Constants.OCPP_RESPONSE_EMPTY
1163 }
1164
1165 private async updateFirmwareSimulation (
1166 chargingStation: ChargingStation,
1167 maxDelay = 30,
1168 minDelay = 15
1169 ): Promise<void> {
1170 if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) {
1171 return
1172 }
1173 if (chargingStation.hasEvses) {
1174 for (const [evseId, evseStatus] of chargingStation.evses) {
1175 if (evseId > 0) {
1176 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1177 if (connectorStatus.transactionStarted === false) {
1178 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1179 chargingStation,
1180 connectorId,
1181 OCPP16ChargePointStatus.Unavailable
1182 )
1183 }
1184 }
1185 }
1186 }
1187 } else {
1188 for (const connectorId of chargingStation.connectors.keys()) {
1189 if (
1190 connectorId > 0 &&
1191 chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
1192 ) {
1193 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1194 chargingStation,
1195 connectorId,
1196 OCPP16ChargePointStatus.Unavailable
1197 )
1198 }
1199 }
1200 }
1201 await chargingStation.ocppRequestService.requestHandler<
1202 OCPP16FirmwareStatusNotificationRequest,
1203 OCPP16FirmwareStatusNotificationResponse
1204 >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1205 status: OCPP16FirmwareStatus.Downloading
1206 })
1207 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1208 chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading
1209 if (
1210 chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
1211 OCPP16FirmwareStatus.DownloadFailed
1212 ) {
1213 await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
1214 await chargingStation.ocppRequestService.requestHandler<
1215 OCPP16FirmwareStatusNotificationRequest,
1216 OCPP16FirmwareStatusNotificationResponse
1217 >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1218 status: chargingStation.stationInfo.firmwareUpgrade.failureStatus
1219 })
1220 chargingStation.stationInfo.firmwareStatus =
1221 chargingStation.stationInfo.firmwareUpgrade.failureStatus
1222 return
1223 }
1224 await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
1225 await chargingStation.ocppRequestService.requestHandler<
1226 OCPP16FirmwareStatusNotificationRequest,
1227 OCPP16FirmwareStatusNotificationResponse
1228 >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1229 status: OCPP16FirmwareStatus.Downloaded
1230 })
1231 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1232 chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded
1233 let wasTransactionsStarted = false
1234 let transactionsStarted: boolean
1235 do {
1236 const runningTransactions = chargingStation.getNumberOfRunningTransactions()
1237 if (runningTransactions > 0) {
1238 const waitTime = secondsToMilliseconds(15)
1239 logger.debug(
1240 `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions} transaction(s) in progress, waiting ${formatDurationMilliSeconds(
1241 waitTime
1242 )} before continuing firmware update simulation`
1243 )
1244 await sleep(waitTime)
1245 transactionsStarted = true
1246 wasTransactionsStarted = true
1247 } else {
1248 if (chargingStation.hasEvses) {
1249 for (const [evseId, evseStatus] of chargingStation.evses) {
1250 if (evseId > 0) {
1251 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
1252 if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
1253 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1254 chargingStation,
1255 connectorId,
1256 OCPP16ChargePointStatus.Unavailable
1257 )
1258 }
1259 }
1260 }
1261 }
1262 } else {
1263 for (const connectorId of chargingStation.connectors.keys()) {
1264 if (
1265 connectorId > 0 &&
1266 chargingStation.getConnectorStatus(connectorId)?.status !==
1267 OCPP16ChargePointStatus.Unavailable
1268 ) {
1269 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
1270 chargingStation,
1271 connectorId,
1272 OCPP16ChargePointStatus.Unavailable
1273 )
1274 }
1275 }
1276 }
1277 transactionsStarted = false
1278 }
1279 } while (transactionsStarted)
1280 !wasTransactionsStarted &&
1281 (await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay))))
1282 if (!checkChargingStation(chargingStation, chargingStation.logPrefix())) {
1283 return
1284 }
1285 await chargingStation.ocppRequestService.requestHandler<
1286 OCPP16FirmwareStatusNotificationRequest,
1287 OCPP16FirmwareStatusNotificationResponse
1288 >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1289 status: OCPP16FirmwareStatus.Installing
1290 })
1291 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1292 chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing
1293 if (
1294 chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
1295 OCPP16FirmwareStatus.InstallationFailed
1296 ) {
1297 await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
1298 await chargingStation.ocppRequestService.requestHandler<
1299 OCPP16FirmwareStatusNotificationRequest,
1300 OCPP16FirmwareStatusNotificationResponse
1301 >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1302 status: chargingStation.stationInfo.firmwareUpgrade.failureStatus
1303 })
1304 chargingStation.stationInfo.firmwareStatus =
1305 chargingStation.stationInfo.firmwareUpgrade.failureStatus
1306 return
1307 }
1308 if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
1309 await sleep(secondsToMilliseconds(getRandomInteger(maxDelay, minDelay)))
1310 await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
1311 }
1312 }
1313
1314 private async handleRequestGetDiagnostics (
1315 chargingStation: ChargingStation,
1316 commandPayload: GetDiagnosticsRequest
1317 ): Promise<GetDiagnosticsResponse> {
1318 if (
1319 !OCPP16ServiceUtils.checkFeatureProfile(
1320 chargingStation,
1321 OCPP16SupportedFeatureProfiles.FirmwareManagement,
1322 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
1323 )
1324 ) {
1325 logger.warn(
1326 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
1327 )
1328 return OCPP16Constants.OCPP_RESPONSE_EMPTY
1329 }
1330 const { location } = commandPayload
1331 const uri = new URL(location)
1332 if (uri.protocol.startsWith('ftp:')) {
1333 let ftpClient: Client | undefined
1334 try {
1335 const logFiles = readdirSync(resolve(dirname(fileURLToPath(import.meta.url)), '../'))
1336 .filter(file => file.endsWith('.log'))
1337 .map(file => join('./', file))
1338 const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
1339 create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
1340 ftpClient = new Client()
1341 const accessResponse = await ftpClient.access({
1342 host: uri.host,
1343 ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
1344 ...(isNotEmptyString(uri.username) && { user: uri.username }),
1345 ...(isNotEmptyString(uri.password) && { password: uri.password })
1346 })
1347 let uploadResponse: FTPResponse | undefined
1348 if (accessResponse.code === 220) {
1349 ftpClient.trackProgress(info => {
1350 logger.info(
1351 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${
1352 info.bytes / 1024
1353 } bytes transferred from diagnostics archive ${info.name}`
1354 )
1355 chargingStation.ocppRequestService
1356 .requestHandler<
1357 OCPP16DiagnosticsStatusNotificationRequest,
1358 OCPP16DiagnosticsStatusNotificationResponse
1359 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1360 status: OCPP16DiagnosticsStatus.Uploading
1361 })
1362 .catch(error => {
1363 logger.error(
1364 `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
1365 OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
1366 }'`,
1367 error
1368 )
1369 })
1370 })
1371 uploadResponse = await ftpClient.uploadFrom(
1372 join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive),
1373 `${uri.pathname}${diagnosticsArchive}`
1374 )
1375 if (uploadResponse.code === 226) {
1376 await chargingStation.ocppRequestService.requestHandler<
1377 OCPP16DiagnosticsStatusNotificationRequest,
1378 OCPP16DiagnosticsStatusNotificationResponse
1379 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1380 status: OCPP16DiagnosticsStatus.Uploaded
1381 })
1382 ftpClient.close()
1383 return { fileName: diagnosticsArchive }
1384 }
1385 throw new OCPPError(
1386 ErrorType.GENERIC_ERROR,
1387 `Diagnostics transfer failed with error code ${accessResponse.code}|${uploadResponse.code}`,
1388 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
1389 )
1390 }
1391 throw new OCPPError(
1392 ErrorType.GENERIC_ERROR,
1393 `Diagnostics transfer failed with error code ${accessResponse.code}|${uploadResponse?.code}`,
1394 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
1395 )
1396 } catch (error) {
1397 await chargingStation.ocppRequestService.requestHandler<
1398 OCPP16DiagnosticsStatusNotificationRequest,
1399 OCPP16DiagnosticsStatusNotificationResponse
1400 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1401 status: OCPP16DiagnosticsStatus.UploadFailed
1402 })
1403 ftpClient?.close()
1404 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1405 return this.handleIncomingRequestError<GetDiagnosticsResponse>(
1406 chargingStation,
1407 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
1408 error as Error,
1409 { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY }
1410 )!
1411 }
1412 } else {
1413 logger.error(
1414 `${chargingStation.logPrefix()} Unsupported protocol ${
1415 uri.protocol
1416 } to transfer the diagnostic logs archive`
1417 )
1418 await chargingStation.ocppRequestService.requestHandler<
1419 OCPP16DiagnosticsStatusNotificationRequest,
1420 OCPP16DiagnosticsStatusNotificationResponse
1421 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1422 status: OCPP16DiagnosticsStatus.UploadFailed
1423 })
1424 return OCPP16Constants.OCPP_RESPONSE_EMPTY
1425 }
1426 }
1427
1428 private handleRequestTriggerMessage (
1429 chargingStation: ChargingStation,
1430 commandPayload: OCPP16TriggerMessageRequest
1431 ): OCPP16TriggerMessageResponse {
1432 const { requestedMessage, connectorId } = commandPayload
1433 if (
1434 !OCPP16ServiceUtils.checkFeatureProfile(
1435 chargingStation,
1436 OCPP16SupportedFeatureProfiles.RemoteTrigger,
1437 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE
1438 ) ||
1439 !OCPP16ServiceUtils.isMessageTriggerSupported(chargingStation, requestedMessage)
1440 ) {
1441 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED
1442 }
1443 if (
1444 !OCPP16ServiceUtils.isConnectorIdValid(
1445 chargingStation,
1446 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
1447 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1448 connectorId!
1449 )
1450 ) {
1451 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED
1452 }
1453 try {
1454 switch (requestedMessage) {
1455 case OCPP16MessageTrigger.BootNotification:
1456 setTimeout(() => {
1457 chargingStation.ocppRequestService
1458 .requestHandler<OCPP16BootNotificationRequest, OCPP16BootNotificationResponse>(
1459 chargingStation,
1460 OCPP16RequestCommand.BOOT_NOTIFICATION,
1461 chargingStation.bootNotificationRequest,
1462 { skipBufferingOnError: true, triggerMessage: true }
1463 )
1464 .then(response => {
1465 chargingStation.bootNotificationResponse = response
1466 })
1467 .catch(Constants.EMPTY_FUNCTION)
1468 }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
1469 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
1470 case OCPP16MessageTrigger.Heartbeat:
1471 setTimeout(() => {
1472 chargingStation.ocppRequestService
1473 .requestHandler<OCPP16HeartbeatRequest, OCPP16HeartbeatResponse>(
1474 chargingStation,
1475 OCPP16RequestCommand.HEARTBEAT,
1476 undefined,
1477 {
1478 triggerMessage: true
1479 }
1480 )
1481 .catch(Constants.EMPTY_FUNCTION)
1482 }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
1483 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
1484 case OCPP16MessageTrigger.StatusNotification:
1485 setTimeout(() => {
1486 if (connectorId != null) {
1487 chargingStation.ocppRequestService
1488 .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
1489 chargingStation,
1490 OCPP16RequestCommand.STATUS_NOTIFICATION,
1491 {
1492 connectorId,
1493 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
1494 status: chargingStation.getConnectorStatus(connectorId)?.status
1495 },
1496 {
1497 triggerMessage: true
1498 }
1499 )
1500 .catch(Constants.EMPTY_FUNCTION)
1501 } else if (chargingStation.hasEvses) {
1502 for (const evseStatus of chargingStation.evses.values()) {
1503 for (const [id, connectorStatus] of evseStatus.connectors) {
1504 chargingStation.ocppRequestService
1505 .requestHandler<
1506 OCPP16StatusNotificationRequest,
1507 OCPP16StatusNotificationResponse
1508 >(
1509 chargingStation,
1510 OCPP16RequestCommand.STATUS_NOTIFICATION,
1511 {
1512 connectorId: id,
1513 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
1514 status: connectorStatus.status
1515 },
1516 {
1517 triggerMessage: true
1518 }
1519 )
1520 .catch(Constants.EMPTY_FUNCTION)
1521 }
1522 }
1523 } else {
1524 for (const [id, connectorStatus] of chargingStation.connectors) {
1525 chargingStation.ocppRequestService
1526 .requestHandler<
1527 OCPP16StatusNotificationRequest,
1528 OCPP16StatusNotificationResponse
1529 >(
1530 chargingStation,
1531 OCPP16RequestCommand.STATUS_NOTIFICATION,
1532 {
1533 connectorId: id,
1534 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
1535 status: connectorStatus.status
1536 },
1537 {
1538 triggerMessage: true
1539 }
1540 )
1541 .catch(Constants.EMPTY_FUNCTION)
1542 }
1543 }
1544 }, OCPP16Constants.OCPP_TRIGGER_MESSAGE_DELAY)
1545 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED
1546 default:
1547 return OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED
1548 }
1549 } catch (error) {
1550 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1551 return this.handleIncomingRequestError<OCPP16TriggerMessageResponse>(
1552 chargingStation,
1553 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
1554 error as Error,
1555 { errorResponse: OCPP16Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED }
1556 )!
1557 }
1558 }
1559
1560 private handleRequestDataTransfer (
1561 chargingStation: ChargingStation,
1562 commandPayload: OCPP16DataTransferRequest
1563 ): OCPP16DataTransferResponse {
1564 const { vendorId } = commandPayload
1565 try {
1566 if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) {
1567 return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED
1568 }
1569 return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID
1570 } catch (error) {
1571 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1572 return this.handleIncomingRequestError<OCPP16DataTransferResponse>(
1573 chargingStation,
1574 OCPP16IncomingRequestCommand.DATA_TRANSFER,
1575 error as Error,
1576 { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED }
1577 )!
1578 }
1579 }
1580
1581 private async handleRequestReserveNow (
1582 chargingStation: ChargingStation,
1583 commandPayload: OCPP16ReserveNowRequest
1584 ): Promise<OCPP16ReserveNowResponse> {
1585 if (
1586 !OCPP16ServiceUtils.checkFeatureProfile(
1587 chargingStation,
1588 OCPP16SupportedFeatureProfiles.Reservation,
1589 OCPP16IncomingRequestCommand.RESERVE_NOW
1590 )
1591 ) {
1592 return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
1593 }
1594 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1595 commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
1596 const { reservationId, idTag, connectorId } = commandPayload
1597 let response: OCPP16ReserveNowResponse
1598 try {
1599 if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) {
1600 return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
1601 }
1602 if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) {
1603 return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
1604 }
1605 if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
1606 return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
1607 }
1608 await removeExpiredReservations(chargingStation)
1609 switch (chargingStation.getConnectorStatus(connectorId)?.status) {
1610 case OCPP16ChargePointStatus.Faulted:
1611 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED
1612 break
1613 case OCPP16ChargePointStatus.Preparing:
1614 case OCPP16ChargePointStatus.Charging:
1615 case OCPP16ChargePointStatus.SuspendedEV:
1616 case OCPP16ChargePointStatus.SuspendedEVSE:
1617 case OCPP16ChargePointStatus.Finishing:
1618 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
1619 break
1620 case OCPP16ChargePointStatus.Unavailable:
1621 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE
1622 break
1623 case OCPP16ChargePointStatus.Reserved:
1624 if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) {
1625 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
1626 break
1627 }
1628 // eslint-disable-next-line no-fallthrough
1629 default:
1630 if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
1631 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
1632 break
1633 }
1634 await chargingStation.addReservation({
1635 id: commandPayload.reservationId,
1636 ...commandPayload
1637 })
1638 response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED
1639 break
1640 }
1641 return response
1642 } catch (error) {
1643 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1644 chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available
1645 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1646 return this.handleIncomingRequestError<OCPP16ReserveNowResponse>(
1647 chargingStation,
1648 OCPP16IncomingRequestCommand.RESERVE_NOW,
1649 error as Error,
1650 { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
1651 )!
1652 }
1653 }
1654
1655 private async handleRequestCancelReservation (
1656 chargingStation: ChargingStation,
1657 commandPayload: OCPP16CancelReservationRequest
1658 ): Promise<GenericResponse> {
1659 if (
1660 !OCPP16ServiceUtils.checkFeatureProfile(
1661 chargingStation,
1662 OCPP16SupportedFeatureProfiles.Reservation,
1663 OCPP16IncomingRequestCommand.CANCEL_RESERVATION
1664 )
1665 ) {
1666 return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
1667 }
1668 try {
1669 const { reservationId } = commandPayload
1670 const reservation = chargingStation.getReservationBy('reservationId', reservationId)
1671 if (reservation == null) {
1672 logger.debug(
1673 `${chargingStation.logPrefix()} Reservation with id ${reservationId} does not exist on charging station`
1674 )
1675 return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
1676 }
1677 await chargingStation.removeReservation(
1678 reservation,
1679 ReservationTerminationReason.RESERVATION_CANCELED
1680 )
1681 return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED
1682 } catch (error) {
1683 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1684 return this.handleIncomingRequestError<GenericResponse>(
1685 chargingStation,
1686 OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
1687 error as Error,
1688 { errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED }
1689 )!
1690 }
1691 }
1692 }