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