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