Add exponential delay at reconnect
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 import { AuthorizationStatus, StartTransactionResponse, StopTransactionReason, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
2 import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
3 import ChargingStationTemplate, { PowerOutType } from '../types/ChargingStationTemplate';
4 import { ConfigurationResponse, DefaultRequestResponse, UnlockResponse } from '../types/ocpp/1.6/RequestResponses';
5 import Connectors, { Connector } from '../types/Connectors';
6 import MeterValue, { MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit } from '../types/ocpp/1.6/MeterValue';
7 import { PerformanceObserver, performance } from 'perf_hooks';
8 import WebSocket, { MessageEvent } from 'ws';
9
10 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
11 import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode';
12 import { ChargePointStatus } from '../types/ocpp/1.6/ChargePointStatus';
13 import ChargingStationInfo from '../types/ChargingStationInfo';
14 import Configuration from '../utils/Configuration';
15 import Constants from '../utils/Constants';
16 import ElectricUtils from '../utils/ElectricUtils';
17 import MeasurandValues from '../types/MeasurandValues';
18 import OCPPError from './OcppError';
19 import Requests from '../types/ocpp/1.6/Requests';
20 import Statistics from '../utils/Statistics';
21 import Utils from '../utils/Utils';
22 import crypto from 'crypto';
23 import fs from 'fs';
24 import logger from '../utils/Logger';
25
26 export default class ChargingStation {
27 private _index: number;
28 private _stationTemplateFile: string;
29 private _stationInfo: ChargingStationInfo;
30 private _bootNotificationMessage: {
31 chargePointModel: string,
32 chargePointVendor: string,
33 chargeBoxSerialNumber?: string,
34 firmwareVersion?: string,
35 };
36
37 private _connectors: Connectors;
38 private _configuration: ChargingStationConfiguration;
39 private _connectorsConfigurationHash: string;
40 private _supervisionUrl: string;
41 private _wsConnectionUrl: string;
42 private _wsConnection: WebSocket;
43 private _hasStopped: boolean;
44 private _hasSocketRestarted: boolean;
45 private _connectionTimeout: number;
46 private _autoReconnectRetryCount: number;
47 private _autoReconnectMaxRetries: number;
48 private _requests: Requests;
49 private _messageQueue: string[];
50 private _automaticTransactionGeneration: AutomaticTransactionGenerator;
51 private _authorizedTags: string[];
52 private _heartbeatInterval: number;
53 private _heartbeatSetInterval: NodeJS.Timeout;
54 private _webSocketPingSetInterval: NodeJS.Timeout;
55 private _statistics: Statistics;
56 private _performanceObserver: PerformanceObserver;
57
58 constructor(index: number, stationTemplateFile: string) {
59 this._index = index;
60 this._stationTemplateFile = stationTemplateFile;
61 this._connectors = {} as Connectors;
62 this._initialize();
63
64 this._hasStopped = false;
65 this._hasSocketRestarted = false;
66 this._connectionTimeout = Configuration.getConnectionTimeout() * 1000; // Ms, zero for disabling
67 this._autoReconnectRetryCount = 0;
68 this._autoReconnectMaxRetries = Configuration.getAutoReconnectMaxRetries(); // -1 for unlimited
69
70 this._requests = {} as Requests;
71 this._messageQueue = [] as string[];
72
73 this._authorizedTags = this._loadAndGetAuthorizedTags();
74 }
75
76 _getStationName(stationTemplate: ChargingStationTemplate): string {
77 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4);
78 }
79
80 _buildStationInfo(): ChargingStationInfo {
81 let stationTemplateFromFile: ChargingStationTemplate;
82 try {
83 // Load template file
84 const fileDescriptor = fs.openSync(this._stationTemplateFile, 'r');
85 stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as ChargingStationTemplate;
86 fs.closeSync(fileDescriptor);
87 } catch (error) {
88 logger.error('Template file ' + this._stationTemplateFile + ' loading error: %j', error);
89 throw error;
90 }
91 const stationInfo: ChargingStationInfo = stationTemplateFromFile || {} as ChargingStationInfo;
92 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
93 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
94 stationInfo.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)];
95 } else {
96 stationInfo.maxPower = stationTemplateFromFile.power as number;
97 }
98 stationInfo.name = this._getStationName(stationTemplateFromFile);
99 stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
100 return stationInfo;
101 }
102
103 get stationInfo(): ChargingStationInfo {
104 return this._stationInfo;
105 }
106
107 _initialize(): void {
108 this._stationInfo = this._buildStationInfo();
109 this._bootNotificationMessage = {
110 chargePointModel: this._stationInfo.chargePointModel,
111 chargePointVendor: this._stationInfo.chargePointVendor,
112 ...!Utils.isUndefined(this._stationInfo.chargeBoxSerialNumberPrefix) && { chargeBoxSerialNumber: this._stationInfo.chargeBoxSerialNumberPrefix },
113 ...!Utils.isUndefined(this._stationInfo.firmwareVersion) && { firmwareVersion: this._stationInfo.firmwareVersion },
114 };
115 this._configuration = this._getTemplateChargingStationConfiguration();
116 this._supervisionUrl = this._getSupervisionURL();
117 this._wsConnectionUrl = this._supervisionUrl + '/' + this._stationInfo.name;
118 // Build connectors if needed
119 const maxConnectors = this._getMaxNumberOfConnectors();
120 if (maxConnectors <= 0) {
121 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with ${maxConnectors} connectors`);
122 }
123 const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
124 if (templateMaxConnectors <= 0) {
125 logger.warn(`${this._logPrefix()} Charging station template ${this._stationTemplateFile} with no connector configurations`);
126 }
127 // Sanity check
128 if (maxConnectors > (this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) && !this._stationInfo.randomConnectors) {
129 logger.warn(`${this._logPrefix()} Number of connectors exceeds the number of connector configurations in template ${this._stationTemplateFile}, forcing random connector configurations affectation`);
130 this._stationInfo.randomConnectors = true;
131 }
132 const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this._stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
133 // FIXME: Handle shrinking the number of connectors
134 if (!this._connectors || (this._connectors && this._connectorsConfigurationHash !== connectorsConfigHash)) {
135 this._connectorsConfigurationHash = connectorsConfigHash;
136 // Add connector Id 0
137 let lastConnector = '0';
138 for (lastConnector in this._stationInfo.Connectors) {
139 if (Utils.convertToInt(lastConnector) === 0 && this._stationInfo.useConnectorId0 && this._stationInfo.Connectors[lastConnector]) {
140 this._connectors[lastConnector] = Utils.cloneObject(this._stationInfo.Connectors[lastConnector]) as Connector;
141 }
142 }
143 // Generate all connectors
144 if ((this._stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
145 for (let index = 1; index <= maxConnectors; index++) {
146 const randConnectorID = this._stationInfo.randomConnectors ? Utils.getRandomInt(Utils.convertToInt(lastConnector), 1) : index;
147 this._connectors[index] = Utils.cloneObject(this._stationInfo.Connectors[randConnectorID]) as Connector;
148 }
149 }
150 }
151 // Avoid duplication of connectors related information
152 delete this._stationInfo.Connectors;
153 // Initialize transaction attributes on connectors
154 for (const connector in this._connectors) {
155 if (!this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
156 this._initTransactionOnConnector(Utils.convertToInt(connector));
157 }
158 }
159 // OCPP parameters
160 this._addConfigurationKey('NumberOfConnectors', this._getNumberOfConnectors().toString(), true);
161 if (!this._getConfigurationKey('MeterValuesSampledData')) {
162 this._addConfigurationKey('MeterValuesSampledData', MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER);
163 }
164 this._stationInfo.powerDivider = this._getPowerDivider();
165 if (this.getEnableStatistics()) {
166 this._statistics = Statistics.getInstance();
167 this._statistics.objName = this._stationInfo.name;
168 this._performanceObserver = new PerformanceObserver((list) => {
169 const entry = list.getEntries()[0];
170 this._statistics.logPerformance(entry, Constants.ENTITY_CHARGING_STATION);
171 this._performanceObserver.disconnect();
172 });
173 }
174 }
175
176 get connectors(): Connectors {
177 return this._connectors;
178 }
179
180 get statistics(): Statistics {
181 return this._statistics;
182 }
183
184 _logPrefix(): string {
185 return Utils.logPrefix(` ${this._stationInfo.name}:`);
186 }
187
188 _getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
189 return this._stationInfo.Configuration ? this._stationInfo.Configuration : {} as ChargingStationConfiguration;
190 }
191
192 _getAuthorizationFile(): string {
193 return this._stationInfo.authorizationFile && this._stationInfo.authorizationFile;
194 }
195
196 _loadAndGetAuthorizedTags(): string[] {
197 let authorizedTags: string[] = [];
198 const authorizationFile = this._getAuthorizationFile();
199 if (authorizationFile) {
200 try {
201 // Load authorization file
202 const fileDescriptor = fs.openSync(authorizationFile, 'r');
203 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
204 fs.closeSync(fileDescriptor);
205 } catch (error) {
206 logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error);
207 throw error;
208 }
209 } else {
210 logger.info(this._logPrefix() + ' No authorization file given in template file ' + this._stationTemplateFile);
211 }
212 return authorizedTags;
213 }
214
215 getRandomTagId(): string {
216 const index = Math.floor(Math.random() * this._authorizedTags.length);
217 return this._authorizedTags[index];
218 }
219
220 hasAuthorizedTags(): boolean {
221 return !Utils.isEmptyArray(this._authorizedTags);
222 }
223
224 getEnableStatistics(): boolean {
225 return !Utils.isUndefined(this._stationInfo.enableStatistics) ? this._stationInfo.enableStatistics : true;
226 }
227
228 _getNumberOfPhases(): number {
229 switch (this._getPowerOutType()) {
230 case PowerOutType.AC:
231 return !Utils.isUndefined(this._stationInfo.numberOfPhases) ? Utils.convertToInt(this._stationInfo.numberOfPhases) : 3;
232 case PowerOutType.DC:
233 return 0;
234 }
235 }
236
237 _getNumberOfRunningTransactions(): number {
238 let trxCount = 0;
239 for (const connector in this._connectors) {
240 if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
241 trxCount++;
242 }
243 }
244 return trxCount;
245 }
246
247 _getPowerDivider(): number {
248 let powerDivider = this._getNumberOfConnectors();
249 if (this._stationInfo.powerSharedByConnectors) {
250 powerDivider = this._getNumberOfRunningTransactions();
251 }
252 return powerDivider;
253 }
254
255 getConnector(id: number): Connector {
256 return this._connectors[id];
257 }
258
259 _getTemplateMaxNumberOfConnectors(): number {
260 return Object.keys(this._stationInfo.Connectors).length;
261 }
262
263 _getMaxNumberOfConnectors(): number {
264 let maxConnectors = 0;
265 if (!Utils.isEmptyArray(this._stationInfo.numberOfConnectors)) {
266 const numberOfConnectors = this._stationInfo.numberOfConnectors as number[];
267 // Distribute evenly the number of connectors
268 maxConnectors = numberOfConnectors[(this._index - 1) % numberOfConnectors.length];
269 } else if (!Utils.isUndefined(this._stationInfo.numberOfConnectors)) {
270 maxConnectors = this._stationInfo.numberOfConnectors as number;
271 } else {
272 maxConnectors = this._stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors();
273 }
274 return maxConnectors;
275 }
276
277 _getNumberOfConnectors(): number {
278 return this._connectors[0] ? Object.keys(this._connectors).length - 1 : Object.keys(this._connectors).length;
279 }
280
281 _getVoltageOut(): number {
282 const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this._stationTemplateFile}, cannot define default voltage out`;
283 let defaultVoltageOut: number;
284 switch (this._getPowerOutType()) {
285 case PowerOutType.AC:
286 defaultVoltageOut = 230;
287 break;
288 case PowerOutType.DC:
289 defaultVoltageOut = 400;
290 break;
291 default:
292 logger.error(errMsg);
293 throw Error(errMsg);
294 }
295 return !Utils.isUndefined(this._stationInfo.voltageOut) ? Utils.convertToInt(this._stationInfo.voltageOut) : defaultVoltageOut;
296 }
297
298 _getTransactionIdTag(transactionId: number): string {
299 for (const connector in this._connectors) {
300 if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
301 return this.getConnector(Utils.convertToInt(connector)).idTag;
302 }
303 }
304 }
305
306 _getPowerOutType(): PowerOutType {
307 return !Utils.isUndefined(this._stationInfo.powerOutType) ? this._stationInfo.powerOutType : PowerOutType.AC;
308 }
309
310 _getSupervisionURL(): string {
311 const supervisionUrls = Utils.cloneObject(this._stationInfo.supervisionURL ? this._stationInfo.supervisionURL : Configuration.getSupervisionURLs()) as string | string[];
312 let indexUrl = 0;
313 if (!Utils.isEmptyArray(supervisionUrls)) {
314 if (Configuration.getDistributeStationsToTenantsEqually()) {
315 indexUrl = this._index % supervisionUrls.length;
316 } else {
317 // Get a random url
318 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
319 }
320 return supervisionUrls[indexUrl];
321 }
322 return supervisionUrls as string;
323 }
324
325 _getReconnectExponentialDelay(): boolean {
326 return !Utils.isUndefined(this._stationInfo.reconnectExponentialDelay) ? this._stationInfo.reconnectExponentialDelay : false;
327 }
328
329 _getAuthorizeRemoteTxRequests(): boolean {
330 const authorizeRemoteTxRequests = this._getConfigurationKey('AuthorizeRemoteTxRequests');
331 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
332 }
333
334 _getLocalAuthListEnabled(): boolean {
335 const localAuthListEnabled = this._getConfigurationKey('LocalAuthListEnabled');
336 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
337 }
338
339 async _startMessageSequence(): Promise<void> {
340 // Start WebSocket ping
341 this._startWebSocketPing();
342 // Start heartbeat
343 this._startHeartbeat();
344 // Initialize connectors status
345 for (const connector in this._connectors) {
346 if (!this._hasStopped && !this.getConnector(Utils.convertToInt(connector)).status && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
347 // Send status in template at startup
348 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
349 } else if (this._hasStopped && this.getConnector(Utils.convertToInt(connector)).bootStatus) {
350 // Send status in template after reset
351 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
352 } else if (!this._hasStopped && this.getConnector(Utils.convertToInt(connector)).status) {
353 // Send previous status at template reload
354 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status);
355 } else {
356 // Send default status
357 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE);
358 }
359 }
360 // Start the ATG
361 if (this._stationInfo.AutomaticTransactionGenerator.enable) {
362 if (!this._automaticTransactionGeneration) {
363 this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
364 }
365 if (this._automaticTransactionGeneration.timeToStop) {
366 this._automaticTransactionGeneration.start();
367 }
368 }
369 if (this.getEnableStatistics()) {
370 this._statistics.start();
371 }
372 }
373
374 async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
375 // Stop WebSocket ping
376 this._stopWebSocketPing();
377 // Stop heartbeat
378 this._stopHeartbeat();
379 // Stop the ATG
380 if (this._stationInfo.AutomaticTransactionGenerator.enable &&
381 this._automaticTransactionGeneration &&
382 !this._automaticTransactionGeneration.timeToStop) {
383 await this._automaticTransactionGeneration.stop(reason);
384 } else {
385 for (const connector in this._connectors) {
386 if (this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
387 await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason);
388 }
389 }
390 }
391 }
392
393 _startWebSocketPing(): void {
394 const webSocketPingInterval: number = this._getConfigurationKey('WebSocketPingInterval') ? Utils.convertToInt(this._getConfigurationKey('WebSocketPingInterval').value) : 0;
395 if (webSocketPingInterval > 0 && !this._webSocketPingSetInterval) {
396 this._webSocketPingSetInterval = setInterval(() => {
397 if (this._wsConnection?.readyState === WebSocket.OPEN) {
398 this._wsConnection.ping((): void => {});
399 }
400 }, webSocketPingInterval * 1000);
401 logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval));
402 } else if (this._webSocketPingSetInterval) {
403 logger.info(this._logPrefix() + ' WebSocket ping every ' + Utils.secondsToHHMMSS(webSocketPingInterval) + ' already started');
404 } else {
405 logger.error(`${this._logPrefix()} WebSocket ping interval set to ${webSocketPingInterval ? Utils.secondsToHHMMSS(webSocketPingInterval) : webSocketPingInterval}, not starting the WebSocket ping`);
406 }
407 }
408
409 _stopWebSocketPing(): void {
410 if (this._webSocketPingSetInterval) {
411 clearInterval(this._webSocketPingSetInterval);
412 this._webSocketPingSetInterval = null;
413 }
414 }
415
416 _restartWebSocketPing(): void {
417 // Stop WebSocket ping
418 this._stopWebSocketPing();
419 // Start WebSocket ping
420 this._startWebSocketPing();
421 }
422
423 _startHeartbeat(): void {
424 if (this._heartbeatInterval && this._heartbeatInterval > 0 && !this._heartbeatSetInterval) {
425 this._heartbeatSetInterval = setInterval(async () => {
426 await this.sendHeartbeat();
427 }, this._heartbeatInterval);
428 logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval));
429 } else if (this._heartbeatSetInterval) {
430 logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._heartbeatInterval) + ' already started');
431 } else {
432 logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._heartbeatInterval ? Utils.milliSecondsToHHMMSS(this._heartbeatInterval) : this._heartbeatInterval}, not starting the heartbeat`);
433 }
434 }
435
436 _stopHeartbeat(): void {
437 if (this._heartbeatSetInterval) {
438 clearInterval(this._heartbeatSetInterval);
439 this._heartbeatSetInterval = null;
440 }
441 }
442
443 _restartHeartbeat(): void {
444 // Stop heartbeat
445 this._stopHeartbeat();
446 // Start heartbeat
447 this._startHeartbeat();
448 }
449
450 _startAuthorizationFileMonitoring(): void {
451 // eslint-disable-next-line @typescript-eslint/no-unused-vars
452 fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
453 try {
454 logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
455 // Initialize _authorizedTags
456 this._authorizedTags = this._loadAndGetAuthorizedTags();
457 } catch (error) {
458 logger.error(this._logPrefix() + ' Authorization file monitoring error: %j', error);
459 }
460 });
461 }
462
463 _startStationTemplateFileMonitoring(): void {
464 // eslint-disable-next-line @typescript-eslint/no-unused-vars
465 fs.watchFile(this._stationTemplateFile, (current, previous) => {
466 try {
467 logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
468 // Initialize
469 this._initialize();
470 if (!this._stationInfo.AutomaticTransactionGenerator.enable &&
471 this._automaticTransactionGeneration) {
472 this._automaticTransactionGeneration.stop().catch(() => { });
473 }
474 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
475 } catch (error) {
476 logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error);
477 }
478 });
479 }
480
481 _startMeterValues(connectorId: number, interval: number): void {
482 if (!this.getConnector(connectorId).transactionStarted) {
483 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
484 return;
485 } else if (this.getConnector(connectorId).transactionStarted && !this.getConnector(connectorId).transactionId) {
486 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
487 return;
488 }
489 if (interval > 0) {
490 this.getConnector(connectorId).transactionSetInterval = setInterval(async () => {
491 if (this.getEnableStatistics()) {
492 const sendMeterValues = performance.timerify(this.sendMeterValues);
493 this._performanceObserver.observe({
494 entryTypes: ['function'],
495 });
496 await sendMeterValues(connectorId, interval, this);
497 } else {
498 await this.sendMeterValues(connectorId, interval, this);
499 }
500 }, interval);
501 } else {
502 logger.error(`${this._logPrefix()} Charging station MeterValueSampleInterval configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
503 }
504 }
505
506 _openWSConnection(options?: WebSocket.ClientOptions): void {
507 if (Utils.isUndefined(options)) {
508 options = {} as WebSocket.ClientOptions;
509 }
510 if (Utils.isUndefined(options.handshakeTimeout)) {
511 options.handshakeTimeout = this._connectionTimeout;
512 }
513 this._wsConnection = new WebSocket(this._wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
514 logger.info(this._logPrefix() + ' Will communicate through URL ' + this._supervisionUrl);
515 }
516
517 start(): void {
518 this._openWSConnection();
519 // Monitor authorization file
520 this._startAuthorizationFileMonitoring();
521 // Monitor station template file
522 this._startStationTemplateFileMonitoring();
523 // Handle Socket incoming messages
524 this._wsConnection.on('message', this.onMessage.bind(this));
525 // Handle Socket error
526 this._wsConnection.on('error', this.onError.bind(this));
527 // Handle Socket close
528 this._wsConnection.on('close', this.onClose.bind(this));
529 // Handle Socket opening connection
530 this._wsConnection.on('open', this.onOpen.bind(this));
531 // Handle Socket ping
532 this._wsConnection.on('ping', this.onPing.bind(this));
533 // Handle Socket pong
534 this._wsConnection.on('pong', this.onPong.bind(this));
535 }
536
537 async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
538 // Stop message sequence
539 await this._stopMessageSequence(reason);
540 // eslint-disable-next-line guard-for-in
541 for (const connector in this._connectors) {
542 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
543 }
544 if (this._wsConnection?.readyState === WebSocket.OPEN) {
545 this._wsConnection.close();
546 }
547 this._hasStopped = true;
548 }
549
550 async _reconnect(error): Promise<void> {
551 logger.error(this._logPrefix() + ' Socket: abnormally closed: %j', error);
552 // Stop heartbeat
553 this._stopHeartbeat();
554 // Stop the ATG if needed
555 if (this._stationInfo.AutomaticTransactionGenerator.enable &&
556 this._stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
557 this._automaticTransactionGeneration &&
558 !this._automaticTransactionGeneration.timeToStop) {
559 this._automaticTransactionGeneration.stop().catch(() => { });
560 }
561 if (this._autoReconnectRetryCount < this._autoReconnectMaxRetries || this._autoReconnectMaxRetries === -1) {
562 this._autoReconnectRetryCount++;
563 const reconnectDelay = (this._getReconnectExponentialDelay() ? Utils.exponentialDelay(this._autoReconnectRetryCount) : this._connectionTimeout);
564 logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
565 await Utils.sleep(reconnectDelay);
566 logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this._autoReconnectRetryCount.toString());
567 this._openWSConnection({ handshakeTimeout : reconnectDelay - 100 });
568 } else if (this._autoReconnectMaxRetries !== -1) {
569 logger.error(`${this._logPrefix()} Socket: max retries reached (${this._autoReconnectRetryCount}) or retry disabled (${this._autoReconnectMaxRetries})`);
570 }
571 }
572
573 async onOpen(): Promise<void> {
574 logger.info(`${this._logPrefix()} Is connected to server through ${this._wsConnectionUrl}`);
575 if (!this._hasSocketRestarted || this._hasStopped) {
576 // Send BootNotification
577 await this.sendBootNotification();
578 }
579 await this._startMessageSequence();
580 if (this._hasSocketRestarted) {
581 if (!Utils.isEmptyArray(this._messageQueue)) {
582 this._messageQueue.forEach((message, index) => {
583 if (this._wsConnection?.readyState === WebSocket.OPEN) {
584 this._messageQueue.splice(index, 1);
585 this._wsConnection.send(message);
586 }
587 });
588 }
589 }
590 this._autoReconnectRetryCount = 0;
591 this._hasSocketRestarted = false;
592 }
593
594 async onError(errorEvent): Promise<void> {
595 switch (errorEvent.code) {
596 case 'ECONNREFUSED':
597 this._hasSocketRestarted = true;
598 await this._reconnect(errorEvent);
599 break;
600 default:
601 logger.error(this._logPrefix() + ' Socket error: %j', errorEvent);
602 break;
603 }
604 }
605
606 async onClose(closeEvent): Promise<void> {
607 switch (closeEvent) {
608 case 1000: // Normal close
609 case 1005:
610 logger.info(this._logPrefix() + ' Socket normally closed: %j', closeEvent);
611 this._autoReconnectRetryCount = 0;
612 break;
613 default: // Abnormal close
614 this._hasSocketRestarted = true;
615 await this._reconnect(closeEvent);
616 break;
617 }
618 }
619
620 onPing(): void {
621 logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
622 }
623
624 onPong(): void {
625 logger.debug(this._logPrefix() + ' Has received a WS pong (rfc6455) from the server');
626 }
627
628 async onMessage(messageEvent: MessageEvent): Promise<void> {
629 let [messageType, messageId, commandName, commandPayload, errorDetails] = [0, '', Constants.ENTITY_CHARGING_STATION, '', ''];
630 try {
631 // Parse the message
632 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString());
633
634 // Check the Type of message
635 switch (messageType) {
636 // Incoming Message
637 case Constants.OCPP_JSON_CALL_MESSAGE:
638 if (this.getEnableStatistics()) {
639 this._statistics.addMessage(commandName, messageType);
640 }
641 // Process the call
642 await this.handleRequest(messageId, commandName, commandPayload);
643 break;
644 // Outcome Message
645 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
646 // Respond
647 // eslint-disable-next-line no-case-declarations
648 let responseCallback; let requestPayload;
649 if (Utils.isIterable(this._requests[messageId])) {
650 [responseCallback, , requestPayload] = this._requests[messageId];
651 } else {
652 throw new Error(`Response request for message id ${messageId} is not iterable`);
653 }
654 if (!responseCallback) {
655 // Error
656 throw new Error(`Response request for unknown message id ${messageId}`);
657 }
658 delete this._requests[messageId];
659 responseCallback(commandName, requestPayload);
660 break;
661 // Error Message
662 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
663 if (!this._requests[messageId]) {
664 // Error
665 throw new Error(`Error request for unknown message id ${messageId}`);
666 }
667 // eslint-disable-next-line no-case-declarations
668 let rejectCallback;
669 if (Utils.isIterable(this._requests[messageId])) {
670 [, rejectCallback] = this._requests[messageId];
671 } else {
672 throw new Error(`Error request for message id ${messageId} is not iterable`);
673 }
674 delete this._requests[messageId];
675 rejectCallback(new OCPPError(commandName, commandPayload, errorDetails));
676 break;
677 // Error
678 default:
679 // eslint-disable-next-line no-case-declarations
680 const errMsg = `${this._logPrefix()} Wrong message type ${messageType}`;
681 logger.error(errMsg);
682 throw new Error(errMsg);
683 }
684 } catch (error) {
685 // Log
686 logger.error('%s Incoming message %j processing error %s on request content type %s', this._logPrefix(), messageEvent, error, this._requests[messageId]);
687 // Send error
688 messageType !== Constants.OCPP_JSON_CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName);
689 }
690 }
691
692 async sendHeartbeat(): Promise<void> {
693 try {
694 const payload = {
695 currentTime: new Date().toISOString(),
696 };
697 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'Heartbeat');
698 } catch (error) {
699 logger.error(this._logPrefix() + ' Send Heartbeat error: %j', error);
700 throw error;
701 }
702 }
703
704 async sendBootNotification(): Promise<void> {
705 try {
706 await this.sendMessage(Utils.generateUUID(), this._bootNotificationMessage, Constants.OCPP_JSON_CALL_MESSAGE, 'BootNotification');
707 } catch (error) {
708 logger.error(this._logPrefix() + ' Send BootNotification error: %j', error);
709 throw error;
710 }
711 }
712
713 async sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode: ChargePointErrorCode = ChargePointErrorCode.NO_ERROR): Promise<void> {
714 this.getConnector(connectorId).status = status;
715 try {
716 const payload = {
717 connectorId,
718 errorCode,
719 status,
720 };
721 await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StatusNotification');
722 } catch (error) {
723 logger.error(this._logPrefix() + ' Send StatusNotification error: %j', error);
724 throw error;
725 }
726 }
727
728 async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
729 try {
730 const payload = {
731 connectorId,
732 ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_IDTAG },
733 meterStart: 0,
734 timestamp: new Date().toISOString(),
735 };
736 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StartTransaction') as StartTransactionResponse;
737 } catch (error) {
738 logger.error(this._logPrefix() + ' Send StartTransaction error: %j', error);
739 throw error;
740 }
741 }
742
743 async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise<StopTransactionResponse> {
744 const idTag = this._getTransactionIdTag(transactionId);
745 try {
746 const payload = {
747 transactionId,
748 ...!Utils.isUndefined(idTag) && { idTag: idTag },
749 meterStop: 0,
750 timestamp: new Date().toISOString(),
751 ...reason && { reason },
752 };
753 return await this.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'StopTransaction') as StartTransactionResponse;
754 } catch (error) {
755 logger.error(this._logPrefix() + ' Send StopTransaction error: %j', error);
756 throw error;
757 }
758 }
759
760 // eslint-disable-next-line consistent-this
761 async sendMeterValues(connectorId: number, interval: number, self: ChargingStation, debug = false): Promise<void> {
762 try {
763 const sampledValues: {
764 timestamp: string;
765 sampledValue: MeterValue[];
766 } = {
767 timestamp: new Date().toISOString(),
768 sampledValue: [],
769 };
770 const meterValuesTemplate = self.getConnector(connectorId).MeterValues;
771 for (let index = 0; index < meterValuesTemplate.length; index++) {
772 const connector = self.getConnector(connectorId);
773 // SoC measurand
774 if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.STATE_OF_CHARGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.STATE_OF_CHARGE)) {
775 sampledValues.sampledValue.push({
776 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.PERCENT },
777 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
778 measurand: meterValuesTemplate[index].measurand,
779 ...!Utils.isUndefined(meterValuesTemplate[index].location) ? { location: meterValuesTemplate[index].location } : { location: MeterValueLocation.EV },
780 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: Utils.getRandomInt(100).toString() },
781 });
782 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
783 if (Utils.convertToInt(sampledValues.sampledValue[sampledValuesIndex].value) > 100 || debug) {
784 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/100`);
785 }
786 // Voltage measurand
787 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.VOLTAGE && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.VOLTAGE)) {
788 const voltageMeasurandValue = Utils.getRandomFloatRounded(self._getVoltageOut() + self._getVoltageOut() * 0.1, self._getVoltageOut() - self._getVoltageOut() * 0.1);
789 sampledValues.sampledValue.push({
790 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
791 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
792 measurand: meterValuesTemplate[index].measurand,
793 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
794 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
795 });
796 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
797 const voltageValue = Utils.convertToFloat(sampledValues.sampledValue[sampledValues.sampledValue.length - 1].value);
798 let phaseValue: string;
799 if (voltageValue >= 0 && voltageValue <= 250) {
800 phaseValue = `L${phase}-N`;
801 } else if (voltageValue > 250) {
802 phaseValue = `L${phase}-L${(phase + 1) % self._getNumberOfPhases() !== 0 ? (phase + 1) % self._getNumberOfPhases() : self._getNumberOfPhases()}`;
803 }
804 sampledValues.sampledValue.push({
805 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.VOLT },
806 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
807 measurand: meterValuesTemplate[index].measurand,
808 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
809 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: voltageMeasurandValue.toString() },
810 phase: phaseValue as MeterValuePhase,
811 });
812 }
813 // Power.Active.Import measurand
814 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.POWER_ACTIVE_IMPORT)) {
815 // FIXME: factor out powerDivider checks
816 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
817 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
818 logger.error(errMsg);
819 throw Error(errMsg);
820 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
821 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
822 logger.error(errMsg);
823 throw Error(errMsg);
824 }
825 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
826 const powerMeasurandValues = {} as MeasurandValues;
827 const maxPower = Math.round(self._stationInfo.maxPower / self._stationInfo.powerDivider);
828 const maxPowerPerPhase = Math.round((self._stationInfo.maxPower / self._stationInfo.powerDivider) / self._getNumberOfPhases());
829 switch (self._getPowerOutType()) {
830 case PowerOutType.AC:
831 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
832 powerMeasurandValues.L1 = Utils.getRandomFloatRounded(maxPowerPerPhase);
833 powerMeasurandValues.L2 = 0;
834 powerMeasurandValues.L3 = 0;
835 if (self._getNumberOfPhases() === 3) {
836 powerMeasurandValues.L2 = Utils.getRandomFloatRounded(maxPowerPerPhase);
837 powerMeasurandValues.L3 = Utils.getRandomFloatRounded(maxPowerPerPhase);
838 }
839 powerMeasurandValues.allPhases = Utils.roundTo(powerMeasurandValues.L1 + powerMeasurandValues.L2 + powerMeasurandValues.L3, 2);
840 }
841 break;
842 case PowerOutType.DC:
843 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
844 powerMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxPower);
845 }
846 break;
847 default:
848 logger.error(errMsg);
849 throw Error(errMsg);
850 }
851 sampledValues.sampledValue.push({
852 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
853 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
854 measurand: meterValuesTemplate[index].measurand,
855 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
856 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues.allPhases.toString() },
857 });
858 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
859 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxPower || debug) {
860 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxPower}`);
861 }
862 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
863 const phaseValue = `L${phase}-N`;
864 sampledValues.sampledValue.push({
865 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT },
866 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
867 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
868 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
869 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: powerMeasurandValues[`L${phase}`] as string },
870 phase: phaseValue as MeterValuePhase,
871 });
872 }
873 // Current.Import measurand
874 } else if (meterValuesTemplate[index].measurand && meterValuesTemplate[index].measurand === MeterValueMeasurand.CURRENT_IMPORT && self._getConfigurationKey('MeterValuesSampledData').value.includes(MeterValueMeasurand.CURRENT_IMPORT)) {
875 // FIXME: factor out powerDivider checks
876 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
877 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
878 logger.error(errMsg);
879 throw Error(errMsg);
880 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
881 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
882 logger.error(errMsg);
883 throw Error(errMsg);
884 }
885 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: Unknown ${self._getPowerOutType()} powerOutType in template file ${self._stationTemplateFile}, cannot calculate ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} measurand value`;
886 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
887 let maxAmperage: number;
888 switch (self._getPowerOutType()) {
889 case PowerOutType.AC:
890 maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
891 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
892 currentMeasurandValues.L1 = Utils.getRandomFloatRounded(maxAmperage);
893 currentMeasurandValues.L2 = 0;
894 currentMeasurandValues.L3 = 0;
895 if (self._getNumberOfPhases() === 3) {
896 currentMeasurandValues.L2 = Utils.getRandomFloatRounded(maxAmperage);
897 currentMeasurandValues.L3 = Utils.getRandomFloatRounded(maxAmperage);
898 }
899 currentMeasurandValues.allPhases = Utils.roundTo((currentMeasurandValues.L1 + currentMeasurandValues.L2 + currentMeasurandValues.L3) / self._getNumberOfPhases(), 2);
900 }
901 break;
902 case PowerOutType.DC:
903 maxAmperage = ElectricUtils.ampTotalFromPower(self._stationInfo.maxPower / self._stationInfo.powerDivider, self._getVoltageOut());
904 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
905 currentMeasurandValues.allPhases = Utils.getRandomFloatRounded(maxAmperage);
906 }
907 break;
908 default:
909 logger.error(errMsg);
910 throw Error(errMsg);
911 }
912 sampledValues.sampledValue.push({
913 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
914 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
915 measurand: meterValuesTemplate[index].measurand,
916 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
917 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues.allPhases.toString() },
918 });
919 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
920 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxAmperage || debug) {
921 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxAmperage}`);
922 }
923 for (let phase = 1; self._getNumberOfPhases() === 3 && phase <= self._getNumberOfPhases(); phase++) {
924 const phaseValue = `L${phase}`;
925 sampledValues.sampledValue.push({
926 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.AMP },
927 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
928 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
929 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
930 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } : { value: currentMeasurandValues[phaseValue] as string },
931 phase: phaseValue as MeterValuePhase,
932 });
933 }
934 // Energy.Active.Import.Register measurand (default)
935 } else if (!meterValuesTemplate[index].measurand || meterValuesTemplate[index].measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
936 // FIXME: factor out powerDivider checks
937 if (Utils.isUndefined(self._stationInfo.powerDivider)) {
938 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider is undefined`;
939 logger.error(errMsg);
940 throw Error(errMsg);
941 } else if (self._stationInfo.powerDivider && self._stationInfo.powerDivider <= 0) {
942 const errMsg = `${self._logPrefix()} MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: powerDivider have zero or below value ${self._stationInfo.powerDivider}`;
943 logger.error(errMsg);
944 throw Error(errMsg);
945 }
946 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
947 const measurandValue = Utils.getRandomInt(self._stationInfo.maxPower / (self._stationInfo.powerDivider * 3600000) * interval);
948 // Persist previous value in connector
949 if (connector && !Utils.isNullOrUndefined(connector.lastEnergyActiveImportRegisterValue) && connector.lastEnergyActiveImportRegisterValue >= 0) {
950 connector.lastEnergyActiveImportRegisterValue += measurandValue;
951 } else {
952 connector.lastEnergyActiveImportRegisterValue = 0;
953 }
954 }
955 sampledValues.sampledValue.push({
956 ...!Utils.isUndefined(meterValuesTemplate[index].unit) ? { unit: meterValuesTemplate[index].unit } : { unit: MeterValueUnit.WATT_HOUR },
957 ...!Utils.isUndefined(meterValuesTemplate[index].context) && { context: meterValuesTemplate[index].context },
958 ...!Utils.isUndefined(meterValuesTemplate[index].measurand) && { measurand: meterValuesTemplate[index].measurand },
959 ...!Utils.isUndefined(meterValuesTemplate[index].location) && { location: meterValuesTemplate[index].location },
960 ...!Utils.isUndefined(meterValuesTemplate[index].value) ? { value: meterValuesTemplate[index].value } :
961 { value: connector.lastEnergyActiveImportRegisterValue.toString() },
962 });
963 const sampledValuesIndex = sampledValues.sampledValue.length - 1;
964 const maxConsumption = Math.round(self._stationInfo.maxPower * 3600 / (self._stationInfo.powerDivider * interval));
965 if (Utils.convertToFloat(sampledValues.sampledValue[sampledValuesIndex].value) > maxConsumption || debug) {
966 logger.error(`${self._logPrefix()} MeterValues measurand ${sampledValues.sampledValue[sampledValuesIndex].measurand ? sampledValues.sampledValue[sampledValuesIndex].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER}: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${sampledValues.sampledValue[sampledValuesIndex].value}/${maxConsumption}`);
967 }
968 // Unsupported measurand
969 } else {
970 logger.info(`${self._logPrefix()} Unsupported MeterValues measurand ${meterValuesTemplate[index].measurand ? meterValuesTemplate[index].measurand : MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER} on connectorId ${connectorId}`);
971 }
972 }
973
974 const payload = {
975 connectorId,
976 transactionId: self.getConnector(connectorId).transactionId,
977 meterValue: sampledValues,
978 };
979 await self.sendMessage(Utils.generateUUID(), payload, Constants.OCPP_JSON_CALL_MESSAGE, 'MeterValues');
980 } catch (error) {
981 logger.error(self._logPrefix() + ' Send MeterValues error: %j', error);
982 throw error;
983 }
984 }
985
986 async sendError(messageId: string, err: Error | OCPPError, commandName: string): Promise<unknown> {
987 // Check exception type: only OCPP error are accepted
988 const error = err instanceof OCPPError ? err : new OCPPError(Constants.OCPP_ERROR_INTERNAL_ERROR, err.message, err.stack && err.stack);
989 // Send error
990 return this.sendMessage(messageId, error, Constants.OCPP_JSON_CALL_ERROR_MESSAGE, commandName);
991 }
992
993 async sendMessage(messageId: string, commandParams, messageType = Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName: string): Promise<any> {
994 // eslint-disable-next-line @typescript-eslint/no-this-alias
995 const self = this;
996 // Send a message through wsConnection
997 return new Promise((resolve, reject) => {
998 let messageToSend;
999 // Type of message
1000 switch (messageType) {
1001 // Request
1002 case Constants.OCPP_JSON_CALL_MESSAGE:
1003 // Build request
1004 this._requests[messageId] = [responseCallback, rejectCallback, commandParams];
1005 messageToSend = JSON.stringify([messageType, messageId, commandName, commandParams]);
1006 break;
1007 // Response
1008 case Constants.OCPP_JSON_CALL_RESULT_MESSAGE:
1009 // Build response
1010 messageToSend = JSON.stringify([messageType, messageId, commandParams]);
1011 break;
1012 // Error Message
1013 case Constants.OCPP_JSON_CALL_ERROR_MESSAGE:
1014 // Build Error Message
1015 messageToSend = JSON.stringify([messageType, messageId, commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : '', commandParams.details ? commandParams.details : {}]);
1016 break;
1017 }
1018 // Check if wsConnection is ready
1019 if (this._wsConnection?.readyState === WebSocket.OPEN) {
1020 if (this.getEnableStatistics()) {
1021 this._statistics.addMessage(commandName, messageType);
1022 }
1023 // Yes: Send Message
1024 this._wsConnection.send(messageToSend);
1025 } else {
1026 let dups = false;
1027 // Handle dups in buffer
1028 for (const message of this._messageQueue) {
1029 // Same message
1030 if (JSON.stringify(messageToSend) === JSON.stringify(message)) {
1031 dups = true;
1032 break;
1033 }
1034 }
1035 if (!dups) {
1036 // Buffer message
1037 this._messageQueue.push(messageToSend);
1038 }
1039 // Reject it
1040 return rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `WebSocket closed for message id '${messageId}' with content '${messageToSend}', message buffered`, commandParams.details ? commandParams.details : {}));
1041 }
1042 // Response?
1043 if (messageType === Constants.OCPP_JSON_CALL_RESULT_MESSAGE) {
1044 // Yes: send Ok
1045 resolve();
1046 } else if (messageType === Constants.OCPP_JSON_CALL_ERROR_MESSAGE) {
1047 // Send timeout
1048 setTimeout(() => rejectCallback(new OCPPError(commandParams.code ? commandParams.code : Constants.OCPP_ERROR_GENERIC_ERROR, commandParams.message ? commandParams.message : `Timeout for message id '${messageId}' with content '${messageToSend}'`, commandParams.details ? commandParams.details : {})), Constants.OCPP_SOCKET_TIMEOUT);
1049 }
1050
1051 // Function that will receive the request's response
1052 function responseCallback(payload, requestPayload): void {
1053 if (self.getEnableStatistics()) {
1054 self._statistics.addMessage(commandName, messageType);
1055 }
1056 // Send the response
1057 self.handleResponse(commandName, payload, requestPayload);
1058 resolve(payload);
1059 }
1060
1061 // Function that will receive the request's rejection
1062 function rejectCallback(error: OCPPError): void {
1063 if (self.getEnableStatistics()) {
1064 self._statistics.addMessage(commandName, messageType);
1065 }
1066 logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams);
1067 // Build Exception
1068 // eslint-disable-next-line no-empty-function
1069 self._requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request
1070 // Send error
1071 reject(error);
1072 }
1073 });
1074 }
1075
1076 handleResponse(commandName: string, payload, requestPayload): void {
1077 const responseCallbackFn = 'handleResponse' + commandName;
1078 if (typeof this[responseCallbackFn] === 'function') {
1079 this[responseCallbackFn](payload, requestPayload);
1080 } else {
1081 logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
1082 }
1083 }
1084
1085 handleResponseBootNotification(payload, requestPayload): void {
1086 if (payload.status === 'Accepted') {
1087 this._heartbeatInterval = Utils.convertToInt(payload.interval) * 1000;
1088 this._heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat();
1089 this._addConfigurationKey('HeartBeatInterval', payload.interval);
1090 this._addConfigurationKey('HeartbeatInterval', payload.interval, false, false);
1091 this._hasStopped && (this._hasStopped = false);
1092 } else if (payload.status === 'Pending') {
1093 logger.info(this._logPrefix() + ' Charging station in pending state on the central server');
1094 } else {
1095 logger.info(this._logPrefix() + ' Charging station rejected by the central server');
1096 }
1097 }
1098
1099 _initTransactionOnConnector(connectorId: number): void {
1100 this.getConnector(connectorId).transactionStarted = false;
1101 this.getConnector(connectorId).transactionId = null;
1102 this.getConnector(connectorId).idTag = null;
1103 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1;
1104 }
1105
1106 _resetTransactionOnConnector(connectorId: number): void {
1107 this._initTransactionOnConnector(connectorId);
1108 if (this.getConnector(connectorId).transactionSetInterval) {
1109 clearInterval(this.getConnector(connectorId).transactionSetInterval);
1110 }
1111 }
1112
1113 handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload): void {
1114 const connectorId = Utils.convertToInt(requestPayload.connectorId);
1115 if (this.getConnector(connectorId).transactionStarted) {
1116 logger.debug(this._logPrefix() + ' Trying to start a transaction on an already used connector ' + connectorId.toString() + ': %j', this.getConnector(connectorId));
1117 return;
1118 }
1119
1120 let transactionConnectorId: number;
1121 for (const connector in this._connectors) {
1122 if (Utils.convertToInt(connector) === connectorId) {
1123 transactionConnectorId = Utils.convertToInt(connector);
1124 break;
1125 }
1126 }
1127 if (!transactionConnectorId) {
1128 logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString());
1129 return;
1130 }
1131 if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1132 this.getConnector(connectorId).transactionStarted = true;
1133 this.getConnector(connectorId).transactionId = payload.transactionId;
1134 this.getConnector(connectorId).idTag = requestPayload.idTag;
1135 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = 0;
1136 this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING).catch(() => { });
1137 logger.info(this._logPrefix() + ' Transaction ' + payload.transactionId.toString() + ' STARTED on ' + this._stationInfo.name + '#' + connectorId.toString() + ' for idTag ' + requestPayload.idTag);
1138 if (this._stationInfo.powerSharedByConnectors) {
1139 this._stationInfo.powerDivider++;
1140 }
1141 const configuredMeterValueSampleInterval = this._getConfigurationKey('MeterValueSampleInterval');
1142 this._startMeterValues(connectorId,
1143 configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
1144 } else {
1145 logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
1146 this._resetTransactionOnConnector(connectorId);
1147 this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE).catch(() => { });
1148 }
1149 }
1150
1151 handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload): void {
1152 let transactionConnectorId: number;
1153 for (const connector in this._connectors) {
1154 if (this.getConnector(Utils.convertToInt(connector)).transactionId === Utils.convertToInt(requestPayload.transactionId)) {
1155 transactionConnectorId = Utils.convertToInt(connector);
1156 break;
1157 }
1158 }
1159 if (!transactionConnectorId) {
1160 logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId);
1161 return;
1162 }
1163 if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1164 this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE).catch(() => { });
1165 if (this._stationInfo.powerSharedByConnectors) {
1166 this._stationInfo.powerDivider--;
1167 }
1168 logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId + ' STOPPED on ' + this._stationInfo.name + '#' + transactionConnectorId.toString());
1169 this._resetTransactionOnConnector(transactionConnectorId);
1170 } else {
1171 logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId + ' REJECTED with status ' + payload.idTagInfo?.status);
1172 }
1173 }
1174
1175 handleResponseStatusNotification(payload, requestPayload): void {
1176 logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
1177 }
1178
1179 handleResponseMeterValues(payload, requestPayload): void {
1180 logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
1181 }
1182
1183 handleResponseHeartbeat(payload, requestPayload): void {
1184 logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
1185 }
1186
1187 async handleRequest(messageId: string, commandName: string, commandPayload): Promise<void> {
1188 let response;
1189 // Call
1190 if (typeof this['handleRequest' + commandName] === 'function') {
1191 try {
1192 // Call the method to build the response
1193 response = await this['handleRequest' + commandName](commandPayload);
1194 } catch (error) {
1195 // Log
1196 logger.error(this._logPrefix() + ' Handle request error: %j', error);
1197 // Send back response to inform backend
1198 await this.sendError(messageId, error, commandName);
1199 throw error;
1200 }
1201 } else {
1202 // Throw exception
1203 await this.sendError(messageId, new OCPPError(Constants.OCPP_ERROR_NOT_IMPLEMENTED, `${commandName} is not implemented`, {}), commandName);
1204 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
1205 }
1206 // Send response
1207 await this.sendMessage(messageId, response, Constants.OCPP_JSON_CALL_RESULT_MESSAGE, commandName);
1208 }
1209
1210 // Simulate charging station restart
1211 handleRequestReset(commandPayload): DefaultRequestResponse {
1212 setImmediate(async () => {
1213 await this.stop(commandPayload.type + 'Reset' as StopTransactionReason);
1214 await Utils.sleep(this._stationInfo.resetTime);
1215 await this.start();
1216 });
1217 logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${Utils.milliSecondsToHHMMSS(this._stationInfo.resetTime)}`);
1218 return Constants.OCPP_RESPONSE_ACCEPTED;
1219 }
1220
1221 async handleRequestUnlockConnector(commandPayload): Promise<UnlockResponse> {
1222 const connectorId = Utils.convertToInt(commandPayload.connectorId);
1223 if (connectorId === 0) {
1224 logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
1225 return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
1226 }
1227 if (this.getConnector(connectorId).transactionStarted) {
1228 const stopResponse = await this.sendStopTransaction(this.getConnector(connectorId).transactionId, StopTransactionReason.UNLOCK_COMMAND);
1229 if (stopResponse.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1230 return Constants.OCPP_RESPONSE_UNLOCKED;
1231 }
1232 return Constants.OCPP_RESPONSE_UNLOCK_FAILED;
1233 }
1234 await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
1235 return Constants.OCPP_RESPONSE_UNLOCKED;
1236 }
1237
1238 _getConfigurationKey(key: string): ConfigurationKey {
1239 return this._configuration.configurationKey.find((configElement) => configElement.key === key);
1240 }
1241
1242 _addConfigurationKey(key: string, value: string, readonly = false, visible = true, reboot = false): void {
1243 const keyFound = this._getConfigurationKey(key);
1244 if (!keyFound) {
1245 this._configuration.configurationKey.push({
1246 key,
1247 readonly,
1248 value,
1249 visible,
1250 reboot,
1251 });
1252 }
1253 }
1254
1255 _setConfigurationKeyValue(key: string, value: string): void {
1256 const keyFound = this._getConfigurationKey(key);
1257 if (keyFound) {
1258 const keyIndex = this._configuration.configurationKey.indexOf(keyFound);
1259 this._configuration.configurationKey[keyIndex].value = value;
1260 }
1261 }
1262
1263 handleRequestGetConfiguration(commandPayload): { configurationKey: ConfigurationKey[]; unknownKey: string[] } {
1264 const configurationKey: ConfigurationKey[] = [];
1265 const unknownKey: string[] = [];
1266 if (Utils.isEmptyArray(commandPayload.key)) {
1267 for (const configuration of this._configuration.configurationKey) {
1268 if (Utils.isUndefined(configuration.visible)) {
1269 configuration.visible = true;
1270 }
1271 if (!configuration.visible) {
1272 continue;
1273 }
1274 configurationKey.push({
1275 key: configuration.key,
1276 readonly: configuration.readonly,
1277 value: configuration.value,
1278 });
1279 }
1280 } else {
1281 for (const key of commandPayload.key as string[]) {
1282 const keyFound = this._getConfigurationKey(key);
1283 if (keyFound) {
1284 if (Utils.isUndefined(keyFound.visible)) {
1285 keyFound.visible = true;
1286 }
1287 if (!keyFound.visible) {
1288 continue;
1289 }
1290 configurationKey.push({
1291 key: keyFound.key,
1292 readonly: keyFound.readonly,
1293 value: keyFound.value,
1294 });
1295 } else {
1296 unknownKey.push(key);
1297 }
1298 }
1299 }
1300 return {
1301 configurationKey,
1302 unknownKey,
1303 };
1304 }
1305
1306 handleRequestChangeConfiguration(commandPayload): ConfigurationResponse {
1307 const keyToChange = this._getConfigurationKey(commandPayload.key);
1308 if (!keyToChange) {
1309 return Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED;
1310 } else if (keyToChange && keyToChange.readonly) {
1311 return Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED;
1312 } else if (keyToChange && !keyToChange.readonly) {
1313 const keyIndex = this._configuration.configurationKey.indexOf(keyToChange);
1314 let valueChanged = false;
1315 if (this._configuration.configurationKey[keyIndex].value !== commandPayload.value) {
1316 this._configuration.configurationKey[keyIndex].value = commandPayload.value as string;
1317 valueChanged = true;
1318 }
1319 let triggerHeartbeatRestart = false;
1320 if (keyToChange.key === 'HeartBeatInterval' && valueChanged) {
1321 this._setConfigurationKeyValue('HeartbeatInterval', commandPayload.value);
1322 triggerHeartbeatRestart = true;
1323 }
1324 if (keyToChange.key === 'HeartbeatInterval' && valueChanged) {
1325 this._setConfigurationKeyValue('HeartBeatInterval', commandPayload.value);
1326 triggerHeartbeatRestart = true;
1327 }
1328 if (triggerHeartbeatRestart) {
1329 this._heartbeatInterval = Utils.convertToInt(commandPayload.value) * 1000;
1330 this._restartHeartbeat();
1331 }
1332 if (keyToChange.key === 'WebSocketPingInterval' && valueChanged) {
1333 this._restartWebSocketPing();
1334 }
1335 if (keyToChange.reboot) {
1336 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
1337 }
1338 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
1339 }
1340 }
1341
1342 async handleRequestRemoteStartTransaction(commandPayload): Promise<DefaultRequestResponse> {
1343 const transactionConnectorID: number = commandPayload.connectorId ? Utils.convertToInt(commandPayload.connectorId) : 1;
1344 if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
1345 // Check if authorized
1346 if (this._authorizedTags.find((value) => value === commandPayload.idTag)) {
1347 // Authorization successful start transaction
1348 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
1349 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
1350 return Constants.OCPP_RESPONSE_ACCEPTED;
1351 }
1352 logger.error(this._logPrefix() + ' Remote starting transaction REJECTED with status ' + commandPayload.idTagInfo?.status + ', idTag ' + commandPayload.idTag);
1353 return Constants.OCPP_RESPONSE_REJECTED;
1354 }
1355 // No local authorization check required => start transaction
1356 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
1357 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this._stationInfo.name + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
1358 return Constants.OCPP_RESPONSE_ACCEPTED;
1359 }
1360
1361 async handleRequestRemoteStopTransaction(commandPayload): Promise<DefaultRequestResponse> {
1362 const transactionId = Utils.convertToInt(commandPayload.transactionId);
1363 for (const connector in this._connectors) {
1364 if (this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
1365 await this.sendStopTransaction(transactionId);
1366 return Constants.OCPP_RESPONSE_ACCEPTED;
1367 }
1368 }
1369 logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
1370 return Constants.OCPP_RESPONSE_REJECTED;
1371 }
1372 }
1373