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 { ChargingStationUtils
} from
'../../ChargingStationUtils';
47 import OCPPResponseService from
'../OCPPResponseService';
48 import { OCPP16ServiceUtils
} from
'./OCPP16ServiceUtils';
50 const moduleName
= 'OCPP16ResponseService';
52 export default class OCPP16ResponseService
extends OCPPResponseService
{
53 private responseHandlers
: Map
<OCPP16RequestCommand
, ResponseHandler
>;
54 private jsonSchemas
: Map
<OCPP16RequestCommand
, JSONSchemaType
<JsonObject
>>;
56 public constructor() {
57 if (new.target
?.name
=== moduleName
) {
58 throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
61 this.responseHandlers
= new Map
<OCPP16RequestCommand
, ResponseHandler
>([
62 [OCPP16RequestCommand
.BOOT_NOTIFICATION
, this.handleResponseBootNotification
.bind(this)],
63 [OCPP16RequestCommand
.HEARTBEAT
, this.emptyResponseHandler
.bind(this)],
64 [OCPP16RequestCommand
.AUTHORIZE
, this.handleResponseAuthorize
.bind(this)],
65 [OCPP16RequestCommand
.START_TRANSACTION
, this.handleResponseStartTransaction
.bind(this)],
66 [OCPP16RequestCommand
.STOP_TRANSACTION
, this.handleResponseStopTransaction
.bind(this)],
67 [OCPP16RequestCommand
.STATUS_NOTIFICATION
, this.emptyResponseHandler
.bind(this)],
68 [OCPP16RequestCommand
.METER_VALUES
, this.emptyResponseHandler
.bind(this)],
69 [OCPP16RequestCommand
.DIAGNOSTICS_STATUS_NOTIFICATION
, this.emptyResponseHandler
.bind(this)],
71 this.jsonSchemas
= new Map
<OCPP16RequestCommand
, JSONSchemaType
<JsonObject
>>([
73 OCPP16RequestCommand
.BOOT_NOTIFICATION
,
77 path
.dirname(fileURLToPath(import.meta
.url
)),
78 '../../../assets/json-schemas/ocpp/1.6/BootNotificationResponse.json'
82 ) as JSONSchemaType
<OCPP16BootNotificationResponse
>,
85 OCPP16RequestCommand
.HEARTBEAT
,
89 path
.dirname(fileURLToPath(import.meta
.url
)),
90 '../../../assets/json-schemas/ocpp/1.6/HeartbeatResponse.json'
94 ) as JSONSchemaType
<OCPP16HeartbeatResponse
>,
97 OCPP16RequestCommand
.AUTHORIZE
,
101 path
.dirname(fileURLToPath(import.meta
.url
)),
102 '../../../assets/json-schemas/ocpp/1.6/AuthorizeResponse.json'
106 ) as JSONSchemaType
<OCPP16AuthorizeResponse
>,
109 OCPP16RequestCommand
.START_TRANSACTION
,
113 path
.dirname(fileURLToPath(import.meta
.url
)),
114 '../../../assets/json-schemas/ocpp/1.6/StartTransactionResponse.json'
118 ) as JSONSchemaType
<OCPP16StartTransactionResponse
>,
121 OCPP16RequestCommand
.STOP_TRANSACTION
,
125 path
.dirname(fileURLToPath(import.meta
.url
)),
126 '../../../assets/json-schemas/ocpp/1.6/StopTransactionResponse.json'
130 ) as JSONSchemaType
<OCPP16StopTransactionResponse
>,
133 OCPP16RequestCommand
.STATUS_NOTIFICATION
,
137 path
.dirname(fileURLToPath(import.meta
.url
)),
138 '../../../assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json'
142 ) as JSONSchemaType
<OCPP16StatusNotificationResponse
>,
145 OCPP16RequestCommand
.METER_VALUES
,
149 path
.dirname(fileURLToPath(import.meta
.url
)),
150 '../../../assets/json-schemas/ocpp/1.6/MeterValuesResponse.json'
154 ) as JSONSchemaType
<OCPP16MeterValuesResponse
>,
157 OCPP16RequestCommand
.DIAGNOSTICS_STATUS_NOTIFICATION
,
161 path
.dirname(fileURLToPath(import.meta
.url
)),
162 '../../../assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json'
166 ) as JSONSchemaType
<DiagnosticsStatusNotificationResponse
>,
169 this.validatePayload
.bind(this);
172 public async responseHandler(
173 chargingStation
: ChargingStation
,
174 commandName
: OCPP16RequestCommand
,
176 requestPayload
: JsonType
178 if (chargingStation
.isRegistered() || commandName
=== OCPP16RequestCommand
.BOOT_NOTIFICATION
) {
180 this.responseHandlers
.has(commandName
) &&
181 ChargingStationUtils
.isRequestCommandSupported(commandName
, chargingStation
)
184 this.validatePayload(chargingStation
, commandName
, payload
);
185 await this.responseHandlers
.get(commandName
)(chargingStation
, payload
, requestPayload
);
188 `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
196 ErrorType
.NOT_IMPLEMENTED
,
197 `${commandName} is not implemented to handle response PDU ${JSON.stringify(
208 ErrorType
.SECURITY_ERROR
,
209 `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
213 )} while the charging station is not registered on the central server. `,
220 private validatePayload(
221 chargingStation
: ChargingStation
,
222 commandName
: OCPP16RequestCommand
,
225 if (this.jsonSchemas
.has(commandName
)) {
226 return this.validateResponsePayload(
229 this.jsonSchemas
.get(commandName
),
234 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
239 private handleResponseBootNotification(
240 chargingStation
: ChargingStation
,
241 payload
: OCPP16BootNotificationResponse
243 if (payload
.status === OCPP16RegistrationStatus
.ACCEPTED
) {
244 ChargingStationConfigurationUtils
.addConfigurationKey(
246 OCPP16StandardParametersKey
.HeartbeatInterval
,
247 payload
.interval
.toString(),
249 { overwrite
: true, save
: true }
251 ChargingStationConfigurationUtils
.addConfigurationKey(
253 OCPP16StandardParametersKey
.HeartBeatInterval
,
254 payload
.interval
.toString(),
256 { overwrite
: true, save
: true }
258 chargingStation
.heartbeatSetInterval
259 ? chargingStation
.restartHeartbeat()
260 : chargingStation
.startHeartbeat();
262 if (Object.values(OCPP16RegistrationStatus
).includes(payload
.status)) {
263 const logMsg
= `${chargingStation.logPrefix()} Charging station in '${
265 }' state on the central server`;
266 payload
.status === OCPP16RegistrationStatus
.REJECTED
267 ? logger
.warn(logMsg
)
268 : logger
.info(logMsg
);
271 chargingStation
.logPrefix() +
272 ' Charging station boot notification response received: %j with undefined registration status',
278 private handleResponseAuthorize(
279 chargingStation
: ChargingStation
,
280 payload
: OCPP16AuthorizeResponse
,
281 requestPayload
: OCPP16AuthorizeRequest
283 let authorizeConnectorId
: number;
284 for (const connectorId
of chargingStation
.connectors
.keys()) {
287 chargingStation
.getConnectorStatus(connectorId
)?.authorizeIdTag
=== requestPayload
.idTag
289 authorizeConnectorId
= connectorId
;
293 const isAuthorizeConnectorIdDefined
= authorizeConnectorId
!== undefined;
294 if (payload
.idTagInfo
.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
295 isAuthorizeConnectorIdDefined
&&
296 (chargingStation
.getConnectorStatus(authorizeConnectorId
).idTagAuthorized
= true);
298 `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' accepted${
299 isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}
` : ''
303 if (isAuthorizeConnectorIdDefined
) {
304 chargingStation
.getConnectorStatus(authorizeConnectorId
).idTagAuthorized
= false;
305 delete chargingStation
.getConnectorStatus(authorizeConnectorId
).authorizeIdTag
;
308 `${chargingStation.logPrefix()} IdTag '${requestPayload.idTag}' rejected with status '${
309 payload.idTagInfo.status
310 }'${isAuthorizeConnectorIdDefined ? ` on connector ${authorizeConnectorId}` : ''}`
315 private async handleResponseStartTransaction(
316 chargingStation: ChargingStation,
317 payload: OCPP16StartTransactionResponse,
318 requestPayload: OCPP16StartTransactionRequest
320 const connectorId = requestPayload.connectorId;
322 let transactionConnectorId: number;
323 for (const id of chargingStation.connectors.keys()) {
324 if (id > 0 && id === connectorId) {
325 transactionConnectorId = id;
329 if (!transactionConnectorId) {
331 chargingStation.logPrefix() +
332 ' Trying to start a transaction on a non existing connector Id ' +
333 connectorId.toString()
338 chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
339 chargingStation.getAuthorizeRemoteTxRequests() &&
340 chargingStation.getLocalAuthListEnabled() &&
341 chargingStation.hasAuthorizedTags() &&
342 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false
345 chargingStation.logPrefix() +
346 ' Trying to start a transaction with a not local authorized idTag ' +
347 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag +
348 ' on connector Id ' +
349 connectorId.toString()
351 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
355 chargingStation.getConnectorStatus(connectorId).transactionRemoteStarted === true &&
356 chargingStation.getAuthorizeRemoteTxRequests() &&
357 chargingStation.getMustAuthorizeAtRemoteStart() &&
358 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized === false &&
359 chargingStation.getConnectorStatus(connectorId).idTagAuthorized === false
362 chargingStation.logPrefix() +
363 ' Trying to start a transaction with a not authorized idTag ' +
364 chargingStation.getConnectorStatus(connectorId).authorizeIdTag +
365 ' on connector Id ' +
366 connectorId.toString()
368 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
372 chargingStation.getConnectorStatus(connectorId).idTagAuthorized &&
373 chargingStation.getConnectorStatus(connectorId).authorizeIdTag !== requestPayload.idTag
376 chargingStation.logPrefix() +
377 ' Trying to start a transaction with an idTag ' +
378 requestPayload.idTag +
379 ' different from the authorize request one ' +
380 chargingStation.getConnectorStatus(connectorId).authorizeIdTag +
381 ' on connector Id ' +
382 connectorId.toString()
384 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
388 chargingStation.getConnectorStatus(connectorId).idTagLocalAuthorized &&
389 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag !== requestPayload.idTag
392 chargingStation.logPrefix() +
393 ' Trying to start a transaction with an idTag ' +
394 requestPayload.idTag +
395 ' different from the local authorized one ' +
396 chargingStation.getConnectorStatus(connectorId).localAuthorizeIdTag +
397 ' on connector Id ' +
398 connectorId.toString()
400 await this.resetConnectorOnStartTransactionError(chargingStation, connectorId);
403 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
405 chargingStation.logPrefix() +
406 ' Trying to start a transaction on an already used connector ' +
407 connectorId.toString() +
409 chargingStation.getConnectorStatus(connectorId)
414 chargingStation.getConnectorStatus(connectorId)?.status !==
415 OCPP16ChargePointStatus.AVAILABLE &&
416 chargingStation.getConnectorStatus(connectorId)?.status !== OCPP16ChargePointStatus.PREPARING
419 `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()}
with status $
{
420 chargingStation
.getConnectorStatus(connectorId
)?.status
425 // if (!Number.isInteger(payload.transactionId)) {
427 // `${chargingStation.logPrefix()} Trying to start a transaction on connector ${connectorId.toString()}
with a non integer transaction Id $
{
428 // payload.transactionId
429 // }, converting to integer`
431 // payload.transactionId = Utils.convertToInt(payload.transactionId);
434 if (payload
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
435 chargingStation
.getConnectorStatus(connectorId
).transactionStarted
= true;
436 chargingStation
.getConnectorStatus(connectorId
).transactionId
= payload
.transactionId
;
437 chargingStation
.getConnectorStatus(connectorId
).transactionIdTag
= requestPayload
.idTag
;
438 chargingStation
.getConnectorStatus(
440 ).transactionEnergyActiveImportRegisterValue
= 0;
441 chargingStation
.getConnectorStatus(connectorId
).transactionBeginMeterValue
=
442 OCPP16ServiceUtils
.buildTransactionBeginMeterValue(
445 requestPayload
.meterStart
447 chargingStation
.getBeginEndMeterValues() &&
448 (await chargingStation
.ocppRequestService
.requestHandler
<
449 OCPP16MeterValuesRequest
,
450 OCPP16MeterValuesResponse
451 >(chargingStation
, OCPP16RequestCommand
.METER_VALUES
, {
453 transactionId
: payload
.transactionId
,
454 meterValue
: [chargingStation
.getConnectorStatus(connectorId
).transactionBeginMeterValue
],
456 await chargingStation
.ocppRequestService
.requestHandler
<
457 OCPP16StatusNotificationRequest
,
458 OCPP16StatusNotificationResponse
459 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
461 status: OCPP16ChargePointStatus
.CHARGING
,
462 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
464 chargingStation
.getConnectorStatus(connectorId
).status = OCPP16ChargePointStatus
.CHARGING
;
466 chargingStation
.logPrefix() +
468 payload
.transactionId
.toString() +
470 chargingStation
.stationInfo
.chargingStationId
+
472 connectorId
.toString() +
474 requestPayload
.idTag
+
477 if (chargingStation
.stationInfo
.powerSharedByConnectors
) {
478 chargingStation
.powerDivider
++;
480 const configuredMeterValueSampleInterval
=
481 ChargingStationConfigurationUtils
.getConfigurationKey(
483 OCPP16StandardParametersKey
.MeterValueSampleInterval
485 chargingStation
.startMeterValues(
487 configuredMeterValueSampleInterval
488 ? Utils
.convertToInt(configuredMeterValueSampleInterval
.value
) * 1000
489 : Constants
.DEFAULT_METER_VALUES_INTERVAL
493 chargingStation
.logPrefix() +
494 ' Starting transaction id ' +
495 payload
.transactionId
.toString() +
496 " REJECTED with status '" +
497 payload
?.idTagInfo
?.status +
499 requestPayload
.idTag
+
502 await this.resetConnectorOnStartTransactionError(chargingStation
, connectorId
);
506 private async resetConnectorOnStartTransactionError(
507 chargingStation
: ChargingStation
,
510 chargingStation
.resetConnectorStatus(connectorId
);
512 chargingStation
.getConnectorStatus(connectorId
).status !== OCPP16ChargePointStatus
.AVAILABLE
514 await chargingStation
.ocppRequestService
.requestHandler
<
515 OCPP16StatusNotificationRequest
,
516 OCPP16StatusNotificationResponse
517 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
519 status: OCPP16ChargePointStatus
.AVAILABLE
,
520 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
522 chargingStation
.getConnectorStatus(connectorId
).status = OCPP16ChargePointStatus
.AVAILABLE
;
526 private async handleResponseStopTransaction(
527 chargingStation
: ChargingStation
,
528 payload
: OCPP16StopTransactionResponse
,
529 requestPayload
: OCPP16StopTransactionRequest
531 const transactionConnectorId
= chargingStation
.getConnectorIdByTransactionId(
532 requestPayload
.transactionId
534 if (!transactionConnectorId
) {
536 chargingStation
.logPrefix() +
537 ' Trying to stop a non existing transaction ' +
538 requestPayload
.transactionId
.toString()
542 if (payload
.idTagInfo
?.status === OCPP16AuthorizationStatus
.ACCEPTED
) {
543 chargingStation
.getBeginEndMeterValues() &&
544 !chargingStation
.getOcppStrictCompliance() &&
545 chargingStation
.getOutOfOrderEndMeterValues() &&
546 (await chargingStation
.ocppRequestService
.requestHandler
<
547 OCPP16MeterValuesRequest
,
548 OCPP16MeterValuesResponse
549 >(chargingStation
, OCPP16RequestCommand
.METER_VALUES
, {
550 connectorId
: transactionConnectorId
,
551 transactionId
: requestPayload
.transactionId
,
553 OCPP16ServiceUtils
.buildTransactionEndMeterValue(
555 transactionConnectorId
,
556 requestPayload
.meterStop
561 !chargingStation
.isChargingStationAvailable() ||
562 !chargingStation
.isConnectorAvailable(transactionConnectorId
)
564 await chargingStation
.ocppRequestService
.requestHandler
<
565 OCPP16StatusNotificationRequest
,
566 OCPP16StatusNotificationResponse
567 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
568 connectorId
: transactionConnectorId
,
569 status: OCPP16ChargePointStatus
.UNAVAILABLE
,
570 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
572 chargingStation
.getConnectorStatus(transactionConnectorId
).status =
573 OCPP16ChargePointStatus
.UNAVAILABLE
;
575 await chargingStation
.ocppRequestService
.requestHandler
<
576 OCPP16BootNotificationRequest
,
577 OCPP16BootNotificationResponse
578 >(chargingStation
, OCPP16RequestCommand
.STATUS_NOTIFICATION
, {
579 connectorId
: transactionConnectorId
,
580 status: OCPP16ChargePointStatus
.AVAILABLE
,
581 errorCode
: OCPP16ChargePointErrorCode
.NO_ERROR
,
583 chargingStation
.getConnectorStatus(transactionConnectorId
).status =
584 OCPP16ChargePointStatus
.AVAILABLE
;
586 if (chargingStation
.stationInfo
.powerSharedByConnectors
) {
587 chargingStation
.powerDivider
--;
589 chargingStation
.resetConnectorStatus(transactionConnectorId
);
591 chargingStation
.logPrefix() +
593 requestPayload
.transactionId
.toString() +
595 chargingStation
.stationInfo
.chargingStationId
+
597 transactionConnectorId
.toString()
601 chargingStation
.logPrefix() +
602 ' Stopping transaction id ' +
603 requestPayload
.transactionId
.toString() +
604 " REJECTED with status '" +
605 payload
.idTagInfo
?.status +