kirby-ocpp 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +192 -88
  3. package/THIRD_PARTY_NOTICES.md +6 -2
  4. package/bin/kirby-ocpp.js +77 -0
  5. package/package.json +23 -44
  6. package/dist/cli.d.ts +0 -2
  7. package/dist/cli.js +0 -72
  8. package/dist/config.d.ts +0 -2
  9. package/dist/config.js +0 -53
  10. package/dist/ocpp/client.d.ts +0 -59
  11. package/dist/ocpp/client.js +0 -299
  12. package/dist/ocpp/schema-validator.d.ts +0 -15
  13. package/dist/ocpp/schema-validator.js +0 -75
  14. package/dist/ocpp/schemas/json/Authorize.json +0 -16
  15. package/dist/ocpp/schemas/json/AuthorizeResponse.json +0 -40
  16. package/dist/ocpp/schemas/json/BootNotification.json +0 -49
  17. package/dist/ocpp/schemas/json/BootNotificationResponse.json +0 -30
  18. package/dist/ocpp/schemas/json/CancelReservation.json +0 -15
  19. package/dist/ocpp/schemas/json/CancelReservationResponse.json +0 -20
  20. package/dist/ocpp/schemas/json/ChangeAvailability.json +0 -24
  21. package/dist/ocpp/schemas/json/ChangeAvailabilityResponse.json +0 -21
  22. package/dist/ocpp/schemas/json/ChangeConfiguration.json +0 -21
  23. package/dist/ocpp/schemas/json/ChangeConfigurationResponse.json +0 -22
  24. package/dist/ocpp/schemas/json/ClearCache.json +0 -8
  25. package/dist/ocpp/schemas/json/ClearCacheResponse.json +0 -20
  26. package/dist/ocpp/schemas/json/ClearChargingProfile.json +0 -27
  27. package/dist/ocpp/schemas/json/ClearChargingProfileResponse.json +0 -20
  28. package/dist/ocpp/schemas/json/DataTransfer.json +0 -23
  29. package/dist/ocpp/schemas/json/DataTransferResponse.json +0 -25
  30. package/dist/ocpp/schemas/json/DiagnosticsStatusNotification.json +0 -22
  31. package/dist/ocpp/schemas/json/DiagnosticsStatusNotificationResponse.json +0 -8
  32. package/dist/ocpp/schemas/json/FirmwareStatusNotification.json +0 -25
  33. package/dist/ocpp/schemas/json/FirmwareStatusNotificationResponse.json +0 -8
  34. package/dist/ocpp/schemas/json/GetCompositeSchedule.json +0 -27
  35. package/dist/ocpp/schemas/json/GetCompositeScheduleResponse.json +0 -79
  36. package/dist/ocpp/schemas/json/GetConfiguration.json +0 -16
  37. package/dist/ocpp/schemas/json/GetConfigurationResponse.json +0 -40
  38. package/dist/ocpp/schemas/json/GetDiagnostics.json +0 -30
  39. package/dist/ocpp/schemas/json/GetDiagnosticsResponse.json +0 -13
  40. package/dist/ocpp/schemas/json/GetLocalListVersion.json +0 -8
  41. package/dist/ocpp/schemas/json/GetLocalListVersionResponse.json +0 -15
  42. package/dist/ocpp/schemas/json/Heartbeat.json +0 -8
  43. package/dist/ocpp/schemas/json/HeartbeatResponse.json +0 -16
  44. package/dist/ocpp/schemas/json/MeterValues.json +0 -151
  45. package/dist/ocpp/schemas/json/MeterValuesResponse.json +0 -8
  46. package/dist/ocpp/schemas/json/RemoteStartTransaction.json +0 -127
  47. package/dist/ocpp/schemas/json/RemoteStartTransactionResponse.json +0 -20
  48. package/dist/ocpp/schemas/json/RemoteStopTransaction.json +0 -15
  49. package/dist/ocpp/schemas/json/RemoteStopTransactionResponse.json +0 -20
  50. package/dist/ocpp/schemas/json/ReserveNow.json +0 -33
  51. package/dist/ocpp/schemas/json/ReserveNowResponse.json +0 -23
  52. package/dist/ocpp/schemas/json/Reset.json +0 -20
  53. package/dist/ocpp/schemas/json/ResetResponse.json +0 -20
  54. package/dist/ocpp/schemas/json/SendLocalList.json +0 -68
  55. package/dist/ocpp/schemas/json/SendLocalListResponse.json +0 -22
  56. package/dist/ocpp/schemas/json/SetChargingProfile.json +0 -124
  57. package/dist/ocpp/schemas/json/SetChargingProfileResponse.json +0 -21
  58. package/dist/ocpp/schemas/json/StartTransaction.json +0 -32
  59. package/dist/ocpp/schemas/json/StartTransactionResponse.json +0 -44
  60. package/dist/ocpp/schemas/json/StatusNotification.json +0 -70
  61. package/dist/ocpp/schemas/json/StatusNotificationResponse.json +0 -8
  62. package/dist/ocpp/schemas/json/StopTransaction.json +0 -177
  63. package/dist/ocpp/schemas/json/StopTransactionResponse.json +0 -37
  64. package/dist/ocpp/schemas/json/TriggerMessage.json +0 -27
  65. package/dist/ocpp/schemas/json/TriggerMessageResponse.json +0 -21
  66. package/dist/ocpp/schemas/json/UnlockConnector.json +0 -15
  67. package/dist/ocpp/schemas/json/UnlockConnectorResponse.json +0 -21
  68. package/dist/ocpp/schemas/json/UpdateFirmware.json +0 -27
  69. package/dist/ocpp/schemas/json/UpdateFirmwareResponse.json +0 -8
  70. package/dist/ocpp/types.d.ts +0 -106
  71. package/dist/ocpp/types.js +0 -1
  72. package/dist/station/authorization.d.ts +0 -20
  73. package/dist/station/authorization.js +0 -78
  74. package/dist/station/charging-profile-registry.d.ts +0 -18
  75. package/dist/station/charging-profile-registry.js +0 -125
  76. package/dist/station/configuration-commands.d.ts +0 -11
  77. package/dist/station/configuration-commands.js +0 -29
  78. package/dist/station/configuration-registry.d.ts +0 -13
  79. package/dist/station/configuration-registry.js +0 -39
  80. package/dist/station/configuration.d.ts +0 -7
  81. package/dist/station/configuration.js +0 -88
  82. package/dist/station/connector-registry.d.ts +0 -25
  83. package/dist/station/connector-registry.js +0 -89
  84. package/dist/station/connector-status.d.ts +0 -7
  85. package/dist/station/connector-status.js +0 -66
  86. package/dist/station/local-auth-list.d.ts +0 -8
  87. package/dist/station/local-auth-list.js +0 -31
  88. package/dist/station/maintenance-lifecycle.d.ts +0 -25
  89. package/dist/station/maintenance-lifecycle.js +0 -110
  90. package/dist/station/meter-value-scheduler.d.ts +0 -21
  91. package/dist/station/meter-value-scheduler.js +0 -78
  92. package/dist/station/metering.d.ts +0 -15
  93. package/dist/station/metering.js +0 -138
  94. package/dist/station/ocpp-message.d.ts +0 -2
  95. package/dist/station/ocpp-message.js +0 -9
  96. package/dist/station/offline-transaction-sync.d.ts +0 -29
  97. package/dist/station/offline-transaction-sync.js +0 -99
  98. package/dist/station/payload.d.ts +0 -4
  99. package/dist/station/payload.js +0 -12
  100. package/dist/station/persistence.d.ts +0 -28
  101. package/dist/station/persistence.js +0 -206
  102. package/dist/station/reservation-rules.d.ts +0 -3
  103. package/dist/station/reservation-rules.js +0 -18
  104. package/dist/station/reservation-scheduler.d.ts +0 -11
  105. package/dist/station/reservation-scheduler.js +0 -33
  106. package/dist/station/smart-charging.d.ts +0 -45
  107. package/dist/station/smart-charging.js +0 -251
  108. package/dist/station/status-scheduler.d.ts +0 -16
  109. package/dist/station/status-scheduler.js +0 -58
  110. package/dist/station/transaction-message-retry.d.ts +0 -3
  111. package/dist/station/transaction-message-retry.js +0 -7
  112. package/dist/station/transaction-rules.d.ts +0 -7
  113. package/dist/station/transaction-rules.js +0 -34
  114. package/dist/station/trigger-message.d.ts +0 -2
  115. package/dist/station/trigger-message.js +0 -11
  116. package/dist/station-store.d.ts +0 -41
  117. package/dist/station-store.js +0 -104
  118. package/dist/station.d.ts +0 -113
  119. package/dist/station.js +0 -1204
  120. package/dist/tui.d.ts +0 -96
  121. package/dist/tui.js +0 -1656
  122. package/docs/OCPP_1.6J_CONFORMANCE.md +0 -61
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026
3
+ Copyright (c) 2026 Kirill Belolipetsky
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,130 +1,234 @@
1
1
  # kirby-ocpp
2
2
 
3
- Terminal OCPP 1.6J charge point emulator for local and staging CSMS testing.
3
+ Terminal OCPP 1.6J charge point emulator for local and staging CSMS testing, written in Rust.
4
4
 
5
- It runs as an interactive TUI, speaks OCPP 1.6 JSON over WebSocket, validates payloads against bundled OCPP 1.6J JSON schemas, and can persist station state per charge point id.
5
+ It speaks OCPP 1.6 JSON over WebSocket (`ws://` and `wss://`, including mutual TLS), validates every payload against the bundled official OCPP 1.6J JSON schemas, persists station state per charge point id, and can be used three ways:
6
+
7
+ - an interactive terminal UI (default when stdout is a terminal);
8
+ - a headless event logger (`--no-tui`, `--json`);
9
+ - a scripted scenario runner for CI (`kirby-ocpp run --scenario ...`).
6
10
 
7
11
  ## Install
8
12
 
9
- Run without installing:
13
+ With Node.js (no Rust needed; installs the prebuilt binary for your platform):
10
14
 
11
15
  ```bash
12
16
  npx kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
13
- ```
14
-
15
- Or install globally:
16
-
17
- ```bash
18
17
  npm install -g kirby-ocpp
19
- kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
20
18
  ```
21
19
 
22
- ## Usage
20
+ With Cargo (Rust 1.88 or newer):
23
21
 
24
22
  ```bash
25
- kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
23
+ cargo install kirby-ocpp
26
24
  ```
27
25
 
28
- Common options:
26
+ Or download a prebuilt binary for Linux (x86_64 glibc, aarch64 glibc, x86_64 static musl for Alpine and minimal containers), macOS or Windows from the [GitHub releases](https://github.com/archhaze24/kirby-ocpp/releases). To build from a checkout: `cargo install --path crates/kirby-ocpp`.
29
27
 
30
- ```bash
31
- kirby-ocpp \
32
- --url ws://localhost:9000/ocpp \
33
- --id CP-001 \
34
- --vendor Kirby \
35
- --model TUI-16 \
36
- --connector 1 \
37
- --connectors 2 \
38
- --heartbeat 30
39
- ```
40
-
41
- WSS/TLS options:
28
+ ## Quick start
42
29
 
43
30
  ```bash
44
- kirby-ocpp \
45
- --url wss://csms.example.com/ocpp \
46
- --id CP-001 \
47
- --tls-ca ./ca.pem \
48
- --tls-cert ./client-cert.pem \
49
- --tls-key ./client-key.pem
31
+ kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 # TUI
32
+ kirby-ocpp --url ws://localhost:9000/ocpp --no-tui --json # headless, JSON lines
33
+ kirby-ocpp --url ws://localhost:9000/ocpp run --scenario charge-session
50
34
  ```
51
35
 
52
- Useful runtime flags:
53
-
54
- - `--no-persist`: run an ephemeral station.
55
- - `--state-dir <path>`: choose where station state is stored.
56
- - `--ws-subprotocol <protocol>`: defaults to `ocpp1.6`.
57
- - `--ws-ping <seconds>`: WebSocket ping interval, `0` disables pings.
58
- - `--no-reconnect`: disable automatic reconnect.
59
- - `--reconnect-initial-ms <ms>`, `--reconnect-max-ms <ms>`, `--reconnect-max-attempts <count>`: reconnect backoff.
60
- - `--call-timeout-ms <ms>`: OCPP CALL response timeout.
61
- - `--tls-server-name <name>` and `--tls-skip-verify`: local/self-signed WSS testing.
62
-
63
- Environment variables mirror the main flags: `OCPP_URL`, `OCPP_CHARGE_POINT_ID`, `OCPP_VENDOR`, `OCPP_MODEL`, `OCPP_WS_SUBPROTOCOL`, `OCPP_WS_PING_INTERVAL`, `OCPP_RECONNECT`, `OCPP_RECONNECT_INITIAL_MS`, `OCPP_RECONNECT_MAX_MS`, `OCPP_RECONNECT_MAX_ATTEMPTS`, `OCPP_CALL_TIMEOUT_MS`, `OCPP_TLS_CA_FILE`, `OCPP_TLS_CERT_FILE`, `OCPP_TLS_KEY_FILE`, `OCPP_TLS_SERVER_NAME`, and `OCPP_TLS_SKIP_VERIFY`.
36
+ `--url` (or `OCPP_URL`) is the only required option. Precedence is flag, then environment variable, then default. Booleans accept `true/false/yes/no/y/n/on/off/1/0`.
37
+
38
+ ## Options
39
+
40
+ | Flag | Env | Default | Meaning |
41
+ | --- | --- | --- | --- |
42
+ | `-u, --url` | `OCPP_URL` | required | CSMS WebSocket URL (`ws://` or `wss://`) |
43
+ | `-i, --id` | `OCPP_CHARGE_POINT_ID` | `CP-001` | Charge point id |
44
+ | `--vendor` | `OCPP_VENDOR` | `archhaze24` | BootNotification vendor |
45
+ | `--model` | `OCPP_MODEL` | `KIRBY` | BootNotification model |
46
+ | `--serial` | `OCPP_SERIAL` | none | Serial number (max 25 chars) |
47
+ | `--firmware-version` | `OCPP_FIRMWARE_VERSION` | none | Firmware version (max 50 chars) |
48
+ | `-c, --connector` | | `1` | Initial connector |
49
+ | `--connectors` | | `1` | Number of connectors |
50
+ | `--heartbeat` | | `30` | Fallback heartbeat interval (s) |
51
+ | `--nominal-power-w` | | `7200` | Nominal charging power per connector (W); charging profiles can only lower it |
52
+ | `--voltage-v` | | `230` | Voltage per phase (V), used for A to W conversion and the `Voltage` reading |
53
+ | `--phases` | | `1` | Number of phases (1-3) |
54
+ | `--ws-subprotocol` | `OCPP_WS_SUBPROTOCOL` | `ocpp1.6` | Requested subprotocol |
55
+ | `--ws-ping` | `OCPP_WS_PING_INTERVAL` | `30` | WebSocket ping interval (s), `0` disables |
56
+ | `--reconnect <BOOL>` | `OCPP_RECONNECT` | `true` | Auto-reconnect after unexpected disconnect |
57
+ | `--no-reconnect` | | | Disable auto-reconnect (wins over `--reconnect`) |
58
+ | `--reconnect-initial-ms` | `OCPP_RECONNECT_INITIAL_MS` | `1000` | Initial backoff; `0` is rejected (exit 2) unless `--reconnect-max-attempts` is set or reconnect is off |
59
+ | `--reconnect-max-ms` | `OCPP_RECONNECT_MAX_MS` | `30000` | Maximum backoff |
60
+ | `--reconnect-max-attempts` | `OCPP_RECONNECT_MAX_ATTEMPTS` | `0` | `0` = unlimited |
61
+ | `--call-timeout-ms` | `OCPP_CALL_TIMEOUT_MS` | `30000` | CALL response timeout |
62
+ | `--tls-ca` | `OCPP_TLS_CA_FILE` | system roots | CA bundle (PEM) |
63
+ | `--tls-cert`, `--tls-key` | `OCPP_TLS_CERT_FILE`, `OCPP_TLS_KEY_FILE` | | Client certificate and key for mTLS (both or neither) |
64
+ | `--tls-server-name` | `OCPP_TLS_SERVER_NAME` | URL host | SNI / verification name override |
65
+ | `--tls-skip-verify` | `OCPP_TLS_SKIP_VERIFY` | off | Skip certificate verification (testing only) |
66
+ | `--no-persist` | `KIRBY_OCPP_NO_PERSIST` | off | Do not load or save station state |
67
+ | `--state-dir` | `KIRBY_OCPP_STATE_DIR` | `~/.kirby-ocpp/stations` | State directory (`%USERPROFILE%` on Windows) |
68
+ | `--count` | `OCPP_COUNT` | `1` | Number of stations (fleet mode, 1-1000), see [Fleet mode](#fleet-mode) |
69
+ | `--id-template` | `OCPP_ID_TEMPLATE` | the `--id` value | Charge point id template with `{n}` / `{n:03}` |
70
+ | `--first-index` | `OCPP_FIRST_INDEX` | `1` | Index of the first station |
71
+ | `--stagger-ms` | `OCPP_STAGGER_MS` | `0` | Delay between starting two stations (ms) |
72
+ | `--rule ACTION=BEHAVIOR[*N]` | `KIRBY_OCPP_RULES` (`;` separates rules) | none | Response rule for a CSMS command, repeatable; see [Response rules](#response-rules) |
73
+ | `--print-config` | | | Print the effective configuration as JSON and exit |
74
+ | `--no-tui` | | | Never start the TUI; log to stdout |
75
+ | `--json` | | | JSON lines instead of text (events and the `run` report) |
76
+ | `--log-file` | | | Also append the headless event log to a file |
77
+ | `--licenses` | | | Print licence notices and exit |
78
+ | `-V, --version`, `-h, --help` | | | |
79
+
80
+ Options are global and may be placed before or after a subcommand.
81
+
82
+ ## TLS
83
+
84
+ `wss://` uses rustls. Without `--tls-ca` the operating system trust store is used. Use `--tls-ca` for a private CA, `--tls-cert`/`--tls-key` for client authentication, `--tls-server-name` when the URL host differs from the certificate name, and `--tls-skip-verify` only against local self-signed servers.
85
+
86
+ ## State persistence
87
+
88
+ Per charge point id, one JSON file `<state dir>/<id>.station.json` holds the local authorization list and its version, authorization cache, charging profiles, writable configuration values, availability (including a `Scheduled` change waiting for a transaction to end), connector count and per-connector state (status, meter, reservation, transaction) and the offline transaction queue. Ids whose escaped name would exceed 100 bytes are shortened and get a hash suffix (`...~<16 hex>.station.json`) so the name always fits the filesystem limit. Files are versioned and written atomically (temp file plus rename, then the directory is fsynced); a corrupt file or one belonging to another charge point is moved to `<file>.bak` and the station starts clean. `--no-persist` disables both loading and saving.
89
+
90
+ ## Terminal UI
91
+
92
+ Seven tabs, switched with `1`-`7` or `Tab`/`Shift-Tab`: Station, Connector, Transaction, Maintenance, Data, Logs, Scenarios. Panes: connectors, actions, log (move focus with Left/Right).
93
+
94
+ | Key | Action |
95
+ | --- | --- |
96
+ | `Up/Down`, `j/k`, `PageUp/PageDown` | Move in the focused pane |
97
+ | `[` `]` | Previous / next connector |
98
+ | `Enter` | Run the selected action / open the selected log row |
99
+ | letters | Accelerators of the current tab's actions (shown in the list) |
100
+ | `/` or `:` or `Ctrl-P` | Command palette (fuzzy search over all actions); `/` searches the log when the log pane is focused on the Logs tab |
101
+ | `Esc` | Close dialog / clear log search |
102
+ | `?` | Help |
103
+ | `q`, `Ctrl-C` | Quit |
104
+
105
+ Actions: connect/disconnect/reconnect, BootNotification, Heartbeat, add connector, response rules (`m` adds one, `Action=off` removes one, `M` clears all; active rules are listed in the Station pane); plug/unplug, set status, StatusNotification, fault toggle; Authorize, StartTransaction, StopTransaction (with reason), MeterValues; set diagnostics/firmware outcome and send their status notifications; DataTransfer, vendor ping and raw CALLs.
106
+
107
+ Data browser (Station tab: `K` config keys, `L` local authorization list, `P` charging profiles; also in the palette): one popup with three pages, switched with `Tab`/`Shift-Tab`, `Left`/`Right` or `1`-`3`. `/` filters the rows fuzzily (`Enter` keeps the filter, `Esc` clears it), `Up/Down`/`j/k`/`PageUp/PageDown`/`g`/`G` move, `Esc`/`q` closes. Forms and confirmations opened from the browser return to it. Edits are local commands that behave like the CSMS-triggered ones, without a CSMS round trip:
108
+
109
+ | Page | Keys |
110
+ | --- | --- |
111
+ | Config keys | `Enter`/`e` edits the selected key (read-only keys are marked `(ro)` and refuse); the value is checked live with the station's own rules, as `ChangeConfiguration` would, and applied with the same side effects (heartbeat/metering timers) and persistence |
112
+ | Local auth list | `a` adds, `Enter`/`e` edits (idTag, status, expiry, parent), `d` removes, `c` clears (asks first). Each add/edit/remove is a differential update that bumps the list version by one; clear empties the list and resets the version to 0 |
113
+ | Charging profiles | `i` installs a profile from JSON (a template is prefilled; paste your own: `csChargingProfiles` object plus connector, validated against the `SetChargingProfile` schema and the station's profile rules), `e` edits the selected one (same id replaces it), `Enter` shows its JSON (`c` there copies), `d` deletes it, `c` clears all (asks first) |
114
+
115
+ The snapshot behind it (`Snapshot::config`, `local_list`, `charging_profiles`) is also available to API users; the local list view holds at most 500 entries (`total` has the real count).
116
+
117
+ Log view (Logs tab): CALL/RESULT/ERROR correlation, filters (all, calls, errors, station, csms; `f`), search (`s`), pause/follow (`p`), jump to newest (`G`), detail view with scroll (`Enter`; `c`/`y` copies), copy row (`y`) or visible rows (`Y`), export as JSONL (`e`, default `./kirby-ocpp-<id>-<timestamp>.jsonl`), clear with confirmation (`c`). The buffer keeps the last 2000 entries. Copying tries external clipboard tools first and falls back to the terminal's OSC 52 sequence.
118
+
119
+ Scenarios tab: one `Run: <name>` action per built-in scenario (keys `o`, `w`, `s`, `i` for the first four) and `l` to load a scenario file, `x` to cancel the running one.
120
+
121
+ ## Headless mode and exit codes
122
+
123
+ Without a TTY (or with `--no-tui`) the tool connects and prints events until SIGINT/SIGTERM, then closes the WebSocket cleanly. With `--json` each line is one JSON object; frames carry a `payload` field.
124
+
125
+ `kirby-ocpp run --scenario <name|file.toml> [--report report.json] [--timeout 60] [--id-tag TAG]` connects, waits for an accepted BootNotification, runs the scenario and prints a PASS/FAIL/SKIP report (a JSON object with `"type": "report"` under `--json`).
126
+
127
+ | Code | Meaning |
128
+ | --- | --- |
129
+ | 0 | Success (scenario passed, normal shutdown) |
130
+ | 1 | Scenario failed or timed out, or a runtime error |
131
+ | 2 | Usage or configuration error |
132
+ | 130 | `run` interrupted by SIGINT (Ctrl-C); SIGTERM still ends it with 1 |
133
+
134
+ ## Fleet mode
135
+
136
+ `--count N` runs N independent stations in one process against the same CSMS, for load and multi-station tests (terminal UI on a TTY, headless with `--no-tui` or without one).
64
137
 
65
- ## TUI
66
-
67
- The interface is organized into tabs. Use `1`-`7` or `Tab` / `Shift+Tab` to switch tabs, `Up` / `Down` to select an action, and `Enter` to run it.
138
+ ```
139
+ kirby-ocpp --url ws://localhost:9000/ocpp --id 'CP-{n:03}' --count 50 --stagger-ms 100 --no-tui
140
+ kirby-ocpp --url ws://localhost:9000/ocpp --id 'CP-{n}' --count 20 run --scenario charge-session
141
+ ```
68
142
 
69
- - `1` Station
70
- - `2` Connector
71
- - `3` Transaction
72
- - `4` Maintenance
73
- - `5` Data
74
- - `6` Logs
75
- - `7` Scenarios
143
+ - The id comes from `--id-template`, or from `--id` when no template is given. `{n}` is the station index, `{n:03}` the index zero-padded to 3 digits; `{{` and `}}` are literal braces. With `--count` above 1 the template needs a placeholder (otherwise exit 2). `--first-index` (default 1) is the index of the first station, so `--count 3 --first-index 10` gives `CP-10`, `CP-11`, `CP-12`. `--count 1` without `--id-template` uses `--id` as is; with an explicit template it is expanded too.
144
+ - Every station is a normal station (own connection, transactions, state file named after its id, so state files never collide). `--stagger-ms` waits that long between starting two stations.
145
+ - The terminal UI shows a selector row on top (`1 CP-001+* 2 CP-002- ...`: index, id, connection glyph `+` connected, `~` connecting/retrying, `-` offline, `*` a transaction is running) and the selected station below. Everything else (status, log, actions, `K`/`L`/`P` browsers, forms, scenarios) works on the selected station only; each station keeps its own log (last 500 rows), dialogs and scenario progress, and events of the other stations keep being recorded. `Alt` combinations may need "Option as Meta" in macOS terminals. With `--count 1` there is no selector and no extra keys. Quit stops the whole fleet.
146
+ - Output is tagged with the station id: `[CP-001] ...` in front of every human line, a `"station"` field in every JSON line, stdout and `--log-file` alike. With `--count 1` nothing is tagged.
147
+ - `run --scenario` runs the scenario on every station at the same time and prints one report per station (tagged), then one aggregate line: `fleet: 3/3 stations PASSED`, or `fleet: 2/3 stations passed, FAILED: CP-2`. With `--json` the aggregate is `{"type":"fleet_report","passed":bool,"total":n,"passed_count":n,"failed":[ids],"stations":[...]}`. `--timeout` covers the whole fleet, stagger included. `--report out.json` writes one file per station: `out.CP-001.json` (characters of the id other than letters, digits, `-` and `_` become `_`).
148
+
149
+ | Fleet key | Action |
150
+ |---|---|
151
+ | `<` `>` | Previous / next station |
152
+ | `Alt-1` .. `Alt-9` | Jump to station 1..9 |
153
+ | `Alt-c` / `Alt-d` | Connect / disconnect all stations |
154
+
155
+ Selector keys work in normal mode only (inside a dialog they are plain input).
156
+ - Exit codes: 0 only if every station passes, 1 if any fails or times out, 2 usage error, 130 SIGINT.
157
+ - `--print-config` with `--count > 1` prints `{"fleet": {...}, "config": {...}}` where `config` is the first station.
158
+
159
+ ## Scenarios
160
+
161
+ `kirby-ocpp scenarios list` lists the built-in scenarios (`charge-session`, `fault-recovery`, `finish-session`, `offline-sync`); `kirby-ocpp scenarios show <name>` prints one as TOML. Built-ins target connector 1 unless `--connector` is given. `offline-sync` works on a stock station: `LocalAuthorizeOffline` and `AllowOfflineTxForUnknownId` default to `true` (switch them off with `ChangeConfiguration` to test the CSMS-side rejection path).
162
+
163
+ ```toml
164
+ name = "my-session"
165
+ description = "Plug in, start, meter, stop"
166
+
167
+ [[steps]]
168
+ action = "plug"
169
+ connector = 1
170
+
171
+ [[steps]]
172
+ action = "start_tx"
173
+ connector = 1
174
+ id_tag = "KIRBY-TAG" # implicitly expects status Accepted
175
+
176
+ [[steps]]
177
+ action = "meter"
178
+ connector = 1
179
+ wh_delta = 1000
180
+
181
+ [[steps]]
182
+ action = "expect_call" # a CALL seen since the last matched expect_call
183
+ call = "MeterValues"
184
+ timeout_ms = 3000
185
+
186
+ [[steps]]
187
+ action = "stop_tx"
188
+ connector = 1
189
+ reason = "Local"
190
+ expect = { status = "Accepted" }
191
+ ```
76
192
 
77
- Global shortcuts:
193
+ Actions: `connect`, `disconnect`, `reconnect`, `boot`, `heartbeat`, `plug`, `unplug`, `authorize`, `start_tx`, `stop_tx`, `meter`, `status`, `fault`, `clear_fault`, `sleep`, `wait_for`, `expect_call` (`call`, optional `direction`, `match` payload subset), `send_raw` (`call`, `payload`, `validate`), `data_transfer`, `set_rule` (`rule`), `clear_rules` (optional `call`). Every step may have `name` and `expect` (`status`, `payload` subset, `error = true`). `wait_for` takes `condition = { kind = ... }` with kinds `connected`, `disconnected`, `registration`, `connector_status`, `tx_active`, `tx_idle`, `no_pending_messages`, and `timeout_ms` (default 5000). Unknown fields are rejected.
78
194
 
79
- - `/`: command palette.
80
- - `[` / `]`: previous/next connector.
81
- - `+`: add connector.
82
- - `j` / `k`: scroll logs.
83
- - `G`: jump logs to bottom.
84
- - `F`: cycle log filter.
85
- - `l`: full-screen plain log view.
86
- - `y`: copy current log buffer.
87
- - `?`: help.
88
- - `q`: quit.
195
+ ## Response rules
89
196
 
90
- Forms prompt for values such as `idTag`, connector id, measurands, and `DataTransfer` payloads. There is no built-in default RFID/idTag.
91
- On the Connector tab, `s` cycles the selected connector status and `S` opens a list of all OCPP connector statuses.
197
+ Response rules make the station misbehave on purpose, to test how a CSMS reacts. A rule overrides how one CSMS command (any of the 19 the station handles, e.g. `Reset`, `RemoteStartTransaction`, `ChangeConfiguration`) is answered. Form: `ACTION=BEHAVIOR[*COUNT]`, action names case-insensitive.
92
198
 
93
- ## Persistence
199
+ | Behavior | Effect |
200
+ | --- | --- |
201
+ | `reject` | The action's own rejection (`Rejected`; `NotSupported` for `SendLocalList` and `UnlockConnector`). `GetConfiguration`, `GetLocalListVersion`, `ClearChargingProfile`, `GetDiagnostics` and `UpdateFirmware` have no rejection status, so they get CALLERROR `NotSupported`. |
202
+ | `delay:MS` | The command is handled normally; only the reply is sent `MS` ms later (0 to 3600000). The station keeps working meanwhile. |
203
+ | `error:CODE[:TEXT]` | CALLERROR with an OCPP 1.6 code (`NotImplemented`, `NotSupported`, `InternalError`, `ProtocolError`, `SecurityError`, `FormationViolation`, `PropertyConstraintViolation`, `OccurenceConstraintViolation`, `TypeConstraintViolation`, `GenericError`). |
204
+ | `drop` | Close the WebSocket without replying; the normal reconnect lifecycle follows (disabled by `--no-reconnect`). |
205
+ | `ignore` | Never reply, so the CSMS call timeout fires. |
94
206
 
95
- State is persisted by default per `chargePointId`. This includes mutable configuration, local auth lists/cache, charging profiles, connector meter values, reservations, and pending offline transactions.
207
+ `*COUNT` applies the rule that many times and then removes it (default: unlimited). There is one rule per action; setting another replaces it. Except for `delay`, the command is **not executed** (no side effects). Rules are runtime only, never persisted. Set them with `--rule` (repeatable) or `KIRBY_OCPP_RULES`; in both, `;` separates several rules, so a rule (e.g. an `error:` text) must not contain `;`, in the TUI (Station tab, `m`), as scenario steps, or through `Command::SetResponseRule` / `ClearResponseRules` in the core API. Each application is logged (`response rule: Reset -> reject, 2 more`).
96
208
 
97
- ```bash
98
- kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 --no-persist
99
- kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 --state-dir ./station-state
209
+ ```sh
210
+ kirby-ocpp --url ws://localhost:9000/ocpp --rule Reset=reject --rule GetConfiguration=delay:5000 \
211
+ --rule ChangeConfiguration=error:InternalError*3 --rule RemoteStartTransaction=ignore*1
100
212
  ```
101
213
 
102
- ## Scope
214
+ In scenarios: `action = "set_rule"` with `rule = "Reset=reject*2"`, and `action = "clear_rules"` with an optional `call = "Reset"` (all rules when omitted).
103
215
 
104
- kirby-ocpp targets OCPP 1.6J. It supports common Charge Point calls and Central System commands for booting, authorization, transactions, metering, local auth lists, reservations, availability, reset, diagnostics, firmware, smart charging, trigger messages, and data transfer.
216
+ ## Testing against a mock
105
217
 
106
- Detailed coverage is tracked in [docs/OCPP_1.6J_CONFORMANCE.md](docs/OCPP_1.6J_CONFORMANCE.md).
218
+ `crates/kirby-ocpp-testkit` is a dev-only scripted mock CSMS (plain and TLS) used by the integration tests; it is not published. Use it from tests of your own, or point `kirby-ocpp` at any real or local CSMS.
107
219
 
108
220
  ## Development
109
221
 
110
- ```bash
111
- npm install
112
- npm run dev -- --url ws://localhost:9000/ocpp --id CP-001
113
- ```
114
-
115
- Before publishing:
222
+ Workspace crates: `kirby-ocpp-core` (protocol, station state machine, scenarios, WebSocket/TLS; no terminal code), `kirby-ocpp-tui` (ratatui UI), `kirby-ocpp` (binary: CLI and headless runner), `kirby-ocpp-testkit` (mock CSMS, not published).
116
223
 
117
224
  ```bash
118
- npm run typecheck
119
- npm test
120
- npm run smoke
121
- npm run pack:check
225
+ cargo fmt --all --check
226
+ cargo clippy --workspace --all-targets -- -D warnings
227
+ cargo test --workspace # includes doc tests and process-level CLI tests
122
228
  ```
123
229
 
124
- `npm run pack:check` builds the package, installs the generated tarball into a temporary directory, and verifies the installed CLI and bundled schemas.
125
-
126
- ## Third-Party Materials
230
+ Protocol coverage is documented in [docs/OCPP_1.6J_CONFORMANCE.md](docs/OCPP_1.6J_CONFORMANCE.md).
127
231
 
128
- This project is MIT licensed. Bundled OCPP 1.6J JSON schemas are sourced from the official Open Charge Alliance OCPP 1.6 download package and are included for validation/conformance. They are not owned by this project and are not covered by this project's MIT license.
232
+ ## License
129
233
 
130
- See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
234
+ MIT, see [LICENSE](LICENSE). The bundled OCPP JSON schemas belong to the Open Charge Alliance; see [THIRD_PARTY_NOTICES.md](crates/kirby-ocpp/THIRD_PARTY_NOTICES.md) or run `kirby-ocpp --licenses`. Crate licences are recorded in `Cargo.lock` and the crates' metadata.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## OCPP 1.6J JSON schemas
4
4
 
5
- This package bundles the OCPP 1.6J JSON schemas under `dist/ocpp/schemas/json` for local protocol validation.
5
+ `kirby-ocpp` bundles the OCPP 1.6J JSON schemas (`crates/kirby-ocpp-core/schemas`) and embeds them in the binary for local protocol validation.
6
6
 
7
7
  The schemas are sourced from the official Open Charge Alliance OCPP 1.6 download package:
8
8
 
@@ -12,4 +12,8 @@ The OCPP 1.6J specification package references Creative Commons Attribution-NoDe
12
12
 
13
13
  - https://creativecommons.org/licenses/by-nd/4.0/legalcode
14
14
 
15
- The bundled schemas are included for compatibility/conformance validation and are not owned by this project. The project license in `LICENSE` applies to this project's own source code, not to Open Charge Alliance materials. Open Charge Alliance does not sponsor or endorse this package.
15
+ The bundled schemas are included, unmodified, for compatibility and conformance validation and are not owned by this project. The project license in `LICENSE` applies to this project's own source code, not to Open Charge Alliance materials. Open Charge Alliance does not sponsor or endorse this project.
16
+
17
+ ## Rust dependencies
18
+
19
+ `kirby-ocpp` is statically linked from open-source Rust crates (tokio, rustls, ring, ratatui, crossterm, clap, serde and others), each under its own licence (MIT, Apache-2.0, ISC, BSD and similar permissive licences). Their names, versions and licences are recorded in `Cargo.lock` and each crate's own metadata; run `cargo install cargo-license && cargo license` in a source checkout, or inspect the dependency tree with `cargo tree`, for the exact list. Binary distributions must keep this notice together with the licence texts of those crates.
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // Launcher: runs the native kirby-ocpp binary shipped by the matching platform package.
3
+ "use strict";
4
+
5
+ const { spawnSync } = require("node:child_process");
6
+ const path = require("node:path");
7
+
8
+ // Candidate packages per platform, in order of preference. The musl build is fully static,
9
+ // so on Linux x64 it is also a valid fallback when the glibc package was not installed.
10
+ const PLATFORMS = {
11
+ "linux-x64": ["@archhaze24/kirby-ocpp-linux-x64", "@archhaze24/kirby-ocpp-linux-x64-musl"],
12
+ "linux-arm64": ["@archhaze24/kirby-ocpp-linux-arm64"],
13
+ "darwin-x64": ["@archhaze24/kirby-ocpp-darwin-x64"],
14
+ "darwin-arm64": ["@archhaze24/kirby-ocpp-darwin-arm64"],
15
+ "win32-x64": ["@archhaze24/kirby-ocpp-win32-x64"],
16
+ };
17
+
18
+ // True on a musl-based Linux (Alpine etc.): glibc reports its version, musl does not.
19
+ function isMusl() {
20
+ if (process.platform !== "linux") return false;
21
+ try {
22
+ const report = process.report.getReport();
23
+ const header = typeof report === "string" ? JSON.parse(report).header : report.header;
24
+ if (header && "glibcVersionRuntime" in header) return !header.glibcVersionRuntime;
25
+ } catch {
26
+ // fall through to the file check
27
+ }
28
+ try {
29
+ return require("node:fs").readdirSync("/lib").some((name) => name.startsWith("ld-musl-"));
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function binaryPath() {
36
+ const key = `${process.platform}-${process.arch}`;
37
+ const candidates = PLATFORMS[key];
38
+ if (!candidates) {
39
+ throw new Error(
40
+ `kirby-ocpp has no prebuilt binary for ${key}. Supported: ${Object.keys(PLATFORMS).join(", ")}. ` +
41
+ "Build from source instead: cargo install kirby-ocpp",
42
+ );
43
+ }
44
+ const ordered = key === "linux-x64" && isMusl() ? [...candidates].reverse() : candidates;
45
+ const exe = process.platform === "win32" ? "kirby-ocpp.exe" : "kirby-ocpp";
46
+ for (const pkg of ordered) {
47
+ try {
48
+ const manifest = require.resolve(`${pkg}/package.json`);
49
+ return path.join(path.dirname(manifest), "bin", exe);
50
+ } catch {
51
+ // not installed, try the next candidate
52
+ }
53
+ }
54
+ throw new Error(
55
+ `The platform package ${ordered[0]} is not installed (npm skipped the optional dependency). ` +
56
+ "Reinstall without --omit=optional, or run: cargo install kirby-ocpp",
57
+ );
58
+ }
59
+
60
+ let binary;
61
+ try {
62
+ binary = binaryPath();
63
+ } catch (error) {
64
+ console.error(`kirby-ocpp: ${error.message}`);
65
+ process.exit(1);
66
+ }
67
+
68
+ const result = spawnSync(binary, process.argv.slice(2), { stdio: "inherit" });
69
+ if (result.error) {
70
+ console.error(`kirby-ocpp: could not run ${binary}: ${result.error.message}`);
71
+ process.exit(1);
72
+ }
73
+ if (result.signal) {
74
+ process.kill(process.pid, result.signal);
75
+ } else {
76
+ process.exit(result.status ?? 1);
77
+ }
package/package.json CHANGED
@@ -1,62 +1,41 @@
1
1
  {
2
2
  "name": "kirby-ocpp",
3
- "version": "0.1.2",
4
- "description": "A terminal OCPP charge point emulator.",
5
- "type": "module",
6
- "author": "archhaze24",
7
- "bin": {
8
- "kirby-ocpp": "dist/cli.js"
9
- },
10
- "files": [
11
- "dist",
12
- "docs",
13
- "README.md",
14
- "THIRD_PARTY_NOTICES.md",
15
- "LICENSE"
16
- ],
17
- "scripts": {
18
- "build": "tsc -p tsconfig.json && node scripts/copy-schemas.mjs",
19
- "dev": "tsx src/cli.ts",
20
- "pack:check": "node scripts/check-packed-install.mjs",
21
- "prepack": "npm run build",
22
- "smoke": "npm run build && node scripts/smoke-ocpp.mjs",
23
- "start": "node dist/cli.js",
24
- "test": "npm run build && node --test tests/*.test.mjs",
25
- "typecheck": "tsc -p tsconfig.json --noEmit"
26
- },
3
+ "version": "0.3.0",
4
+ "description": "Terminal OCPP 1.6J charge point emulator for testing a CSMS (TUI, headless and scenarios)",
27
5
  "keywords": [
28
6
  "ocpp",
29
7
  "ocpp16",
8
+ "ev",
30
9
  "charge-point",
31
10
  "emulator",
32
- "tui",
33
- "cli"
11
+ "csms",
12
+ "tui"
34
13
  ],
35
14
  "license": "MIT",
15
+ "author": "archhaze24",
36
16
  "repository": {
37
17
  "type": "git",
38
18
  "url": "git+https://github.com/archhaze24/kirby-ocpp.git"
39
19
  },
40
- "bugs": {
41
- "url": "https://github.com/archhaze24/kirby-ocpp/issues"
42
- },
43
20
  "homepage": "https://github.com/archhaze24/kirby-ocpp#readme",
44
- "engines": {
45
- "node": ">=20"
21
+ "bin": {
22
+ "kirby-ocpp": "bin/kirby-ocpp.js"
46
23
  },
47
- "dependencies": {
48
- "blessed": "^0.1.81",
49
- "commander": "^12.1.0",
50
- "jsonschema": "^1.5.0",
51
- "nanoid": "^5.0.7",
52
- "ws": "^8.18.0",
53
- "zod": "^3.23.8"
24
+ "files": [
25
+ "bin",
26
+ "README.md",
27
+ "LICENSE",
28
+ "THIRD_PARTY_NOTICES.md"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
54
32
  },
55
- "devDependencies": {
56
- "@types/blessed": "^0.1.25",
57
- "@types/node": "^22.10.2",
58
- "@types/ws": "^8.5.13",
59
- "tsx": "^4.19.2",
60
- "typescript": "^5.7.2"
33
+ "optionalDependencies": {
34
+ "@archhaze24/kirby-ocpp-linux-x64": "0.3.0",
35
+ "@archhaze24/kirby-ocpp-linux-x64-musl": "0.3.0",
36
+ "@archhaze24/kirby-ocpp-linux-arm64": "0.3.0",
37
+ "@archhaze24/kirby-ocpp-darwin-x64": "0.3.0",
38
+ "@archhaze24/kirby-ocpp-darwin-arm64": "0.3.0",
39
+ "@archhaze24/kirby-ocpp-win32-x64": "0.3.0"
61
40
  }
62
41
  }
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/cli.js DELETED
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander";
3
- import { createRequire } from "node:module";
4
- import { ZodError } from "zod";
5
- import { parseConfig } from "./config.js";
6
- import { Station } from "./station.js";
7
- import { Tui } from "./tui.js";
8
- const program = new Command();
9
- const require = createRequire(import.meta.url);
10
- const packageJson = require("../package.json");
11
- program
12
- .name("kirby-ocpp")
13
- .description("Terminal OCPP charge point emulator")
14
- .version(packageJson.version ?? "0.1.0")
15
- .option("-u, --url <url>", "Central System WebSocket URL, e.g. ws://localhost:9000/ocpp")
16
- .option("-i, --id <id>", "charge point id", process.env.OCPP_CHARGE_POINT_ID ?? "CP-001")
17
- .option("--vendor <vendor>", "charge point vendor", process.env.OCPP_VENDOR ?? "archhaze24")
18
- .option("--model <model>", "charge point model", process.env.OCPP_MODEL ?? "KIRBY")
19
- .option("-c, --connector <id>", "initial connector id", "1")
20
- .option("--connectors <count>", "number of station connectors", "1")
21
- .option("--heartbeat <seconds>", "fallback heartbeat interval in seconds", "30")
22
- .option("--no-persist", "do not save station state such as local auth list and charging profiles")
23
- .option("--state-dir <path>", "directory for persisted station state")
24
- .option("--ws-subprotocol <protocol>", "WebSocket subprotocol to request", "ocpp1.6")
25
- .option("--ws-ping <seconds>", "WebSocket ping interval in seconds, 0 disables pings", "30")
26
- .option("--no-reconnect", "do not automatically reconnect after an unexpected WebSocket disconnect")
27
- .option("--reconnect-initial-ms <milliseconds>", "initial automatic reconnect delay in milliseconds", "1000")
28
- .option("--reconnect-max-ms <milliseconds>", "maximum automatic reconnect delay in milliseconds", "30000")
29
- .option("--reconnect-max-attempts <attempts>", "maximum automatic reconnect attempts, 0 means unlimited", "0")
30
- .option("--call-timeout-ms <milliseconds>", "OCPP CALL response timeout in milliseconds", "30000")
31
- .option("--tls-ca <path>", "CA certificate bundle for wss:// connections")
32
- .option("--tls-cert <path>", "client certificate for wss:// mutual TLS")
33
- .option("--tls-key <path>", "client private key for wss:// mutual TLS")
34
- .option("--tls-server-name <name>", "TLS server name override for wss:// connections")
35
- .option("--tls-skip-verify", "disable TLS certificate verification for local/self-signed wss:// testing")
36
- .parse();
37
- const options = program.opts();
38
- try {
39
- const config = parseConfig({
40
- centralSystemUrl: options.url,
41
- chargePointId: options.id,
42
- vendor: options.vendor,
43
- model: options.model,
44
- connectorId: options.connector,
45
- connectorCount: Math.max(Number.parseInt(options.connectors, 10), Number.parseInt(options.connector, 10)),
46
- heartbeatIntervalSeconds: options.heartbeat,
47
- persistState: options.persist,
48
- stateDirectory: options.stateDir,
49
- webSocketSubprotocol: options.wsSubprotocol,
50
- webSocketPingIntervalSeconds: options.wsPing,
51
- webSocketReconnectEnabled: options.reconnect,
52
- webSocketReconnectInitialDelayMs: options.reconnectInitialMs,
53
- webSocketReconnectMaxDelayMs: options.reconnectMaxMs,
54
- webSocketReconnectMaxAttempts: options.reconnectMaxAttempts,
55
- callTimeoutMs: options.callTimeoutMs,
56
- tlsRejectUnauthorized: !options.tlsSkipVerify,
57
- tlsCaFile: options.tlsCa,
58
- tlsCertFile: options.tlsCert,
59
- tlsKeyFile: options.tlsKey,
60
- tlsServerName: options.tlsServerName
61
- });
62
- const station = new Station(config);
63
- const tui = new Tui(station);
64
- tui.start();
65
- }
66
- catch (error) {
67
- if (error instanceof ZodError) {
68
- console.error(error.errors.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("\n"));
69
- process.exit(1);
70
- }
71
- throw error;
72
- }
package/dist/config.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { StationConfig } from "./ocpp/types.js";
2
- export declare function parseConfig(input: Partial<Record<keyof StationConfig, unknown>>): StationConfig;