From 5d68d55e6b18849f73e40bb25def43631ac7631c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 3 Apr 2026 22:56:28 +0200 Subject: [PATCH] refactor: enforce utility usage and centralize constants across all components - Replace .length/.size checks with isEmpty/isNotEmptyArray in 7 src/ files - Replace Number.parseInt/parseFloat with convertToInt in OCPPServiceUtils (intentional fail-fast: malformed templates now throw instead of silent NaN) - Keep Number.parseFloat in getLimitFromSampledValueTemplateCustomValue (NaN fallback contract requires it) - Centralize route names (ROUTE_NAMES), placeholder (EMPTY_VALUE_PLACEHOLDER), and localStorage key in Vue UI (11 files) - Extract 13 magic values to constants in Python ocpp-server - Rename constants: WORKER_SET_VERSION, DEFAULT_RATE_WINDOW_MS, MS_PER_HOUR, DEFAULT_*_SECONDS in Python, DEFAULT_*_BYTES in UIServerSecurity --- .../AutomaticTransactionGenerator.ts | 3 +- src/charging-station/ChargingStation.ts | 20 ++++---- src/charging-station/Helpers.ts | 4 +- .../ocpp/2.0/OCPP20ServiceUtils.ts | 6 ++- src/charging-station/ocpp/OCPPServiceUtils.ts | 12 ++--- .../ocpp/auth/cache/InMemoryAuthCache.ts | 4 +- .../ui-server/AbstractUIServer.ts | 4 +- .../ui-server/UIHttpServer.ts | 8 ++-- src/charging-station/ui-server/UIMCPServer.ts | 16 +++++-- .../ui-server/UIServerSecurity.ts | 6 +-- .../ui-server/UIWebSocketServer.ts | 9 ++-- src/worker/WorkerConstants.ts | 2 +- src/worker/WorkerSet.ts | 8 +++- .../ChargingStationTestConstants.ts | 2 +- .../OCPP20ServiceUtils-ReconnectDelay.test.ts | 13 +++--- .../ui-server/UIHttpServer.test.ts | 4 +- .../ui-server/UIMCPServer.test.ts | 4 +- tests/ocpp-server/server.py | 46 ++++++++++++------- tests/ocpp-server/test_server.py | 6 +-- ui/web/src/App.vue | 3 +- .../actions/AddChargingStations.vue | 3 +- .../components/actions/SetSupervisionUrl.vue | 4 +- .../components/actions/StartTransaction.vue | 12 +++-- .../charging-stations/CSConnector.vue | 8 ++-- .../components/charging-stations/CSData.vue | 12 +++-- ui/web/src/composables/Constants.ts | 10 ++++ ui/web/src/composables/index.ts | 2 + ui/web/src/main.ts | 9 ++-- ui/web/src/router/index.ts | 11 +++-- ui/web/src/views/ChargingStationsView.vue | 12 +++-- 30 files changed, 161 insertions(+), 102 deletions(-) diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index 6a73c97e..ca65b7d6 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -20,6 +20,7 @@ import { Constants, convertToDate, formatDurationMilliSeconds, + isEmpty, isValidDate, logger, logPrefix, @@ -385,7 +386,7 @@ export class AutomaticTransactionGenerator { private startConnectors (stopAbsoluteDuration?: boolean): void { if ( - this.connectorsStatus.size > 0 && + !isEmpty(this.connectorsStatus) && this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors() ) { this.connectorsStatus.clear() diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 3f64f267..716b8e42 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -177,7 +177,7 @@ export class ChargingStation extends EventEmitter { public wsConnection: null | WebSocket public get hasEvses (): boolean { - return isEmpty(this.connectors) && this.evses.size > 0 + return isEmpty(this.connectors) && !isEmpty(this.evses) } public get wsConnectionUrl (): URL { @@ -1233,7 +1233,7 @@ export class ChargingStation extends EventEmitter { } private flushMessageBuffer (): void { - if (!this.flushingMessageBuffer && this.messageQueue.length > 0) { + if (!this.flushingMessageBuffer && isNotEmptyArray(this.messageQueue)) { this.flushingMessageBuffer = true this.sendMessageBuffer(() => { this.flushingMessageBuffer = false @@ -1862,7 +1862,7 @@ export class ChargingStation extends EventEmitter { private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { if (configuration.connectorsStatus != null && configuration.evsesStatus == null) { const isTupleFormat = - configuration.connectorsStatus.length > 0 && + isNotEmptyArray(configuration.connectorsStatus) && Array.isArray(configuration.connectorsStatus[0]) const entries: [number, ConnectorStatus][] = isTupleFormat ? (configuration.connectorsStatus as [number, ConnectorStatus][]) @@ -1875,7 +1875,7 @@ export class ChargingStation extends EventEmitter { } } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { const isTupleFormat = - configuration.evsesStatus.length > 0 && Array.isArray(configuration.evsesStatus[0]) + isNotEmptyArray(configuration.evsesStatus) && Array.isArray(configuration.evsesStatus[0]) const evseEntries: [number, EvseStatusConfiguration][] = isTupleFormat ? (configuration.evsesStatus as [number, EvseStatusConfiguration][]) : (configuration.evsesStatus as EvseStatusConfiguration[]).map((status, index) => [ @@ -1887,7 +1887,7 @@ export class ChargingStation extends EventEmitter { delete evseStatus.connectorsStatus const connIsTupleFormat = evseStatusConfiguration.connectorsStatus != null && - evseStatusConfiguration.connectorsStatus.length > 0 && + isNotEmptyArray(evseStatusConfiguration.connectorsStatus) && Array.isArray(evseStatusConfiguration.connectorsStatus[0]) const connEntries: [number, ConnectorStatus][] = connIsTupleFormat ? (evseStatusConfiguration.connectorsStatus as [number, ConnectorStatus][]) @@ -2436,12 +2436,12 @@ export class ChargingStation extends EventEmitter { if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) { delete configurationData.automaticTransactionGenerator } - if (this.connectors.size > 0) { + if (!isEmpty(this.connectors)) { configurationData.connectorsStatus = buildConnectorsStatus(this) } else { delete configurationData.connectorsStatus } - if (this.evses.size > 0) { + if (!isEmpty(this.evses)) { configurationData.evsesStatus = buildEvsesStatus(this) } else { delete configurationData.evsesStatus @@ -2453,10 +2453,10 @@ export class ChargingStation extends EventEmitter { automaticTransactionGenerator: configurationData.automaticTransactionGenerator, configurationKey: configurationData.configurationKey, stationInfo: configurationData.stationInfo, - ...(this.connectors.size > 0 && { + ...(!isEmpty(this.connectors) && { connectorsStatus: configurationData.connectorsStatus, }), - ...(this.evses.size > 0 && { + ...(!isEmpty(this.evses) && { evsesStatus: configurationData.evsesStatus, }), } satisfies ChargingStationConfiguration), @@ -2524,7 +2524,7 @@ export class ChargingStation extends EventEmitter { onCompleteCallback: () => void, messageIdx?: number ): void => { - if (this.messageQueue.length > 0) { + if (isNotEmptyArray(this.messageQueue)) { const message = this.messageQueue[0] let beginId: string | undefined let commandName: RequestCommand | undefined diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 780a72d5..a51b9c70 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -272,7 +272,7 @@ export const getMaxNumberOfEvses = (evses: Record | undefi if (evses == null) { return -1 } - return Object.keys(evses).length + return isEmpty(evses) ? 0 : Object.keys(evses).length } const getMaxNumberOfConnectors = ( @@ -281,7 +281,7 @@ const getMaxNumberOfConnectors = ( if (connectors == null) { return -1 } - return Object.keys(connectors).length + return isEmpty(connectors) ? 0 : Object.keys(connectors).length } export const getBootConnectorStatus = ( diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index 7dc41c2b..3598d176 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -11,6 +11,7 @@ import { OCPP20ComponentName, type OCPP20ConnectorStatusEnumType, type OCPP20EVSEType, + type OCPP20GetVariableResultType, OCPP20IdTokenEnumType, type OCPP20IdTokenInfoType, type OCPP20IdTokenType, @@ -531,7 +532,10 @@ export class OCPP20ServiceUtils { variable: { name: variableName }, }, ]) - if (results.length > 0 && results[0].attributeValue != null) { + if ( + isNotEmptyArray(results) && + results[0].attributeValue != null + ) { return results[0].attributeValue } return undefined diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index b4826ec8..b022c19a 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -67,7 +67,7 @@ const moduleName = 'OCPPServiceUtils' const SOC_MAXIMUM_VALUE = 100 const UNIT_DIVIDER_KILO = 1000 -const MILLISECONDS_PER_HOUR = 3_600_000 +const MS_PER_HOUR = 3_600_000 export type Ajv = _Ajv.default // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -235,7 +235,7 @@ const buildSocMeasurandValue = ( const socMinimumValue = socSampledValueTemplate.minimumValue ?? 0 const socSampledValueTemplateValue = isNotEmptyString(socSampledValueTemplate.value) ? getRandomFloatFluctuatedRounded( - Number.parseInt(socSampledValueTemplate.value), + convertToInt(socSampledValueTemplate.value), socSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT ) : randomInt(socMinimumValue, socMaximumValue + 1) @@ -292,7 +292,7 @@ const buildVoltageMeasurandValue = ( } const voltageSampledValueTemplateValue = isNotEmptyString(voltageSampledValueTemplate.value) - ? Number.parseInt(voltageSampledValueTemplate.value) + ? convertToInt(voltageSampledValueTemplate.value) : chargingStation.getVoltageOut() const fluctuationPercent = voltageSampledValueTemplate.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT @@ -361,7 +361,7 @@ const addPhaseVoltageToMeterValue = ( let phaseMeasurandValue: number | undefined if (phaseSampledValueTemplate != null) { const templateValue = isNotEmptyString(phaseSampledValueTemplate.value) - ? Number.parseInt(phaseSampledValueTemplate.value) + ? convertToInt(phaseSampledValueTemplate.value) : nominalVoltage phaseMeasurandValue = getRandomFloatFluctuatedRounded( templateValue, @@ -401,7 +401,7 @@ const buildEnergyMeasurandValue = ( const connectorMaximumAvailablePower = chargingStation.getConnectorMaximumAvailablePower(connectorId) const connectorMaximumEnergyRounded = roundTo( - (connectorMaximumAvailablePower * interval) / MILLISECONDS_PER_HOUR, + (connectorMaximumAvailablePower * interval) / MS_PER_HOUR, 2 ) const connectorMinimumEnergyRounded = roundTo(energyTemplate.minimumValue ?? 0, 2) @@ -1155,7 +1155,7 @@ export const buildMeterValue = ( const connectorMaximumAvailablePower = chargingStation.getConnectorMaximumAvailablePower(connectorId) const connectorMaximumEnergyRounded = roundTo( - (connectorMaximumAvailablePower * interval) / MILLISECONDS_PER_HOUR, + (connectorMaximumAvailablePower * interval) / MS_PER_HOUR, 2 ) const connectorMinimumEnergyRounded = roundTo(energyMeasurand.template.minimumValue ?? 0, 2) diff --git a/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts b/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts index 039be166..ca4a5c5b 100644 --- a/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts +++ b/src/charging-station/ocpp/auth/cache/InMemoryAuthCache.ts @@ -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, roundTo, truncateId } from '../../../../utils/index.js' +import { Constants, isEmpty, logger, roundTo, truncateId } from '../../../../utils/index.js' import { AuthorizationStatus } from '../types/AuthTypes.js' const moduleName = 'InMemoryAuthCache' @@ -429,7 +429,7 @@ export class InMemoryAuthCache implements AuthCache { * Evict least recently used entry */ private evictLRU (): void { - if (this.lruOrder.size === 0) { + if (isEmpty(this.lruOrder)) { return } diff --git a/src/charging-station/ui-server/AbstractUIServer.ts b/src/charging-station/ui-server/AbstractUIServer.ts index 04d4d5ab..752da653 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -26,7 +26,7 @@ import { UIServiceFactory } from './ui-services/UIServiceFactory.js' import { createRateLimiter, DEFAULT_RATE_LIMIT, - DEFAULT_RATE_WINDOW, + DEFAULT_RATE_WINDOW_MS, isValidCredential, } from './UIServerSecurity.js' import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js' @@ -70,7 +70,7 @@ export abstract class AbstractUIServer { ) } this.responseHandlers = new Map() - this.rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW) + this.rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW_MS) this.uiServices = new Map() } diff --git a/src/charging-station/ui-server/UIHttpServer.ts b/src/charging-station/ui-server/UIHttpServer.ts index 8dce2ca3..3e79fdde 100644 --- a/src/charging-station/ui-server/UIHttpServer.ts +++ b/src/charging-station/ui-server/UIHttpServer.ts @@ -23,8 +23,8 @@ import { generateUUID, getErrorMessage, JSONStringify, logger } from '../../util import { AbstractUIServer } from './AbstractUIServer.js' import { createBodySizeLimiter, - DEFAULT_COMPRESSION_THRESHOLD, - DEFAULT_MAX_PAYLOAD_SIZE, + DEFAULT_COMPRESSION_THRESHOLD_BYTES, + DEFAULT_MAX_PAYLOAD_SIZE_BYTES, } from './UIServerSecurity.js' import { HttpMethod, isProtocolAndVersionSupported } from './UIServerUtils.js' @@ -62,7 +62,7 @@ export class UIHttpServer extends AbstractUIServer { const body = JSONStringify(payload, undefined, MapStringifyFormat.object) const shouldCompress = this.acceptsGzip.get(uuid) === true && - Buffer.byteLength(body) >= DEFAULT_COMPRESSION_THRESHOLD + Buffer.byteLength(body) >= DEFAULT_COMPRESSION_THRESHOLD_BYTES if (shouldCompress) { res.writeHead(this.responseStatusToStatusCode(payload.status), { @@ -179,7 +179,7 @@ export class UIHttpServer extends AbstractUIServer { } const bodyBuffer: Uint8Array[] = [] - const checkBodySize = createBodySizeLimiter(DEFAULT_MAX_PAYLOAD_SIZE) + const checkBodySize = createBodySizeLimiter(DEFAULT_MAX_PAYLOAD_SIZE_BYTES) req .on('data', (chunk: Uint8Array) => { if (!checkBodySize(chunk.length)) { diff --git a/src/charging-station/ui-server/UIMCPServer.ts b/src/charging-station/ui-server/UIMCPServer.ts index 1045e49e..cbeaea3e 100644 --- a/src/charging-station/ui-server/UIMCPServer.ts +++ b/src/charging-station/ui-server/UIMCPServer.ts @@ -22,7 +22,13 @@ import { type UIServerConfiguration, type UUIDv4, } from '../../types/index.js' -import { generateUUID, getErrorMessage, isNotEmptyArray, logger } from '../../utils/index.js' +import { + generateUUID, + getErrorMessage, + isEmpty, + isNotEmptyArray, + logger, +} from '../../utils/index.js' import { AbstractUIServer } from './AbstractUIServer.js' import { mcpToolSchemas, @@ -31,7 +37,7 @@ import { registerMCPResources, registerMCPSchemaResources, } from './mcp/index.js' -import { DEFAULT_MAX_PAYLOAD_SIZE } from './UIServerSecurity.js' +import { DEFAULT_MAX_PAYLOAD_SIZE_BYTES } from './UIServerSecurity.js' import { HttpMethod } from './UIServerUtils.js' const moduleName = 'UIMCPServer' @@ -284,7 +290,7 @@ export class UIMCPServer extends AbstractUIServer { } private injectOcppJsonSchemas (mcpServer: McpServer): void { - if (this.ocppSchemaCache.size === 0) { + if (isEmpty(this.ocppSchemaCache)) { return } // Access MCP SDK internal handler map — pinned to @modelcontextprotocol/sdk@~1.29.x @@ -439,7 +445,7 @@ export class UIMCPServer extends AbstractUIServer { cache.set(procedureName, entry) } } - if (cache.size > 0) { + if (!isEmpty(cache)) { logger.info( `${this.logPrefix(moduleName, 'loadOcppSchemas')} OCPP JSON schema injection enabled for ${cache.size.toString()} tool(s)` ) @@ -452,7 +458,7 @@ export class UIMCPServer extends AbstractUIServer { let received = 0 for await (const chunk of req) { received += (chunk as Buffer).length - if (received > DEFAULT_MAX_PAYLOAD_SIZE) { + if (received > DEFAULT_MAX_PAYLOAD_SIZE_BYTES) { throw new BaseError('Payload too large') } chunks.push(chunk as Buffer) diff --git a/src/charging-station/ui-server/UIServerSecurity.ts b/src/charging-station/ui-server/UIServerSecurity.ts index fa5b9a83..27c545ea 100644 --- a/src/charging-station/ui-server/UIServerSecurity.ts +++ b/src/charging-station/ui-server/UIServerSecurity.ts @@ -5,12 +5,12 @@ interface RateLimitEntry { resetTime: number } -export const DEFAULT_MAX_PAYLOAD_SIZE = 1048576 +export const DEFAULT_MAX_PAYLOAD_SIZE_BYTES = 1048576 export const DEFAULT_RATE_LIMIT = 100 -export const DEFAULT_RATE_WINDOW = 60000 +export const DEFAULT_RATE_WINDOW_MS = 60000 export const DEFAULT_MAX_STATIONS = 100 export const DEFAULT_MAX_TRACKED_IPS = 10000 -export const DEFAULT_COMPRESSION_THRESHOLD = 1024 +export const DEFAULT_COMPRESSION_THRESHOLD_BYTES = 1024 export const isValidCredential = (provided: string, expected: string): boolean => { try { diff --git a/src/charging-station/ui-server/UIWebSocketServer.ts b/src/charging-station/ui-server/UIWebSocketServer.ts index 51b39b70..4684560a 100644 --- a/src/charging-station/ui-server/UIWebSocketServer.ts +++ b/src/charging-station/ui-server/UIWebSocketServer.ts @@ -22,7 +22,10 @@ import { validateUUID, } from '../../utils/index.js' import { AbstractUIServer } from './AbstractUIServer.js' -import { DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_MAX_PAYLOAD_SIZE } from './UIServerSecurity.js' +import { + DEFAULT_COMPRESSION_THRESHOLD_BYTES, + DEFAULT_MAX_PAYLOAD_SIZE_BYTES, +} from './UIServerSecurity.js' import { getProtocolAndVersion, handleProtocols, @@ -43,14 +46,14 @@ export class UIWebSocketServer extends AbstractUIServer { super(uiServerConfiguration, bootstrap) this.webSocketServer = new WebSocketServer({ handleProtocols, - maxPayload: DEFAULT_MAX_PAYLOAD_SIZE, + maxPayload: DEFAULT_MAX_PAYLOAD_SIZE_BYTES, noServer: true, perMessageDeflate: { clientNoContextTakeover: true, concurrencyLimit: 10, serverMaxWindowBits: 12, serverNoContextTakeover: true, - threshold: DEFAULT_COMPRESSION_THRESHOLD, + threshold: DEFAULT_COMPRESSION_THRESHOLD_BYTES, zlibDeflateOptions: { chunkSize: 16 * 1024, level: 6, diff --git a/src/worker/WorkerConstants.ts b/src/worker/WorkerConstants.ts index b7460257..2a25f1c8 100644 --- a/src/worker/WorkerConstants.ts +++ b/src/worker/WorkerConstants.ts @@ -8,7 +8,7 @@ export const EMPTY_FUNCTION = Object.freeze(() => { /* This is intentional */ }) -export const workerSetVersion = '1.0.1' +export const WORKER_SET_VERSION = '1.0.1' export const DEFAULT_ELEMENT_ADD_DELAY_MS = 0 export const DEFAULT_WORKER_START_DELAY_MS = 500 diff --git a/src/worker/WorkerSet.ts b/src/worker/WorkerSet.ts index b8e4ec24..7fd618b4 100644 --- a/src/worker/WorkerSet.ts +++ b/src/worker/WorkerSet.ts @@ -5,7 +5,11 @@ import { EventEmitterAsyncResource } from 'node:events' import { SHARE_ENV, Worker } from 'node:worker_threads' import { WorkerAbstract } from './WorkerAbstract.js' -import { DEFAULT_ELEMENTS_PER_WORKER, EMPTY_FUNCTION, workerSetVersion } from './WorkerConstants.js' +import { + DEFAULT_ELEMENTS_PER_WORKER, + EMPTY_FUNCTION, + WORKER_SET_VERSION, +} from './WorkerConstants.js' import { type SetInfo, type UUIDv4, @@ -37,7 +41,7 @@ export class WorkerSet extends Worke size: this.size, started: this.started, type: 'set', - version: workerSetVersion, + version: WORKER_SET_VERSION, worker: 'thread', } } diff --git a/tests/charging-station/ChargingStationTestConstants.ts b/tests/charging-station/ChargingStationTestConstants.ts index 06baa4d7..4560d1d2 100644 --- a/tests/charging-station/ChargingStationTestConstants.ts +++ b/tests/charging-station/ChargingStationTestConstants.ts @@ -16,7 +16,7 @@ export const TEST_CHARGING_STATION_BASE_NAME = 'CS-TEST' export const TEST_CHARGING_STATION_HASH_ID = 'cs-test-hash-001' /** - * Timer Intervals (seconds) + * Timer Intervals * Test values for timing-related configuration and expectations */ export const TEST_HEARTBEAT_INTERVAL_SECONDS = 60 diff --git a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-ReconnectDelay.test.ts b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-ReconnectDelay.test.ts index 0f795840..890691e7 100644 --- a/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-ReconnectDelay.test.ts +++ b/tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-ReconnectDelay.test.ts @@ -22,8 +22,8 @@ import { TEST_CHARGING_STATION_BASE_NAME } from '../../ChargingStationTestConsta import { createMockChargingStation } from '../../ChargingStationTestUtils.js' import { upsertConfigurationKey } from './OCPP20TestUtils.js' -const DEFAULT_WAIT_MINIMUM_S = 30 -const DEFAULT_RANDOM_RANGE_S = 10 +const DEFAULT_WAIT_MINIMUM_SECONDS = 30 +const DEFAULT_RANDOM_RANGE_SECONDS = 10 const DEFAULT_REPEAT_TIMES = 5 const MS_PER_SECOND = 1000 @@ -116,8 +116,8 @@ await describe('OCPP20ServiceUtils.computeReconnectDelay', async () => { const delay = OCPP20ServiceUtils.computeReconnectDelay(station, retryCount) // Assert — retryCount=1 → effectiveRetry=0 → baseDelay = 30s * 2^0 = 30000ms - const expectedBaseDelayMs = DEFAULT_WAIT_MINIMUM_S * MS_PER_SECOND - const maxJitterMs = DEFAULT_RANDOM_RANGE_S * MS_PER_SECOND + const expectedBaseDelayMs = DEFAULT_WAIT_MINIMUM_SECONDS * MS_PER_SECOND + const maxJitterMs = DEFAULT_RANDOM_RANGE_SECONDS * MS_PER_SECOND assert.ok(delay >= expectedBaseDelayMs, 'delay should be >= default base') assert.ok(delay < expectedBaseDelayMs + maxJitterMs, 'delay should be < default base + jitter') }) @@ -132,8 +132,9 @@ await describe('OCPP20ServiceUtils.computeReconnectDelay', async () => { // Assert — both capped: effectiveRetry = min(retryCount-1, repeatTimes) = 5 // baseDelay = 30s * 2^5 = 960000ms - const cappedBaseDelayMs = DEFAULT_WAIT_MINIMUM_S * MS_PER_SECOND * 2 ** DEFAULT_REPEAT_TIMES - const maxJitterMs = DEFAULT_RANDOM_RANGE_S * MS_PER_SECOND + const cappedBaseDelayMs = + DEFAULT_WAIT_MINIMUM_SECONDS * MS_PER_SECOND * 2 ** DEFAULT_REPEAT_TIMES + const maxJitterMs = DEFAULT_RANDOM_RANGE_SECONDS * MS_PER_SECOND assert.ok(delayBeyondCap >= cappedBaseDelayMs, 'beyond-cap delay should be >= capped base') assert.ok( delayBeyondCap < cappedBaseDelayMs + maxJitterMs, diff --git a/tests/charging-station/ui-server/UIHttpServer.test.ts b/tests/charging-station/ui-server/UIHttpServer.test.ts index 906681c7..7ca34ca3 100644 --- a/tests/charging-station/ui-server/UIHttpServer.test.ts +++ b/tests/charging-station/ui-server/UIHttpServer.test.ts @@ -10,7 +10,7 @@ import { gunzipSync } from 'node:zlib' import type { UIServerConfiguration, UUIDv4 } from '../../../src/types/index.js' import { UIHttpServer } from '../../../src/charging-station/ui-server/UIHttpServer.js' -import { DEFAULT_COMPRESSION_THRESHOLD } from '../../../src/charging-station/ui-server/UIServerSecurity.js' +import { DEFAULT_COMPRESSION_THRESHOLD_BYTES } from '../../../src/charging-station/ui-server/UIServerSecurity.js' import { ApplicationProtocol, ResponseStatus } from '../../../src/types/index.js' import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' import { GZIP_STREAM_FLUSH_DELAY_MS, TEST_UUID } from './UIServerTestConstants.js' @@ -49,7 +49,7 @@ const createHttpServerConfig = () => createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP }) const createLargePayload = (status: ResponseStatus = ResponseStatus.SUCCESS) => ({ - data: 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD + 100), + data: 'x'.repeat(DEFAULT_COMPRESSION_THRESHOLD_BYTES + 100), status, }) diff --git a/tests/charging-station/ui-server/UIMCPServer.test.ts b/tests/charging-station/ui-server/UIMCPServer.test.ts index 3c5df4bc..ce84b8b5 100644 --- a/tests/charging-station/ui-server/UIMCPServer.test.ts +++ b/tests/charging-station/ui-server/UIMCPServer.test.ts @@ -25,7 +25,7 @@ import { } from '../../../src/charging-station/ui-server/mcp/index.js' import { AbstractUIService } from '../../../src/charging-station/ui-server/ui-services/AbstractUIService.js' import { UIMCPServer } from '../../../src/charging-station/ui-server/UIMCPServer.js' -import { DEFAULT_MAX_PAYLOAD_SIZE } from '../../../src/charging-station/ui-server/UIServerSecurity.js' +import { DEFAULT_MAX_PAYLOAD_SIZE_BYTES } from '../../../src/charging-station/ui-server/UIServerSecurity.js' import { BaseError } from '../../../src/exception/index.js' import { ApplicationProtocol, @@ -671,7 +671,7 @@ await describe('UIMCPServer', async () => { }) await it('should reject with BaseError when payload too large', async () => { - const oversizedChunk = Buffer.alloc(DEFAULT_MAX_PAYLOAD_SIZE + 1) + const oversizedChunk = Buffer.alloc(DEFAULT_MAX_PAYLOAD_SIZE_BYTES + 1) const mockReq = Readable.from([oversizedChunk]) await assert.rejects( diff --git a/tests/ocpp-server/server.py b/tests/ocpp-server/server.py index 0ebab661..e8f88bc9 100644 --- a/tests/ocpp-server/server.py +++ b/tests/ocpp-server/server.py @@ -58,11 +58,23 @@ logger = logging.getLogger(__name__) # Server defaults DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 9000 -DEFAULT_HEARTBEAT_INTERVAL = 60 +DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 60 +DEFAULT_MESSAGE_TIMEOUT_SECONDS = 30 DEFAULT_TOTAL_COST = 10.0 +DEFAULT_SECURITY_PROFILE = 0 +DEFAULT_CONFIG_SLOT = 1 +DEFAULT_EVSE_ID = 1 +DEFAULT_CONNECTOR_ID = 1 +DEFAULT_OCPP_CSMS_URL = "ws://127.0.0.1:9000" +DEFAULT_TEST_TOKEN = "test_token" # noqa: S105 +DEFAULT_TOKEN_TYPE = "ISO14443" # noqa: S105 +DEFAULT_VENDOR_ID = "TestVendor" +DEFAULT_FIRMWARE_URL = "https://example.com/firmware/v2.0.bin" +DEFAULT_LOG_URL = "https://example.com/logs" +DEFAULT_CUSTOMER_ID = "test_customer_001" FALLBACK_TRANSACTION_ID = "test_transaction_123" MAX_REQUEST_ID = 2**31 - 1 -SHUTDOWN_TIMEOUT = 30.0 +SHUTDOWN_TIMEOUT_SECONDS = 30.0 SUBPROTOCOLS: list[websockets.Subprotocol] = [ websockets.Subprotocol("ocpp2.0"), websockets.Subprotocol("ocpp2.0.1"), @@ -236,7 +248,7 @@ class ChargePoint(ocpp.v201.ChargePoint): self._boot_index[0] = idx + 1 return ocpp.v201.call_result.BootNotification( current_time=datetime.now(timezone.utc).isoformat(), - interval=DEFAULT_HEARTBEAT_INTERVAL, + interval=DEFAULT_HEARTBEAT_INTERVAL_SECONDS, status=status, ) @@ -442,8 +454,8 @@ class ChargePoint(ocpp.v201.ChargePoint): async def _send_request_start_transaction(self): request = ocpp.v201.call.RequestStartTransaction( - id_token={"id_token": "test_token", "type": "ISO14443"}, - evse_id=1, + id_token={"id_token": DEFAULT_TEST_TOKEN, "type": DEFAULT_TOKEN_TYPE}, + evse_id=DEFAULT_EVSE_ID, remote_start_id=_random_request_id(), ) await self.call(request, suppress=False) @@ -468,7 +480,9 @@ class ChargePoint(ocpp.v201.ChargePoint): await self._call_and_log(request, Action.reset, ResetStatusEnumType.accepted) async def _send_unlock_connector(self): - request = ocpp.v201.call.UnlockConnector(evse_id=1, connector_id=1) + request = ocpp.v201.call.UnlockConnector( + evse_id=DEFAULT_EVSE_ID, connector_id=DEFAULT_CONNECTOR_ID + ) await self._call_and_log( request, Action.unlock_connector, UnlockStatusEnumType.unlocked ) @@ -493,7 +507,7 @@ class ChargePoint(ocpp.v201.ChargePoint): async def _send_data_transfer(self): request = ocpp.v201.call.DataTransfer( - vendor_id="TestVendor", message_id="TestMessage", data="test_data" + vendor_id=DEFAULT_VENDOR_ID, message_id="TestMessage", data="test_data" ) await self._call_and_log( request, Action.data_transfer, DataTransferStatusEnumType.accepted @@ -519,7 +533,7 @@ class ChargePoint(ocpp.v201.ChargePoint): request_id=_random_request_id(), report=True, clear=False, - customer_identifier="test_customer_001", + customer_identifier=DEFAULT_CUSTOMER_ID, ) await self._call_and_log( request, @@ -554,7 +568,7 @@ class ChargePoint(ocpp.v201.ChargePoint): async def _send_get_log(self): request = ocpp.v201.call.GetLog( - log={"remote_location": "https://example.com/logs"}, + log={"remote_location": DEFAULT_LOG_URL}, log_type=LogEnumType.diagnostics_log, request_id=_random_request_id(), ) @@ -589,13 +603,13 @@ class ChargePoint(ocpp.v201.ChargePoint): async def _send_set_network_profile(self): request = ocpp.v201.call.SetNetworkProfile( - configuration_slot=1, + configuration_slot=DEFAULT_CONFIG_SLOT, connection_data={ "ocpp_version": "OCPP20", "ocpp_transport": "JSON", - "ocpp_csms_url": "ws://127.0.0.1:9000", - "message_timeout": 30, - "security_profile": 0, + "ocpp_csms_url": DEFAULT_OCPP_CSMS_URL, + "message_timeout": DEFAULT_MESSAGE_TIMEOUT_SECONDS, + "security_profile": DEFAULT_SECURITY_PROFILE, "ocpp_interface": "Wired0", }, ) @@ -609,7 +623,7 @@ class ChargePoint(ocpp.v201.ChargePoint): request = ocpp.v201.call.UpdateFirmware( request_id=_random_request_id(), firmware={ - "location": "https://example.com/firmware/v2.0.bin", + "location": DEFAULT_FIRMWARE_URL, "retrieve_date_time": datetime.now(timezone.utc).isoformat(), }, ) @@ -1099,13 +1113,13 @@ async def main(): await shutdown_event.wait() try: - async with asyncio.timeout(SHUTDOWN_TIMEOUT): + async with asyncio.timeout(SHUTDOWN_TIMEOUT_SECONDS): await server.wait_closed() except TimeoutError: logger.warning( "Shutdown timed out after %.0fs" " — connections may not have closed cleanly", - SHUTDOWN_TIMEOUT, + SHUTDOWN_TIMEOUT_SECONDS, ) logger.info("Server shutdown complete") diff --git a/tests/ocpp-server/test_server.py b/tests/ocpp-server/test_server.py index 8ae4699f..2a55cc2b 100644 --- a/tests/ocpp-server/test_server.py +++ b/tests/ocpp-server/test_server.py @@ -43,7 +43,7 @@ from ocpp.v201.enums import ( ) from server import ( - DEFAULT_HEARTBEAT_INTERVAL, + DEFAULT_HEARTBEAT_INTERVAL_SECONDS, DEFAULT_TOTAL_COST, FALLBACK_TRANSACTION_ID, MAX_REQUEST_ID, @@ -439,7 +439,7 @@ class TestBootNotificationHandler: reason="PowerUp", ) assert response.status == RegistrationStatusEnumType.accepted - assert response.interval == DEFAULT_HEARTBEAT_INTERVAL + assert response.interval == DEFAULT_HEARTBEAT_INTERVAL_SECONDS assert isinstance(response.current_time, str) assert "T" in response.current_time @@ -544,7 +544,7 @@ class TestBootNotificationHandler: reason="PowerUp", ) assert response.status == RegistrationStatusEnumType.accepted - assert response.interval == DEFAULT_HEARTBEAT_INTERVAL + assert response.interval == DEFAULT_HEARTBEAT_INTERVAL_SECONDS class TestHeartbeatHandler: diff --git a/ui/web/src/App.vue b/ui/web/src/App.vue index 0bcfc609..6eb0816f 100644 --- a/ui/web/src/App.vue +++ b/ui/web/src/App.vue @@ -1,7 +1,7 @@