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