]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor: use utility functions consistently across codebase
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 3 Apr 2026 16:33:14 +0000 (18:33 +0200)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 3 Apr 2026 16:33:14 +0000 (18:33 +0200)
Replace manual patterns with existing utility functions to improve DRY
compliance:

- Use isEmpty/isNotEmptyArray/isNotEmptyString instead of .length checks
- Use convertToInt/convertToFloat instead of Number.parseInt/Number
- Use roundTo instead of Math.round(x * N) / N
- Use ensureError in catch blocks for proper error type narrowing
- Add UNIT_DIVIDER_KILO, MILLISECONDS_PER_HOUR and
  DEFAULT_EXPONENTIAL_BACKOFF_JITTER_MS to Constants

Note: isEmpty/isNotEmptyString apply whitespace trimming, which
tightens validation for identifiers and configuration values.

20 files changed:
src/charging-station/ChargingStation.ts
src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20CertificateManager.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20VariableManager.ts
src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts
src/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.ts
src/charging-station/ocpp/auth/adapters/OCPP20AuthAdapter.ts
src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts
src/charging-station/ocpp/auth/services/OCPPAuthServiceImpl.ts
src/charging-station/ocpp/auth/utils/AuthHelpers.ts
src/charging-station/ocpp/auth/utils/AuthValidators.ts
src/charging-station/ocpp/auth/utils/ConfigValidator.ts
src/charging-station/ui-server/UIMCPServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/utils/Configuration.ts
src/utils/Logger.ts
src/utils/StatisticUtils.ts

index 84277c64febb699bc7ba44b6fe5571d0e8a77a8a..fc44f3043f86d749995042cacb524adb83bab83d 100644 (file)
@@ -275,10 +275,8 @@ export class ChargingStation extends EventEmitter {
       try {
         this.internalStopMessageSequence()
       } catch (error) {
-        logger.error(
-          `${this.logPrefix()} Error while stopping the internal message sequence:`,
-          error
-        )
+        const e = ensureError(error)
+        logger.error(`${this.logPrefix()} Error while stopping the internal message sequence:`, e)
       }
     })
 
@@ -346,7 +344,8 @@ export class ChargingStation extends EventEmitter {
       try {
         await this.stop()
       } catch (error) {
-        logger.error(`${this.logPrefix()} Error stopping station during delete:`, error)
+        const e = ensureError(error)
+        logger.error(`${this.logPrefix()} Error stopping station during delete:`, e)
       }
     }
     AutomaticTransactionGenerator.deleteInstance(this)
@@ -371,9 +370,10 @@ export class ChargingStation extends EventEmitter {
       try {
         rmSync(this.configurationFile, { force: true })
       } catch (error) {
+        const e = ensureError(error)
         logger.error(
           `${this.logPrefix()} Failed to delete configuration file ${this.configurationFile}:`,
-          error
+          e
         )
       }
     }
@@ -957,7 +957,8 @@ export class ChargingStation extends EventEmitter {
     try {
       await this.stop(reason, graceful ? this.stationInfo?.stopTransactionsOnStopped : false)
     } catch (error) {
-      logger.error(`${this.logPrefix()} Error during reset stop phase:`, error)
+      const e = ensureError(error)
+      logger.error(`${this.logPrefix()} Error during reset stop phase:`, e)
       return
     }
     await sleep(this.stationInfo?.resetTime ?? 0)
@@ -1058,9 +1059,10 @@ export class ChargingStation extends EventEmitter {
                   this.restartHeartbeat()
                   this.restartWebSocketPing()
                 } catch (error) {
+                  const e = ensureError(error)
                   logger.error(
                     `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
-                    error
+                    e
                   )
                 }
               }
@@ -2218,8 +2220,9 @@ export class ChargingStation extends EventEmitter {
       }
     } catch (error) {
       if (!Array.isArray(request)) {
+        const e = ensureError(error)
         // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
+        logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, e)
         // OCPP 2.0.1 §4.2.3: respond with CALLERROR using messageId "-1"
         if (this.stationInfo?.ocppVersion !== OCPPVersion.VERSION_16) {
           await this.ocppRequestService
index 8037b67b322a2def5d04df2366ef415ddbf05bea..07dc12741679b72cbd72578921edd8980eb72828 100644 (file)
@@ -7,7 +7,7 @@ import {
   type ResponsePayload,
   ResponseStatus,
 } from '../../types/index.js'
-import { logger } from '../../utils/index.js'
+import { isNotEmptyArray, logger } from '../../utils/index.js'
 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
 
 const moduleName = 'UIServiceWorkerBroadcastChannel'
@@ -33,7 +33,7 @@ export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
   private buildResponsePayload (uuid: string): ResponsePayload {
     const responsesArray = this.responses.get(uuid)?.responses ?? []
     const responsesStatus =
-      responsesArray.length > 0 &&
+      isNotEmptyArray(responsesArray) &&
       responsesArray.every(response => response.status === ResponseStatus.SUCCESS)
         ? ResponseStatus.SUCCESS
         : ResponseStatus.FAILURE
index 2c5eb3560bdd38bcb4f595af801f97cf62c108d8..e3f16aa7f8dc57aa4b180c5f2d108a631a2438b0 100644 (file)
@@ -976,7 +976,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
           )
         }
       }
-      if (allChargingProfiles.length === 0) {
+      if (isEmpty(allChargingProfiles)) {
         return OCPP16Constants.OCPP_RESPONSE_REJECTED
       }
       const compositeSchedule = this.composeCompositeSchedule(
index ea79c572a353ff47bbca875de303db134cd9ea3d..338b839e20b59d672c5d248594dcbffece6c83fd 100644 (file)
@@ -16,7 +16,7 @@ import {
   HashAlgorithmEnumType,
   InstallCertificateUseEnumType,
 } from '../../../types/index.js'
-import { convertToDate, getErrorMessage, isEmpty } from '../../../utils/index.js'
+import { convertToDate, getErrorMessage, isEmpty, isNotEmptyArray } from '../../../utils/index.js'
 
 /**
  * Interface for ChargingStation with certificate manager
@@ -283,7 +283,7 @@ export class OCPP20CertificateManager {
         .map(dirent => dirent.name)
 
       for (const certType of certTypes) {
-        if (filterTypes != null && filterTypes.length > 0) {
+        if (filterTypes != null && isNotEmptyArray(filterTypes)) {
           if (!filterTypes.includes(certType as InstallCertificateUseEnumType)) {
             continue
           }
index eeb89f2b677f4869863afd249e178a5aca110b43..f6f57e763465060dbefc574ffae05db5fa904811 100644 (file)
@@ -132,6 +132,7 @@ import {
   convertToIntOrNaN,
   generateUUID,
   isEmpty,
+  isNotEmptyArray,
   isNotEmptyString,
   logger,
   promiseWithTimeout,
@@ -442,7 +443,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
               connectorId,
               response.transactionId,
               {
-                ...(startedMeterValues.length > 0 && { meterValue: startedMeterValues }),
+                ...(isNotEmptyArray(startedMeterValues) && { meterValue: startedMeterValues }),
                 remoteStartId: request.remoteStartId,
               }
             ).catch((error: unknown) => {
@@ -984,7 +985,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
               ]
               return order.indexOf(a.type) - order.indexOf(b.type)
             })
-            if (entry.attributes.length > 0) {
+            if (isNotEmptyArray(entry.attributes)) {
               reportData.push({
                 component: entry.component,
                 variable: entry.variable,
@@ -1118,7 +1119,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     transactionId?: string
   ): boolean {
     const queue = connectorStatus.transactionEventQueue
-    if (queue == null || queue.length === 0) {
+    if (queue == null || !isNotEmptyArray(queue)) {
       return false
     }
     if (transactionId == null) {
@@ -1719,10 +1720,10 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     const stationState = this.getStationState(chargingStation)
     const cached = stationState.reportDataCache.get(commandPayload.requestId)
     const reportData = cached ?? this.buildReportData(chargingStation, commandPayload.reportBase)
-    if (!cached && reportData.length > 0) {
+    if (!cached && isNotEmptyArray(reportData)) {
       stationState.reportDataCache.set(commandPayload.requestId, reportData)
     }
-    if (reportData.length === 0) {
+    if (!isNotEmptyArray(reportData)) {
       logger.info(
         `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetBaseReport: No data available for reportBase ${commandPayload.reportBase}`
       )
@@ -1781,12 +1782,12 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       )
 
       return {
-        certificateHashDataChain:
-          result.certificateHashDataChain.length > 0 ? result.certificateHashDataChain : undefined,
-        status:
-          result.certificateHashDataChain.length > 0
-            ? GetInstalledCertificateStatusEnumType.Accepted
-            : GetInstalledCertificateStatusEnumType.NotFound,
+        certificateHashDataChain: isNotEmptyArray(result.certificateHashDataChain)
+          ? result.certificateHashDataChain
+          : undefined,
+        status: isNotEmptyArray(result.certificateHashDataChain)
+          ? GetInstalledCertificateStatusEnumType.Accepted
+          : GetInstalledCertificateStatusEnumType.NotFound,
       }
     } catch (error) {
       logger.error(
@@ -2251,7 +2252,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       OCPP20ComponentName.OCPPCommCtrlr,
       OCPP20RequiredVariableName.NetworkConfigurationPriority
     )
-    if (priorityValue.length > 0) {
+    if (isNotEmptyString(priorityValue)) {
       const priorities = priorityValue.split(',').map(Number)
       if (!priorities.includes(commandPayload.configurationSlot)) {
         logger.warn(
@@ -2375,7 +2376,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       )
       if (
         masterPassGroupId != null &&
-        masterPassGroupId.length > 0 &&
+        isNotEmptyString(masterPassGroupId) &&
         groupIdToken?.idToken === masterPassGroupId
       ) {
         logger.debug(
@@ -3283,7 +3284,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       chunks.push(reportData.slice(i, i + maxItemsPerMessage))
     }
 
-    if (chunks.length === 0) {
+    if (!isNotEmptyArray(chunks)) {
       chunks.push(undefined) // undefined means reportData will be omitted from the request
     }
 
@@ -3296,7 +3297,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
         requestId,
         seqNo,
         tbc: !isLastChunk,
-        ...(chunk !== undefined && chunk.length > 0 && { reportData: chunk }),
+        ...(chunk !== undefined && isNotEmptyArray(chunk) && { reportData: chunk }),
       }
 
       await chargingStation.ocppRequestService.requestHandler<
@@ -3322,7 +3323,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
     if (
       stationState.isDrainingSecurityEvents ||
       !chargingStation.isWebSocketConnectionOpened() ||
-      stationState.securityEventQueue.length === 0
+      !isNotEmptyArray(stationState.securityEventQueue)
     ) {
       return
     }
@@ -3332,7 +3333,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       `${chargingStation.logPrefix()} ${moduleName}.sendQueuedSecurityEvents: Draining ${queue.length.toString()} queued security event(s)`
     )
     const drainNextEvent = (): void => {
-      if (queue.length === 0 || !chargingStation.isWebSocketConnectionOpened()) {
+      if (!isNotEmptyArray(queue) || !chargingStation.isWebSocketConnectionOpened()) {
         stationState.isDrainingSecurityEvents = false
         return
       }
@@ -3771,7 +3772,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       return false
     }
 
-    if (chargingProfile.chargingSchedule.length === 0) {
+    if (!isNotEmptyArray(chargingProfile.chargingSchedule)) {
       logger.warn(
         `${chargingStation.logPrefix()} ${moduleName}.validateChargingProfile: Charging profile must contain at least one charging schedule`
       )
@@ -3935,7 +3936,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       return false
     }
 
-    if (schedule.chargingSchedulePeriod.length === 0) {
+    if (!isNotEmptyArray(schedule.chargingSchedulePeriod)) {
       logger.warn(
         `${chargingStation.logPrefix()} ${moduleName}.validateChargingSchedule: Schedule must contain at least one charging schedule period`
       )
index 9922cb630bc7e312d40596826db2dab7001a7b38..0b949562298cad1fd1eb99716ace52b10c82e0c0 100644 (file)
@@ -45,6 +45,7 @@ import {
   convertToIntOrNaN,
   formatDurationMilliSeconds,
   generateUUID,
+  isNotEmptyArray,
   logger,
   validateIdentifierString,
 } from '../../../utils/index.js'
@@ -167,7 +168,7 @@ export class OCPP20ServiceUtils {
         measurandsKey,
         OCPP20ReadingContextEnumType.TRANSACTION_BEGIN
       ) as OCPP20MeterValue
-      return startedMeterValue.sampledValue.length > 0 ? [startedMeterValue] : []
+      return isNotEmptyArray(startedMeterValue.sampledValue) ? [startedMeterValue] : []
     } catch (error) {
       logger.warn(
         `${chargingStation.logPrefix()} ${moduleName}.buildTransactionStartedMeterValues: ${(error as Error).message}`
@@ -686,7 +687,7 @@ export class OCPP20ServiceUtils {
     const connectorStatus = chargingStation.getConnectorStatus(connectorId)
     if (
       connectorStatus?.transactionEventQueue == null ||
-      connectorStatus.transactionEventQueue.length === 0
+      !isNotEmptyArray(connectorStatus.transactionEventQueue)
     ) {
       return
     }
@@ -838,7 +839,7 @@ export class OCPP20ServiceUtils {
           interval,
           measurandsKey
         ) as OCPP20MeterValue
-        if (meterValue.sampledValue.length > 0) {
+        if (isNotEmptyArray(meterValue.sampledValue)) {
           cs.transactionEndedMeterValues?.push(meterValue)
         }
       }
@@ -875,7 +876,7 @@ export class OCPP20ServiceUtils {
       {
         idToken:
           idTag != null ? { idToken: idTag, type: OCPP20IdTokenEnumType.ISO14443 } : undefined,
-        ...(startedMeterValues.length > 0 && { meterValue: startedMeterValues }),
+        ...(isNotEmptyArray(startedMeterValues) && { meterValue: startedMeterValues }),
       }
     )
     return {
@@ -1034,7 +1035,7 @@ export class OCPP20ServiceUtils {
         }
       }
     }
-    if (terminationPromises.length > 0) {
+    if (isNotEmptyArray(terminationPromises)) {
       await Promise.all(terminationPromises)
     }
   }
@@ -1143,7 +1144,7 @@ export class OCPP20ServiceUtils {
         measurandsKey,
         OCPP20ReadingContextEnumType.TRANSACTION_END
       ) as OCPP20MeterValue
-      if (finalMeterValue.sampledValue.length > 0) {
+      if (isNotEmptyArray(finalMeterValue.sampledValue)) {
         return [...endedMeterValues, finalMeterValue]
       }
     } catch (error) {
@@ -1151,7 +1152,7 @@ export class OCPP20ServiceUtils {
         `${chargingStation.logPrefix()} ${moduleName}.buildTransactionEndedMeterValues: ${(error as Error).message}`
       )
     }
-    return endedMeterValues.length > 0 ? endedMeterValues : []
+    return isNotEmptyArray(endedMeterValues) ? endedMeterValues : []
   }
 
   private static readVariableAsIntervalMs (
@@ -1222,7 +1223,7 @@ export class OCPP20ServiceUtils {
       transactionId,
       {
         evseId,
-        meterValue: endedMeterValues.length > 0 ? endedMeterValues : undefined,
+        meterValue: isNotEmptyArray(endedMeterValues) ? endedMeterValues : undefined,
         stoppedReason,
       }
     )
@@ -1331,7 +1332,7 @@ export function buildTransactionEvent (
     transactionEventRequest.idToken = commandParams.idToken
     connectorStatus.transactionIdTokenSent = true
   }
-  if (commandParams.meterValue !== undefined && commandParams.meterValue.length > 0) {
+  if (commandParams.meterValue !== undefined && isNotEmptyArray(commandParams.meterValue)) {
     transactionEventRequest.meterValue = commandParams.meterValue
   }
   if (commandParams.cableMaxCurrent !== undefined) {
index 2a00f67267caab7be404d84c557b1df74a6abfc8..6a20a04f10cbfe863c768e96167db680678cf349 100644 (file)
@@ -19,7 +19,7 @@ import {
   SetVariableStatusEnumType,
   type VariableType,
 } from '../../../types/index.js'
-import { Constants, convertToIntOrNaN, logger } from '../../../utils/index.js'
+import { Constants, convertToIntOrNaN, isEmpty, logger } from '../../../utils/index.js'
 import { type ChargingStation } from '../../ChargingStation.js'
 import {
   addConfigurationKey,
@@ -479,7 +479,7 @@ export class OCPP20VariableManager {
 
     let variableValue = this.resolveVariableValue(chargingStation, component, variable)
 
-    if (variableValue.length === 0) {
+    if (isEmpty(variableValue)) {
       if (
         resolvedAttributeType === AttributeEnumType.Target &&
         variableMetadata.supportsTarget === true
index 1de52d7eff520d15d21c4672d1829df0cf32c575..c2687bd4f81ecee6fd2564875092f5e71a412422 100644 (file)
@@ -22,7 +22,14 @@ import {
   ReasonCodeEnumType,
   type VariableName,
 } from '../../../types/index.js'
-import { Constants, convertToIntOrNaN, has, isEmpty } from '../../../utils/index.js'
+import {
+  Constants,
+  convertToFloat,
+  convertToInt,
+  convertToIntOrNaN,
+  has,
+  isEmpty,
+} from '../../../utils/index.js'
 import { OCPP20Constants } from './OCPP20Constants.js'
 
 /**
@@ -2597,7 +2604,7 @@ export function validateValue (
           reason: ReasonCodeEnumType.InvalidValue,
         }
       }
-      const num = Number(rawValue)
+      const num = convertToFloat(rawValue)
       if (variableMetadata.positive && num <= 0) {
         return {
           info: 'Positive decimal > 0 required',
@@ -2657,7 +2664,7 @@ export function validateValue (
           reason: ReasonCodeEnumType.InvalidValue,
         }
       }
-      const num = Number(rawValue)
+      const num = convertToInt(rawValue)
       if (variableMetadata.allowZero && !variableMetadata.positive && num < 0) {
         return {
           info: 'Integer >= 0 required',
@@ -2708,7 +2715,7 @@ export function validateValue (
         }
       }
       const tokens = rawValue.split(',').map(t => t.trim())
-      if (tokens.some(t => t.length === 0)) {
+      if (tokens.some(t => isEmpty(t))) {
         return { info: 'Empty list member', ok: false, reason: ReasonCodeEnumType.InvalidValue }
       }
       const seen = new Set<string>()
index 17d5d38600b0987acbbf3d4fcce0ede0aef05ed1..1e2eb685a6c3cdda6caec9ad113eaebb9565d02d 100644 (file)
@@ -16,7 +16,13 @@ import {
   RequestCommand,
   StandardParametersKey,
 } from '../../../../types/index.js'
-import { convertToBoolean, getErrorMessage, logger, truncateId } from '../../../../utils/index.js'
+import {
+  convertToBoolean,
+  getErrorMessage,
+  isEmpty,
+  logger,
+  truncateId,
+} from '../../../../utils/index.js'
 import {
   AuthContext,
   AuthenticationMethod,
@@ -300,10 +306,7 @@ export class OCPP16AuthAdapter implements OCPPAuthAdapter<string> {
     }
 
     // Check length (OCPP 1.6 spec: max 20 characters)
-    if (
-      identifier.value.length === 0 ||
-      identifier.value.length > AuthValidators.MAX_IDTAG_LENGTH
-    ) {
+    if (isEmpty(identifier.value) || identifier.value.length > AuthValidators.MAX_IDTAG_LENGTH) {
       return false
     }
 
index 8463478911fb0f80c50900ab69a18f17eb2aae41..84c07e9a05edab9a520243cd9a1105100c04d07a 100644 (file)
@@ -23,7 +23,7 @@ import {
   OCPP20RequiredVariableName,
   OCPPVersion,
 } from '../../../../types/index.js'
-import { getErrorMessage, logger, truncateId } from '../../../../utils/index.js'
+import { getErrorMessage, isEmpty, logger, truncateId } from '../../../../utils/index.js'
 import {
   AuthContext,
   AuthenticationMethod,
@@ -395,7 +395,7 @@ export class OCPP20AuthAdapter implements OCPPAuthAdapter<OCPP20IdTokenType> {
     }
 
     // Check length (OCPP 2.0 spec: max 36 characters)
-    if (identifier.value.length === 0 || identifier.value.length > 36) {
+    if (isEmpty(identifier.value) || identifier.value.length > 36) {
       return false
     }
 
index 4b46aac910a682c263ab941fc5a2c135f8ae3d29..a0693cf06eabe075da9b3b7a57750e341d85d816 100644 (file)
@@ -3,7 +3,7 @@ import { secondsToMilliseconds } from 'date-fns'
 import type { AuthCache, CacheStats } from '../interfaces/OCPPAuthService.js'
 import type { AuthorizationResult } from '../types/AuthTypes.js'
 
-import { Constants, logger, truncateId } from '../../../../utils/index.js'
+import { Constants, logger, roundTo, truncateId } from '../../../../utils/index.js'
 import { AuthorizationStatus } from '../types/AuthTypes.js'
 
 const moduleName = 'InMemoryAuthCache'
@@ -243,7 +243,7 @@ export class InMemoryAuthCache implements AuthCache {
     return {
       evictions: this.stats.evictions,
       expiredEntries: this.stats.expired,
-      hitRate: Math.round(hitRate * 100) / 100,
+      hitRate: roundTo(hitRate, 2),
       hits: this.stats.hits,
       memoryUsage,
       misses: this.stats.misses,
index f320921bb2d5773fac2f3a279dcf5d13779749d3..33539295c5c72e0c11832c2e8f48bfeb692effb8 100644 (file)
@@ -11,6 +11,7 @@ import {
   getErrorMessage,
   has,
   logger,
+  roundTo,
   truncateId,
 } from '../../../../utils/index.js'
 import { type ChargingStation } from '../../../index.js'
@@ -336,13 +337,13 @@ export class OCPPAuthServiceImpl implements OCPPAuthService {
     }
 
     return {
-      avgResponseTime: Math.round(avgResponseTime * 100) / 100,
-      cacheHitRate: Math.round(cacheHitRate * 10000) / 100,
+      avgResponseTime: roundTo(avgResponseTime, 2),
+      cacheHitRate: roundTo(cacheHitRate * 100, 2),
       failedAuth: this.metrics.failedAuth,
       lastUpdatedDate: this.metrics.lastReset,
-      localUsageRate: Math.round(localUsageRate * 10000) / 100,
+      localUsageRate: roundTo(localUsageRate * 100, 2),
       rateLimit: rateLimitStatistics,
-      remoteSuccessRate: Math.round(remoteSuccessRate * 10000) / 100,
+      remoteSuccessRate: roundTo(remoteSuccessRate * 100, 2),
       successfulAuth: this.metrics.successfulAuth,
       totalRequests: this.metrics.totalRequests,
     }
index f380dbe0ca655d6de71c02cfc7702ae852f8ade1..1579b62a93e98d066a63642211dce6d09d263f80 100644 (file)
@@ -8,7 +8,7 @@ import type {
   Identifier,
 } from '../types/AuthTypes.js'
 
-import { truncateId } from '../../../../utils/index.js'
+import { isEmpty, truncateId } from '../../../../utils/index.js'
 import { AuthorizationStatus } from '../types/AuthTypes.js'
 
 /**
@@ -169,7 +169,7 @@ function isTemporaryFailure (result: AuthorizationResult): boolean {
  * @returns The first ACCEPTED result, or the first result with merged metadata
  */
 function mergeAuthResults (results: AuthorizationResult[]): AuthorizationResult | undefined {
-  if (results.length === 0) {
+  if (isEmpty(results)) {
     return undefined
   }
 
index 1961bf6a1f25ef6ee1e3c84b8d08f319f0eb01fc..2a1f3f07ad47845b619cc813f13d0c828f109153 100644 (file)
@@ -173,9 +173,14 @@ function validateIdentifier (identifier: unknown): boolean {
     case IdentifierType.MOBILE_APP:
     case IdentifierType.NO_AUTHORIZATION:
       // OCPP 2.0 types - use IdToken max length
-      return typedIdentifier.value.length > 0 && typedIdentifier.value.length <= MAX_IDTOKEN_LENGTH
+      return (
+        isNotEmptyString(typedIdentifier.value) &&
+        typedIdentifier.value.length <= MAX_IDTOKEN_LENGTH
+      )
     case IdentifierType.ID_TAG:
-      return typedIdentifier.value.length > 0 && typedIdentifier.value.length <= MAX_IDTAG_LENGTH
+      return (
+        isNotEmptyString(typedIdentifier.value) && typedIdentifier.value.length <= MAX_IDTAG_LENGTH
+      )
 
     default:
       return false
index 7f9e5bffb5b1b45db97e6090bb3b8f7e0d32f384..ed6df7a98b8a588ae19504a0938008306701f8e1 100644 (file)
@@ -1,4 +1,4 @@
-import { logger } from '../../../../utils/index.js'
+import { isNotEmptyArray, logger } from '../../../../utils/index.js'
 import { type AuthConfiguration, AuthenticationError, AuthErrorCode } from '../types/AuthTypes.js'
 
 const moduleName = 'AuthConfigValidator'
@@ -27,7 +27,7 @@ function checkAuthMethodsEnabled (config: AuthConfiguration): void {
   if (hasCertificate) enabledMethods.push('certificate')
   if (hasOffline) enabledMethods.push('offline')
 
-  if (enabledMethods.length > 0) {
+  if (isNotEmptyArray(enabledMethods)) {
     logger.debug(`${moduleName}: Enabled authentication methods: ${enabledMethods.join(', ')}`)
   }
 }
index 38854dcd1aebaffbe573055b271bd3e6d8e27d9a..1045e49ed343226e0631d472d48899b8348097f9 100644 (file)
@@ -22,7 +22,7 @@ import {
   type UIServerConfiguration,
   type UUIDv4,
 } from '../../types/index.js'
-import { generateUUID, getErrorMessage, logger } from '../../utils/index.js'
+import { generateUUID, getErrorMessage, isNotEmptyArray, logger } from '../../utils/index.js'
 import { AbstractUIServer } from './AbstractUIServer.js'
 import {
   mcpToolSchemas,
@@ -191,7 +191,7 @@ export class UIMCPServer extends AbstractUIServer {
       }
       return s.version !== OCPPVersion.VERSION_20 && s.version !== OCPPVersion.VERSION_201
     })
-    if (mismatched.length > 0) {
+    if (isNotEmptyArray(mismatched)) {
       const ids = mismatched.map(s => s.hashId).join(', ')
       const versions = [...new Set(mismatched.map(s => s.version ?? 'unknown'))].join(', ')
       return UIMCPServer.createToolErrorResponse(
index 58c5a358e695aa0610fb54cb17b2831789ea837c..4d39ad4ac9c716e188f11032797962553beb37f9 100644 (file)
@@ -294,10 +294,10 @@ export abstract class AbstractUIService {
     }
     return {
       status: err != null ? ResponseStatus.FAILURE : ResponseStatus.SUCCESS,
-      ...(succeededStationInfos.length > 0 && {
+      ...(isNotEmptyArray(succeededStationInfos) && {
         hashIdsSucceeded: succeededStationInfos.map(stationInfo => stationInfo.hashId),
       }),
-      ...(failedStationInfos.length > 0 && {
+      ...(isNotEmptyArray(failedStationInfos) && {
         hashIdsFailed: failedStationInfos.map(stationInfo => stationInfo.hashId),
       }),
       ...(err != null && { errorMessage: err.message, errorStack: err.stack }),
index 27f2604e8678e6a88b8a9eec0fa5df8001577914..39e95297a313cef8a99484160afb70d1774e8737 100644 (file)
@@ -35,7 +35,14 @@ import {
 } from './ConfigurationUtils.js'
 import { Constants } from './Constants.js'
 import { ensureError, handleFileException } from './ErrorUtils.js'
-import { has, isCFEnvironment, isNotEmptyString, mergeDeepRight, once } from './Utils.js'
+import {
+  convertToInt,
+  has,
+  isCFEnvironment,
+  isNotEmptyString,
+  mergeDeepRight,
+  once,
+} from './Utils.js'
 
 type ConfigurationSectionType =
   | LogConfiguration
@@ -283,7 +290,7 @@ export class Configuration {
     if (isCFEnvironment()) {
       delete uiServerConfiguration.options?.host
       if (uiServerConfiguration.options != null) {
-        uiServerConfiguration.options.port = Number.parseInt(env.PORT ?? '')
+        uiServerConfiguration.options.port = convertToInt(env.PORT ?? '')
       }
     }
     return uiServerConfiguration
@@ -378,7 +385,7 @@ export class Configuration {
                 Configuration.configurationFileReloading = false
               })
               .catch((error: unknown) => {
-                throw typeof error === 'string' ? new Error(error) : error
+                throw ensureError(error)
               })
           } else {
             Configuration.configurationFileReloading = false
index 8e4d9dfe5bdfd9b5d4243ab19805e0b460759d7e..c494211eab72de8827113b3c9863a4775c302e70 100644 (file)
@@ -11,7 +11,7 @@ import DailyRotateFile from 'winston-daily-rotate-file'
 
 import { ConfigurationSection, type LogConfiguration } from '../types/index.js'
 import { Configuration } from './Configuration.js'
-import { insertAt, isNotEmptyString } from './Utils.js'
+import { insertAt, isEmpty, isNotEmptyString } from './Utils.js'
 
 let loggerInstance: undefined | WinstonLogger
 
@@ -79,7 +79,7 @@ const getLoggerInstance = (): WinstonLogger => {
     level: logConfiguration.level,
     silent:
       logConfiguration.enabled === false ||
-      logTransports.length === 0 ||
+      isEmpty(logTransports) ||
       process.env.NODE_ENV === 'test',
     transports: logTransports,
   })
index f835e517798e5b34e214a1c18495338daf3d6ff5..7e99cedb0104b359ea791778a9b8e07536c3cd54 100644 (file)
@@ -1,4 +1,4 @@
-import { isNotEmptyArray } from './Utils.js'
+import { isEmpty, isNotEmptyArray } from './Utils.js'
 
 export const average = (dataSet: number[]): number => {
   if (!isNotEmptyArray<number>(dataSet)) {
@@ -37,7 +37,7 @@ export const percentile = (dataSet: number[], percentile: number): number => {
   if (percentile < 0 || percentile > 100) {
     throw new RangeError('Percentile is not between 0 and 100')
   }
-  if (dataSet.length === 0) {
+  if (isEmpty(dataSet)) {
     return 0
   }
   const sortedDataSet = dataSet.slice().sort((a, b) => a - b)