a86e995efb477d53675da0a458e7d9e37ca32b84
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
1 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
2
3 import {
4 ChangeAvailabilityRequest,
5 ChangeConfigurationRequest,
6 ClearChargingProfileRequest,
7 GetConfigurationRequest,
8 GetDiagnosticsRequest,
9 MessageTrigger,
10 OCPP16AvailabilityType,
11 OCPP16IncomingRequestCommand,
12 OCPP16RequestCommand,
13 OCPP16TriggerMessageRequest,
14 RemoteStartTransactionRequest,
15 RemoteStopTransactionRequest,
16 ResetRequest,
17 SetChargingProfileRequest,
18 UnlockConnectorRequest,
19 } from '../../../types/ocpp/1.6/Requests';
20 import {
21 ChangeAvailabilityResponse,
22 ChangeConfigurationResponse,
23 ClearChargingProfileResponse,
24 GetConfigurationResponse,
25 GetDiagnosticsResponse,
26 OCPP16TriggerMessageResponse,
27 SetChargingProfileResponse,
28 UnlockConnectorResponse,
29 } from '../../../types/ocpp/1.6/Responses';
30 import {
31 ChargingProfilePurposeType,
32 OCPP16ChargingProfile,
33 } from '../../../types/ocpp/1.6/ChargingProfile';
34 import { Client, FTPResponse } from 'basic-ftp';
35 import {
36 OCPP16AuthorizationStatus,
37 OCPP16AuthorizeResponse,
38 OCPP16StartTransactionResponse,
39 OCPP16StopTransactionReason,
40 OCPP16StopTransactionResponse,
41 } from '../../../types/ocpp/1.6/Transaction';
42
43 import type ChargingStation from '../../ChargingStation';
44 import Constants from '../../../utils/Constants';
45 import { DefaultResponse } from '../../../types/ocpp/Responses';
46 import { ErrorType } from '../../../types/ocpp/ErrorType';
47 import { IncomingRequestHandler } from '../../../types/ocpp/Requests';
48 import { JsonType } from '../../../types/JsonType';
49 import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
50 import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
51 import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
52 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
53 import { OCPP16StandardParametersKey } from '../../../types/ocpp/1.6/Configuration';
54 import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
55 import OCPPError from '../../../exception/OCPPError';
56 import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
57 import { URL } from 'url';
58 import Utils from '../../../utils/Utils';
59 import fs from 'fs';
60 import logger from '../../../utils/Logger';
61 import path from 'path';
62 import tar from 'tar';
63
64 const moduleName = 'OCPP16IncomingRequestService';
65
66 export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
67 private incomingRequestHandlers: Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>;
68
69 public constructor(chargingStation: ChargingStation) {
70 if (new.target?.name === moduleName) {
71 throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
72 }
73 super(chargingStation);
74 this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
75 [OCPP16IncomingRequestCommand.RESET, this.handleRequestReset.bind(this)],
76 [OCPP16IncomingRequestCommand.CLEAR_CACHE, this.handleRequestClearCache.bind(this)],
77 [OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, this.handleRequestUnlockConnector.bind(this)],
78 [
79 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
80 this.handleRequestGetConfiguration.bind(this),
81 ],
82 [
83 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
84 this.handleRequestChangeConfiguration.bind(this),
85 ],
86 [
87 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
88 this.handleRequestSetChargingProfile.bind(this),
89 ],
90 [
91 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
92 this.handleRequestClearChargingProfile.bind(this),
93 ],
94 [
95 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
96 this.handleRequestChangeAvailability.bind(this),
97 ],
98 [
99 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
100 this.handleRequestRemoteStartTransaction.bind(this),
101 ],
102 [
103 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
104 this.handleRequestRemoteStopTransaction.bind(this),
105 ],
106 [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.handleRequestGetDiagnostics.bind(this)],
107 [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
108 ]);
109 }
110
111 public async handleRequest(
112 messageId: string,
113 commandName: OCPP16IncomingRequestCommand,
114 commandPayload: JsonType
115 ): Promise<void> {
116 let result: JsonType;
117 if (
118 this.chargingStation.getOcppStrictCompliance() &&
119 this.chargingStation.isInPendingState() &&
120 (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
121 commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
122 ) {
123 throw new OCPPError(
124 ErrorType.SECURITY_ERROR,
125 `${commandName} cannot be issued to handle request payload ${JSON.stringify(
126 commandPayload,
127 null,
128 2
129 )} while the charging station is in pending state on the central server`,
130 commandName
131 );
132 }
133 if (
134 this.chargingStation.isRegistered() ||
135 (!this.chargingStation.getOcppStrictCompliance() && this.chargingStation.isInUnknownState())
136 ) {
137 if (this.incomingRequestHandlers.has(commandName)) {
138 try {
139 // Call the method to build the result
140 result = await this.incomingRequestHandlers.get(commandName)(commandPayload);
141 } catch (error) {
142 // Log
143 logger.error(this.chargingStation.logPrefix() + ' Handle request error: %j', error);
144 throw error;
145 }
146 } else {
147 // Throw exception
148 throw new OCPPError(
149 ErrorType.NOT_IMPLEMENTED,
150 `${commandName} is not implemented to handle request payload ${JSON.stringify(
151 commandPayload,
152 null,
153 2
154 )}`,
155 commandName
156 );
157 }
158 } else {
159 throw new OCPPError(
160 ErrorType.SECURITY_ERROR,
161 `${commandName} cannot be issued to handle request payload ${JSON.stringify(
162 commandPayload,
163 null,
164 2
165 )} while the charging station is not registered on the central server.`,
166 commandName
167 );
168 }
169 // Send the built result
170 await this.chargingStation.ocppRequestService.sendResult(messageId, result, commandName);
171 }
172
173 // Simulate charging station restart
174 private handleRequestReset(commandPayload: ResetRequest): DefaultResponse {
175 // eslint-disable-next-line @typescript-eslint/no-misused-promises
176 setImmediate(async (): Promise<void> => {
177 await this.chargingStation.stop(
178 (commandPayload.type + 'Reset') as OCPP16StopTransactionReason
179 );
180 await Utils.sleep(this.chargingStation.stationInfo.resetTime);
181 this.chargingStation.start();
182 });
183 logger.info(
184 `${this.chargingStation.logPrefix()} ${
185 commandPayload.type
186 } reset command received, simulating it. The station will be back online in ${Utils.formatDurationMilliSeconds(
187 this.chargingStation.stationInfo.resetTime
188 )}`
189 );
190 return Constants.OCPP_RESPONSE_ACCEPTED;
191 }
192
193 private handleRequestClearCache(): DefaultResponse {
194 return Constants.OCPP_RESPONSE_ACCEPTED;
195 }
196
197 private async handleRequestUnlockConnector(
198 commandPayload: UnlockConnectorRequest
199 ): Promise<UnlockConnectorResponse> {
200 const connectorId = commandPayload.connectorId;
201 if (connectorId === 0) {
202 logger.error(
203 this.chargingStation.logPrefix() + ' Trying to unlock connector ' + connectorId.toString()
204 );
205 return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
206 }
207 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted) {
208 const transactionId = this.chargingStation.getConnectorStatus(connectorId).transactionId;
209 if (
210 this.chargingStation.getBeginEndMeterValues() &&
211 this.chargingStation.getOcppStrictCompliance() &&
212 !this.chargingStation.getOutOfOrderEndMeterValues()
213 ) {
214 // FIXME: Implement OCPP version agnostic helpers
215 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
216 this.chargingStation,
217 connectorId,
218 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId)
219 );
220 await this.chargingStation.ocppRequestService.sendMessageHandler(
221 OCPP16RequestCommand.METER_VALUES,
222 {
223 connectorId,
224 transactionId,
225 meterValue: transactionEndMeterValue,
226 }
227 );
228 }
229 const stopResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
230 OCPP16RequestCommand.STOP_TRANSACTION,
231 {
232 transactionId,
233 meterStop:
234 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId),
235 idTag: this.chargingStation.getTransactionIdTag(transactionId),
236 reason: OCPP16StopTransactionReason.UNLOCK_COMMAND,
237 }
238 )) as OCPP16StopTransactionResponse;
239 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
240 return Constants.OCPP_RESPONSE_UNLOCKED;
241 }
242 return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
243 }
244 await this.chargingStation.ocppRequestService.sendMessageHandler(
245 OCPP16RequestCommand.STATUS_NOTIFICATION,
246 {
247 connectorId,
248 status: OCPP16ChargePointStatus.AVAILABLE,
249 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
250 }
251 );
252 this.chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
253 return Constants.OCPP_RESPONSE_UNLOCKED;
254 }
255
256 private handleRequestGetConfiguration(
257 commandPayload: GetConfigurationRequest
258 ): GetConfigurationResponse {
259 const configurationKey: OCPPConfigurationKey[] = [];
260 const unknownKey: string[] = [];
261 if (Utils.isEmptyArray(commandPayload.key)) {
262 for (const configuration of this.chargingStation.configuration.configurationKey) {
263 if (Utils.isUndefined(configuration.visible)) {
264 configuration.visible = true;
265 }
266 if (!configuration.visible) {
267 continue;
268 }
269 configurationKey.push({
270 key: configuration.key,
271 readonly: configuration.readonly,
272 value: configuration.value,
273 });
274 }
275 } else {
276 for (const key of commandPayload.key) {
277 const keyFound = this.chargingStation.getConfigurationKey(key);
278 if (keyFound) {
279 if (Utils.isUndefined(keyFound.visible)) {
280 keyFound.visible = true;
281 }
282 if (!keyFound.visible) {
283 continue;
284 }
285 configurationKey.push({
286 key: keyFound.key,
287 readonly: keyFound.readonly,
288 value: keyFound.value,
289 });
290 } else {
291 unknownKey.push(key);
292 }
293 }
294 }
295 return {
296 configurationKey,
297 unknownKey,
298 };
299 }
300
301 private handleRequestChangeConfiguration(
302 commandPayload: ChangeConfigurationRequest
303 ): ChangeConfigurationResponse {
304 // JSON request fields type sanity check
305 if (!Utils.isString(commandPayload.key)) {
306 logger.error(
307 `${this.chargingStation.logPrefix()} ${
308 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
309 } request key field is not a string:`,
310 commandPayload
311 );
312 }
313 if (!Utils.isString(commandPayload.value)) {
314 logger.error(
315 `${this.chargingStation.logPrefix()} ${
316 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
317 } request value field is not a string:`,
318 commandPayload
319 );
320 }
321 const keyToChange = this.chargingStation.getConfigurationKey(commandPayload.key, true);
322 if (!keyToChange) {
323 return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
324 } else if (keyToChange && keyToChange.readonly) {
325 return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
326 } else if (keyToChange && !keyToChange.readonly) {
327 const keyIndex = this.chargingStation.configuration.configurationKey.indexOf(keyToChange);
328 let valueChanged = false;
329 if (
330 this.chargingStation.configuration.configurationKey[keyIndex].value !== commandPayload.value
331 ) {
332 this.chargingStation.configuration.configurationKey[keyIndex].value = commandPayload.value;
333 valueChanged = true;
334 }
335 let triggerHeartbeatRestart = false;
336 if (keyToChange.key === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) {
337 this.chargingStation.setConfigurationKeyValue(
338 OCPP16StandardParametersKey.HeartbeatInterval,
339 commandPayload.value
340 );
341 triggerHeartbeatRestart = true;
342 }
343 if (keyToChange.key === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) {
344 this.chargingStation.setConfigurationKeyValue(
345 OCPP16StandardParametersKey.HeartBeatInterval,
346 commandPayload.value
347 );
348 triggerHeartbeatRestart = true;
349 }
350 if (triggerHeartbeatRestart) {
351 this.chargingStation.restartHeartbeat();
352 }
353 if (keyToChange.key === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) {
354 this.chargingStation.restartWebSocketPing();
355 }
356 if (keyToChange.reboot) {
357 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
358 }
359 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
360 }
361 }
362
363 private handleRequestSetChargingProfile(
364 commandPayload: SetChargingProfileRequest
365 ): SetChargingProfileResponse {
366 if (!this.chargingStation.getConnectorStatus(commandPayload.connectorId)) {
367 logger.error(
368 `${this.chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector Id ${
369 commandPayload.connectorId
370 }`
371 );
372 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
373 }
374 if (
375 commandPayload.csChargingProfiles.chargingProfilePurpose ===
376 ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
377 commandPayload.connectorId !== 0
378 ) {
379 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
380 }
381 if (
382 commandPayload.csChargingProfiles.chargingProfilePurpose ===
383 ChargingProfilePurposeType.TX_PROFILE &&
384 (commandPayload.connectorId === 0 ||
385 !this.chargingStation.getConnectorStatus(commandPayload.connectorId)?.transactionStarted)
386 ) {
387 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
388 }
389 this.chargingStation.setChargingProfile(
390 commandPayload.connectorId,
391 commandPayload.csChargingProfiles
392 );
393 logger.debug(
394 `${this.chargingStation.logPrefix()} Charging profile(s) set, dump their stack: %j`,
395 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
396 );
397 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
398 }
399
400 private handleRequestClearChargingProfile(
401 commandPayload: ClearChargingProfileRequest
402 ): ClearChargingProfileResponse {
403 if (!this.chargingStation.getConnectorStatus(commandPayload.connectorId)) {
404 logger.error(
405 `${this.chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector Id ${
406 commandPayload.connectorId
407 }`
408 );
409 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
410 }
411 if (
412 commandPayload.connectorId &&
413 !Utils.isEmptyArray(
414 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
415 )
416 ) {
417 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles = [];
418 logger.debug(
419 `${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`,
420 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
421 );
422 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
423 }
424 if (!commandPayload.connectorId) {
425 let clearedCP = false;
426 for (const connectorId of this.chargingStation.connectors.keys()) {
427 if (
428 !Utils.isEmptyArray(this.chargingStation.getConnectorStatus(connectorId).chargingProfiles)
429 ) {
430 this.chargingStation
431 .getConnectorStatus(connectorId)
432 .chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
433 let clearCurrentCP = false;
434 if (chargingProfile.chargingProfileId === commandPayload.id) {
435 clearCurrentCP = true;
436 }
437 if (
438 !commandPayload.chargingProfilePurpose &&
439 chargingProfile.stackLevel === commandPayload.stackLevel
440 ) {
441 clearCurrentCP = true;
442 }
443 if (
444 !chargingProfile.stackLevel &&
445 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
446 ) {
447 clearCurrentCP = true;
448 }
449 if (
450 chargingProfile.stackLevel === commandPayload.stackLevel &&
451 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
452 ) {
453 clearCurrentCP = true;
454 }
455 if (clearCurrentCP) {
456 this.chargingStation.getConnectorStatus(
457 commandPayload.connectorId
458 ).chargingProfiles[index] = {} as OCPP16ChargingProfile;
459 logger.debug(
460 `${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`,
461 this.chargingStation.getConnectorStatus(commandPayload.connectorId)
462 .chargingProfiles
463 );
464 clearedCP = true;
465 }
466 });
467 }
468 }
469 if (clearedCP) {
470 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
471 }
472 }
473 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
474 }
475
476 private async handleRequestChangeAvailability(
477 commandPayload: ChangeAvailabilityRequest
478 ): Promise<ChangeAvailabilityResponse> {
479 const connectorId: number = commandPayload.connectorId;
480 if (!this.chargingStation.getConnectorStatus(connectorId)) {
481 logger.error(
482 `${this.chargingStation.logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}`
483 );
484 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
485 }
486 const chargePointStatus: OCPP16ChargePointStatus =
487 commandPayload.type === OCPP16AvailabilityType.OPERATIVE
488 ? OCPP16ChargePointStatus.AVAILABLE
489 : OCPP16ChargePointStatus.UNAVAILABLE;
490 if (connectorId === 0) {
491 let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
492 for (const id of this.chargingStation.connectors.keys()) {
493 if (this.chargingStation.getConnectorStatus(id)?.transactionStarted) {
494 response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
495 }
496 this.chargingStation.getConnectorStatus(id).availability = commandPayload.type;
497 if (response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
498 await this.chargingStation.ocppRequestService.sendMessageHandler(
499 OCPP16RequestCommand.STATUS_NOTIFICATION,
500 {
501 connectorId: id,
502 status: chargePointStatus,
503 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
504 }
505 );
506 this.chargingStation.getConnectorStatus(id).status = chargePointStatus;
507 }
508 }
509 return response;
510 } else if (
511 connectorId > 0 &&
512 (this.chargingStation.getConnectorStatus(0).availability ===
513 OCPP16AvailabilityType.OPERATIVE ||
514 (this.chargingStation.getConnectorStatus(0).availability ===
515 OCPP16AvailabilityType.INOPERATIVE &&
516 commandPayload.type === OCPP16AvailabilityType.INOPERATIVE))
517 ) {
518 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted) {
519 this.chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
520 return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
521 }
522 this.chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
523 await this.chargingStation.ocppRequestService.sendMessageHandler(
524 OCPP16RequestCommand.STATUS_NOTIFICATION,
525 { connectorId, status: chargePointStatus, errorCode: OCPP16ChargePointErrorCode.NO_ERROR }
526 );
527 this.chargingStation.getConnectorStatus(connectorId).status = chargePointStatus;
528 return Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
529 }
530 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
531 }
532
533 private async handleRequestRemoteStartTransaction(
534 commandPayload: RemoteStartTransactionRequest
535 ): Promise<DefaultResponse> {
536 const transactionConnectorId: number = commandPayload.connectorId;
537 if (transactionConnectorId) {
538 await this.chargingStation.ocppRequestService.sendMessageHandler(
539 OCPP16RequestCommand.STATUS_NOTIFICATION,
540 {
541 connectorId: transactionConnectorId,
542 status: OCPP16ChargePointStatus.PREPARING,
543 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
544 }
545 );
546 this.chargingStation.getConnectorStatus(transactionConnectorId).status =
547 OCPP16ChargePointStatus.PREPARING;
548 if (
549 this.chargingStation.isChargingStationAvailable() &&
550 this.chargingStation.isConnectorAvailable(transactionConnectorId)
551 ) {
552 // Check if authorized
553 if (this.chargingStation.getAuthorizeRemoteTxRequests()) {
554 let authorized = false;
555 if (
556 this.chargingStation.getLocalAuthListEnabled() &&
557 this.chargingStation.hasAuthorizedTags() &&
558 this.chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)
559 ) {
560 this.chargingStation.getConnectorStatus(transactionConnectorId).localAuthorizeIdTag =
561 commandPayload.idTag;
562 this.chargingStation.getConnectorStatus(transactionConnectorId).idTagLocalAuthorized =
563 true;
564 authorized = true;
565 } else if (this.chargingStation.getMayAuthorizeAtRemoteStart()) {
566 this.chargingStation.getConnectorStatus(transactionConnectorId).authorizeIdTag =
567 commandPayload.idTag;
568 const authorizeResponse: OCPP16AuthorizeResponse =
569 (await this.chargingStation.ocppRequestService.sendMessageHandler(
570 OCPP16RequestCommand.AUTHORIZE,
571 {
572 idTag: commandPayload.idTag,
573 }
574 )) as OCPP16AuthorizeResponse;
575 if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
576 authorized = true;
577 }
578 } else {
579 logger.warn(
580 `${this.chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
581 );
582 }
583 if (authorized) {
584 // Authorization successful, start transaction
585 if (
586 this.setRemoteStartTransactionChargingProfile(
587 transactionConnectorId,
588 commandPayload.chargingProfile
589 )
590 ) {
591 this.chargingStation.getConnectorStatus(
592 transactionConnectorId
593 ).transactionRemoteStarted = true;
594 if (
595 (
596 (await this.chargingStation.ocppRequestService.sendMessageHandler(
597 OCPP16RequestCommand.START_TRANSACTION,
598 {
599 connectorId: transactionConnectorId,
600 idTag: commandPayload.idTag,
601 }
602 )) as OCPP16StartTransactionResponse
603 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
604 ) {
605 logger.debug(
606 this.chargingStation.logPrefix() +
607 ' Transaction remotely STARTED on ' +
608 this.chargingStation.stationInfo.chargingStationId +
609 '#' +
610 transactionConnectorId.toString() +
611 ' for idTag ' +
612 commandPayload.idTag
613 );
614 return Constants.OCPP_RESPONSE_ACCEPTED;
615 }
616 return this.notifyRemoteStartTransactionRejected(
617 transactionConnectorId,
618 commandPayload.idTag
619 );
620 }
621 return this.notifyRemoteStartTransactionRejected(
622 transactionConnectorId,
623 commandPayload.idTag
624 );
625 }
626 return this.notifyRemoteStartTransactionRejected(
627 transactionConnectorId,
628 commandPayload.idTag
629 );
630 }
631 // No authorization check required, start transaction
632 if (
633 this.setRemoteStartTransactionChargingProfile(
634 transactionConnectorId,
635 commandPayload.chargingProfile
636 )
637 ) {
638 this.chargingStation.getConnectorStatus(transactionConnectorId).transactionRemoteStarted =
639 true;
640 if (
641 (
642 (await this.chargingStation.ocppRequestService.sendMessageHandler(
643 OCPP16RequestCommand.START_TRANSACTION,
644 {
645 connectorId: transactionConnectorId,
646 idTag: commandPayload.idTag,
647 }
648 )) as OCPP16StartTransactionResponse
649 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
650 ) {
651 logger.debug(
652 this.chargingStation.logPrefix() +
653 ' Transaction remotely STARTED on ' +
654 this.chargingStation.stationInfo.chargingStationId +
655 '#' +
656 transactionConnectorId.toString() +
657 ' for idTag ' +
658 commandPayload.idTag
659 );
660 return Constants.OCPP_RESPONSE_ACCEPTED;
661 }
662 return this.notifyRemoteStartTransactionRejected(
663 transactionConnectorId,
664 commandPayload.idTag
665 );
666 }
667 return this.notifyRemoteStartTransactionRejected(
668 transactionConnectorId,
669 commandPayload.idTag
670 );
671 }
672 return this.notifyRemoteStartTransactionRejected(
673 transactionConnectorId,
674 commandPayload.idTag
675 );
676 }
677 return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
678 }
679
680 private async notifyRemoteStartTransactionRejected(
681 connectorId: number,
682 idTag: string
683 ): Promise<DefaultResponse> {
684 if (
685 this.chargingStation.getConnectorStatus(connectorId).status !==
686 OCPP16ChargePointStatus.AVAILABLE
687 ) {
688 await this.chargingStation.ocppRequestService.sendMessageHandler(
689 OCPP16RequestCommand.STATUS_NOTIFICATION,
690 {
691 connectorId,
692 status: OCPP16ChargePointStatus.AVAILABLE,
693 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
694 }
695 );
696 this.chargingStation.getConnectorStatus(connectorId).status =
697 OCPP16ChargePointStatus.AVAILABLE;
698 }
699 logger.warn(
700 this.chargingStation.logPrefix() +
701 ' Remote starting transaction REJECTED on connector Id ' +
702 connectorId.toString() +
703 ', idTag ' +
704 idTag +
705 ', availability ' +
706 this.chargingStation.getConnectorStatus(connectorId).availability +
707 ', status ' +
708 this.chargingStation.getConnectorStatus(connectorId).status
709 );
710 return Constants.OCPP_RESPONSE_REJECTED;
711 }
712
713 private setRemoteStartTransactionChargingProfile(
714 connectorId: number,
715 cp: OCPP16ChargingProfile
716 ): boolean {
717 if (cp && cp.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
718 this.chargingStation.setChargingProfile(connectorId, cp);
719 logger.debug(
720 `${this.chargingStation.logPrefix()} Charging profile(s) set at remote start transaction, dump their stack: %j`,
721 this.chargingStation.getConnectorStatus(connectorId).chargingProfiles
722 );
723 return true;
724 } else if (cp && cp.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
725 logger.warn(
726 `${this.chargingStation.logPrefix()} Not allowed to set ${
727 cp.chargingProfilePurpose
728 } charging profile(s) at remote start transaction`
729 );
730 return false;
731 } else if (!cp) {
732 return true;
733 }
734 }
735
736 private async handleRequestRemoteStopTransaction(
737 commandPayload: RemoteStopTransactionRequest
738 ): Promise<DefaultResponse> {
739 const transactionId = commandPayload.transactionId;
740 for (const connectorId of this.chargingStation.connectors.keys()) {
741 if (
742 connectorId > 0 &&
743 this.chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId
744 ) {
745 await this.chargingStation.ocppRequestService.sendMessageHandler(
746 OCPP16RequestCommand.STATUS_NOTIFICATION,
747 {
748 connectorId,
749 status: OCPP16ChargePointStatus.FINISHING,
750 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
751 }
752 );
753 this.chargingStation.getConnectorStatus(connectorId).status =
754 OCPP16ChargePointStatus.FINISHING;
755 if (
756 this.chargingStation.getBeginEndMeterValues() &&
757 this.chargingStation.getOcppStrictCompliance() &&
758 !this.chargingStation.getOutOfOrderEndMeterValues()
759 ) {
760 // FIXME: Implement OCPP version agnostic helpers
761 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
762 this.chargingStation,
763 connectorId,
764 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId)
765 );
766 await this.chargingStation.ocppRequestService.sendMessageHandler(
767 OCPP16RequestCommand.METER_VALUES,
768 {
769 connectorId,
770 transactionId,
771 meterValue: transactionEndMeterValue,
772 }
773 );
774 }
775 await this.chargingStation.ocppRequestService.sendMessageHandler(
776 OCPP16RequestCommand.STOP_TRANSACTION,
777 {
778 transactionId,
779 meterStop:
780 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId),
781 idTag: this.chargingStation.getTransactionIdTag(transactionId),
782 }
783 );
784 return Constants.OCPP_RESPONSE_ACCEPTED;
785 }
786 }
787 logger.info(
788 this.chargingStation.logPrefix() +
789 ' Trying to remote stop a non existing transaction ' +
790 transactionId.toString()
791 );
792 return Constants.OCPP_RESPONSE_REJECTED;
793 }
794
795 private async handleRequestGetDiagnostics(
796 commandPayload: GetDiagnosticsRequest
797 ): Promise<GetDiagnosticsResponse> {
798 logger.debug(
799 this.chargingStation.logPrefix() +
800 ' ' +
801 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS +
802 ' request received: %j',
803 commandPayload
804 );
805 const uri = new URL(commandPayload.location);
806 if (uri.protocol.startsWith('ftp:')) {
807 let ftpClient: Client;
808 try {
809 const logFiles = fs
810 .readdirSync(path.resolve(__dirname, '../../../../'))
811 .filter((file) => file.endsWith('.log'))
812 .map((file) => path.join('./', file));
813 const diagnosticsArchive =
814 this.chargingStation.stationInfo.chargingStationId + '_logs.tar.gz';
815 tar.create({ gzip: true }, logFiles).pipe(fs.createWriteStream(diagnosticsArchive));
816 ftpClient = new Client();
817 const accessResponse = await ftpClient.access({
818 host: uri.host,
819 ...(!Utils.isEmptyString(uri.port) && { port: Utils.convertToInt(uri.port) }),
820 ...(!Utils.isEmptyString(uri.username) && { user: uri.username }),
821 ...(!Utils.isEmptyString(uri.password) && { password: uri.password }),
822 });
823 let uploadResponse: FTPResponse;
824 if (accessResponse.code === 220) {
825 // eslint-disable-next-line @typescript-eslint/no-misused-promises
826 ftpClient.trackProgress(async (info) => {
827 logger.info(
828 `${this.chargingStation.logPrefix()} ${
829 info.bytes / 1024
830 } bytes transferred from diagnostics archive ${info.name}`
831 );
832 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
833 OCPP16DiagnosticsStatus.Uploading
834 );
835 });
836 uploadResponse = await ftpClient.uploadFrom(
837 path.join(path.resolve(__dirname, '../../../../'), diagnosticsArchive),
838 uri.pathname + diagnosticsArchive
839 );
840 if (uploadResponse.code === 226) {
841 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
842 OCPP16DiagnosticsStatus.Uploaded
843 );
844 if (ftpClient) {
845 ftpClient.close();
846 }
847 return { fileName: diagnosticsArchive };
848 }
849 throw new OCPPError(
850 ErrorType.GENERIC_ERROR,
851 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
852 uploadResponse?.code && '|' + uploadResponse?.code.toString()
853 }`,
854 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
855 );
856 }
857 throw new OCPPError(
858 ErrorType.GENERIC_ERROR,
859 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
860 uploadResponse?.code && '|' + uploadResponse?.code.toString()
861 }`,
862 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
863 );
864 } catch (error) {
865 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
866 OCPP16DiagnosticsStatus.UploadFailed
867 );
868 if (ftpClient) {
869 ftpClient.close();
870 }
871 return this.handleIncomingRequestError(
872 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
873 error as Error,
874 { errorResponse: Constants.OCPP_RESPONSE_EMPTY }
875 );
876 }
877 } else {
878 logger.error(
879 `${this.chargingStation.logPrefix()} Unsupported protocol ${
880 uri.protocol
881 } to transfer the diagnostic logs archive`
882 );
883 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
884 OCPP16DiagnosticsStatus.UploadFailed
885 );
886 return Constants.OCPP_RESPONSE_EMPTY;
887 }
888 }
889
890 private handleRequestTriggerMessage(
891 commandPayload: OCPP16TriggerMessageRequest
892 ): OCPP16TriggerMessageResponse {
893 try {
894 switch (commandPayload.requestedMessage) {
895 case MessageTrigger.BootNotification:
896 setTimeout(() => {
897 this.chargingStation.ocppRequestService
898 .sendMessageHandler(
899 OCPP16RequestCommand.BOOT_NOTIFICATION,
900 {
901 chargePointModel:
902 this.chargingStation.getBootNotificationRequest().chargePointModel,
903 chargePointVendor:
904 this.chargingStation.getBootNotificationRequest().chargePointVendor,
905 chargeBoxSerialNumber:
906 this.chargingStation.getBootNotificationRequest().chargeBoxSerialNumber,
907 firmwareVersion:
908 this.chargingStation.getBootNotificationRequest().firmwareVersion,
909 chargePointSerialNumber:
910 this.chargingStation.getBootNotificationRequest().chargePointSerialNumber,
911 iccid: this.chargingStation.getBootNotificationRequest().iccid,
912 imsi: this.chargingStation.getBootNotificationRequest().imsi,
913 meterSerialNumber:
914 this.chargingStation.getBootNotificationRequest().meterSerialNumber,
915 meterType: this.chargingStation.getBootNotificationRequest().meterType,
916 },
917 { skipBufferingOnError: true, triggerMessage: true }
918 )
919 .catch(() => {
920 /* This is intentional */
921 });
922 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
923 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
924 case MessageTrigger.Heartbeat:
925 setTimeout(() => {
926 this.chargingStation.ocppRequestService
927 .sendMessageHandler(OCPP16RequestCommand.HEARTBEAT, null, { triggerMessage: true })
928 .catch(() => {
929 /* This is intentional */
930 });
931 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
932 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
933 default:
934 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED;
935 }
936 } catch (error) {
937 return this.handleIncomingRequestError(
938 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
939 error as Error,
940 { errorResponse: Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED }
941 );
942 }
943 }
944 }