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