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