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