X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=7df0a35ff7486bb7282455b4c3d63f629bbf7d99;hb=48847bc0d5e594c3e4c0b81c580ac6df48052668;hp=5a724e53672d93ee2ea90355184cd3001d48e918;hpb=ffb833ec8daea8112feed14af15cdfdb67f9d41b;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 5a724e53..7df0a35f 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,68 +1,16 @@ // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved. -import { createHash } from 'node:crypto' +import { createHash, randomInt } from 'node:crypto' import { EventEmitter } from 'node:events' -import { type FSWatcher, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, type FSWatcher, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { URL } from 'node:url' import { parentPort } from 'node:worker_threads' import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns' -import { mergeDeepRight } from 'rambda' +import { mergeDeepRight, once } from 'rambda' import { type RawData, WebSocket } from 'ws' -import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js' -import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js' -import { - addConfigurationKey, - deleteConfigurationKey, - getConfigurationKey, - setConfigurationKeyValue -} from './ConfigurationKeyUtils.js' -import { - buildConnectorsMap, - buildTemplateName, - checkChargingStation, - checkConfiguration, - checkConnectorsConfiguration, - checkStationInfoConnectorStatus, - checkTemplate, - createBootNotificationRequest, - createSerialNumber, - getAmperageLimitationUnitDivider, - getBootConnectorStatus, - getChargingStationConnectorChargingProfilesPowerLimit, - getChargingStationId, - getDefaultVoltageOut, - getHashId, - getIdTagsFile, - getMaxNumberOfEvses, - getNumberOfReservableConnectors, - getPhaseRotationValue, - hasFeatureProfile, - hasReservationExpired, - initializeConnectorsMapStatus, - propagateSerialNumber, - setChargingStationOptions, - stationTemplateToStationInfo, - warnTemplateKeysDeprecation -} from './Helpers.js' -import { IdTagsCache } from './IdTagsCache.js' -import { - OCPP16IncomingRequestService, - OCPP16RequestService, - OCPP16ResponseService, - OCPP20IncomingRequestService, - OCPP20RequestService, - OCPP20ResponseService, - type OCPPIncomingRequestService, - type OCPPRequestService, - buildMeterValue, - buildTransactionEndMeterValue, - getMessageTypeString, - sendAndSetConnectorStatus -} from './ocpp/index.js' -import { SharedLRUCache } from './SharedLRUCache.js' import { BaseError, OCPPError } from '../exception/index.js' import { PerformanceStatistics } from '../performance/index.js' import { @@ -114,17 +62,14 @@ import { SupervisionUrlDistribution, SupportedFeatureProfiles, type Voltage, - type WSError, WebSocketCloseEventStatusCode, + type WSError, type WsOptions } from '../types/index.js' import { ACElectricUtils, AsyncLock, AsyncLockType, - Configuration, - Constants, - DCElectricUtils, buildAddedMessage, buildChargingStationAutomaticTransactionGeneratorConfiguration, buildConnectorsStatus, @@ -134,26 +79,79 @@ import { buildStoppedMessage, buildUpdatedMessage, clone, + Configuration, + Constants, convertToBoolean, convertToDate, convertToInt, + DCElectricUtils, exponentialDelay, formatDurationMilliSeconds, formatDurationSeconds, - getRandomInteger, getWebSocketCloseEventStatusString, handleFileException, isNotEmptyArray, isNotEmptyString, - logPrefix, logger, + logPrefix, min, - once, roundTo, secureRandom, sleep, watchJsonFile } from '../utils/index.js' +import { AutomaticTransactionGenerator } from './AutomaticTransactionGenerator.js' +import { ChargingStationWorkerBroadcastChannel } from './broadcast-channel/ChargingStationWorkerBroadcastChannel.js' +import { + addConfigurationKey, + deleteConfigurationKey, + getConfigurationKey, + setConfigurationKeyValue +} from './ConfigurationKeyUtils.js' +import { + buildConnectorsMap, + buildTemplateName, + checkChargingStation, + checkConfiguration, + checkConnectorsConfiguration, + checkStationInfoConnectorStatus, + checkTemplate, + createBootNotificationRequest, + createSerialNumber, + getAmperageLimitationUnitDivider, + getBootConnectorStatus, + getChargingStationConnectorChargingProfilesPowerLimit, + getChargingStationId, + getDefaultVoltageOut, + getHashId, + getIdTagsFile, + getMaxNumberOfEvses, + getNumberOfReservableConnectors, + getPhaseRotationValue, + hasFeatureProfile, + hasReservationExpired, + initializeConnectorsMapStatus, + propagateSerialNumber, + setChargingStationOptions, + stationTemplateToStationInfo, + warnTemplateKeysDeprecation +} from './Helpers.js' +import { IdTagsCache } from './IdTagsCache.js' +import { + buildMeterValue, + buildTransactionEndMeterValue, + getMessageTypeString, + OCPP16IncomingRequestService, + OCPP16RequestService, + OCPP16ResponseService, + OCPP20IncomingRequestService, + OCPP20RequestService, + OCPP20ResponseService, + type OCPPIncomingRequestService, + type OCPPRequestService, + sendAndSetConnectorStatus +} from './ocpp/index.js' +import { SharedLRUCache } from './SharedLRUCache.js' export class ChargingStation extends EventEmitter { public readonly index: number @@ -230,7 +228,7 @@ export class ChargingStation extends EventEmitter { this.wsConnectionRetried ? true : this.getAutomaticTransactionGeneratorConfiguration()?.stopAbsoluteDuration - ).catch(error => { + ).catch((error: unknown) => { logger.error(`${this.logPrefix()} Error while starting the message sequence:`, error) }) this.wsConnectionRetried = false @@ -263,14 +261,17 @@ export class ChargingStation extends EventEmitter { } public get wsConnectionUrl (): URL { + const wsConnectionBaseUrlStr = `${ + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value + : this.configuredSupervisionUrl.href + }` return new URL( - `${ - this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) - ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value - : this.configuredSupervisionUrl.href - }/${this.stationInfo?.chargingStationId}` + `${wsConnectionBaseUrlStr}${ + !wsConnectionBaseUrlStr.endsWith('/') ? '/' : '' + }${this.stationInfo?.chargingStationId}` ) } @@ -288,7 +289,7 @@ export class ChargingStation extends EventEmitter { readFileSync(this.templateFile, 'utf8') ) as ChargingStationTemplate } catch { - stationTemplate = undefined + // Ignore } return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`) } @@ -412,14 +413,17 @@ export class ChargingStation extends EventEmitter { const connectorChargingProfilesPowerLimit = getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId) return min( - isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, + isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion isNaN(connectorAmperageLimitationPowerLimit!) - ? Infinity + ? Number.POSITIVE_INFINITY : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion connectorAmperageLimitationPowerLimit!, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - isNaN(connectorChargingProfilesPowerLimit!) ? Infinity : connectorChargingProfilesPowerLimit! + isNaN(connectorChargingProfilesPowerLimit!) + ? Number.POSITIVE_INFINITY + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorChargingProfilesPowerLimit! ) } @@ -554,7 +558,7 @@ export class ChargingStation extends EventEmitter { this.heartbeatSetInterval = setInterval(() => { this.ocppRequestService .requestHandler(this, RequestCommand.HEARTBEAT) - .catch(error => { + .catch((error: unknown) => { logger.error( `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`, error @@ -639,7 +643,7 @@ export class ChargingStation extends EventEmitter { meterValue: [meterValue] } ) - .catch(error => { + .catch((error: unknown) => { logger.error( `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`, error @@ -843,7 +847,7 @@ export class ChargingStation extends EventEmitter { this.wsConnection.on('close', this.onClose.bind(this)) // Handle WebSocket open this.wsConnection.on('open', () => { - this.onOpen().catch(error => + this.onOpen().catch((error: unknown) => logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error) ) }) @@ -945,7 +949,7 @@ export class ChargingStation extends EventEmitter { ) } return await this.ocppRequestService.requestHandler< - StopTransactionRequest, + Partial, StopTransactionResponse >(this, RequestCommand.STOP_TRANSACTION, { transactionId, @@ -1156,7 +1160,7 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const stationTemplate = this.getTemplateFromFile()! checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) - const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this) + const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation) warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile) if (stationTemplate.Connectors != null) { checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) @@ -1193,15 +1197,6 @@ export class ChargingStation extends EventEmitter { } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` ) } - stationInfo.firmwareUpgrade = mergeDeepRight( - { - versionUpgrade: { - step: 1 - }, - reset: true - }, - stationTemplate.firmwareUpgrade ?? {} - ) if (stationTemplate.resetTime != null) { stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime) } @@ -1238,6 +1233,7 @@ export class ChargingStation extends EventEmitter { const stationInfoFromFile = this.getStationInfoFromFile( stationInfoFromTemplate.stationInfoPersistentConfiguration ) + let stationInfo: ChargingStationInfo // Priority: // 1. charging station info from template // 2. charging station info from configuration file @@ -1245,19 +1241,14 @@ export class ChargingStation extends EventEmitter { stationInfoFromFile != null && stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash ) { - return setChargingStationOptions( - { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromFile }, - options - ) + stationInfo = stationInfoFromFile + } else { + stationInfo = stationInfoFromTemplate + stationInfoFromFile != null && + propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo) } - stationInfoFromFile != null && - propagateSerialNumber( - this.getTemplateFromFile(), - stationInfoFromFile, - stationInfoFromTemplate - ) return setChargingStationOptions( - { ...Constants.DEFAULT_STATION_INFO, ...stationInfoFromTemplate }, + mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo), options ) } @@ -1373,7 +1364,9 @@ export class ChargingStation extends EventEmitter { addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0') } if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) { - addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { visible: false }) + addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { + visible: false + }) } if ( this.stationInfo?.supervisionUrlOcppConfiguration === true && @@ -1391,7 +1384,9 @@ export class ChargingStation extends EventEmitter { isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null ) { - deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { save: false }) + deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { + save: false + }) } if ( isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && @@ -1555,7 +1550,7 @@ export class ChargingStation extends EventEmitter { } const templateConnectorId = connectorId > 0 && stationTemplate.randomConnectors === true - ? getRandomInteger(templateMaxAvailableConnectors, 1) + ? randomInt(1, templateMaxAvailableConnectors) : connectorId const connectorStatus = stationTemplate.Connectors[templateConnectorId] checkStationInfoConnectorStatus( @@ -1750,7 +1745,9 @@ export class ChargingStation extends EventEmitter { ...(this.connectors.size > 0 && { connectorsStatus: configurationData.connectorsStatus }), - ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }) + ...(this.evses.size > 0 && { + evsesStatus: configurationData.evsesStatus + }) } satisfies ChargingStationConfiguration) ) .digest('hex') @@ -1768,7 +1765,7 @@ export class ChargingStation extends EventEmitter { this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) this.sharedLRUCache.setChargingStationConfiguration(configurationData) this.configurationFileHash = configurationHash - }).catch(error => { + }).catch((error: unknown) => { handleFileException( this.configurationFile, FileType.ChargingStationConfiguration, @@ -1827,7 +1824,9 @@ export class ChargingStation extends EventEmitter { if (this.isWebSocketConnectionOpened()) { this.emit(ChargingStationEvents.updated) logger.info( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} succeeded` + `${this.logPrefix()} Connection to OCPP server through ${ + this.wsConnectionUrl.href + } succeeded` ) let registrationRetryCount = 0 if (!this.isRegistered()) { @@ -1912,7 +1911,9 @@ export class ChargingStation extends EventEmitter { .then(() => { this.emit(ChargingStationEvents.updated) }) - .catch(error => logger.error(`${this.logPrefix()} Error while reconnecting:`, error)) + .catch((error: unknown) => + logger.error(`${this.logPrefix()} Error while reconnecting:`, error) + ) break } } @@ -2084,7 +2085,9 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-base-to-string }' message '${data.toString()}'${ this.requests.has(messageId) - ? ` matching cached request '${JSON.stringify(this.getCachedRequest(messageType, messageId))}'` + ? ` matching cached request '${JSON.stringify( + this.getCachedRequest(messageType, messageId) + )}'` : '' } processing error:`, error @@ -2234,21 +2237,28 @@ export class ChargingStation extends EventEmitter { for (const [evseId, evseStatus] of this.evses) { if (evseId > 0) { for (const [connectorId, connectorStatus] of evseStatus.connectors) { - const connectorBootStatus = getBootConnectorStatus(this, connectorId, connectorStatus) - await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus, evseId) + await sendAndSetConnectorStatus( + this, + connectorId, + getBootConnectorStatus(this, connectorId, connectorStatus), + evseId + ) } } } } else { for (const connectorId of this.connectors.keys()) { if (connectorId > 0) { - const connectorBootStatus = getBootConnectorStatus( + await sendAndSetConnectorStatus( this, connectorId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(connectorId)! + getBootConnectorStatus( + this, + connectorId, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getConnectorStatus(connectorId)! + ) ) - await sendAndSetConnectorStatus(this, connectorId, connectorBootStatus) } } } @@ -2365,11 +2375,11 @@ export class ChargingStation extends EventEmitter { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion Configuration.getSupervisionUrlDistribution()! ) && - logger.error( + logger.warn( // eslint-disable-next-line @typescript-eslint/no-base-to-string - `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${ + `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${ SupervisionUrlDistribution.CHARGING_STATION_AFFINITY - }` + }'` ) configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length break