From: Jérôme Benoit Date: Sun, 15 Mar 2026 18:19:07 +0000 (+0100) Subject: fix(ocpp20): isolate VariableManager overrides per station X-Git-Tag: ocpp-server@v3.1.0~19 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=77bd1a0315b3a3b75a47ed6f08d4c4eb5d35feed;p=e-mobility-charging-stations-simulator.git fix(ocpp20): isolate VariableManager overrides per station runtimeOverrides, minSetOverrides, and maxSetOverrides were stored as flat Maps on the singleton OCPP20VariableManager, causing SetVariables on one station to affect all others sharing the same service instance. Apply the same Map> pattern already used by invalidVariables in the same class. resetRuntimeOverrides now accepts optional stationId, matching the invalidateMappingsCache API. --- diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 84dba914..3e2a783e 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -667,7 +667,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { this.stationStates.delete(chargingStation) } try { - OCPP20VariableManager.getInstance().resetRuntimeOverrides() + OCPP20VariableManager.getInstance().resetRuntimeOverrides(chargingStation.stationInfo?.hashId) logger.debug(`${chargingStation.logPrefix()} ${moduleName}.stop: Runtime overrides cleared`) } catch (error) { logger.error( diff --git a/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts b/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts index 0d61f912..94a59a27 100644 --- a/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts +++ b/src/charging-station/ocpp/2.0/OCPP20VariableManager.ts @@ -60,9 +60,9 @@ export class OCPP20VariableManager { ) private readonly invalidVariables = new Map>() // stationId → composite keys (lower case) - private readonly maxSetOverrides = new Map() // composite key (lower case) → value - private readonly minSetOverrides = new Map() // composite key (lower case) → value - private readonly runtimeOverrides = new Map() // composite key (lower case) → value + private readonly maxSetOverrides = new Map>() // stationId → composite key → value + private readonly minSetOverrides = new Map>() // stationId → composite key → value + private readonly runtimeOverrides = new Map>() // stationId → composite key → value private readonly validatedStations = new Set() // stationId private constructor () { @@ -114,8 +114,16 @@ export class OCPP20VariableManager { } } - public resetRuntimeOverrides (): void { - this.runtimeOverrides.clear() + public resetRuntimeOverrides (stationId?: string): void { + if (stationId != null) { + this.runtimeOverrides.delete(stationId) + this.minSetOverrides.delete(stationId) + this.maxSetOverrides.delete(stationId) + } else { + this.runtimeOverrides.clear() + this.minSetOverrides.clear() + this.maxSetOverrides.clear() + } } public setVariables ( @@ -123,6 +131,7 @@ export class OCPP20VariableManager { setVariableData: OCPP20SetVariableDataType[] ): OCPP20SetVariableResultType[] { this.validatePersistentMappings(chargingStation) + const stationId = this.getStationId(chargingStation) // Collect paired MinSet/MaxSet entries for atomic cross-validation const pairedBounds = new Map() @@ -156,11 +165,11 @@ export class OCPP20VariableManager { const newMax = convertToIntOrNaN(pair.maxValue) if (Number.isNaN(newMin) || Number.isNaN(newMax) || newMin > newMax) continue savedOverrides.set(varKey, { - prevMax: this.maxSetOverrides.get(varKey), - prevMin: this.minSetOverrides.get(varKey), + prevMax: this.getMaxSetOverrides(stationId).get(varKey), + prevMin: this.getMinSetOverrides(stationId).get(varKey), }) - this.minSetOverrides.set(varKey, pair.minValue) - this.maxSetOverrides.set(varKey, pair.maxValue) + this.getMinSetOverrides(stationId).set(varKey, pair.minValue) + this.getMaxSetOverrides(stationId).set(varKey, pair.maxValue) } const results: OCPP20SetVariableResultType[] = [] @@ -220,16 +229,16 @@ export class OCPP20VariableManager { } if (minRejected) { if (saved.prevMin != null) { - this.minSetOverrides.set(varKey, saved.prevMin) + this.getMinSetOverrides(stationId).set(varKey, saved.prevMin) } else { - this.minSetOverrides.delete(varKey) + this.getMinSetOverrides(stationId).delete(varKey) } } if (maxRejected) { if (saved.prevMax != null) { - this.maxSetOverrides.set(varKey, saved.prevMax) + this.getMaxSetOverrides(stationId).set(varKey, saved.prevMax) } else { - this.maxSetOverrides.delete(varKey) + this.getMaxSetOverrides(stationId).delete(varKey) } } } @@ -302,6 +311,33 @@ export class OCPP20VariableManager { return set } + private getMaxSetOverrides (stationId: string): Map { + let map = this.maxSetOverrides.get(stationId) + if (map == null) { + map = new Map() + this.maxSetOverrides.set(stationId, map) + } + return map + } + + private getMinSetOverrides (stationId: string): Map { + let map = this.minSetOverrides.get(stationId) + if (map == null) { + map = new Map() + this.minSetOverrides.set(stationId, map) + } + return map + } + + private getRuntimeOverrides (stationId: string): Map { + let map = this.runtimeOverrides.get(stationId) + if (map == null) { + map = new Map() + this.runtimeOverrides.set(stationId, map) + } + return map + } + private getStationId (chargingStation: ChargingStation): string { const stationId = chargingStation.stationInfo?.hashId if (stationId == null) { @@ -388,7 +424,10 @@ export class OCPP20VariableManager { } if (resolvedAttributeType === AttributeEnumType.MinSet) { - if (variableMetadata.min === undefined && this.minSetOverrides.get(variableKey) == null) { + if ( + variableMetadata.min === undefined && + this.getMinSetOverrides(stationId).get(variableKey) == null + ) { return this.rejectGet( variable, component, @@ -399,7 +438,7 @@ export class OCPP20VariableManager { ) } const minValue = - this.minSetOverrides.get(variableKey) ?? + this.getMinSetOverrides(stationId).get(variableKey) ?? (variableMetadata.min !== undefined ? variableMetadata.min.toString() : '') return { attributeStatus: GetVariableStatusEnumType.Accepted, @@ -410,7 +449,10 @@ export class OCPP20VariableManager { } } if (resolvedAttributeType === AttributeEnumType.MaxSet) { - if (variableMetadata.max === undefined && this.maxSetOverrides.get(variableKey) == null) { + if ( + variableMetadata.max === undefined && + this.getMaxSetOverrides(stationId).get(variableKey) == null + ) { return this.rejectGet( variable, component, @@ -421,7 +463,7 @@ export class OCPP20VariableManager { ) } const maxValue = - this.maxSetOverrides.get(variableKey) ?? + this.getMaxSetOverrides(stationId).get(variableKey) ?? (variableMetadata.max !== undefined ? variableMetadata.max.toString() : '') return { attributeStatus: GetVariableStatusEnumType.Accepted, @@ -604,7 +646,8 @@ export class OCPP20VariableManager { variableMetadata.persistence === PersistenceEnumType.Volatile && variableMetadata.mutability !== MutabilityEnumType.ReadOnly ) { - const override = this.runtimeOverrides.get(compositeKey) + const stationId = this.getStationId(chargingStation) + const override = this.getRuntimeOverrides(stationId).get(compositeKey) if (override != null) { value = override } @@ -768,7 +811,7 @@ export class OCPP20VariableManager { } if (resolvedAttributeType === AttributeEnumType.MinSet) { const currentMax = - this.maxSetOverrides.get(variableKey) ?? + this.getMaxSetOverrides(stationId).get(variableKey) ?? (variableMetadata.max !== undefined ? variableMetadata.max.toString() : undefined) if (currentMax != null && intValue > convertToIntOrNaN(currentMax)) { return this.rejectSet( @@ -780,10 +823,10 @@ export class OCPP20VariableManager { 'MinSet higher than MaxSet' ) } - this.minSetOverrides.set(variableKey, attributeValue) + this.getMinSetOverrides(stationId).set(variableKey, attributeValue) } else { const currentMin = - this.minSetOverrides.get(variableKey) ?? + this.getMinSetOverrides(stationId).get(variableKey) ?? (variableMetadata.min !== undefined ? variableMetadata.min.toString() : undefined) if (currentMin != null && intValue < convertToIntOrNaN(currentMin)) { return this.rejectSet( @@ -795,7 +838,7 @@ export class OCPP20VariableManager { 'MaxSet lower than MinSet' ) } - this.maxSetOverrides.set(variableKey, attributeValue) + this.getMaxSetOverrides(stationId).set(variableKey, attributeValue) } return { attributeStatus: SetVariableStatusEnumType.Accepted, @@ -909,8 +952,8 @@ export class OCPP20VariableManager { if (variableMetadata.dataType === DataEnumType.integer) { const num = convertToIntOrNaN(attributeValue) if (!Number.isNaN(num)) { - const overrideMinRaw = this.minSetOverrides.get(variableKey) - const overrideMaxRaw = this.maxSetOverrides.get(variableKey) + const overrideMinRaw = this.getMinSetOverrides(stationId).get(variableKey) + const overrideMaxRaw = this.getMaxSetOverrides(stationId).get(variableKey) if (overrideMinRaw != null) { const overrideMin = convertToIntOrNaN(overrideMinRaw) if (!Number.isNaN(overrideMin) && num < overrideMin) { @@ -979,7 +1022,7 @@ export class OCPP20VariableManager { } // Apply volatile runtime override generically (single location) if (variableMetadata.persistence === PersistenceEnumType.Volatile) { - this.runtimeOverrides.set(variableKey, attributeValue) + this.getRuntimeOverrides(stationId).set(variableKey, attributeValue) } if (rebootRequired) {