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