From bae23467f97a83cd21e3632e2c337f26c4414b38 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 27 Mar 2026 22:40:34 +0100 Subject: [PATCH] fix: use case-insensitive boolean parsing for OCPP configuration values MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - 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 --- .../chargex.station-template.json | 4 ++++ src/charging-station/ConfigurationKeyUtils.ts | 15 +++++++++++++++ .../ocpp/2.0/OCPP20IncomingRequestService.ts | 16 +++++++++++----- .../ocpp/2.0/OCPP20VariableManager.ts | 2 +- .../ocpp/2.0/OCPP20VariableRegistry.ts | 2 +- .../ocpp/auth/adapters/OCPP16AuthAdapter.ts | 4 ++-- src/utils/Utils.ts | 5 ++++- .../ConfigurationKeyUtils.test.ts | 6 +++--- .../ocpp/2.0/OCPP20VariableManager.test.ts | 2 +- ui/web/src/composables/Utils.ts | 5 ++++- 10 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/assets/station-templates/chargex.station-template.json b/src/assets/station-templates/chargex.station-template.json index fe87192e..b3122496 100644 --- a/src/assets/station-templates/chargex.station-template.json +++ b/src/assets/station-templates/chargex.station-template.json @@ -41,6 +41,7 @@ "configurationKey": [ { "key": "AllowOfflineTxForUnknownId", + "readonly": false, "value": "True" }, { @@ -60,14 +61,17 @@ }, { "key": "MeterValueSampleInterval", + "readonly": false, "value": "300" }, { "key": "TransactionMessageAttempts", + "readonly": false, "value": "3" }, { "key": "TransactionMessageRetryInterval", + "readonly": false, "value": "20" }, { diff --git a/src/charging-station/ConfigurationKeyUtils.ts b/src/charging-station/ConfigurationKeyUtils.ts index 826cbe00..86c934d9 100644 --- a/src/charging-station/ConfigurationKeyUtils.ts +++ b/src/charging-station/ConfigurationKeyUtils.ts @@ -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, diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index ed17e2d6..1146fb44 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -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( diff --git a/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts b/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts index 4717498f..4a34b59f 100644 --- a/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts +++ b/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts @@ -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, diff --git a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts index bc3dd33b..edfaeaa3 100644 --- a/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts +++ b/src/charging-station/ocpp/2.0/OCPP20VariableRegistry.ts @@ -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, diff --git a/src/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.ts b/src/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.ts index 7e189980..abbb05e7 100644 --- a/src/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.ts +++ b/src/charging-station/ocpp/auth/adapters/OCPP16AuthAdapter.ts @@ -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 { 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`, diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 71159ee7..48db01f7 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -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 diff --git a/tests/charging-station/ConfigurationKeyUtils.test.ts b/tests/charging-station/ConfigurationKeyUtils.test.ts index 7e61aff0..7e1d8fae 100644 --- a/tests/charging-station/ConfigurationKeyUtils.test.ts +++ b/tests/charging-station/ConfigurationKeyUtils.test.ts @@ -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) }) }) diff --git a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts index eb043680..545368fd 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20VariableManager.test.ts @@ -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, [ { diff --git a/ui/web/src/composables/Utils.ts b/ui/web/src/composables/Utils.ts index f4117b4f..8e1a72cd 100644 --- a/ui/web/src/composables/Utils.ts +++ b/ui/web/src/composables/Utils.ts @@ -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 -- 2.43.0