Convert sendMeterValues to OCPP message sending handler
[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 );
220 await this.chargingStation.ocppRequestService.sendTransactionEndMeterValues(
221 connectorId,
222 transactionId,
223 transactionEndMeterValue
224 );
225 }
226 const stopResponse = (await this.chargingStation.ocppRequestService.sendMessageHandler(
227 OCPP16RequestCommand.STOP_TRANSACTION,
228 {
229 transactionId,
230 meterStop:
231 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId),
232 idTag: this.chargingStation.getTransactionIdTag(transactionId),
233 reason: OCPP16StopTransactionReason.UNLOCK_COMMAND,
234 }
235 )) as OCPP16StopTransactionResponse;
c0560973
JB
236 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
237 return Constants.OCPP_RESPONSE_UNLOCKED;
238 }
239 return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
240 }
93b4a429
JB
241 await this.chargingStation.ocppRequestService.sendMessageHandler(
242 OCPP16RequestCommand.STATUS_NOTIFICATION,
243 {
244 connectorId,
245 status: OCPP16ChargePointStatus.AVAILABLE,
246 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
247 }
e7aeea18 248 );
734d790d 249 this.chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
c0560973
JB
250 return Constants.OCPP_RESPONSE_UNLOCKED;
251 }
252
e7aeea18
JB
253 private handleRequestGetConfiguration(
254 commandPayload: GetConfigurationRequest
255 ): GetConfigurationResponse {
c0560973
JB
256 const configurationKey: OCPPConfigurationKey[] = [];
257 const unknownKey: string[] = [];
258 if (Utils.isEmptyArray(commandPayload.key)) {
259 for (const configuration of this.chargingStation.configuration.configurationKey) {
260 if (Utils.isUndefined(configuration.visible)) {
261 configuration.visible = true;
262 }
263 if (!configuration.visible) {
264 continue;
265 }
266 configurationKey.push({
267 key: configuration.key,
268 readonly: configuration.readonly,
269 value: configuration.value,
270 });
271 }
272 } else {
273 for (const key of commandPayload.key) {
274 const keyFound = this.chargingStation.getConfigurationKey(key);
275 if (keyFound) {
276 if (Utils.isUndefined(keyFound.visible)) {
277 keyFound.visible = true;
278 }
279 if (!keyFound.visible) {
280 continue;
281 }
282 configurationKey.push({
283 key: keyFound.key,
284 readonly: keyFound.readonly,
285 value: keyFound.value,
286 });
287 } else {
288 unknownKey.push(key);
289 }
290 }
291 }
292 return {
293 configurationKey,
294 unknownKey,
295 };
296 }
297
e7aeea18
JB
298 private handleRequestChangeConfiguration(
299 commandPayload: ChangeConfigurationRequest
300 ): ChangeConfigurationResponse {
c0560973
JB
301 // JSON request fields type sanity check
302 if (!Utils.isString(commandPayload.key)) {
e7aeea18
JB
303 logger.error(
304 `${this.chargingStation.logPrefix()} ${
78085c42 305 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
e7aeea18
JB
306 } request key field is not a string:`,
307 commandPayload
308 );
c0560973
JB
309 }
310 if (!Utils.isString(commandPayload.value)) {
e7aeea18
JB
311 logger.error(
312 `${this.chargingStation.logPrefix()} ${
78085c42 313 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
e7aeea18
JB
314 } request value field is not a string:`,
315 commandPayload
316 );
c0560973
JB
317 }
318 const keyToChange = this.chargingStation.getConfigurationKey(commandPayload.key, true);
319 if (!keyToChange) {
320 return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
321 } else if (keyToChange && keyToChange.readonly) {
322 return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
323 } else if (keyToChange && !keyToChange.readonly) {
324 const keyIndex = this.chargingStation.configuration.configurationKey.indexOf(keyToChange);
325 let valueChanged = false;
e7aeea18
JB
326 if (
327 this.chargingStation.configuration.configurationKey[keyIndex].value !== commandPayload.value
328 ) {
c0560973
JB
329 this.chargingStation.configuration.configurationKey[keyIndex].value = commandPayload.value;
330 valueChanged = true;
331 }
332 let triggerHeartbeatRestart = false;
333 if (keyToChange.key === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) {
e7aeea18
JB
334 this.chargingStation.setConfigurationKeyValue(
335 OCPP16StandardParametersKey.HeartbeatInterval,
336 commandPayload.value
337 );
c0560973
JB
338 triggerHeartbeatRestart = true;
339 }
340 if (keyToChange.key === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) {
e7aeea18
JB
341 this.chargingStation.setConfigurationKeyValue(
342 OCPP16StandardParametersKey.HeartBeatInterval,
343 commandPayload.value
344 );
c0560973
JB
345 triggerHeartbeatRestart = true;
346 }
347 if (triggerHeartbeatRestart) {
348 this.chargingStation.restartHeartbeat();
349 }
350 if (keyToChange.key === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) {
351 this.chargingStation.restartWebSocketPing();
352 }
353 if (keyToChange.reboot) {
354 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
355 }
356 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
357 }
358 }
359
e7aeea18
JB
360 private handleRequestSetChargingProfile(
361 commandPayload: SetChargingProfileRequest
362 ): SetChargingProfileResponse {
734d790d 363 if (!this.chargingStation.getConnectorStatus(commandPayload.connectorId)) {
e7aeea18
JB
364 logger.error(
365 `${this.chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector Id ${
366 commandPayload.connectorId
367 }`
368 );
c0560973
JB
369 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
370 }
e7aeea18
JB
371 if (
372 commandPayload.csChargingProfiles.chargingProfilePurpose ===
373 ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
374 commandPayload.connectorId !== 0
375 ) {
c0560973
JB
376 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
377 }
e7aeea18
JB
378 if (
379 commandPayload.csChargingProfiles.chargingProfilePurpose ===
380 ChargingProfilePurposeType.TX_PROFILE &&
381 (commandPayload.connectorId === 0 ||
382 !this.chargingStation.getConnectorStatus(commandPayload.connectorId)?.transactionStarted)
383 ) {
c0560973
JB
384 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
385 }
e7aeea18
JB
386 this.chargingStation.setChargingProfile(
387 commandPayload.connectorId,
388 commandPayload.csChargingProfiles
389 );
390 logger.debug(
391 `${this.chargingStation.logPrefix()} Charging profile(s) set, dump their stack: %j`,
392 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
393 );
c0560973
JB
394 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
395 }
396
e7aeea18
JB
397 private handleRequestClearChargingProfile(
398 commandPayload: ClearChargingProfileRequest
399 ): ClearChargingProfileResponse {
734d790d 400 if (!this.chargingStation.getConnectorStatus(commandPayload.connectorId)) {
e7aeea18
JB
401 logger.error(
402 `${this.chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector Id ${
403 commandPayload.connectorId
404 }`
405 );
c0560973
JB
406 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
407 }
e7aeea18
JB
408 if (
409 commandPayload.connectorId &&
410 !Utils.isEmptyArray(
411 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
412 )
413 ) {
734d790d 414 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles = [];
e7aeea18
JB
415 logger.debug(
416 `${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`,
417 this.chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
418 );
c0560973
JB
419 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
420 }
421 if (!commandPayload.connectorId) {
422 let clearedCP = false;
734d790d 423 for (const connectorId of this.chargingStation.connectors.keys()) {
e7aeea18
JB
424 if (
425 !Utils.isEmptyArray(this.chargingStation.getConnectorStatus(connectorId).chargingProfiles)
426 ) {
427 this.chargingStation
428 .getConnectorStatus(connectorId)
429 .chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
430 let clearCurrentCP = false;
431 if (chargingProfile.chargingProfileId === commandPayload.id) {
432 clearCurrentCP = true;
433 }
434 if (
435 !commandPayload.chargingProfilePurpose &&
436 chargingProfile.stackLevel === commandPayload.stackLevel
437 ) {
438 clearCurrentCP = true;
439 }
440 if (
441 !chargingProfile.stackLevel &&
442 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
443 ) {
444 clearCurrentCP = true;
445 }
446 if (
447 chargingProfile.stackLevel === commandPayload.stackLevel &&
448 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
449 ) {
450 clearCurrentCP = true;
451 }
452 if (clearCurrentCP) {
453 this.chargingStation.getConnectorStatus(
454 commandPayload.connectorId
455 ).chargingProfiles[index] = {} as OCPP16ChargingProfile;
456 logger.debug(
457 `${this.chargingStation.logPrefix()} Charging profile(s) cleared, dump their stack: %j`,
458 this.chargingStation.getConnectorStatus(commandPayload.connectorId)
459 .chargingProfiles
460 );
461 clearedCP = true;
462 }
463 });
c0560973
JB
464 }
465 }
466 if (clearedCP) {
467 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
468 }
469 }
470 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
471 }
472
e7aeea18
JB
473 private async handleRequestChangeAvailability(
474 commandPayload: ChangeAvailabilityRequest
475 ): Promise<ChangeAvailabilityResponse> {
c0560973 476 const connectorId: number = commandPayload.connectorId;
734d790d 477 if (!this.chargingStation.getConnectorStatus(connectorId)) {
e7aeea18
JB
478 logger.error(
479 `${this.chargingStation.logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}`
480 );
c0560973
JB
481 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
482 }
e7aeea18
JB
483 const chargePointStatus: OCPP16ChargePointStatus =
484 commandPayload.type === OCPP16AvailabilityType.OPERATIVE
485 ? OCPP16ChargePointStatus.AVAILABLE
486 : OCPP16ChargePointStatus.UNAVAILABLE;
c0560973
JB
487 if (connectorId === 0) {
488 let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
734d790d
JB
489 for (const id of this.chargingStation.connectors.keys()) {
490 if (this.chargingStation.getConnectorStatus(id)?.transactionStarted) {
c0560973
JB
491 response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
492 }
734d790d 493 this.chargingStation.getConnectorStatus(id).availability = commandPayload.type;
c0560973 494 if (response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
93b4a429
JB
495 await this.chargingStation.ocppRequestService.sendMessageHandler(
496 OCPP16RequestCommand.STATUS_NOTIFICATION,
497 {
498 connectorId: id,
499 status: chargePointStatus,
500 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
501 }
e7aeea18 502 );
734d790d 503 this.chargingStation.getConnectorStatus(id).status = chargePointStatus;
c0560973
JB
504 }
505 }
506 return response;
e7aeea18
JB
507 } else if (
508 connectorId > 0 &&
509 (this.chargingStation.getConnectorStatus(0).availability ===
510 OCPP16AvailabilityType.OPERATIVE ||
511 (this.chargingStation.getConnectorStatus(0).availability ===
512 OCPP16AvailabilityType.INOPERATIVE &&
513 commandPayload.type === OCPP16AvailabilityType.INOPERATIVE))
514 ) {
734d790d
JB
515 if (this.chargingStation.getConnectorStatus(connectorId)?.transactionStarted) {
516 this.chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
c0560973
JB
517 return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
518 }
734d790d 519 this.chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
93b4a429
JB
520 await this.chargingStation.ocppRequestService.sendMessageHandler(
521 OCPP16RequestCommand.STATUS_NOTIFICATION,
522 { connectorId, status: chargePointStatus, errorCode: OCPP16ChargePointErrorCode.NO_ERROR }
e7aeea18 523 );
734d790d 524 this.chargingStation.getConnectorStatus(connectorId).status = chargePointStatus;
c0560973
JB
525 return Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
526 }
527 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
528 }
529
e7aeea18
JB
530 private async handleRequestRemoteStartTransaction(
531 commandPayload: RemoteStartTransactionRequest
532 ): Promise<DefaultResponse> {
a7fc8211
JB
533 const transactionConnectorId: number = commandPayload.connectorId;
534 if (transactionConnectorId) {
93b4a429
JB
535 await this.chargingStation.ocppRequestService.sendMessageHandler(
536 OCPP16RequestCommand.STATUS_NOTIFICATION,
537 {
538 connectorId: transactionConnectorId,
539 status: OCPP16ChargePointStatus.PREPARING,
540 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
541 }
e7aeea18
JB
542 );
543 this.chargingStation.getConnectorStatus(transactionConnectorId).status =
544 OCPP16ChargePointStatus.PREPARING;
545 if (
546 this.chargingStation.isChargingStationAvailable() &&
547 this.chargingStation.isConnectorAvailable(transactionConnectorId)
548 ) {
e060fe58 549 // Check if authorized
a7fc8211
JB
550 if (this.chargingStation.getAuthorizeRemoteTxRequests()) {
551 let authorized = false;
e7aeea18
JB
552 if (
553 this.chargingStation.getLocalAuthListEnabled() &&
554 this.chargingStation.hasAuthorizedTags() &&
555 this.chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)
556 ) {
557 this.chargingStation.getConnectorStatus(transactionConnectorId).localAuthorizeIdTag =
558 commandPayload.idTag;
559 this.chargingStation.getConnectorStatus(transactionConnectorId).idTagLocalAuthorized =
560 true;
36f6a92e 561 authorized = true;
71068fb9 562 } else if (this.chargingStation.getMayAuthorizeAtRemoteStart()) {
2e3d65ae
JB
563 this.chargingStation.getConnectorStatus(transactionConnectorId).authorizeIdTag =
564 commandPayload.idTag;
565 const authorizeResponse: OCPP16AuthorizeResponse =
566 (await this.chargingStation.ocppRequestService.sendMessageHandler(
567 OCPP16RequestCommand.AUTHORIZE,
568 {
569 idTag: commandPayload.idTag,
570 }
571 )) as OCPP16AuthorizeResponse;
a7fc8211
JB
572 if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
573 authorized = true;
a7fc8211 574 }
71068fb9 575 } else {
e7aeea18
JB
576 logger.warn(
577 `${this.chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
578 );
a7fc8211
JB
579 }
580 if (authorized) {
581 // Authorization successful, start transaction
e7aeea18
JB
582 if (
583 this.setRemoteStartTransactionChargingProfile(
584 transactionConnectorId,
585 commandPayload.chargingProfile
586 )
587 ) {
588 this.chargingStation.getConnectorStatus(
589 transactionConnectorId
590 ).transactionRemoteStarted = true;
591 if (
592 (
e7454a1f
JB
593 (await this.chargingStation.ocppRequestService.sendMessageHandler(
594 OCPP16RequestCommand.START_TRANSACTION,
595 {
596 connectorId: transactionConnectorId,
597 idTag: commandPayload.idTag,
598 }
599 )) as OCPP16StartTransactionResponse
e7aeea18
JB
600 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
601 ) {
602 logger.debug(
603 this.chargingStation.logPrefix() +
604 ' Transaction remotely STARTED on ' +
605 this.chargingStation.stationInfo.chargingStationId +
606 '#' +
607 transactionConnectorId.toString() +
608 ' for idTag ' +
609 commandPayload.idTag
610 );
e060fe58
JB
611 return Constants.OCPP_RESPONSE_ACCEPTED;
612 }
e7aeea18
JB
613 return this.notifyRemoteStartTransactionRejected(
614 transactionConnectorId,
615 commandPayload.idTag
616 );
e060fe58 617 }
e7aeea18
JB
618 return this.notifyRemoteStartTransactionRejected(
619 transactionConnectorId,
620 commandPayload.idTag
621 );
a7fc8211 622 }
e7aeea18
JB
623 return this.notifyRemoteStartTransactionRejected(
624 transactionConnectorId,
625 commandPayload.idTag
626 );
36f6a92e 627 }
a7fc8211 628 // No authorization check required, start transaction
e7aeea18
JB
629 if (
630 this.setRemoteStartTransactionChargingProfile(
631 transactionConnectorId,
632 commandPayload.chargingProfile
633 )
634 ) {
635 this.chargingStation.getConnectorStatus(transactionConnectorId).transactionRemoteStarted =
636 true;
637 if (
638 (
e7454a1f
JB
639 (await this.chargingStation.ocppRequestService.sendMessageHandler(
640 OCPP16RequestCommand.START_TRANSACTION,
641 {
642 connectorId: transactionConnectorId,
643 idTag: commandPayload.idTag,
644 }
645 )) as OCPP16StartTransactionResponse
e7aeea18
JB
646 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
647 ) {
648 logger.debug(
649 this.chargingStation.logPrefix() +
650 ' Transaction remotely STARTED on ' +
651 this.chargingStation.stationInfo.chargingStationId +
652 '#' +
653 transactionConnectorId.toString() +
654 ' for idTag ' +
655 commandPayload.idTag
656 );
e060fe58
JB
657 return Constants.OCPP_RESPONSE_ACCEPTED;
658 }
e7aeea18
JB
659 return this.notifyRemoteStartTransactionRejected(
660 transactionConnectorId,
661 commandPayload.idTag
662 );
e060fe58 663 }
e7aeea18
JB
664 return this.notifyRemoteStartTransactionRejected(
665 transactionConnectorId,
666 commandPayload.idTag
667 );
c0560973 668 }
e7aeea18
JB
669 return this.notifyRemoteStartTransactionRejected(
670 transactionConnectorId,
671 commandPayload.idTag
672 );
c0560973 673 }
57939a9d 674 return this.notifyRemoteStartTransactionRejected(transactionConnectorId, commandPayload.idTag);
a7fc8211
JB
675 }
676
e7aeea18
JB
677 private async notifyRemoteStartTransactionRejected(
678 connectorId: number,
679 idTag: string
680 ): Promise<DefaultResponse> {
681 if (
682 this.chargingStation.getConnectorStatus(connectorId).status !==
683 OCPP16ChargePointStatus.AVAILABLE
684 ) {
93b4a429
JB
685 await this.chargingStation.ocppRequestService.sendMessageHandler(
686 OCPP16RequestCommand.STATUS_NOTIFICATION,
687 {
688 connectorId,
689 status: OCPP16ChargePointStatus.AVAILABLE,
690 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
691 }
e7aeea18
JB
692 );
693 this.chargingStation.getConnectorStatus(connectorId).status =
694 OCPP16ChargePointStatus.AVAILABLE;
e060fe58 695 }
e7aeea18
JB
696 logger.warn(
697 this.chargingStation.logPrefix() +
698 ' Remote starting transaction REJECTED on connector Id ' +
699 connectorId.toString() +
700 ', idTag ' +
701 idTag +
702 ', availability ' +
703 this.chargingStation.getConnectorStatus(connectorId).availability +
704 ', status ' +
705 this.chargingStation.getConnectorStatus(connectorId).status
706 );
c0560973
JB
707 return Constants.OCPP_RESPONSE_REJECTED;
708 }
709
e7aeea18
JB
710 private setRemoteStartTransactionChargingProfile(
711 connectorId: number,
712 cp: OCPP16ChargingProfile
713 ): boolean {
a7fc8211
JB
714 if (cp && cp.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
715 this.chargingStation.setChargingProfile(connectorId, cp);
e7aeea18
JB
716 logger.debug(
717 `${this.chargingStation.logPrefix()} Charging profile(s) set at remote start transaction, dump their stack: %j`,
718 this.chargingStation.getConnectorStatus(connectorId).chargingProfiles
719 );
a7fc8211
JB
720 return true;
721 } else if (cp && cp.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
e7aeea18
JB
722 logger.warn(
723 `${this.chargingStation.logPrefix()} Not allowed to set ${
724 cp.chargingProfilePurpose
725 } charging profile(s) at remote start transaction`
726 );
a7fc8211 727 return false;
e060fe58
JB
728 } else if (!cp) {
729 return true;
a7fc8211
JB
730 }
731 }
732
e7aeea18
JB
733 private async handleRequestRemoteStopTransaction(
734 commandPayload: RemoteStopTransactionRequest
735 ): Promise<DefaultResponse> {
c0560973 736 const transactionId = commandPayload.transactionId;
734d790d 737 for (const connectorId of this.chargingStation.connectors.keys()) {
e7aeea18
JB
738 if (
739 connectorId > 0 &&
740 this.chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId
741 ) {
93b4a429
JB
742 await this.chargingStation.ocppRequestService.sendMessageHandler(
743 OCPP16RequestCommand.STATUS_NOTIFICATION,
744 {
745 connectorId,
746 status: OCPP16ChargePointStatus.FINISHING,
747 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
748 }
e7aeea18
JB
749 );
750 this.chargingStation.getConnectorStatus(connectorId).status =
751 OCPP16ChargePointStatus.FINISHING;
68c993d5
JB
752 if (
753 this.chargingStation.getBeginEndMeterValues() &&
754 this.chargingStation.getOcppStrictCompliance() &&
755 !this.chargingStation.getOutOfOrderEndMeterValues()
756 ) {
757 // FIXME: Implement OCPP version agnostic helpers
758 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
759 this.chargingStation,
760 connectorId,
761 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId)
762 );
763 await this.chargingStation.ocppRequestService.sendTransactionEndMeterValues(
764 connectorId,
765 transactionId,
766 transactionEndMeterValue
767 );
768 }
769 await this.chargingStation.ocppRequestService.sendMessageHandler(
770 OCPP16RequestCommand.STOP_TRANSACTION,
771 {
772 transactionId,
773 meterStop:
774 this.chargingStation.getEnergyActiveImportRegisterByTransactionId(transactionId),
775 idTag: this.chargingStation.getTransactionIdTag(transactionId),
776 }
e7aeea18 777 );
c0560973
JB
778 return Constants.OCPP_RESPONSE_ACCEPTED;
779 }
780 }
e7aeea18
JB
781 logger.info(
782 this.chargingStation.logPrefix() +
783 ' Trying to remote stop a non existing transaction ' +
784 transactionId.toString()
785 );
c0560973
JB
786 return Constants.OCPP_RESPONSE_REJECTED;
787 }
47e22477 788
e7aeea18
JB
789 private async handleRequestGetDiagnostics(
790 commandPayload: GetDiagnosticsRequest
791 ): Promise<GetDiagnosticsResponse> {
792 logger.debug(
793 this.chargingStation.logPrefix() +
794 ' ' +
795 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS +
796 ' request received: %j',
797 commandPayload
798 );
a3868ec4 799 const uri = new URL(commandPayload.location);
47e22477
JB
800 if (uri.protocol.startsWith('ftp:')) {
801 let ftpClient: Client;
802 try {
e7aeea18
JB
803 const logFiles = fs
804 .readdirSync(path.resolve(__dirname, '../../../../'))
805 .filter((file) => file.endsWith('.log'))
806 .map((file) => path.join('./', file));
807 const diagnosticsArchive =
808 this.chargingStation.stationInfo.chargingStationId + '_logs.tar.gz';
47e22477
JB
809 tar.create({ gzip: true }, logFiles).pipe(fs.createWriteStream(diagnosticsArchive));
810 ftpClient = new Client();
811 const accessResponse = await ftpClient.access({
812 host: uri.host,
e8191622
JB
813 ...(!Utils.isEmptyString(uri.port) && { port: Utils.convertToInt(uri.port) }),
814 ...(!Utils.isEmptyString(uri.username) && { user: uri.username }),
815 ...(!Utils.isEmptyString(uri.password) && { password: uri.password }),
47e22477
JB
816 });
817 let uploadResponse: FTPResponse;
818 if (accessResponse.code === 220) {
819 // eslint-disable-next-line @typescript-eslint/no-misused-promises
820 ftpClient.trackProgress(async (info) => {
e7aeea18
JB
821 logger.info(
822 `${this.chargingStation.logPrefix()} ${
823 info.bytes / 1024
824 } bytes transferred from diagnostics archive ${info.name}`
825 );
826 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
827 OCPP16DiagnosticsStatus.Uploading
828 );
47e22477 829 });
e7aeea18
JB
830 uploadResponse = await ftpClient.uploadFrom(
831 path.join(path.resolve(__dirname, '../../../../'), diagnosticsArchive),
832 uri.pathname + diagnosticsArchive
833 );
47e22477 834 if (uploadResponse.code === 226) {
e7aeea18
JB
835 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
836 OCPP16DiagnosticsStatus.Uploaded
837 );
47e22477
JB
838 if (ftpClient) {
839 ftpClient.close();
840 }
841 return { fileName: diagnosticsArchive };
842 }
e7aeea18
JB
843 throw new OCPPError(
844 ErrorType.GENERIC_ERROR,
845 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
846 uploadResponse?.code && '|' + uploadResponse?.code.toString()
847 }`,
848 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
849 );
47e22477 850 }
e7aeea18
JB
851 throw new OCPPError(
852 ErrorType.GENERIC_ERROR,
853 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
854 uploadResponse?.code && '|' + uploadResponse?.code.toString()
855 }`,
856 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
857 );
47e22477 858 } catch (error) {
e7aeea18
JB
859 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
860 OCPP16DiagnosticsStatus.UploadFailed
861 );
47e22477
JB
862 if (ftpClient) {
863 ftpClient.close();
864 }
e7aeea18
JB
865 return this.handleIncomingRequestError(
866 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
867 error as Error,
868 { errorResponse: Constants.OCPP_RESPONSE_EMPTY }
869 );
47e22477
JB
870 }
871 } else {
e7aeea18
JB
872 logger.error(
873 `${this.chargingStation.logPrefix()} Unsupported protocol ${
874 uri.protocol
875 } to transfer the diagnostic logs archive`
876 );
877 await this.chargingStation.ocppRequestService.sendDiagnosticsStatusNotification(
878 OCPP16DiagnosticsStatus.UploadFailed
879 );
47e22477
JB
880 return Constants.OCPP_RESPONSE_EMPTY;
881 }
882 }
802cfa13 883
e7aeea18
JB
884 private handleRequestTriggerMessage(
885 commandPayload: OCPP16TriggerMessageRequest
886 ): OCPP16TriggerMessageResponse {
802cfa13
JB
887 try {
888 switch (commandPayload.requestedMessage) {
889 case MessageTrigger.BootNotification:
890 setTimeout(() => {
e7aeea18 891 this.chargingStation.ocppRequestService
6a8b180d
JB
892 .sendMessageHandler(
893 OCPP16RequestCommand.BOOT_NOTIFICATION,
894 {
895 chargePointModel:
896 this.chargingStation.getBootNotificationRequest().chargePointModel,
897 chargePointVendor:
898 this.chargingStation.getBootNotificationRequest().chargePointVendor,
899 chargeBoxSerialNumber:
900 this.chargingStation.getBootNotificationRequest().chargeBoxSerialNumber,
901 firmwareVersion:
902 this.chargingStation.getBootNotificationRequest().firmwareVersion,
903 chargePointSerialNumber:
904 this.chargingStation.getBootNotificationRequest().chargePointSerialNumber,
905 iccid: this.chargingStation.getBootNotificationRequest().iccid,
906 imsi: this.chargingStation.getBootNotificationRequest().imsi,
907 meterSerialNumber:
908 this.chargingStation.getBootNotificationRequest().meterSerialNumber,
909 meterType: this.chargingStation.getBootNotificationRequest().meterType,
910 },
911 { skipBufferingOnError: true, triggerMessage: true }
e7aeea18
JB
912 )
913 .catch(() => {
914 /* This is intentional */
915 });
802cfa13
JB
916 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
917 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
918 case MessageTrigger.Heartbeat:
919 setTimeout(() => {
e7aeea18 920 this.chargingStation.ocppRequestService
94a464f9 921 .sendMessageHandler(OCPP16RequestCommand.HEARTBEAT, null, { triggerMessage: true })
e7aeea18
JB
922 .catch(() => {
923 /* This is intentional */
924 });
802cfa13
JB
925 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
926 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
927 default:
928 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED;
929 }
930 } catch (error) {
e7aeea18
JB
931 return this.handleIncomingRequestError(
932 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
933 error as Error,
934 { errorResponse: Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED }
935 );
802cfa13
JB
936 }
937 }
c0560973 938}