]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blob - src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
553e0ae4096f832e876ab59ca5688dc532875f45
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ResponseService.ts
1 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
2
3 import type { ValidateFunction } from 'ajv'
4
5 import { secondsToMilliseconds } from 'date-fns'
6
7 import {
8 addConfigurationKey,
9 type ChargingStation,
10 getConfigurationKey,
11 hasReservationExpired,
12 resetConnectorStatus,
13 } from '../../../charging-station/index.js'
14 import { OCPPError } from '../../../exception/index.js'
15 import {
16 type ChangeConfigurationResponse,
17 ChargingStationEvents,
18 ErrorType,
19 type GenericResponse,
20 type GetConfigurationResponse,
21 type GetDiagnosticsResponse,
22 type JsonType,
23 OCPP16AuthorizationStatus,
24 type OCPP16AuthorizeRequest,
25 type OCPP16AuthorizeResponse,
26 type OCPP16BootNotificationResponse,
27 type OCPP16ChangeAvailabilityResponse,
28 OCPP16ChargePointStatus,
29 type OCPP16ClearChargingProfileResponse,
30 type OCPP16DataTransferResponse,
31 type OCPP16DiagnosticsStatusNotificationResponse,
32 type OCPP16FirmwareStatusNotificationResponse,
33 type OCPP16GetCompositeScheduleResponse,
34 type OCPP16HeartbeatResponse,
35 OCPP16IncomingRequestCommand,
36 type OCPP16MeterValuesRequest,
37 type OCPP16MeterValuesResponse,
38 OCPP16RequestCommand,
39 type OCPP16ReserveNowResponse,
40 OCPP16StandardParametersKey,
41 type OCPP16StartTransactionRequest,
42 type OCPP16StartTransactionResponse,
43 type OCPP16StatusNotificationResponse,
44 type OCPP16StopTransactionRequest,
45 type OCPP16StopTransactionResponse,
46 type OCPP16TriggerMessageResponse,
47 type OCPP16UpdateFirmwareResponse,
48 OCPPVersion,
49 RegistrationStatusEnumType,
50 ReservationTerminationReason,
51 type ResponseHandler,
52 type SetChargingProfileResponse,
53 type UnlockConnectorResponse,
54 } from '../../../types/index.js'
55 import { Constants, convertToInt, isAsyncFunction, logger } from '../../../utils/index.js'
56 import { OCPPResponseService } from '../OCPPResponseService.js'
57 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
58
59 const moduleName = 'OCPP16ResponseService'
60
61 export class OCPP16ResponseService extends OCPPResponseService {
62 public incomingRequestResponsePayloadValidateFunctions: Map<
63 OCPP16IncomingRequestCommand,
64 ValidateFunction<JsonType>
65 >
66
67 protected payloadValidateFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
68 private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
69
70 public constructor () {
71 // if (new.target.name === moduleName) {
72 // throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
73 // }
74 super(OCPPVersion.VERSION_16)
75 this.responseHandlers = new Map<OCPP16RequestCommand, ResponseHandler>([
76 [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
77 [
78 OCPP16RequestCommand.BOOT_NOTIFICATION,
79 this.handleResponseBootNotification.bind(this) as ResponseHandler,
80 ],
81 [OCPP16RequestCommand.DATA_TRANSFER, this.emptyResponseHandler],
82 [
83 OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
84 this.emptyResponseHandler.bind(this) as ResponseHandler,
85 ],
86 [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.emptyResponseHandler],
87 [OCPP16RequestCommand.HEARTBEAT, this.emptyResponseHandler],
88 [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler],
89 [
90 OCPP16RequestCommand.START_TRANSACTION,
91 this.handleResponseStartTransaction.bind(this) as ResponseHandler,
92 ],
93 [
94 OCPP16RequestCommand.STATUS_NOTIFICATION,
95 this.emptyResponseHandler.bind(this) as ResponseHandler,
96 ],
97 [
98 OCPP16RequestCommand.STOP_TRANSACTION,
99 this.handleResponseStopTransaction.bind(this) as ResponseHandler,
100 ],
101 ])
102 this.payloadValidateFunctions = new Map<OCPP16RequestCommand, ValidateFunction<JsonType>>([
103 [
104 OCPP16RequestCommand.AUTHORIZE,
105 this.ajv.compile(
106 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16AuthorizeResponse>(
107 'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json',
108 moduleName,
109 'constructor'
110 )
111 ),
112 ],
113 [
114 OCPP16RequestCommand.BOOT_NOTIFICATION,
115 this.ajv.compile(
116 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16BootNotificationResponse>(
117 'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json',
118 moduleName,
119 'constructor'
120 )
121 ),
122 ],
123 [
124 OCPP16RequestCommand.DATA_TRANSFER,
125 this.ajv.compile(
126 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
127 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
128 moduleName,
129 'constructor'
130 )
131 ),
132 ],
133 [
134 OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
135 this.ajv.compile(
136 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationResponse>(
137 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json',
138 moduleName,
139 'constructor'
140 )
141 ),
142 ],
143 [
144 OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
145 this.ajv.compile(
146 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationResponse>(
147 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json',
148 moduleName,
149 'constructor'
150 )
151 ),
152 ],
153 [
154 OCPP16RequestCommand.HEARTBEAT,
155 this.ajv.compile(
156 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatResponse>(
157 'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json',
158 moduleName,
159 'constructor'
160 )
161 ),
162 ],
163 [
164 OCPP16RequestCommand.METER_VALUES,
165 this.ajv.compile(
166 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16MeterValuesResponse>(
167 'assets/json-schemas/ocpp/1.6/MeterValuesResponse.json',
168 moduleName,
169 'constructor'
170 )
171 ),
172 ],
173 [
174 OCPP16RequestCommand.START_TRANSACTION,
175 this.ajv.compile(
176 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionResponse>(
177 'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json',
178 moduleName,
179 'constructor'
180 )
181 ),
182 ],
183 [
184 OCPP16RequestCommand.STATUS_NOTIFICATION,
185 this.ajv.compile(
186 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationResponse>(
187 'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json',
188 moduleName,
189 'constructor'
190 )
191 ),
192 ],
193 [
194 OCPP16RequestCommand.STOP_TRANSACTION,
195 this.ajv.compile(
196 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionResponse>(
197 'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json',
198 moduleName,
199 'constructor'
200 )
201 ),
202 ],
203 ])
204 this.incomingRequestResponsePayloadValidateFunctions = new Map<
205 OCPP16IncomingRequestCommand,
206 ValidateFunction<JsonType>
207 >([
208 [
209 OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
210 this.ajvIncomingRequest.compile(
211 OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
212 'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
213 moduleName,
214 'constructor'
215 )
216 ),
217 ],
218 [
219 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
220 this.ajvIncomingRequest.compile(
221 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityResponse>(
222 'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json',
223 moduleName,
224 'constructor'
225 )
226 ),
227 ],
228 [
229 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
230 this.ajvIncomingRequest.compile(
231 OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationResponse>(
232 'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json',
233 moduleName,
234 'constructor'
235 )
236 ),
237 ],
238 [
239 OCPP16IncomingRequestCommand.CLEAR_CACHE,
240 this.ajvIncomingRequest.compile(
241 OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
242 'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json',
243 moduleName,
244 'constructor'
245 )
246 ),
247 ],
248 [
249 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
250 this.ajvIncomingRequest.compile(
251 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileResponse>(
252 'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json',
253 moduleName,
254 'constructor'
255 )
256 ),
257 ],
258 [
259 OCPP16IncomingRequestCommand.DATA_TRANSFER,
260 this.ajvIncomingRequest.compile(
261 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
262 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
263 moduleName,
264 'constructor'
265 )
266 ),
267 ],
268 [
269 OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
270 this.ajvIncomingRequest.compile(
271 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16GetCompositeScheduleResponse>(
272 'assets/json-schemas/ocpp/1.6/GetCompositeScheduleResponse.json',
273 moduleName,
274 'constructor'
275 )
276 ),
277 ],
278 [
279 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
280 this.ajvIncomingRequest.compile(
281 OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationResponse>(
282 'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json',
283 moduleName,
284 'constructor'
285 )
286 ),
287 ],
288 [
289 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
290 this.ajvIncomingRequest.compile(
291 OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsResponse>(
292 'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json',
293 moduleName,
294 'constructor'
295 )
296 ),
297 ],
298 [
299 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
300 this.ajvIncomingRequest.compile(
301 OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
302 'assets/json-schemas/ocpp/1.6/RemoteStartTransactionResponse.json',
303 moduleName,
304 'constructor'
305 )
306 ),
307 ],
308 [
309 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
310 this.ajvIncomingRequest.compile(
311 OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
312 'assets/json-schemas/ocpp/1.6/RemoteStopTransactionResponse.json',
313 moduleName,
314 'constructor'
315 )
316 ),
317 ],
318 [
319 OCPP16IncomingRequestCommand.RESERVE_NOW,
320 this.ajvIncomingRequest.compile(
321 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
322 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
323 moduleName,
324 'constructor'
325 )
326 ),
327 ],
328 [
329 OCPP16IncomingRequestCommand.RESET,
330 this.ajvIncomingRequest.compile(
331 OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
332 'assets/json-schemas/ocpp/1.6/ResetResponse.json',
333 moduleName,
334 'constructor'
335 )
336 ),
337 ],
338 [
339 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
340 this.ajvIncomingRequest.compile(
341 OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
342 'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json',
343 moduleName,
344 'constructor'
345 )
346 ),
347 ],
348 [
349 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
350 this.ajvIncomingRequest.compile(
351 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageResponse>(
352 'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json',
353 moduleName,
354 'constructor'
355 )
356 ),
357 ],
358 [
359 OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
360 this.ajvIncomingRequest.compile(
361 OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorResponse>(
362 'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json',
363 moduleName,
364 'constructor'
365 )
366 ),
367 ],
368 [
369 OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
370 this.ajvIncomingRequest.compile(
371 OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareResponse>(
372 'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json',
373 moduleName,
374 'constructor'
375 )
376 ),
377 ],
378 ])
379 this.validatePayload = this.validatePayload.bind(this)
380 }
381
382 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
383 public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
384 chargingStation: ChargingStation,
385 commandName: OCPP16RequestCommand,
386 payload: ResType,
387 requestPayload: ReqType
388 ): Promise<void> {
389 if (
390 chargingStation.inAcceptedState() ||
391 ((chargingStation.inUnknownState() || chargingStation.inPendingState()) &&
392 commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) ||
393 (chargingStation.stationInfo?.ocppStrictCompliance === false &&
394 (chargingStation.inUnknownState() || chargingStation.inPendingState()))
395 ) {
396 if (
397 this.responseHandlers.has(commandName) &&
398 OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)
399 ) {
400 try {
401 this.validatePayload(chargingStation, commandName, payload)
402 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
403 const responseHandler = this.responseHandlers.get(commandName)!
404 if (isAsyncFunction(responseHandler)) {
405 await responseHandler(chargingStation, payload, requestPayload)
406 } else {
407 ;(
408 responseHandler as (
409 chargingStation: ChargingStation,
410 payload: JsonType,
411 requestPayload?: JsonType
412 ) => void
413 )(chargingStation, payload, requestPayload)
414 }
415 } catch (error) {
416 logger.error(
417 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
418 error
419 )
420 throw error
421 }
422 } else {
423 // Throw exception
424 throw new OCPPError(
425 ErrorType.NOT_IMPLEMENTED,
426 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
427 payload,
428 undefined,
429 2
430 )}`,
431 commandName,
432 payload
433 )
434 }
435 } else {
436 throw new OCPPError(
437 ErrorType.SECURITY_ERROR,
438 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
439 payload,
440 undefined,
441 2
442 )} while the charging station is not registered on the central server`,
443 commandName,
444 payload
445 )
446 }
447 }
448
449 private handleResponseAuthorize (
450 chargingStation: ChargingStation,
451 payload: OCPP16AuthorizeResponse,
452 requestPayload: OCPP16AuthorizeRequest
453 ): void {
454 let authorizeConnectorId: number | undefined
455 if (chargingStation.hasEvses) {
456 for (const [evseId, evseStatus] of chargingStation.evses) {
457 if (evseId > 0) {
458 for (const [connectorId, connectorStatus] of evseStatus.connectors) {
459 if (connectorStatus.authorizeIdTag === requestPayload.idTag) {
460 authorizeConnectorId = connectorId
461 break
462 }
463 }
464 }
465 }
466 } else {
467 for (const connectorId of chargingStation.connectors.keys()) {
468 if (
469 connectorId > 0 &&
470 chargingStation.getConnectorStatus(connectorId)?.authorizeIdTag === requestPayload.idTag
471 ) {
472 authorizeConnectorId = connectorId
473 break
474 }
475 }
476 }
477 if (authorizeConnectorId != null) {
478 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
479 const authorizeConnectorStatus = chargingStation.getConnectorStatus(authorizeConnectorId)!
480 if (payload.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
481 authorizeConnectorStatus.idTagAuthorized = true
482 logger.debug(
483 `${chargingStation.logPrefix()} idTag '${
484 requestPayload.idTag
485 }' accepted on connector id ${authorizeConnectorId.toString()}`
486 )
487 } else {
488 authorizeConnectorStatus.idTagAuthorized = false
489 delete authorizeConnectorStatus.authorizeIdTag
490 logger.debug(
491 `${chargingStation.logPrefix()} idTag '${
492 requestPayload.idTag
493 }' rejected with status '${payload.idTagInfo.status}'`
494 )
495 }
496 } else {
497 logger.error(
498 `${chargingStation.logPrefix()} idTag '${
499 requestPayload.idTag
500 }' has no authorize request pending`
501 )
502 }
503 }
504
505 private handleResponseBootNotification (
506 chargingStation: ChargingStation,
507 payload: OCPP16BootNotificationResponse
508 ): void {
509 if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
510 chargingStation.bootNotificationResponse = payload
511 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
512 if (payload.interval != null) {
513 addConfigurationKey(
514 chargingStation,
515 OCPP16StandardParametersKey.HeartbeatInterval,
516 payload.interval.toString(),
517 {},
518 { overwrite: true, save: true }
519 )
520 addConfigurationKey(
521 chargingStation,
522 OCPP16StandardParametersKey.HeartBeatInterval,
523 payload.interval.toString(),
524 { visible: false },
525 { overwrite: true, save: true }
526 )
527 }
528 if (chargingStation.inAcceptedState()) {
529 chargingStation.emit(ChargingStationEvents.accepted)
530 } else if (chargingStation.inPendingState()) {
531 chargingStation.emit(ChargingStationEvents.pending)
532 } else if (chargingStation.inRejectedState()) {
533 chargingStation.emit(ChargingStationEvents.rejected)
534 }
535 const logMsg = `${chargingStation.logPrefix()} Charging station in '${
536 payload.status
537 }' state on the central server`
538 payload.status === RegistrationStatusEnumType.REJECTED
539 ? logger.warn(logMsg)
540 : logger.info(logMsg)
541 } else {
542 delete chargingStation.bootNotificationResponse
543 logger.error(
544 `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
545 payload
546 )
547 }
548 }
549
550 private async handleResponseStartTransaction (
551 chargingStation: ChargingStation,
552 payload: OCPP16StartTransactionResponse,
553 requestPayload: OCPP16StartTransactionRequest
554 ): Promise<void> {
555 const { connectorId } = requestPayload
556 if (connectorId === 0 || !chargingStation.hasConnector(connectorId)) {
557 logger.error(
558 `${chargingStation.logPrefix()} Trying to start a transaction on a non existing connector id ${connectorId.toString()}`
559 )
560 return
561 }
562 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
563 if (
564 connectorStatus?.transactionRemoteStarted === true &&
565 chargingStation.getAuthorizeRemoteTxRequests() &&
566 chargingStation.getLocalAuthListEnabled() &&
567 chargingStation.hasIdTags() &&
568 connectorStatus.idTagLocalAuthorized === false
569 ) {
570 logger.error(
571 `${chargingStation.logPrefix()} Trying to start a transaction with a not local authorized idTag ${
572 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
573 connectorStatus.localAuthorizeIdTag
574 } on connector id ${connectorId.toString()}`
575 )
576 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
577 return
578 }
579 if (
580 connectorStatus?.transactionRemoteStarted === true &&
581 chargingStation.getAuthorizeRemoteTxRequests() &&
582 chargingStation.stationInfo?.remoteAuthorization === true &&
583 connectorStatus.idTagLocalAuthorized === false &&
584 connectorStatus.idTagAuthorized === false
585 ) {
586 logger.error(
587 `${chargingStation.logPrefix()} Trying to start a transaction with a not authorized idTag ${
588 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
589 connectorStatus.authorizeIdTag
590 } on connector id ${connectorId.toString()}`
591 )
592 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
593 return
594 }
595 if (
596 connectorStatus?.idTagAuthorized === true &&
597 connectorStatus.authorizeIdTag != null &&
598 connectorStatus.authorizeIdTag !== requestPayload.idTag
599 ) {
600 logger.error(
601 `${chargingStation.logPrefix()} Trying to start a transaction with an idTag ${
602 requestPayload.idTag
603 } different from the authorize request one ${
604 connectorStatus.authorizeIdTag
605 } on connector id ${connectorId.toString()}`
606 )
607 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
608 return
609 }
610 if (
611 connectorStatus?.idTagLocalAuthorized === true &&
612 connectorStatus.localAuthorizeIdTag != null &&
613 connectorStatus.localAuthorizeIdTag !== requestPayload.idTag
614 ) {
615 logger.error(
616 `${chargingStation.logPrefix()} Trying to start a transaction with an idTag ${
617 requestPayload.idTag
618 } different from the local authorized one ${
619 connectorStatus.localAuthorizeIdTag
620 } on connector id ${connectorId.toString()}`
621 )
622 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
623 return
624 }
625 if (connectorStatus?.transactionStarted === true) {
626 logger.error(
627 `${chargingStation.logPrefix()} Trying to start a transaction on an already used connector id ${connectorId.toString()} by idTag ${
628 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
629 connectorStatus.transactionIdTag
630 }`
631 )
632 return
633 }
634 if (chargingStation.hasEvses) {
635 for (const [evseId, evseStatus] of chargingStation.evses) {
636 if (evseStatus.connectors.size > 1) {
637 for (const [id, status] of evseStatus.connectors) {
638 if (id !== connectorId && status.transactionStarted === true) {
639 logger.error(
640 `${chargingStation.logPrefix()} Trying to start a transaction on an already used evse id ${evseId.toString()} by connector id ${id.toString()} with idTag ${
641 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
642 status.transactionIdTag
643 }`
644 )
645 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
646 return
647 }
648 }
649 }
650 }
651 }
652 if (
653 connectorStatus?.status !== OCPP16ChargePointStatus.Available &&
654 connectorStatus?.status !== OCPP16ChargePointStatus.Preparing
655 ) {
656 logger.error(
657 `${chargingStation.logPrefix()} Trying to start a transaction on connector id ${connectorId.toString()} with status ${
658 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
659 connectorStatus?.status
660 }`
661 )
662 return
663 }
664 if (!Number.isSafeInteger(payload.transactionId)) {
665 logger.warn(
666 `${chargingStation.logPrefix()} Trying to start a transaction on connector id ${connectorId.toString()} with a non integer transaction id ${payload.transactionId.toString()}, converting to integer`
667 )
668 payload.transactionId = convertToInt(payload.transactionId)
669 }
670
671 if (payload.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED) {
672 connectorStatus.transactionStarted = true
673 connectorStatus.transactionStart = requestPayload.timestamp
674 connectorStatus.transactionId = payload.transactionId
675 connectorStatus.transactionIdTag = requestPayload.idTag
676 connectorStatus.transactionEnergyActiveImportRegisterValue = 0
677 connectorStatus.transactionBeginMeterValue =
678 OCPP16ServiceUtils.buildTransactionBeginMeterValue(
679 chargingStation,
680 connectorId,
681 requestPayload.meterStart
682 )
683 if (requestPayload.reservationId != null) {
684 const reservation = chargingStation.getReservationBy(
685 'reservationId',
686 requestPayload.reservationId
687 )
688 if (reservation != null) {
689 if (reservation.idTag !== requestPayload.idTag) {
690 logger.warn(
691 `${chargingStation.logPrefix()} Reserved transaction ${payload.transactionId.toString()} started with a different idTag ${
692 requestPayload.idTag
693 } than the reservation one ${reservation.idTag}`
694 )
695 }
696 if (hasReservationExpired(reservation)) {
697 logger.warn(
698 `${chargingStation.logPrefix()} Reserved transaction ${payload.transactionId.toString()} started with expired reservation ${requestPayload.reservationId.toString()} (expiry date: ${reservation.expiryDate.toISOString()}))`
699 )
700 }
701 await chargingStation.removeReservation(
702 reservation,
703 ReservationTerminationReason.TRANSACTION_STARTED
704 )
705 } else {
706 logger.warn(
707 `${chargingStation.logPrefix()} Reserved transaction ${payload.transactionId.toString()} started with unknown reservation ${requestPayload.reservationId.toString()}`
708 )
709 }
710 }
711 chargingStation.stationInfo?.beginEndMeterValues === true &&
712 (await chargingStation.ocppRequestService.requestHandler<
713 OCPP16MeterValuesRequest,
714 OCPP16MeterValuesResponse
715 >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
716 connectorId,
717 meterValue: [connectorStatus.transactionBeginMeterValue],
718 transactionId: payload.transactionId,
719 } satisfies OCPP16MeterValuesRequest))
720 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
721 chargingStation,
722 connectorId,
723 OCPP16ChargePointStatus.Charging
724 )
725 logger.info(
726 `${chargingStation.logPrefix()} Transaction with id ${payload.transactionId.toString()} STARTED on ${
727 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
728 chargingStation.stationInfo?.chargingStationId
729 }#${connectorId.toString()} for idTag '${requestPayload.idTag}'`
730 )
731 if (chargingStation.stationInfo?.powerSharedByConnectors === true) {
732 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
733 ++chargingStation.powerDivider!
734 }
735 const configuredMeterValueSampleInterval = getConfigurationKey(
736 chargingStation,
737 OCPP16StandardParametersKey.MeterValueSampleInterval
738 )
739 chargingStation.startMeterValues(
740 connectorId,
741 configuredMeterValueSampleInterval != null
742 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
743 : Constants.DEFAULT_METER_VALUES_INTERVAL
744 )
745 } else {
746 logger.warn(
747 `${chargingStation.logPrefix()} Starting transaction with id ${payload.transactionId.toString()} REJECTED on ${
748 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
749 chargingStation.stationInfo?.chargingStationId
750 }#${connectorId.toString()} with status '${payload.idTagInfo.status}', idTag '${
751 requestPayload.idTag
752 }'${
753 OCPP16ServiceUtils.hasReservation(chargingStation, connectorId, requestPayload.idTag)
754 ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
755 `, reservationId '${requestPayload.reservationId?.toString()}'`
756 : ''
757 }`
758 )
759 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId)
760 }
761 }
762
763 private async handleResponseStopTransaction (
764 chargingStation: ChargingStation,
765 payload: OCPP16StopTransactionResponse,
766 requestPayload: OCPP16StopTransactionRequest
767 ): Promise<void> {
768 const transactionConnectorId = chargingStation.getConnectorIdByTransactionId(
769 requestPayload.transactionId
770 )
771 if (transactionConnectorId == null) {
772 logger.error(
773 `${chargingStation.logPrefix()} Trying to stop a non existing transaction with id ${requestPayload.transactionId.toString()}`
774 )
775 return
776 }
777 chargingStation.stationInfo?.beginEndMeterValues === true &&
778 chargingStation.stationInfo.ocppStrictCompliance === false &&
779 chargingStation.stationInfo.outOfOrderEndMeterValues === true &&
780 (await chargingStation.ocppRequestService.requestHandler<
781 OCPP16MeterValuesRequest,
782 OCPP16MeterValuesResponse
783 >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
784 connectorId: transactionConnectorId,
785 meterValue: [
786 OCPP16ServiceUtils.buildTransactionEndMeterValue(
787 chargingStation,
788 transactionConnectorId,
789 requestPayload.meterStop
790 ),
791 ],
792 transactionId: requestPayload.transactionId,
793 }))
794 if (
795 !chargingStation.isChargingStationAvailable() ||
796 !chargingStation.isConnectorAvailable(transactionConnectorId)
797 ) {
798 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
799 chargingStation,
800 transactionConnectorId,
801 OCPP16ChargePointStatus.Unavailable
802 )
803 } else {
804 await OCPP16ServiceUtils.sendAndSetConnectorStatus(
805 chargingStation,
806 transactionConnectorId,
807 OCPP16ChargePointStatus.Available
808 )
809 }
810 if (chargingStation.stationInfo?.powerSharedByConnectors === true) {
811 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
812 chargingStation.powerDivider!--
813 }
814 resetConnectorStatus(chargingStation.getConnectorStatus(transactionConnectorId))
815 chargingStation.stopMeterValues(transactionConnectorId)
816 const logMsg = `${chargingStation.logPrefix()} Transaction with id ${requestPayload.transactionId.toString()} STOPPED on ${
817 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
818 chargingStation.stationInfo?.chargingStationId
819 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
820 }#${transactionConnectorId.toString()} with status '${payload.idTagInfo?.status}'`
821 if (
822 payload.idTagInfo == null ||
823 payload.idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
824 ) {
825 logger.info(logMsg)
826 } else {
827 logger.warn(logMsg)
828 }
829 }
830
831 private async resetConnectorOnStartTransactionError (
832 chargingStation: ChargingStation,
833 connectorId: number
834 ): Promise<void> {
835 chargingStation.stopMeterValues(connectorId)
836 const connectorStatus = chargingStation.getConnectorStatus(connectorId)
837 resetConnectorStatus(connectorStatus)
838 await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
839 }
840
841 private validatePayload (
842 chargingStation: ChargingStation,
843 commandName: OCPP16RequestCommand,
844 payload: JsonType
845 ): boolean {
846 if (this.payloadValidateFunctions.has(commandName)) {
847 return this.validateResponsePayload(chargingStation, commandName, payload)
848 }
849 logger.warn(
850 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
851 )
852 return false
853 }
854 }