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