X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2FChargingStation.ts;h=547a4f3693321bf271d23cdae07c6cb51df01876;hb=8ca6874c767f7068b101291232cf69b05bbd28c1;hp=84aa81ca649336eda64779744c87600ac5d9a098;hpb=3637ca2c3a39b1185de71fae3577679a7e43891d;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 84aa81ca..547a4f36 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,13 +1,31 @@ // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved. -import crypto from 'crypto'; import fs from 'fs'; +import crypto from 'node:crypto'; import path from 'path'; import { URL } from 'url'; import { parentPort } from 'worker_threads'; +import merge from 'just-merge'; import WebSocket, { type RawData } from 'ws'; +import AuthorizedTagsCache from './AuthorizedTagsCache'; +import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; +import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; +import { ChargingStationUtils } from './ChargingStationUtils'; +import ChargingStationWorkerBroadcastChannel from './ChargingStationWorkerBroadcastChannel'; +import { MessageChannelUtils } from './MessageChannelUtils'; +import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService'; +import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService'; +import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService'; +import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils'; +import OCPP20IncomingRequestService from './ocpp/2.0/OCPP20IncomingRequestService'; +import OCPP20RequestService from './ocpp/2.0/OCPP20RequestService'; +import OCPP20ResponseService from './ocpp/2.0/OCPP20ResponseService'; +import type OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService'; +import type OCPPRequestService from './ocpp/OCPPRequestService'; +import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils'; +import SharedLRUCache from './SharedLRUCache'; import BaseError from '../exception/BaseError'; import OCPPError from '../exception/OCPPError'; import PerformanceStatistics from '../performance/PerformanceStatistics'; @@ -25,15 +43,13 @@ import { SupervisionUrlDistribution } from '../types/ConfigurationData'; import type { ConnectorStatus } from '../types/ConnectorStatus'; import { FileType } from '../types/FileType'; import type { JsonType } from '../types/JsonType'; -import { ChargePointErrorCode } from '../types/ocpp/ChargePointErrorCode'; -import { ChargePointStatus } from '../types/ocpp/ChargePointStatus'; -import { ChargingProfile, ChargingRateUnitType } from '../types/ocpp/ChargingProfile'; import { ConnectorPhaseRotation, StandardParametersKey, SupportedFeatureProfiles, VendorDefaultParametersKey, } from '../types/ocpp/Configuration'; +import { ConnectorStatusEnum } from '../types/ocpp/ConnectorStatusEnum'; import { ErrorType } from '../types/ocpp/ErrorType'; import { MessageType } from '../types/ocpp/MessageType'; import { MeterValue, MeterValueMeasurand } from '../types/ocpp/MeterValues'; @@ -49,6 +65,7 @@ import { type IncomingRequest, IncomingRequestCommand, type MeterValuesRequest, + type OutgoingRequest, RequestCommand, type ResponseCallback, type StatusNotificationRequest, @@ -75,22 +92,6 @@ import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils'; import FileUtils from '../utils/FileUtils'; import logger from '../utils/Logger'; import Utils from '../utils/Utils'; -import AuthorizedTagsCache from './AuthorizedTagsCache'; -import AutomaticTransactionGenerator from './AutomaticTransactionGenerator'; -import { ChargingStationConfigurationUtils } from './ChargingStationConfigurationUtils'; -import { ChargingStationUtils } from './ChargingStationUtils'; -import ChargingStationWorkerBroadcastChannel from './ChargingStationWorkerBroadcastChannel'; -import { MessageChannelUtils } from './MessageChannelUtils'; -import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService'; -import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService'; -import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService'; -import { OCPP16ServiceUtils } from './ocpp/1.6/OCPP16ServiceUtils'; -import OCPP20IncomingRequestService from './ocpp/2.0/OCPP20IncomingRequestService'; -import OCPP20RequestService from './ocpp/2.0/OCPP20RequestService'; -import OCPP20ResponseService from './ocpp/2.0/OCPP20ResponseService'; -import type OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService'; -import type OCPPRequestService from './ocpp/OCPPRequestService'; -import SharedLRUCache from './SharedLRUCache'; export default class ChargingStation { public readonly index: number; @@ -264,6 +265,11 @@ export default class ChargingStation { : defaultVoltageOut; } + public getMaximumPower(stationInfo?: ChargingStationInfo): number { + const localStationInfo = stationInfo ?? this.stationInfo; + return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower; + } + public getConnectorMaximumAvailablePower(connectorId: number): number { let connectorAmperageLimitationPowerLimit: number; if ( @@ -281,13 +287,14 @@ export default class ChargingStation { this.powerDivider; } const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; - const connectorChargingProfilePowerLimit = this.getChargingProfilePowerLimit(connectorId); + const connectorChargingProfilesPowerLimit = + ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId); return Math.min( isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower, isNaN(connectorAmperageLimitationPowerLimit) ? Infinity : connectorAmperageLimitationPowerLimit, - isNaN(connectorChargingProfilePowerLimit) ? Infinity : connectorChargingProfilePowerLimit + isNaN(connectorChargingProfilesPowerLimit) ? Infinity : connectorChargingProfilesPowerLimit ); } @@ -340,16 +347,16 @@ export default class ChargingStation { public getEnergyActiveImportRegisterByTransactionId( transactionId: number, - meterStop = false + rounded = false ): number { return this.getEnergyActiveImportRegister( this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)), - meterStop + rounded ); } - public getEnergyActiveImportRegisterByConnectorId(connectorId: number): number { - return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId)); + public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number { + return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded); } public getAuthorizeRemoteTxRequests(): boolean { @@ -376,12 +383,15 @@ export default class ChargingStation { this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval ) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.heartbeatSetInterval = setInterval(async (): Promise => { - await this.ocppRequestService.requestHandler( - this, - RequestCommand.HEARTBEAT - ); + this.heartbeatSetInterval = setInterval(() => { + this.ocppRequestService + .requestHandler(this, RequestCommand.HEARTBEAT) + .catch((error) => { + logger.error( + `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`, + error + ); + }); }, this.getHeartbeatInterval()); logger.info( this.logPrefix() + @@ -447,18 +457,16 @@ export default class ChargingStation { return; } if (interval > 0) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.getConnectorStatus(connectorId).transactionSetInterval = setInterval( - // eslint-disable-next-line @typescript-eslint/no-misused-promises - async (): Promise => { - // FIXME: Implement OCPP version agnostic helpers - const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue( - this, - connectorId, - this.getConnectorStatus(connectorId).transactionId, - interval - ); - await this.ocppRequestService.requestHandler( + this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => { + // FIXME: Implement OCPP version agnostic helpers + const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue( + this, + connectorId, + this.getConnectorStatus(connectorId).transactionId, + interval + ); + this.ocppRequestService + .requestHandler( this, RequestCommand.METER_VALUES, { @@ -466,10 +474,14 @@ export default class ChargingStation { transactionId: this.getConnectorStatus(connectorId).transactionId, meterValue: [meterValue], } - ); - }, - interval - ); + ) + .catch((error) => { + logger.error( + `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`, + error + ); + }); + }, interval); } else { logger.error( `${this.logPrefix()} Charging station ${ @@ -630,7 +642,7 @@ export default class ChargingStation { if (params?.terminateOpened) { this.terminateWSConnection(); } - const ocppVersion = this.getOcppVersion(); + const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16; let protocol: string; switch (ocppVersion) { case OCPPVersion.VERSION_16: @@ -758,6 +770,12 @@ export default class ChargingStation { this.messageBuffer.forEach((message) => { // TODO: evaluate the need to track performance this.wsConnection.send(message); + const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse; + logger.debug( + `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString( + messageType + )} payload sent: ${message}` + ); this.messageBuffer.delete(message); }); } @@ -832,6 +850,7 @@ export default class ChargingStation { this.index, stationTemplate ); + stationInfo.ocppVersion = stationTemplate.ocppVersion ?? OCPPVersion.VERSION_16; ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo); if (!Utils.isEmptyArray(stationTemplate.power)) { stationTemplate.power = stationTemplate.power as number[]; @@ -859,6 +878,12 @@ export default class ChargingStation { } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` ); } + stationInfo.firmwareUpgrade = merge( + { + reset: true, + }, + stationTemplate.firmwareUpgrade ?? {} + ); stationInfo.resetTime = stationTemplate.resetTime ? stationTemplate.resetTime * 1000 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME; @@ -928,10 +953,6 @@ export default class ChargingStation { } } - private getOcppVersion(): OCPPVersion { - return this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16; - } - private getOcppPersistentConfiguration(): boolean { return this.stationInfo?.ocppPersistentConfiguration ?? true; } @@ -970,7 +991,8 @@ export default class ChargingStation { // OCPP configuration this.ocppConfiguration = this.getOcppConfiguration(); this.initializeOcppConfiguration(); - switch (this.getOcppVersion()) { + const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16; + switch (ocppVersion) { case OCPPVersion.VERSION_16: this.ocppIncomingRequestService = OCPP16IncomingRequestService.getInstance(); @@ -987,7 +1009,7 @@ export default class ChargingStation { ); break; default: - this.handleUnsupportedVersion(this.getOcppVersion()); + this.handleUnsupportedVersion(ocppVersion); break; } if (this.stationInfo?.autoRegister === true) { @@ -1002,11 +1024,17 @@ export default class ChargingStation { this.stationInfo.firmwareVersion && this.stationInfo.firmwareVersionPattern ) { + const versionStep = this.stationInfo.firmwareUpgrade?.versionUpgrade?.step ?? 1; + const patternGroup: number = + this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? + this.stationInfo.firmwareVersion.split('.').length; const match = this.stationInfo.firmwareVersion .match(new RegExp(this.stationInfo.firmwareVersionPattern)) - .slice(1, this.stationInfo.firmwareVersion.split('.').length + 1); + .slice(1, patternGroup + 1); const patchLevelIndex = match.length - 1; - match[patchLevelIndex] = (Utils.convertToInt(match[patchLevelIndex]) + 1).toString(); + match[patchLevelIndex] = ( + Utils.convertToInt(match[patchLevelIndex]) + versionStep + ).toString(); this.stationInfo.firmwareVersion = match.join('.'); } } @@ -1547,7 +1575,7 @@ export default class ChargingStation { logger.debug( `${this.logPrefix()} << Command '${ requestCommandName ?? Constants.UNKNOWN_COMMAND - }' received error payload: ${JSON.stringify(request)}` + }' received error response payload: ${JSON.stringify(request)}` ); errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails)); break; @@ -1621,19 +1649,16 @@ export default class ChargingStation { logger.error(this.logPrefix() + ' WebSocket error:', error); } - private getEnergyActiveImportRegister( - connectorStatus: ConnectorStatus, - meterStop = false - ): number { + private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number { if (this.getMeteringPerTransaction() === true) { return ( - (meterStop === true + (rounded === true ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue) : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0 ); } return ( - (meterStop === true + (rounded === true ? Math.round(connectorStatus?.energyActiveImportRegisterValue) : connectorStatus?.energyActiveImportRegisterValue) ?? 0 ); @@ -1711,11 +1736,6 @@ export default class ChargingStation { return powerDivider; } - private getMaximumPower(stationInfo?: ChargingStationInfo): number { - const localStationInfo = stationInfo ?? this.stationInfo; - return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower; - } - private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined { const maximumPower = this.getMaximumPower(stationInfo); switch (this.getCurrentOutType(stationInfo)) { @@ -1749,57 +1769,6 @@ export default class ChargingStation { } } - private getChargingProfilePowerLimit(connectorId: number): number | undefined { - let limit: number, matchingChargingProfile: ChargingProfile; - let chargingProfiles: ChargingProfile[] = []; - // Get charging profiles for connector and sort by stack level - chargingProfiles = this.getConnectorStatus(connectorId).chargingProfiles.sort( - (a, b) => b.stackLevel - a.stackLevel - ); - // Get profiles on connector 0 - if (this.getConnectorStatus(0).chargingProfiles) { - chargingProfiles.push( - ...this.getConnectorStatus(0).chargingProfiles.sort((a, b) => b.stackLevel - a.stackLevel) - ); - } - if (!Utils.isEmptyArray(chargingProfiles)) { - const result = ChargingStationUtils.getLimitFromChargingProfiles( - chargingProfiles, - this.logPrefix() - ); - if (!Utils.isNullOrUndefined(result)) { - limit = result.limit; - matchingChargingProfile = result.matchingChargingProfile; - switch (this.getCurrentOutType()) { - case CurrentType.AC: - limit = - matchingChargingProfile.chargingSchedule.chargingRateUnit === - ChargingRateUnitType.WATT - ? limit - : ACElectricUtils.powerTotal(this.getNumberOfPhases(), this.getVoltageOut(), limit); - break; - case CurrentType.DC: - limit = - matchingChargingProfile.chargingSchedule.chargingRateUnit === - ChargingRateUnitType.WATT - ? limit - : DCElectricUtils.power(this.getVoltageOut(), limit); - } - const connectorMaximumPower = this.getMaximumPower() / this.powerDivider; - if (limit > connectorMaximumPower) { - logger.error( - `${this.logPrefix()} Charging profile id ${ - matchingChargingProfile.chargingProfileId - } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}, dump charging profiles' stack: %j`, - this.getConnectorStatus(connectorId).chargingProfiles - ); - limit = connectorMaximumPower; - } - } - } - return limit; - } - private async startMessageSequence(): Promise { if (this.stationInfo?.autoRegister === true) { await this.ocppRequestService.requestHandler< @@ -1815,7 +1784,7 @@ export default class ChargingStation { this.startHeartbeat(); // Initialize connectors status for (const connectorId of this.connectors.keys()) { - let chargePointStatus: ChargePointStatus; + let connectorStatus: ConnectorStatusEnum; if (connectorId === 0) { continue; } else if ( @@ -1823,29 +1792,29 @@ export default class ChargingStation { (this.isChargingStationAvailable() === false || this.isConnectorAvailable(connectorId) === false) ) { - chargePointStatus = ChargePointStatus.UNAVAILABLE; + connectorStatus = ConnectorStatusEnum.UNAVAILABLE; } else if ( !this.getConnectorStatus(connectorId)?.status && this.getConnectorStatus(connectorId)?.bootStatus ) { // Set boot status in template at startup - chargePointStatus = this.getConnectorStatus(connectorId).bootStatus; + connectorStatus = this.getConnectorStatus(connectorId).bootStatus; } else if (this.getConnectorStatus(connectorId)?.status) { // Set previous status at startup - chargePointStatus = this.getConnectorStatus(connectorId).status; + connectorStatus = this.getConnectorStatus(connectorId).status; } else { // Set default status - chargePointStatus = ChargePointStatus.AVAILABLE; + connectorStatus = ConnectorStatusEnum.AVAILABLE; } await this.ocppRequestService.requestHandler< StatusNotificationRequest, StatusNotificationResponse - >(this, RequestCommand.STATUS_NOTIFICATION, { - connectorId, - status: chargePointStatus, - errorCode: ChargePointErrorCode.NO_ERROR, - }); - this.getConnectorStatus(connectorId).status = chargePointStatus; + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus) + ); + this.getConnectorStatus(connectorId).status = connectorStatus; } if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { await this.ocppRequestService.requestHandler< @@ -1882,11 +1851,15 @@ export default class ChargingStation { await this.ocppRequestService.requestHandler< StatusNotificationRequest, StatusNotificationResponse - >(this, RequestCommand.STATUS_NOTIFICATION, { - connectorId, - status: ChargePointStatus.UNAVAILABLE, - errorCode: ChargePointErrorCode.NO_ERROR, - }); + >( + this, + RequestCommand.STATUS_NOTIFICATION, + OCPPServiceUtils.buildStatusNotificationRequest( + this, + connectorId, + ConnectorStatusEnum.UNAVAILABLE + ) + ); this.getConnectorStatus(connectorId).status = null; } }