refactor: cleanup eslint configuration
[e-mobility-charging-stations-simulator.git] / src / charging-station / Helpers.ts
index 9a5eeb6ea97822f9946d36b672ea82bc1770d51b..726a0ab3a23f4fabbd5291aca49cc7ebc059c8b6 100644 (file)
@@ -1,18 +1,18 @@
 import { createHash, randomBytes } from 'node:crypto'
 import type { EventEmitter } from 'node:events'
-import { basename, dirname, join } from 'node:path'
+import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
 import { env } from 'node:process'
 import { fileURLToPath } from 'node:url'
 
 import chalk from 'chalk'
 import {
-  type Interval,
   addDays,
   addSeconds,
   addWeeks,
   differenceInDays,
   differenceInSeconds,
   differenceInWeeks,
+  type Interval,
   isAfter,
   isBefore,
   isDate,
@@ -22,8 +22,6 @@ import {
 } from 'date-fns'
 import { maxTime } from 'date-fns/constants'
 
-import type { ChargingStation } from './ChargingStation.js'
-import { getConfigurationKey } from './ConfigurationKeyUtils.js'
 import { BaseError } from '../exception/index.js'
 import {
   AmpereUnits,
@@ -36,6 +34,7 @@ import {
   type ChargingSchedulePeriod,
   type ChargingStationConfiguration,
   type ChargingStationInfo,
+  type ChargingStationOptions,
   type ChargingStationTemplate,
   type ChargingStationWorkerMessageEvents,
   ConnectorPhaseRotation,
@@ -55,23 +54,36 @@ import {
 } from '../types/index.js'
 import {
   ACElectricUtils,
+  clone,
   Constants,
-  DCElectricUtils,
-  cloneObject,
   convertToDate,
   convertToInt,
+  DCElectricUtils,
   isArraySorted,
   isEmptyObject,
   isEmptyString,
   isNotEmptyArray,
   isNotEmptyString,
-  isValidTime,
+  isValidDate,
   logger,
   secureRandom
 } from '../utils/index.js'
+import type { ChargingStation } from './ChargingStation.js'
+import { getConfigurationKey } from './ConfigurationKeyUtils.js'
 
 const moduleName = 'Helpers'
 
+export const buildTemplateName = (templateFile: string): string => {
+  if (isAbsolute(templateFile)) {
+    templateFile = relative(
+      resolve(join(dirname(fileURLToPath(import.meta.url)), 'assets', 'station-templates')),
+      templateFile
+    )
+  }
+  const templateFileParsedPath = parse(templateFile)
+  return join(templateFileParsedPath.dir, templateFileParsedPath.name)
+}
+
 export const getChargingStationId = (
   index: number,
   stationTemplate: ChargingStationTemplate | undefined
@@ -145,16 +157,16 @@ export const getHashId = (index: number, stationTemplate: ChargingStationTemplat
   const chargingStationInfo = {
     chargePointModel: stationTemplate.chargePointModel,
     chargePointVendor: stationTemplate.chargePointVendor,
-    ...(stationTemplate.chargeBoxSerialNumberPrefix !== undefined && {
+    ...(stationTemplate.chargeBoxSerialNumberPrefix != null && {
       chargeBoxSerialNumber: stationTemplate.chargeBoxSerialNumberPrefix
     }),
-    ...(stationTemplate.chargePointSerialNumberPrefix !== undefined && {
+    ...(stationTemplate.chargePointSerialNumberPrefix != null && {
       chargePointSerialNumber: stationTemplate.chargePointSerialNumberPrefix
     }),
-    ...(stationTemplate.meterSerialNumberPrefix !== undefined && {
+    ...(stationTemplate.meterSerialNumberPrefix != null && {
       meterSerialNumber: stationTemplate.meterSerialNumberPrefix
     }),
-    ...(stationTemplate.meterType !== undefined && {
+    ...(stationTemplate.meterType != null && {
       meterType: stationTemplate.meterType
     })
   }
@@ -247,14 +259,6 @@ export const checkTemplate = (
     logger.error(`${logPrefix} ${errorMsg}`)
     throw new BaseError(errorMsg)
   }
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  if (isEmptyObject(stationTemplate.AutomaticTransactionGenerator!)) {
-    stationTemplate.AutomaticTransactionGenerator = Constants.DEFAULT_ATG_CONFIGURATION
-    logger.warn(
-      `${logPrefix} Empty automatic transaction generator configuration from template file ${templateFile}, set to default: %j`,
-      Constants.DEFAULT_ATG_CONFIGURATION
-    )
-  }
   if (stationTemplate.idTagsFile == null || isEmptyString(stationTemplate.idTagsFile)) {
     logger.warn(
       `${logPrefix} Missing id tags file in template file ${templateFile}. That can lead to issues with the Automatic Transaction Generator`
@@ -331,7 +335,7 @@ export const buildConnectorsMap = (
       const connectorStatus = connectors[connector]
       const connectorId = convertToInt(connector)
       checkStationInfoConnectorStatus(connectorId, connectorStatus, logPrefix, templateFile)
-      connectorsMap.set(connectorId, cloneObject<ConnectorStatus>(connectorStatus))
+      connectorsMap.set(connectorId, clone<ConnectorStatus>(connectorStatus))
     }
   } else {
     logger.warn(
@@ -341,6 +345,37 @@ export const buildConnectorsMap = (
   return connectorsMap
 }
 
+export const setChargingStationOptions = (
+  stationInfo: ChargingStationInfo,
+  options?: ChargingStationOptions
+): ChargingStationInfo => {
+  if (options?.supervisionUrls != null) {
+    stationInfo.supervisionUrls = options.supervisionUrls
+  }
+  if (options?.persistentConfiguration != null) {
+    stationInfo.stationInfoPersistentConfiguration = options.persistentConfiguration
+    stationInfo.ocppPersistentConfiguration = options.persistentConfiguration
+    stationInfo.automaticTransactionGeneratorPersistentConfiguration =
+      options.persistentConfiguration
+  }
+  if (options?.autoStart != null) {
+    stationInfo.autoStart = options.autoStart
+  }
+  if (options?.autoRegister != null) {
+    stationInfo.autoRegister = options.autoRegister
+  }
+  if (options?.enableStatistics != null) {
+    stationInfo.enableStatistics = options.enableStatistics
+  }
+  if (options?.ocppStrictCompliance != null) {
+    stationInfo.ocppStrictCompliance = options.ocppStrictCompliance
+  }
+  if (options?.stopTransactionsOnStopped != null) {
+    stationInfo.stopTransactionsOnStopped = options.stopTransactionsOnStopped
+  }
+  return stationInfo
+}
+
 export const initializeConnectorsMapStatus = (
   connectors: Map<number, ConnectorStatus>,
   logPrefix: string
@@ -348,9 +383,9 @@ export const initializeConnectorsMapStatus = (
   for (const connectorId of connectors.keys()) {
     if (connectorId > 0 && connectors.get(connectorId)?.transactionStarted === true) {
       logger.warn(
-        `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${connectors.get(
-          connectorId
-        )?.transactionId}`
+        `${logPrefix} Connector id ${connectorId} at initialization has a transaction started with id ${
+          connectors.get(connectorId)?.transactionId
+        }`
       )
     }
     if (connectorId === 0) {
@@ -367,11 +402,14 @@ export const initializeConnectorsMapStatus = (
   }
 }
 
-export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void => {
+export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefined): void => {
+  if (connectorStatus == null) {
+    return
+  }
   connectorStatus.chargingProfiles =
     connectorStatus.transactionId != null && isNotEmptyArray(connectorStatus.chargingProfiles)
-      ? connectorStatus.chargingProfiles?.filter(
-        (chargingProfile) => chargingProfile.transactionId !== connectorStatus.transactionId
+      ? connectorStatus.chargingProfiles.filter(
+        chargingProfile => chargingProfile.transactionId !== connectorStatus.transactionId
       )
       : []
   connectorStatus.idTagLocalAuthorized = false
@@ -390,29 +428,28 @@ export const resetConnectorStatus = (connectorStatus: ConnectorStatus): void =>
 export const createBootNotificationRequest = (
   stationInfo: ChargingStationInfo,
   bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
-): BootNotificationRequest => {
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  const ocppVersion = stationInfo.ocppVersion!
+): BootNotificationRequest | undefined => {
+  const ocppVersion = stationInfo.ocppVersion
   switch (ocppVersion) {
     case OCPPVersion.VERSION_16:
       return {
         chargePointModel: stationInfo.chargePointModel,
         chargePointVendor: stationInfo.chargePointVendor,
-        ...(stationInfo.chargeBoxSerialNumber !== undefined && {
+        ...(stationInfo.chargeBoxSerialNumber != null && {
           chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber
         }),
-        ...(stationInfo.chargePointSerialNumber !== undefined && {
+        ...(stationInfo.chargePointSerialNumber != null && {
           chargePointSerialNumber: stationInfo.chargePointSerialNumber
         }),
-        ...(stationInfo.firmwareVersion !== undefined && {
+        ...(stationInfo.firmwareVersion != null && {
           firmwareVersion: stationInfo.firmwareVersion
         }),
-        ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
-        ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi }),
-        ...(stationInfo.meterSerialNumber !== undefined && {
+        ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
+        ...(stationInfo.imsi != null && { imsi: stationInfo.imsi }),
+        ...(stationInfo.meterSerialNumber != null && {
           meterSerialNumber: stationInfo.meterSerialNumber
         }),
-        ...(stationInfo.meterType !== undefined && {
+        ...(stationInfo.meterType != null && {
           meterType: stationInfo.meterType
         })
       } satisfies OCPP16BootNotificationRequest
@@ -423,16 +460,16 @@ export const createBootNotificationRequest = (
         chargingStation: {
           model: stationInfo.chargePointModel,
           vendorName: stationInfo.chargePointVendor,
-          ...(stationInfo.firmwareVersion !== undefined && {
+          ...(stationInfo.firmwareVersion != null && {
             firmwareVersion: stationInfo.firmwareVersion
           }),
-          ...(stationInfo.chargeBoxSerialNumber !== undefined && {
+          ...(stationInfo.chargeBoxSerialNumber != null && {
             serialNumber: stationInfo.chargeBoxSerialNumber
           }),
-          ...((stationInfo.iccid !== undefined || stationInfo.imsi !== undefined) && {
+          ...((stationInfo.iccid != null || stationInfo.imsi != null) && {
             modem: {
-              ...(stationInfo.iccid !== undefined && { iccid: stationInfo.iccid }),
-              ...(stationInfo.imsi !== undefined && { imsi: stationInfo.imsi })
+              ...(stationInfo.iccid != null && { iccid: stationInfo.iccid }),
+              ...(stationInfo.imsi != null && { imsi: stationInfo.imsi })
             }
           })
         }
@@ -457,7 +494,7 @@ export const warnTemplateKeysDeprecation = (
       templateKey.deprecatedKey,
       logPrefix,
       templateFile,
-      templateKey.key !== undefined ? `Use '${templateKey.key}' instead` : undefined
+      templateKey.key != null ? `Use '${templateKey.key}' instead` : undefined
     )
     convertDeprecatedTemplateKey(stationTemplate, templateKey.deprecatedKey, templateKey.key)
   }
@@ -466,13 +503,14 @@ export const warnTemplateKeysDeprecation = (
 export const stationTemplateToStationInfo = (
   stationTemplate: ChargingStationTemplate
 ): ChargingStationInfo => {
-  stationTemplate = cloneObject<ChargingStationTemplate>(stationTemplate)
+  stationTemplate = clone<ChargingStationTemplate>(stationTemplate)
   delete stationTemplate.power
   delete stationTemplate.powerUnit
   delete stationTemplate.Connectors
   delete stationTemplate.Evses
   delete stationTemplate.Configuration
   delete stationTemplate.AutomaticTransactionGenerator
+  delete stationTemplate.numberOfConnectors
   delete stationTemplate.chargeBoxSerialNumberPrefix
   delete stationTemplate.chargePointSerialNumberPrefix
   delete stationTemplate.meterSerialNumberPrefix
@@ -564,7 +602,7 @@ export const getConnectorChargingProfiles = (
   chargingStation: ChargingStation,
   connectorId: number
 ): ChargingProfile[] => {
-  return cloneObject<ChargingProfile[]>(
+  return clone<ChargingProfile[]>(
     (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles ?? [])
       .sort((a, b) => b.stackLevel - a.stackLevel)
       .concat(
@@ -660,7 +698,7 @@ export const waitChargingStationEvents = async (
   event: ChargingStationWorkerMessageEvents,
   eventsToWait: number
 ): Promise<number> => {
-  return await new Promise<number>((resolve) => {
+  return await new Promise<number>(resolve => {
     let events = 0
     if (eventsToWait === 0) {
       resolve(events)
@@ -678,11 +716,11 @@ export const waitChargingStationEvents = async (
 const getConfiguredMaxNumberOfConnectors = (stationTemplate: ChargingStationTemplate): number => {
   let configuredMaxNumberOfConnectors = 0
   if (isNotEmptyArray(stationTemplate.numberOfConnectors)) {
-    const numberOfConnectors = stationTemplate.numberOfConnectors as number[]
+    const numberOfConnectors = stationTemplate.numberOfConnectors
     configuredMaxNumberOfConnectors =
       numberOfConnectors[Math.floor(secureRandom() * numberOfConnectors.length)]
   } else if (stationTemplate.numberOfConnectors != null) {
-    configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors as number
+    configuredMaxNumberOfConnectors = stationTemplate.numberOfConnectors
   } else if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
     configuredMaxNumberOfConnectors =
       // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -750,7 +788,7 @@ const warnDeprecatedTemplateKey = (
   templateFile: string,
   logMsgToAppend = ''
 ): void => {
-  if (template[key as keyof ChargingStationTemplate] !== undefined) {
+  if (template[key as keyof ChargingStationTemplate] != null) {
     const logMsg = `Deprecated template key '${key}' usage in file '${templateFile}'${
       isNotEmptyString(logMsgToAppend) ? `. ${logMsgToAppend}` : ''
     }`
@@ -764,8 +802,8 @@ const convertDeprecatedTemplateKey = (
   deprecatedKey: string,
   key?: string
 ): void => {
-  if (template[deprecatedKey as keyof ChargingStationTemplate] !== undefined) {
-    if (key !== undefined) {
+  if (template[deprecatedKey as keyof ChargingStationTemplate] != null) {
+    if (key != null) {
       (template as unknown as Record<string, unknown>)[key] =
         template[deprecatedKey as keyof ChargingStationTemplate]
     }
@@ -796,25 +834,24 @@ const getLimitFromChargingProfiles = (
 ): ChargingProfilesLimit | undefined => {
   const debugLogMsg = `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Matching charging profile found for power limitation: %j`
   const currentDate = new Date()
-  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
+  const connectorStatus = chargingStation.getConnectorStatus(connectorId)
   for (const chargingProfile of chargingProfiles) {
     const chargingSchedule = chargingProfile.chargingSchedule
-    if (chargingSchedule.startSchedule == null && connectorStatus.transactionStarted === true) {
+    if (chargingSchedule.startSchedule == null) {
       logger.debug(
         `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no startSchedule defined. Trying to set it to the connector current transaction start date`
       )
       // OCPP specifies that if startSchedule is not defined, it should be relative to start of the connector transaction
-      chargingSchedule.startSchedule = connectorStatus.transactionStart
+      chargingSchedule.startSchedule = connectorStatus?.transactionStart
     }
-    if (chargingSchedule.startSchedule != null && !isDate(chargingSchedule.startSchedule)) {
+    if (!isDate(chargingSchedule.startSchedule)) {
       logger.warn(
         `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} startSchedule property is not a Date instance. Trying to convert it to a Date instance`
       )
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       chargingSchedule.startSchedule = convertToDate(chargingSchedule.startSchedule)!
     }
-    if (chargingSchedule.startSchedule != null && chargingSchedule.duration == null) {
+    if (chargingSchedule.duration == null) {
       logger.debug(
         `${logPrefix} ${moduleName}.getLimitFromChargingProfiles: Charging profile id ${chargingProfile.chargingProfileId} has no duration defined and will be set to the maximum time allowed`
       )
@@ -830,10 +867,8 @@ const getLimitFromChargingProfiles = (
     // Check if the charging profile is active
     if (
       isWithinInterval(currentDate, {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        start: chargingSchedule.startSchedule!,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!)
+        start: chargingSchedule.startSchedule,
+        end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration)
       })
     ) {
       if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
@@ -877,8 +912,7 @@ const getLimitFromChargingProfiles = (
           // Find the right schedule period
           if (
             isAfter(
-              // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-              addSeconds(chargingSchedule.startSchedule!, chargingSchedulePeriod.startPeriod),
+              addSeconds(chargingSchedule.startSchedule, chargingSchedulePeriod.startPeriod),
               currentDate
             )
           ) {
@@ -899,14 +933,11 @@ const getLimitFromChargingProfiles = (
             (index < chargingSchedule.chargingSchedulePeriod.length - 1 &&
               differenceInSeconds(
                 addSeconds(
-                  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                  chargingSchedule.startSchedule!,
+                  chargingSchedule.startSchedule,
                   chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod
                 ),
-                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-                chargingSchedule.startSchedule!
-                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-              ) > chargingSchedule.duration!)
+                chargingSchedule.startSchedule
+              ) > chargingSchedule.duration)
           ) {
             const result: ChargingProfilesLimit = {
               limit: previousChargingSchedulePeriod.limit,
@@ -922,9 +953,9 @@ const getLimitFromChargingProfiles = (
 }
 
 export const prepareChargingProfileKind = (
-  connectorStatus: ConnectorStatus,
+  connectorStatus: ConnectorStatus | undefined,
   chargingProfile: ChargingProfile,
-  currentDate: Date,
+  currentDate: string | number | Date,
   logPrefix: string
 ): boolean => {
   switch (chargingProfile.chargingProfileKind) {
@@ -941,7 +972,7 @@ export const prepareChargingProfileKind = (
         )
         delete chargingProfile.chargingSchedule.startSchedule
       }
-      if (connectorStatus.transactionStarted === true) {
+      if (connectorStatus?.transactionStarted === true) {
         chargingProfile.chargingSchedule.startSchedule = connectorStatus.transactionStart
       }
       // FIXME: Handle relative charging profile duration
@@ -952,19 +983,19 @@ export const prepareChargingProfileKind = (
 
 export const canProceedChargingProfile = (
   chargingProfile: ChargingProfile,
-  currentDate: Date,
+  currentDate: string | number | Date,
   logPrefix: string
 ): boolean => {
   if (
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    (isValidTime(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom!)) ||
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    (isValidTime(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo!))
+    (isValidDate(chargingProfile.validFrom) && isBefore(currentDate, chargingProfile.validFrom)) ||
+    (isValidDate(chargingProfile.validTo) && isAfter(currentDate, chargingProfile.validTo))
   ) {
     logger.debug(
       `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${
         chargingProfile.chargingProfileId
-      } is not valid for the current date ${currentDate.toISOString()}`
+      } is not valid for the current date ${
+        isDate(currentDate) ? currentDate.toISOString() : currentDate
+      }`
     )
     return false
   }
@@ -977,7 +1008,7 @@ export const canProceedChargingProfile = (
     )
     return false
   }
-  if (!isValidTime(chargingProfile.chargingSchedule.startSchedule)) {
+  if (!isValidDate(chargingProfile.chargingSchedule.startSchedule)) {
     logger.error(
       `${logPrefix} ${moduleName}.canProceedChargingProfile: Charging profile id ${chargingProfile.chargingProfileId} has an invalid startSchedule date defined`
     )
@@ -1026,12 +1057,12 @@ const canProceedRecurringChargingProfile = (
  */
 const prepareRecurringChargingProfile = (
   chargingProfile: ChargingProfile,
-  currentDate: Date,
+  currentDate: string | number | Date,
   logPrefix: string
 ): boolean => {
   const chargingSchedule = chargingProfile.chargingSchedule
   let recurringIntervalTranslated = false
-  let recurringInterval: Interval
+  let recurringInterval: Interval | undefined
   switch (chargingProfile.recurrencyKind) {
     case RecurrencyKindType.DAILY:
       recurringInterval = {
@@ -1090,12 +1121,12 @@ const prepareRecurringChargingProfile = (
       `${logPrefix} ${moduleName}.prepareRecurringChargingProfile: Recurring ${
         chargingProfile.recurrencyKind
       } charging profile id ${chargingProfile.chargingProfileId} recurrency time interval [${toDate(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        recurringInterval!.start
+        recurringInterval?.start as Date
       ).toISOString()}, ${toDate(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        recurringInterval!.end
-      ).toISOString()}] has not been properly translated to current date ${currentDate.toISOString()} `
+        recurringInterval?.end as Date
+      ).toISOString()}] has not been properly translated to current date ${
+        isDate(currentDate) ? currentDate.toISOString() : currentDate
+      } `
     )
   }
   return recurringIntervalTranslated