]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix: use case-insensitive boolean parsing for OCPP configuration values
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Mar 2026 21:40:34 +0000 (22:40 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 27 Mar 2026 21:40:34 +0000 (22:40 +0100)
- Replace strict string comparisons (=== 'true'/'false') with
  convertToBoolean() or .toLowerCase() across OCPP 1.6 and 2.0 stacks
- Add missing OCPP 1.6→2.0 key mappings for HeartbeatInterval,
  HeartBeatInterval, and WebSocketPingInterval
- Add missing readonly field to 4 chargex template configuration keys
- Add .trim() to convertToBoolean for whitespace-padded values

src/assets/station-templates/chargex.station-template.json
src/charging-station/ConfigurationKeyUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.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/utils/Utils.ts
tests/charging-station/ConfigurationKeyUtils.test.ts
tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts
ui/web/src/composables/Utils.ts

index fe87192e88a01a90ae3d32c3f14b8af169e1d745..b3122496a987f66f19f7ce245ca82f676be0ffbe 100644 (file)
@@ -41,6 +41,7 @@
     "configurationKey": [
       {
         "key": "AllowOfflineTxForUnknownId",
+        "readonly": false,
         "value": "True"
       },
       {
       },
       {
         "key": "MeterValueSampleInterval",
+        "readonly": false,
         "value": "300"
       },
       {
         "key": "TransactionMessageAttempts",
+        "readonly": false,
         "value": "3"
       },
       {
         "key": "TransactionMessageRetryInterval",
+        "readonly": false,
         "value": "20"
       },
       {
index 826cbe0027e09e9240acad1c23e7b7c43890609e..86c934d9ff452f3cf53cc8effce3f68ea06a7c5f 100644 (file)
@@ -64,6 +64,21 @@ const OCPP2_PARAMETER_KEY_MAP = new Map<
             StandardParametersKey.ReserveConnectorZeroSupported,
             buildConfigKey(OCPP20ComponentName.ReservationCtrlr, StandardParametersKey.NonEvseSpecific),
           ],
+          [
+            StandardParametersKey.HeartbeatInterval,
+            buildConfigKey(OCPP20ComponentName.OCPPCommCtrlr, StandardParametersKey.HeartbeatInterval),
+          ],
+          [
+            StandardParametersKey.HeartBeatInterval,
+            buildConfigKey(OCPP20ComponentName.OCPPCommCtrlr, StandardParametersKey.HeartbeatInterval),
+          ],
+          [
+            StandardParametersKey.WebSocketPingInterval,
+            buildConfigKey(
+              OCPP20ComponentName.ChargingStation,
+              StandardParametersKey.WebSocketPingInterval
+            ),
+          ],
         ] as [ConfigurationKeyType, ConfigurationKeyType][]
       ).map(([from, to]) => [
         from,
index ed17e2d6fc19b8270ce35adda12413b09fe4d847..1146fb44c14d7726df5f5bfbd6e5d193acd277b4 100644 (file)
@@ -123,6 +123,7 @@ import {
   UploadLogStatusEnumType,
 } from '../../../types/index.js'
 import {
+  convertToBoolean,
   convertToDate,
   generateUUID,
   logger,
@@ -1901,7 +1902,11 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
         variable: { name: 'AllowReset' },
       },
     ])
-    if (allowResetResults.length > 0 && allowResetResults[0].attributeValue === 'false') {
+    if (
+      allowResetResults.length > 0 &&
+      allowResetResults[0].attributeValue != null &&
+      !convertToBoolean(allowResetResults[0].attributeValue)
+    ) {
       logger.warn(
         `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: AllowReset is false, rejecting reset request`
       )
@@ -2161,7 +2166,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
           variable: { name: 'AllowSecurityProfileDowngrade' },
         },
       ])
-      const allowDowngrade = allowDowngradeResults[0]?.attributeValue?.toLowerCase() === 'true'
+      const allowDowngrade = convertToBoolean(allowDowngradeResults[0]?.attributeValue)
 
       // B09.FR.31 (errata 2025-09 §2.12): Allow downgrade except to profile 1 when enabled
       if (!allowDowngrade || newSecurityProfile <= 1) {
@@ -2302,7 +2307,8 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
       },
     ])
     const shouldAuthorizeRemoteStart =
-      authorizeRemoteStartResults[0]?.attributeValue?.toLowerCase() !== 'false'
+      authorizeRemoteStartResults[0]?.attributeValue == null ||
+      convertToBoolean(authorizeRemoteStartResults[0].attributeValue)
 
     let isAuthorized = true
     if (shouldAuthorizeRemoteStart) {
@@ -3494,7 +3500,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
           variable: { name: 'SimulateSignatureVerificationFailure' },
         },
       ])
-      const simulateFailure = verificationResults[0]?.attributeValue?.toLowerCase() === 'true'
+      const simulateFailure = convertToBoolean(verificationResults[0]?.attributeValue)
 
       if (simulateFailure) {
         // L01.FR.03: InvalidSignature + SecurityEventNotification
@@ -3556,7 +3562,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
           variable: { name: 'AllowNewSessionsPendingFirmwareUpdate' },
         },
       ])
-      const allowNewSessions = allowNewSessionsResults[0]?.attributeValue?.toLowerCase() === 'true'
+      const allowNewSessions = convertToBoolean(allowNewSessionsResults[0]?.attributeValue)
       while (
         !checkAborted() &&
         [...chargingStation.evses].some(
index 4717498fed8df15ad5f38b9f91aac0d0ef94dc99..4a34b59f949fd1d04c60b7d9815b031d9cfc5266 100644 (file)
@@ -937,7 +937,7 @@ export class OCPP20VariableManager {
       isOCPP20RequiredVariableName(variable.name) &&
       variable.name === OCPP20RequiredVariableName.AuthorizeRemoteStart
     ) {
-      if (attributeValue !== 'true' && attributeValue !== 'false') {
+      if (attributeValue.toLowerCase() !== 'true' && attributeValue.toLowerCase() !== 'false') {
         return this.rejectSet(
           variable,
           component,
index bc3dd33bbab236c4af6217744163e514148c4e24..edfaeaa3d4cc64334cbc3a2f6d49e2341311fded 100644 (file)
@@ -2499,7 +2499,7 @@ export function validateValue (
   }
   switch (variableMetadata.dataType) {
     case DataEnumType.boolean: {
-      if (rawValue !== 'true' && rawValue !== 'false') {
+      if (rawValue.toLowerCase() !== 'true' && rawValue.toLowerCase() !== 'false') {
         return {
           info: 'Boolean must be "true" or "false"',
           ok: false,
index 7e189980c58526dfc7f5d7e3218a1fa3e56075ea..abbb05e7d68c8705b283168276bc9496184bb0d1 100644 (file)
@@ -16,7 +16,7 @@ import {
   RequestCommand,
   StandardParametersKey,
 } from '../../../../types/index.js'
-import { logger, truncateId } from '../../../../utils/index.js'
+import { convertToBoolean, logger, truncateId } from '../../../../utils/index.js'
 import {
   AuthContext,
   AuthenticationMethod,
@@ -364,7 +364,7 @@ export class OCPP16AuthAdapter implements OCPPAuthAdapter<string> {
         this.chargingStation,
         StandardParametersKey.AllowOfflineTxForUnknownId
       )
-      return configKey?.value === 'true'
+      return convertToBoolean(configKey?.value)
     } catch (error) {
       logger.warn(
         `${this.chargingStation.logPrefix()} ${moduleName}.getOfflineTransactionConfig: Error getting offline transaction config`,
index 71159ee761371ee5af3cc5d1c9bf7043e6f6a3ed..48db01f71efb6ee431587eb2ed8175fb39506467 100644 (file)
@@ -283,7 +283,10 @@ export const convertToBoolean = (value: unknown): boolean => {
     // Check the type
     if (typeof value === 'boolean') {
       return value
-    } else if (typeof value === 'string' && (value.toLowerCase() === 'true' || value === '1')) {
+    } else if (
+      typeof value === 'string' &&
+      (value.trim().toLowerCase() === 'true' || value === '1')
+    ) {
       result = true
     } else if (typeof value === 'number' && value === 1) {
       result = true
index 7e61aff0b3c8cfb7db751928ad41202624b65283..7e1d8fae40af1f774e05d9b46056b4432c2f6c18 100644 (file)
@@ -147,18 +147,18 @@ await describe('ConfigurationKeyUtils', async () => {
     await it('should not resolve unmapped keys on OCPP 2.0.1 station', () => {
       // Arrange
       const cs = createStationForVersion(OCPPVersion.VERSION_201)
-      addConfigurationKey(cs, StandardParametersKey.HeartbeatInterval, '30', undefined, {
+      addConfigurationKey(cs, StandardParametersKey.NumberOfConnectors, '2', undefined, {
         save: false,
       })
 
       // Act
-      const k = getConfigurationKey(cs, StandardParametersKey.HeartbeatInterval)
+      const k = getConfigurationKey(cs, StandardParametersKey.NumberOfConnectors)
 
       // Assert
       if (k == null) {
         assert.fail('Expected configuration key to be found')
       }
-      assert.strictEqual(k.key, StandardParametersKey.HeartbeatInterval)
+      assert.strictEqual(k.key, StandardParametersKey.NumberOfConnectors)
     })
   })
 
index eb043680f54608ed0179dcf59342a896a2dc4384..545368fd69fb07f23f780250d4d075b2d8fb6543 100644 (file)
@@ -201,7 +201,7 @@ await describe('B05 - OCPP20VariableManager', async () => {
     })
 
     await it('should reject invalid values for AuthorizeRemoteStart (AuthCtrlr)', () => {
-      const invalidValues = ['', '1', 'TRUE', 'False', 'yes']
+      const invalidValues = ['', '1', 'yes']
       for (const val of invalidValues) {
         const res = manager.setVariables(station, [
           {
index f4117b4f1e6e5f3d196078cb4f275b06d31ac292..8e1a72cdbbacfe60455af41a1ad687e30027623a 100644 (file)
@@ -13,7 +13,10 @@ export const convertToBoolean = (value: unknown): boolean => {
     // Check the type
     if (typeof value === 'boolean') {
       return value
-    } else if (typeof value === 'string' && (value.toLowerCase() === 'true' || value === '1')) {
+    } else if (
+      typeof value === 'string' &&
+      (value.trim().toLowerCase() === 'true' || value === '1')
+    ) {
       result = true
     } else if (typeof value === 'number' && value === 1) {
       result = true