kirby-ocpp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/THIRD_PARTY_NOTICES.md +15 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +72 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +53 -0
- package/dist/ocpp/client.d.ts +59 -0
- package/dist/ocpp/client.js +294 -0
- package/dist/ocpp/schema-validator.d.ts +15 -0
- package/dist/ocpp/schema-validator.js +75 -0
- package/dist/ocpp/schemas/json/Authorize.json +16 -0
- package/dist/ocpp/schemas/json/AuthorizeResponse.json +40 -0
- package/dist/ocpp/schemas/json/BootNotification.json +49 -0
- package/dist/ocpp/schemas/json/BootNotificationResponse.json +30 -0
- package/dist/ocpp/schemas/json/CancelReservation.json +15 -0
- package/dist/ocpp/schemas/json/CancelReservationResponse.json +20 -0
- package/dist/ocpp/schemas/json/ChangeAvailability.json +24 -0
- package/dist/ocpp/schemas/json/ChangeAvailabilityResponse.json +21 -0
- package/dist/ocpp/schemas/json/ChangeConfiguration.json +21 -0
- package/dist/ocpp/schemas/json/ChangeConfigurationResponse.json +22 -0
- package/dist/ocpp/schemas/json/ClearCache.json +8 -0
- package/dist/ocpp/schemas/json/ClearCacheResponse.json +20 -0
- package/dist/ocpp/schemas/json/ClearChargingProfile.json +27 -0
- package/dist/ocpp/schemas/json/ClearChargingProfileResponse.json +20 -0
- package/dist/ocpp/schemas/json/DataTransfer.json +23 -0
- package/dist/ocpp/schemas/json/DataTransferResponse.json +25 -0
- package/dist/ocpp/schemas/json/DiagnosticsStatusNotification.json +22 -0
- package/dist/ocpp/schemas/json/DiagnosticsStatusNotificationResponse.json +8 -0
- package/dist/ocpp/schemas/json/FirmwareStatusNotification.json +25 -0
- package/dist/ocpp/schemas/json/FirmwareStatusNotificationResponse.json +8 -0
- package/dist/ocpp/schemas/json/GetCompositeSchedule.json +27 -0
- package/dist/ocpp/schemas/json/GetCompositeScheduleResponse.json +79 -0
- package/dist/ocpp/schemas/json/GetConfiguration.json +16 -0
- package/dist/ocpp/schemas/json/GetConfigurationResponse.json +40 -0
- package/dist/ocpp/schemas/json/GetDiagnostics.json +30 -0
- package/dist/ocpp/schemas/json/GetDiagnosticsResponse.json +13 -0
- package/dist/ocpp/schemas/json/GetLocalListVersion.json +8 -0
- package/dist/ocpp/schemas/json/GetLocalListVersionResponse.json +15 -0
- package/dist/ocpp/schemas/json/Heartbeat.json +8 -0
- package/dist/ocpp/schemas/json/HeartbeatResponse.json +16 -0
- package/dist/ocpp/schemas/json/MeterValues.json +151 -0
- package/dist/ocpp/schemas/json/MeterValuesResponse.json +8 -0
- package/dist/ocpp/schemas/json/RemoteStartTransaction.json +127 -0
- package/dist/ocpp/schemas/json/RemoteStartTransactionResponse.json +20 -0
- package/dist/ocpp/schemas/json/RemoteStopTransaction.json +15 -0
- package/dist/ocpp/schemas/json/RemoteStopTransactionResponse.json +20 -0
- package/dist/ocpp/schemas/json/ReserveNow.json +33 -0
- package/dist/ocpp/schemas/json/ReserveNowResponse.json +23 -0
- package/dist/ocpp/schemas/json/Reset.json +20 -0
- package/dist/ocpp/schemas/json/ResetResponse.json +20 -0
- package/dist/ocpp/schemas/json/SendLocalList.json +68 -0
- package/dist/ocpp/schemas/json/SendLocalListResponse.json +22 -0
- package/dist/ocpp/schemas/json/SetChargingProfile.json +124 -0
- package/dist/ocpp/schemas/json/SetChargingProfileResponse.json +21 -0
- package/dist/ocpp/schemas/json/StartTransaction.json +32 -0
- package/dist/ocpp/schemas/json/StartTransactionResponse.json +44 -0
- package/dist/ocpp/schemas/json/StatusNotification.json +70 -0
- package/dist/ocpp/schemas/json/StatusNotificationResponse.json +8 -0
- package/dist/ocpp/schemas/json/StopTransaction.json +177 -0
- package/dist/ocpp/schemas/json/StopTransactionResponse.json +37 -0
- package/dist/ocpp/schemas/json/TriggerMessage.json +27 -0
- package/dist/ocpp/schemas/json/TriggerMessageResponse.json +21 -0
- package/dist/ocpp/schemas/json/UnlockConnector.json +15 -0
- package/dist/ocpp/schemas/json/UnlockConnectorResponse.json +21 -0
- package/dist/ocpp/schemas/json/UpdateFirmware.json +27 -0
- package/dist/ocpp/schemas/json/UpdateFirmwareResponse.json +8 -0
- package/dist/ocpp/types.d.ts +106 -0
- package/dist/ocpp/types.js +1 -0
- package/dist/station/authorization.d.ts +20 -0
- package/dist/station/authorization.js +78 -0
- package/dist/station/charging-profile-registry.d.ts +18 -0
- package/dist/station/charging-profile-registry.js +125 -0
- package/dist/station/configuration-commands.d.ts +11 -0
- package/dist/station/configuration-commands.js +29 -0
- package/dist/station/configuration-registry.d.ts +13 -0
- package/dist/station/configuration-registry.js +39 -0
- package/dist/station/configuration.d.ts +7 -0
- package/dist/station/configuration.js +88 -0
- package/dist/station/connector-registry.d.ts +25 -0
- package/dist/station/connector-registry.js +89 -0
- package/dist/station/connector-status.d.ts +7 -0
- package/dist/station/connector-status.js +66 -0
- package/dist/station/local-auth-list.d.ts +8 -0
- package/dist/station/local-auth-list.js +31 -0
- package/dist/station/maintenance-lifecycle.d.ts +25 -0
- package/dist/station/maintenance-lifecycle.js +110 -0
- package/dist/station/meter-value-scheduler.d.ts +21 -0
- package/dist/station/meter-value-scheduler.js +78 -0
- package/dist/station/metering.d.ts +15 -0
- package/dist/station/metering.js +138 -0
- package/dist/station/ocpp-message.d.ts +2 -0
- package/dist/station/ocpp-message.js +9 -0
- package/dist/station/offline-transaction-sync.d.ts +29 -0
- package/dist/station/offline-transaction-sync.js +99 -0
- package/dist/station/payload.d.ts +4 -0
- package/dist/station/payload.js +12 -0
- package/dist/station/persistence.d.ts +28 -0
- package/dist/station/persistence.js +206 -0
- package/dist/station/reservation-rules.d.ts +3 -0
- package/dist/station/reservation-rules.js +18 -0
- package/dist/station/reservation-scheduler.d.ts +11 -0
- package/dist/station/reservation-scheduler.js +33 -0
- package/dist/station/smart-charging.d.ts +45 -0
- package/dist/station/smart-charging.js +251 -0
- package/dist/station/status-scheduler.d.ts +16 -0
- package/dist/station/status-scheduler.js +58 -0
- package/dist/station/transaction-message-retry.d.ts +3 -0
- package/dist/station/transaction-message-retry.js +7 -0
- package/dist/station/transaction-rules.d.ts +7 -0
- package/dist/station/transaction-rules.js +34 -0
- package/dist/station/trigger-message.d.ts +2 -0
- package/dist/station/trigger-message.js +11 -0
- package/dist/station-store.d.ts +41 -0
- package/dist/station-store.js +104 -0
- package/dist/station.d.ts +112 -0
- package/dist/station.js +1201 -0
- package/dist/tui.d.ts +95 -0
- package/dist/tui.js +1601 -0
- package/docs/OCPP_1.6J_CONFORMANCE.md +61 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# kirby-ocpp
|
|
2
|
+
|
|
3
|
+
Terminal OCPP 1.6J charge point emulator for local and staging CSMS testing.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Run without installing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install globally:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g kirby-ocpp
|
|
19
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Common options:
|
|
29
|
+
|
|
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:
|
|
42
|
+
|
|
43
|
+
```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
|
|
50
|
+
```
|
|
51
|
+
|
|
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`.
|
|
64
|
+
|
|
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.
|
|
68
|
+
|
|
69
|
+
- `1` Station
|
|
70
|
+
- `2` Connector
|
|
71
|
+
- `3` Transaction
|
|
72
|
+
- `4` Maintenance
|
|
73
|
+
- `5` Data
|
|
74
|
+
- `6` Logs
|
|
75
|
+
- `7` Scenarios
|
|
76
|
+
|
|
77
|
+
Global shortcuts:
|
|
78
|
+
|
|
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.
|
|
89
|
+
|
|
90
|
+
Forms prompt for values such as `idTag`, connector id, measurands, and `DataTransfer` payloads. There is no built-in default RFID/idTag.
|
|
91
|
+
|
|
92
|
+
## Persistence
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 --no-persist
|
|
98
|
+
kirby-ocpp --url ws://localhost:9000/ocpp --id CP-001 --state-dir ./station-state
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Scope
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
Detailed coverage is tracked in [docs/OCPP_1.6J_CONFORMANCE.md](docs/OCPP_1.6J_CONFORMANCE.md).
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install
|
|
111
|
+
npm run dev -- --url ws://localhost:9000/ocpp --id CP-001
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Before publishing:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm run typecheck
|
|
118
|
+
npm test
|
|
119
|
+
npm run smoke
|
|
120
|
+
npm run pack:check
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`npm run pack:check` builds the package, installs the generated tarball into a temporary directory, and verifies the installed CLI and bundled schemas.
|
|
124
|
+
|
|
125
|
+
## Third-Party Materials
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
## OCPP 1.6J JSON schemas
|
|
4
|
+
|
|
5
|
+
This package bundles the OCPP 1.6J JSON schemas under `dist/ocpp/schemas/json` for local protocol validation.
|
|
6
|
+
|
|
7
|
+
The schemas are sourced from the official Open Charge Alliance OCPP 1.6 download package:
|
|
8
|
+
|
|
9
|
+
- https://openchargealliance.org/my-oca/ocpp/
|
|
10
|
+
|
|
11
|
+
The OCPP 1.6J specification package references Creative Commons Attribution-NoDerivatives 4.0 International:
|
|
12
|
+
|
|
13
|
+
- https://creativecommons.org/licenses/by-nd/4.0/legalcode
|
|
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.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
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
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const configSchema = z.object({
|
|
3
|
+
centralSystemUrl: z
|
|
4
|
+
.string({
|
|
5
|
+
required_error: "Use --url or OCPP_URL to set the Central System WebSocket URL"
|
|
6
|
+
})
|
|
7
|
+
.url(),
|
|
8
|
+
chargePointId: z.string().min(1),
|
|
9
|
+
vendor: z.string().min(1),
|
|
10
|
+
model: z.string().min(1),
|
|
11
|
+
connectorId: z.coerce.number().int().positive(),
|
|
12
|
+
connectorCount: z.coerce.number().int().positive(),
|
|
13
|
+
heartbeatIntervalSeconds: z.coerce.number().int().positive(),
|
|
14
|
+
persistState: z.coerce.boolean(),
|
|
15
|
+
stateDirectory: z.string().min(1).optional(),
|
|
16
|
+
webSocketSubprotocol: z.string().min(1),
|
|
17
|
+
webSocketPingIntervalSeconds: z.coerce.number().int().nonnegative(),
|
|
18
|
+
webSocketReconnectEnabled: z.coerce.boolean(),
|
|
19
|
+
webSocketReconnectInitialDelayMs: z.coerce.number().int().nonnegative(),
|
|
20
|
+
webSocketReconnectMaxDelayMs: z.coerce.number().int().nonnegative(),
|
|
21
|
+
webSocketReconnectMaxAttempts: z.coerce.number().int().nonnegative(),
|
|
22
|
+
callTimeoutMs: z.coerce.number().int().positive(),
|
|
23
|
+
tlsRejectUnauthorized: z.coerce.boolean(),
|
|
24
|
+
tlsCaFile: z.string().min(1).optional(),
|
|
25
|
+
tlsCertFile: z.string().min(1).optional(),
|
|
26
|
+
tlsKeyFile: z.string().min(1).optional(),
|
|
27
|
+
tlsServerName: z.string().min(1).optional()
|
|
28
|
+
});
|
|
29
|
+
export function parseConfig(input) {
|
|
30
|
+
return configSchema.parse({
|
|
31
|
+
centralSystemUrl: input.centralSystemUrl ?? process.env.OCPP_URL,
|
|
32
|
+
chargePointId: input.chargePointId ?? process.env.OCPP_CHARGE_POINT_ID ?? "CP-001",
|
|
33
|
+
vendor: input.vendor ?? process.env.OCPP_VENDOR ?? "archhaze24",
|
|
34
|
+
model: input.model ?? process.env.OCPP_MODEL ?? "KIRBY",
|
|
35
|
+
connectorId: input.connectorId ?? 1,
|
|
36
|
+
connectorCount: input.connectorCount ?? input.connectorId ?? 1,
|
|
37
|
+
heartbeatIntervalSeconds: input.heartbeatIntervalSeconds ?? 30,
|
|
38
|
+
persistState: input.persistState ?? process.env.KIRBY_OCPP_NO_PERSIST !== "1",
|
|
39
|
+
stateDirectory: input.stateDirectory ?? process.env.KIRBY_OCPP_STATE_DIR,
|
|
40
|
+
webSocketSubprotocol: input.webSocketSubprotocol ?? process.env.OCPP_WS_SUBPROTOCOL ?? "ocpp1.6",
|
|
41
|
+
webSocketPingIntervalSeconds: input.webSocketPingIntervalSeconds ?? process.env.OCPP_WS_PING_INTERVAL ?? 30,
|
|
42
|
+
webSocketReconnectEnabled: input.webSocketReconnectEnabled ?? process.env.OCPP_RECONNECT !== "0",
|
|
43
|
+
webSocketReconnectInitialDelayMs: input.webSocketReconnectInitialDelayMs ?? process.env.OCPP_RECONNECT_INITIAL_MS ?? 1_000,
|
|
44
|
+
webSocketReconnectMaxDelayMs: input.webSocketReconnectMaxDelayMs ?? process.env.OCPP_RECONNECT_MAX_MS ?? 30_000,
|
|
45
|
+
webSocketReconnectMaxAttempts: input.webSocketReconnectMaxAttempts ?? process.env.OCPP_RECONNECT_MAX_ATTEMPTS ?? 0,
|
|
46
|
+
callTimeoutMs: input.callTimeoutMs ?? process.env.OCPP_CALL_TIMEOUT_MS ?? 30_000,
|
|
47
|
+
tlsRejectUnauthorized: input.tlsRejectUnauthorized ?? process.env.OCPP_TLS_SKIP_VERIFY !== "1",
|
|
48
|
+
tlsCaFile: input.tlsCaFile ?? process.env.OCPP_TLS_CA_FILE,
|
|
49
|
+
tlsCertFile: input.tlsCertFile ?? process.env.OCPP_TLS_CERT_FILE,
|
|
50
|
+
tlsKeyFile: input.tlsKeyFile ?? process.env.OCPP_TLS_KEY_FILE,
|
|
51
|
+
tlsServerName: input.tlsServerName ?? process.env.OCPP_TLS_SERVER_NAME
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import type { CallResponse, OcppMessage } from "./types.js";
|
|
3
|
+
interface OcppClientEvents {
|
|
4
|
+
connected: [];
|
|
5
|
+
disconnected: [code?: number, reason?: string];
|
|
6
|
+
error: [error: Error];
|
|
7
|
+
log: [direction: "in" | "out", message: OcppMessage];
|
|
8
|
+
call: [messageId: string, action: string, payload: Record<string, unknown>];
|
|
9
|
+
}
|
|
10
|
+
export interface OcppClientOptions {
|
|
11
|
+
subprotocol?: string;
|
|
12
|
+
pingIntervalSeconds?: number;
|
|
13
|
+
tlsRejectUnauthorized?: boolean;
|
|
14
|
+
tlsCaFile?: string;
|
|
15
|
+
tlsCertFile?: string;
|
|
16
|
+
tlsKeyFile?: string;
|
|
17
|
+
tlsServerName?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare interface OcppClient {
|
|
20
|
+
on<K extends keyof OcppClientEvents>(event: K, listener: (...args: OcppClientEvents[K]) => void): this;
|
|
21
|
+
emit<K extends keyof OcppClientEvents>(event: K, ...args: OcppClientEvents[K]): boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class OcppClient extends EventEmitter {
|
|
24
|
+
private readonly centralSystemUrl;
|
|
25
|
+
private readonly chargePointId;
|
|
26
|
+
private readonly options;
|
|
27
|
+
private socket?;
|
|
28
|
+
private pingTimer?;
|
|
29
|
+
private awaitingPong;
|
|
30
|
+
private readonly pending;
|
|
31
|
+
private readonly incomingCalls;
|
|
32
|
+
private readonly schemaValidator;
|
|
33
|
+
constructor(centralSystemUrl: string, chargePointId: string, options?: OcppClientOptions);
|
|
34
|
+
get isConnected(): boolean;
|
|
35
|
+
connect(): void;
|
|
36
|
+
close(): void;
|
|
37
|
+
call(action: string, payload: Record<string, unknown>, timeoutMs?: number): Promise<CallResponse>;
|
|
38
|
+
reply(messageId: string, payload: Record<string, unknown>): void;
|
|
39
|
+
replyError(messageId: string, code: string, description: string, details?: Record<string, unknown>): void;
|
|
40
|
+
private send;
|
|
41
|
+
private handleMessage;
|
|
42
|
+
private resolveCall;
|
|
43
|
+
private rejectCall;
|
|
44
|
+
private rejectPending;
|
|
45
|
+
private toError;
|
|
46
|
+
private assertValidRequest;
|
|
47
|
+
private assertValidResponse;
|
|
48
|
+
private validationErrorCode;
|
|
49
|
+
private isValidCall;
|
|
50
|
+
private isValidCallResult;
|
|
51
|
+
private isValidCallError;
|
|
52
|
+
private isObjectPayload;
|
|
53
|
+
private readMessageId;
|
|
54
|
+
private startPingTimer;
|
|
55
|
+
private stopPingTimer;
|
|
56
|
+
private buildChargePointUrl;
|
|
57
|
+
private buildWebSocketOptions;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import WebSocket from "ws";
|
|
5
|
+
import { OcppSchemaValidator } from "./schema-validator.js";
|
|
6
|
+
export class OcppClient extends EventEmitter {
|
|
7
|
+
centralSystemUrl;
|
|
8
|
+
chargePointId;
|
|
9
|
+
options;
|
|
10
|
+
socket;
|
|
11
|
+
pingTimer;
|
|
12
|
+
awaitingPong = false;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
incomingCalls = new Map();
|
|
15
|
+
schemaValidator = new OcppSchemaValidator();
|
|
16
|
+
constructor(centralSystemUrl, chargePointId, options = {}) {
|
|
17
|
+
super();
|
|
18
|
+
this.centralSystemUrl = centralSystemUrl;
|
|
19
|
+
this.chargePointId = chargePointId;
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
get isConnected() {
|
|
23
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
24
|
+
}
|
|
25
|
+
connect() {
|
|
26
|
+
this.close();
|
|
27
|
+
const url = this.buildChargePointUrl();
|
|
28
|
+
const subprotocol = this.options.subprotocol ?? "ocpp1.6";
|
|
29
|
+
this.socket = new WebSocket(url, [subprotocol], this.buildWebSocketOptions());
|
|
30
|
+
this.socket.on("open", () => {
|
|
31
|
+
if (this.socket?.protocol !== subprotocol) {
|
|
32
|
+
const error = new Error(`Central System did not negotiate required WebSocket subprotocol ${subprotocol}`);
|
|
33
|
+
this.emit("error", error);
|
|
34
|
+
this.socket?.close(1002, "OCPP subprotocol required");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.startPingTimer();
|
|
38
|
+
this.emit("connected");
|
|
39
|
+
});
|
|
40
|
+
this.socket.on("message", (data) => this.handleMessage(data.toString()));
|
|
41
|
+
this.socket.on("pong", () => {
|
|
42
|
+
this.awaitingPong = false;
|
|
43
|
+
});
|
|
44
|
+
this.socket.on("close", (code, reason) => {
|
|
45
|
+
this.stopPingTimer();
|
|
46
|
+
this.rejectPending(new Error(`Connection closed (${code})`));
|
|
47
|
+
this.emit("disconnected", code, reason.toString());
|
|
48
|
+
});
|
|
49
|
+
this.socket.on("error", (error) => this.emit("error", error));
|
|
50
|
+
}
|
|
51
|
+
close() {
|
|
52
|
+
if (!this.socket) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.rejectPending(new Error("Connection closed"));
|
|
56
|
+
this.stopPingTimer();
|
|
57
|
+
this.socket.removeAllListeners();
|
|
58
|
+
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
|
|
59
|
+
this.socket.close();
|
|
60
|
+
}
|
|
61
|
+
this.socket = undefined;
|
|
62
|
+
}
|
|
63
|
+
async call(action, payload, timeoutMs = 30_000) {
|
|
64
|
+
if (!this.isConnected || !this.socket) {
|
|
65
|
+
throw new Error("Not connected to Central System");
|
|
66
|
+
}
|
|
67
|
+
this.assertValidRequest(action, payload);
|
|
68
|
+
const messageId = nanoid(12);
|
|
69
|
+
const message = [2, messageId, action, payload];
|
|
70
|
+
const response = new Promise((resolve, reject) => {
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
this.pending.delete(messageId);
|
|
73
|
+
reject(new Error(`${action} timed out after ${timeoutMs}ms`));
|
|
74
|
+
}, timeoutMs);
|
|
75
|
+
this.pending.set(messageId, { action, resolve, reject, timeout });
|
|
76
|
+
});
|
|
77
|
+
this.send(message);
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
reply(messageId, payload) {
|
|
81
|
+
const action = this.incomingCalls.get(messageId);
|
|
82
|
+
if (!action) {
|
|
83
|
+
throw new Error(`Cannot reply to unknown CALL ${messageId}`);
|
|
84
|
+
}
|
|
85
|
+
this.assertValidResponse(action, payload);
|
|
86
|
+
this.incomingCalls.delete(messageId);
|
|
87
|
+
this.send([3, messageId, payload]);
|
|
88
|
+
}
|
|
89
|
+
replyError(messageId, code, description, details = {}) {
|
|
90
|
+
this.incomingCalls.delete(messageId);
|
|
91
|
+
this.send([4, messageId, code, description, details]);
|
|
92
|
+
}
|
|
93
|
+
send(message) {
|
|
94
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
95
|
+
throw new Error("WebSocket is not open");
|
|
96
|
+
}
|
|
97
|
+
this.socket.send(JSON.stringify(message));
|
|
98
|
+
this.emit("log", "out", message);
|
|
99
|
+
}
|
|
100
|
+
handleMessage(raw) {
|
|
101
|
+
let message;
|
|
102
|
+
try {
|
|
103
|
+
message = JSON.parse(raw);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.emit("log", "in", message);
|
|
110
|
+
if (!Array.isArray(message)) {
|
|
111
|
+
this.emit("error", new Error("Invalid OCPP message: expected array"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (message[0] === 2) {
|
|
115
|
+
if (!this.isValidCall(message)) {
|
|
116
|
+
const messageId = this.readMessageId(message);
|
|
117
|
+
if (messageId) {
|
|
118
|
+
this.replyError(messageId, "FormationViolation", "Invalid CALL frame");
|
|
119
|
+
}
|
|
120
|
+
this.emit("error", new Error("Invalid CALL frame"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const call = message;
|
|
124
|
+
if (!this.schemaValidator.hasRequestSchema(call[2])) {
|
|
125
|
+
this.replyError(call[1], "NotImplemented", `${call[2]} is not an OCPP 1.6J action`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const validation = this.schemaValidator.validateRequest(call[2], call[3]);
|
|
129
|
+
if (!validation.valid) {
|
|
130
|
+
this.replyError(call[1], this.validationErrorCode(validation.errorNames), validation.errors.join("; "));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.incomingCalls.set(call[1], call[2]);
|
|
134
|
+
this.emit("call", call[1], call[2], call[3] ?? {});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (message[0] === 3) {
|
|
138
|
+
if (!this.isValidCallResult(message)) {
|
|
139
|
+
this.emit("error", new Error("Invalid CALLRESULT frame"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.resolveCall(message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (message[0] === 4) {
|
|
146
|
+
if (!this.isValidCallError(message)) {
|
|
147
|
+
this.emit("error", new Error("Invalid CALLERROR frame"));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
this.rejectCall(message);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.emit("error", new Error(`Unsupported OCPP message type: ${message[0]}`));
|
|
154
|
+
}
|
|
155
|
+
resolveCall(message) {
|
|
156
|
+
const [, messageId, payload] = message;
|
|
157
|
+
const pending = this.pending.get(messageId);
|
|
158
|
+
if (!pending) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
clearTimeout(pending.timeout);
|
|
162
|
+
this.pending.delete(messageId);
|
|
163
|
+
const validation = this.schemaValidator.validateResponse(pending.action, payload);
|
|
164
|
+
if (!validation.valid) {
|
|
165
|
+
pending.reject(new Error(`${pending.action} response violates OCPP 1.6J schema (${this.validationErrorCode(validation.errorNames)}): ${validation.errors.join("; ")}`));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
pending.resolve({ messageId, payload });
|
|
169
|
+
}
|
|
170
|
+
rejectCall(message) {
|
|
171
|
+
const error = this.toError(message);
|
|
172
|
+
const pending = this.pending.get(error.messageId);
|
|
173
|
+
if (!pending) {
|
|
174
|
+
this.emit("error", new Error(error.description || error.code));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
clearTimeout(pending.timeout);
|
|
178
|
+
this.pending.delete(error.messageId);
|
|
179
|
+
pending.reject(new Error(`${pending.action} failed: ${error.code} ${error.description}`.trim()));
|
|
180
|
+
}
|
|
181
|
+
rejectPending(error) {
|
|
182
|
+
for (const [messageId, pending] of this.pending) {
|
|
183
|
+
clearTimeout(pending.timeout);
|
|
184
|
+
pending.reject(error);
|
|
185
|
+
this.pending.delete(messageId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
toError(message) {
|
|
189
|
+
const [, messageId, code, description, details] = message;
|
|
190
|
+
return { messageId, code, description, details };
|
|
191
|
+
}
|
|
192
|
+
assertValidRequest(action, payload) {
|
|
193
|
+
const validation = this.schemaValidator.validateRequest(action, payload);
|
|
194
|
+
if (!validation.valid) {
|
|
195
|
+
throw new Error(`${action} request violates OCPP 1.6J schema (${this.validationErrorCode(validation.errorNames)}): ${validation.errors.join("; ")}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
assertValidResponse(action, payload) {
|
|
199
|
+
const validation = this.schemaValidator.validateResponse(action, payload);
|
|
200
|
+
if (!validation.valid) {
|
|
201
|
+
throw new Error(`${action} response violates OCPP 1.6J schema (${this.validationErrorCode(validation.errorNames)}): ${validation.errors.join("; ")}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
validationErrorCode(errorNames) {
|
|
205
|
+
if (errorNames.includes("required")) {
|
|
206
|
+
return "OccurenceConstraintViolation";
|
|
207
|
+
}
|
|
208
|
+
if (errorNames.includes("type")) {
|
|
209
|
+
return "TypeConstraintViolation";
|
|
210
|
+
}
|
|
211
|
+
if (errorNames.some((name) => ["additionalProperties", "enum", "maxLength", "minLength", "minimum", "maximum", "multipleOf", "format"].includes(name))) {
|
|
212
|
+
return "PropertyConstraintViolation";
|
|
213
|
+
}
|
|
214
|
+
return "FormationViolation";
|
|
215
|
+
}
|
|
216
|
+
isValidCall(message) {
|
|
217
|
+
return (message.length === 4 &&
|
|
218
|
+
message[0] === 2 &&
|
|
219
|
+
typeof message[1] === "string" &&
|
|
220
|
+
typeof message[2] === "string" &&
|
|
221
|
+
this.isObjectPayload(message[3]));
|
|
222
|
+
}
|
|
223
|
+
isValidCallResult(message) {
|
|
224
|
+
return message.length === 3 && message[0] === 3 && typeof message[1] === "string" && this.isObjectPayload(message[2]);
|
|
225
|
+
}
|
|
226
|
+
isValidCallError(message) {
|
|
227
|
+
return (message.length === 5 &&
|
|
228
|
+
message[0] === 4 &&
|
|
229
|
+
typeof message[1] === "string" &&
|
|
230
|
+
typeof message[2] === "string" &&
|
|
231
|
+
typeof message[3] === "string" &&
|
|
232
|
+
this.isObjectPayload(message[4]));
|
|
233
|
+
}
|
|
234
|
+
isObjectPayload(value) {
|
|
235
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
236
|
+
}
|
|
237
|
+
readMessageId(message) {
|
|
238
|
+
return typeof message[1] === "string" ? message[1] : undefined;
|
|
239
|
+
}
|
|
240
|
+
startPingTimer() {
|
|
241
|
+
this.stopPingTimer();
|
|
242
|
+
const intervalSeconds = this.options.pingIntervalSeconds ?? 30;
|
|
243
|
+
if (intervalSeconds <= 0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.awaitingPong = false;
|
|
247
|
+
this.pingTimer = setInterval(() => {
|
|
248
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
249
|
+
if (this.awaitingPong) {
|
|
250
|
+
this.emit("error", new Error("WebSocket pong timed out"));
|
|
251
|
+
this.socket.terminate();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.awaitingPong = true;
|
|
255
|
+
this.socket.ping();
|
|
256
|
+
}
|
|
257
|
+
}, intervalSeconds * 1000);
|
|
258
|
+
this.pingTimer.unref();
|
|
259
|
+
}
|
|
260
|
+
stopPingTimer() {
|
|
261
|
+
if (!this.pingTimer) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
clearInterval(this.pingTimer);
|
|
265
|
+
this.pingTimer = undefined;
|
|
266
|
+
this.awaitingPong = false;
|
|
267
|
+
}
|
|
268
|
+
buildChargePointUrl() {
|
|
269
|
+
const base = this.centralSystemUrl.replace(/\/+$/, "");
|
|
270
|
+
const id = encodeURIComponent(this.chargePointId);
|
|
271
|
+
if (base.endsWith(`/${id}`)) {
|
|
272
|
+
return base;
|
|
273
|
+
}
|
|
274
|
+
return `${base}/${id}`;
|
|
275
|
+
}
|
|
276
|
+
buildWebSocketOptions() {
|
|
277
|
+
const options = {
|
|
278
|
+
rejectUnauthorized: this.options.tlsRejectUnauthorized ?? true
|
|
279
|
+
};
|
|
280
|
+
if (this.options.tlsCaFile) {
|
|
281
|
+
options.ca = readFileSync(this.options.tlsCaFile);
|
|
282
|
+
}
|
|
283
|
+
if (this.options.tlsCertFile) {
|
|
284
|
+
options.cert = readFileSync(this.options.tlsCertFile);
|
|
285
|
+
}
|
|
286
|
+
if (this.options.tlsKeyFile) {
|
|
287
|
+
options.key = readFileSync(this.options.tlsKeyFile);
|
|
288
|
+
}
|
|
289
|
+
if (this.options.tlsServerName) {
|
|
290
|
+
options.servername = this.options.tlsServerName;
|
|
291
|
+
}
|
|
292
|
+
return options;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface OcppSchemaValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
errorNames: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class OcppSchemaValidator {
|
|
7
|
+
private readonly validator;
|
|
8
|
+
private readonly schemas;
|
|
9
|
+
constructor(schemaDirectory?: string);
|
|
10
|
+
hasRequestSchema(action: string): boolean;
|
|
11
|
+
hasResponseSchema(action: string): boolean;
|
|
12
|
+
validateRequest(action: string, payload: unknown): OcppSchemaValidationResult;
|
|
13
|
+
validateResponse(action: string, payload: unknown): OcppSchemaValidationResult;
|
|
14
|
+
private validate;
|
|
15
|
+
}
|