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