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