#### ocpp
-Send OCPP commands directly to charging stations:
+Request charging station(s) to send OCPP messages to the CSMS:
```shell
-evse-cli ocpp heartbeat [hashId...]
-evse-cli ocpp authorize --id-tag <tag> [hashId...]
-evse-cli ocpp boot-notification [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 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
```
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`.
---
name: evse-simulator
-description: Control and monitor an OCPP charging station simulator via CLI. Use when users ask to manage charging stations, send OCPP messages, start/stop the simulator or ATG, list stations or templates, or check simulator state.
+description: Control and monitor an OCPP charging station simulator via CLI. Use when users ask to manage charging stations, trigger OCPP messages from stations to the CSMS, start/stop the simulator or ATG, list stations or templates, or check simulator state.
license: Apache-2.0
compatibility: Requires the simulator UI server to be running with WebSocket enabled.
metadata:
### OCPP Messages
+Request station(s) to send OCPP messages to the CSMS:
+
```shell
evse-cli ocpp heartbeat [hashId...]
evse-cli ocpp boot-notification [hashId...]
evse-cli atg start
```
-### Send OCPP heartbeat to all stations
+### Request all stations to send OCPP Heartbeat
```shell
evse-cli ocpp heartbeat
cmd
.command('open [hashIds...]')
- .description('Open WebSocket connection')
+ .description('Open WebSocket connection to CSMS on station(s)')
.action(async (hashIds: string[]) => {
const payload: RequestPayload = buildHashIdsPayload(hashIds)
await runAction(program, ProcedureName.OPEN_CONNECTION, payload)
cmd
.command('close [hashIds...]')
- .description('Close WebSocket connection')
+ .description('Close WebSocket connection to CSMS on station(s)')
.action(async (hashIds: string[]) => {
const payload: RequestPayload = buildHashIdsPayload(hashIds)
await runAction(program, ProcedureName.CLOSE_CONNECTION, payload)
cmd
.command('lock [hashIds...]')
- .description('Lock a connector')
+ .description('Lock connector on station(s)')
.requiredOption('--connector-id <id>', 'connector ID', parseInteger)
.action(async (hashIds: string[], options: { connectorId: number }) => {
const payload: RequestPayload = {
cmd
.command('unlock [hashIds...]')
- .description('Unlock a connector')
+ .description('Unlock connector on station(s)')
.requiredOption('--connector-id <id>', 'connector ID', parseInteger)
.action(async (hashIds: string[], options: { connectorId: number }) => {
const payload: RequestPayload = {
cmd
.command('authorize [hashIds...]')
- .description('Send OCPP Authorize')
+ .description('Request station(s) to send OCPP Authorize')
.requiredOption('--id-tag <tag>', 'RFID tag for authorization')
.action(async (hashIds: string[], options: { idTag: string }) => {
const payload: RequestPayload = {
cmd
.command('data-transfer [hashIds...]')
- .description('Send OCPP DataTransfer')
+ .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)')
cmd
.command('meter-values [hashIds...]')
- .description('Send OCPP MeterValues')
+ .description('Request station(s) to send OCPP MeterValues')
.requiredOption('--connector-id <id>', 'connector ID', parseInteger)
.action(async (hashIds: string[], options: { connectorId: number }) => {
const payload: RequestPayload = {
cmd
.command('status-notification [hashIds...]')
- .description('Send OCPP StatusNotification')
+ .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')
)
const simpleOcppCommands: [string, string, ProcedureName][] = [
- ['boot-notification', 'Send OCPP BootNotification', ProcedureName.BOOT_NOTIFICATION],
+ [
+ 'boot-notification',
+ 'Request station(s) to send OCPP BootNotification',
+ ProcedureName.BOOT_NOTIFICATION,
+ ],
[
'diagnostics-status-notification',
- 'Send OCPP DiagnosticsStatusNotification',
+ 'Request station(s) to send OCPP DiagnosticsStatusNotification',
ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
],
[
'firmware-status-notification',
- 'Send OCPP FirmwareStatusNotification',
+ 'Request station(s) to send OCPP FirmwareStatusNotification',
ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
],
[
'get-15118-ev-certificate',
- 'Send OCPP Get15118EVCertificate',
+ 'Request station(s) to send OCPP Get15118EVCertificate',
ProcedureName.GET_15118_EV_CERTIFICATE,
],
[
'get-certificate-status',
- 'Send OCPP GetCertificateStatus',
+ 'Request station(s) to send OCPP GetCertificateStatus',
ProcedureName.GET_CERTIFICATE_STATUS,
],
- ['heartbeat', 'Send OCPP Heartbeat', ProcedureName.HEARTBEAT],
+ ['heartbeat', 'Request station(s) to send OCPP Heartbeat', ProcedureName.HEARTBEAT],
[
'log-status-notification',
- 'Send OCPP LogStatusNotification',
+ 'Request station(s) to send OCPP LogStatusNotification',
ProcedureName.LOG_STATUS_NOTIFICATION,
],
[
'notify-customer-information',
- 'Send OCPP NotifyCustomerInformation',
+ 'Request station(s) to send OCPP NotifyCustomerInformation',
ProcedureName.NOTIFY_CUSTOMER_INFORMATION,
],
- ['notify-report', 'Send OCPP NotifyReport', ProcedureName.NOTIFY_REPORT],
+ ['notify-report', 'Request station(s) to send OCPP NotifyReport', ProcedureName.NOTIFY_REPORT],
[
'security-event-notification',
- 'Send OCPP SecurityEventNotification',
+ 'Request station(s) to send OCPP SecurityEventNotification',
ProcedureName.SECURITY_EVENT_NOTIFICATION,
],
- ['sign-certificate', 'Send OCPP SignCertificate', ProcedureName.SIGN_CERTIFICATE],
- ['transaction-event', 'Send OCPP TransactionEvent', ProcedureName.TRANSACTION_EVENT],
+ [
+ 'sign-certificate',
+ 'Request station(s) to send OCPP SignCertificate',
+ ProcedureName.SIGN_CERTIFICATE,
+ ],
+ [
+ 'transaction-event',
+ 'Request station(s) to send OCPP TransactionEvent',
+ ProcedureName.TRANSACTION_EVENT,
+ ],
]
for (const [name, description, procedureName] of simpleOcppCommands) {
cmd
.command('start [hashIds...]')
- .description('Start a transaction')
+ .description('Start a transaction on station(s)')
.requiredOption('--connector-id <id>', 'connector ID', parseInteger)
.requiredOption('--id-tag <tag>', 'RFID tag for authorization')
.action(async (hashIds: string[], options: { connectorId: number; idTag: string }) => {
cmd
.command('stop [hashIds...]')
- .description('Stop a transaction')
+ .description('Stop a transaction on station(s)')
.requiredOption('--transaction-id <id>', 'transaction ID', parseInteger)
.action(async (hashIds: string[], options: { transactionId: number }) => {
const payload: RequestPayload = {
import chalk from 'chalk'
import Table from 'cli-table3'
-import { type ConnectorEntry, type EvseEntry, OCPP16ChargePointStatus } from 'ui-common'
+import { type ConnectorEntry, type EvseEntry } from 'ui-common'
const NO_BORDER = {
bottom: '',
}
}
-export const countConnectors = (
- evses: EvseEntry[],
- connectors: ConnectorEntry[]
-): { available: number; total: number } => {
- let total = 0
- let available = 0
+const STATUS_ABBREVIATIONS: Record<string, string> = {
+ Finishing: 'Fi',
+ SuspendedEV: 'SE',
+ SuspendedEVSE: 'SS',
+}
+
+const statusLetter = (status: string | undefined): string => {
+ if (status == null || status.length === 0) return '?'
+ return STATUS_ABBREVIATIONS[status] ?? status.charAt(0).toUpperCase()
+}
+
+export const formatConnectors = (evses: EvseEntry[], connectors: ConnectorEntry[]): string => {
+ const entries: string[] = []
if (evses.length > 0) {
for (const evse of evses) {
if (evse.evseId > 0) {
for (const c of evse.evseStatus.connectors) {
if (c.connectorId > 0) {
- total++
- if (c.connectorStatus.status === OCPP16ChargePointStatus.AVAILABLE) {
- available++
- }
+ entries.push(`${c.connectorId.toString()}:${statusLetter(c.connectorStatus.status)}`)
}
}
}
} else {
for (const c of connectors) {
if (c.connectorId > 0) {
- total++
- if (c.connectorStatus.status === OCPP16ChargePointStatus.AVAILABLE) {
- available++
- }
+ entries.push(`${c.connectorId.toString()}:${statusLetter(c.connectorStatus.status)}`)
}
}
}
- return { available, total }
+ return entries.length > 0 ? chalk.dim(entries.join(' ')) : chalk.dim('–')
}
export const fuzzyTime = (ts: number | undefined): string => {
import {
borderlessTable,
- countConnectors,
+ formatConnectors,
fuzzyTime,
statusIcon,
truncateId,
return
}
- const table = borderlessTable(['', 'Name', 'Hash ID', 'WS', 'OCPP', 'Template', 'Updated'])
+ const table = borderlessTable([
+ '',
+ 'Name',
+ 'Hash ID',
+ 'Reg',
+ 'WS',
+ 'Connectors',
+ 'OCPP',
+ 'Template',
+ 'Updated',
+ ])
for (const cs of stations) {
const si = cs.stationInfo
- const { available, total } = countConnectors(cs.evses ?? [], cs.connectors ?? [])
+ const reg = cs.bootNotificationResponse?.status
table.push([
statusIcon(cs.started),
si.chargingStationId,
chalk.dim(truncateId(si.hashId)),
- `${wsIcon(cs.wsState)} ${chalk.dim(`${available.toString()}/${total.toString()}`)}`,
+ reg ?? chalk.dim('–'),
+ wsIcon(cs.wsState),
+ formatConnectors(cs.evses ?? [], cs.connectors ?? []),
chalk.dim(si.ocppVersion ?? '–'),
chalk.dim(si.templateName.replace('.station-template', '')),
fuzzyTime(cs.timestamp),
import { describe, it } from 'node:test'
import { OCPP16AvailabilityType, OCPP16ChargePointStatus } from 'ui-common'
-import { countConnectors, fuzzyTime, statusIcon, truncateId, wsIcon } from '../src/output/format.js'
+import {
+ formatConnectors,
+ fuzzyTime,
+ statusIcon,
+ truncateId,
+ wsIcon,
+} from '../src/output/format.js'
await describe('format helpers', async () => {
await describe('truncateId', async () => {
})
})
- await describe('countConnectors', async () => {
- await it('counts from evses when evses present (evseId > 0, connectorId > 0)', () => {
- const evses = [
- {
- evseId: 1,
- evseStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- connectors: [
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ await describe('formatConnectors', async () => {
+ await it('formats connectors from evses', () => {
+ const result = formatConnectors(
+ [
+ {
+ evseId: 1,
+ evseStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ connectors: [
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.AVAILABLE,
+ },
},
- },
- {
- connectorId: 2,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.CHARGING,
+ {
+ connectorId: 2,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.OCCUPIED,
+ },
},
- },
- ],
+ ],
+ },
},
- },
- ]
- const result = countConnectors(evses, [])
- assert.deepStrictEqual(result, { available: 1, total: 2 })
+ ],
+ []
+ )
+ assert.ok(result.includes('1:A'))
+ assert.ok(result.includes('2:O'))
})
await it('skips evseId 0', () => {
- const evses = [
- {
- evseId: 0,
- evseStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- connectors: [
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ const result = formatConnectors(
+ [
+ {
+ evseId: 0,
+ evseStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ connectors: [
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.AVAILABLE,
+ },
},
- },
- ],
+ ],
+ },
},
- },
- {
- evseId: 1,
- evseStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- connectors: [
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
- },
- },
- ],
- },
- },
- ]
- const result = countConnectors(evses, [])
- assert.deepStrictEqual(result, { available: 1, total: 1 })
+ ],
+ []
+ )
+ assert.ok(result.includes('–'))
})
- await it('skips connectorId 0 within evses', () => {
- const evses = [
- {
- evseId: 1,
- evseStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- connectors: [
- {
- connectorId: 0,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ await it('skips connectorId 0', () => {
+ const result = formatConnectors(
+ [
+ {
+ evseId: 1,
+ evseStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ connectors: [
+ {
+ connectorId: 0,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.AVAILABLE,
+ },
},
- },
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.AVAILABLE,
+ },
},
- },
- ],
+ ],
+ },
},
- },
- ]
- const result = countConnectors(evses, [])
- assert.deepStrictEqual(result, { available: 1, total: 1 })
- })
-
- await it('counts available connectors (status === AVAILABLE)', () => {
- const evses = [
- {
- evseId: 1,
- evseStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- connectors: [
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
- },
- },
- {
- connectorId: 2,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.UNAVAILABLE,
- },
- },
- {
- connectorId: 3,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.FAULTED,
- },
- },
- ],
- },
- },
- ]
- const result = countConnectors(evses, [])
- assert.deepStrictEqual(result, { available: 1, total: 3 })
+ ],
+ []
+ )
+ assert.ok(result.includes('1:A'))
+ assert.ok(!result.includes('0:'))
})
await it('falls back to connectors array when evses empty', () => {
- const connectors = [
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ const result = formatConnectors(
+ [],
+ [
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.AVAILABLE,
+ },
},
- },
- {
- connectorId: 2,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.CHARGING,
+ {
+ connectorId: 2,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.CHARGING,
+ },
},
- },
- ]
- const result = countConnectors([], connectors)
- assert.deepStrictEqual(result, { available: 1, total: 2 })
+ ]
+ )
+ assert.ok(result.includes('1:A'))
+ assert.ok(result.includes('2:C'))
+ })
+
+ await it('returns dash for empty arrays', () => {
+ const result = formatConnectors([], [])
+ assert.ok(result.includes('–'))
})
- await it('skips connectorId 0 in connectors fallback', () => {
- const connectors = [
- {
- connectorId: 0,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ await it('uses two-letter abbreviations for ambiguous statuses', () => {
+ const result = formatConnectors(
+ [],
+ [
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.SUSPENDED_EV,
+ },
},
- },
- {
- connectorId: 1,
- connectorStatus: {
- availability: OCPP16AvailabilityType.OPERATIVE,
- status: OCPP16ChargePointStatus.AVAILABLE,
+ {
+ connectorId: 2,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.SUSPENDED_EVSE,
+ },
},
- },
- ]
- const result = countConnectors([], connectors)
- assert.deepStrictEqual(result, { available: 1, total: 1 })
+ {
+ connectorId: 3,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.FINISHING,
+ },
+ },
+ {
+ connectorId: 4,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: OCPP16ChargePointStatus.FAULTED,
+ },
+ },
+ ]
+ )
+ assert.ok(result.includes('1:SE'))
+ assert.ok(result.includes('2:SS'))
+ assert.ok(result.includes('3:Fi'))
+ assert.ok(result.includes('4:F'))
})
- await it('returns {available: 0, total: 0} for empty arrays', () => {
- const result = countConnectors([], [])
- assert.deepStrictEqual(result, { available: 0, total: 0 })
+ await it('handles undefined status', () => {
+ const result = formatConnectors(
+ [],
+ [
+ {
+ connectorId: 1,
+ connectorStatus: {
+ availability: OCPP16AvailabilityType.OPERATIVE,
+ status: undefined as unknown as OCPP16ChargePointStatus,
+ },
+ },
+ ]
+ )
+ assert.ok(result.includes('1:?'))
})
})