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