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