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