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