Switch to poolifier worker threads pool implementation.
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
1 import { AuthorizationStatus, AuthorizeRequest, AuthorizeResponse, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
2 import { AvailabilityType, BootNotificationRequest, ChangeAvailabilityRequest, ChangeConfigurationRequest, ClearChargingProfileRequest, GetConfigurationRequest, HeartbeatRequest, IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, RequestCommand, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
3 import { BootNotificationResponse, ChangeAvailabilityResponse, ChangeConfigurationResponse, ClearChargingProfileResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, SetChargingProfileResponse, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses';
4 import { ChargingProfile, ChargingProfilePurposeType } from '../types/ocpp/1.6/ChargingProfile';
5 import ChargingStationConfiguration, { ConfigurationKey } from '../types/ChargingStationConfiguration';
6 import ChargingStationTemplate, { PowerOutType, VoltageOut } from '../types/ChargingStationTemplate';
7 import Connectors, { Connector } from '../types/Connectors';
8 import { MeterValue, MeterValueLocation, MeterValueMeasurand, MeterValuePhase, MeterValueUnit, MeterValuesRequest, MeterValuesResponse, SampledValue } from '../types/ocpp/1.6/MeterValues';
9 import { PerformanceObserver, performance } from 'perf_hooks';
10 import Requests, { IncomingRequest, Request } from '../types/ocpp/Requests';
11 import WebSocket, { MessageEvent } from 'ws';
12
13 import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
14 import { ChargePointErrorCode } from '../types/ocpp/1.6/ChargePointErrorCode';
15 import { ChargePointStatus } from '../types/ocpp/1.6/ChargePointStatus';
16 import ChargingStationInfo from '../types/ChargingStationInfo';
17 import Configuration from '../utils/Configuration';
18 import Constants from '../utils/Constants';
19 import ElectricUtils from '../utils/ElectricUtils';
20 import { ErrorType } from '../types/ocpp/ErrorType';
21 import MeasurandValues from '../types/MeasurandValues';
22 import { MessageType } from '../types/ocpp/MessageType';
23 import { OCPPConfigurationKey } from '../types/ocpp/Configuration';
24 import OCPPError from './OcppError';
25 import { StandardParametersKey } from '../types/ocpp/1.6/Configuration';
26 import Statistics from '../utils/Statistics';
27 import Utils from '../utils/Utils';
28 import { WebSocketCloseEventStatusCode } from '../types/WebSocket';
29 import crypto from 'crypto';
30 import fs from 'fs';
31 import logger from '../utils/Logger';
32
33 export default class ChargingStation {
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;
56
57 constructor(index: number, stationTemplateFile: string) {
58 this.index = index;
59 this.stationTemplateFile = stationTemplateFile;
60 this.connectors = {} as Connectors;
61 this._initialize();
62
63 this.hasStopped = false;
64 this.hasSocketRestarted = false;
65 this.autoReconnectRetryCount = 0;
66
67 this.requests = {} as Requests;
68 this.messageQueue = [] as string[];
69
70 this.authorizedTags = this._loadAndGetAuthorizedTags();
71 }
72
73 _getChargingStationId(stationTemplate: ChargingStationTemplate): string {
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 const idSuffix = stationTemplate.nameSuffix ? stationTemplate.nameSuffix : '';
78 return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + instanceIndex.toString() + ('000000000' + this.index.toString()).substr(('000000000' + this.index.toString()).length - 4) + idSuffix;
79 }
80
81 _buildStationInfo(): ChargingStationInfo {
82 let stationTemplateFromFile: ChargingStationTemplate;
83 try {
84 // Load template file
85 const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r');
86 stationTemplateFromFile = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as ChargingStationTemplate;
87 fs.closeSync(fileDescriptor);
88 } catch (error) {
89 logger.error('Template file ' + this.stationTemplateFile + ' loading error: %j', error);
90 throw error;
91 }
92 const stationInfo: ChargingStationInfo = stationTemplateFromFile || {} as ChargingStationInfo;
93 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
94 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
95 stationInfo.maxPower = stationTemplateFromFile.power[Math.floor(Math.random() * stationTemplateFromFile.power.length)];
96 } else {
97 stationInfo.maxPower = stationTemplateFromFile.power as number;
98 }
99 stationInfo.chargingStationId = this._getChargingStationId(stationTemplateFromFile);
100 stationInfo.resetTime = stationTemplateFromFile.resetTime ? stationTemplateFromFile.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
101 return stationInfo;
102 }
103
104 _initialize(): void {
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 },
111 };
112 this.configuration = this._getTemplateChargingStationConfiguration();
113 this.supervisionUrl = this._getSupervisionURL();
114 this.wsConnectionUrl = this.supervisionUrl + '/' + this.stationInfo.chargingStationId;
115 // Build connectors if needed
116 const maxConnectors = this._getMaxNumberOfConnectors();
117 if (maxConnectors <= 0) {
118 logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with ${maxConnectors} connectors`);
119 }
120 const templateMaxConnectors = this._getTemplateMaxNumberOfConnectors();
121 if (templateMaxConnectors <= 0) {
122 logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with no connector configuration`);
123 }
124 if (!this.stationInfo.Connectors[0]) {
125 logger.warn(`${this._logPrefix()} Charging station template ${this.stationTemplateFile} with no connector Id 0 configuration`);
126 }
127 // Sanity check
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;
131 }
132 const connectorsConfigHash = crypto.createHash('sha256').update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString()).digest('hex');
133 // FIXME: Handle shrinking the number of connectors
134 if (!this.connectors || (this.connectors && this.connectorsConfigurationHash !== connectorsConfigHash)) {
135 this.connectorsConfigurationHash = connectorsConfigHash;
136 // Add connector Id 0
137 let lastConnector = '0';
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;
142 if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
143 this.connectors[lastConnector].chargingProfiles = [];
144 }
145 }
146 }
147 // Generate all connectors
148 if ((this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
149 for (let index = 1; index <= maxConnectors; index++) {
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;
153 if (Utils.isUndefined(this.connectors[lastConnector]?.chargingProfiles)) {
154 this.connectors[index].chargingProfiles = [];
155 }
156 }
157 }
158 }
159 // Avoid duplication of connectors related information
160 delete this.stationInfo.Connectors;
161 // Initialize transaction attributes on connectors
162 for (const connector in this.connectors) {
163 if (Utils.convertToInt(connector) > 0 && !this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
164 this._initTransactionOnConnector(Utils.convertToInt(connector));
165 }
166 }
167 // OCPP parameters
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);
171 }
172 this.stationInfo.powerDivider = this._getPowerDivider();
173 if (this.getEnableStatistics()) {
174 this.statistics = new Statistics(this.stationInfo.chargingStationId);
175 this.performanceObserver = new PerformanceObserver((list) => {
176 const entry = list.getEntries()[0];
177 this.statistics.logPerformance(entry, Constants.ENTITY_CHARGING_STATION);
178 this.performanceObserver.disconnect();
179 });
180 }
181 }
182
183 _logPrefix(): string {
184 return Utils.logPrefix(` ${this.stationInfo.chargingStationId}:`);
185 }
186
187 _isWebSocketOpen(): boolean {
188 return this.wsConnection?.readyState === WebSocket.OPEN;
189 }
190
191 _isRegistered(): boolean {
192 return this.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED;
193 }
194
195 _getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
196 return this.stationInfo.Configuration ? this.stationInfo.Configuration : {} as ChargingStationConfiguration;
197 }
198
199 _getAuthorizationFile(): string {
200 return this.stationInfo.authorizationFile && this.stationInfo.authorizationFile;
201 }
202
203 _getUseConnectorId0(): boolean {
204 return !Utils.isUndefined(this.stationInfo.useConnectorId0) ? this.stationInfo.useConnectorId0 : true;
205 }
206
207 _loadAndGetAuthorizedTags(): string[] {
208 let authorizedTags: string[] = [];
209 const authorizationFile = this._getAuthorizationFile();
210 if (authorizationFile) {
211 try {
212 // Load authorization file
213 const fileDescriptor = fs.openSync(authorizationFile, 'r');
214 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
215 fs.closeSync(fileDescriptor);
216 } catch (error) {
217 logger.error(this._logPrefix() + ' Authorization file ' + authorizationFile + ' loading error: %j', error);
218 throw error;
219 }
220 } else {
221 logger.info(this._logPrefix() + ' No authorization file given in template file ' + this.stationTemplateFile);
222 }
223 return authorizedTags;
224 }
225
226 getRandomTagId(): string {
227 const index = Math.floor(Math.random() * this.authorizedTags.length);
228 return this.authorizedTags[index];
229 }
230
231 hasAuthorizedTags(): boolean {
232 return !Utils.isEmptyArray(this.authorizedTags);
233 }
234
235 getEnableStatistics(): boolean {
236 return !Utils.isUndefined(this.stationInfo.enableStatistics) ? this.stationInfo.enableStatistics : true;
237 }
238
239 _getNumberOfPhases(): number {
240 switch (this._getPowerOutType()) {
241 case PowerOutType.AC:
242 return !Utils.isUndefined(this.stationInfo.numberOfPhases) ? this.stationInfo.numberOfPhases : 3;
243 case PowerOutType.DC:
244 return 0;
245 }
246 }
247
248 _getNumberOfRunningTransactions(): number {
249 let trxCount = 0;
250 for (const connector in this.connectors) {
251 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
252 trxCount++;
253 }
254 }
255 return trxCount;
256 }
257
258 // 0 for disabling
259 _getConnectionTimeout(): number {
260 if (!Utils.isUndefined(this.stationInfo.connectionTimeout)) {
261 return this.stationInfo.connectionTimeout;
262 }
263 if (!Utils.isUndefined(Configuration.getConnectionTimeout())) {
264 return Configuration.getConnectionTimeout();
265 }
266 return 30;
267 }
268
269 // -1 for unlimited, 0 for disabling
270 _getAutoReconnectMaxRetries(): number {
271 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
272 return this.stationInfo.autoReconnectMaxRetries;
273 }
274 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
275 return Configuration.getAutoReconnectMaxRetries();
276 }
277 return -1;
278 }
279
280 // 0 for disabling
281 _getRegistrationMaxRetries(): number {
282 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
283 return this.stationInfo.registrationMaxRetries;
284 }
285 return -1;
286 }
287
288 _getPowerDivider(): number {
289 let powerDivider = this._getNumberOfConnectors();
290 if (this.stationInfo.powerSharedByConnectors) {
291 powerDivider = this._getNumberOfRunningTransactions();
292 }
293 return powerDivider;
294 }
295
296 getConnector(id: number): Connector {
297 return this.connectors[id];
298 }
299
300 _isConnectorAvailable(id: number): boolean {
301 return this.getConnector(id).availability === AvailabilityType.OPERATIVE;
302 }
303
304 _isChargingStationAvailable(): boolean {
305 return this.getConnector(0).availability === AvailabilityType.OPERATIVE;
306 }
307
308 _getTemplateMaxNumberOfConnectors(): number {
309 return Object.keys(this.stationInfo.Connectors).length;
310 }
311
312 _getMaxNumberOfConnectors(): number {
313 let maxConnectors = 0;
314 if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
315 const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
316 // Distribute evenly the number of connectors
317 maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length];
318 } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
319 maxConnectors = this.stationInfo.numberOfConnectors as number;
320 } else {
321 maxConnectors = this.stationInfo.Connectors[0] ? this._getTemplateMaxNumberOfConnectors() - 1 : this._getTemplateMaxNumberOfConnectors();
322 }
323 return maxConnectors;
324 }
325
326 _getNumberOfConnectors(): number {
327 return this.connectors[0] ? Object.keys(this.connectors).length - 1 : Object.keys(this.connectors).length;
328 }
329
330 _getVoltageOut(): number {
331 const errMsg = `${this._logPrefix()} Unknown ${this._getPowerOutType()} powerOutType in template file ${this.stationTemplateFile}, cannot define default voltage out`;
332 let defaultVoltageOut: number;
333 switch (this._getPowerOutType()) {
334 case PowerOutType.AC:
335 defaultVoltageOut = VoltageOut.VOLTAGE_230;
336 break;
337 case PowerOutType.DC:
338 defaultVoltageOut = VoltageOut.VOLTAGE_400;
339 break;
340 default:
341 logger.error(errMsg);
342 throw Error(errMsg);
343 }
344 return !Utils.isUndefined(this.stationInfo.voltageOut) ? this.stationInfo.voltageOut : defaultVoltageOut;
345 }
346
347 _getTransactionIdTag(transactionId: number): string {
348 for (const connector in this.connectors) {
349 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
350 return this.getConnector(Utils.convertToInt(connector)).idTag;
351 }
352 }
353 }
354
355 _getTransactionMeterStop(transactionId: number): number {
356 for (const connector in this.connectors) {
357 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionId === transactionId) {
358 return this.getConnector(Utils.convertToInt(connector)).lastEnergyActiveImportRegisterValue;
359 }
360 }
361 }
362
363 _getPowerOutType(): PowerOutType {
364 return !Utils.isUndefined(this.stationInfo.powerOutType) ? this.stationInfo.powerOutType : PowerOutType.AC;
365 }
366
367 _getSupervisionURL(): string {
368 const supervisionUrls = Utils.cloneObject<string | string[]>(this.stationInfo.supervisionURL ? this.stationInfo.supervisionURL : Configuration.getSupervisionURLs());
369 let indexUrl = 0;
370 if (!Utils.isEmptyArray(supervisionUrls)) {
371 if (Configuration.getDistributeStationsToTenantsEqually()) {
372 indexUrl = this.index % supervisionUrls.length;
373 } else {
374 // Get a random url
375 indexUrl = Math.floor(Math.random() * supervisionUrls.length);
376 }
377 return supervisionUrls[indexUrl];
378 }
379 return supervisionUrls as string;
380 }
381
382 _getReconnectExponentialDelay(): boolean {
383 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay) ? this.stationInfo.reconnectExponentialDelay : false;
384 }
385
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 }
395 }
396
397 _getAuthorizeRemoteTxRequests(): boolean {
398 const authorizeRemoteTxRequests = this._getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests);
399 return authorizeRemoteTxRequests ? Utils.convertToBoolean(authorizeRemoteTxRequests.value) : false;
400 }
401
402 _getLocalAuthListEnabled(): boolean {
403 const localAuthListEnabled = this._getConfigurationKey(StandardParametersKey.LocalAuthListEnabled);
404 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
405 }
406
407 async _startMessageSequence(): Promise<void> {
408 // Start WebSocket ping
409 this._startWebSocketPing();
410 // Start heartbeat
411 this._startHeartbeat();
412 // Initialize connectors status
413 for (const connector in this.connectors) {
414 if (Utils.convertToInt(connector) === 0) {
415 continue;
416 } else if (!this.hasStopped && !this.getConnector(Utils.convertToInt(connector))?.status && this.getConnector(Utils.convertToInt(connector))?.bootStatus) {
417 // Send status in template at startup
418 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
419 } else if (this.hasStopped && this.getConnector(Utils.convertToInt(connector))?.bootStatus) {
420 // Send status in template after reset
421 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).bootStatus);
422 } else if (!this.hasStopped && this.getConnector(Utils.convertToInt(connector))?.status) {
423 // Send previous status at template reload
424 await this.sendStatusNotification(Utils.convertToInt(connector), this.getConnector(Utils.convertToInt(connector)).status);
425 } else {
426 // Send default status
427 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.AVAILABLE);
428 }
429 }
430 // Start the ATG
431 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
432 if (!this.automaticTransactionGeneration) {
433 this.automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
434 }
435 if (this.automaticTransactionGeneration.timeToStop) {
436 this.automaticTransactionGeneration.start();
437 }
438 }
439 if (this.getEnableStatistics()) {
440 this.statistics.start();
441 }
442 }
443
444 async _stopMessageSequence(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
445 // Stop WebSocket ping
446 this._stopWebSocketPing();
447 // Stop heartbeat
448 this._stopHeartbeat();
449 // Stop the ATG
450 if (this.stationInfo.AutomaticTransactionGenerator.enable &&
451 this.automaticTransactionGeneration &&
452 !this.automaticTransactionGeneration.timeToStop) {
453 await this.automaticTransactionGeneration.stop(reason);
454 } else {
455 for (const connector in this.connectors) {
456 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector)).transactionStarted) {
457 await this.sendStopTransaction(this.getConnector(Utils.convertToInt(connector)).transactionId, reason);
458 }
459 }
460 }
461 }
462
463 _startWebSocketPing(): void {
464 const webSocketPingInterval: number = this._getConfigurationKey(StandardParametersKey.WebSocketPingInterval) ? Utils.convertToInt(this._getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value) : 0;
465 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
466 this.webSocketPingSetInterval = setInterval(() => {
467 if (this._isWebSocketOpen()) {
468 this.wsConnection.ping((): void => { });
469 }
470 }, webSocketPingInterval * 1000);
471 logger.info(this._logPrefix() + ' WebSocket ping started every ' + Utils.secondsToHHMMSS(webSocketPingInterval));
472 } else if (this.webSocketPingSetInterval) {
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 {
480 if (this.webSocketPingSetInterval) {
481 clearInterval(this.webSocketPingSetInterval);
482 this.webSocketPingSetInterval = null;
483 }
484 }
485
486 _restartWebSocketPing(): void {
487 // Stop WebSocket ping
488 this._stopWebSocketPing();
489 // Start WebSocket ping
490 this._startWebSocketPing();
491 }
492
493 _startHeartbeat(): void {
494 if (this._getHeartbeatInterval() && this._getHeartbeatInterval() > 0 && !this.heartbeatSetInterval) {
495 this.heartbeatSetInterval = setInterval(async () => {
496 await this.sendHeartbeat();
497 }, this._getHeartbeatInterval());
498 logger.info(this._logPrefix() + ' Heartbeat started every ' + Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()));
499 } else if (this.heartbeatSetInterval) {
500 logger.info(this._logPrefix() + ' Heartbeat every ' + Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()) + ' already started');
501 } else {
502 logger.error(`${this._logPrefix()} Heartbeat interval set to ${this._getHeartbeatInterval() ? Utils.milliSecondsToHHMMSS(this._getHeartbeatInterval()) : this._getHeartbeatInterval()}, not starting the heartbeat`);
503 }
504 }
505
506 _stopHeartbeat(): void {
507 if (this.heartbeatSetInterval) {
508 clearInterval(this.heartbeatSetInterval);
509 this.heartbeatSetInterval = null;
510 }
511 }
512
513 _restartHeartbeat(): void {
514 // Stop heartbeat
515 this._stopHeartbeat();
516 // Start heartbeat
517 this._startHeartbeat();
518 }
519
520 _startAuthorizationFileMonitoring(): void {
521 fs.watch(this._getAuthorizationFile()).on('change', (e) => {
522 try {
523 logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
524 // Initialize _authorizedTags
525 this.authorizedTags = this._loadAndGetAuthorizedTags();
526 } catch (error) {
527 logger.error(this._logPrefix() + ' Authorization file monitoring error: %j', error);
528 }
529 });
530 }
531
532 _startStationTemplateFileMonitoring(): void {
533 fs.watch(this.stationTemplateFile).on('change', (e) => {
534 try {
535 logger.debug(this._logPrefix() + ' Template file ' + this.stationTemplateFile + ' have changed, reload');
536 // Initialize
537 this._initialize();
538 // Stop the ATG
539 if (!this.stationInfo.AutomaticTransactionGenerator.enable &&
540 this.automaticTransactionGeneration) {
541 this.automaticTransactionGeneration.stop().catch(() => { });
542 }
543 // Start the ATG
544 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
545 if (!this.automaticTransactionGeneration) {
546 this.automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
547 }
548 if (this.automaticTransactionGeneration.timeToStop) {
549 this.automaticTransactionGeneration.start();
550 }
551 }
552 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
553 } catch (error) {
554 logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error);
555 }
556 });
557 }
558
559 _startMeterValues(connectorId: number, interval: number): void {
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) {
569 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`);
570 return;
571 } else if (this.getConnector(connectorId)?.transactionStarted && !this.getConnector(connectorId)?.transactionId) {
572 logger.error(`${this._logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`);
573 return;
574 }
575 if (interval > 0) {
576 this.getConnector(connectorId).transactionSetInterval = setInterval(async () => {
577 if (this.getEnableStatistics()) {
578 const sendMeterValues = performance.timerify(this.sendMeterValues);
579 this.performanceObserver.observe({
580 entryTypes: ['function'],
581 });
582 await sendMeterValues(connectorId, interval, this);
583 } else {
584 await this.sendMeterValues(connectorId, interval, this);
585 }
586 }, interval);
587 } else {
588 logger.error(`${this._logPrefix()} Charging station ${StandardParametersKey.MeterValueSampleInterval} configuration set to ${Utils.milliSecondsToHHMMSS(interval)}, not sending MeterValues`);
589 }
590 }
591
592 _openWSConnection(options?: WebSocket.ClientOptions, forceCloseOpened = false): void {
593 if (Utils.isUndefined(options)) {
594 options = {} as WebSocket.ClientOptions;
595 }
596 if (Utils.isUndefined(options.handshakeTimeout)) {
597 options.handshakeTimeout = this._getConnectionTimeout() * 1000;
598 }
599 if (this._isWebSocketOpen() && forceCloseOpened) {
600 this.wsConnection.close();
601 }
602 this.wsConnection = new WebSocket(this.wsConnectionUrl, 'ocpp' + Constants.OCPP_VERSION_16, options);
603 logger.info(this._logPrefix() + ' Will communicate through URL ' + this.supervisionUrl);
604 }
605
606 start(): void {
607 this._openWSConnection();
608 // Monitor authorization file
609 this._startAuthorizationFileMonitoring();
610 // Monitor station template file
611 this._startStationTemplateFileMonitoring();
612 // Handle Socket incoming messages
613 this.wsConnection.on('message', this.onMessage.bind(this));
614 // Handle Socket error
615 this.wsConnection.on('error', this.onError.bind(this));
616 // Handle Socket close
617 this.wsConnection.on('close', this.onClose.bind(this));
618 // Handle Socket opening connection
619 this.wsConnection.on('open', this.onOpen.bind(this));
620 // Handle Socket ping
621 this.wsConnection.on('ping', this.onPing.bind(this));
622 // Handle Socket pong
623 this.wsConnection.on('pong', this.onPong.bind(this));
624 }
625
626 async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
627 // Stop message sequence
628 await this._stopMessageSequence(reason);
629 for (const connector in this.connectors) {
630 if (Utils.convertToInt(connector) > 0) {
631 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.UNAVAILABLE);
632 }
633 }
634 if (this._isWebSocketOpen()) {
635 this.wsConnection.close();
636 }
637 this.bootNotificationResponse = null;
638 this.hasStopped = true;
639 }
640
641 async _reconnect(error): Promise<void> {
642 // Stop heartbeat
643 this._stopHeartbeat();
644 // Stop the ATG if needed
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);
654 logger.error(`${this._logPrefix()} Socket: connection retry in ${Utils.roundTo(reconnectDelay, 2)}ms, timeout ${reconnectDelay - 100}ms`);
655 await Utils.sleep(reconnectDelay);
656 logger.error(this._logPrefix() + ' Socket: reconnecting try #' + this.autoReconnectRetryCount.toString());
657 this._openWSConnection({ handshakeTimeout: reconnectDelay - 100 });
658 this.hasSocketRestarted = true;
659 } else if (this._getAutoReconnectMaxRetries() !== -1) {
660 logger.error(`${this._logPrefix()} Socket reconnect failure: max retries reached (${this.autoReconnectRetryCount}) or retry disabled (${this._getAutoReconnectMaxRetries()})`);
661 }
662 }
663
664 async onOpen(): Promise<void> {
665 logger.info(`${this._logPrefix()} Is connected to server through ${this.wsConnectionUrl}`);
666 if (!this._isRegistered()) {
667 // Send BootNotification
668 let registrationRetryCount = 0;
669 do {
670 this.bootNotificationResponse = await this.sendBootNotification();
671 if (!this._isRegistered()) {
672 registrationRetryCount++;
673 await Utils.sleep(this.bootNotificationResponse?.interval ? this.bootNotificationResponse.interval * 1000 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL);
674 }
675 } while (!this._isRegistered() && (registrationRetryCount <= this._getRegistrationMaxRetries() || this._getRegistrationMaxRetries() === -1));
676 }
677 if (this._isRegistered()) {
678 await this._startMessageSequence();
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);
684 });
685 }
686 }
687 } else {
688 logger.error(`${this._logPrefix()} Registration failure: max retries reached (${this._getRegistrationMaxRetries()}) or retry disabled (${this._getRegistrationMaxRetries()})`);
689 }
690 this.autoReconnectRetryCount = 0;
691 this.hasSocketRestarted = false;
692 }
693
694 async onError(errorEvent): Promise<void> {
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 // }
701 }
702
703 async onClose(closeEvent): Promise<void> {
704 switch (closeEvent) {
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)}'`);
708 this.autoReconnectRetryCount = 0;
709 break;
710 default: // Abnormal close
711 logger.error(`${this._logPrefix()} Socket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(closeEvent)}'`);
712 await this._reconnect(closeEvent);
713 break;
714 }
715 }
716
717 onPing(): void {
718 logger.debug(this._logPrefix() + ' Has received a WS ping (rfc6455) from the server');
719 }
720
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> {
726 let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [0, '', '' as IncomingRequestCommand, {}, {}];
727 let responseCallback: (payload?: Record<string, unknown> | string, requestPayload?: Record<string, unknown>) => void;
728 let rejectCallback: (error: OCPPError) => void;
729 let requestPayload: Record<string, unknown>;
730 let errMsg: string;
731 try {
732 // Parse the message
733 [messageType, messageId, commandName, commandPayload, errorDetails] = JSON.parse(messageEvent.toString()) as IncomingRequest;
734
735 // Check the Type of message
736 switch (messageType) {
737 // Incoming Message
738 case MessageType.CALL_MESSAGE:
739 if (this.getEnableStatistics()) {
740 this.statistics.addMessage(commandName, messageType);
741 }
742 // Process the call
743 await this.handleRequest(messageId, commandName, commandPayload);
744 break;
745 // Outcome Message
746 case MessageType.CALL_RESULT_MESSAGE:
747 // Respond
748 if (Utils.isIterable(this.requests[messageId])) {
749 [responseCallback, , requestPayload] = this.requests[messageId];
750 } else {
751 throw new Error(`Response request for message id ${messageId} is not iterable`);
752 }
753 if (!responseCallback) {
754 // Error
755 throw new Error(`Response request for unknown message id ${messageId}`);
756 }
757 delete this.requests[messageId];
758 responseCallback(commandName, requestPayload);
759 break;
760 // Error Message
761 case MessageType.CALL_ERROR_MESSAGE:
762 if (!this.requests[messageId]) {
763 // Error
764 throw new Error(`Error request for unknown message id ${messageId}`);
765 }
766 if (Utils.isIterable(this.requests[messageId])) {
767 [, rejectCallback] = this.requests[messageId];
768 } else {
769 throw new Error(`Error request for message id ${messageId} is not iterable`);
770 }
771 delete this.requests[messageId];
772 rejectCallback(new OCPPError(commandName, commandPayload.toString(), errorDetails));
773 break;
774 // Error
775 default:
776 errMsg = `${this._logPrefix()} Wrong message type ${messageType}`;
777 logger.error(errMsg);
778 throw new Error(errMsg);
779 }
780 } catch (error) {
781 // Log
782 logger.error('%s Incoming message %j processing error %j on request content type %j', this._logPrefix(), messageEvent, error, this.requests[messageId]);
783 // Send error
784 messageType !== MessageType.CALL_ERROR_MESSAGE && await this.sendError(messageId, error, commandName);
785 }
786 }
787
788 async sendHeartbeat(): Promise<void> {
789 try {
790 const payload: HeartbeatRequest = {};
791 await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.HEARTBEAT);
792 } catch (error) {
793 this.handleRequestError(RequestCommand.HEARTBEAT, error);
794 }
795 }
796
797 async sendBootNotification(): Promise<BootNotificationResponse> {
798 try {
799 return await this.sendMessage(Utils.generateUUID(), this.bootNotificationRequest, MessageType.CALL_MESSAGE, RequestCommand.BOOT_NOTIFICATION) as BootNotificationResponse;
800 } catch (error) {
801 this.handleRequestError(RequestCommand.BOOT_NOTIFICATION, error);
802 }
803 }
804
805 async sendStatusNotification(connectorId: number, status: ChargePointStatus, errorCode: ChargePointErrorCode = ChargePointErrorCode.NO_ERROR): Promise<void> {
806 this.getConnector(connectorId).status = status;
807 try {
808 const payload: StatusNotificationRequest = {
809 connectorId,
810 errorCode,
811 status,
812 };
813 await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.STATUS_NOTIFICATION);
814 } catch (error) {
815 this.handleRequestError(RequestCommand.STATUS_NOTIFICATION, error);
816 }
817 }
818
819 async sendAuthorize(idTag?: string): Promise<AuthorizeResponse> {
820 try {
821 const payload: AuthorizeRequest = {
822 ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
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
830 async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
831 try {
832 const payload: StartTransactionRequest = {
833 connectorId,
834 ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_TAGID },
835 meterStart: 0,
836 timestamp: new Date().toISOString(),
837 };
838 return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.START_TRANSACTION) as StartTransactionResponse;
839 } catch (error) {
840 this.handleRequestError(RequestCommand.START_TRANSACTION, error);
841 }
842 }
843
844 async sendStopTransaction(transactionId: number, reason: StopTransactionReason = StopTransactionReason.NONE): Promise<StopTransactionResponse> {
845 const idTag = this._getTransactionIdTag(transactionId);
846 try {
847 const payload: StopTransactionRequest = {
848 transactionId,
849 ...!Utils.isUndefined(idTag) && { idTag: idTag },
850 meterStop: this._getTransactionMeterStop(transactionId),
851 timestamp: new Date().toISOString(),
852 ...reason && { reason },
853 };
854 return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.STOP_TRANSACTION) as StartTransactionResponse;
855 } catch (error) {
856 this.handleRequestError(RequestCommand.STOP_TRANSACTION, error);
857 }
858 }
859
860 async sendError(messageId: string, error: OCPPError, commandName: RequestCommand | IncomingRequestCommand): Promise<unknown> {
861 // Send error
862 return this.sendMessage(messageId, error, MessageType.CALL_ERROR_MESSAGE, commandName);
863 }
864
865 async sendMessage(messageId: string, commandParams: any, messageType: MessageType = MessageType.CALL_RESULT_MESSAGE, commandName: RequestCommand | IncomingRequestCommand): Promise<any> {
866 // eslint-disable-next-line @typescript-eslint/no-this-alias
867 const self = this;
868 // Send a message through wsConnection
869 return new Promise((resolve: (value?: any | PromiseLike<any>) => void, reject: (reason?: any) => void) => {
870 let messageToSend: string;
871 // Type of message
872 switch (messageType) {
873 // Request
874 case MessageType.CALL_MESSAGE:
875 // Build request
876 this.requests[messageId] = [responseCallback, rejectCallback, commandParams] as Request;
877 messageToSend = JSON.stringify([messageType, messageId, commandName, commandParams]);
878 break;
879 // Response
880 case MessageType.CALL_RESULT_MESSAGE:
881 // Build response
882 messageToSend = JSON.stringify([messageType, messageId, commandParams]);
883 break;
884 // Error Message
885 case MessageType.CALL_ERROR_MESSAGE:
886 // Build Error Message
887 messageToSend = JSON.stringify([messageType, messageId, commandParams.code ? commandParams.code : ErrorType.GENERIC_ERROR, commandParams.message ? commandParams.message : '', commandParams.details ? commandParams.details : {}]);
888 break;
889 }
890 // Check if wsConnection opened and charging station registered
891 if (this._isWebSocketOpen() && (this._isRegistered() || commandName === RequestCommand.BOOT_NOTIFICATION)) {
892 if (this.getEnableStatistics()) {
893 this.statistics.addMessage(commandName, messageType);
894 }
895 // Yes: Send Message
896 this.wsConnection.send(messageToSend);
897 } else if (commandName !== RequestCommand.BOOT_NOTIFICATION) {
898 let dups = false;
899 // Handle dups in buffer
900 for (const message of this.messageQueue) {
901 // Same message
902 if (messageToSend === message) {
903 dups = true;
904 break;
905 }
906 }
907 if (!dups) {
908 // Buffer message
909 this.messageQueue.push(messageToSend);
910 }
911 // Reject it
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 : {}));
913 }
914 // Response?
915 if (messageType === MessageType.CALL_RESULT_MESSAGE) {
916 // Yes: send Ok
917 resolve();
918 } else if (messageType === MessageType.CALL_ERROR_MESSAGE) {
919 // Send timeout
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);
921 }
922
923 // Function that will receive the request's response
924 async function responseCallback(payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
925 if (self.getEnableStatistics()) {
926 self.statistics.addMessage(commandName, messageType);
927 }
928 // Send the response
929 await self.handleResponse(commandName as RequestCommand, payload, requestPayload);
930 resolve(payload);
931 }
932
933 // Function that will receive the request's rejection
934 function rejectCallback(error: OCPPError): void {
935 if (self.getEnableStatistics()) {
936 self.statistics.addMessage(commandName, messageType);
937 }
938 logger.debug(`${self._logPrefix()} Error: %j occurred when calling command %s with parameters: %j`, error, commandName, commandParams);
939 // Build Exception
940 // eslint-disable-next-line no-empty-function
941 self.requests[messageId] = [() => { }, () => { }, {}]; // Properly format the request
942 // Send error
943 reject(error);
944 }
945 });
946 }
947
948 async handleResponse(commandName: RequestCommand, payload: Record<string, unknown> | string, requestPayload: Record<string, unknown>): Promise<void> {
949 const responseCallbackFn = 'handleResponse' + commandName;
950 if (typeof this[responseCallbackFn] === 'function') {
951 await this[responseCallbackFn](payload, requestPayload);
952 } else {
953 logger.error(this._logPrefix() + ' Trying to call an undefined response callback function: ' + responseCallbackFn);
954 }
955 }
956
957 handleResponseBootNotification(payload: BootNotificationResponse, requestPayload: BootNotificationRequest): void {
958 if (payload.status === RegistrationStatus.ACCEPTED) {
959 this.heartbeatSetInterval ? this._restartHeartbeat() : this._startHeartbeat();
960 this._addConfigurationKey(StandardParametersKey.HeartBeatInterval, payload.interval.toString());
961 this._addConfigurationKey(StandardParametersKey.HeartbeatInterval, payload.interval.toString(), false, false);
962 this.hasStopped && (this.hasStopped = false);
963 } else if (payload.status === RegistrationStatus.PENDING) {
964 logger.info(this._logPrefix() + ' Charging station in pending state on the central server');
965 } else {
966 logger.info(this._logPrefix() + ' Charging station rejected by the central server');
967 }
968 }
969
970 _initTransactionOnConnector(connectorId: number): void {
971 this.getConnector(connectorId).transactionStarted = false;
972 this.getConnector(connectorId).transactionId = null;
973 this.getConnector(connectorId).idTag = null;
974 this.getConnector(connectorId).lastEnergyActiveImportRegisterValue = -1;
975 }
976
977 _resetTransactionOnConnector(connectorId: number): void {
978 this._initTransactionOnConnector(connectorId);
979 if (this.getConnector(connectorId)?.transactionSetInterval) {
980 clearInterval(this.getConnector(connectorId).transactionSetInterval);
981 }
982 }
983
984 async handleResponseStartTransaction(payload: StartTransactionResponse, requestPayload: StartTransactionRequest): Promise<void> {
985 const connectorId = requestPayload.connectorId;
986
987 let transactionConnectorId: number;
988 for (const connector in this.connectors) {
989 if (Utils.convertToInt(connector) > 0 && Utils.convertToInt(connector) === connectorId) {
990 transactionConnectorId = Utils.convertToInt(connector);
991 break;
992 }
993 }
994 if (!transactionConnectorId) {
995 logger.error(this._logPrefix() + ' Trying to start a transaction on a non existing connector Id ' + connectorId.toString());
996 return;
997 }
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
1003 if (payload?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
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;
1008 await this.sendStatusNotification(connectorId, ChargePointStatus.CHARGING);
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++;
1012 }
1013 const configuredMeterValueSampleInterval = this._getConfigurationKey(StandardParametersKey.MeterValueSampleInterval);
1014 this._startMeterValues(connectorId,
1015 configuredMeterValueSampleInterval ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000 : 60000);
1016 } else {
1017 logger.error(this._logPrefix() + ' Starting transaction id ' + payload.transactionId.toString() + ' REJECTED with status ' + payload?.idTagInfo?.status + ', idTag ' + requestPayload.idTag);
1018 this._resetTransactionOnConnector(connectorId);
1019 await this.sendStatusNotification(connectorId, ChargePointStatus.AVAILABLE);
1020 }
1021 }
1022
1023 async handleResponseStopTransaction(payload: StopTransactionResponse, requestPayload: StopTransactionRequest): Promise<void> {
1024 let transactionConnectorId: number;
1025 for (const connector in this.connectors) {
1026 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector))?.transactionId === requestPayload.transactionId) {
1027 transactionConnectorId = Utils.convertToInt(connector);
1028 break;
1029 }
1030 }
1031 if (!transactionConnectorId) {
1032 logger.error(this._logPrefix() + ' Trying to stop a non existing transaction ' + requestPayload.transactionId.toString());
1033 return;
1034 }
1035 if (payload.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
1036 if (!this._isChargingStationAvailable() || !this._isConnectorAvailable(transactionConnectorId)) {
1037 await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.UNAVAILABLE);
1038 } else {
1039 await this.sendStatusNotification(transactionConnectorId, ChargePointStatus.AVAILABLE);
1040 }
1041 if (this.stationInfo.powerSharedByConnectors) {
1042 this.stationInfo.powerDivider--;
1043 }
1044 logger.info(this._logPrefix() + ' Transaction ' + requestPayload.transactionId.toString() + ' STOPPED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorId.toString());
1045 this._resetTransactionOnConnector(transactionConnectorId);
1046 } else {
1047 logger.error(this._logPrefix() + ' Stopping transaction id ' + requestPayload.transactionId.toString() + ' REJECTED with status ' + payload.idTagInfo?.status);
1048 }
1049 }
1050
1051 handleResponseStatusNotification(payload: StatusNotificationRequest, requestPayload: StatusNotificationResponse): void {
1052 logger.debug(this._logPrefix() + ' Status notification response received: %j to StatusNotification request: %j', payload, requestPayload);
1053 }
1054
1055 handleResponseMeterValues(payload: MeterValuesRequest, requestPayload: MeterValuesResponse): void {
1056 logger.debug(this._logPrefix() + ' MeterValues response received: %j to MeterValues request: %j', payload, requestPayload);
1057 }
1058
1059 handleResponseHeartbeat(payload: HeartbeatResponse, requestPayload: HeartbeatRequest): void {
1060 logger.debug(this._logPrefix() + ' Heartbeat response received: %j to Heartbeat request: %j', payload, requestPayload);
1061 }
1062
1063 handleResponseAuthorize(payload: AuthorizeResponse, requestPayload: AuthorizeRequest): void {
1064 logger.debug(this._logPrefix() + ' Authorize response received: %j to Authorize request: %j', payload, requestPayload);
1065 }
1066
1067 async handleRequest(messageId: string, commandName: IncomingRequestCommand, commandPayload: Record<string, unknown>): Promise<void> {
1068 let response;
1069 // Call
1070 if (typeof this['handleRequest' + commandName] === 'function') {
1071 try {
1072 // Call the method to build the response
1073 response = await this['handleRequest' + commandName](commandPayload);
1074 } catch (error) {
1075 // Log
1076 logger.error(this._logPrefix() + ' Handle request error: %j', error);
1077 // Send back response to inform backend
1078 await this.sendError(messageId, error, commandName);
1079 throw error;
1080 }
1081 } else {
1082 // Throw exception
1083 await this.sendError(messageId, new OCPPError(ErrorType.NOT_IMPLEMENTED, `${commandName} is not implemented`, {}), commandName);
1084 throw new Error(`${commandName} is not implemented ${JSON.stringify(commandPayload, null, ' ')}`);
1085 }
1086 // Send response
1087 await this.sendMessage(messageId, response, MessageType.CALL_RESULT_MESSAGE, commandName);
1088 }
1089
1090 // Simulate charging station restart
1091 handleRequestReset(commandPayload: ResetRequest): DefaultResponse {
1092 setImmediate(async () => {
1093 await this.stop(commandPayload.type + 'Reset' as StopTransactionReason);
1094 await Utils.sleep(this.stationInfo.resetTime);
1095 await this.start();
1096 });
1097 logger.info(`${this._logPrefix()} ${commandPayload.type} reset command received, simulating it. The station will be back online in ${Utils.milliSecondsToHHMMSS(this.stationInfo.resetTime)}`);
1098 return Constants.OCPP_RESPONSE_ACCEPTED;
1099 }
1100
1101 handleRequestClearCache(): DefaultResponse {
1102 return Constants.OCPP_RESPONSE_ACCEPTED;
1103 }
1104
1105 async handleRequestUnlockConnector(commandPayload: UnlockConnectorRequest): Promise<UnlockConnectorResponse> {
1106 const connectorId = commandPayload.connectorId;
1107 if (connectorId === 0) {
1108 logger.error(this._logPrefix() + ' Trying to unlock connector ' + connectorId.toString());
1109 return Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED;
1110 }
1111 if (this.getConnector(connectorId)?.transactionStarted) {
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
1122 _getConfigurationKey(key: string | StandardParametersKey, caseInsensitive = false): ConfigurationKey {
1123 const configurationKey: ConfigurationKey = this.configuration.configurationKey.find((configElement) => {
1124 if (caseInsensitive) {
1125 return configElement.key.toLowerCase() === key.toLowerCase();
1126 }
1127 return configElement.key === key;
1128 });
1129 return configurationKey;
1130 }
1131
1132 _addConfigurationKey(key: string | StandardParametersKey, value: string, readonly = false, visible = true, reboot = false): void {
1133 const keyFound = this._getConfigurationKey(key);
1134 if (!keyFound) {
1135 this.configuration.configurationKey.push({
1136 key,
1137 readonly,
1138 value,
1139 visible,
1140 reboot,
1141 });
1142 } else {
1143 logger.error(`${this._logPrefix()} Trying to add an already existing configuration key: %j`, keyFound);
1144 }
1145 }
1146
1147 _setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
1148 const keyFound = this._getConfigurationKey(key);
1149 if (keyFound) {
1150 const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
1151 this.configuration.configurationKey[keyIndex].value = value;
1152 } else {
1153 logger.error(`${this._logPrefix()} Trying to set a value on a non existing configuration key: %j`, { key, value });
1154 }
1155 }
1156
1157 handleRequestGetConfiguration(commandPayload: GetConfigurationRequest): GetConfigurationResponse {
1158 const configurationKey: OCPPConfigurationKey[] = [];
1159 const unknownKey: string[] = [];
1160 if (Utils.isEmptyArray(commandPayload.key)) {
1161 for (const configuration of this.configuration.configurationKey) {
1162 if (Utils.isUndefined(configuration.visible)) {
1163 configuration.visible = true;
1164 }
1165 if (!configuration.visible) {
1166 continue;
1167 }
1168 configurationKey.push({
1169 key: configuration.key,
1170 readonly: configuration.readonly,
1171 value: configuration.value,
1172 });
1173 }
1174 } else {
1175 for (const key of commandPayload.key) {
1176 const keyFound = this._getConfigurationKey(key);
1177 if (keyFound) {
1178 if (Utils.isUndefined(keyFound.visible)) {
1179 keyFound.visible = true;
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 {
1190 unknownKey.push(key);
1191 }
1192 }
1193 }
1194 return {
1195 configurationKey,
1196 unknownKey,
1197 };
1198 }
1199
1200 handleRequestChangeConfiguration(commandPayload: ChangeConfigurationRequest): ChangeConfigurationResponse {
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 }
1208 const keyToChange = this._getConfigurationKey(commandPayload.key, true);
1209 if (!keyToChange) {
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) {
1214 const keyIndex = this.configuration.configurationKey.indexOf(keyToChange);
1215 let valueChanged = false;
1216 if (this.configuration.configurationKey[keyIndex].value !== commandPayload.value) {
1217 this.configuration.configurationKey[keyIndex].value = commandPayload.value;
1218 valueChanged = true;
1219 }
1220 let triggerHeartbeatRestart = false;
1221 if (keyToChange.key === StandardParametersKey.HeartBeatInterval && valueChanged) {
1222 this._setConfigurationKeyValue(StandardParametersKey.HeartbeatInterval, commandPayload.value);
1223 triggerHeartbeatRestart = true;
1224 }
1225 if (keyToChange.key === StandardParametersKey.HeartbeatInterval && valueChanged) {
1226 this._setConfigurationKeyValue(StandardParametersKey.HeartBeatInterval, commandPayload.value);
1227 triggerHeartbeatRestart = true;
1228 }
1229 if (triggerHeartbeatRestart) {
1230 this._restartHeartbeat();
1231 }
1232 if (keyToChange.key === StandardParametersKey.WebSocketPingInterval && valueChanged) {
1233 this._restartWebSocketPing();
1234 }
1235 if (keyToChange.reboot) {
1236 return Constants.OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED;
1237 }
1238 return Constants.OCPP_CONFIGURATION_RESPONSE_ACCEPTED;
1239 }
1240 }
1241
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
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}`);
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;
1275 }
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;
1279 }
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 }
1304 }
1305 if (clearedCP) {
1306 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED;
1307 }
1308 }
1309 return Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN;
1310 }
1311
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;
1321 for (const connector in this.connectors) {
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;
1326 response === Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED && this.sendStatusNotification(Utils.convertToInt(connector), chargePointStatus);
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;
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
1341 async handleRequestRemoteStartTransaction(commandPayload: RemoteStartTransactionRequest): Promise<DefaultResponse> {
1342 const transactionConnectorID: number = commandPayload.connectorId ? commandPayload.connectorId : 1;
1343 if (this._isChargingStationAvailable() && this._isConnectorAvailable(transactionConnectorID)) {
1344 if (this._getAuthorizeRemoteTxRequests() && this._getLocalAuthListEnabled() && this.hasAuthorizedTags()) {
1345 // Check if authorized
1346 if (this.authorizedTags.find((value) => value === commandPayload.idTag)) {
1347 await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
1348 if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
1349 this._setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
1350 } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
1351 return Constants.OCPP_RESPONSE_REJECTED;
1352 }
1353 // Authorization successful start transaction
1354 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
1355 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
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;
1360 }
1361 await this.sendStatusNotification(transactionConnectorID, ChargePointStatus.PREPARING);
1362 if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose === ChargingProfilePurposeType.TX_PROFILE) {
1363 this._setChargingProfile(transactionConnectorID, commandPayload.chargingProfile);
1364 } else if (commandPayload.chargingProfile && commandPayload.chargingProfile.chargingProfilePurpose !== ChargingProfilePurposeType.TX_PROFILE) {
1365 return Constants.OCPP_RESPONSE_REJECTED;
1366 }
1367 // No local authorization check required => start transaction
1368 await this.sendStartTransaction(transactionConnectorID, commandPayload.idTag);
1369 logger.debug(this._logPrefix() + ' Transaction remotely STARTED on ' + this.stationInfo.chargingStationId + '#' + transactionConnectorID.toString() + ' for idTag ' + commandPayload.idTag);
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;
1374 }
1375
1376 async handleRequestRemoteStopTransaction(commandPayload: RemoteStopTransactionRequest): Promise<DefaultResponse> {
1377 const transactionId = commandPayload.transactionId;
1378 for (const connector in this.connectors) {
1379 if (Utils.convertToInt(connector) > 0 && this.getConnector(Utils.convertToInt(connector))?.transactionId === transactionId) {
1380 await this.sendStatusNotification(Utils.convertToInt(connector), ChargePointStatus.FINISHING);
1381 await this.sendStopTransaction(transactionId);
1382 return Constants.OCPP_RESPONSE_ACCEPTED;
1383 }
1384 }
1385 logger.info(this._logPrefix() + ' Trying to remote stop a non existing transaction ' + transactionId.toString());
1386 return Constants.OCPP_RESPONSE_REJECTED;
1387 }
1388
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
1441 if (Utils.isUndefined(self.stationInfo.powerDivider)) {
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);
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}`;
1447 logger.error(errMsg);
1448 throw Error(errMsg);
1449 }
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`;
1451 const powerMeasurandValues = {} as MeasurandValues;
1452 const maxPower = Math.round(self.stationInfo.maxPower / self.stationInfo.powerDivider);
1453 const maxPowerPerPhase = Math.round((self.stationInfo.maxPower / self.stationInfo.powerDivider) / self._getNumberOfPhases());
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
1501 if (Utils.isUndefined(self.stationInfo.powerDivider)) {
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);
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}`;
1507 logger.error(errMsg);
1508 throw Error(errMsg);
1509 }
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`;
1511 const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
1512 let maxAmperage: number;
1513 switch (self._getPowerOutType()) {
1514 case PowerOutType.AC:
1515 maxAmperage = ElectricUtils.ampPerPhaseFromPower(self._getNumberOfPhases(), self.stationInfo.maxPower / self.stationInfo.powerDivider, self._getVoltageOut());
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:
1528 maxAmperage = ElectricUtils.ampTotalFromPower(self.stationInfo.maxPower / self.stationInfo.powerDivider, self._getVoltageOut());
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
1562 if (Utils.isUndefined(self.stationInfo.powerDivider)) {
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);
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}`;
1568 logger.error(errMsg);
1569 throw Error(errMsg);
1570 }
1571 if (Utils.isUndefined(meterValuesTemplate[index].value)) {
1572 const measurandValue = Utils.getRandomInt(self.stationInfo.maxPower / (self.stationInfo.powerDivider * 3600000) * interval);
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;
1589 const maxConsumption = Math.round(self.stationInfo.maxPower * 3600 / (self.stationInfo.powerDivider * interval));
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
1609 private handleRequestError(commandName: RequestCommand, error: Error) {
1610 logger.error(this._logPrefix() + ' Send ' + commandName + ' error: %j', error);
1611 throw error;
1612 }
1613 }
1614