Merge pull request #574 from JulianHBuecher/reservation-feature
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16ServiceUtils.ts
index de9c8548926477a3c73f8de0bd3c5c0bced4aa7c..c73c32dc2ce940f9c017550183a44dd45a3a7a43 100644 (file)
@@ -1,43 +1,35 @@
 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
 
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-
 import type { JSONSchemaType } from 'ajv';
 
-import OCPPError from '../../../exception/OCPPError';
-import { CurrentType, Voltage } from '../../../types/ChargingStationTemplate';
-import type { JsonType } from '../../../types/JsonType';
-import type {
-  MeasurandPerPhaseSampledValueTemplates,
-  SampledValueTemplate,
-} from '../../../types/MeasurandPerPhaseSampledValueTemplates';
-import type { MeasurandValues } from '../../../types/MeasurandValues';
-import type { OCPP16ChargingProfile } from '../../../types/ocpp/1.6/ChargingProfile';
-import {
-  OCPP16StandardParametersKey,
-  OCPP16SupportedFeatureProfiles,
-} from '../../../types/ocpp/1.6/Configuration';
+import { type ChargingStation, ChargingStationUtils } from '../../../charging-station';
+import { OCPPError } from '../../../exception';
 import {
+  CurrentType,
+  ErrorType,
+  type JsonType,
+  type MeasurandPerPhaseSampledValueTemplates,
+  type MeasurandValues,
   MeterValueContext,
   MeterValueLocation,
   MeterValueUnit,
+  OCPP16AuthorizationStatus,
+  type OCPP16AuthorizeRequest,
+  type OCPP16AuthorizeResponse,
+  type OCPP16ChargingProfile,
+  type OCPP16IncomingRequestCommand,
   type OCPP16MeterValue,
   OCPP16MeterValueMeasurand,
   OCPP16MeterValuePhase,
-  type OCPP16SampledValue,
-} from '../../../types/ocpp/1.6/MeterValues';
-import {
-  type OCPP16IncomingRequestCommand,
   OCPP16RequestCommand,
-} from '../../../types/ocpp/1.6/Requests';
-import { ErrorType } from '../../../types/ocpp/ErrorType';
-import { OCPPVersion } from '../../../types/ocpp/OCPPVersion';
-import Constants from '../../../utils/Constants';
-import { ACElectricUtils, DCElectricUtils } from '../../../utils/ElectricUtils';
-import logger from '../../../utils/Logger';
-import Utils from '../../../utils/Utils';
-import type ChargingStation from '../../ChargingStation';
+  type OCPP16SampledValue,
+  OCPP16StandardParametersKey,
+  type OCPP16SupportedFeatureProfiles,
+  OCPPVersion,
+  type SampledValueTemplate,
+  Voltage,
+} from '../../../types';
+import { ACElectricUtils, Constants, DCElectricUtils, Utils, logger } from '../../../utils';
 import { OCPPServiceUtils } from '../OCPPServiceUtils';
 
 export class OCPP16ServiceUtils extends OCPPServiceUtils {
@@ -76,24 +68,32 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       OCPP16MeterValueMeasurand.STATE_OF_CHARGE
     );
     if (socSampledValueTemplate) {
+      const socMaximumValue = 100;
+      const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0;
       const socSampledValueTemplateValue = socSampledValueTemplate.value
         ? Utils.getRandomFloatFluctuatedRounded(
             parseInt(socSampledValueTemplate.value),
             socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
           )
-        : Utils.getRandomInteger(100);
+        : Utils.getRandomInteger(socMaximumValue, socMinimumValue);
       meterValue.sampledValue.push(
         OCPP16ServiceUtils.buildSampledValue(socSampledValueTemplate, socSampledValueTemplateValue)
       );
       const sampledValuesIndex = meterValue.sampledValue.length - 1;
-      if (Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > 100 || debug) {
+      if (
+        Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) > socMaximumValue ||
+        Utils.convertToInt(meterValue.sampledValue[sampledValuesIndex].value) < socMinimumValue ||
+        debug
+      ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${socMinimumValue}/${
             meterValue.sampledValue[sampledValuesIndex].value
-          }/100`
+          }/${socMaximumValue}}`
         );
       }
     }
@@ -249,6 +249,10 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       const connectorMaximumPowerPerPhase = Math.round(
         connectorMaximumAvailablePower / chargingStation.getNumberOfPhases()
       );
+      const connectorMinimumPower = Math.round(powerSampledValueTemplate.minimumValue) ?? 0;
+      const connectorMinimumPowerPerPhase = Math.round(
+        connectorMinimumPower / chargingStation.getNumberOfPhases()
+      );
       switch (chargingStation.getCurrentOutType()) {
         case CurrentType.AC:
           if (chargingStation.getNumberOfPhases() === 3) {
@@ -299,15 +303,24 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
             powerMeasurandValues.L1 =
               phase1FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
             powerMeasurandValues.L2 =
               phase2FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
             powerMeasurandValues.L3 =
               phase3FluctuatedValue ??
               defaultFluctuatedPowerPerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumPowerPerPhase / unitDivider);
+              Utils.getRandomFloatRounded(
+                connectorMaximumPowerPerPhase / unitDivider,
+                connectorMinimumPowerPerPhase / unitDivider
+              );
           } else {
             powerMeasurandValues.L1 = powerSampledValueTemplate.value
               ? Utils.getRandomFloatFluctuatedRounded(
@@ -319,7 +332,10 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   powerSampledValueTemplate.fluctuationPercent ??
                     Constants.DEFAULT_FLUCTUATION_PERCENT
                 )
-              : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
+              : Utils.getRandomFloatRounded(
+                  connectorMaximumPower / unitDivider,
+                  connectorMinimumPower / unitDivider
+                );
             powerMeasurandValues.L2 = 0;
             powerMeasurandValues.L3 = 0;
           }
@@ -339,7 +355,10 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                 powerSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               )
-            : Utils.getRandomFloatRounded(connectorMaximumPower / unitDivider);
+            : Utils.getRandomFloatRounded(
+                connectorMaximumPower / unitDivider,
+                connectorMinimumPower / unitDivider
+              );
           break;
         default:
           logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
@@ -353,16 +372,21 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       );
       const sampledValuesIndex = meterValue.sampledValue.length - 1;
       const connectorMaximumPowerRounded = Utils.roundTo(connectorMaximumPower / unitDivider, 2);
+      const connectorMinimumPowerRounded = Utils.roundTo(connectorMinimumPower / unitDivider, 2);
       if (
         Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
           connectorMaximumPowerRounded ||
+        Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
+          connectorMinimumPowerRounded ||
         debug
       ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${connectorMinimumPowerRounded}/${
             meterValue.sampledValue[sampledValuesIndex].value
           }/${connectorMaximumPowerRounded}`
         );
@@ -387,9 +411,15 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
           connectorMaximumPowerPerPhase / unitDivider,
           2
         );
+        const connectorMinimumPowerPerPhaseRounded = Utils.roundTo(
+          connectorMinimumPowerPerPhase / unitDivider,
+          2
+        );
         if (
           Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
             connectorMaximumPowerPerPhaseRounded ||
+          Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+            connectorMinimumPowerPerPhaseRounded ||
           debug
         ) {
           logger.error(
@@ -398,7 +428,9 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
               OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
             }: phase ${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
-            }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
+            }, connector id ${connectorId}, transaction id ${
+              connector?.transactionId
+            }, value: ${connectorMinimumPowerPerPhaseRounded}/${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].value
             }/${connectorMaximumPowerPerPhaseRounded}`
           );
@@ -451,6 +483,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       const currentMeasurandValues: MeasurandValues = {} as MeasurandValues;
       const connectorMaximumAvailablePower =
         chargingStation.getConnectorMaximumAvailablePower(connectorId);
+      const connectorMinimumAmperage = currentSampledValueTemplate.minimumValue ?? 0;
       let connectorMaximumAmperage: number;
       switch (chargingStation.getCurrentOutType()) {
         case CurrentType.AC:
@@ -507,15 +540,15 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
             currentMeasurandValues.L1 =
               phase1FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L2 =
               phase2FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L3 =
               phase3FluctuatedValue ??
               defaultFluctuatedAmperagePerPhase ??
-              Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
           } else {
             currentMeasurandValues.L1 = currentSampledValueTemplate.value
               ? Utils.getRandomFloatFluctuatedRounded(
@@ -527,7 +560,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   currentSampledValueTemplate.fluctuationPercent ??
                     Constants.DEFAULT_FLUCTUATION_PERCENT
                 )
-              : Utils.getRandomFloatRounded(connectorMaximumAmperage);
+              : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
             currentMeasurandValues.L2 = 0;
             currentMeasurandValues.L3 = 0;
           }
@@ -552,7 +585,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                 currentSampledValueTemplate.fluctuationPercent ??
                   Constants.DEFAULT_FLUCTUATION_PERCENT
               )
-            : Utils.getRandomFloatRounded(connectorMaximumAmperage);
+            : Utils.getRandomFloatRounded(connectorMaximumAmperage, connectorMinimumAmperage);
           break;
         default:
           logger.error(`${chargingStation.logPrefix()} ${errMsg}`);
@@ -568,13 +601,17 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       if (
         Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) >
           connectorMaximumAmperage ||
+        Utils.convertToFloat(meterValue.sampledValue[sampledValuesIndex].value) <
+          connectorMinimumAmperage ||
         debug
       ) {
         logger.error(
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
+          }: connector id ${connectorId}, transaction id ${
+            connector?.transactionId
+          }, value: ${connectorMinimumAmperage}/${
             meterValue.sampledValue[sampledValuesIndex].value
           }/${connectorMaximumAmperage}`
         );
@@ -598,6 +635,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
         if (
           Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) >
             connectorMaximumAmperage ||
+          Utils.convertToFloat(meterValue.sampledValue[sampledValuesPerPhaseIndex].value) <
+            connectorMinimumAmperage ||
           debug
         ) {
           logger.error(
@@ -606,7 +645,9 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
               OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
             }: phase ${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].phase
-            }, connectorId ${connectorId}, transaction ${connector?.transactionId}, value: ${
+            }, connector id ${connectorId}, transaction id ${
+              connector?.transactionId
+            }, value: ${connectorMinimumAmperage}/${
               meterValue.sampledValue[sampledValuesPerPhaseIndex].value
             }/${connectorMaximumAmperage}`
           );
@@ -675,7 +716,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
           `${chargingStation.logPrefix()} MeterValues measurand ${
             meterValue.sampledValue[sampledValuesIndex].measurand ??
             OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-          }: connectorId ${connectorId}, transaction ${
+          }: connector id ${connectorId}, transaction id ${
             connector?.transactionId
           }, value: ${energyValueRounded}/${connectorMaximumEnergyRounded}, duration: ${Utils.roundTo(
             interval / (3600 * 1000),
@@ -756,7 +797,7 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       Utils.isNullOrUndefined(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)
     ) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an uninitialized charging profiles array attribute, applying deferred initialization`
       );
       chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
     }
@@ -764,12 +805,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles) === false
     ) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connectorId ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId} with an improper attribute type for the charging profiles array, applying proper type initialization`
       );
       chargingStation.getConnectorStatus(connectorId).chargingProfiles = [];
     }
     let cpReplaced = false;
-    if (!Utils.isEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+    if (Utils.isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
       chargingStation
         .getConnectorStatus(connectorId)
         ?.chargingProfiles?.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
@@ -786,13 +827,43 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp);
   }
 
-  public static parseJsonSchemaFile<T extends JsonType>(relativePath: string): JSONSchemaType<T> {
+  public static parseJsonSchemaFile<T extends JsonType>(
+    relativePath: string,
+    moduleName?: string,
+    methodName?: string
+  ): JSONSchemaType<T> {
     return super.parseJsonSchemaFile<T>(
-      path.resolve(path.dirname(fileURLToPath(import.meta.url)), relativePath),
-      OCPPVersion.VERSION_16
+      relativePath,
+      OCPPVersion.VERSION_16,
+      moduleName,
+      methodName
     );
   }
 
+  public static async isIdTagAuthorized(
+    chargingStation: ChargingStation,
+    connectorId: number,
+    idTag: string,
+    parentIdTag?: string
+  ): Promise<boolean> {
+    let authorized = false;
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId);
+    if (OCPP16ServiceUtils.isIdTagLocalAuthorized(chargingStation, idTag)) {
+      connectorStatus.localAuthorizeIdTag = idTag;
+      connectorStatus.idTagLocalAuthorized = true;
+      authorized = true;
+    } else if (chargingStation.getMustAuthorizeAtRemoteStart() === true) {
+      connectorStatus.authorizeIdTag = idTag;
+      authorized = await OCPP16ServiceUtils.isIdTagRemoteAuthorized(chargingStation, idTag);
+    } else {
+      logger.warn(
+        `${chargingStation.logPrefix()} The charging station configuration expects authorize at
+          remote start transaction but local authorization or authorize isn't enabled`
+      );
+    }
+    return authorized;
+  }
+
   private static buildSampledValue(
     sampledValueTemplate: SampledValueTemplate,
     value: number,
@@ -868,4 +939,30 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
         return MeterValueUnit.VOLT;
     }
   }
+
+  private static isIdTagLocalAuthorized(chargingStation: ChargingStation, idTag: string): boolean {
+    return (
+      chargingStation.getLocalAuthListEnabled() === true &&
+      chargingStation.hasIdTags() === true &&
+      Utils.isNotEmptyString(
+        chargingStation.idTagsCache
+          .getIdTags(ChargingStationUtils.getIdTagsFile(chargingStation.stationInfo))
+          ?.find((tag) => tag === idTag)
+      )
+    );
+  }
+
+  private static async isIdTagRemoteAuthorized(
+    chargingStation: ChargingStation,
+    idTag: string
+  ): Promise<boolean> {
+    const authorizeResponse: OCPP16AuthorizeResponse =
+      await chargingStation.ocppRequestService.requestHandler<
+        OCPP16AuthorizeRequest,
+        OCPP16AuthorizeResponse
+      >(chargingStation, OCPP16RequestCommand.AUTHORIZE, {
+        idTag: idTag,
+      });
+    return authorizeResponse?.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED;
+  }
 }