OCPP: Cleanup helpers
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
... / ...
CommitLineData
1// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
2
3import fs from 'fs';
4import path from 'path';
5import { URL, fileURLToPath } from 'url';
6
7import type { JSONSchemaType } from 'ajv';
8import { Client, FTPResponse } from 'basic-ftp';
9import tar from 'tar';
10
11import OCPPError from '../../../exception/OCPPError';
12import type { JsonObject, JsonType } from '../../../types/JsonType';
13import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
14import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
15import {
16 ChargingProfilePurposeType,
17 OCPP16ChargingProfile,
18} from '../../../types/ocpp/1.6/ChargingProfile';
19import {
20 OCPP16StandardParametersKey,
21 OCPP16SupportedFeatureProfiles,
22} from '../../../types/ocpp/1.6/Configuration';
23import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
24import {
25 ChangeAvailabilityRequest,
26 ChangeConfigurationRequest,
27 ClearChargingProfileRequest,
28 DiagnosticsStatusNotificationRequest,
29 GetConfigurationRequest,
30 GetDiagnosticsRequest,
31 MessageTrigger,
32 OCPP16AvailabilityType,
33 OCPP16BootNotificationRequest,
34 OCPP16ClearCacheRequest,
35 OCPP16HeartbeatRequest,
36 OCPP16IncomingRequestCommand,
37 OCPP16RequestCommand,
38 OCPP16StatusNotificationRequest,
39 OCPP16TriggerMessageRequest,
40 RemoteStartTransactionRequest,
41 RemoteStopTransactionRequest,
42 ResetRequest,
43 SetChargingProfileRequest,
44 UnlockConnectorRequest,
45} from '../../../types/ocpp/1.6/Requests';
46import type {
47 ChangeAvailabilityResponse,
48 ChangeConfigurationResponse,
49 ClearChargingProfileResponse,
50 DiagnosticsStatusNotificationResponse,
51 GetConfigurationResponse,
52 GetDiagnosticsResponse,
53 OCPP16BootNotificationResponse,
54 OCPP16HeartbeatResponse,
55 OCPP16StatusNotificationResponse,
56 OCPP16TriggerMessageResponse,
57 SetChargingProfileResponse,
58 UnlockConnectorResponse,
59} from '../../../types/ocpp/1.6/Responses';
60import {
61 OCPP16AuthorizationStatus,
62 OCPP16AuthorizeRequest,
63 OCPP16AuthorizeResponse,
64 OCPP16StartTransactionRequest,
65 OCPP16StartTransactionResponse,
66 OCPP16StopTransactionReason,
67} from '../../../types/ocpp/1.6/Transaction';
68import type { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
69import { ErrorType } from '../../../types/ocpp/ErrorType';
70import type { IncomingRequestHandler } from '../../../types/ocpp/Requests';
71import type { DefaultResponse } from '../../../types/ocpp/Responses';
72import Constants from '../../../utils/Constants';
73import logger from '../../../utils/Logger';
74import Utils from '../../../utils/Utils';
75import type ChargingStation from '../../ChargingStation';
76import { ChargingStationConfigurationUtils } from '../../ChargingStationConfigurationUtils';
77import { ChargingStationUtils } from '../../ChargingStationUtils';
78import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
79import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
80
81const moduleName = 'OCPP16IncomingRequestService';
82
83export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
84 private incomingRequestHandlers: Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>;
85 private jsonSchemas: Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>;
86
87 public constructor() {
88 if (new.target?.name === moduleName) {
89 throw new TypeError(`Cannot construct ${new.target?.name} instances directly`);
90 }
91 super();
92 this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
93 [OCPP16IncomingRequestCommand.RESET, this.handleRequestReset.bind(this)],
94 [OCPP16IncomingRequestCommand.CLEAR_CACHE, this.handleRequestClearCache.bind(this)],
95 [OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, this.handleRequestUnlockConnector.bind(this)],
96 [
97 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
98 this.handleRequestGetConfiguration.bind(this),
99 ],
100 [
101 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
102 this.handleRequestChangeConfiguration.bind(this),
103 ],
104 [
105 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
106 this.handleRequestSetChargingProfile.bind(this),
107 ],
108 [
109 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
110 this.handleRequestClearChargingProfile.bind(this),
111 ],
112 [
113 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
114 this.handleRequestChangeAvailability.bind(this),
115 ],
116 [
117 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
118 this.handleRequestRemoteStartTransaction.bind(this),
119 ],
120 [
121 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
122 this.handleRequestRemoteStopTransaction.bind(this),
123 ],
124 [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.handleRequestGetDiagnostics.bind(this)],
125 [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
126 ]);
127 this.jsonSchemas = new Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>([
128 [
129 OCPP16IncomingRequestCommand.RESET,
130 JSON.parse(
131 fs.readFileSync(
132 path.resolve(
133 path.dirname(fileURLToPath(import.meta.url)),
134 '../../../assets/json-schemas/ocpp/1.6/Reset.json'
135 ),
136 'utf8'
137 )
138 ) as JSONSchemaType<ResetRequest>,
139 ],
140 [
141 OCPP16IncomingRequestCommand.CLEAR_CACHE,
142 JSON.parse(
143 fs.readFileSync(
144 path.resolve(
145 path.dirname(fileURLToPath(import.meta.url)),
146 '../../../assets/json-schemas/ocpp/1.6/ClearCache.json'
147 ),
148 'utf8'
149 )
150 ) as JSONSchemaType<OCPP16ClearCacheRequest>,
151 ],
152 [
153 OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
154 JSON.parse(
155 fs.readFileSync(
156 path.resolve(
157 path.dirname(fileURLToPath(import.meta.url)),
158 '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json'
159 ),
160 'utf8'
161 )
162 ) as JSONSchemaType<UnlockConnectorRequest>,
163 ],
164 [
165 OCPP16IncomingRequestCommand.GET_CONFIGURATION,
166 JSON.parse(
167 fs.readFileSync(
168 path.resolve(
169 path.dirname(fileURLToPath(import.meta.url)),
170 '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json'
171 ),
172 'utf8'
173 )
174 ) as JSONSchemaType<GetConfigurationRequest>,
175 ],
176 [
177 OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
178 JSON.parse(
179 fs.readFileSync(
180 path.resolve(
181 path.dirname(fileURLToPath(import.meta.url)),
182 '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json'
183 ),
184 'utf8'
185 )
186 ) as JSONSchemaType<ChangeConfigurationRequest>,
187 ],
188 [
189 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
190 JSON.parse(
191 fs.readFileSync(
192 path.resolve(
193 path.dirname(fileURLToPath(import.meta.url)),
194 '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json'
195 ),
196 'utf8'
197 )
198 ) as JSONSchemaType<GetDiagnosticsRequest>,
199 ],
200 [
201 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
202 JSON.parse(
203 fs.readFileSync(
204 path.resolve(
205 path.dirname(fileURLToPath(import.meta.url)),
206 '../../../assets/json-schemas/ocpp/1.6/SetChargingProfile.json'
207 ),
208 'utf8'
209 )
210 ) as JSONSchemaType<SetChargingProfileRequest>,
211 ],
212 [
213 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
214 JSON.parse(
215 fs.readFileSync(
216 path.resolve(
217 path.dirname(fileURLToPath(import.meta.url)),
218 '../../../assets/json-schemas/ocpp/1.6/ClearChargingProfile.json'
219 ),
220 'utf8'
221 )
222 ) as JSONSchemaType<ClearChargingProfileRequest>,
223 ],
224 [
225 OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
226 JSON.parse(
227 fs.readFileSync(
228 path.resolve(
229 path.dirname(fileURLToPath(import.meta.url)),
230 '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json'
231 ),
232 'utf8'
233 )
234 ) as JSONSchemaType<ChangeAvailabilityRequest>,
235 ],
236 [
237 OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
238 JSON.parse(
239 fs.readFileSync(
240 path.resolve(
241 path.dirname(fileURLToPath(import.meta.url)),
242 '../../../assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json'
243 ),
244 'utf8'
245 )
246 ) as JSONSchemaType<RemoteStartTransactionRequest>,
247 ],
248 [
249 OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
250 JSON.parse(
251 fs.readFileSync(
252 path.resolve(
253 path.dirname(fileURLToPath(import.meta.url)),
254 '../../../assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json'
255 ),
256 'utf8'
257 )
258 ) as JSONSchemaType<RemoteStopTransactionRequest>,
259 ],
260 [
261 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
262 JSON.parse(
263 fs.readFileSync(
264 path.resolve(
265 path.dirname(fileURLToPath(import.meta.url)),
266 '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json'
267 ),
268 'utf8'
269 )
270 ) as JSONSchemaType<OCPP16TriggerMessageRequest>,
271 ],
272 ]);
273 this.validatePayload.bind(this);
274 }
275
276 public async incomingRequestHandler(
277 chargingStation: ChargingStation,
278 messageId: string,
279 commandName: OCPP16IncomingRequestCommand,
280 commandPayload: JsonType
281 ): Promise<void> {
282 let response: JsonType;
283 if (
284 chargingStation.getOcppStrictCompliance() &&
285 chargingStation.isInPendingState() &&
286 (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
287 commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
288 ) {
289 throw new OCPPError(
290 ErrorType.SECURITY_ERROR,
291 `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
292 commandPayload,
293 null,
294 2
295 )} while the charging station is in pending state on the central server`,
296 commandName,
297 commandPayload
298 );
299 }
300 if (
301 chargingStation.isRegistered() ||
302 (!chargingStation.getOcppStrictCompliance() && chargingStation.isInUnknownState())
303 ) {
304 if (
305 this.incomingRequestHandlers.has(commandName) &&
306 OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
307 ) {
308 try {
309 this.validatePayload(chargingStation, commandName, commandPayload);
310 // Call the method to build the response
311 response = await this.incomingRequestHandlers.get(commandName)(
312 chargingStation,
313 commandPayload
314 );
315 } catch (error) {
316 // Log
317 logger.error(
318 `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
319 error
320 );
321 throw error;
322 }
323 } else {
324 // Throw exception
325 throw new OCPPError(
326 ErrorType.NOT_IMPLEMENTED,
327 `${commandName} is not implemented to handle request PDU ${JSON.stringify(
328 commandPayload,
329 null,
330 2
331 )}`,
332 commandName,
333 commandPayload
334 );
335 }
336 } else {
337 throw new OCPPError(
338 ErrorType.SECURITY_ERROR,
339 `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
340 commandPayload,
341 null,
342 2
343 )} while the charging station is not registered on the central server.`,
344 commandName,
345 commandPayload
346 );
347 }
348 // Send the built response
349 await chargingStation.ocppRequestService.sendResponse(
350 chargingStation,
351 messageId,
352 response,
353 commandName
354 );
355 }
356
357 private validatePayload(
358 chargingStation: ChargingStation,
359 commandName: OCPP16IncomingRequestCommand,
360 commandPayload: JsonType
361 ): boolean {
362 if (this.jsonSchemas.has(commandName)) {
363 return this.validateIncomingRequestPayload(
364 chargingStation,
365 commandName,
366 this.jsonSchemas.get(commandName),
367 commandPayload
368 );
369 }
370 logger.warn(
371 `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
372 );
373 return false;
374 }
375
376 // Simulate charging station restart
377 private handleRequestReset(
378 chargingStation: ChargingStation,
379 commandPayload: ResetRequest
380 ): DefaultResponse {
381 // eslint-disable-next-line @typescript-eslint/no-misused-promises
382 setImmediate(async (): Promise<void> => {
383 await chargingStation.reset((commandPayload.type + 'Reset') as OCPP16StopTransactionReason);
384 });
385 logger.info(
386 `${chargingStation.logPrefix()} ${
387 commandPayload.type
388 } reset command received, simulating it. The station will be back online in ${Utils.formatDurationMilliSeconds(
389 chargingStation.stationInfo.resetTime
390 )}`
391 );
392 return Constants.OCPP_RESPONSE_ACCEPTED;
393 }
394
395 private handleRequestClearCache(): DefaultResponse {
396 return Constants.OCPP_RESPONSE_ACCEPTED;
397 }
398
399 private async handleRequestUnlockConnector(
400 chargingStation: ChargingStation,
401 commandPayload: UnlockConnectorRequest
402 ): Promise<UnlockConnectorResponse> {
403 const connectorId = commandPayload.connectorId;
404 if (connectorId === 0) {
405 logger.error(
406 chargingStation.logPrefix() + ' Trying to unlock connector ' + connectorId.toString()
407 );
408 return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
409 }
410 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
411 const stopResponse = await chargingStation.stopTransactionOnConnector(
412 connectorId,
413 OCPP16StopTransactionReason.UNLOCK_COMMAND
414 );
415 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
416 return Constants.OCPP_RESPONSE_UNLOCKED;
417 }
418 return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
419 }
420 await chargingStation.ocppRequestService.requestHandler<
421 OCPP16StatusNotificationRequest,
422 OCPP16StatusNotificationResponse
423 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
424 connectorId,
425 status: OCPP16ChargePointStatus.AVAILABLE,
426 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
427 });
428 chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
429 return Constants.OCPP_RESPONSE_UNLOCKED;
430 }
431
432 private handleRequestGetConfiguration(
433 chargingStation: ChargingStation,
434 commandPayload: GetConfigurationRequest
435 ): GetConfigurationResponse {
436 const configurationKey: OCPPConfigurationKey[] = [];
437 const unknownKey: string[] = [];
438 if (Utils.isEmptyArray(commandPayload.key) === true) {
439 for (const configuration of chargingStation.ocppConfiguration.configurationKey) {
440 if (Utils.isUndefined(configuration.visible) === true) {
441 configuration.visible = true;
442 }
443 if (configuration.visible === false) {
444 continue;
445 }
446 configurationKey.push({
447 key: configuration.key,
448 readonly: configuration.readonly,
449 value: configuration.value,
450 });
451 }
452 } else {
453 for (const key of commandPayload.key) {
454 const keyFound = ChargingStationConfigurationUtils.getConfigurationKey(
455 chargingStation,
456 key
457 );
458 if (keyFound) {
459 if (Utils.isUndefined(keyFound.visible) === true) {
460 keyFound.visible = true;
461 }
462 if (keyFound.visible === false) {
463 continue;
464 }
465 configurationKey.push({
466 key: keyFound.key,
467 readonly: keyFound.readonly,
468 value: keyFound.value,
469 });
470 } else {
471 unknownKey.push(key);
472 }
473 }
474 }
475 return {
476 configurationKey,
477 unknownKey,
478 };
479 }
480
481 private handleRequestChangeConfiguration(
482 chargingStation: ChargingStation,
483 commandPayload: ChangeConfigurationRequest
484 ): ChangeConfigurationResponse {
485 const keyToChange = ChargingStationConfigurationUtils.getConfigurationKey(
486 chargingStation,
487 commandPayload.key,
488 true
489 );
490 if (!keyToChange) {
491 return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
492 } else if (keyToChange && keyToChange.readonly) {
493 return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
494 } else if (keyToChange && !keyToChange.readonly) {
495 let valueChanged = false;
496 if (keyToChange.value !== commandPayload.value) {
497 ChargingStationConfigurationUtils.setConfigurationKeyValue(
498 chargingStation,
499 commandPayload.key,
500 commandPayload.value,
501 true
502 );
503 valueChanged = true;
504 }
505 let triggerHeartbeatRestart = false;
506 if (keyToChange.key === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) {
507 ChargingStationConfigurationUtils.setConfigurationKeyValue(
508 chargingStation,
509 OCPP16StandardParametersKey.HeartbeatInterval,
510 commandPayload.value
511 );
512 triggerHeartbeatRestart = true;
513 }
514 if (keyToChange.key === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) {
515 ChargingStationConfigurationUtils.setConfigurationKeyValue(
516 chargingStation,
517 OCPP16StandardParametersKey.HeartBeatInterval,
518 commandPayload.value
519 );
520 triggerHeartbeatRestart = true;
521 }
522 if (triggerHeartbeatRestart) {
523 chargingStation.restartHeartbeat();
524 }
525 if (keyToChange.key === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) {
526 chargingStation.restartWebSocketPing();
527 }
528 if (keyToChange.reboot) {
529 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
530 }
531 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
532 }
533 }
534
535 private handleRequestSetChargingProfile(
536 chargingStation: ChargingStation,
537 commandPayload: SetChargingProfileRequest
538 ): SetChargingProfileResponse {
539 if (
540 !OCPP16ServiceUtils.checkFeatureProfile(
541 chargingStation,
542 OCPP16SupportedFeatureProfiles.SmartCharging,
543 OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
544 )
545 ) {
546 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED;
547 }
548 if (!chargingStation.getConnectorStatus(commandPayload.connectorId)) {
549 logger.error(
550 `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector Id ${
551 commandPayload.connectorId
552 }`
553 );
554 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
555 }
556 if (
557 commandPayload.csChargingProfiles.chargingProfilePurpose ===
558 ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
559 commandPayload.connectorId !== 0
560 ) {
561 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
562 }
563 if (
564 commandPayload.csChargingProfiles.chargingProfilePurpose ===
565 ChargingProfilePurposeType.TX_PROFILE &&
566 (commandPayload.connectorId === 0 ||
567 chargingStation.getConnectorStatus(commandPayload.connectorId)?.transactionStarted ===
568 false)
569 ) {
570 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED;
571 }
572 OCPP16ServiceUtils.setChargingProfile(
573 chargingStation,
574 commandPayload.connectorId,
575 commandPayload.csChargingProfiles
576 );
577 logger.debug(
578 `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${
579 commandPayload.connectorId
580 }, dump their stack: %j`,
581 chargingStation.getConnectorStatus(commandPayload.connectorId).chargingProfiles
582 );
583 return Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED;
584 }
585
586 private handleRequestClearChargingProfile(
587 chargingStation: ChargingStation,
588 commandPayload: ClearChargingProfileRequest
589 ): ClearChargingProfileResponse {
590 if (
591 !OCPP16ServiceUtils.checkFeatureProfile(
592 chargingStation,
593 OCPP16SupportedFeatureProfiles.SmartCharging,
594 OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
595 )
596 ) {
597 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
598 }
599 const connectorStatus = chargingStation.getConnectorStatus(commandPayload.connectorId);
600 if (!connectorStatus) {
601 logger.error(
602 `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector Id ${
603 commandPayload.connectorId
604 }`
605 );
606 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
607 }
608 if (commandPayload.connectorId && !Utils.isEmptyArray(connectorStatus.chargingProfiles)) {
609 connectorStatus.chargingProfiles = [];
610 logger.debug(
611 `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${
612 commandPayload.connectorId
613 }, dump their stack: %j`,
614 connectorStatus.chargingProfiles
615 );
616 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
617 }
618 if (!commandPayload.connectorId) {
619 let clearedCP = false;
620 for (const connectorId of chargingStation.connectors.keys()) {
621 if (!Utils.isEmptyArray(chargingStation.getConnectorStatus(connectorId).chargingProfiles)) {
622 chargingStation
623 .getConnectorStatus(connectorId)
624 .chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
625 let clearCurrentCP = false;
626 if (chargingProfile.chargingProfileId === commandPayload.id) {
627 clearCurrentCP = true;
628 }
629 if (
630 !commandPayload.chargingProfilePurpose &&
631 chargingProfile.stackLevel === commandPayload.stackLevel
632 ) {
633 clearCurrentCP = true;
634 }
635 if (
636 !chargingProfile.stackLevel &&
637 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
638 ) {
639 clearCurrentCP = true;
640 }
641 if (
642 chargingProfile.stackLevel === commandPayload.stackLevel &&
643 chargingProfile.chargingProfilePurpose === commandPayload.chargingProfilePurpose
644 ) {
645 clearCurrentCP = true;
646 }
647 if (clearCurrentCP) {
648 connectorStatus.chargingProfiles.splice(index, 1);
649 logger.debug(
650 `${chargingStation.logPrefix()} Matching charging profile(s) cleared on connector id ${
651 commandPayload.connectorId
652 }, dump their stack: %j`,
653 connectorStatus.chargingProfiles
654 );
655 clearedCP = true;
656 }
657 });
658 }
659 }
660 if (clearedCP) {
661 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
662 }
663 }
664 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
665 }
666
667 private async handleRequestChangeAvailability(
668 chargingStation: ChargingStation,
669 commandPayload: ChangeAvailabilityRequest
670 ): Promise<ChangeAvailabilityResponse> {
671 const connectorId: number = commandPayload.connectorId;
672 if (!chargingStation.getConnectorStatus(connectorId)) {
673 logger.error(
674 `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector Id ${connectorId.toString()}`
675 );
676 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
677 }
678 const chargePointStatus: OCPP16ChargePointStatus =
679 commandPayload.type === OCPP16AvailabilityType.OPERATIVE
680 ? OCPP16ChargePointStatus.AVAILABLE
681 : OCPP16ChargePointStatus.UNAVAILABLE;
682 if (connectorId === 0) {
683 let response: ChangeAvailabilityResponse = Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
684 for (const id of chargingStation.connectors.keys()) {
685 if (chargingStation.getConnectorStatus(id)?.transactionStarted === true) {
686 response = Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
687 }
688 chargingStation.getConnectorStatus(id).availability = commandPayload.type;
689 if (response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
690 await chargingStation.ocppRequestService.requestHandler<
691 OCPP16StatusNotificationRequest,
692 OCPP16StatusNotificationResponse
693 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
694 connectorId: id,
695 status: chargePointStatus,
696 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
697 });
698 chargingStation.getConnectorStatus(id).status = chargePointStatus;
699 }
700 }
701 return response;
702 } else if (
703 connectorId > 0 &&
704 (chargingStation.getConnectorStatus(0).availability === OCPP16AvailabilityType.OPERATIVE ||
705 (chargingStation.getConnectorStatus(0).availability ===
706 OCPP16AvailabilityType.INOPERATIVE &&
707 commandPayload.type === OCPP16AvailabilityType.INOPERATIVE))
708 ) {
709 if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
710 chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
711 return Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED;
712 }
713 chargingStation.getConnectorStatus(connectorId).availability = commandPayload.type;
714 await chargingStation.ocppRequestService.requestHandler<
715 OCPP16StatusNotificationRequest,
716 OCPP16StatusNotificationResponse
717 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
718 connectorId,
719 status: chargePointStatus,
720 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
721 });
722 chargingStation.getConnectorStatus(connectorId).status = chargePointStatus;
723 return Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED;
724 }
725 return Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED;
726 }
727
728 private async handleRequestRemoteStartTransaction(
729 chargingStation: ChargingStation,
730 commandPayload: RemoteStartTransactionRequest
731 ): Promise<DefaultResponse> {
732 const transactionConnectorId = commandPayload.connectorId;
733 const connectorStatus = chargingStation.getConnectorStatus(transactionConnectorId);
734 if (transactionConnectorId) {
735 const remoteStartTransactionLogMsg =
736 chargingStation.logPrefix() +
737 ' Transaction remotely STARTED on ' +
738 chargingStation.stationInfo.chargingStationId +
739 '#' +
740 transactionConnectorId.toString() +
741 " for idTag '" +
742 commandPayload.idTag +
743 "'";
744 await chargingStation.ocppRequestService.requestHandler<
745 OCPP16StatusNotificationRequest,
746 OCPP16StatusNotificationResponse
747 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
748 connectorId: transactionConnectorId,
749 status: OCPP16ChargePointStatus.PREPARING,
750 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
751 });
752 connectorStatus.status = OCPP16ChargePointStatus.PREPARING;
753 if (chargingStation.isChargingStationAvailable() && connectorStatus) {
754 // Check if authorized
755 if (chargingStation.getAuthorizeRemoteTxRequests()) {
756 let authorized = false;
757 if (
758 chargingStation.getLocalAuthListEnabled() &&
759 chargingStation.hasAuthorizedTags() &&
760 chargingStation.authorizedTagsCache
761 .getAuthorizedTags(
762 ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo)
763 )
764 .find((value) => value === commandPayload.idTag)
765 ) {
766 connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
767 connectorStatus.idTagLocalAuthorized = true;
768 authorized = true;
769 } else if (chargingStation.getMustAuthorizeAtRemoteStart()) {
770 connectorStatus.authorizeIdTag = commandPayload.idTag;
771 const authorizeResponse: OCPP16AuthorizeResponse =
772 await chargingStation.ocppRequestService.requestHandler<
773 OCPP16AuthorizeRequest,
774 OCPP16AuthorizeResponse
775 >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
776 idTag: commandPayload.idTag,
777 });
778 if (authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
779 authorized = true;
780 }
781 } else {
782 logger.warn(
783 `${chargingStation.logPrefix()} The charging station configuration expects authorize at remote start transaction but local authorization or authorize isn't enabled`
784 );
785 }
786 if (authorized) {
787 // Authorization successful, start transaction
788 if (
789 this.setRemoteStartTransactionChargingProfile(
790 chargingStation,
791 transactionConnectorId,
792 commandPayload.chargingProfile
793 )
794 ) {
795 connectorStatus.transactionRemoteStarted = true;
796 if (
797 (
798 await chargingStation.ocppRequestService.requestHandler<
799 OCPP16StartTransactionRequest,
800 OCPP16StartTransactionResponse
801 >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
802 connectorId: transactionConnectorId,
803 idTag: commandPayload.idTag,
804 })
805 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
806 ) {
807 logger.debug(remoteStartTransactionLogMsg);
808 return Constants.OCPP_RESPONSE_ACCEPTED;
809 }
810 return this.notifyRemoteStartTransactionRejected(
811 chargingStation,
812 transactionConnectorId,
813 commandPayload.idTag
814 );
815 }
816 return this.notifyRemoteStartTransactionRejected(
817 chargingStation,
818 transactionConnectorId,
819 commandPayload.idTag
820 );
821 }
822 return this.notifyRemoteStartTransactionRejected(
823 chargingStation,
824 transactionConnectorId,
825 commandPayload.idTag
826 );
827 }
828 // No authorization check required, start transaction
829 if (
830 this.setRemoteStartTransactionChargingProfile(
831 chargingStation,
832 transactionConnectorId,
833 commandPayload.chargingProfile
834 )
835 ) {
836 connectorStatus.transactionRemoteStarted = true;
837 if (
838 (
839 await chargingStation.ocppRequestService.requestHandler<
840 OCPP16StartTransactionRequest,
841 OCPP16StartTransactionResponse
842 >(chargingStation, OCPP16RequestCommand.START_TRANSACTION, {
843 connectorId: transactionConnectorId,
844 idTag: commandPayload.idTag,
845 })
846 ).idTagInfo.status === OCPP16AuthorizationStatus.ACCEPTED
847 ) {
848 logger.debug(remoteStartTransactionLogMsg);
849 return Constants.OCPP_RESPONSE_ACCEPTED;
850 }
851 return this.notifyRemoteStartTransactionRejected(
852 chargingStation,
853 transactionConnectorId,
854 commandPayload.idTag
855 );
856 }
857 return this.notifyRemoteStartTransactionRejected(
858 chargingStation,
859 transactionConnectorId,
860 commandPayload.idTag
861 );
862 }
863 return this.notifyRemoteStartTransactionRejected(
864 chargingStation,
865 transactionConnectorId,
866 commandPayload.idTag
867 );
868 }
869 return this.notifyRemoteStartTransactionRejected(
870 chargingStation,
871 transactionConnectorId,
872 commandPayload.idTag
873 );
874 }
875
876 private async notifyRemoteStartTransactionRejected(
877 chargingStation: ChargingStation,
878 connectorId: number,
879 idTag: string
880 ): Promise<DefaultResponse> {
881 if (
882 chargingStation.getConnectorStatus(connectorId).status !== OCPP16ChargePointStatus.AVAILABLE
883 ) {
884 await chargingStation.ocppRequestService.requestHandler<
885 OCPP16StatusNotificationRequest,
886 OCPP16StatusNotificationResponse
887 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
888 connectorId,
889 status: OCPP16ChargePointStatus.AVAILABLE,
890 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
891 });
892 chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.AVAILABLE;
893 }
894 logger.warn(
895 chargingStation.logPrefix() +
896 ' Remote starting transaction REJECTED on connector Id ' +
897 connectorId.toString() +
898 ", idTag '" +
899 idTag +
900 "', availability '" +
901 chargingStation.getConnectorStatus(connectorId).availability +
902 "', status '" +
903 chargingStation.getConnectorStatus(connectorId).status +
904 "'"
905 );
906 return Constants.OCPP_RESPONSE_REJECTED;
907 }
908
909 private setRemoteStartTransactionChargingProfile(
910 chargingStation: ChargingStation,
911 connectorId: number,
912 cp: OCPP16ChargingProfile
913 ): boolean {
914 if (cp && cp.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
915 OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, cp);
916 logger.debug(
917 `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on connector id ${connectorId}, dump their stack: %j`,
918 chargingStation.getConnectorStatus(connectorId).chargingProfiles
919 );
920 return true;
921 } else if (cp && cp.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
922 logger.warn(
923 `${chargingStation.logPrefix()} Not allowed to set ${
924 cp.chargingProfilePurpose
925 } charging profile(s) at remote start transaction`
926 );
927 return false;
928 } else if (!cp) {
929 return true;
930 }
931 }
932
933 private async handleRequestRemoteStopTransaction(
934 chargingStation: ChargingStation,
935 commandPayload: RemoteStopTransactionRequest
936 ): Promise<DefaultResponse> {
937 const transactionId = commandPayload.transactionId;
938 for (const connectorId of chargingStation.connectors.keys()) {
939 if (
940 connectorId > 0 &&
941 chargingStation.getConnectorStatus(connectorId)?.transactionId === transactionId
942 ) {
943 await chargingStation.ocppRequestService.requestHandler<
944 OCPP16StatusNotificationRequest,
945 OCPP16StatusNotificationResponse
946 >(chargingStation, OCPP16RequestCommand.STATUS_NOTIFICATION, {
947 connectorId,
948 status: OCPP16ChargePointStatus.FINISHING,
949 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
950 });
951 chargingStation.getConnectorStatus(connectorId).status = OCPP16ChargePointStatus.FINISHING;
952 const stopResponse = await chargingStation.stopTransactionOnConnector(
953 connectorId,
954 OCPP16StopTransactionReason.REMOTE
955 );
956 if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
957 return Constants.OCPP_RESPONSE_ACCEPTED;
958 }
959 return Constants.OCPP_RESPONSE_REJECTED;
960 }
961 }
962 logger.warn(
963 chargingStation.logPrefix() +
964 ' Trying to remote stop a non existing transaction ' +
965 transactionId.toString()
966 );
967 return Constants.OCPP_RESPONSE_REJECTED;
968 }
969
970 private async handleRequestGetDiagnostics(
971 chargingStation: ChargingStation,
972 commandPayload: GetDiagnosticsRequest
973 ): Promise<GetDiagnosticsResponse> {
974 if (
975 !OCPP16ServiceUtils.checkFeatureProfile(
976 chargingStation,
977 OCPP16SupportedFeatureProfiles.FirmwareManagement,
978 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
979 )
980 ) {
981 return Constants.OCPP_RESPONSE_EMPTY;
982 }
983 logger.debug(
984 chargingStation.logPrefix() +
985 ' ' +
986 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS +
987 ' request received: %j',
988 commandPayload
989 );
990 const uri = new URL(commandPayload.location);
991 if (uri.protocol.startsWith('ftp:')) {
992 let ftpClient: Client;
993 try {
994 const logFiles = fs
995 .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'))
996 .filter((file) => file.endsWith('.log'))
997 .map((file) => path.join('./', file));
998 const diagnosticsArchive = chargingStation.stationInfo.chargingStationId + '_logs.tar.gz';
999 tar.create({ gzip: true }, logFiles).pipe(fs.createWriteStream(diagnosticsArchive));
1000 ftpClient = new Client();
1001 const accessResponse = await ftpClient.access({
1002 host: uri.host,
1003 ...(!Utils.isEmptyString(uri.port) && { port: Utils.convertToInt(uri.port) }),
1004 ...(!Utils.isEmptyString(uri.username) && { user: uri.username }),
1005 ...(!Utils.isEmptyString(uri.password) && { password: uri.password }),
1006 });
1007 let uploadResponse: FTPResponse;
1008 if (accessResponse.code === 220) {
1009 // eslint-disable-next-line @typescript-eslint/no-misused-promises
1010 ftpClient.trackProgress(async (info) => {
1011 logger.info(
1012 `${chargingStation.logPrefix()} ${
1013 info.bytes / 1024
1014 } bytes transferred from diagnostics archive ${info.name}`
1015 );
1016 await chargingStation.ocppRequestService.requestHandler<
1017 DiagnosticsStatusNotificationRequest,
1018 DiagnosticsStatusNotificationResponse
1019 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1020 status: OCPP16DiagnosticsStatus.Uploading,
1021 });
1022 });
1023 uploadResponse = await ftpClient.uploadFrom(
1024 path.join(
1025 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'),
1026 diagnosticsArchive
1027 ),
1028 uri.pathname + diagnosticsArchive
1029 );
1030 if (uploadResponse.code === 226) {
1031 await chargingStation.ocppRequestService.requestHandler<
1032 DiagnosticsStatusNotificationRequest,
1033 DiagnosticsStatusNotificationResponse
1034 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1035 status: OCPP16DiagnosticsStatus.Uploaded,
1036 });
1037 if (ftpClient) {
1038 ftpClient.close();
1039 }
1040 return { fileName: diagnosticsArchive };
1041 }
1042 throw new OCPPError(
1043 ErrorType.GENERIC_ERROR,
1044 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
1045 uploadResponse?.code && '|' + uploadResponse?.code.toString()
1046 }`,
1047 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
1048 );
1049 }
1050 throw new OCPPError(
1051 ErrorType.GENERIC_ERROR,
1052 `Diagnostics transfer failed with error code ${accessResponse.code.toString()}${
1053 uploadResponse?.code && '|' + uploadResponse?.code.toString()
1054 }`,
1055 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
1056 );
1057 } catch (error) {
1058 await chargingStation.ocppRequestService.requestHandler<
1059 DiagnosticsStatusNotificationRequest,
1060 DiagnosticsStatusNotificationResponse
1061 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1062 status: OCPP16DiagnosticsStatus.UploadFailed,
1063 });
1064 if (ftpClient) {
1065 ftpClient.close();
1066 }
1067 return this.handleIncomingRequestError(
1068 chargingStation,
1069 OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
1070 error as Error,
1071 { errorResponse: Constants.OCPP_RESPONSE_EMPTY }
1072 );
1073 }
1074 } else {
1075 logger.error(
1076 `${chargingStation.logPrefix()} Unsupported protocol ${
1077 uri.protocol
1078 } to transfer the diagnostic logs archive`
1079 );
1080 await chargingStation.ocppRequestService.requestHandler<
1081 DiagnosticsStatusNotificationRequest,
1082 DiagnosticsStatusNotificationResponse
1083 >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
1084 status: OCPP16DiagnosticsStatus.UploadFailed,
1085 });
1086 return Constants.OCPP_RESPONSE_EMPTY;
1087 }
1088 }
1089
1090 private handleRequestTriggerMessage(
1091 chargingStation: ChargingStation,
1092 commandPayload: OCPP16TriggerMessageRequest
1093 ): OCPP16TriggerMessageResponse {
1094 if (
1095 !OCPP16ServiceUtils.checkFeatureProfile(
1096 chargingStation,
1097 OCPP16SupportedFeatureProfiles.RemoteTrigger,
1098 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE
1099 )
1100 ) {
1101 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED;
1102 }
1103 // TODO: factor out the check on connector id
1104 if (commandPayload?.connectorId < 0) {
1105 logger.warn(
1106 `${chargingStation.logPrefix()} ${
1107 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE
1108 } incoming request received with invalid connectorId ${commandPayload.connectorId}`
1109 );
1110 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED;
1111 }
1112 try {
1113 switch (commandPayload.requestedMessage) {
1114 case MessageTrigger.BootNotification:
1115 setTimeout(() => {
1116 chargingStation.ocppRequestService
1117 .requestHandler<OCPP16BootNotificationRequest, OCPP16BootNotificationResponse>(
1118 chargingStation,
1119 OCPP16RequestCommand.BOOT_NOTIFICATION,
1120 chargingStation.bootNotificationRequest,
1121 { skipBufferingOnError: true, triggerMessage: true }
1122 )
1123 .then((response) => {
1124 chargingStation.bootNotificationResponse = response;
1125 })
1126 .catch(() => {
1127 /* This is intentional */
1128 });
1129 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
1130 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
1131 case MessageTrigger.Heartbeat:
1132 setTimeout(() => {
1133 chargingStation.ocppRequestService
1134 .requestHandler<OCPP16HeartbeatRequest, OCPP16HeartbeatResponse>(
1135 chargingStation,
1136 OCPP16RequestCommand.HEARTBEAT,
1137 null,
1138 {
1139 triggerMessage: true,
1140 }
1141 )
1142 .catch(() => {
1143 /* This is intentional */
1144 });
1145 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
1146 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
1147 case MessageTrigger.StatusNotification:
1148 setTimeout(() => {
1149 if (commandPayload?.connectorId) {
1150 chargingStation.ocppRequestService
1151 .requestHandler<OCPP16StatusNotificationRequest, OCPP16StatusNotificationResponse>(
1152 chargingStation,
1153 OCPP16RequestCommand.STATUS_NOTIFICATION,
1154 {
1155 connectorId: commandPayload.connectorId,
1156 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
1157 status: chargingStation.getConnectorStatus(commandPayload.connectorId).status,
1158 },
1159 {
1160 triggerMessage: true,
1161 }
1162 )
1163 .catch(() => {
1164 /* This is intentional */
1165 });
1166 } else {
1167 for (const connectorId of chargingStation.connectors.keys()) {
1168 chargingStation.ocppRequestService
1169 .requestHandler<
1170 OCPP16StatusNotificationRequest,
1171 OCPP16StatusNotificationResponse
1172 >(
1173 chargingStation,
1174 OCPP16RequestCommand.STATUS_NOTIFICATION,
1175 {
1176 connectorId,
1177 errorCode: OCPP16ChargePointErrorCode.NO_ERROR,
1178 status: chargingStation.getConnectorStatus(connectorId).status,
1179 },
1180 {
1181 triggerMessage: true,
1182 }
1183 )
1184 .catch(() => {
1185 /* This is intentional */
1186 });
1187 }
1188 }
1189 }, Constants.OCPP_TRIGGER_MESSAGE_DELAY);
1190 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED;
1191 default:
1192 return Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED;
1193 }
1194 } catch (error) {
1195 return this.handleIncomingRequestError(
1196 chargingStation,
1197 OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
1198 error as Error,
1199 { errorResponse: Constants.OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED }
1200 );
1201 }
1202 }
1203}