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