Add amperage limitation support via vendor specific OCPP key
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 18 Mar 2022 15:01:11 +0000 (16:01 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 18 Mar 2022 15:01:11 +0000 (16:01 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
README.md
src/assets/station-templates/abb-atg.station-template.json
src/assets/station-templates/evlink.station-template.json
src/assets/station-templates/keba.station-template.json
src/assets/station-templates/schneider-imredd.station-template.json
src/assets/station-templates/schneider.station-template.json
src/charging-station/ChargingStation.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/types/ChargingStationInfo.ts
src/types/ChargingStationTemplate.ts

index 9e9b07761341ad38a29626c7e282d5c402105bce..978a2bb1b851b9e79edbc23f50ed5f50ce335c28 100644 (file)
--- 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                                                                                                                                     |
index 9579ef59e324d548cea775f51862477167a6ee38..f4c3c52b45ac4fb1e0ea0e79c2d8f8145c845f90 100644 (file)
@@ -1,7 +1,6 @@
 {
   "authorizationFile": "authorization-tags.json",
   "baseName": "CS-ABB",
-  "nameSuffix": "-Roaming",
   "chargePointModel": "MD_TERRA_53",
   "chargePointVendor": "ABB",
   "firmwareVersion": "4.0.4.22",
index db3eede44a05ee58e23354d66aef0c1feb79a489..b96f1ae08c5b1b62d6665e1e9bf90a2eac788d1d 100644 (file)
@@ -13,6 +13,7 @@
   "numberOfConnectors": 1,
   "randomConnectors": false,
   "voltageOut": 231,
+  "amperageLimitationOcppKey": "maxintensitysocket",
   "Configuration": {
     "configurationKey": [
       {
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
-      },
-      {
-        "key": "maxintensitysocket",
-        "readonly": false,
-        "value": "32"
       }
     ]
   },
index c1ee792bdee42d57e636291137393d9fa454d1db..3bc060b59637989682a622e7b7e6f7d587ec596d 100644 (file)
@@ -8,6 +8,8 @@
   "powerUnit": "W",
   "numberOfConnectors": 1,
   "randomConnectors": false,
+  "amperageLimitationOcppKey": "MaxAvailableCurrent",
+  "amperageLimitationUnit": "mA",
   "Configuration": {
     "configurationKey": [
       {
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
-      },
-      {
-        "key": "MaxAvailableCurrent",
-        "readonly": false,
-        "value": "32000"
       }
     ]
   },
index a374a53cfb4c6d62441fa03c818b759d2f65902d..34bba08f2aa9ec4ac8265fff575017a3c6d04841 100644 (file)
@@ -12,6 +12,7 @@
   "numberOfConnectors": 1,
   "randomConnectors": false,
   "voltageOut": 231,
+  "amperageLimitationOcppKey": "maxintensitysocket",
   "Configuration": {
     "configurationKey": [
       {
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
-      },
-      {
-        "key": "maxintensitysocket",
-        "readonly": false,
-        "value": "32"
       }
     ]
   },
index 4c7fcc895a2d4d15659fd8628a92f69a855387d6..17e205181e9d368c8db182f95ada5d4f20c621b1 100644 (file)
@@ -12,6 +12,7 @@
   "numberOfConnectors": 2,
   "randomConnectors": false,
   "voltageOut": 231,
+  "amperageLimitationOcppKey": "maxintensitysocket",
   "Configuration": {
     "configurationKey": [
       {
         "key": "WebSocketPingInterval",
         "readonly": false,
         "value": "60"
-      },
-      {
-        "key": "maxintensitysocket",
-        "readonly": false,
-        "value": "32"
       }
     ]
   },
index 118805fefa4f72f2277e638d09842db1b618e611..5f5de2dc7b5b84334aaf2dfe0fe021cf9872ecc4 100644 (file)
@@ -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<void> {
     if (this.stationInfo.autoRegister) {
       await this.ocppRequestService.sendMessageHandler<BootNotificationResponse>(
index 1fb8e750530d248aaa71b95317c047f52531f4dc..23d8a014472d0bc2f5bd3726d9f215db163a397d 100644 (file)
@@ -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`
index cc7d1e3bac9197e2332ddc302b9eef3584772142..0e8b172217a9379ef7d41737849b85c372e7e318 100644 (file)
@@ -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 {
index 65a7ad6fdd1a356c93df055f7abbedcea10ed687..a9d42724aad0f9df2fbb0754a3b013c2cf0b5049 100644 (file)
@@ -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;