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