Jérôme Benoit [Sat, 28 Mar 2026 14:21:35 +0000 (15:21 +0100)]
refactor: move ConfigurationValueSize, ReportingValueSize, ValueSize to OCPP20OptionalVariableName
Per OCPP 2.0.1 appendices CSV (dm_components_vars.csv), these three
DeviceDataCtrlr variables are required=no. Move them from
OCPP20RequiredVariableName to OCPP20OptionalVariableName to match
the spec classification.
Jérôme Benoit [Sat, 28 Mar 2026 14:13:11 +0000 (15:13 +0100)]
refactor: add SimulateSignatureVerificationFailure to OCPP20VendorVariableName enum
Replace string literal with enum reference in OCPP20VariableRegistry
and OCPP20IncomingRequestService for type safety and consistency with
CertificatePrivateKey and ConnectionUrl vendor variables.
Jérôme Benoit [Sat, 28 Mar 2026 13:20:30 +0000 (14:20 +0100)]
refactor: use OCPP20RequiredVariableName for measurands in OCPP20ServiceUtils
Replace StandardParametersKey with OCPP20RequiredVariableName for
TxStartedMeasurands and TxEndedMeasurands in buildTransaction*MeterValues.
Remove unused StandardParametersKey import.
Jérôme Benoit [Sat, 28 Mar 2026 12:29:21 +0000 (13:29 +0100)]
refactor: move debug param to last position in buildMeterValue
Reorder buildMeterValue signature so debug is the last optional
parameter, consistent with all other validate*MeasurandValue functions.
Removes need to pass explicit false to reach measurandsKey and context.
Jérôme Benoit [Sat, 28 Mar 2026 12:05:34 +0000 (13:05 +0100)]
refactor: replace Measurands string literal with OCPP20RequiredVariableName enum
Add Measurands to OCPP20RequiredVariableName enum (required per OCPP
2.0.1 Part 2 §2.7.1, AlignedDataCtrlr component) and replace all string
literals in ConfigurationKeyUtils mapping and OCPP20VariableRegistry.
Jérôme Benoit [Sat, 28 Mar 2026 00:17:49 +0000 (01:17 +0100)]
fix: correct WebSocketPingInterval documentation and registry default
- Remove vendor-specific ChargingStation.WebSocketPingInterval entry
from README (already listed under OCPPCommCtrlr per OCPP 2.0.1 spec)
- Use Constants.DEFAULT_WEBSOCKET_PING_INTERVAL in OCPPCommCtrlr registry
entry instead of hardcoded '30' for single source of truth
Jérôme Benoit [Sat, 28 Mar 2026 00:09:34 +0000 (01:09 +0100)]
fix: map WebSocketPingInterval to OCPPCommCtrlr per OCPP 2.0.1 spec
ChargingStation.WebSocketPingInterval is vendor-specific. The canonical
variable per OCPP 2.0.1 Part 4 §8.4 is OCPPCommCtrlr.WebSocketPingInterval.
Update both the 1.6→2.0 mapping and the keba-ocpp2 template.
Jérôme Benoit [Sat, 28 Mar 2026 00:02:08 +0000 (01:02 +0100)]
test: add whitespace-padded value coverage for convertToBoolean
Align test coverage across root simulator and web UI components for
trim handling of ' true ', ' 1 ', ' false ', ' TRUE ', 'True', 'FALSE',
and numeric 2.
Jérôme Benoit [Fri, 27 Mar 2026 23:50:22 +0000 (00:50 +0100)]
test: restore and harmonize OCPP 2.0 transaction meter value assertions
- Add symmetric buildTransactionEndedMeterValues tests via requestStopTransaction
(context=Transaction.End, measurand=Energy, no config key, no EVSE template)
- Strengthen buildTransactionStartedMeterValues assertions with strict
context and measurand checks using OCPP enum constants
- Refactor Started tests: shared station setup in beforeEach, remove duplication
- Configure TxEndedMeasurands in deauthorization test for real coverage
- Remove comments that restate test names
Jérôme Benoit [Fri, 27 Mar 2026 23:13:03 +0000 (00:13 +0100)]
feat: support configurable measurands per transaction stage in OCPP 2.0
- Thread measurandsKey and context params through the meter value pipeline
(getSampledValueTemplate, build*MeasurandValue, voltage helpers)
- buildTransactionStartedMeterValues uses SampledDataCtrlr.TxStartedMeasurands
with Transaction.Begin context
- buildTransactionEndedMeterValues uses SampledDataCtrlr.TxEndedMeasurands
with Transaction.End context
- Add OCPP 1.6→2.0 mappings for MeterValuesAlignedData,
ClockAlignedDataInterval, StopTxnSampledData, StopTxnAlignedData
- Reorder getSampledValueTemplate params: measurandsKey before measurand
- Log warn (not debug) when meter value building fails in transaction events
- Add .trim() to convertToBoolean for whitespace-padded values
Jérôme Benoit [Fri, 27 Mar 2026 21:40:34 +0000 (22:40 +0100)]
fix: use case-insensitive boolean parsing for OCPP configuration values
- 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
Jérôme Benoit [Fri, 27 Mar 2026 20:50:43 +0000 (21:50 +0100)]
refactor: harmonize errMsg → errorMsg for intra-file naming consistency
OCPPServiceUtils.ts used both errMsg (4x) and errorMsg (1x) for the
same semantic. PerformanceStatistics.ts used errMsg (4x) while the
rest of the codebase uses errorMsg. Align to errorMsg everywhere.
Jérôme Benoit [Fri, 27 Mar 2026 19:13:36 +0000 (20:13 +0100)]
refactor: use Number.isNaN() instead of global isNaN()
Replace global isNaN() with Number.isNaN() for stricter type checking.
Global isNaN() coerces the argument to a number first, which can produce
unexpected results for non-numeric strings.
Jérôme Benoit [Fri, 27 Mar 2026 19:10:32 +0000 (20:10 +0100)]
fix: replace incorrect zero fallbacks for maximumPower and maximumAmperage
Station physical properties (maximumPower, maximumAmperage) are validated
> 0 at startup. Using ?? 0 as fallback silently produces invalid states:
0W power limits blocking all charging, 0A amperage in config keys.
Jérôme Benoit [Fri, 27 Mar 2026 18:50:01 +0000 (19:50 +0100)]
refactor: eliminate all non-null assertion suppressions with proper null guards
Remove 104 eslint-disable-next-line @typescript-eslint/no-non-null-assertion
suppressions across 23 files by replacing non-null assertions (!) with
semantically correct alternatives: local variable extraction with null
guards, optional chaining (?.), nullish coalescing (??) with proper
default constants, and error logging for invalid states.
Key improvements:
- Use existing DEFAULT_POOL_MAX_SIZE, DEFAULT_POOL_MIN_SIZE, and
DEFAULT_ELEMENTS_PER_WORKER constants instead of hardcoded fallbacks
- Add proper error logging for powerDivider undefined/invalid states
instead of silently computing wrong values
- Add connectorId null guard with throw in handleMeterValues instead of
silently defaulting to connector 0
- Ensure .finally() always sends a response to prevent caller hangs
- Align variable naming with codebase conventions (connectorStatus,
templateStatistics, commandStatisticsData, entryStatisticsData)
Jérôme Benoit [Fri, 27 Mar 2026 14:30:15 +0000 (15:30 +0100)]
refactor: type buildTransactionEvent with OCPP20TransactionEventOptions
Replace the untyped Record<string, unknown> cast in buildTransactionEvent
with the proper OCPP20TransactionEventOptions parameter type. Add build
hint fields to the options interface. Remove 64 stale as-unknown-as casts
from transaction event tests.
Widen buildRegistryKey, buildCaseInsensitiveCompositeKey, and
VariableMetadata.component to accept OCPP20ComponentName and VariableName
enums directly, eliminating 429 as-string casts across the registry and
variable manager. 8 casts retained on enum comparison lines required by
no-unsafe-enum-comparison lint rule.
Jérôme Benoit [Fri, 27 Mar 2026 13:23:14 +0000 (14:23 +0100)]
refactor: consolidate enforceMessageLimits types with generic R and RejectionReason
- Add generic type parameter R to enforceMessageLimits and
enforcePostCalculationBytesLimit, eliminating 4 'as typeof' casts
- Extract RejectionReason interface (additionalInfo + reasonCode) to
replace inline anonymous types, aligned with StatusInfoType fields
- Remove all enum→string→enum round-trips in callers
Jérôme Benoit [Fri, 27 Mar 2026 12:55:08 +0000 (13:55 +0100)]
fix: type buildRejected reasonCode as ReasonCodeEnumType instead of string
Eliminates the pointless enum→string→enum round-trip: the callback
parameter is now typed as ReasonCodeEnumType directly, removing the
as-string casts at call sites and the reverse keyof-typeof reconversion
in callers.
Add LocalAuthListCtrlr.Enabled, ReservationCtrlr.Enabled/NonEvseSpecific,
TxCtrlr.EVConnectionTimeOut, AuthCtrlr.LocalAuthorizationOffline to
keba-ocpp2 template — previously impossible due to key collisions.
- Add LocalAuthListEnabled → LocalAuthListCtrlr.Enabled key remapping
- Add ReserveConnectorZeroSupported → ReservationCtrlr.NonEvseSpecific remapping
- Remove AuthCtrlr.Enabled from template (no application consumer)
- Add NonEvseSpecific, MaxEnergyOnInvalidId to OCPP20OptionalVariableName enum
- Replace string literals with enum refs in registry and service utils
- Add tests for the 2 new key remappings
Jérôme Benoit [Fri, 27 Mar 2026 11:31:06 +0000 (12:31 +0100)]
fix: use Component.Variable[.Instance] key format for OCPP 2.0 variable persistence
The previous format used bare variable names (e.g., 'Enabled'), causing
collisions across components (9 components define Enabled, 8+ define
Available, etc.). Now uses the spec's (Component, Variable, Instance)
triplet as the persisted configuration key.
- Rewrite computeConfigurationKeyName to return Component.Variable format
- Remove shouldFlattenInstance (no longer needed)
- Extract buildConfigKey helper in ConfigurationKeyUtils, export via barrel
- Add MaxCertificateChainSize to OCPP20OptionalVariableName enum
- Update all direct config key lookups with component prefix
- Update keba-ocpp2 template keys to new format
- Update OCPP2_PARAMETER_KEY_MAP resolved values
- Replace all enum string literals in log messages with enum references
- Update all tests to use new key format
Jérôme Benoit [Thu, 26 Mar 2026 22:55:28 +0000 (23:55 +0100)]
fix: remove OrganizationName from ISO15118Ctrlr (spec says SecurityCtrlr only)
Per OCPP 2.0.1 appendix section 3.1.15, OrganizationName belongs to
SecurityCtrlr only. The duplicate entry under ISO15118Ctrlr was not
in the spec and shadowed the SecurityCtrlr default value.
Jérôme Benoit [Thu, 26 Mar 2026 22:25:20 +0000 (23:25 +0100)]
refactor: add Enabled to OCPP20RequiredVariableName enum and fix LocalAuthListCtrlr.Enabled mapping
Replace all 'Enabled' string literals with OCPP20RequiredVariableName.Enabled
in the variable registry and auth adapter. Move LocalAuthListEnabled
default from AuthCtrlr to LocalAuthListCtrlr component per OCPP 2.0.1 spec.
Jérôme Benoit [Thu, 26 Mar 2026 22:09:05 +0000 (23:09 +0100)]
fix: align OCPP 2.0 variable names to spec (LocalPreAuthorization, LocalAuthorizationOffline)
Rename LocalPreAuthorize → LocalPreAuthorization and
LocalAuthorizeOffline → LocalAuthorizationOffline to match the OCPP
2.0.1 appendix. Add key resolution mapping for both. Replace string
literals with enum references in OCPP20AuthAdapter.
Jérôme Benoit [Thu, 26 Mar 2026 20:35:50 +0000 (21:35 +0100)]
fix: use OCPP version-specific parameter keys for meter value measurands
Remap keba-ocpp2 template configuration keys from OCPP 1.6 names to
OCPP 2.0 variable names (MeterValuesSampledData → TxUpdatedMeasurands,
MeterValueSampleInterval → TxUpdatedInterval, AuthorizeRemoteTxRequests
→ AuthorizeRemoteStart). Remove OCPP 1.6-only keys with no 2.0
equivalent. Use StandardParametersKey union to resolve the correct
key at runtime based on OCPP version.
Jérôme Benoit [Thu, 26 Mar 2026 20:00:03 +0000 (21:00 +0100)]
docs: list OCPP 2.0 device model variables by component in README
Replace the incorrect GetVariables/SetVariables command listing with
141 device model variables across 19 components extracted from
OCPP20VariableRegistry.ts. Vendor-specific variables are marked.
Matches the structure used for OCPP 1.6 configuration keys.
- Add test for InvalidSignature path when SimulateSignatureVerificationFailure is true
- Add test for normal path when SimulateSignatureVerificationFailure is false (TDD red phase)
- Tests verify lifecycle termination and SecurityEventNotification type correctness
- Evidence: typecheck & lint pass; tests fail as expected (awaiting Task 3 implementation)
* fix(ocpp2): address PR review feedback on firmware signature verification
- Await sendSecurityEventNotification in failure path for consistency with success path
- Reorder test assertions: check array length before index access
- Add tick-forward verification that lifecycle stops after InvalidSignature
- Remove redundant standardCleanup() calls already handled by afterEach
- Remove firmware signature verification note from README
* fix(ocpp2): improve firmware update spec conformance and test quality
- L01.FR.07: Move EVSE unavailability check inside wait loop for continuous monitoring
- L01.FR.30: Honor retries/retryInterval with simulated download retry cycle
- Merge retry test into existing DownloadFailed test (DRY)
- Add EVSE continuous monitoring lifecycle test (L01.FR.07)
- Fix assertion ordering: length checks before index access
- Remove as-unknown-as-string cast in favor of String()
- Tighten assert.ok to assert.notStrictEqual where applicable
* fix(tests): type CapturedOCPPRequest.command as OCPP20RequestCommand
- Replace string type with OCPP20RequestCommand enum on CapturedOCPPRequest.command
- Remove 6 unnecessary 'as string' casts across UpdateFirmware and TransactionEvent tests
- Replace 'Unavailable' string literal with OCPP20ConnectorStatusEnumType.Unavailable
- Remove unnecessary optional chaining on non-nullable payload
- Complete @param JSDoc for retries/retryInterval on simulateFirmwareUpdateLifecycle
* fix(tests): replace flaky sentRequests assertions with mock.method spy in EVSE monitoring test
Mock sendEvseStatusNotifications directly instead of asserting on fire-and-forget
side effects captured in sentRequests. The internal async chain (sendAndSetConnectorStatus
→ dynamic import → requestHandler) needed non-deterministic flushMicrotasks() stacking.
The spy makes assertions synchronous and CI-deterministic.
Jérôme Benoit [Thu, 26 Mar 2026 16:42:21 +0000 (17:42 +0100)]
fix: respect elementAddDelay by using sequential startup when configured
The perf commit 218fd550 switched to parallel Promise.allSettled for
startup speed, but broke elementAddDelay since all stations launched
simultaneously. Now uses sequential await loop when elementAddDelay > 0
and parallel allSettled when 0 or unset. Removes redundant outer
try/catch — config errors propagate as fatal, station errors are
handled per-station in both paths.
Jérôme Benoit [Thu, 26 Mar 2026 00:02:01 +0000 (01:02 +0100)]
fix: cleanup connector after queued TransactionEvent.Ended replay
Extract cleanupEndedTransaction() in OCPP20ServiceUtils to consolidate
stopPeriodicMeterValues + resetConnectorStatus + unlock + Available
status. Called from both stopTransaction20 (normal flow) and
sendQueuedTransactionEvents (offline replay). Fixes stale Occupied
status and orphaned meter value interval after queued Ended events.
Jérôme Benoit [Wed, 25 Mar 2026 22:15:51 +0000 (23:15 +0100)]
feat: add server-side refresh notification over WebSocket
Bootstrap calls scheduleClientNotification() after each cache mutation.
A 500ms debounce collapses rapid updates into a single broadcast of
ProtocolNotification [ServerNotification.REFRESH] to all connected WS
clients. setChargingStationData/deleteChargingStationData return boolean
so callers only notify on actual change. Frontend responseHandler uses
isServerNotification/isProtocolResponse guards to dispatch messages.
Jérôme Benoit [Wed, 25 Mar 2026 21:20:43 +0000 (22:20 +0100)]
fix: post updated message on connectorStatusChanged to fix stale UI cache
The .finally() in internalSendMessage emits the updated event BEFORE
sendAndSetConnectorStatus updates connectorStatus.status, causing the
UI cache to snapshot the old status. The connectorStatusChanged event
fires AFTER the update, so posting an updated message on it ensures
the cache receives the correct connector status.
Jérôme Benoit [Tue, 24 Mar 2026 18:08:34 +0000 (19:08 +0100)]
fix: prevent shutdown timeout with promiseWithTimeout helper
Extract a shared promiseWithTimeout<T>() utility from 3 divergent
Promise.race+timeout implementations. Use it in ChargingStation.stop()
to cap stopMessageSequence at 30s so the stopped event is always
emitted even when the OCPP server is unreachable, preventing the
60s Bootstrap shutdown timeout.
Also fix mock server SecurityEventNotification handler parameter name
and add OCPP 2.0.1 E2E test plan.
Jérôme Benoit [Tue, 24 Mar 2026 16:54:43 +0000 (17:54 +0100)]
docs: harmonize JSDoc @param/@returns across entire codebase
- Add missing dash separator to @param tags in OCPP20VariableRegistry
and OCPPServiceUtils (35 occurrences)
- Fill empty @param descriptions in Helpers, IdTagsCache,
OCPPRequestService, and Worker modules (23 occurrences)
- Replace type-only @returns with meaningful descriptions
Jérôme Benoit [Tue, 24 Mar 2026 16:45:25 +0000 (17:45 +0100)]
docs: add JSDoc to ChargingStation public API methods
Add concise JSDoc with @param and @returns tags to 29 public methods
following the project's established pattern (param - description).
Resolves all 45 jsdoc/require-param and jsdoc/require-returns warnings.
- Replace throw new Error with BaseError in OCPP20CertificateManager and
OCPP20VariableManager (3 occurrences)
- Move getMessageTypeString from OCPPServiceUtils to Helpers, resolving
the barrel bypass in ErrorUtils — consumers import via charging-station
barrel, tests moved to Helpers.test.ts
- Remove redundant console.warn in Helpers (logger.warn already called)
- Remove unused chalk import from Helpers
- Replace 71 improper assert.ok(comparison) with assert.strictEqual/notStrictEqual
in 10 test files per TEST_STYLE_GUIDE (assert.ok for boolean/existence only)
- Rename lastUpdated → lastUpdatedDate in auth interfaces and strategies
to follow Date suffix naming convention
- Extract hardcoded 'ws://localhost' to OCPP20Constants.DEFAULT_CONNECTION_URL
- Replace setTimeout(resolve, 50) with proper httpServer.close() await
in UIMCPServer integration test
Jérôme Benoit [Tue, 24 Mar 2026 11:47:53 +0000 (12:47 +0100)]
fix(webui): remove deprecated baseUrl for TypeScript 6 compatibility
TS6 deprecated baseUrl (TS5101). Since paths entries already use './'
prefix, they work standalone without baseUrl. Vite resolve.alias handles
runtime resolution independently.
Jérôme Benoit [Tue, 24 Mar 2026 11:07:03 +0000 (12:07 +0100)]
refactor(ocpp-server): harmonize shutdown tests with project conventions
Move fixture near other fixtures, replace _patch_main tuple return with
contextmanager, parametrize SIGINT/SIGTERM, remove dead code helpers,
merge two test classes into one.
Jérôme Benoit [Tue, 24 Mar 2026 10:44:28 +0000 (11:44 +0100)]
feat(ocpp-server): add graceful shutdown with signal handling
Handle SIGINT/SIGTERM for clean WebSocket close (code 1001) to all
connected charge points. Includes shutdown drain timeout, double-signal
force quit, and cross-platform Windows fallback via call_soon_threadsafe.
Verify that every broadcast channel OCPP command has a corresponding
MCP tool schema and OCPP schema mapping entry. Ensures schema coverage
stays in sync with supported commands as a single source of truth guard.
Jérôme Benoit [Tue, 24 Mar 2026 00:43:04 +0000 (01:43 +0100)]
refactor(broadcast-channel): extract response status registries from switch/case
Replace 11 repetitive switch/case blocks in commandResponseToResponseStatus
with two static registries:
- emptyResponseCommands: Set of commands where empty response = success
- acceptedStatusCommands: Map of commands to status check predicates
Custom cases (AUTHORIZE, START/STOP_TRANSACTION, HEARTBEAT,
TRANSACTION_EVENT) remain in switch for version-specific logic.
Adding a new command now requires 1 line in the registry instead of
a 5-line case block.
Replace 20 identical BroadcastChannel handler methods (handleAuthorize,
handleDataTransfer, handleHeartbeat, handleStatusNotification, etc.)
with a single passthrough(RequestCommand) factory method that creates
a CommandHandler forwarding the request payload to requestHandler.
Retain only handlers with custom logic: handleBootNotification (merges
bootNotificationRequest), handleMeterValues (version-specific meter
value building), handleStopTransaction (adds meterStop).
Jérôme Benoit [Tue, 24 Mar 2026 00:16:06 +0000 (01:16 +0100)]
refactor(ocpp): extract buildEmptyMeterValue helper for empty MeterValue construction
Replace 4 inline { sampledValue: [], timestamp: new Date() } constructions
with buildEmptyMeterValue() helper in OCPPServiceUtils and OCPP16ServiceUtils.
* docs: restructure MCP section for agent config, update uiServer type in config table
* feat(ui-server): log deprecation warning when HTTP transport is used
* fix(ui-server): harmonize log levels and message patterns with existing transports
* test(ui-server): align MCP tests with style guide
- Use createLoggerMocks() for log assertions instead of trivial checks
- Add server.stop() to factory fallback tests to prevent resource leaks
- Replace fragile globalThis.clearTimeout override with t.mock.method
* docs: restore PDU acronym definition in WebSocket Protocol section
* fix(ui-server): use UI protocol version for MCP server version
* fix(ui-server): derive log file paths from Configuration instead of hardcoding
* perf(ui-server): tail-read log files instead of loading entire file into memory
* refactor(ui-server): convert log resources to tools with tail parameter
Follow MCP best practices: resources are for static snapshots,
tools are for parameterized on-demand data.
- Replace log://combined and log://error resources with
readCombinedLog and readErrorLog tools
- Add tail parameter (default 200, max 5000) following the
filesystem server head/tail pattern
- Add readOnlyHint annotation per MCP tool spec
- Return totalLines metadata in response for context
- Keep station and template resources (small, static data)
* feat(ui-server): expose OCPP JSON schemas as MCP resource templates
Add schema://ocpp/{version} listing and schema://ocpp/{version}/{command}
resource templates that serve the existing JSON schema files from
src/assets/json-schemas/ocpp/ on demand.
LLM agents can discover available OCPP commands per version and read
the full JSON schema for any command without bloating tools/list.
Schemas are read from disk on demand — no memory duplication with
the AJV validators already loaded by OCPP services.
- Add ocppSchemaMapping linking ProcedureName to JSON Schema files
- 16 OCPP tools now have ocpp16Payload/ocpp20Payload fields with
version affinity in descriptions
- Intercept tools/list to inject full JSON Schemas from disk
into inputSchema.properties (no Zod duplication, DRY)
- Add payload flattening: extract versioned payload, spread flat
- Add mutual exclusivity: reject if both payloads provided
- Add pre-flight version check: clear error on version mismatch
- Runtime guard: graceful fallback if SDK internal API changes
* refactor(ui-server): use OCPPVersion enum instead of string literals
* refactor(ui-server): improve MCP code elegance and project rule compliance
- Replace as-unknown-as cast with Reflect.get() for SDK handler access
- Replace generic Error with BaseError in stop/readRequestBody
- Extract createToolErrorResponse/createToolResponse static helpers (DRY)
- Extract closeTransportSafely for duplicate transport cleanup
- Extract registerLogReadTool factory for duplicate log tools (DRY)
- Remove unnecessary String() conversions in resource handlers
* fix(ui-server): fix body size double-counting and schema resource path
- readRequestBody: pass chunk.length (not accumulated total) to
createBodySizeLimiter — matches UIHttpServer pattern
- getSchemaBaseDir: use correct relative path instead of broken
resolve().includes('assets') condition (always true)
* refactor(ui-server): consolidate version fallback guards and clarify README version docs
* fix(ui-server): harden MCP schema resources against path traversal and fix transport leak
- Add path traversal guard in schema resource handlers to validate resolved
paths stay within the OCPP schema base directory
- Close MCP transport on 405 Method Not Allowed responses to prevent leak
- Fix log file path to use direct path when rotation is disabled
- Fix misleading Heartbeat tool description that referenced unsupported
ocpp payload fields
* fix(ui-server): address remaining review findings and lint warnings
- Reorder stop() to delete-before-reject preventing re-entrant issues
- Use ephemeral port in integration test instead of fixed port 18999
- Document synchronous authenticate() assumption and SDK pattern deviation
- Add 'modelcontextprotocol' to cspell wordlist
- Convert JSDoc to inline comment to fix jsdoc lint warnings
- Use exact pathname match (url.pathname !== '/mcp') instead of
startsWith to prevent unintended paths from reaching the MCP handler
- Pin @modelcontextprotocol/sdk to ~1.27.1 (patch-only) since the
OCPP schema injection relies on SDK internals pinned to 1.27.x
- Add startup log confirming successful OCPP JSON schema injection
* test(ui-server): add comprehensive UIMCPServer unit tests
Cover private method logic missing from initial test suite:
- invokeProcedure: service null, dual payload, version mismatch,
direct response, service error, 30s timeout (mock timers)
- checkVersionCompatibility: OCPP 1.6/2.0/2.0.1 matching, cross-version
errors, hashIds filtering, all-stations fallback
- readRequestBody: valid JSON, payload too large, invalid JSON, stream error
- loadOcppSchemas: disk loading, cache entry validation
Refactor for elegance and guide compliance:
- Centralize Reflect.get wrappers in TestableUIMCPServer class
- Extract assertToolError() helper to eliminate repeated triplet
- Move createMockChargingStationDataWithVersion() to UIServerTestUtils
- Replace magic number with DEFAULT_MAX_PAYLOAD_SIZE import
* fix(ui-server): per-request McpServer factory, async iterator body reader, transport leak
- Create a new McpServer per HTTP request via createMcpServer() factory
to fix concurrency bug: McpServer.connect() overwrites a single
internal _transport field, causing cross-talk under concurrent requests.
OCPP schema cache and UI service are pre-loaded at startup; tool
registration is ~12µs per request (negligible).
- Replace event-listener readRequestBody with for-await-of async
iterator pattern. Single settlement guaranteed by language semantics,
eliminating the double-reject bug on payload-too-large.
- Close transport on mcpServer.connect() failure to prevent resource leak.
- Clean up both transport and McpServer on response close via unified
cleanup callback.
* fix: align StatusNotification field name with OCPP 2.0.1 schema, fix asset paths
- Use 'connectorStatus' (OCPP 2.0.1 schema field name) instead of
'status' (non-standard) in OCPP20RequestService.buildRequestPayload
and both call sites in OCPP20IncomingRequestService
- Fix esbuild-relative asset paths for OCPP JSON schemas and MCP
resource handlers — import.meta.url resolves to dist/start.js,
not the original source tree depth
* refactor(ocpp): align payload builders to object-param API, fix StatusNotification field names
- Refactor buildStatusNotificationRequest from positional params to
object-based StatusNotificationRequest input. Builder accepts both
'status' (internal/OCPP 1.6) and 'connectorStatus' (OCPP 2.0.1)
with consistent fallback order (connectorStatus ?? status).
- Refactor buildTransactionEvent from 3 overloads to single
object-based OCPP20TransactionEventRequest input, removing
positional param complexity and non-null assertions.
- Refactor sendAndSetConnectorStatus from positional params to
object-based StatusNotificationRequest input. Callers pass
version-correct field names: 'status' in OCPP 1.6 code,
'connectorStatus' in OCPP 2.0 code, either in common code.
- Extract getSchemaBaseDir() as protected method on UIMCPServer
for test overridability, replacing fragile symlink hack.
- Migrate all call sites (28 sendAndSetConnectorStatus, 60+
buildTransactionEvent) and their corresponding tests.
* build: externalize @modelcontextprotocol/sdk and zod from esbuild bundle
Add @modelcontextprotocol/* and zod to esbuild external list for
consistency with all other production dependencies (ajv, ws, winston,
etc.) which are already externalized. Reduces bundle size and avoids
bundling the SDK's transitive dependency tree.
- Add localeCompare to schema file sort in MCPResourceHandlers to
ensure locale-aware alphabetical ordering (SonarCloud bug)
- Extract sendErrorResponse helper from handleMcpRequest to reduce
cognitive complexity from 16 to below threshold (SonarCloud smell)
* chore: update .serena/project.yml with new default config fields
* refactor(ocpp): buildMeterValue resolves connectorId/evseId from transactionId
- Refactor buildMeterValue signature from (station, connectorId,
transactionId, interval) to (station, transactionId, interval).
connectorId and evseId are resolved internally from the unique
transactionId via getConnectorIdByTransactionId/getEvseIdByTransactionId.
- Refactor getSampledValueTemplate to accept optional evseId. When
provided, EVSE-level MeterValues templates take priority; falls back
to merging connector-level templates from all EVSE connectors.
- Add MeterValues field to EvseStatus and EvseTemplate types to support
EVSE-level meter value template definitions.
- Refactor handleMeterValues in BroadcastChannel from if/else to
switch/case on OCPP version with explicit default error.
- Simplify OCPP20IncomingRequestService fallback MeterValues (no
transaction = static empty meter value, skip buildMeterValue).
- Add buildMeterValue unit tests covering OCPP 1.6/2.0 resolution,
unknown transactionId throw, EVSE-level template priority, and
connector template merge fallback.
- Add TEST_TRANSACTION_ID_STRING constant for OCPP 2.0 tests, fix
BroadcastChannel test to use string transactionId for OCPP 2.0.1.
- Fix commandResponseToResponseStatus for Authorize: use switch/case
on OCPP version. OCPP 1.6 checks idTagInfo.status, OCPP 2.0 checks
idTokenInfo.status. Previously only checked idTagInfo (1.6 field),
causing MCP to report 'failure' for successful OCPP 2.0 authorizations.
- Make AuthorizeResponse and AuthorizeRequest cross-version union types
(OCPP16 | OCPP20) consistent with AuthorizationStatus pattern.
- Make isIdTagRemoteAuthorized version-aware: sends idTag for OCPP 1.6,
idToken for OCPP 2.0, and checks the version-correct response field.
- Use version-specific casts (OCPP16AuthorizeResponse,
OCPP20AuthorizeResponse) in version-switched code paths instead of
agnostic union type.
* refactor(ocpp): harmonize version switch/case in auth functions
- Refactor isIdTagAuthorizedUnified and isIdTagAuthorized from if/else
on OCPP version to switch/case with explicit VERSION_16, VERSION_20/201
cases and separate default fallback.
- Remove unnecessary optional chaining on stationInfo inside
version-switched cases where stationInfo is guaranteed non-null.
- Separate VERSION_16 from default in all auth switch/case blocks
for spec-compliant version handling.
* fix(ocpp): buildMeterValue from transactionId, log date locale fix, MeterValues EVSE templates
- Refactor buildMeterValue to resolve connectorId/evseId from
transactionId internally. Returns empty MeterValue when transactionId
is undefined (MCP pass-through with provided meterValue).
- Fix log file date resolution: use local date instead of UTC to match
Winston DailyRotateFile naming convention.
- Add optional date parameter to readCombinedLog/readErrorLog MCP tools
for accessing rotated log files by date (YYYY-MM-DD).
- Add MeterValues field to EvseStatus/EvseTemplate types for EVSE-level
meter value template definitions. getSampledValueTemplate checks
EVSE-level templates first, falls back to merging connector templates.
- Refactor handleMeterValues to switch/case on OCPP version with
explicit default error.
- Make AuthorizeRequest/AuthorizeResponse cross-version union types.
Fix commandResponseToResponseStatus and isIdTagRemoteAuthorized with
version-aware switch/case for OCPP 1.6 (idTagInfo) vs 2.0 (idTokenInfo).
- Add integration tests for readCombinedLog tool (default date, explicit
date, missing file error). Add buildMeterValue unit tests.
Jérôme Benoit [Sun, 22 Mar 2026 18:26:00 +0000 (19:26 +0100)]
fix(ocpp2): implement StopTxOnInvalidId and MaxEnergyOnInvalidId per E05 (#1745)
* fix(ocpp2): implement StopTxOnInvalidId and MaxEnergyOnInvalidId per E05
Implement the full deauthorization decision tree per OCPP 2.0.1 spec:
- StopTxOnInvalidId=false: transaction continues, no termination
- StopTxOnInvalidId=true + MaxEnergyOnInvalidId>0: defer termination
until energy limit reached (checked in periodic meter values callback)
- StopTxOnInvalidId=true + MaxEnergyOnInvalidId=0: immediate suspension
and termination
Also includes:
- Deduplicate transactionSetInterval/transactionTxUpdatedSetInterval
into single transactionMeterValuesSetInterval field
- Add transactionDeauthorized/transactionDeauthorizedEnergyWh to
ConnectorStatus for deauth state tracking (cleared in resetConnectorStatus)
- Extract readVariableValue/readVariableAsBoolean/readVariableAsInteger
helpers using convertToInt and convertToBoolean utilities
- Fix buildEvsesStatus test that was missing evse-to-connector wiring
* refactor(ocpp2): address PR review comments
- Add logger.warn in readVariableAsInteger catch for diagnostic
- Replace unsafe 'as string' cast with .toString() in periodic callback
- Add tests for StopTxOnInvalidId=false and deauth state tracking
* chore: update webui.png
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* test: add resetConnectorStatus unit tests to Helpers.test.ts
Verify all 18 transaction fields are properly cleaned, TX_PROFILE
charging profiles matching the transaction are removed, and
non-transaction fields are preserved.
Jérôme Benoit [Sun, 22 Mar 2026 15:36:32 +0000 (16:36 +0100)]
fix(ocpp2): align MeterValues implementation with OCPP 2.0.1 spec (#1744)
* fix(ocpp2): use AlignedDataInterval for standalone MeterValues
Add getAlignedDataInterval() helper to OCPP20ServiceUtils that reads
AlignedDataCtrlr.Interval from the variable registry (default 900s).
Replace getTxUpdatedInterval() with getAlignedDataInterval() in the
broadcast channel handleMeterValues OCPP 2.0 branch. Standalone
MeterValuesRequest is non-transaction data per OCPP 2.0.1 spec and
should use the aligned data interval, not the tx-updated interval.
* fix(ocpp2): include meter value in TransactionEvent Started
Add buildTransactionBeginMeterValues() to OCPP20ServiceUtils following
the buildFinalMeterValues() pattern. Builds an OCPP20MeterValue with
Transaction.Begin context and Energy.Active.Import.Register measurand.
Wire it into both TransactionEvent(Started) call sites:
- OCPPServiceUtils.startTransactionOnConnector (ATG/broadcast channel)
- OCPP20IncomingRequestService RequestStartTransaction event listener
This aligns OCPP 2.0 with the OCPP 1.6 beginEndMeterValues behavior
per OCPP 2.0.1 spec SampledDataTxStartedMeasurands requirement.
* test(ocpp2): add tests for buildTransactionBeginMeterValues
Test Transaction.Begin context, energy register value, default to 0
when undefined, and empty array when energy is negative.
* refactor(ocpp2): address PR review comments
- Extract buildEnergyMeterValues private helper to eliminate DRY
violation between buildTransactionBeginMeterValues and
buildFinalMeterValues
- Add AlignedDataInterval to OCPP20RequiredVariableName enum replacing
raw string literal in getAlignedDataInterval
- Clarify zero-energy test name to document that 0 Wh is a valid
Transaction.Begin reading
- Add meterValue assertions to RequestStartTransaction test verifying
Transaction.Begin context and Energy.Active.Import.Register measurand
* refactor(ocpp2): extract readVariableAsIntervalMs to eliminate DRY
getAlignedDataInterval and getTxUpdatedInterval were structurally
identical. Extract the shared variable-reading logic into a private
readVariableAsIntervalMs helper that accepts component name, variable
name, and default seconds. Both public methods become one-liner
delegates.
* refactor(ocpp2): extract terminateTransaction and resolveActiveTransaction
requestDeauthorizeTransaction and requestStopTransaction shared
identical transaction termination logic (build final meter values,
send TransactionEvent Ended, stop periodic, reset connector status).
Extract resolveActiveTransaction for the shared precondition check
and transactionId string resolution, and terminateTransaction for
the shared Ended event + cleanup workflow. Both public methods now
focus only on their unique behavior.
* fix(test): use enum constants instead of string literals in RequestStartTransaction test
Replace 'Transaction.Begin' and 'Energy.Active.Import.Register' string
literals with OCPP20ReadingContextEnumType.TRANSACTION_BEGIN and
OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER to match
codebase conventions.
* refactor(test): replace string literals with enum constants in OCPP 2.0 tests
Replace hardcoded string literals with their corresponding OCPP 2.0.1
enum values across test files for type safety and consistency:
requestDeauthorizeTransaction mocks returned { status: 'Accepted' }
but OCPP20TransactionEventResponse has no status field. Replace with
properly typed empty response objects since the return value is not
asserted in these tests.
* refactor(ocpp2): align method names with OCPP 2.0.1 spec terminology
The spec uses Started/Updated/Ended for TransactionEvent types, not
Begin/Final.
* refactor(test): replace 'Operative' string literal with enum constants
Use OCPP20OperationalStatusEnumType.Operative in OCPP 2.0 test files
and the cross-version AvailabilityType.Operative in MessageChannelUtils
test to match ConnectorStatus.availability typed field.
Jérôme Benoit [Sun, 22 Mar 2026 11:40:27 +0000 (12:40 +0100)]
fix(ocpp2): build meter values payload in broadcast channel for OCPP 2.0.x
The OCPP 2.0 branch in handleMeterValues was passing the raw broadcast
channel payload through to requestHandler without constructing the
required evseId and meterValue fields. Build the payload using
buildMeterValue and resolve evseId from connectorId, matching the
pattern used by the OCPP 1.6 branch.
Jérôme Benoit [Sat, 21 Mar 2026 20:29:59 +0000 (21:29 +0100)]
refactor(ocpp2): use enum values instead of string literals in variable registry
Replace all string literals that have corresponding TypeScript enums:
- OCPP20OperationalStatusEnumType for AvailabilityState defaultValue/enumeration
- OCPP20ChargingRateUnitEnumType for ChargingScheduleAllowedChargingRateUnit
defaultValue, enumeration, and description
Self-transitions are not allowed per OCPP 2.0.1 spec. The boot
sequence warning needs a different fix (skip StatusNotification
when connector status unchanged).