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