1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
4 import path from
'path';
5 import { fileURLToPath
} from
'url';
7 import type { JSONSchemaType
} from
'ajv';
9 import OCPPError from
'../../../exception/OCPPError';
10 import type { JsonObject
, JsonType
} from
'../../../types/JsonType';
11 import { OCPP16ChargePointErrorCode
} from
'../../../types/ocpp/1.6/ChargePointErrorCode';
12 import { OCPP16ChargePointStatus
} from
'../../../types/ocpp/1.6/ChargePointStatus';
13 import { OCPP16StandardParametersKey
} from
'../../../types/ocpp/1.6/Configuration';
15 OCPP16MeterValuesRequest
,
16 OCPP16MeterValuesResponse
,
17 } from
'../../../types/ocpp/1.6/MeterValues';
19 OCPP16BootNotificationRequest
,
21 OCPP16StatusNotificationRequest
,
22 } from
'../../../types/ocpp/1.6/Requests';
24 DiagnosticsStatusNotificationResponse
,
25 OCPP16BootNotificationResponse
,
26 OCPP16HeartbeatResponse
,
27 OCPP16RegistrationStatus
,
28 OCPP16StatusNotificationResponse
,
29 } from
'../../../types/ocpp/1.6/Responses';
31 OCPP16AuthorizationStatus
,
32 OCPP16AuthorizeRequest
,
33 OCPP16AuthorizeResponse
,
34 OCPP16StartTransactionRequest
,
35 OCPP16StartTransactionResponse
,
36 OCPP16StopTransactionRequest
,
37 OCPP16StopTransactionResponse
,
38 } from
'../../../types/ocpp/1.6/Transaction';
39 import { ErrorType
} from
'../../../types/ocpp/ErrorType';
40 import type { ResponseHandler
} from
'../../../types/ocpp/Responses';
41 import Constants from
'../../../utils/Constants';
42 import logger from
'../../../utils/Logger';
43 import Utils from
'../../../utils/Utils';
44 import type ChargingStation from
'../../ChargingStation';
45 import { ChargingStationConfigurationUtils
} from
'../../ChargingStationConfigurationUtils';
46 import OCPPResponseService from
'../OCPPResponseService';
47 import { OCPP16ServiceUtils
} from
'./OCPP16ServiceUtils';
49 const moduleName
= 'OCPP16ResponseService';
51 export default class OCPP16ResponseService
extends OCPPResponseService
{
52 private responseHandlers
: Map
<OCPP16RequestCommand
, ResponseHandler
>;
53 private jsonSchemas
: Map
<OCPP16RequestCommand
, JSONSchemaType
<JsonObject
>>;
55 public constructor() {
56 if (new.target
?.name
=== moduleName
) {
57 throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
60 this.responseHandlers
= new Map
<OCPP16RequestCommand
, ResponseHandler
>([
61 [OCPP16RequestCommand
.BOOT_NOTIFICATION
, this.handleResponseBootNotification
.bind(this)],
62 [OCPP16RequestCommand
.HEARTBEAT
, this.emptyResponseHandler
.bind(this)],
63 [OCPP16RequestCommand
.AUTHORIZE
, this.handleResponseAuthorize
.bind(this)],
64 [OCPP16RequestCommand
.START_TRANSACTION
, this.handleResponseStartTransaction
.bind(this)],
65 [OCPP16RequestCommand
.STOP_TRANSACTION
, this.handleResponseStopTransaction
.bind(this)],
66 [OCPP16RequestCommand
.STATUS_NOTIFICATION
, this.emptyResponseHandler
.bind(this)],
67 [OCPP16RequestCommand
.METER_VALUES
, this.emptyResponseHandler
.bind(this)],
68 [OCPP16RequestCommand
.DIAGNOSTICS_STATUS_NOTIFICATION
, this.emptyResponseHandler
.bind(this)],
70 this.jsonSchemas
= new Map
<OCPP16RequestCommand
, JSONSchemaType
<JsonObject
>>([
72 OCPP16RequestCommand
.BOOT_NOTIFICATION
,
76 path
.dirname(fileURLToPath(import.meta
.url
)),
77 '../../../assets/json-schemas/ocpp/1.6/BootNotificationResponse.json'
81 ) as JSONSchemaType
<OCPP16BootNotificationResponse
>,
84 OCPP16RequestCommand
.HEARTBEAT
,
88 path
.dirname(fileURLToPath(import.meta
.url
)),
89 '../../../assets/json-schemas/ocpp/1.6/HeartbeatResponse.json'
93 ) as JSONSchemaType
<OCPP16HeartbeatResponse
>,
96 OCPP16RequestCommand
.AUTHORIZE
,
100 path
.dirname(fileURLToPath(import.meta
.url
)),
101 '../../../assets/json-schemas/ocpp/1.6/AuthorizeResponse.json'
105 ) as JSONSchemaType
<OCPP16AuthorizeResponse
>,
108 OCPP16RequestCommand
.START_TRANSACTION
,
112 path
.dirname(fileURLToPath(import.meta
.url
)),
113 '../../../assets/json-schemas/ocpp/1.6/StartTransactionResponse.json'
117 ) as JSONSchemaType
<OCPP16StartTransactionResponse
>,
120 OCPP16RequestCommand
.STOP_TRANSACTION
,
124 path
.dirname(fileURLToPath(import.meta
.url
)),
125 '../../../assets/json-schemas/ocpp/1.6/StopTransactionResponse.json'
129 ) as JSONSchemaType
<OCPP16StopTransactionResponse
>,
132 OCPP16RequestCommand
.STATUS_NOTIFICATION
,
136 path
.dirname(fileURLToPath(import.meta
.url
)),
137 '../../../assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json'
141 ) as JSONSchemaType
<OCPP16StatusNotificationResponse
>,
144 OCPP16RequestCommand
.METER_VALUES
,
148 path
.dirname(fileURLToPath(import.meta
.url
)),
149 '../../../assets/json-schemas/ocpp/1.6/MeterValuesResponse.json'
153 ) as JSONSchemaType
<OCPP16MeterValuesResponse
>,
156 OCPP16RequestCommand
.DIAGNOSTICS_STATUS_NOTIFICATION
,
160 path
.dirname(fileURLToPath(import.meta
.url
)),
161 '../../../assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json'
165 ) as JSONSchemaType
<DiagnosticsStatusNotificationResponse
>,
168 this.validatePayload
.bind(this);
171 public async responseHandler(
172 chargingStation
: ChargingStation
,
173 commandName
: OCPP16RequestCommand
,
175 requestPayload
: JsonType
178 chargingStation
.isRegistered() === true ||
179 commandName
=== OCPP16RequestCommand
.BOOT_NOTIFICATION
182 this.responseHandlers
.has(commandName
) === true &&
183 OCPP16ServiceUtils
.isRequestCommandSupported(chargingStation
, commandName
) === true
186 this.validatePayload(chargingStation
, commandName
, payload
);
187 await this.responseHandlers
.get(commandName
)(chargingStation
, payload
, requestPayload
);
190 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
198 ErrorType
.NOT_IMPLEMENTED
,
199 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
210 ErrorType
.SECURITY_ERROR
,
211 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
215 )} while the charging station is not registered on the central server. `,
222 private validatePayload(
223 chargingStation
: ChargingStation
,
224 commandName
: OCPP16RequestCommand
,
227 if (this.jsonSchemas
.has(commandName
)) {
228 return this.validateResponsePayload(
231 this.jsonSchemas
.get(commandName
),
236 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
241 private handleResponseBootNotification(
242 chargingStation
: ChargingStation
,
243 payload
: OCPP16BootNotificationResponse
245 if (payload
.status === OCPP16RegistrationStatus
.ACCEPTED
) {
246 ChargingStationConfigurationUtils
.addConfigurationKey(
248 OCPP16StandardParametersKey
.HeartbeatInterval
,
249 payload
.interval
.toString(),
251 { overwrite
: true, save
: true }
253 ChargingStationConfigurationUtils
.addConfigurationKey(
255 OCPP16StandardParametersKey
.HeartBeatInterval
,
256 payload
.interval
.toString(),
258 { overwrite
: true, save
: true }
260 chargingStation
.heartbeatSetInterval
261 ? chargingStation
.restartHeartbeat()
262 : chargingStation
.startHeartbeat();
264 if (Object.values(OCPP16RegistrationStatus
).includes(payload
.status)) {
265 const logMsg
= `${chargingStation.logPrefix()} Charging station in '${
267 }' state on the central server`;
268 payload
.status === OCPP16RegistrationStatus
.REJECTED
269 ? logger
.warn(logMsg
)
270 : logger
.info(logMsg
);
273 chargingStation
.logPrefix() +
274 ' Charging station boot notification response received: %j with undefined registration status',
280 private handleResponseAuthorize(
281 chargingStation
: ChargingStation
,
282 payload
: OCPP16AuthorizeResponse
,
283 requestPayload
: OCPP16AuthorizeRequest
285 let authorizeConnectorId
: number;
286 for (const connectorId
of chargingStation
.connectors
.keys()) {
289 chargingStation
.getConnectorStatus(connectorId
)?.authorizeIdTag
=== requestPayload
.idTag
291 authorizeConnectorId
= connectorId
;
295 const isAuthorizeConnectorIdDefined
= authorizeConnectorId
!== undefined;
296 if (payload
.idTagInfo
.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
297 isAuthorizeConnectorIdDefined
&&
298 (chargingStation
.getConnectorStatus(authorizeConnectorId
).idTagAuthorized
= true);
300 `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' accepted${
301 isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}
` : ''
305 if (isAuthorizeConnectorIdDefined
) {
306 chargingStation
.getConnectorStatus(authorizeConnectorId
).idTagAuthorized
= false;
307 delete chargingStation
.getConnectorStatus(authorizeConnectorId
).authorizeIdTag
;
310 `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' rejected with status '${
311 payload.idTagInfo.status
312 }'${isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}` : ''}`
317 private async handleResponseStartTransaction(
318 chargingStation: ChargingStation,
319 payload: OCPP16StartTransactionResponse,
320 requestPayload: OCPP16StartTransactionRequest
322 const connectorId = requestPayload.connectorId;
324 let transactionConnectorId: number;
325 for (const id of chargingStation.connectors.keys()) {
326 if (id > 0 && id === connectorId) {
327 transactionConnectorId = id;
331 if (!transactionConnectorId) {
333 chargingStation.logPrefix() +
334 ' Trying to start a transaction on a non existing connector Id ' +
335 connectorId.toString()
340 chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
341 chargingStation.getAuthorizeRemoteTxRequests() === true &&
342 chargingStation.getLocalAuthListEnabled() === true &&
343 chargingStation.hasAuthorizedTags() &&
344 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false
347 chargingStation.logPrefix() +
348 ' Trying to start a transaction with a not local authorized idTag ' +
349 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag +
350 ' on connector Id ' +
351 connectorId.toString()
353 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
357 chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
358 chargingStation.getAuthorizeRemoteTxRequests() === true &&
359 chargingStation.getMustAuthorizeAtRemoteStart() === true &&
360 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false &&
361 chargingStation.getConnectorStatus(connectorId).idTagAuthorized === false
364 chargingStation.logPrefix() +
365 ' Trying to start a transaction with a not authorized idTag ' +
366 chargingStation.getConnectorStatus(connectorId).authorizeIdTag +
367 ' on connector Id ' +
368 connectorId.toString()
370 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
374 chargingStation.getConnectorStatus(connectorId).idTagAuthorized &&
375 chargingStation.getConnectorStatus(connectorId).authorizeIdTag !== requestPayload.idTag
378 chargingStation.logPrefix() +
379 ' Trying to start a transaction with an idTag ' +
380 requestPayload.idTag +
381 ' different from the authorize request one ' +
382 chargingStation.getConnectorStatus(connectorId).authorizeIdTag +
383 ' on connector Id ' +
384 connectorId.toString()
386 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
390 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized &&
391 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag !== requestPayload.idTag
394 chargingStation.logPrefix() +
395 ' Trying to start a transaction with an idTag ' +
396 requestPayload.idTag +
397 ' different from the local authorized one ' +
398 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag +
399 ' on connector Id ' +
400 connectorId.toString()
402 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
405 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
407 chargingStation.logPrefix() +
408 ' Trying to start a transaction on an already used connector ' +
409 connectorId.toString() +
411 chargingStation.getConnectorStatus(connectorId)
416 chargingStation.getConnectorStatus(connectorId)?.status !==
417 OCPP16ChargePointStatus.AVAILABLE &&
418 chargingStation.getConnectorStatus(connectorId)?.status !== OCPP16ChargePointStatus.PREPARING
421 `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()}
with status $
{
422 chargingStation
.getConnectorStatus(connectorId
)?.status
427 // if (!Number.isInteger(payload.transactionId)) {
429 // `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()}
with a non integer transaction Id $
{
430 // payload.transactionId
431 // }, converting to integer`
433 // payload.transactionId = Utils.convertToInt(payload.transactionId);
436 if (payload
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
437 chargingStation
.getConnectorStatus(connectorId
).transactionStarted
= true;
438 chargingStation
.getConnectorStatus(connectorId
).transactionId
= payload
.transactionId
;
439 chargingStation
.getConnectorStatus(connectorId
).transactionIdTag
= requestPayload
.idTag
;
440 chargingStation
.getConnectorStatus(
442 ).transactionEnergyActiveImportRegisterValue
= 0;
443 chargingStation
.getConnectorStatus(connectorId
).transactionBeginMeterValue
=
444 OCPP16ServiceUtils
.buildTransactionBeginMeterValue(
447 requestPayload
.meterStart
449 chargingStation
.getBeginEndMeterValues() &&
450 (await chargingStation
.ocppRequestService
.requestHandler
<
451 OCPP16MeterValuesRequest
,
452 OCPP16MeterValuesResponse
453 >(chargingStation
, OCPP16RequestCommand
.METER_VALUES
, {
455 transactionId
: payload
.transactionId
,
456 meterValue
: [chargingStation
.getConnectorStatus(connectorId
).transactionBeginMeterValue
],
458 await chargingStation
.ocppRequestService
.requestHandler
<
459 OCPP16StatusNotificationRequest
,
460 OCPP16StatusNotificationResponse
461 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
463 status: OCPP16ChargePointStatus
.CHARGING
,
464 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
466 chargingStation
.getConnectorStatus(connectorId
).status = OCPP16ChargePointStatus
.CHARGING
;
468 chargingStation
.logPrefix() +
470 payload
.transactionId
.toString() +
472 chargingStation
.stationInfo
.chargingStationId
+
474 connectorId
.toString() +
476 requestPayload
.idTag
+
479 if (chargingStation
.stationInfo
.powerSharedByConnectors
) {
480 chargingStation
.powerDivider
++;
482 const configuredMeterValueSampleInterval
=
483 ChargingStationConfigurationUtils
.getConfigurationKey(
485 OCPP16StandardParametersKey
.MeterValueSampleInterval
487 chargingStation
.startMeterValues(
489 configuredMeterValueSampleInterval
490 ? Utils
.convertToInt(configuredMeterValueSampleInterval
.value
) * 1000
491 : Constants
.DEFAULT_METER_VALUES_INTERVAL
495 chargingStation
.logPrefix() +
496 ' Starting transaction id ' +
497 payload
.transactionId
.toString() +
498 " REJECTED with status '" +
499 payload
?.idTagInfo
?.status +
501 requestPayload
.idTag
+
504 await this.resetConnectorOnStartTransactionError(chargingStation
, connectorId
);
508 private async resetConnectorOnStartTransactionError(
509 chargingStation
: ChargingStation
,
512 chargingStation
.resetConnectorStatus(connectorId
);
514 chargingStation
.getConnectorStatus(connectorId
).status !== OCPP16ChargePointStatus
.AVAILABLE
516 await chargingStation
.ocppRequestService
.requestHandler
<
517 OCPP16StatusNotificationRequest
,
518 OCPP16StatusNotificationResponse
519 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
521 status: OCPP16ChargePointStatus
.AVAILABLE
,
522 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
524 chargingStation
.getConnectorStatus(connectorId
).status = OCPP16ChargePointStatus
.AVAILABLE
;
528 private async handleResponseStopTransaction(
529 chargingStation
: ChargingStation
,
530 payload
: OCPP16StopTransactionResponse
,
531 requestPayload
: OCPP16StopTransactionRequest
533 const transactionConnectorId
= chargingStation
.getConnectorIdByTransactionId(
534 requestPayload
.transactionId
536 if (!transactionConnectorId
) {
538 chargingStation
.logPrefix() +
539 ' Trying to stop a non existing transaction ' +
540 requestPayload
.transactionId
.toString()
544 if (payload
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
545 chargingStation
.getBeginEndMeterValues() === true &&
546 chargingStation
.getOcppStrictCompliance() === false &&
547 chargingStation
.getOutOfOrderEndMeterValues() === true &&
548 (await chargingStation
.ocppRequestService
.requestHandler
<
549 OCPP16MeterValuesRequest
,
550 OCPP16MeterValuesResponse
551 >(chargingStation
, OCPP16RequestCommand
.METER_VALUES
, {
552 connectorId
: transactionConnectorId
,
553 transactionId
: requestPayload
.transactionId
,
555 OCPP16ServiceUtils
.buildTransactionEndMeterValue(
557 transactionConnectorId
,
558 requestPayload
.meterStop
563 chargingStation
.isChargingStationAvailable() === false ||
564 chargingStation
.isConnectorAvailable(transactionConnectorId
) === false
566 await chargingStation
.ocppRequestService
.requestHandler
<
567 OCPP16StatusNotificationRequest
,
568 OCPP16StatusNotificationResponse
569 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
570 connectorId
: transactionConnectorId
,
571 status: OCPP16ChargePointStatus
.UNAVAILABLE
,
572 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
574 chargingStation
.getConnectorStatus(transactionConnectorId
).status =
575 OCPP16ChargePointStatus
.UNAVAILABLE
;
577 await chargingStation
.ocppRequestService
.requestHandler
<
578 OCPP16BootNotificationRequest
,
579 OCPP16BootNotificationResponse
580 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
581 connectorId
: transactionConnectorId
,
582 status: OCPP16ChargePointStatus
.AVAILABLE
,
583 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
585 chargingStation
.getConnectorStatus(transactionConnectorId
).status =
586 OCPP16ChargePointStatus
.AVAILABLE
;
588 if (chargingStation
.stationInfo
.powerSharedByConnectors
) {
589 chargingStation
.powerDivider
--;
591 chargingStation
.resetConnectorStatus(transactionConnectorId
);
593 chargingStation
.logPrefix() +
595 requestPayload
.transactionId
.toString() +
597 chargingStation
.stationInfo
.chargingStationId
+
599 transactionConnectorId
.toString()
603 chargingStation
.logPrefix() +
604 ' Stopping transaction id ' +
605 requestPayload
.transactionId
.toString() +
606 " REJECTED with status '" +
607 payload
.idTagInfo
?.status +