From cc6e8ab5f669ff49b1d476efffc509be8128bccc Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 18 Mar 2022 16:01:11 +0100 Subject: [PATCH] Add amperage limitation support via vendor specific OCPP key MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- README.md | 2 + .../abb-atg.station-template.json | 1 - .../evlink.station-template.json | 6 +- .../keba.station-template.json | 7 +- .../schneider-imredd.station-template.json | 6 +- .../schneider.station-template.json | 6 +- src/charging-station/ChargingStation.ts | 78 ++++++++++++++++++- .../ocpp/1.6/OCPP16ServiceUtils.ts | 60 +++++++------- src/types/ChargingStationInfo.ts | 3 +- src/types/ChargingStationTemplate.ts | 9 +++ 10 files changed, 123 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 9e9b0776..978a2bb1 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ But the modifications to test have to be done to the files in the build result d | autoReconnectMaxRetries | | -1 (unlimited) | integer | connection retries to the OCPP-J server | | reconnectExponentialDelay | true/false | false | boolean | connection delay retry to the OCPP-J server | | registrationMaxRetries | | -1 (unlimited) | integer | charging stations boot notification retries | +| amperageLimitationOcppKey | | undefined | string | charging stations OCPP parameter key used to set the amperage limit, per phase for each connector on AC and global for DC | +| amperageLimitationUnit | A/cA/dA/mA | A | string | charging stations amperage limit unit | | enableStatistics | true/false | true | boolean | enable charging stations statistics | | mayAuthorizeAtRemoteStart | true/false | true | boolean | always send authorize at remote start transaction when AuthorizeRemoteTxRequests is enabled | | beginEndMeterValues | true/false | false | boolean | enable Transaction.{Begin,End} MeterValues | diff --git a/src/assets/station-templates/abb-atg.station-template.json b/src/assets/station-templates/abb-atg.station-template.json index 9579ef59..f4c3c52b 100644 --- a/src/assets/station-templates/abb-atg.station-template.json +++ b/src/assets/station-templates/abb-atg.station-template.json @@ -1,7 +1,6 @@ { "authorizationFile": "authorization-tags.json", "baseName": "CS-ABB", - "nameSuffix": "-Roaming", "chargePointModel": "MD_TERRA_53", "chargePointVendor": "ABB", "firmwareVersion": "4.0.4.22", diff --git a/src/assets/station-templates/evlink.station-template.json b/src/assets/station-templates/evlink.station-template.json index db3eede4..b96f1ae0 100644 --- a/src/assets/station-templates/evlink.station-template.json +++ b/src/assets/station-templates/evlink.station-template.json @@ -13,6 +13,7 @@ "numberOfConnectors": 1, "randomConnectors": false, "voltageOut": 231, + "amperageLimitationOcppKey": "maxintensitysocket", "Configuration": { "configurationKey": [ { @@ -44,11 +45,6 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" - }, - { - "key": "maxintensitysocket", - "readonly": false, - "value": "32" } ] }, diff --git a/src/assets/station-templates/keba.station-template.json b/src/assets/station-templates/keba.station-template.json index c1ee792b..3bc060b5 100644 --- a/src/assets/station-templates/keba.station-template.json +++ b/src/assets/station-templates/keba.station-template.json @@ -8,6 +8,8 @@ "powerUnit": "W", "numberOfConnectors": 1, "randomConnectors": false, + "amperageLimitationOcppKey": "MaxAvailableCurrent", + "amperageLimitationUnit": "mA", "Configuration": { "configurationKey": [ { @@ -39,11 +41,6 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" - }, - { - "key": "MaxAvailableCurrent", - "readonly": false, - "value": "32000" } ] }, diff --git a/src/assets/station-templates/schneider-imredd.station-template.json b/src/assets/station-templates/schneider-imredd.station-template.json index a374a53c..34bba08f 100644 --- a/src/assets/station-templates/schneider-imredd.station-template.json +++ b/src/assets/station-templates/schneider-imredd.station-template.json @@ -12,6 +12,7 @@ "numberOfConnectors": 1, "randomConnectors": false, "voltageOut": 231, + "amperageLimitationOcppKey": "maxintensitysocket", "Configuration": { "configurationKey": [ { @@ -43,11 +44,6 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" - }, - { - "key": "maxintensitysocket", - "readonly": false, - "value": "32" } ] }, diff --git a/src/assets/station-templates/schneider.station-template.json b/src/assets/station-templates/schneider.station-template.json index 4c7fcc89..17e20518 100644 --- a/src/assets/station-templates/schneider.station-template.json +++ b/src/assets/station-templates/schneider.station-template.json @@ -12,6 +12,7 @@ "numberOfConnectors": 2, "randomConnectors": false, "voltageOut": 231, + "amperageLimitationOcppKey": "maxintensitysocket", "Configuration": { "configurationKey": [ { @@ -43,11 +44,6 @@ "key": "WebSocketPingInterval", "readonly": false, "value": "60" - }, - { - "key": "maxintensitysocket", - "readonly": false, - "value": "32" } ] }, diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 118805fe..5f5de2dc 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -1,5 +1,6 @@ // Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +import { ACElectricUtils, DCElectricUtils } from '../utils/ElectricUtils'; import { AvailabilityType, BootNotificationRequest, @@ -20,6 +21,7 @@ import ChargingStationOcppConfiguration, { ConfigurationKey, } from '../types/ChargingStationOcppConfiguration'; import ChargingStationTemplate, { + AmpereUnits, CurrentType, PowerUnits, Voltage, @@ -229,6 +231,22 @@ export default class ChargingStation { : defaultVoltageOut; } + public getMaximumConfiguredPower(): number | undefined { + let maximumConfiguredPower = + (this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower; + if (this.getAmperageLimitation() < this.stationInfo.maximumAmperage) { + maximumConfiguredPower = + this.getCurrentOutType() === CurrentType.AC + ? ACElectricUtils.powerTotal( + this.getNumberOfPhases(), + this.getVoltageOut(), + this.getAmperageLimitation() + ) + : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation()); + } + return maximumConfiguredPower; + } + public getTransactionIdTag(transactionId: number): string | undefined { for (const connectorId of this.connectors.keys()) { if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) { @@ -835,13 +853,13 @@ export default class ChargingStation { if (!Utils.isEmptyArray(stationInfo.power)) { stationInfo.power = stationInfo.power as number[]; const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationInfo.power.length); - stationInfo.maxPower = + stationInfo.maximumPower = stationInfo.powerUnit === PowerUnits.KILO_WATT ? stationInfo.power[powerArrayRandomIndex] * 1000 : stationInfo.power[powerArrayRandomIndex]; } else { stationInfo.power = stationInfo.power as number; - stationInfo.maxPower = + stationInfo.maximumPower = stationInfo.powerUnit === PowerUnits.KILO_WATT ? stationInfo.power * 1000 : stationInfo.power; @@ -954,7 +972,6 @@ export default class ChargingStation { this.bootNotificationRequest = this.createBootNotificationRequest(this.stationInfo); this.ocppConfiguration = this.getOcppConfiguration(); delete this.stationInfo.Configuration; - this.saveStationInfo(); // Build connectors if needed const maxConnectors = this.getMaxNumberOfConnectors(); if (maxConnectors <= 0) { @@ -1039,6 +1056,9 @@ export default class ChargingStation { } } } + // The connectors attribute need to be initialized + this.stationInfo.maximumAmperage = this.getMaximumAmperage(); + this.saveStationInfo(); // Avoid duplication of connectors related information in RAM delete this.stationInfo.Connectors; // Initialize transaction attributes on connectors @@ -1098,6 +1118,15 @@ export default class ChargingStation { ) { this.deleteConfigurationKey(this.getSupervisionUrlOcppKey(), { save: false }); } + if ( + this.stationInfo.amperageLimitationOcppKey && + !this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey) + ) { + this.addConfigurationKey( + this.stationInfo.amperageLimitationOcppKey, + (this.stationInfo.maximumAmperage * this.getAmperageLimitationUnitDivider()).toString() + ); + } if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) { this.addConfigurationKey( StandardParametersKey.SupportedFeatureProfiles, @@ -1557,6 +1586,49 @@ export default class ChargingStation { return maxConnectors; } + private getMaximumAmperage(): number | undefined { + switch (this.getCurrentOutType()) { + case CurrentType.AC: + return ACElectricUtils.amperagePerPhaseFromPower( + this.getNumberOfPhases(), + (this.stationInfo['maxPower'] as number) ?? + this.stationInfo.maximumPower / this.getNumberOfConnectors(), + this.getVoltageOut() + ); + case CurrentType.DC: + return DCElectricUtils.amperage(this.stationInfo.maximumPower, this.getVoltageOut()); + } + } + + private getAmperageLimitationUnitDivider(): number { + let unitDivider = 1; + switch (this.stationInfo.amperageLimitationUnit) { + case AmpereUnits.DECI_AMPERE: + unitDivider = 10; + break; + case AmpereUnits.CENTI_AMPERE: + unitDivider = 100; + break; + case AmpereUnits.MILLI_AMPERE: + unitDivider = 1000; + break; + } + return unitDivider; + } + + private getAmperageLimitation(): number | undefined { + if ( + this.stationInfo.amperageLimitationOcppKey && + this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey) + ) { + return ( + Utils.convertToInt( + this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey).value + ) / this.getAmperageLimitationUnitDivider() + ); + } + } + private async startMessageSequence(): Promise { if (this.stationInfo.autoRegister) { await this.ocppRequestService.sendMessageHandler( diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 1fb8e750..23d8a014 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -279,11 +279,11 @@ export class OCPP16ServiceUtils { } measurand value`; const powerMeasurandValues = {} as MeasurandValues; const unitDivider = powerSampledValueTemplate?.unit === MeterValueUnit.KILO_WATT ? 1000 : 1; - const maxPower = Math.round( - chargingStation.stationInfo.maxPower / chargingStation.stationInfo.powerDivider + const maximumPower = Math.round( + chargingStation.getMaximumConfiguredPower() / chargingStation.stationInfo.powerDivider ); - const maxPowerPerPhase = Math.round( - chargingStation.stationInfo.maxPower / + const maximumPowerPerPhase = Math.round( + chargingStation.getMaximumConfiguredPower() / chargingStation.stationInfo.powerDivider / chargingStation.getNumberOfPhases() ); @@ -321,15 +321,15 @@ export class OCPP16ServiceUtils { powerMeasurandValues.L1 = phase1FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(maxPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded(maximumPowerPerPhase / unitDivider); powerMeasurandValues.L2 = phase2FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(maxPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded(maximumPowerPerPhase / unitDivider); powerMeasurandValues.L3 = phase3FluctuatedValue ?? defaultFluctuatedPowerPerPhase ?? - Utils.getRandomFloatRounded(maxPowerPerPhase / unitDivider); + Utils.getRandomFloatRounded(maximumPowerPerPhase / unitDivider); } else { powerMeasurandValues.L1 = powerSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( @@ -337,7 +337,7 @@ export class OCPP16ServiceUtils { powerSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(maxPower / unitDivider); + : Utils.getRandomFloatRounded(maximumPower / unitDivider); powerMeasurandValues.L2 = 0; powerMeasurandValues.L3 = 0; } @@ -353,7 +353,7 @@ export class OCPP16ServiceUtils { powerSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(maxPower / unitDivider); + : Utils.getRandomFloatRounded(maximumPower / unitDivider); break; default: logger.error(errMsg); @@ -366,7 +366,7 @@ export class OCPP16ServiceUtils { ) ); const sampledValuesIndex = meterValue.sampledValue.length - 1; - const maxPowerRounded = Utils.roundTo(maxPower / unitDivider, 2); + const maxPowerRounded = Utils.roundTo(maximumPower / unitDivider, 2); if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxPowerRounded || debug @@ -396,7 +396,7 @@ export class OCPP16ServiceUtils { ) ); const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; - const maxPowerPerPhaseRounded = Utils.roundTo(maxPowerPerPhase / unitDivider, 2); + const maxPowerPerPhaseRounded = Utils.roundTo(maximumPowerPerPhase / unitDivider, 2); if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > maxPowerPerPhaseRounded || @@ -455,12 +455,12 @@ export class OCPP16ServiceUtils { OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER } measurand value`; const currentMeasurandValues: MeasurandValues = {} as MeasurandValues; - let maxAmperage: number; + let maximumAmperage: number; switch (chargingStation.getCurrentOutType()) { case CurrentType.AC: - maxAmperage = ACElectricUtils.amperagePerPhaseFromPower( + maximumAmperage = ACElectricUtils.amperagePerPhaseFromPower( chargingStation.getNumberOfPhases(), - chargingStation.stationInfo.maxPower / chargingStation.stationInfo.powerDivider, + chargingStation.getMaximumConfiguredPower() / chargingStation.stationInfo.powerDivider, chargingStation.getVoltageOut() ); if (chargingStation.getNumberOfPhases() === 3) { @@ -495,15 +495,15 @@ export class OCPP16ServiceUtils { currentMeasurandValues.L1 = phase1FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(maxAmperage); + Utils.getRandomFloatRounded(maximumAmperage); currentMeasurandValues.L2 = phase2FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(maxAmperage); + Utils.getRandomFloatRounded(maximumAmperage); currentMeasurandValues.L3 = phase3FluctuatedValue ?? defaultFluctuatedAmperagePerPhase ?? - Utils.getRandomFloatRounded(maxAmperage); + Utils.getRandomFloatRounded(maximumAmperage); } else { currentMeasurandValues.L1 = currentSampledValueTemplate.value ? Utils.getRandomFloatFluctuatedRounded( @@ -511,7 +511,7 @@ export class OCPP16ServiceUtils { currentSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(maxAmperage); + : Utils.getRandomFloatRounded(maximumAmperage); currentMeasurandValues.L2 = 0; currentMeasurandValues.L3 = 0; } @@ -522,8 +522,8 @@ export class OCPP16ServiceUtils { ); break; case CurrentType.DC: - maxAmperage = DCElectricUtils.amperage( - chargingStation.stationInfo.maxPower / chargingStation.stationInfo.powerDivider, + maximumAmperage = DCElectricUtils.amperage( + chargingStation.getMaximumConfiguredPower() / chargingStation.stationInfo.powerDivider, chargingStation.getVoltageOut() ); currentMeasurandValues.allPhases = currentSampledValueTemplate.value @@ -532,7 +532,7 @@ export class OCPP16ServiceUtils { currentSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(maxAmperage); + : Utils.getRandomFloatRounded(maximumAmperage); break; default: logger.error(errMsg); @@ -546,7 +546,7 @@ export class OCPP16ServiceUtils { ); const sampledValuesIndex = meterValue.sampledValue.length - 1; if ( - Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maxAmperage || + Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) > maximumAmperage || debug ) { logger.error( @@ -555,7 +555,7 @@ export class OCPP16ServiceUtils { OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ meterValue.sampledValue[sampledValuesIndex].value - }/${maxAmperage}` + }/${maximumAmperage}` ); } for ( @@ -576,7 +576,7 @@ export class OCPP16ServiceUtils { const sampledValuesPerPhaseIndex = meterValue.sampledValue.length - 1; if ( Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) > - maxAmperage || + maximumAmperage || debug ) { logger.error( @@ -587,7 +587,7 @@ export class OCPP16ServiceUtils { meterValue.sampledValue[sampledValuesPerPhaseIndex].phase }, connectorId ${connectorId}, transaction ${connector.transactionId}, value: ${ meterValue.sampledValue[sampledValuesPerPhaseIndex].value - }/${maxAmperage}` + }/${maximumAmperage}` ); } } @@ -601,8 +601,8 @@ export class OCPP16ServiceUtils { ); const unitDivider = energySampledValueTemplate?.unit === MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1; - const maxEnergyRounded = Utils.roundTo( - ((chargingStation.stationInfo.maxPower / chargingStation.stationInfo.powerDivider) * + const maximumEnergyRounded = Utils.roundTo( + ((chargingStation.getMaximumConfiguredPower() / chargingStation.stationInfo.powerDivider) * interval) / (3600 * 1000), 2 @@ -613,7 +613,7 @@ export class OCPP16ServiceUtils { parseInt(energySampledValueTemplate.value), energySampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) - : Utils.getRandomFloatRounded(maxEnergyRounded); + : Utils.getRandomFloatRounded(maximumEnergyRounded); // Persist previous value on connector if ( connector && @@ -639,14 +639,14 @@ export class OCPP16ServiceUtils { ) ); const sampledValuesIndex = meterValue.sampledValue.length - 1; - if (energyValueRounded > maxEnergyRounded || debug) { + if (energyValueRounded > maximumEnergyRounded || debug) { logger.error( `${chargingStation.logPrefix()} MeterValues measurand ${ meterValue.sampledValue[sampledValuesIndex].measurand ?? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER }: connectorId ${connectorId}, transaction ${ connector.transactionId - }, value: ${energyValueRounded}/${maxEnergyRounded}, duration: ${Utils.roundTo( + }, value: ${energyValueRounded}/${maximumEnergyRounded}, duration: ${Utils.roundTo( interval / (3600 * 1000), 4 )}h` diff --git a/src/types/ChargingStationInfo.ts b/src/types/ChargingStationInfo.ts index cc7d1e3b..0e8b1722 100644 --- a/src/types/ChargingStationInfo.ts +++ b/src/types/ChargingStationInfo.ts @@ -4,8 +4,9 @@ export default interface ChargingStationInfo extends ChargingStationTemplate { chargingStationId?: string; chargeBoxSerialNumber?: string; chargePointSerialNumber?: string; - maxPower?: number; // Always in Watt + maximumPower?: number; // Always in Watt powerDivider?: number; + maximumAmperage?: number; // Always in Ampere } export interface ChargingStationInfoConfiguration { diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index 65a7ad6f..a9d42724 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -15,6 +15,13 @@ export enum PowerUnits { KILO_WATT = 'kW', } +export enum AmpereUnits { + MILLI_AMPERE = 'mA', + CENTI_AMPERE = 'cA', + DECI_AMPERE = 'dA', + AMPERE = 'A', +} + export enum Voltage { VOLTAGE_110 = 110, VOLTAGE_230 = 230, @@ -77,6 +84,8 @@ export default interface ChargingStationTemplate { registrationMaxRetries?: number; enableStatistics?: boolean; mayAuthorizeAtRemoteStart: boolean; + amperageLimitationOcppKey?: string; + amperageLimitationUnit?: AmpereUnits; beginEndMeterValues?: boolean; outOfOrderEndMeterValues?: boolean; meteringPerTransaction?: boolean; -- 2.34.1