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