* fix(cli): make high-level transaction and authorize commands OCPP version-aware
- transaction start: detect station OCPP version, send TRANSACTION_EVENT
with idToken/evse payload for 2.0.x or START_TRANSACTION for 1.6
- transaction stop: same version detection, send TRANSACTION_EVENT Ended
for 2.0.x or STOP_TRANSACTION for 1.6; accept string transaction IDs
- ocpp authorize --id-tag: send idToken object for 2.0.x, idTag for 1.6;
-p payload passthrough unchanged (low-level override)
- action.ts: extract fetchStationList helper, export resolveOcppVersion
* docs(cli): clarify prefix matching behavior in resolveOcppVersion JSDoc
* refactor(cli): use switch/case for OCPP version branching and clean up help text
* fix(cli): address audit findings for cross-version OCPP compliance
- R1: status-notification version-aware: --error-code optional, validated
for OCPP 1.6; OCPP 2.0.x uses connectorStatus with optional --evse-id
- R2: add optional --evse-id to meter-values
- R3: skipped (callers already throw clear error for heterogeneous/no-match)
- R4: skipped (handleStopTransaction is 1.6-only, transactionId always number)
- R5: JSDoc on version-specific ProcedureName entries
- R6: comment on handleStopTransaction noting 1.6-only
* refactor(cli): consolidate version detection, fix DRY violations, and harden edge cases
- F-01: comment noting OCPP20TriggerReasonEnumType duplication
- F-02/F-04: extract resolveOcppVersionFromProgram (single fetch, returns resolved hashIds)
- F-03: export MIXED_OCPP_VERSION_ERROR constant
- F-05: default evse.id to 1, add --evse-id to transaction start
- F-06: document -p limitation in help text
- F-07: remove idTag ?? '' dead code
- F-08: remove redundant === id check
- F-09: add test for ambiguous prefix with homogeneous versions
- F-10: rewrite handleStopTransaction comment
* style: remove unnecessary duplication comments in ui/common enums
* refactor(cli): use commander conflicts() for mutually exclusive options
ocpp authorize: --id-tag and -p/--payload are now declared as mutually
exclusive via Option.conflicts(). Commander enforces this at parse time.
* fix(cli): resolve audit findings — BUG-01, DRY-01, DEAD-01, ROBUST-01, PERF-01
- BUG-01: use resolvedHashIds (not raw hashIds) in resolveOcppVersionFromProgram
- DRY-01: extract resolveShortHashIdsFromList pure helper, used by both
resolveShortHashIds and resolveOcppVersionFromProgram
- DEAD-01/DOC-01: remove unused resolveOcppVersion, update JSDoc references
- ROBUST-01: seqNo 1 for transaction stop (start uses 0)
- PERF-01: resolveOcppVersionFromProgram returns config, runAction accepts
preloadedConfig to skip redundant loadConfig call
* chore: refine .gitignore
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
* fix(cli): simplify OCPP 2.0.x transaction payloads to match builder options
- transaction start: send flat OCPP20TransactionEventOptions (connectorId,
evseId, idToken) instead of wire-format fields (seqNo, timestamp,
transactionInfo nesting, evse structure)
- transaction stop: send flat transactionId instead of nested
transactionInfo, remove hardcoded seqNo/timestamp/triggerReason
- Remove evseId ?? 1 default (let builder resolve from connectorId)
- Remove unused imports (randomUUID, OCPP20TriggerReasonEnumType)
- Fix meter-values --evse-id misleading help text
- Add test for mixed matching/non-matching hashIds
Addresses review comments from hyperspace-insights and Copilot,
cross-validated with audit v2 findings BUG-01, BUG-02, BUG-03.
* fix(cli): harmonize all OCPP commands with version-aware conflicts pattern
- Add early NO_STATIONS_ERROR check in resolveOcppVersionFromProgram
- Refactor status-notification, meter-values, transaction start/stop to
use .addOption/.conflicts() pattern matching authorize's design
- Make meter-values version-aware: OCPP 1.6 sends connectorId, OCPP 2.0
sends evseId, passthrough mode sends only routing fields
- Fix --evse-id help text in transaction start (was claiming false default)
- Fix pre-existing jsdoc/require-throws-type lint warning
* fix(cli): add connectorId to transaction stop for OCPP 2.0.x and harmonize evseId conflicts
- Add --connector-id option to transaction stop (defaults to 1) so
buildTransactionEvent resolves the correct connector on multi-connector
stations
- Convert all --evse-id options to .addOption/.conflicts('payload') for
consistent UX with other semantic options
- Add evseId to reverse .conflicts() lists on -p/--payload options
* fix(cli): update JSDoc and comments to match current behavior
- Update resolveOcppVersionFromProgram JSDoc: returns OCPPVersion (not
undefined), add @throws documentation for all error cases
- Update MIN_FULL_HASH_LENGTH comment: now validates existence, not just
skips resolution
* fix(cli): route version-detection errors through formatter for --json mode
Add handleActionErrors wrapper that catches errors from version
detection, option validation, and hash-ID resolution before runAction,
routing them through the same formatter used by runAction. Without this,
errors in --json mode were output as plain text instead of structured
JSON.
* Apply suggestion from @jerome-benoit
* refactor(cli): extract shared error formatter and clean up exports
- Extract formatError() from duplicated catch blocks in
handleActionErrors and runAction
- Align authorize error message with the pattern used by all other
commands ('X is required when -p/--payload is not provided')
- Remove unnecessary exports from 3 internal-only error constants
(NO_STATIONS_ERROR, MIXED_OCPP_VERSION_ERROR, UNKNOWN_OCPP_VERSION_ERROR)
* fix(cli): standardize option help texts and remove redundancy
- Standardize EVSE ID help text to 'EVSE ID (OCPP 2.0.x; derived from
connector ID if omitted)' across meter-values, status-notification,
and transaction start
- Simplify error-code help text from 'OCPP 1.6 only; required for 1.6'
to 'OCPP 1.6' (the constraint is already enforced at runtime)
* docs(cli): document version-aware commands and new OCPP 2.0.x options
- Add --evse-id to meter-values, status-notification, transaction start
- Add --connector-id to transaction stop (required for OCPP 2.0.x)
- Mark --error-code as optional (OCPP 1.6 only)
- Mark --vendor-id as optional in data-transfer
- Document -p/--payload support for transaction commands
- Add Version-aware commands section with OCPP 1.6 vs 2.0.x mapping
- Update both README.md and SKILL.md consistently
* docs(cli): fix pre-existing documentation gaps in README and SKILL.md
- Add missing station add options: --persistent-config, --ocpp-strict
- Add missing station delete --delete-config example in SKILL.md
- Add missing atg stop --connector-ids in SKILL.md
- Fix meter-values: both --connector-id and --evse-id are optional
(at least one required)
- Fix ATG --connector-ids notation: <id,...> instead of <ids...>
- Add hashId prefix matching note to SKILL.md
* fix(cli): harmonize user-facing strings and fix documentation accuracy
Code:
- Harmonize 'station(s)' terminology (drop 'charging' prefix in
station.ts descriptions to match all other command files)
- Fix NO_STATIONS_ERROR to use consistent 'stations' wording
- Fix data-transfer --data description: free-form string, not JSON only
- Fix transaction unsupported version error: direct users to
'ocpp transaction-event -p' instead of generic '-p' which forces 1.6
Docs (README + SKILL.md):
- Fix merge claim: -p conflicts with typed options on most commands,
only data-transfer allows merge
- Add meter-values 'at least one required' constraint note
- Add --connector-id required for transaction stop on OCPP 2.0.x in
version-aware table
- Fix data-transfer --data placeholder from <json> to <data>
* refactor(cli): extract duplicated unsupported version error in transaction.ts
Extract inline error string into a local const within
createTransactionCommands to avoid duplication across the start and
stop default switch branches.
* fix(cli): correct meter-values payload for OCPP 2.0.x stations
The OCPP 2.0.x MeterValues JSON schema does not allow connectorId as a
property. The previous implementation conditionally included connectorId
in the payload for OCPP 2.0.x, which caused schema validation failures
in the simulator ("additionalProperty: connectorId — must NOT have
additional properties").
For OCPP 2.0.x, evseId is the only valid EVSE identifier in a
MeterValues request. The fix now requires --evse-id for OCPP 2.0.x
stations and sends only evseId in the payload. If only --connector-id
is supplied against a 2.0.x station, a clear error message guides the
user to use --evse-id instead. The --connector-id help text is also
updated to indicate it is OCPP 1.6 only for this command.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(cli): align meter-values documentation with evseId-only OCPP 2.0.x fix
Update README and SKILL.md to reflect that meter-values requires
--evse-id for OCPP 2.0.x (connectorId is not a valid MeterValues
field in 2.0.x). Show separate OCPP 1.6 and 2.0.x usage lines.
* fix(cli): harmonize meter-values help texts and error message
- Drop 'only' from --connector-id help: '(OCPP 1.6)' matches
--error-code pattern in status-notification
- Drop 'required' from --evse-id help: '(OCPP 2.0.x)' matches
version-only pattern used for version-exclusive options
- Remove verbose parenthetical from evseId error message to match
the terse pattern used by all other version-specific errors
* fix(cli): use SCREAMING_CASE for error constant and add missing exit code
- Rename unsupportedVersionError to UNSUPPORTED_VERSION_ERROR in
transaction.ts to match the naming convention used by all error
constants in action.ts
- Add exit code 143 (SIGTERM) to SKILL.md exit codes table
* fix(cli): complete resolveOcppVersionFromList JSDoc for unknown versions
Add 'or all versions are unknown' to the @returns description to
document that the function also returns undefined when all targeted
stations have ocppVersion: undefined (Set {undefined}, size 1).
* fix(cli): align error constant naming and fix README station list comment
- Rename UNSUPPORTED_VERSION_ERROR to UNSUPPORTED_OCPP_VERSION_ERROR in
transaction.ts to match the naming convention with OCPP_ prefix used
by the exported constant in action.ts
- Fix README inline comment: 'List all stations' to match station.ts
description (was still 'List all charging stations')
---------
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
performanceRecords.json
performanceRecords.json.lock
*.db
+.hermes/
START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
START_CHARGING_STATION = 'startChargingStation',
START_SIMULATOR = 'startSimulator',
+ /** OCPP 1.6 only. Use TRANSACTION_EVENT for OCPP 2.0.x. */
START_TRANSACTION = 'startTransaction',
STATUS_NOTIFICATION = 'statusNotification',
STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
STOP_CHARGING_STATION = 'stopChargingStation',
STOP_SIMULATOR = 'stopSimulator',
+ /** OCPP 1.6 only. Use TRANSACTION_EVENT for OCPP 2.0.x. */
STOP_TRANSACTION = 'stopTransaction',
+ /** OCPP 2.0.x only. Use START_TRANSACTION / STOP_TRANSACTION for OCPP 1.6. */
TRANSACTION_EVENT = 'transactionEvent',
UNLOCK_CONNECTOR = 'unlockConnector',
}
#### station
```shell
-evse-cli station list # List all charging stations
+evse-cli station list # List all stations
evse-cli station start [hashId...] # Start station(s)
evse-cli station stop [hashId...] # Stop station(s)
evse-cli station add -t <template> -n <count> # Add stations from template
**`station add` options:**
-| Option | Required | Description |
-| ------------------------- | -------- | ------------------------- |
-| `-t, --template <name>` | Yes | Station template name |
-| `-n, --count <n>` | Yes | Number of stations to add |
-| `--supervision-url <url>` | No | Override supervision URL |
-| `--auto-start` | No | Auto-start added stations |
+| Option | Required | Description |
+| ------------------------- | -------- | ------------------------------------ |
+| `-t, --template <name>` | Yes | Station template name |
+| `-n, --count <n>` | Yes | Number of stations to add |
+| `--supervision-url <url>` | No | Override supervision URL |
+| `--auto-start` | No | Auto-start added stations |
+| `--persistent-config` | No | Enable persistent OCPP configuration |
+| `--ocpp-strict` | No | Enable OCPP strict compliance |
**`station delete` options:**
#### atg
```shell
-evse-cli atg start [hashId...] [--connector-ids <ids...>] # Start ATG
-evse-cli atg stop [hashId...] [--connector-ids <ids...>] # Stop ATG
+evse-cli atg start [hashId...] [--connector-ids <id,...>] # Start ATG
+evse-cli atg stop [hashId...] [--connector-ids <id,...>] # Stop ATG
```
#### transaction
```shell
-evse-cli transaction start --connector-id <id> --id-tag <tag> [hashId...]
-evse-cli transaction stop --transaction-id <id> [hashId...]
+evse-cli transaction start --connector-id <id> --id-tag <tag> [--evse-id <id>] [hashId...]
+evse-cli transaction stop --transaction-id <id> [--connector-id <id>] [hashId...]
```
+Both commands auto-detect the station's OCPP version and adapt the procedure and payload (see [Version-aware commands](#version-aware-commands)). The `-p, --payload` option uses the OCPP 1.6 procedure; for 2.0.x raw payloads use `ocpp transaction-event -p`.
+
#### ocpp
Request charging station(s) to send OCPP messages to the CSMS:
```shell
-evse-cli ocpp heartbeat [hashId...] # Heartbeat
-evse-cli ocpp authorize --id-tag <tag> [hashId...] # Authorize
-evse-cli ocpp boot-notification [hashId...] # BootNotification
-evse-cli ocpp status-notification --connector-id <id> --error-code <code> --status <status> [hashId...] # StatusNotification
-evse-cli ocpp meter-values --connector-id <id> [hashId...] # MeterValues
-evse-cli ocpp data-transfer --vendor-id <id> [--message-id <id>] [--data <json>] [hashId...] # DataTransfer
+evse-cli ocpp heartbeat [hashId...] # Heartbeat
+evse-cli ocpp authorize --id-tag <tag> [hashId...] # Authorize
+evse-cli ocpp boot-notification [hashId...] # BootNotification
+evse-cli ocpp status-notification --connector-id <id> [--error-code <code>] --status <status> [--evse-id <id>] [hashId...] # StatusNotification
+evse-cli ocpp meter-values --connector-id <id> [hashId...] # MeterValues (OCPP 1.6)
+evse-cli ocpp meter-values --evse-id <id> [hashId...] # MeterValues (OCPP 2.0.x)
+evse-cli ocpp data-transfer [--vendor-id <id>] [--message-id <id>] [--data <data>] [hashId...] # DataTransfer
```
+`meter-values` requires `--connector-id` for OCPP 1.6 and `--evse-id` for OCPP 2.0.x.
+
Other OCPP commands (no extra options): `diagnostics-status-notification`, `firmware-status-notification`, `get-15118-ev-certificate`, `get-certificate-status`, `log-status-notification`, `notify-customer-information`, `notify-report`, `security-event-notification`, `sign-certificate`, `transaction-event`.
-All OCPP commands accept `-p, --payload <json|@file|->` to pass a custom JSON payload:
+All OCPP and transaction commands accept `-p, --payload <json|@file|->` to pass a custom JSON payload:
```shell
evse-cli ocpp boot-notification -p '{"reason":"PowerUp"}' [hashId...] # Inline JSON
cat boot.json | jq '.reason = "RemoteReset"' | evse-cli ocpp boot-notification -p - [hashId...] # From stdin
```
-The payload is merged with command-specific options (e.g., `--id-tag`, `--connector-id`). Command options take precedence over payload fields.
+For `data-transfer`, command options merge with `-p` payload (options take precedence). For all other typed-option commands, `-p` conflicts with typed options and they cannot be combined.
+
+#### Version-aware commands
+
+Commands with typed options (`authorize`, `meter-values`, `status-notification`, `transaction start`, `transaction stop`) auto-detect the target station's OCPP version and build the appropriate payload:
+
+| Option | OCPP 1.6 | OCPP 2.0.x |
+| ------------------ | ---------------------------------- | ------------------------------------------------------------- |
+| `--id-tag` | Sent as `idTag` | Wrapped as `idToken` (type: ISO14443) |
+| `--connector-id` | Sent as `connectorId` | Required for `transaction stop`; not valid for `meter-values` |
+| `--evse-id` | N/A | Sent as `evseId`; required for `meter-values` |
+| `--error-code` | Required for `status-notification` | N/A |
+| `--transaction-id` | Integer | UUID string |
+
+When `-p` is provided, version detection is skipped and the raw payload is passed through as-is.
#### supervision
evse-cli station add -t <template> -n 2 --auto-start # Add and auto-start
evse-cli station add -t <template> -n 1 --supervision-url ws://csms:8180/path
evse-cli station delete [hashId...] # Delete station(s)
+evse-cli station delete --delete-config [hashId...] # Delete with config files
```
### Templates
```shell
evse-cli atg start [hashId...] # Start ATG on all connectors
evse-cli atg start --connector-ids 1,2 [hashId...] # Start on specific connectors
-evse-cli atg stop [hashId...] # Stop ATG
+evse-cli atg stop [hashId...] # Stop ATG on all connectors
+evse-cli atg stop --connector-ids 1,2 [hashId...] # Stop on specific connectors
```
### Transactions
```shell
-evse-cli transaction start --connector-id <id> --id-tag <tag> [hashId...]
-evse-cli transaction stop --transaction-id <id> [hashId...]
+evse-cli transaction start --connector-id <id> --id-tag <tag> [--evse-id <id>] [hashId...]
+evse-cli transaction stop --transaction-id <id> [--connector-id <id>] [hashId...]
```
+Both commands auto-detect the station's OCPP version and adapt the procedure and payload. The `-p` option uses the OCPP 1.6 procedure; for 2.0.x raw payloads use `ocpp transaction-event -p`.
+
### OCPP Messages
Request station(s) to send OCPP messages to the CSMS:
evse-cli ocpp heartbeat [hashId...]
evse-cli ocpp boot-notification [hashId...]
evse-cli ocpp authorize --id-tag <tag> [hashId...]
-evse-cli ocpp status-notification --connector-id <id> --error-code <code> --status <status> [hashId...]
-evse-cli ocpp meter-values --connector-id <id> [hashId...]
-evse-cli ocpp data-transfer --vendor-id <id> [--message-id <id>] [--data <json>] [hashId...]
+evse-cli ocpp status-notification --connector-id <id> [--error-code <code>] --status <status> [--evse-id <id>] [hashId...]
+evse-cli ocpp meter-values --connector-id <id> [hashId...] # OCPP 1.6
+evse-cli ocpp meter-values --evse-id <id> [hashId...] # OCPP 2.0.x
+evse-cli ocpp data-transfer [--vendor-id <id>] [--message-id <id>] [--data <data>] [hashId...]
```
+`meter-values` requires `--connector-id` for OCPP 1.6 and `--evse-id` for OCPP 2.0.x.
+
Other OCPP commands (no extra options): `diagnostics-status-notification`, `firmware-status-notification`, `get-15118-ev-certificate`, `get-certificate-status`, `log-status-notification`, `notify-customer-information`, `notify-report`, `security-event-notification`, `sign-certificate`, `transaction-event`.
-All OCPP commands accept `-p, --payload <json|@file|->` for custom JSON payloads:
+All OCPP and transaction commands accept `-p, --payload <json|@file|->` for custom JSON payloads:
```shell
evse-cli ocpp boot-notification -p '{"reason":"PowerUp"}' [hashId...] # Inline
cat payload.json | evse-cli ocpp boot-notification -p - [hashId...] # From stdin
```
+### Version-aware commands
+
+Commands with typed options (`authorize`, `meter-values`, `status-notification`, `transaction start`, `transaction stop`) auto-detect the target station's OCPP version and build the appropriate payload. Key differences:
+
+- `--id-tag`: sent as `idTag` (1.6) or wrapped as `idToken` (2.0.x)
+- `--error-code`: required for `status-notification` on 1.6 only
+- `--evse-id`: OCPP 2.0.x only; required for `meter-values`
+- `--connector-id`: OCPP 1.6 for `meter-values`; required for `transaction stop` on 2.0.x
+- `--transaction-id`: integer (1.6) or UUID string (2.0.x)
+
+When `-p` is provided, version detection is skipped and the raw payload is passed through.
+
### Supervision
```shell
| `0` | Success |
| `1` | Error (connection, server, auth) |
| `130` | Interrupted (Ctrl+C) |
+| `143` | Terminated (SIGTERM) |
## hashId Convention
-Omitting `[hashId...]` applies the command to ALL stations. Pass one or more hash IDs to target specific stations. Get hash IDs from `evse-cli station list` or `evse-cli --json station list`.
+Omitting `[hashId...]` applies the command to ALL stations. Pass one or more hash IDs to target specific stations. Hash IDs support prefix matching — a short unambiguous prefix works in place of the full ID. Get hash IDs from `evse-cli station list` or `evse-cli --json station list`.
## Common Workflows
import process from 'node:process'
import {
+ type OCPPVersion,
ProcedureName,
type RequestPayload,
ResponseStatus,
import { createFormatter } from '../output/formatter.js'
import { resolvePayload } from './resolve-payload.js'
-export const parseInteger = (value: string): number => {
+const NO_STATIONS_ERROR = 'No stations available. Start stations before running this command.'
+
+const MIXED_OCPP_VERSION_ERROR =
+ 'Cannot determine a common OCPP version for the targeted stations. ' +
+ 'Target homogeneous stations (same OCPP version) or use -p to pass the payload directly.'
+
+const UNKNOWN_OCPP_VERSION_ERROR =
+ 'The targeted station(s) have not reported their OCPP version yet. ' +
+ 'Ensure stations are connected and registered, or use -p to pass the payload directly.'
+
+export const UNSUPPORTED_OCPP_VERSION_ERROR =
+ 'Unsupported OCPP version for this command. Use -p to pass the payload directly.'
+
+export const parseInteger = (value: string, nameOrPrevious?: number | string): number => {
const n = Number.parseInt(value, 10)
if (Number.isNaN(n)) {
- throw new Error(`Expected integer, got '${value}'`)
+ const label = typeof nameOrPrevious === 'string' ? nameOrPrevious : undefined
+ throw new Error(
+ label != null
+ ? `${label}: expected integer, got '${value}'`
+ : `Expected integer, got '${value}'`
+ )
}
return n
}
-// SHA-384 hex hashes are 96 chars. Treat anything >= half-length as a full hash (skip resolution).
+// SHA-384 hex hashes are 96 chars; >= half-length is treated as a full or near-full hash.
const MIN_FULL_HASH_LENGTH = 48
-const resolveShortHashIds = async (
- hashIds: string[],
+const fetchStationList = async (
config: UIServerConfigurationSection
-): Promise<string[]> => {
- if (hashIds.length === 0) return []
-
- const allFull = hashIds.every(id => id.length >= MIN_FULL_HASH_LENGTH)
- if (allFull) return hashIds
-
+): Promise<StationListPayload> => {
let response
try {
response = await executeCommand({
})
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : String(error)
- throw new Error(`Failed to resolve hash ID prefixes: ${msg}`)
+ throw new Error(`Failed to fetch charging station list: ${msg}`)
}
if (response.status !== ResponseStatus.SUCCESS || !Array.isArray(response.chargingStations)) {
- throw new Error(
- `Failed to list charging stations for hash ID resolution (status: ${response.status})`
- )
+ throw new Error(`Failed to list charging stations (status: ${response.status})`)
}
- const listResponse = response as StationListPayload
- const allHashIds = listResponse.chargingStations.map(cs => cs.stationInfo.hashId)
+ return response as StationListPayload
+}
- return hashIds.map(input => {
- if (input.length >= MIN_FULL_HASH_LENGTH) return input
+/**
+ * Pure helper: resolves short hash-ID prefixes to full hashes against a
+ * pre-fetched list of all station hash IDs.
+ * @param hashIds - station hash IDs or unique hash-ID prefixes to resolve
+ * @param allHashIds - full hash IDs of all known stations
+ * @returns fully-resolved hash IDs in the same order as the input
+ * @throws {Error} if any prefix matches zero stations or more than one station
+ */
+const resolveShortHashIdsFromList = (hashIds: string[], allHashIds: string[]): string[] =>
+ hashIds.map(input => {
+ if (input.length >= MIN_FULL_HASH_LENGTH) {
+ if (allHashIds.includes(input)) return input
+ throw new Error(`No station found matching hash ID '${input}'`)
+ }
const matches = allHashIds.filter(full => full.startsWith(input))
if (matches.length === 1) return matches[0]
`Ambiguous hash prefix '${input}' matches ${matches.length.toString()} stations`
)
})
+
+const resolveShortHashIds = async (
+ hashIds: string[],
+ config: UIServerConfigurationSection
+): Promise<string[]> => {
+ if (hashIds.length === 0) return []
+
+ const allFull = hashIds.every(id => id.length >= MIN_FULL_HASH_LENGTH)
+ if (allFull) return hashIds
+
+ const listResponse = await fetchStationList(config)
+ const allHashIds = listResponse.chargingStations.map(cs => cs.stationInfo.hashId)
+
+ return resolveShortHashIdsFromList(hashIds, allHashIds)
+}
+
+/**
+ * Pure helper: resolves the common OCPP version from a pre-fetched station list.
+ * Exported for unit testing; callers should use resolveOcppVersionFromProgram.
+ *
+ * Unlike resolveShortHashIdsFromList, this function never throws.
+ * @param hashIds - station hash IDs or unique hash-ID prefixes to target;
+ * each entry is matched via startsWith against station hashIds.
+ * Non-matching prefixes are silently excluded from targeting (no throw).
+ * Pass an empty array to target all stations.
+ * @param chargingStations - station list to filter and inspect
+ * @returns the common OCPPVersion, or undefined if the targeted set is empty,
+ * the targeted stations run heterogeneous versions, or all versions are unknown
+ */
+export const resolveOcppVersionFromList = (
+ hashIds: string[],
+ chargingStations: { stationInfo: { hashId: string; ocppVersion?: OCPPVersion } }[]
+): OCPPVersion | undefined => {
+ const targeted =
+ hashIds.length === 0
+ ? chargingStations
+ : chargingStations.filter(cs => hashIds.some(id => cs.stationInfo.hashId.startsWith(id)))
+
+ if (targeted.length === 0) return undefined
+
+ const versions = new Set(targeted.map(cs => cs.stationInfo.ocppVersion))
+ if (versions.size !== 1) return undefined
+
+ return [...versions][0]
+}
+
+/**
+ * Loads config from program options, resolves short hash-ID prefixes to full
+ * hashes in a single station-list fetch, and returns the common OCPP version
+ * together with the resolved hash IDs and the loaded config. Pass the returned
+ * resolvedHashIds into the payload and config into runAction to avoid a second
+ * station-list fetch and config load.
+ * @param program - Commander root program (provides config and server URL options)
+ * @param hashIds - station hash IDs or unique hash-ID prefixes to target
+ * @returns the common OCPPVersion, the fully-resolved hash IDs,
+ * and the loaded UI server config
+ * @throws {Error} if no stations are available, hash IDs don't match, or the
+ * targeted stations do not share a single known OCPP version
+ */
+export const resolveOcppVersionFromProgram = async (
+ program: Command,
+ hashIds: string[]
+): Promise<{
+ config: UIServerConfigurationSection
+ ocppVersion: OCPPVersion
+ resolvedHashIds: string[]
+}> => {
+ const rootOpts = program.opts<GlobalOptions>()
+ const config = await loadConfig({ configPath: rootOpts.config, url: rootOpts.serverUrl })
+ const listResponse = await fetchStationList(config)
+
+ if (listResponse.chargingStations.length === 0) {
+ throw new Error(NO_STATIONS_ERROR)
+ }
+
+ const allHashIds = listResponse.chargingStations.map(cs => cs.stationInfo.hashId)
+ const resolvedHashIds =
+ hashIds.length === 0 ? hashIds : resolveShortHashIdsFromList(hashIds, allHashIds)
+ const ocppVersion = resolveOcppVersionFromList(resolvedHashIds, listResponse.chargingStations)
+ if (ocppVersion == null) {
+ const targeted =
+ resolvedHashIds.length === 0
+ ? listResponse.chargingStations
+ : listResponse.chargingStations.filter(cs =>
+ resolvedHashIds.some(id => cs.stationInfo.hashId.startsWith(id))
+ )
+ const hasUnknown = targeted.some(cs => cs.stationInfo.ocppVersion == null)
+ throw new Error(hasUnknown ? UNKNOWN_OCPP_VERSION_ERROR : MIXED_OCPP_VERSION_ERROR)
+ }
+ return { config, ocppVersion, resolvedHashIds }
+}
+
+const formatError = (program: Command, error: unknown): void => {
+ const rootOpts = program.opts<GlobalOptions>()
+ const formatter = createFormatter(rootOpts.json)
+ if (error instanceof ServerFailureError) {
+ formatter.output(error.payload)
+ } else {
+ formatter.error(error)
+ }
+ process.exitCode = 1
+}
+
+export const handleActionErrors = async (
+ program: Command,
+ fn: () => Promise<void>
+): Promise<void> => {
+ try {
+ await fn()
+ } catch (error: unknown) {
+ formatError(program, error)
+ }
}
export const runAction = async (
program: Command,
procedureName: ProcedureName,
payload: RequestPayload,
- rawPayload?: string
+ rawPayload?: string,
+ preloadedConfig?: UIServerConfigurationSection
): Promise<void> => {
const rootOpts = program.opts<GlobalOptions>()
const formatter = createFormatter(rootOpts.json)
mergedPayload = { ...extra, ...payload }
}
- const config = await loadConfig({ configPath: rootOpts.config, url: rootOpts.serverUrl })
+ const config =
+ preloadedConfig ??
+ (await loadConfig({ configPath: rootOpts.config, url: rootOpts.serverUrl }))
let resolvedPayload = mergedPayload
if (Array.isArray(mergedPayload.hashIds) && mergedPayload.hashIds.length > 0) {
await executeCommand({ config, formatter, payload: resolvedPayload, procedureName })
process.exitCode = 0
} catch (error: unknown) {
- if (error instanceof ServerFailureError) {
- formatter.output(error.payload)
- } else {
- formatter.error(error)
- }
- process.exitCode = 1
+ formatError(program, error)
}
}
-import { Command } from 'commander'
-import { ProcedureName, type RequestPayload } from 'ui-common'
+import { Command, Option } from 'commander'
+import { OCPP20IdTokenEnumType, OCPPVersion, ProcedureName, type RequestPayload } from 'ui-common'
-import { parseInteger, runAction } from './action.js'
+import {
+ handleActionErrors,
+ parseInteger,
+ resolveOcppVersionFromProgram,
+ runAction,
+ UNSUPPORTED_OCPP_VERSION_ERROR,
+} from './action.js'
import { buildHashIdsPayload, PAYLOAD_DESC, PAYLOAD_OPTION, pickDefined } from './payload.js'
export const createOcppCommands = (program: Command): Command => {
cmd
.command('authorize [hashIds...]')
.description('Request station(s) to send OCPP Authorize')
- .requiredOption('--id-tag <tag>', 'RFID tag for authorization')
- .option(PAYLOAD_OPTION, PAYLOAD_DESC)
- .action(async (hashIds: string[], options: { idTag: string; payload?: string }) => {
- const payload: RequestPayload = {
- idTag: options.idTag,
- ...buildHashIdsPayload(hashIds),
- }
- await runAction(program, ProcedureName.AUTHORIZE, payload, options.payload)
+ .addOption(new Option('--id-tag <tag>', 'RFID tag for authorization').conflicts('payload'))
+ .addOption(new Option(PAYLOAD_OPTION, PAYLOAD_DESC).conflicts('idTag'))
+ .action(async (hashIds: string[], options: { idTag?: string; payload?: string }) => {
+ await handleActionErrors(program, async () => {
+ let payload: RequestPayload
+ if (options.payload == null) {
+ if (options.idTag == null) {
+ throw new Error('--id-tag is required when -p/--payload is not provided')
+ }
+ // High-level: detect OCPP version and build correct payload
+ const { config, ocppVersion, resolvedHashIds } = await resolveOcppVersionFromProgram(
+ program,
+ hashIds
+ )
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ payload = {
+ idTag: options.idTag,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ payload = {
+ idToken: { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 },
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ default:
+ throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+ }
+ await runAction(program, ProcedureName.AUTHORIZE, payload, undefined, config)
+ } else {
+ // Low-level passthrough: -p provided, use only routing fields; raw payload has full control
+ payload = buildHashIdsPayload(hashIds)
+ await runAction(program, ProcedureName.AUTHORIZE, payload, options.payload)
+ }
+ })
})
cmd
.description('Request station(s) to send OCPP DataTransfer')
.option('--vendor-id <id>', 'vendor identifier')
.option('--message-id <id>', 'message identifier')
- .option('--data <json>', 'data payload (JSON string)')
+ .option('--data <data>', 'data payload (free-form string)')
.option(PAYLOAD_OPTION, PAYLOAD_DESC)
.action(
async (
cmd
.command('meter-values [hashIds...]')
.description('Request station(s) to send OCPP MeterValues')
- .requiredOption('--connector-id <id>', 'connector ID', parseInteger)
- .option(PAYLOAD_OPTION, PAYLOAD_DESC)
- .action(async (hashIds: string[], options: { connectorId: number; payload?: string }) => {
- const payload: RequestPayload = {
- connectorId: options.connectorId,
- ...buildHashIdsPayload(hashIds),
+ .addOption(
+ new Option('--connector-id <id>', 'connector ID (OCPP 1.6)')
+ .argParser(parseInteger)
+ .conflicts('payload')
+ )
+ .addOption(
+ new Option('--evse-id <id>', 'EVSE ID (OCPP 2.0.x)')
+ .argParser(parseInteger)
+ .conflicts('payload')
+ )
+ .addOption(new Option(PAYLOAD_OPTION, PAYLOAD_DESC).conflicts(['connectorId', 'evseId']))
+ .action(
+ async (
+ hashIds: string[],
+ options: { connectorId?: number; evseId?: number; payload?: string }
+ ) => {
+ await handleActionErrors(program, async () => {
+ let payload: RequestPayload
+ if (options.payload == null) {
+ if (options.connectorId == null && options.evseId == null) {
+ throw new Error(
+ '--connector-id or --evse-id is required when -p/--payload is not provided'
+ )
+ }
+ // High-level: detect OCPP version and build correct payload
+ const { config, ocppVersion, resolvedHashIds } = await resolveOcppVersionFromProgram(
+ program,
+ hashIds
+ )
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ if (options.connectorId == null) {
+ throw new Error('--connector-id is required for OCPP 1.6 stations')
+ }
+ payload = {
+ connectorId: options.connectorId,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ if (options.evseId == null) {
+ throw new Error('--evse-id is required for OCPP 2.0.x stations')
+ }
+ payload = {
+ evseId: options.evseId,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ default:
+ throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+ }
+ await runAction(program, ProcedureName.METER_VALUES, payload, undefined, config)
+ } else {
+ // Low-level passthrough: -p provided, use only routing fields; raw payload has full control
+ payload = buildHashIdsPayload(hashIds)
+ await runAction(program, ProcedureName.METER_VALUES, payload, options.payload)
+ }
+ })
}
- await runAction(program, ProcedureName.METER_VALUES, payload, options.payload)
- })
+ )
cmd
.command('status-notification [hashIds...]')
.description('Request station(s) to send OCPP StatusNotification')
- .requiredOption('--connector-id <id>', 'connector ID', parseInteger)
- .requiredOption('--error-code <code>', 'connector error code')
- .requiredOption('--status <status>', 'connector status')
- .option(PAYLOAD_OPTION, PAYLOAD_DESC)
+ .addOption(
+ new Option('--connector-id <id>', 'connector ID').argParser(parseInteger).conflicts('payload')
+ )
+ .addOption(
+ new Option('--error-code <code>', 'connector error code (OCPP 1.6)').conflicts('payload')
+ )
+ .addOption(
+ new Option('--evse-id <id>', 'EVSE ID (OCPP 2.0.x; derived from connector ID if omitted)')
+ .argParser(parseInteger)
+ .conflicts('payload')
+ )
+ .addOption(new Option('--status <status>', 'connector status').conflicts('payload'))
+ .addOption(
+ new Option(PAYLOAD_OPTION, PAYLOAD_DESC).conflicts([
+ 'connectorId',
+ 'errorCode',
+ 'evseId',
+ 'status',
+ ])
+ )
.action(
async (
hashIds: string[],
- options: { connectorId: number; errorCode: string; payload?: string; status: string }
- ) => {
- const payload: RequestPayload = {
- connectorId: options.connectorId,
- errorCode: options.errorCode,
- status: options.status,
- ...buildHashIdsPayload(hashIds),
+ options: {
+ connectorId?: number
+ errorCode?: string
+ evseId?: number
+ payload?: string
+ status?: string
}
- await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload, options.payload)
+ ) => {
+ await handleActionErrors(program, async () => {
+ let payload: RequestPayload
+ if (options.payload == null) {
+ if (options.connectorId == null) {
+ throw new Error('--connector-id is required when -p/--payload is not provided')
+ }
+ if (options.status == null) {
+ throw new Error('--status is required when -p/--payload is not provided')
+ }
+ // High-level: detect OCPP version and build correct payload
+ const { config, ocppVersion, resolvedHashIds } = await resolveOcppVersionFromProgram(
+ program,
+ hashIds
+ )
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ if (options.errorCode == null) {
+ throw new Error('--error-code is required for OCPP 1.6 stations')
+ }
+ payload = {
+ connectorId: options.connectorId,
+ errorCode: options.errorCode,
+ status: options.status,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ payload = {
+ connectorId: options.connectorId,
+ connectorStatus: options.status,
+ ...(options.evseId != null && { evseId: options.evseId }),
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ default:
+ throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+ }
+ await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload, undefined, config)
+ } else {
+ // Low-level passthrough: -p provided, use only routing fields; raw payload has full control
+ payload = buildHashIdsPayload(hashIds)
+ await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload, options.payload)
+ }
+ })
}
)
cmd
.command('list')
- .description('List all charging stations')
+ .description('List all stations')
.action(async () => {
await runAction(program, ProcedureName.LIST_CHARGING_STATIONS, {})
})
cmd
.command('start [hashIds...]')
- .description('Start charging station(s)')
+ .description('Start station(s)')
.action(async (hashIds: string[]) => {
const payload: RequestPayload = buildHashIdsPayload(hashIds)
await runAction(program, ProcedureName.START_CHARGING_STATION, payload)
cmd
.command('stop [hashIds...]')
- .description('Stop charging station(s)')
+ .description('Stop station(s)')
.action(async (hashIds: string[]) => {
const payload: RequestPayload = buildHashIdsPayload(hashIds)
await runAction(program, ProcedureName.STOP_CHARGING_STATION, payload)
cmd
.command('add')
- .description('Add charging stations from template')
+ .description('Add stations from template')
.requiredOption('-t, --template <name>', 'station template name')
.requiredOption('-n, --count <n>', 'number of stations to add', parseInteger)
.option('--supervision-url <url>', 'supervision URL for new stations')
cmd
.command('delete [hashIds...]')
- .description('Delete charging station(s)')
+ .description('Delete station(s)')
.option('--delete-config', 'delete station configuration files')
.action(async (hashIds: string[], options: { deleteConfig?: true }) => {
const payload: RequestPayload = {
-import { Command } from 'commander'
-import { ProcedureName, type RequestPayload } from 'ui-common'
+import { Command, Option } from 'commander'
+import {
+ OCPP20IdTokenEnumType,
+ OCPP20TransactionEventEnumType,
+ OCPPVersion,
+ ProcedureName,
+ type RequestPayload,
+} from 'ui-common'
-import { parseInteger, runAction } from './action.js'
+import {
+ handleActionErrors,
+ parseInteger,
+ resolveOcppVersionFromProgram,
+ runAction,
+} from './action.js'
import { buildHashIdsPayload, PAYLOAD_DESC, PAYLOAD_OPTION } from './payload.js'
export const createTransactionCommands = (program: Command): Command => {
const cmd = new Command('transaction').description('Transaction management')
+ const UNSUPPORTED_OCPP_VERSION_ERROR =
+ 'Unsupported OCPP version for this command. ' +
+ 'Use ocpp transaction-event -p to pass the payload directly.'
cmd
.command('start [hashIds...]')
.description('Start a transaction on station(s)')
- .requiredOption('--connector-id <id>', 'connector ID', parseInteger)
- .requiredOption('--id-tag <tag>', 'RFID tag for authorization')
- .option(PAYLOAD_OPTION, PAYLOAD_DESC)
+ .addOption(
+ new Option('--connector-id <id>', 'connector ID').argParser(parseInteger).conflicts('payload')
+ )
+ .addOption(new Option('--id-tag <tag>', 'RFID tag for authorization').conflicts('payload'))
+ .addOption(
+ new Option('--evse-id <id>', 'EVSE ID (OCPP 2.0.x; derived from connector ID if omitted)')
+ .argParser(parseInteger)
+ .conflicts('payload')
+ )
+ .addOption(
+ new Option(
+ PAYLOAD_OPTION,
+ PAYLOAD_DESC +
+ ' (uses OCPP 1.6 procedure; for 2.0.x raw payloads use ocpp transaction-event)'
+ ).conflicts(['connectorId', 'evseId', 'idTag'])
+ )
.action(
async (
hashIds: string[],
- options: { connectorId: number; idTag: string; payload?: string }
+ options: { connectorId?: number; evseId?: number; idTag?: string; payload?: string }
) => {
- const payload: RequestPayload = {
- connectorId: options.connectorId,
- idTag: options.idTag,
- ...buildHashIdsPayload(hashIds),
- }
- await runAction(program, ProcedureName.START_TRANSACTION, payload, options.payload)
+ await handleActionErrors(program, async () => {
+ let procedureName: ProcedureName
+ let payload: RequestPayload
+ if (options.payload == null) {
+ if (options.connectorId == null) {
+ throw new Error('--connector-id is required when -p/--payload is not provided')
+ }
+ if (options.idTag == null) {
+ throw new Error('--id-tag is required when -p/--payload is not provided')
+ }
+ // High-level: detect OCPP version and build correct payload
+ const { config, ocppVersion, resolvedHashIds } = await resolveOcppVersionFromProgram(
+ program,
+ hashIds
+ )
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ procedureName = ProcedureName.START_TRANSACTION
+ payload = {
+ connectorId: options.connectorId,
+ idTag: options.idTag,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ procedureName = ProcedureName.TRANSACTION_EVENT
+ payload = {
+ connectorId: options.connectorId,
+ eventType: OCPP20TransactionEventEnumType.STARTED,
+ ...(options.evseId != null && { evseId: options.evseId }),
+ idToken: { idToken: options.idTag, type: OCPP20IdTokenEnumType.ISO14443 },
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ default:
+ throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+ }
+ await runAction(program, procedureName, payload, undefined, config)
+ } else {
+ // Low-level passthrough: -p provided, uses OCPP 1.6 procedure; for 2.0.x raw payloads use ocpp transaction-event
+ procedureName = ProcedureName.START_TRANSACTION
+ payload = buildHashIdsPayload(hashIds)
+ await runAction(program, procedureName, payload, options.payload)
+ }
+ })
}
)
cmd
.command('stop [hashIds...]')
.description('Stop a transaction on station(s)')
- .requiredOption('--transaction-id <id>', 'transaction ID', parseInteger)
- .option(PAYLOAD_OPTION, PAYLOAD_DESC)
- .action(async (hashIds: string[], options: { payload?: string; transactionId: number }) => {
- const payload: RequestPayload = {
- transactionId: options.transactionId,
- ...buildHashIdsPayload(hashIds),
+ .addOption(new Option('--transaction-id <id>', 'transaction ID').conflicts('payload'))
+ .addOption(
+ new Option('--connector-id <id>', 'connector ID (required for OCPP 2.0.x)')
+ .argParser(parseInteger)
+ .conflicts('payload')
+ )
+ .addOption(
+ new Option(
+ PAYLOAD_OPTION,
+ PAYLOAD_DESC +
+ ' (uses OCPP 1.6 procedure; for 2.0.x raw payloads use ocpp transaction-event)'
+ ).conflicts(['transactionId', 'connectorId'])
+ )
+ .action(
+ async (
+ hashIds: string[],
+ options: { connectorId?: number; payload?: string; transactionId?: string }
+ ) => {
+ await handleActionErrors(program, async () => {
+ let procedureName: ProcedureName
+ let payload: RequestPayload
+ if (options.payload == null) {
+ if (options.transactionId == null) {
+ throw new Error('--transaction-id is required when -p/--payload is not provided')
+ }
+ // High-level: detect OCPP version and build correct payload
+ const { config, ocppVersion, resolvedHashIds } = await resolveOcppVersionFromProgram(
+ program,
+ hashIds
+ )
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ procedureName = ProcedureName.STOP_TRANSACTION
+ payload = {
+ transactionId: parseInteger(
+ options.transactionId,
+ '--transaction-id (OCPP 1.6 requires integer)'
+ ),
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ if (options.connectorId == null) {
+ throw new Error('--connector-id is required for OCPP 2.0.x stations')
+ }
+ procedureName = ProcedureName.TRANSACTION_EVENT
+ payload = {
+ connectorId: options.connectorId,
+ eventType: OCPP20TransactionEventEnumType.ENDED,
+ transactionId: options.transactionId,
+ ...buildHashIdsPayload(resolvedHashIds),
+ }
+ break
+ default:
+ throw new Error(UNSUPPORTED_OCPP_VERSION_ERROR)
+ }
+ await runAction(program, procedureName, payload, undefined, config)
+ } else {
+ // Low-level passthrough: -p provided, uses OCPP 1.6 procedure; for 2.0.x raw payloads use ocpp transaction-event
+ procedureName = ProcedureName.STOP_TRANSACTION
+ payload = buildHashIdsPayload(hashIds)
+ await runAction(program, procedureName, payload, options.payload)
+ }
+ })
}
- await runAction(program, ProcedureName.STOP_TRANSACTION, payload, options.payload)
- })
+ )
return cmd
}
--- /dev/null
+/** @file Unit tests for resolveOcppVersionFromList */
+
+import assert from 'node:assert'
+import { describe, it } from 'node:test'
+import { OCPPVersion } from 'ui-common'
+
+import { resolveOcppVersionFromList } from '../src/commands/action.js'
+
+interface StationEntry {
+ stationInfo: { hashId: string; ocppVersion?: OCPPVersion }
+}
+
+const station = (hashId: string, ocppVersion?: OCPPVersion): StationEntry => ({
+ stationInfo: { hashId, ocppVersion },
+})
+
+await describe('resolveOcppVersionFromList', async () => {
+ await describe('empty station list', async () => {
+ await it('returns undefined when list is empty and no hashIds given', () => {
+ assert.strictEqual(resolveOcppVersionFromList([], []), undefined)
+ })
+
+ await it('returns undefined when list is empty and hashIds are given', () => {
+ assert.strictEqual(resolveOcppVersionFromList(['abc'], []), undefined)
+ })
+ })
+
+ await describe('no hashIds (all stations)', async () => {
+ await it('returns the common version when all stations share one version', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_201),
+ station('bbb222', OCPPVersion.VERSION_201),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList([], stations), OCPPVersion.VERSION_201)
+ })
+
+ await it('returns undefined when stations have heterogeneous versions', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_16),
+ station('bbb222', OCPPVersion.VERSION_201),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList([], stations), undefined)
+ })
+
+ await it('returns undefined when ocppVersion is missing on any station', () => {
+ const stations = [station('aaa111', OCPPVersion.VERSION_16), station('bbb222', undefined)]
+ assert.strictEqual(resolveOcppVersionFromList([], stations), undefined)
+ })
+
+ await it('returns undefined when ocppVersion is missing on all stations', () => {
+ const stations = [station('aaa111', undefined), station('bbb222', undefined)]
+ assert.strictEqual(resolveOcppVersionFromList([], stations), undefined)
+ })
+ })
+
+ await describe('hashId filtering', async () => {
+ await it('returns version when exact hashId matches', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_16),
+ station('bbb222', OCPPVersion.VERSION_201),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList(['aaa111'], stations), OCPPVersion.VERSION_16)
+ })
+
+ await it('matches by prefix', () => {
+ const stations = [
+ station('aaa111bbbccc', OCPPVersion.VERSION_201),
+ station('xxx999', OCPPVersion.VERSION_16),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList(['aaa'], stations), OCPPVersion.VERSION_201)
+ })
+
+ await it('returns the common version when an ambiguous prefix matches multiple stations with homogeneous versions', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_201),
+ station('aaa222', OCPPVersion.VERSION_201),
+ station('bbb333', OCPPVersion.VERSION_16),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList(['aaa'], stations), OCPPVersion.VERSION_201)
+ })
+
+ await it('returns undefined when prefix matches no station', () => {
+ const stations = [station('aaa111', OCPPVersion.VERSION_16)]
+ assert.strictEqual(resolveOcppVersionFromList(['zzz'], stations), undefined)
+ })
+
+ await it('returns undefined when matched stations have heterogeneous versions', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_16),
+ station('aaa222', OCPPVersion.VERSION_201),
+ ]
+ assert.strictEqual(resolveOcppVersionFromList(['aaa'], stations), undefined)
+ })
+
+ await it("returns version when some hashIds match and others don't", () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_201),
+ station('bbb222', OCPPVersion.VERSION_16),
+ ]
+ // 'aaa' matches aaa111, 'zzz' matches nothing — only aaa111 targeted
+ assert.strictEqual(
+ resolveOcppVersionFromList(['aaa', 'zzz'], stations),
+ OCPPVersion.VERSION_201
+ )
+ })
+
+ await it('returns common version when multiple hashIds all resolve to same version', () => {
+ const stations = [
+ station('aaa111', OCPPVersion.VERSION_201),
+ station('bbb222', OCPPVersion.VERSION_201),
+ station('ccc333', OCPPVersion.VERSION_16),
+ ]
+ assert.strictEqual(
+ resolveOcppVersionFromList(['aaa111', 'bbb222'], stations),
+ OCPPVersion.VERSION_201
+ )
+ })
+
+ await it('returns undefined when matched stations have unknown/missing ocppVersion', () => {
+ const stations = [station('aaa111', undefined), station('bbb222', OCPPVersion.VERSION_201)]
+ assert.strictEqual(resolveOcppVersionFromList(['aaa111'], stations), undefined)
+ })
+ })
+
+ await describe('OCPP version values', async () => {
+ await it('returns VERSION_16 correctly', () => {
+ assert.strictEqual(
+ resolveOcppVersionFromList([], [station('a', OCPPVersion.VERSION_16)]),
+ OCPPVersion.VERSION_16
+ )
+ })
+
+ await it('returns VERSION_20 correctly', () => {
+ assert.strictEqual(
+ resolveOcppVersionFromList([], [station('a', OCPPVersion.VERSION_20)]),
+ OCPPVersion.VERSION_20
+ )
+ })
+
+ await it('returns VERSION_201 correctly', () => {
+ assert.strictEqual(
+ resolveOcppVersionFromList([], [station('a', OCPPVersion.VERSION_201)]),
+ OCPPVersion.VERSION_201
+ )
+ })
+ })
+})
UPDATED = 'Updated',
}
+export enum OCPP20TriggerReasonEnumType {
+ ABNORMAL_CONDITION = 'AbnormalCondition',
+ AUTHORIZED = 'Authorized',
+ CABLE_PLUGGED_IN = 'CablePluggedIn',
+ CHARGING_RATE_CHANGED = 'ChargingRateChanged',
+ CHARGING_STATE_CHANGED = 'ChargingStateChanged',
+ DEAUTHORIZED = 'Deauthorized',
+ ENERGY_LIMIT_REACHED = 'EnergyLimitReached',
+ EV_COMMUNICATION_LOST = 'EVCommunicationLost',
+ EV_CONNECT_TIMEOUT = 'EVConnectTimeout',
+ EV_DEPARTED = 'EVDeparted',
+ EV_DETECTED = 'EVDetected',
+ METER_VALUE_CLOCK = 'MeterValueClock',
+ METER_VALUE_PERIODIC = 'MeterValuePeriodic',
+ REMOTE_START = 'RemoteStart',
+ REMOTE_STOP = 'RemoteStop',
+ RESET_COMMAND = 'ResetCommand',
+ SIGNED_DATA_RECEIVED = 'SignedDataReceived',
+ STOP_AUTHORIZED = 'StopAuthorized',
+ TIME_LIMIT_REACHED = 'TimeLimitReached',
+ TRIGGER = 'Trigger',
+ UNLOCK_COMMAND = 'UnlockCommand',
+}
+
export enum OCPPProtocol {
JSON = 'json',
}