# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
+
+# list of regex patterns for memories to completely ignore.
+# Matching memories will not appear in list_memories or activate_project output
+# and cannot be accessed via read_memory or write_memory.
+# To access ignored memory files, use the read_file tool on the raw file path.
+# Extends the list from the global configuration, merging the two lists.
+# Example: ["_archive/.*", "_episodes/.*"]
+ignored_memory_patterns: []
+
+# advanced configuration option allowing to configure language server-specific options.
+# Maps the language key to the options.
+# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
+# No documentation on options means no options are available.
+ls_specific_settings: {}
- [Version 1.6](#version-16-1)
- [Version 2.0.x](#version-20x-1)
- [UI Protocol](#ui-protocol)
+ - [MCP Protocol](#mcp-protocol-model-context-protocol)
- [WebSocket Protocol](#websocket-protocol)
- - [HTTP Protocol](#http-protocol)
+ - [HTTP Protocol (deprecated)](#http-protocol-deprecated)
- [Support, Feedback, Contributing](#support-feedback-contributing)
- [Code of Conduct](#code-of-conduct)
- [Licensing](#licensing)
| supervisionUrlDistribution | round-robin/random/charging-station-affinity | charging-station-affinity | string | supervision urls distribution policy to simulated charging stations |
| log | | {<br />"enabled": true,<br />"file": "logs/combined.log",<br />"errorFile": "logs/error.log",<br />"statisticsInterval": 60,<br />"level": "info",<br />"console": false,<br />"format": "simple",<br />"rotate": true<br />} | {<br />enabled?: boolean;<br />file?: string;<br />errorFile?: string;<br />statisticsInterval?: number;<br />level?: string;<br />console?: boolean;<br />format?: string;<br />rotate?: boolean;<br />maxFiles?: string \| number;<br />maxSize?: string \| number;<br />} | Log configuration section:<br />- _enabled_: enable logging<br />- _file_: log file relative path<br />- _errorFile_: error log file relative path<br />- _statisticsInterval_: seconds between charging stations statistics output in the logs<br />- _level_: emerg/alert/crit/error/warning/notice/info/debug [winston](https://github.com/winstonjs/winston) logging level</br >- _console_: output logs on the console<br />- _format_: [winston](https://github.com/winstonjs/winston) log format<br />- _rotate_: enable daily log files rotation<br />- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options<br />- _maxSize_: maximum size of log files in bytes, or units of kb, mb, and gb: https://github.com/winstonjs/winston-daily-rotate-file#options |
| worker | | {<br />"processType": "workerSet",<br />"startDelay": 500,<br />"elementAddDelay": 0,<br />"elementsPerWorker": 'auto',<br />"poolMinSize": 4,<br />"poolMaxSize": 16<br />} | {<br />processType?: WorkerProcessType;<br />startDelay?: number;<br />elementAddDelay?: number;<br />elementsPerWorker?: number \| 'auto' \| 'all';<br />poolMinSize?: number;<br />poolMaxSize?: number;<br />resourceLimits?: ResourceLimits;<br />} | Worker configuration section:<br />- _processType_: worker threads process type (`workerSet`/`fixedPool`/`dynamicPool`)<br />- _startDelay_: milliseconds to wait at worker threads startup (only for `workerSet` worker threads process type)<br />- _elementAddDelay_: milliseconds to wait between charging station add<br />- _elementsPerWorker_: number of charging stations per worker threads for the `workerSet` process type (`auto` means (number of stations) / (number of CPUs) \* 1.5 if (number of stations) > (number of CPUs), otherwise 1; `all` means a unique worker will run all charging stations)<br />- _poolMinSize_: worker threads pool minimum number of threads</br >- _poolMaxSize_: worker threads pool maximum number of threads<br />- _resourceLimits_: worker threads [resource limits](https://nodejs.org/api/worker_threads.html#new-workerfilename-options) object option |
-| uiServer | | {<br />"enabled": false,<br />"type": "ws",<br />"version": "1.1",<br />"options": {<br />"host": "localhost",<br />"port": 8080<br />}<br />} | {<br />enabled?: boolean;<br />type?: ApplicationProtocol;<br />version?: ApplicationProtocolVersion;<br />options?: ServerOptions;<br />authentication?: {<br />enabled: boolean;<br />type: AuthenticationType;<br />username?: string;<br />password?: string;<br />}<br />} | UI server configuration section:<br />- _enabled_: enable UI server<br />- _type_: 'http' or 'ws'<br />- _version_: HTTP version '1.1' or '2.0'<br />- _options_: node.js net module [listen options](https://nodejs.org/api/net.html#serverlistenoptions-callback)<br />- _authentication_: authentication type configuration section |
+| uiServer | | {<br />"enabled": false,<br />"type": "ws",<br />"version": "1.1",<br />"options": {<br />"host": "localhost",<br />"port": 8080<br />}<br />} | {<br />enabled?: boolean;<br />type?: ApplicationProtocol;<br />version?: ApplicationProtocolVersion;<br />options?: ServerOptions;<br />authentication?: {<br />enabled: boolean;<br />type: AuthenticationType;<br />username?: string;<br />password?: string;<br />}<br />} | UI server configuration section:<br />- _enabled_: enable UI server<br />- _type_: 'ws', 'mcp' or 'http' (deprecated)<br />- _version_: HTTP version '1.1' or '2.0' (ws and mcp transports only support '1.1')<br />- _options_: node.js net module [listen options](https://nodejs.org/api/net.html#serverlistenoptions-callback)<br />- _authentication_: authentication type configuration section |
| performanceStorage | | {<br />"enabled": true,<br />"type": "none",<br />} | {<br />enabled?: boolean;<br />type?: string;<br />uri?: string;<br />} | Performance storage configuration section:<br />- _enabled_: enable performance storage<br />- _type_: 'jsonfile', 'mongodb' or 'none'<br />- _uri_: storage URI |
| stationTemplateUrls | | {}[] | {<br />file: string;<br />numberOfStations: number;<br />provisionedNumberOfStations?: number;<br />}[] | array of charging station templates URIs configuration section:<br />- _file_: charging station configuration template file relative path<br />- _numberOfStations_: template number of stations at startup<br />- _provisionedNumberOfStations_: template provisioned number of stations after startup |
#### Evses section syntax example
+`MeterValues` can be defined at EVSE level or at connector level. EVSE-level definitions apply to all connectors of the EVSE and override connector-level definitions.
+
+##### MeterValues at EVSE level
+
+```json
+ "Evses": {
+ "0": {
+ "Connectors": {
+ "0": {}
+ }
+ },
+ "1": {
+ "MeterValues": [
+ ...
+ {
+ "unit": "W",
+ "measurand": "Power.Active.Import",
+ "phase": "L1-N",
+ "value": "5000",
+ "fluctuationPercent": "10"
+ },
+ ...
+ {
+ "unit": "A",
+ "measurand": "Current.Import",
+ "minimum": "0.5"
+ },
+ ...
+ {
+ "unit": "Wh"
+ },
+ ...
+ ],
+ "Connectors": {
+ "1": {
+ "bootStatus": "Available"
+ }
+ }
+ }
+ },
+```
+
+##### MeterValues at connector level
+
```json
"Evses": {
"0": {
## UI Protocol
-Protocol to control the simulator via a WebSocket or HTTP server:
+Protocol to control the simulator via the UI server. Three transport types are available:
+
+### MCP Protocol (Model Context Protocol)
+
+The recommended transport for programmatic access. [MCP](https://spec.modelcontextprotocol.io) enables LLM agents and AI tools to discover and use the simulator's capabilities automatically.
+
+#### Agent configuration
+
+| Parameter | Value | Description |
+| ---------------- | -------------------------- | ------------------------------------------------------------------------------------- |
+| URL | `http://<host>:<port>/mcp` | Streamable HTTP endpoint (stateless) |
+| Transport | Streamable HTTP | `POST /mcp` for requests, `GET /mcp` for SSE stream, `DELETE /mcp` for session close |
+| Authentication | Basic Auth (optional) | If enabled in simulator config, use `Authorization: Basic <base64(user:pass)>` header |
+| Protocol version | `2025-03-26` | MCP specification version |
+
+### WebSocket Protocol
+
+SRPC protocol over WebSocket for real-time dashboard communication. PDU stands for 'Protocol Data Unit'.
```mermaid
sequenceDiagram
Client->>UI Server: request
UI Server->>Client: response
-Note over UI Server,Client: HTTP or WebSocket
+Note over UI Server,Client: WebSocket
```
-### WebSocket Protocol
-
-SRPC protocol over WebSocket. PDU stands for 'Protocol Data Unit'.
-
- Request:
[`uuid`, `ProcedureName`, `PDU`]
`uuid`: String uniquely representing this request
`responsesFailed`: failed responses payload array (optional)
}
-### HTTP Protocol
+### HTTP Protocol (deprecated)
+
+> **Deprecated**: Use `"type": "mcp"` for HTTP-based access to the simulator.
To learn how to use the HTTP protocol to pilot the simulator, an [Insomnia](https://insomnia.rest/) HTTP requests collection is available in [src/assets/ui-protocol](./src/assets/ui-protocol) directory.
'reservability',
// VPN protocol acronyms
'PPTP',
+ // UI server protocol acronyms
+ 'UIMCP',
+ 'Streamable',
+ 'modelcontextprotocol',
],
},
},
"@mikro-orm/core": "^6.6.9",
"@mikro-orm/mariadb": "^6.6.9",
"@mikro-orm/reflection": "^6.6.9",
+ "@modelcontextprotocol/sdk": "~1.27.1",
"ajv": "^8.18.0",
"ajv-formats": "^3.0.1",
"basic-ftp": "^5.2.0",
"tar": "^7.5.12",
"winston": "^3.19.0",
"winston-daily-rotate-file": "^5.0.0",
- "ws": "^8.20.0"
+ "ws": "^8.20.0",
+ "zod": "^4.3.6"
},
"optionalDependencies": {
"bufferutil": "^4.1.0",
'@mikro-orm/reflection':
specifier: ^6.6.9
version: 6.6.9(@mikro-orm/core@6.6.9)
+ '@modelcontextprotocol/sdk':
+ specifier: ~1.27.1
+ version: 1.27.1(zod@4.3.6)
ajv:
specifier: ^8.18.0
version: 8.18.0
ws:
specifier: ^8.20.0
version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
devDependencies:
'@commitlint/cli':
specifier: ^20.5.0
'@noble/hashes':
optional: true
+ '@hono/node-server@1.19.11':
+ resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
+ engines: {node: '>=18.14.1'}
+ peerDependencies:
+ hono: ^4
+
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
peerDependencies:
'@mikro-orm/core': ^6.0.0
+ '@modelcontextprotocol/sdk@1.27.1':
+ resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@cfworker/json-schema': ^4.1.1
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ '@cfworker/json-schema':
+ optional: true
+
'@mongodb-js/saslprep@1.4.6':
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
+ accepts@2.0.0:
+ resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+ engines: {node: '>= 0.6'}
+
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
bn.js@5.2.3:
resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==}
+ body-parser@2.2.2:
+ resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
+ engines: {node: '>=18'}
+
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==}
engines: {node: '>=20'}
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
cacheable-lookup@7.0.0:
resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
engines: {node: '>=14.16'}
constants-browserify@1.0.0:
resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==}
+ content-disposition@1.0.1:
+ resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
+ engines: {node: '>=18'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
conventional-changelog-angular@8.3.0:
resolution: {integrity: sha512-DOuBwYSqWzfwuRByY9O4oOIvDlkUCTDzfbOgcSbkY+imXXj+4tmrEFao3K+FxemClYfYnZzsvudbwrhje9VHDA==}
engines: {node: '>=18'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+ cookie-signature@1.2.2:
+ resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
+ engines: {node: '>=6.6.0'}
+
+ cookie@0.7.2:
+ resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
+ engines: {node: '>= 0.6'}
+
copy-to-clipboard@3.3.3:
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+ cors@2.8.6:
+ resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
+ engines: {node: '>= 0.10'}
+
cosmiconfig-typescript-loader@6.2.0:
resolution: {integrity: sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==}
engines: {node: '>=v18'}
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
+ eventsource-parser@3.0.6:
+ resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
+ engines: {node: '>=18.0.0'}
+
+ eventsource@3.0.7:
+ resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
+ engines: {node: '>=18.0.0'}
+
evp_bytestokey@1.0.3:
resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==}
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
+ express-rate-limit@8.3.1:
+ resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ express: '>= 4.11'
+
+ express@5.2.1:
+ resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
+ engines: {node: '>= 18'}
+
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
hmac-drbg@1.0.1:
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
+ hono@4.12.8:
+ resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
+ engines: {node: '>=16.9.0'}
+
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
+ iconv-lite@0.7.2:
+ resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
+ engines: {node: '>=0.10.0'}
+
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
iota-array@1.0.0:
resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==}
+ ip-address@10.1.0:
+ resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
+ engines: {node: '>= 12'}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
is-any-array@2.0.1:
resolution: {integrity: sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ==}
is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+ is-promise@4.0.0:
+ resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
+
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+ jose@6.2.2:
+ resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
+
js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'}
json-schema-typed@7.0.3:
resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==}
+ json-schema-typed@8.0.2:
+ resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
+
json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+ media-typer@1.1.0:
+ resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
+ engines: {node: '>= 0.8'}
+
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
+ merge-descriptors@2.0.0:
+ resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
+ engines: {node: '>=18'}
+
merge-source-map@1.0.4:
resolution: {integrity: sha512-PGSmS0kfnTnMJCzJ16BLLCEe6oeYCamKFFdQKshi4BmM6FUwipjVOcBFGxqtQtirtAG4iZvHlqST9CpZKqlRjA==}
ndarray@1.0.19:
resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==}
+ negotiator@1.0.0:
+ resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+ engines: {node: '>= 0.6'}
+
neostandard@0.13.0:
resolution: {integrity: sha512-R3iglFr+Dla/8qFBqsMxBvcYBOgP6rAGw7uRHKMpM3bUP0wLDRzUstxtEI9RfEwn7xszE/UUnh8H090Ru4Z52A==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
+ path-to-regexp@8.3.0:
+ resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
+
path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
+ pkce-challenge@5.0.1:
+ resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
+ engines: {node: '>=16.20.0'}
+
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
resolution: {integrity: sha512-hNp56d5uuREVde7UqP+dmBkwzxrhJwYU5nL/mdivyFfkRZdgAgojkyBeU3jKo7ZHrjdSx6Q1CwUmYJI6INt20g==}
hasBin: true
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
public-encrypt@4.0.3:
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
+ raw-body@3.0.2:
+ resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
+ engines: {node: '>= 0.10'}
+
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
+ router@2.2.0:
+ resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
+ engines: {node: '>= 18'}
+
run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
+ type-is@2.0.1:
+ resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
+ engines: {node: '>= 0.6'}
+
type@2.7.3:
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
unplugin-utils@0.3.1:
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
engines: {node: '>=20.19.0'}
varint@5.0.2:
resolution: {integrity: sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==}
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
verror@1.10.0:
resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==}
engines: {'0': node >=0.6.0}
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
engines: {node: '>=12.20'}
+ zod-to-json-schema@3.25.1:
+ resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
+ peerDependencies:
+ zod: ^3.25 || ^4
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
snapshots:
0x@5.8.0:
'@exodus/bytes@1.15.0': {}
+ '@hono/node-server@1.19.11(hono@4.12.8)':
+ dependencies:
+ hono: 4.12.8
+
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
globby: 11.1.0
ts-morph: 27.0.2
+ '@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
+ dependencies:
+ '@hono/node-server': 1.19.11(hono@4.12.8)
+ ajv: 8.18.0
+ ajv-formats: 3.0.1(ajv@8.18.0)
+ content-type: 1.0.5
+ cors: 2.8.6
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.0.6
+ express: 5.2.1
+ express-rate-limit: 8.3.1(express@5.2.1)
+ hono: 4.12.8
+ jose: 6.2.2
+ json-schema-typed: 8.0.2
+ pkce-challenge: 5.0.1
+ raw-body: 3.0.2
+ zod: 4.3.6
+ zod-to-json-schema: 3.25.1(zod@4.3.6)
+ transitivePeerDependencies:
+ - supports-color
+
'@mongodb-js/saslprep@1.4.6':
dependencies:
sparse-bitfield: 3.0.3
dependencies:
event-target-shim: 5.0.1
+ accepts@2.0.0:
+ dependencies:
+ mime-types: 3.0.2
+ negotiator: 1.0.0
+
acorn-jsx@5.3.2(acorn@8.16.0):
dependencies:
acorn: 8.16.0
bn.js@5.2.3: {}
+ body-parser@2.2.2:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 4.4.3
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ on-finished: 2.4.1
+ qs: 6.15.0
+ raw-body: 3.0.2
+ type-is: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
boolbase@1.0.0: {}
boxen@5.1.2:
byte-counter@0.1.0: {}
+ bytes@3.1.2: {}
+
cacheable-lookup@7.0.0: {}
cacheable-request@13.0.18:
constants-browserify@1.0.0: {}
+ content-disposition@1.0.1: {}
+
+ content-type@1.0.5: {}
+
conventional-changelog-angular@8.3.0:
dependencies:
compare-func: 2.0.0
convert-source-map@2.0.0: {}
+ cookie-signature@1.2.2: {}
+
+ cookie@0.7.2: {}
+
copy-to-clipboard@3.3.3:
dependencies:
toggle-selection: 1.0.6
core-util-is@1.0.3: {}
+ cors@2.8.6:
+ dependencies:
+ object-assign: 4.1.1
+ vary: 1.1.2
+
cosmiconfig-typescript-loader@6.2.0(@types/node@24.12.0)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3):
dependencies:
'@types/node': 24.12.0
events@3.3.0: {}
+ eventsource-parser@3.0.6: {}
+
+ eventsource@3.0.7:
+ dependencies:
+ eventsource-parser: 3.0.6
+
evp_bytestokey@1.0.3:
dependencies:
md5.js: 1.3.5
expect-type@1.3.0: {}
+ express-rate-limit@8.3.1(express@5.2.1):
+ dependencies:
+ express: 5.2.1
+ ip-address: 10.1.0
+
+ express@5.2.1:
+ dependencies:
+ accepts: 2.0.0
+ body-parser: 2.2.2
+ content-disposition: 1.0.1
+ content-type: 1.0.5
+ cookie: 0.7.2
+ cookie-signature: 1.2.2
+ debug: 4.4.3
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 2.1.1
+ fresh: 2.0.0
+ http-errors: 2.0.1
+ merge-descriptors: 2.0.0
+ mime-types: 3.0.2
+ on-finished: 2.4.1
+ once: 1.4.0
+ parseurl: 1.3.3
+ proxy-addr: 2.0.7
+ qs: 6.15.0
+ range-parser: 1.2.1
+ router: 2.2.0
+ send: 1.2.1
+ serve-static: 2.2.1
+ statuses: 2.0.2
+ type-is: 2.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
exsolve@1.0.8: {}
ext@1.7.0:
hasown: 2.0.2
mime-types: 2.1.35
+ forwarded@0.2.0: {}
+
fresh@2.0.0: {}
from2-string@1.1.0:
minimalistic-assert: 1.0.1
minimalistic-crypto-utils: 1.0.1
+ hono@4.12.8: {}
+
hookable@5.5.3: {}
hsl-to-rgb-for-reals@1.1.1: {}
dependencies:
safer-buffer: 2.1.2
+ iconv-lite@0.7.2:
+ dependencies:
+ safer-buffer: 2.1.2
+
ieee754@1.2.1: {}
ignore@5.3.2: {}
iota-array@1.0.0: {}
+ ip-address@10.1.0: {}
+
+ ipaddr.js@1.9.1: {}
+
is-any-array@2.0.1: {}
is-arguments@1.2.0:
is-potential-custom-element-name@1.0.1: {}
+ is-promise@4.0.0: {}
+
is-property@1.0.2: {}
is-regex@1.2.1:
jiti@2.6.1: {}
+ jose@6.2.2: {}
+
js-beautify@1.15.4:
dependencies:
config-chain: 1.1.13
json-schema-typed@7.0.3: {}
+ json-schema-typed@8.0.2: {}
+
json-schema@0.4.0: {}
json-stable-stringify-without-jsonify@1.0.1: {}
md5.js@1.3.5:
dependencies:
- hash-base: 3.0.5
+ hash-base: 3.1.2
inherits: 2.0.4
safe-buffer: 5.2.1
mdn-data@2.27.1: {}
+ media-typer@1.1.0: {}
+
memory-pager@1.5.0: {}
meow@13.2.0: {}
+ merge-descriptors@2.0.0: {}
+
merge-source-map@1.0.4:
dependencies:
source-map: 0.5.7
iota-array: 1.0.0
is-buffer: 1.1.6
+ negotiator@1.0.0: {}
+
neostandard@0.13.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@humanwhocodes/gitignore-to-minimatch': 1.0.2
lru-cache: 11.2.7
minipass: 7.1.3
+ path-to-regexp@8.3.0: {}
+
path-type@4.0.0: {}
pathe@2.0.3: {}
pify@2.3.0: {}
+ pkce-challenge@5.0.1: {}
+
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
transitivePeerDependencies:
- react-native-b4a
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
public-encrypt@4.0.3:
dependencies:
bn.js: 5.2.3
range-parser@1.2.1: {}
+ raw-body@3.0.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.1
+ iconv-lite: 0.7.2
+ unpipe: 1.0.0
+
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10
+ router@2.2.0:
+ dependencies:
+ debug: 4.4.3
+ depd: 2.0.0
+ is-promise: 4.0.0
+ parseurl: 1.3.3
+ path-to-regexp: 8.3.0
+ transitivePeerDependencies:
+ - supports-color
+
run-async@2.4.1: {}
run-parallel@1.2.0:
type-fest@4.41.0: {}
+ type-is@2.0.1:
+ dependencies:
+ content-type: 1.0.5
+ media-typer: 1.1.0
+ mime-types: 3.0.2
+
type@2.7.3: {}
typed-array-buffer@1.0.3:
universalify@2.0.1: {}
+ unpipe@1.0.0: {}
+
unplugin-utils@0.3.1:
dependencies:
pathe: 2.0.3
varint@5.0.2: {}
+ vary@1.1.2: {}
+
verror@1.10.0:
dependencies:
assert-plus: 1.0.0
yocto-queue@0.1.0: {}
yocto-queue@1.2.2: {}
+
+ zod-to-json-schema@3.25.1(zod@4.3.6):
+ dependencies:
+ zod: 4.3.6
+
+ zod@4.3.6: {}
entryPoints: ['./src/start.ts', './src/charging-station/ChargingStationWorker.ts'],
external: [
'@mikro-orm/*',
+ '@modelcontextprotocol/*',
'ajv',
'ajv-formats',
'basic-ftp',
'winston/*',
'winston-daily-rotate-file',
'ws',
+ 'zod',
],
format: 'esm',
minify: true,
type Response,
StandardParametersKey,
type Status,
+ type StatusNotificationRequest,
type StopTransactionReason,
SupervisionUrlDistribution,
SupportedFeatureProfiles,
connectorStatus.reservation = reservation
await sendAndSetConnectorStatus(
this,
- reservation.connectorId,
- ConnectorStatusEnum.Reserved,
- undefined,
+ {
+ connectorId: reservation.connectorId,
+ status: ConnectorStatusEnum.Reserved,
+ } as unknown as StatusNotificationRequest,
{ send: reservation.connectorId !== 0 }
)
}
case ReservationTerminationReason.RESERVATION_CANCELED:
await sendAndSetConnectorStatus(
this,
- reservation.connectorId,
- ConnectorStatusEnum.Available,
- undefined,
+ {
+ connectorId: reservation.connectorId,
+ status: ConnectorStatusEnum.Available,
+ } as unknown as StatusNotificationRequest,
{ send: reservation.connectorId !== 0 }
)
delete connector.reservation
for (const [evseId, evseStatus] of this.evses) {
if (evseId > 0) {
for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- await sendAndSetConnectorStatus(
- this,
+ await sendAndSetConnectorStatus(this, {
connectorId,
- getBootConnectorStatus(this, connectorId, connectorStatus),
- evseId
- )
+ evseId,
+ status: getBootConnectorStatus(this, connectorId, connectorStatus),
+ } as unknown as StatusNotificationRequest)
}
}
}
)
continue
}
- await sendAndSetConnectorStatus(
- this,
+ await sendAndSetConnectorStatus(this, {
connectorId,
- getBootConnectorStatus(this, connectorId, connectorStatus)
- )
+ status: getBootConnectorStatus(this, connectorId, connectorStatus),
+ } as unknown as StatusNotificationRequest)
}
}
}
for (const [evseId, evseStatus] of this.evses) {
if (evseId > 0) {
for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- await sendAndSetConnectorStatus(
- this,
+ await sendAndSetConnectorStatus(this, {
connectorId,
- ConnectorStatusEnum.Unavailable,
- evseId
- )
+ evseId,
+ status: ConnectorStatusEnum.Unavailable,
+ } as unknown as StatusNotificationRequest)
delete connectorStatus.status
}
}
} else {
for (const connectorId of this.connectors.keys()) {
if (connectorId > 0) {
- await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
+ await sendAndSetConnectorStatus(this, {
+ connectorId,
+ status: ConnectorStatusEnum.Unavailable,
+ } as unknown as StatusNotificationRequest)
delete this.getConnectorStatus(connectorId)?.status
}
}
type MessageEvent,
type MeterValuesRequest,
type MeterValuesResponse,
+ type OCPP16AuthorizeResponse,
OCPP20AuthorizationStatusEnumType,
+ type OCPP20AuthorizeResponse,
type OCPP20Get15118EVCertificateRequest,
type OCPP20Get15118EVCertificateResponse,
type OCPP20GetCertificateStatusRequest,
): ResponseStatus {
switch (command) {
case BroadcastChannelProcedureName.AUTHORIZE:
+ switch (this.chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ if (
+ (commandResponse as OCPP16AuthorizeResponse).idTagInfo.status ===
+ AuthorizationStatus.ACCEPTED
+ ) {
+ return ResponseStatus.SUCCESS
+ }
+ return ResponseStatus.FAILURE
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ if (
+ (commandResponse as OCPP20AuthorizeResponse).idTokenInfo.status ===
+ AuthorizationStatus.Accepted
+ ) {
+ return ResponseStatus.SUCCESS
+ }
+ return ResponseStatus.FAILURE
+ default:
+ return ResponseStatus.FAILURE
+ }
case BroadcastChannelProcedureName.START_TRANSACTION:
case BroadcastChannelProcedureName.STOP_TRANSACTION:
if (
(
commandResponse as
- | AuthorizeResponse
+ | OCPP16AuthorizeResponse
| StartTransactionResponse
| StopTransactionResponse
).idTagInfo?.status === AuthorizationStatus.ACCEPTED
requestPayload?: BroadcastChannelRequestPayload
): Promise<MeterValuesResponse> {
const connectorId = requestPayload?.connectorId
- if (
- this.chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_20 ||
- this.chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_201
- ) {
- const alignedDataInterval = OCPP20ServiceUtils.getAlignedDataInterval(this.chargingStation)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const evseId = this.chargingStation.getEvseIdByConnectorId(connectorId!)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const transactionId = this.chargingStation.getConnectorStatus(connectorId!)?.transactionId
- return await this.chargingStation.ocppRequestService.requestHandler<
- MeterValuesRequest,
- MeterValuesResponse
- >(
- this.chargingStation,
- RequestCommand.METER_VALUES,
- {
- evseId,
- meterValue: [
- buildMeterValue(
- this.chargingStation,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorId!,
- transactionId,
- alignedDataInterval
- ),
- ],
- ...requestPayload,
- } as MeterValuesRequest,
- this.requestParams
- )
+ switch (this.chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ const configuredMeterValueSampleInterval = getConfigurationKey(
+ this.chargingStation,
+ StandardParametersKey.MeterValueSampleInterval
+ )
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const transactionId = this.chargingStation.getConnectorStatus(connectorId!)?.transactionId
+ return await this.chargingStation.ocppRequestService.requestHandler<
+ MeterValuesRequest,
+ MeterValuesResponse
+ >(
+ this.chargingStation,
+ RequestCommand.METER_VALUES,
+ {
+ meterValue: [
+ buildMeterValue(
+ this.chargingStation,
+ convertToInt(transactionId),
+ configuredMeterValueSampleInterval != null
+ ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
+ : Constants.DEFAULT_METER_VALUES_INTERVAL
+ ),
+ ],
+ ...requestPayload,
+ } as MeterValuesRequest,
+ this.requestParams
+ )
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201: {
+ const alignedDataInterval = OCPP20ServiceUtils.getAlignedDataInterval(this.chargingStation)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const evseId = this.chargingStation.getEvseIdByConnectorId(connectorId!)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const transactionId = this.chargingStation.getConnectorStatus(connectorId!)?.transactionId
+ return await this.chargingStation.ocppRequestService.requestHandler<
+ MeterValuesRequest,
+ MeterValuesResponse
+ >(
+ this.chargingStation,
+ RequestCommand.METER_VALUES,
+ {
+ evseId,
+ meterValue: [
+ buildMeterValue(
+ this.chargingStation,
+
+ transactionId,
+ alignedDataInterval
+ ),
+ ],
+ ...requestPayload,
+ } as MeterValuesRequest,
+ this.requestParams
+ )
+ }
+ default:
+ throw new BaseError(
+ `${this.chargingStation.logPrefix()} ${moduleName}.handleMeterValues: Unsupported OCPP version for MeterValues`
+ )
}
- const configuredMeterValueSampleInterval = getConfigurationKey(
- this.chargingStation,
- StandardParametersKey.MeterValueSampleInterval
- )
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const transactionId = this.chargingStation.getConnectorStatus(connectorId!)?.transactionId
- return await this.chargingStation.ocppRequestService.requestHandler<
- MeterValuesRequest,
- MeterValuesResponse
- >(
- this.chargingStation,
- RequestCommand.METER_VALUES,
- {
- meterValue: [
- buildMeterValue(
- this.chargingStation,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorId!,
- convertToInt(transactionId),
- configuredMeterValueSampleInterval != null
- ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
- : Constants.DEFAULT_METER_VALUES_INTERVAL
- ),
- ],
- ...requestPayload,
- } as MeterValuesRequest,
- this.requestParams
- )
}
private async handleNotifyCustomerInformation (
meterValue: [
buildMeterValue(
chargingStation,
- connectorId,
convertToInt(connectorStatus.transactionId),
0
) as OCPP16MeterValue,
meterValue: [
buildMeterValue(
chargingStation,
- id,
convertToInt(cs.transactionId),
0
) as OCPP16MeterValue,
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
chargingStation.getConnectorStatus(connectorId)!.availability = type
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- chargePointStatus
- )
+ status: chargePointStatus,
+ } as OCPP16StatusNotificationRequest)
return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
}
return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
}
return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
}
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Available
- )
+ status: OCPP16ChargePointStatus.Available,
+ } as OCPP16StatusNotificationRequest)
return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
}
if (evseId > 0) {
for (const [connectorId, connectorStatus] of evseStatus.connectors) {
if (connectorStatus.transactionStarted === false) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
+ status: OCPP16ChargePointStatus.Unavailable,
+ } as OCPP16StatusNotificationRequest)
}
}
}
connectorId > 0 &&
chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
+ status: OCPP16ChargePointStatus.Unavailable,
+ } as OCPP16StatusNotificationRequest)
}
}
}
if (evseId > 0) {
for (const [connectorId, connectorStatus] of evseStatus.connectors) {
if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
+ status: OCPP16ChargePointStatus.Unavailable,
+ } as OCPP16StatusNotificationRequest)
}
}
}
chargingStation.getConnectorStatus(connectorId)?.status !==
OCPP16ChargePointStatus.Unavailable
) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
+ status: OCPP16ChargePointStatus.Unavailable,
+ } as OCPP16StatusNotificationRequest)
}
}
}
import { OCPPError } from '../../../exception/index.js'
import {
- type ConnectorStatusEnum,
ErrorType,
type JsonObject,
type JsonType,
type OCPP16MeterValue,
OCPP16RequestCommand,
type OCPP16StartTransactionRequest,
+ type OCPP16StatusNotificationRequest,
OCPPVersion,
type RequestParams,
} from '../../../types/index.js'
// Pre request actions hook
switch (commandName) {
case OCPP16RequestCommand.START_TRANSACTION:
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- (commandParams as OCPP16StartTransactionRequest).connectorId,
- OCPP16ChargePointStatus.Preparing
- )
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
+ connectorId: (commandParams as OCPP16StartTransactionRequest).connectorId,
+ status: OCPP16ChargePointStatus.Preparing,
+ } as OCPP16StatusNotificationRequest)
break
}
const response = (await this.sendMessage(
case OCPP16RequestCommand.STATUS_NOTIFICATION:
return buildStatusNotificationRequest(
chargingStation,
- commandParams.connectorId as number,
- commandParams.status as ConnectorStatusEnum,
- commandParams.evseId as number | undefined
+ commandParams as unknown as OCPP16StatusNotificationRequest
) as unknown as Request
case OCPP16RequestCommand.STOP_TRANSACTION:
chargingStation.stationInfo?.transactionDataMeterValues === true &&
OCPP16StandardParametersKey,
type OCPP16StartTransactionRequest,
type OCPP16StartTransactionResponse,
+ type OCPP16StatusNotificationRequest,
type OCPP16StopTransactionRequest,
type OCPP16StopTransactionResponse,
OCPPVersion,
meterValue: [connectorStatus.transactionBeginMeterValue],
transactionId: payload.transactionId,
} satisfies OCPP16MeterValuesRequest))
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Charging
- )
+ status: OCPP16ChargePointStatus.Charging,
+ } as OCPP16StatusNotificationRequest)
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.handleResponseStartTransaction: Transaction with id ${payload.transactionId.toString()} STARTED on ${
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
!chargingStation.isChargingStationAvailable() ||
!chargingStation.isConnectorAvailable(transactionConnectorId)
) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- transactionConnectorId,
- OCPP16ChargePointStatus.Unavailable
- )
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
+ connectorId: transactionConnectorId,
+ status: OCPP16ChargePointStatus.Unavailable,
+ } as OCPP16StatusNotificationRequest)
} else {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- transactionConnectorId,
- OCPP16ChargePointStatus.Available
- )
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
+ connectorId: transactionConnectorId,
+ status: OCPP16ChargePointStatus.Available,
+ } as OCPP16StatusNotificationRequest)
}
if (chargingStation.stationInfo?.powerSharedByConnectors === true) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
OCPP16RequestCommand,
type OCPP16SampledValue,
OCPP16StandardParametersKey,
+ type OCPP16StatusNotificationRequest,
OCPP16StopTransactionReason,
type OCPP16SupportedFeatureProfiles,
OCPPVersion,
}
connectorStatus.availability = availabilityType
if (response === OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- chargePointStatus
- )
+ status: chargePointStatus,
+ } as OCPP16StatusNotificationRequest)
}
responses.push(response)
}
chargingStation: ChargingStation,
connectorId: number
): Promise<GenericResponse> => {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(chargingStation, {
connectorId,
- OCPP16ChargePointStatus.Finishing
- )
+ status: OCPP16ChargePointStatus.Finishing,
+ } as OCPP16StatusNotificationRequest)
const stopResponse = await OCPP16ServiceUtils.stopTransactionOnConnector(
chargingStation,
connectorId,
}
connectorStatus.transactionMeterValuesSetInterval = setInterval(() => {
const transactionId = convertToInt(connectorStatus.transactionId)
- const meterValue = buildMeterValue(chargingStation, connectorId, transactionId, interval)
+ const meterValue = buildMeterValue(chargingStation, transactionId, interval)
chargingStation.ocppRequestService
.requestHandler<MeterValuesRequest, MeterValuesResponse>(
chargingStation,
const txUpdatedInterval = OCPP20ServiceUtils.getTxUpdatedInterval(chargingStation)
const meterValue = buildMeterValue(
chargingStation,
- cId,
connector.transactionId,
txUpdatedInterval
) as OCPP20MeterValue
}
}
if (!hasSentTransactionEvent) {
- const fallbackEvseId = evse?.id ?? 0
- let meterValue: OCPP20MeterValue
- try {
- meterValue = buildMeterValue(
- chargingStation,
- fallbackEvseId > 0 ? fallbackEvseId : 1,
- undefined,
- OCPP20ServiceUtils.getTxUpdatedInterval(chargingStation)
- ) as OCPP20MeterValue
- } catch {
- meterValue = {
- sampledValue: [{ value: 0 }],
- timestamp: new Date(),
- }
+ const meterValue: OCPP20MeterValue = {
+ sampledValue: [{ value: 0 }],
+ timestamp: new Date(),
}
chargingStation.ocppRequestService
.requestHandler<OCPP20MeterValuesRequest, OCPP20MeterValuesResponse>(
chargingStation,
OCPP20RequestCommand.METER_VALUES,
{
- evseId: fallbackEvseId,
+ evseId: evse?.id ?? 1,
meterValue: [meterValue],
},
{ skipBufferingOnError: true, triggerMessage: true }
? this.getRestoredConnectorStatus(chargingStation, connectorId)
: newConnectorStatus
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- resolvedStatus as ConnectorStatusEnum
- ).catch((error: unknown) => {
+ connectorStatus: resolvedStatus,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.handleConnectorChangeAvailability: Error sending status notification for connector ${connectorId.toString()}:`,
error
`${chargingStation.logPrefix()} ${moduleName}.handleRequestUnlockConnector: Unlocking connector ${connectorId.toString()} on EVSE ${evseId.toString()}`
)
- await sendAndSetConnectorStatus(
- chargingStation,
+ await sendAndSetConnectorStatus(chargingStation, {
connectorId,
- ConnectorStatusEnum.Available,
- evseId
- )
+ connectorStatus: ConnectorStatusEnum.Available,
+ evseId,
+ } as unknown as OCPP20StatusNotificationRequest)
return { status: UnlockStatusEnumType.Unlocked }
} catch (error) {
): void {
for (const [, evse] of chargingStation.evses) {
for (const [connectorId] of evse.connectors) {
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- status as ConnectorStatusEnum
- ).catch((error: unknown) => {
+ connectorStatus: status,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.sendAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
error
const evse = chargingStation.getEvseStatus(evseId)
if (evse) {
for (const [connectorId] of evse.connectors) {
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- status as ConnectorStatusEnum
- ).catch((error: unknown) => {
+ connectorStatus: status,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.sendEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
error
if (evseId > 0) {
for (const [connectorId] of evseStatus.connectors) {
const restoredStatus = this.getRestoredConnectorStatus(chargingStation, connectorId)
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- restoredStatus as ConnectorStatusEnum
- ).catch((error: unknown) => {
+ connectorStatus: restoredStatus,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.sendRestoredAllConnectorsStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
error
if (evse) {
for (const [connectorId] of evse.connectors) {
const restoredStatus = this.getRestoredConnectorStatus(chargingStation, connectorId)
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- restoredStatus as ConnectorStatusEnum
- ).catch((error: unknown) => {
+ connectorStatus: restoredStatus,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.sendRestoredEvseStatusNotifications: Error sending status notification for connector ${connectorId.toString()}:`,
error
.requestHandler<
OCPP20StatusNotificationRequest,
OCPP20StatusNotificationResponse
- >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, evseId, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
+ >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId, connectorStatus: resolvedStatus, evseId } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
.catch(errorHandler)
}
}
.requestHandler<
OCPP20StatusNotificationRequest,
OCPP20StatusNotificationResponse
- >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId: evse.connectorId, evseId: evse.id, status: resolvedStatus } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
+ >(chargingStation, OCPP20RequestCommand.STATUS_NOTIFICATION, { connectorId: evse.connectorId, connectorStatus: resolvedStatus, evseId: evse.id } as unknown as OCPP20StatusNotificationRequest, { skipBufferingOnError: true, triggerMessage: true })
.catch(errorHandler)
} else if (chargingStation.hasEvses) {
this.triggerAllEvseStatusNotifications(chargingStation, errorHandler)
import { OCPPError } from '../../../exception/index.js'
import {
type CertificateSigningUseEnumType,
- type ConnectorStatusEnum,
ErrorType,
type JsonObject,
type JsonType,
OCPP20RequestCommand,
type OCPP20SignCertificateRequest,
+ type OCPP20StatusNotificationRequest,
+ type OCPP20TransactionEventRequest,
OCPPVersion,
type RequestParams,
} from '../../../types/index.js'
case OCPP20RequestCommand.STATUS_NOTIFICATION:
return buildStatusNotificationRequest(
chargingStation,
- commandParams.connectorId as number,
- commandParams.status as ConnectorStatusEnum,
- commandParams.evseId as number | undefined
+ commandParams as unknown as OCPP20StatusNotificationRequest
) as unknown as Request
case OCPP20RequestCommand.TRANSACTION_EVENT:
- return buildTransactionEvent(chargingStation, commandParams) as unknown as Request
+ return buildTransactionEvent(
+ chargingStation,
+ commandParams as unknown as OCPP20TransactionEventRequest
+ ) as unknown as Request
default: {
// OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
const errorMsg = `Unsupported OCPP command ${commandName as string} for payload building`
OCPP20RequestCommand,
type OCPP20SecurityEventNotificationResponse,
type OCPP20SignCertificateResponse,
+ type OCPP20StatusNotificationRequest,
type OCPP20StatusNotificationResponse,
OCPP20TransactionEventEnumType,
type OCPP20TransactionEventRequest,
payload.idTokenInfo == null ||
payload.idTokenInfo.status === OCPP20AuthorizationStatusEnumType.Accepted
if (connectorId != null && isIdTokenAccepted) {
- sendAndSetConnectorStatus(
- chargingStation,
+ sendAndSetConnectorStatus(chargingStation, {
connectorId,
- ConnectorStatusEnum.Occupied
- ).catch((error: unknown) => {
+ connectorStatus: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP20StatusNotificationRequest).catch((error: unknown) => {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.handleResponseTransactionEvent: Error sending StatusNotification(Occupied):`,
error
type ConnectorStatus,
ConnectorStatusEnum,
ErrorType,
- type JsonObject,
OCPP20ChargingStateEnumType,
OCPP20ComponentName,
type OCPP20EVSEType,
OCPP20ReasonEnumType,
OCPP20RequestCommand,
OCPP20RequiredVariableName,
+ type OCPP20StatusNotificationRequest,
OCPP20TransactionEventEnumType,
type OCPP20TransactionEventOptions,
type OCPP20TransactionEventRequest,
// Offline: build and queue pre-built payload (sent as-is via rawPayload on reconnect)
if (!chargingStation.isWebSocketConnectionOpened()) {
// E04.FR.03: offline flag SHALL be TRUE for any TransactionEventRequest that occurred while offline
- const transactionEventRequest = buildTransactionEvent(
- chargingStation,
- eventType,
- triggerReason,
+ const transactionEventRequest = buildTransactionEvent(chargingStation, {
connectorId,
+ eventType,
transactionId,
- { ...options, offline: true }
- )
+ triggerReason,
+ ...options,
+ offline: true,
+ } as unknown as OCPP20TransactionEventRequest)
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.sendTransactionEvent: Station offline, queueing TransactionEvent with seqNo=${transactionEventRequest.seqNo.toString()}`
)
}
const meterValue = buildMeterValue(
chargingStation,
- connectorId,
connectorStatus.transactionId,
interval
) as OCPP20MeterValue
OCPP20ServiceUtils.stopPeriodicMeterValues(chargingStation, connectorId)
resetConnectorStatus(connectorStatus)
- await sendAndSetConnectorStatus(chargingStation, connectorId, ConnectorStatusEnum.Available)
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId,
+ connectorStatus: ConnectorStatusEnum.Available,
+ } as unknown as OCPP20StatusNotificationRequest)
return response
}
}
-export function buildTransactionEvent (
- chargingStation: ChargingStation,
- eventType: OCPP20TransactionEventEnumType,
- triggerReason: OCPP20TriggerReasonEnumType,
- connectorId: number,
- transactionId: string,
- options?: OCPP20TransactionEventOptions
-): OCPP20TransactionEventRequest
-export function buildTransactionEvent (
- chargingStation: ChargingStation,
- commandParams: JsonObject
-): OCPP20TransactionEventRequest
+
/**
* @param chargingStation - Charging station instance
- * @param eventTypeOrParams - Event type enum or minimal params object
- * @param triggerReasonArg - Trigger reason (explicit overload)
- * @param connectorIdArg - Connector identifier (explicit overload)
- * @param transactionIdArg - Transaction UUID (explicit overload)
- * @param options - Optional transaction event fields
+ * @param commandParams - Transaction event request parameters
* @returns Built TransactionEventRequest
*/
export function buildTransactionEvent (
chargingStation: ChargingStation,
- eventTypeOrParams: JsonObject | OCPP20TransactionEventEnumType,
- triggerReasonArg?: OCPP20TriggerReasonEnumType,
- connectorIdArg?: number,
- transactionIdArg?: string,
- options: OCPP20TransactionEventOptions = {}
+ commandParams: OCPP20TransactionEventRequest
): OCPP20TransactionEventRequest {
- let eventType: OCPP20TransactionEventEnumType
- let triggerReason: OCPP20TriggerReasonEnumType
- let connectorId: number
- let transactionId: string
-
- if (typeof eventTypeOrParams === 'object') {
- const params = eventTypeOrParams
- eventType = params.eventType as OCPP20TransactionEventEnumType
- triggerReason =
- params.triggerReason != null
- ? (params.triggerReason as OCPP20TriggerReasonEnumType)
- : eventType === OCPP20TransactionEventEnumType.Ended
- ? OCPP20TriggerReasonEnumType.RemoteStop
- : OCPP20TriggerReasonEnumType.Authorized
- const evse = params.evse as undefined | { connectorId?: number; id?: number }
- connectorId =
- params.connectorId != null
- ? (params.connectorId as number)
- : (evse?.connectorId ?? evse?.id ?? 1)
- transactionId =
- params.transactionId != null
- ? (params.transactionId as string)
- : eventType === OCPP20TransactionEventEnumType.Ended
- ? (chargingStation.getConnectorStatus(connectorId)?.transactionId?.toString() ??
- generateUUID())
- : generateUUID()
- options = params as unknown as OCPP20TransactionEventOptions
- } else {
- eventType = eventTypeOrParams
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- triggerReason = triggerReasonArg!
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorId = connectorIdArg!
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- transactionId = transactionIdArg!
- }
+ const params = commandParams as Record<string, unknown>
+ const eventType = params.eventType as OCPP20TransactionEventEnumType
+ const triggerReason =
+ params.triggerReason != null
+ ? (params.triggerReason as OCPP20TriggerReasonEnumType)
+ : eventType === OCPP20TransactionEventEnumType.Ended
+ ? OCPP20TriggerReasonEnumType.RemoteStop
+ : OCPP20TriggerReasonEnumType.Authorized
+ const inputEvse = params.evse as undefined | { connectorId?: number; id?: number }
+ const connectorId =
+ params.connectorId != null
+ ? (params.connectorId as number)
+ : (inputEvse?.connectorId ?? inputEvse?.id ?? 1)
+ const transactionId =
+ params.transactionId != null
+ ? (params.transactionId as string)
+ : eventType === OCPP20TransactionEventEnumType.Ended
+ ? (chargingStation.getConnectorStatus(connectorId)?.transactionId?.toString() ??
+ generateUUID())
+ : generateUUID()
+ const options = params as unknown as OCPP20TransactionEventOptions
if (!validateIdentifierString(transactionId, 36)) {
const errorMsg = `Invalid transaction ID format (must be non-empty string ≤36 characters): ${transactionId}`
import {
AuthorizationStatus,
type AuthorizeRequest,
- type AuthorizeResponse,
ChargePointErrorCode,
ChargingStationEvents,
type ConnectorStatus,
MeterValueMeasurand,
MeterValuePhase,
MeterValueUnit,
- type OCPP16ChargePointStatus,
+ type OCPP16AuthorizeResponse,
type OCPP16MeterValue,
type OCPP16SampledValue,
type OCPP16StatusNotificationRequest,
OCPP16StopTransactionReason,
OCPP20AuthorizationStatusEnumType,
+ type OCPP20AuthorizeResponse,
type OCPP20ConnectorStatusEnumType,
OCPP20IdTokenEnumType,
type OCPP20MeterValue,
OCPP20ReasonEnumType,
type OCPP20SampledValue,
+ type OCPP20StatusNotificationRequest,
OCPP20TransactionEventEnumType,
OCPP20TriggerReasonEnumType,
OCPPVersion,
export const buildStatusNotificationRequest = (
chargingStation: ChargingStation,
- connectorId: number,
- status: ConnectorStatusEnum,
- evseId?: number
+ commandParams: StatusNotificationRequest
): StatusNotificationRequest => {
switch (chargingStation.stationInfo?.ocppVersion) {
- case OCPPVersion.VERSION_16:
+ case OCPPVersion.VERSION_16: {
+ const params = commandParams as OCPP16StatusNotificationRequest
return {
- connectorId,
+ connectorId: params.connectorId,
errorCode: ChargePointErrorCode.NO_ERROR,
- status: status as OCPP16ChargePointStatus,
+ status: params.status,
} satisfies OCPP16StatusNotificationRequest
+ }
case OCPPVersion.VERSION_20:
case OCPPVersion.VERSION_201: {
+ const params = commandParams as Record<string, unknown>
+ const connectorId = params.connectorId as number
+ const connectorStatus = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
+ const evseId = params.evseId as number | undefined
const resolvedEvseId = evseId ?? chargingStation.getEvseIdByConnectorId(connectorId)
if (resolvedEvseId === undefined) {
throw new OCPPError(
}
return {
connectorId,
- connectorStatus: status as OCPP20ConnectorStatusEnumType,
+ connectorStatus: connectorStatus as OCPP20ConnectorStatusEnumType,
evseId: resolvedEvseId,
timestamp: new Date(),
- } satisfies StatusNotificationRequest
+ } satisfies OCPP20StatusNotificationRequest
}
default:
throw new OCPPError(
connectorId: number,
idTag: string
): Promise<boolean> => {
- const stationOcppVersion = chargingStation.stationInfo?.ocppVersion
- // OCPP 2.0+ always uses unified auth system
- // OCPP 1.6 can optionally use unified or legacy system
- const shouldUseUnified =
- stationOcppVersion === OCPPVersion.VERSION_20 || stationOcppVersion === OCPPVersion.VERSION_201
-
- if (shouldUseUnified) {
- try {
- logger.debug(
- `${chargingStation.logPrefix()} Using unified auth system for idTag '${idTag}' on connector ${connectorId.toString()}`
- )
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ try {
+ logger.debug(
+ `${chargingStation.logPrefix()} Using unified auth system for idTag '${idTag}' on connector ${connectorId.toString()}`
+ )
- // Dynamic import to avoid circular dependencies
- const { OCPPAuthServiceFactory } = await import('./auth/services/OCPPAuthServiceFactory.js')
- const {
- AuthContext,
- AuthorizationStatus: UnifiedAuthorizationStatus,
- IdentifierType,
- } = await import('./auth/types/AuthTypes.js')
+ // Dynamic import to avoid circular dependencies
+ const { OCPPAuthServiceFactory } = await import('./auth/services/OCPPAuthServiceFactory.js')
+ const {
+ AuthContext,
+ AuthorizationStatus: UnifiedAuthorizationStatus,
+ IdentifierType,
+ } = await import('./auth/types/AuthTypes.js')
- // Get unified auth service
- const authService = await OCPPAuthServiceFactory.getInstance(chargingStation)
+ // Get unified auth service
+ const authService = await OCPPAuthServiceFactory.getInstance(chargingStation)
- // Create auth request with unified types
- const authResult = await authService.authorize({
- allowOffline: false,
- connectorId,
- context: AuthContext.TRANSACTION_START,
- identifier: {
- type: IdentifierType.ID_TAG,
- value: idTag,
- },
- timestamp: new Date(),
- })
+ // Create auth request with unified types
+ const authResult = await authService.authorize({
+ allowOffline: false,
+ connectorId,
+ context: AuthContext.TRANSACTION_START,
+ identifier: {
+ type: IdentifierType.ID_TAG,
+ value: idTag,
+ },
+ timestamp: new Date(),
+ })
+
+ logger.debug(
+ `${chargingStation.logPrefix()} Unified auth result for idTag '${idTag}': ${authResult.status} using ${authResult.method} method`
+ )
+ return authResult.status === UnifiedAuthorizationStatus.ACCEPTED
+ } catch (error) {
+ logger.error(`${chargingStation.logPrefix()} Unified auth failed for OCPP 2.0`, error)
+ return false
+ }
+ case OCPPVersion.VERSION_16:
logger.debug(
- `${chargingStation.logPrefix()} Unified auth result for idTag '${idTag}': ${authResult.status} using ${authResult.method} method`
- )
-
- // Use AuthorizationStatus enum from unified system
- return authResult.status === UnifiedAuthorizationStatus.ACCEPTED
- } catch (error) {
- logger.error(
- `${chargingStation.logPrefix()} Unified auth failed, falling back to legacy system`,
- error
+ `${chargingStation.logPrefix()} Using legacy auth system for idTag '${idTag}' on connector ${connectorId.toString()}`
)
- // Fall back to legacy system on error (only for OCPP 1.6)
- if (chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_16) {
- return isIdTagAuthorized(chargingStation, connectorId, idTag)
- }
- // For OCPP 2.0, return false on error (no legacy fallback)
+ return isIdTagAuthorized(chargingStation, connectorId, idTag)
+ default:
return false
- }
}
-
- // Use legacy auth system for OCPP 1.6 when unified auth not explicitly enabled
- logger.debug(
- `${chargingStation.logPrefix()} Using legacy auth system for idTag '${idTag}' on connector ${connectorId.toString()}`
- )
- return isIdTagAuthorized(chargingStation, connectorId, idTag)
}
/**
connectorId: number,
idTag: string
): Promise<boolean> => {
- // OCPP 2.0+ always delegates to unified system
- if (
- chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_20 ||
- chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_201
- ) {
- return isIdTagAuthorizedUnified(chargingStation, connectorId, idTag)
- }
-
- // Legacy authorization logic for OCPP 1.6
- if (
- !chargingStation.getLocalAuthListEnabled() &&
- chargingStation.stationInfo?.remoteAuthorization === false
- ) {
- logger.warn(
- `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`
- )
- }
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)
- if (
- connectorStatus != null &&
- chargingStation.getLocalAuthListEnabled() &&
- isIdTagLocalAuthorized(chargingStation, idTag)
- ) {
- connectorStatus.localAuthorizeIdTag = idTag
- connectorStatus.idTagLocalAuthorized = true
- return true
- } else if (chargingStation.stationInfo?.remoteAuthorization === true) {
- return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag)
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16: {
+ if (
+ !chargingStation.getLocalAuthListEnabled() &&
+ chargingStation.stationInfo.remoteAuthorization === false
+ ) {
+ logger.warn(
+ `${chargingStation.logPrefix()} The charging station expects to authorize RFID tags but nor local authorization nor remote authorization are enabled. Misbehavior may occur`
+ )
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (
+ connectorStatus != null &&
+ chargingStation.getLocalAuthListEnabled() &&
+ isIdTagLocalAuthorized(chargingStation, idTag)
+ ) {
+ connectorStatus.localAuthorizeIdTag = idTag
+ connectorStatus.idTagLocalAuthorized = true
+ return true
+ } else if (chargingStation.stationInfo.remoteAuthorization === true) {
+ return await isIdTagRemoteAuthorized(chargingStation, connectorId, idTag)
+ }
+ return false
+ }
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ return isIdTagAuthorizedUnified(chargingStation, connectorId, idTag)
+ default:
+ return false
}
- return false
}
const isIdTagLocalAuthorized = (chargingStation: ChargingStation, idTag: string): boolean => {
): Promise<boolean> => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
chargingStation.getConnectorStatus(connectorId)!.authorizeIdTag = idTag
- return (
- (
- await chargingStation.ocppRequestService.requestHandler<AuthorizeRequest, AuthorizeResponse>(
- chargingStation,
- RequestCommand.AUTHORIZE,
- {
- idTag,
- }
+ switch (chargingStation.stationInfo?.ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ return (
+ (
+ await chargingStation.ocppRequestService.requestHandler<
+ AuthorizeRequest,
+ OCPP16AuthorizeResponse
+ >(chargingStation, RequestCommand.AUTHORIZE, {
+ idTag,
+ })
+ ).idTagInfo.status === AuthorizationStatus.ACCEPTED
)
- ).idTagInfo.status === AuthorizationStatus.ACCEPTED
- )
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ return (
+ (
+ await chargingStation.ocppRequestService.requestHandler<
+ AuthorizeRequest,
+ OCPP20AuthorizeResponse
+ >(chargingStation, RequestCommand.AUTHORIZE, {
+ idToken: { idToken: idTag, type: OCPP20IdTokenEnumType.ISO14443 },
+ })
+ ).idTokenInfo.status === AuthorizationStatus.Accepted
+ )
+ default:
+ return false
+ }
}
export const sendAndSetConnectorStatus = async (
chargingStation: ChargingStation,
- connectorId: number,
- status: ConnectorStatusEnum,
- evseId?: number,
+ commandParams: StatusNotificationRequest,
options?: { send: boolean }
): Promise<void> => {
options = { send: true, ...options }
+ const params = commandParams as Record<string, unknown>
+ const connectorId = params.connectorId as number
+ const status = (params.connectorStatus ?? params.status) as ConnectorStatusEnum
const connectorStatus = chargingStation.getConnectorStatus(connectorId)
if (connectorStatus == null) {
return
await chargingStation.ocppRequestService.requestHandler<
StatusNotificationRequest,
StatusNotificationResponse
- >(chargingStation, RequestCommand.STATUS_NOTIFICATION, {
- connectorId,
- evseId,
- status,
- } as unknown as StatusNotificationRequest)
+ >(chargingStation, RequestCommand.STATUS_NOTIFICATION, commandParams)
}
connectorStatus.status = status
chargingStation.emitChargingStationEvent(ChargingStationEvents.connectorStatusChanged, {
connectorStatus?.reservation != null &&
connectorStatus.status !== ConnectorStatusEnum.Reserved
) {
- await sendAndSetConnectorStatus(chargingStation, connectorId, ConnectorStatusEnum.Reserved)
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId,
+ status: ConnectorStatusEnum.Reserved,
+ } as unknown as StatusNotificationRequest)
} else if (connectorStatus?.status !== ConnectorStatusEnum.Available) {
- await sendAndSetConnectorStatus(chargingStation, connectorId, ConnectorStatusEnum.Available)
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId,
+ status: ConnectorStatusEnum.Available,
+ } as unknown as StatusNotificationRequest)
}
}
const buildSocMeasurandValue = (
chargingStation: ChargingStation,
- connectorId: number
+ connectorId: number,
+ evseId?: number
): null | SingleValueMeasurandData => {
const socSampledValueTemplate = getSampledValueTemplate(
chargingStation,
connectorId,
- MeterValueMeasurand.STATE_OF_CHARGE
+ MeterValueMeasurand.STATE_OF_CHARGE,
+ evseId
)
if (socSampledValueTemplate == null) {
return null
const buildVoltageMeasurandValue = (
chargingStation: ChargingStation,
- connectorId: number
+ connectorId: number,
+ evseId?: number
): null | SingleValueMeasurandData => {
const voltageSampledValueTemplate = getSampledValueTemplate(
chargingStation,
connectorId,
- MeterValueMeasurand.VOLTAGE
+ MeterValueMeasurand.VOLTAGE,
+ evseId
)
if (voltageSampledValueTemplate == null) {
return null
chargingStation,
connectorId,
MeterValueMeasurand.VOLTAGE,
+ undefined,
phaseLineToNeutralValue
)
let voltagePhaseLineToNeutralMeasurandValue: number | undefined
chargingStation,
connectorId,
MeterValueMeasurand.VOLTAGE,
+ undefined,
phaseLineToLineValue
)
let voltagePhaseLineToLineMeasurandValue: number | undefined
const buildEnergyMeasurandValue = (
chargingStation: ChargingStation,
connectorId: number,
- interval: number
+ interval: number,
+ evseId?: number
): null | SingleValueMeasurandData => {
- const energyTemplate = getSampledValueTemplate(chargingStation, connectorId)
+ const energyTemplate = getSampledValueTemplate(chargingStation, connectorId, undefined, evseId)
if (energyTemplate == null) {
return null
}
const buildPowerMeasurandValue = (
chargingStation: ChargingStation,
- connectorId: number
+ connectorId: number,
+ evseId?: number
): MultiPhaseMeasurandData | null => {
const powerTemplate = getSampledValueTemplate(
chargingStation,
connectorId,
- MeterValueMeasurand.POWER_ACTIVE_IMPORT
+ MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+ evseId
)
if (powerTemplate == null) {
return null
chargingStation,
connectorId,
MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+ evseId,
MeterValuePhase.L1_N
),
L2: getSampledValueTemplate(
chargingStation,
connectorId,
MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+ evseId,
MeterValuePhase.L2_N
),
L3: getSampledValueTemplate(
chargingStation,
connectorId,
MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+ evseId,
MeterValuePhase.L3_N
),
}
const buildCurrentMeasurandValue = (
chargingStation: ChargingStation,
- connectorId: number
+ connectorId: number,
+ evseId?: number
): MultiPhaseMeasurandData | null => {
const currentTemplate = getSampledValueTemplate(
chargingStation,
connectorId,
- MeterValueMeasurand.CURRENT_IMPORT
+ MeterValueMeasurand.CURRENT_IMPORT,
+ evseId
)
if (currentTemplate == null) {
return null
chargingStation,
connectorId,
MeterValueMeasurand.CURRENT_IMPORT,
+ evseId,
MeterValuePhase.L1
),
L2: getSampledValueTemplate(
chargingStation,
connectorId,
MeterValueMeasurand.CURRENT_IMPORT,
+ evseId,
MeterValuePhase.L2
),
L3: getSampledValueTemplate(
chargingStation,
connectorId,
MeterValueMeasurand.CURRENT_IMPORT,
+ evseId,
MeterValuePhase.L3
),
}
export const buildMeterValue = (
chargingStation: ChargingStation,
- connectorId: number,
transactionId: number | string | undefined,
interval: number,
debug = false
): MeterValue => {
- const connector = chargingStation.getConnectorStatus(connectorId)
-
+ if (transactionId == null) {
+ return { sampledValue: [], timestamp: new Date() }
+ }
switch (chargingStation.stationInfo?.ocppVersion) {
case OCPPVersion.VERSION_16: {
+ const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)
+ if (connectorId == null) {
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ `Cannot build MeterValues: no connector found for transaction ${String(transactionId)}`,
+ RequestCommand.METER_VALUES
+ )
+ }
+ const connector = chargingStation.getConnectorStatus(connectorId)
const meterValue: OCPP16MeterValue = {
sampledValue: [],
timestamp: new Date(),
}
case OCPPVersion.VERSION_20:
case OCPPVersion.VERSION_201: {
+ const connectorId = chargingStation.getConnectorIdByTransactionId(transactionId)
+ const evseId = chargingStation.getEvseIdByTransactionId(transactionId)
+ if (connectorId == null || evseId == null) {
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ `Cannot build MeterValues: no connector/EVSE found for transaction ${String(transactionId)}`,
+ RequestCommand.METER_VALUES
+ )
+ }
+ const connector = chargingStation.getConnectorStatus(connectorId)
const meterValue: OCPP20MeterValue = {
sampledValue: [],
timestamp: new Date(),
return buildSampledValueForOCPP20(sampledValueTemplate, value, context, phase)
}
// SoC measurand
- const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId)
+ const socMeasurand = buildSocMeasurandValue(chargingStation, connectorId, evseId)
if (socMeasurand != null) {
const socSampledValue = buildVersionedSampledValue(
socMeasurand.template,
)
}
// Voltage measurand
- const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId)
+ const voltageMeasurand = buildVoltageMeasurandValue(chargingStation, connectorId, evseId)
if (voltageMeasurand != null) {
addMainVoltageToMeterValue(
chargingStation,
}
}
// Energy.Active.Import.Register measurand
- const energyMeasurand = buildEnergyMeasurandValue(chargingStation, connectorId, interval)
+ const energyMeasurand = buildEnergyMeasurandValue(
+ chargingStation,
+ connectorId,
+ interval,
+ evseId
+ )
if (energyMeasurand != null) {
updateConnectorEnergyValues(connector, energyMeasurand.value)
const unitDivider =
)
}
// Power.Active.Import measurand
- const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId)
+ const powerMeasurand = buildPowerMeasurandValue(chargingStation, connectorId, evseId)
if (powerMeasurand?.values.allPhases != null) {
const powerSampledValue = buildVersionedSampledValue(
powerMeasurand.template,
meterValue.sampledValue.push(powerSampledValue)
}
// Current.Import measurand
- const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId)
+ const currentMeasurand = buildCurrentMeasurandValue(chargingStation, connectorId, evseId)
if (currentMeasurand?.values.allPhases != null) {
const currentSampledValue = buildVersionedSampledValue(
currentMeasurand.template,
chargingStation: ChargingStation,
connectorId: number,
measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ evseId?: number,
phase?: MeterValuePhase
): SampledValueTemplate | undefined => {
const onPhaseStr = phase != null ? `on phase ${phase} ` : ''
)
return
}
- const sampledValueTemplates =
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.getConnectorStatus(connectorId)!.MeterValues
+ let sampledValueTemplates: SampledValueTemplate[] | undefined
+ if (evseId != null) {
+ const evseStatus = chargingStation.getEvseStatus(evseId)
+ if (evseStatus != null) {
+ if (isNotEmptyArray(evseStatus.MeterValues)) {
+ sampledValueTemplates = evseStatus.MeterValues
+ } else {
+ const connectorTemplates: SampledValueTemplate[] = []
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ if (isNotEmptyArray(connectorStatus.MeterValues)) {
+ connectorTemplates.push(...connectorStatus.MeterValues)
+ }
+ }
+ sampledValueTemplates = isNotEmptyArray(connectorTemplates) ? connectorTemplates : undefined
+ }
+ }
+ } else {
+ sampledValueTemplates = chargingStation.getConnectorStatus(connectorId)?.MeterValues
+ }
for (
let index = 0;
isNotEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
DEFAULT_RATE_LIMIT,
DEFAULT_RATE_WINDOW,
} from './UIServerSecurity.js'
-import { isProtocolAndVersionSupported } from './UIServerUtils.js'
+import { HttpMethod, isProtocolAndVersionSupported } from './UIServerUtils.js'
const moduleName = 'UIHttpServer'
const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW)
-enum HttpMethods {
- GET = 'GET',
- PATCH = 'PATCH',
- POST = 'POST',
- PUT = 'PUT',
-}
-
+/**
+ * @deprecated Use UIMCPServer (ApplicationProtocol.MCP) instead. Will be removed in a future major version.
+ */
export class UIHttpServer extends AbstractUIServer {
protected override readonly uiServerType = 'UI HTTP Server'
}
}
+ /**
+ * @deprecated Use UIMCPServer (ApplicationProtocol.MCP) instead. Will be removed in a future major version.
+ */
public start (): void {
this.httpServer.on('request', this.requestListener.bind(this))
this.startHttpServer()
}
})
- if (req.method !== HttpMethods.POST) {
+ if (req.method !== HttpMethod.POST) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new BaseError(`Unsupported HTTP method: '${req.method}'`)
}
--- /dev/null
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
+import type { IncomingMessage, ServerResponse } from 'node:http'
+
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
+import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
+import { readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+import type { AbstractUIService } from './ui-services/AbstractUIService.js'
+
+import { BaseError } from '../../exception/index.js'
+import {
+ OCPPVersion,
+ type ProcedureName,
+ type ProtocolRequest,
+ type ProtocolResponse,
+ ProtocolVersion,
+ type RequestPayload,
+ type ResponsePayload,
+ type UIServerConfiguration,
+ type UUIDv4,
+} from '../../types/index.js'
+import { generateUUID, logger } from '../../utils/index.js'
+import { AbstractUIServer } from './AbstractUIServer.js'
+import {
+ mcpToolSchemas,
+ ocppSchemaMapping,
+ registerMCPLogTools,
+ registerMCPResources,
+ registerMCPSchemaResources,
+} from './mcp/index.js'
+import {
+ createRateLimiter,
+ DEFAULT_MAX_PAYLOAD_SIZE,
+ DEFAULT_RATE_LIMIT,
+ DEFAULT_RATE_WINDOW,
+} from './UIServerSecurity.js'
+import { HttpMethod } from './UIServerUtils.js'
+
+const moduleName = 'UIMCPServer'
+
+const MCP_TOOL_TIMEOUT_MS = 30_000
+
+const rateLimiter = createRateLimiter(DEFAULT_RATE_LIMIT, DEFAULT_RATE_WINDOW)
+
+export class UIMCPServer extends AbstractUIServer {
+ protected override readonly uiServerType = 'UI MCP Server'
+
+ private ocppSchemaCache: Map<string, { ocpp16?: unknown; ocpp20?: unknown }>
+
+ private readonly pendingMcpRequests: Map<
+ UUIDv4,
+ {
+ reject: (error: Error) => void
+ resolve: (payload: ResponsePayload) => void
+ timeout: ReturnType<typeof setTimeout>
+ }
+ >
+
+ private service: AbstractUIService | undefined
+
+ public constructor (protected override readonly uiServerConfiguration: UIServerConfiguration) {
+ super(uiServerConfiguration)
+ this.pendingMcpRequests = new Map()
+ this.ocppSchemaCache = new Map()
+ }
+
+ private static createToolErrorResponse (error: string): CallToolResult {
+ return {
+ content: [{ text: JSON.stringify({ error, status: 'failure' }), type: 'text' as const }],
+ isError: true,
+ }
+ }
+
+ private static createToolResponse (payload: unknown): CallToolResult {
+ return { content: [{ text: JSON.stringify(payload), type: 'text' as const }] }
+ }
+
+ public override hasResponseHandler (uuid: UUIDv4): boolean {
+ return super.hasResponseHandler(uuid) || this.pendingMcpRequests.has(uuid)
+ }
+
+ public sendRequest (_request: ProtocolRequest): void {
+ logger.warn(
+ `${this.logPrefix(moduleName, 'sendRequest')} Server-initiated requests not supported in stateless MCP mode`
+ )
+ }
+
+ public sendResponse (response: ProtocolResponse): void {
+ const [uuid, payload] = response
+ const pending = this.pendingMcpRequests.get(uuid)
+ if (pending != null) {
+ clearTimeout(pending.timeout)
+ this.pendingMcpRequests.delete(uuid)
+ pending.resolve(payload)
+ } else {
+ logger.error(
+ `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}`
+ )
+ }
+ }
+
+ public start (): void {
+ const version = ProtocolVersion['0.0.1']
+ this.registerProtocolVersionUIService(version)
+ this.service = this.uiServices.get(version)
+ this.ocppSchemaCache = this.loadOcppSchemas()
+
+ this.httpServer.on('request', (req: IncomingMessage, res: ServerResponse) => {
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
+ if (url.pathname !== '/mcp') {
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('404 Not Found')
+ if (!req.complete) {
+ req.destroy()
+ }
+ return
+ }
+
+ const clientIp = req.socket.remoteAddress ?? 'unknown'
+ if (!rateLimiter(clientIp)) {
+ res.writeHead(429, { 'Content-Type': 'text/plain' }).end('429 Too Many Requests')
+ return
+ }
+
+ let authError: Error | undefined
+ // authenticate() is synchronous — authError is set before the if-check
+ this.authenticate(req, err => {
+ authError = err
+ })
+ if (authError != null) {
+ res
+ .writeHead(401, {
+ 'Content-Type': 'text/plain',
+ 'WWW-Authenticate': 'Basic realm=users',
+ })
+ .end('401 Unauthorized')
+ return
+ }
+
+ this.handleMcpRequest(req, res).catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'start.httpServer.request')} Unhandled MCP request error:`,
+ error
+ )
+ })
+ })
+
+ this.startHttpServer()
+ }
+
+ public override stop (): void {
+ for (const [uuid, pending] of [...this.pendingMcpRequests]) {
+ clearTimeout(pending.timeout)
+ this.pendingMcpRequests.delete(uuid)
+ pending.reject(new BaseError('Server stopping'))
+ }
+ super.stop()
+ }
+
+ protected getSchemaBaseDir (): string {
+ return join(dirname(fileURLToPath(import.meta.url)), 'assets', 'json-schemas', 'ocpp')
+ }
+
+ private checkVersionCompatibility (
+ hashIds: string[] | undefined,
+ ocpp16Payload: Record<string, unknown> | undefined,
+ ocpp20Payload: Record<string, unknown> | undefined,
+ procedureName: ProcedureName
+ ): CallToolResult | undefined {
+ if (ocpp16Payload == null && ocpp20Payload == null) {
+ return undefined
+ }
+ const expectedVersion = ocpp16Payload != null ? OCPPVersion.VERSION_16 : OCPPVersion.VERSION_20
+ const payloadLabel = ocpp16Payload != null ? 'ocpp16Payload' : 'ocpp20Payload'
+ const alternativeLabel = ocpp16Payload != null ? 'ocpp20Payload' : 'ocpp16Payload'
+ const stationsToCheck =
+ hashIds != null
+ ? hashIds
+ .map(id => {
+ const data = this.getChargingStationData(id)
+ return data != null
+ ? { hashId: id, version: data.stationInfo.ocppVersion }
+ : undefined
+ })
+ .filter(s => s != null)
+ : this.listChargingStationData().map(data => ({
+ hashId: data.stationInfo.hashId,
+ version: data.stationInfo.ocppVersion,
+ }))
+ const mismatched = stationsToCheck.filter(s => {
+ if (expectedVersion === OCPPVersion.VERSION_16) {
+ return s.version !== OCPPVersion.VERSION_16
+ }
+ return s.version !== OCPPVersion.VERSION_20 && s.version !== OCPPVersion.VERSION_201
+ })
+ if (mismatched.length > 0) {
+ const ids = mismatched.map(s => s.hashId).join(', ')
+ const versions = [...new Set(mismatched.map(s => s.version ?? 'unknown'))].join(', ')
+ return UIMCPServer.createToolErrorResponse(
+ `Station(s) ${ids} run OCPP ${versions} but received ${payloadLabel} for '${procedureName}'. ` +
+ `Use ${alternativeLabel} instead, or target only compatible stations via hashIds.`
+ )
+ }
+ return undefined
+ }
+
+ private closeTransportSafely (transport: StreamableHTTPServerTransport): void {
+ transport.close().catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'handleMcpRequest')} MCP transport close error:`,
+ error
+ )
+ })
+ }
+
+ // Per the MCP SDK design, McpServer.connect() overwrites a single internal _transport field.
+ // A new McpServer must be created per request to avoid transport cross-talk under concurrency.
+ // Tool registration is ~12µs for 33 tools (Map.set + closure allocation) — negligible.
+ private createMcpServer (): McpServer {
+ const mcpServer = new McpServer({
+ name: 'e-mobility-charging-stations-simulator',
+ version: ProtocolVersion['0.0.1'],
+ })
+
+ for (const [procedureName, schema] of mcpToolSchemas) {
+ mcpServer.registerTool(
+ procedureName,
+ {
+ description: schema.description,
+ inputSchema: schema.inputSchema.shape,
+ },
+ async (input: Record<string, unknown>) => {
+ return await this.invokeProcedure(procedureName, input as RequestPayload, this.service)
+ }
+ )
+ }
+
+ registerMCPResources(mcpServer, this)
+ registerMCPSchemaResources(mcpServer)
+ registerMCPLogTools(mcpServer)
+ this.injectOcppJsonSchemas(mcpServer)
+
+ return mcpServer
+ }
+
+ private async handleMcpRequest (req: IncomingMessage, res: ServerResponse): Promise<void> {
+ const mcpServer = this.createMcpServer()
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })
+ try {
+ await mcpServer.connect(transport)
+ } catch (error: unknown) {
+ logger.error(`${this.logPrefix(moduleName, 'handleMcpRequest')} MCP connect error:`, error)
+ this.closeTransportSafely(transport)
+ this.sendErrorResponse(res, 500)
+ return
+ }
+
+ const cleanup = (): void => {
+ this.closeTransportSafely(transport)
+ mcpServer.close().catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix(moduleName, 'handleMcpRequest')} MCP server close error:`,
+ error
+ )
+ })
+ }
+
+ try {
+ if (req.method === HttpMethod.POST) {
+ const body = await this.readRequestBody(req)
+ res.on('close', cleanup)
+ await transport.handleRequest(req, res, body)
+ } else if (req.method === HttpMethod.GET || req.method === HttpMethod.DELETE) {
+ res.on('close', cleanup)
+ await transport.handleRequest(req, res)
+ } else {
+ this.sendErrorResponse(res, 405)
+ cleanup()
+ }
+ } catch (error: unknown) {
+ logger.error(`${this.logPrefix(moduleName, 'handleMcpRequest')} MCP transport error:`, error)
+ const isBadRequest =
+ error instanceof SyntaxError ||
+ (error instanceof Error && error.message.includes('Payload too large'))
+ this.sendErrorResponse(res, isBadRequest ? 400 : 500)
+ }
+ }
+
+ private injectOcppJsonSchemas (mcpServer: McpServer): void {
+ if (this.ocppSchemaCache.size === 0) {
+ return
+ }
+ // Access MCP SDK internal handler map — pinned to @modelcontextprotocol/sdk@~1.27.x
+ // The SDK does not provide a public API for wrapping existing handlers.
+ // setRequestHandler() replaces handlers entirely, losing Zod→JSON Schema conversion.
+ const handlers = Reflect.get(mcpServer.server, '_requestHandlers') as
+ | Map<string, (...args: unknown[]) => Promise<unknown>>
+ | undefined
+ if (handlers == null || !(handlers instanceof Map)) {
+ logger.warn(
+ `${this.logPrefix(moduleName, 'injectOcppJsonSchemas')} MCP SDK internal API changed — OCPP schema injection disabled`
+ )
+ return
+ }
+ const originalHandler = handlers.get('tools/list')
+ if (originalHandler == null) {
+ return
+ }
+ handlers.set('tools/list', async (...args: unknown[]) => {
+ const result = (await originalHandler(...args)) as {
+ tools: { inputSchema: { properties: Record<string, unknown> }; name: string }[]
+ }
+ for (const tool of result.tools) {
+ const schemas = this.ocppSchemaCache.get(tool.name)
+ if (schemas == null) {
+ continue
+ }
+ if (schemas.ocpp16 != null && tool.inputSchema.properties.ocpp16Payload != null) {
+ tool.inputSchema.properties.ocpp16Payload = {
+ ...schemas.ocpp16,
+ description: `OCPP 1.6 ${tool.name} request payload`,
+ }
+ }
+ if (schemas.ocpp20 != null && tool.inputSchema.properties.ocpp20Payload != null) {
+ tool.inputSchema.properties.ocpp20Payload = {
+ ...schemas.ocpp20,
+ description: `OCPP 2.0.1 ${tool.name} request payload`,
+ }
+ }
+ }
+ return result
+ })
+ }
+
+ private async invokeProcedure (
+ procedureName: ProcedureName,
+ input: RequestPayload,
+ service: AbstractUIService | undefined
+ ): Promise<CallToolResult> {
+ if (service == null) {
+ return UIMCPServer.createToolErrorResponse('UI service not available')
+ }
+
+ const { ocpp16Payload, ocpp20Payload, ...rest } = input as RequestPayload & {
+ ocpp16Payload?: Record<string, unknown>
+ ocpp20Payload?: Record<string, unknown>
+ }
+
+ if (ocpp16Payload != null && ocpp20Payload != null) {
+ return UIMCPServer.createToolErrorResponse(
+ 'Cannot provide both ocpp16Payload and ocpp20Payload. Use ocpp16Payload for OCPP 1.6 stations or ocpp20Payload for OCPP 2.0 stations.'
+ )
+ }
+
+ const versionMismatchError = this.checkVersionCompatibility(
+ rest.hashIds,
+ ocpp16Payload,
+ ocpp20Payload,
+ procedureName
+ )
+ if (versionMismatchError != null) {
+ return versionMismatchError
+ }
+
+ const flatPayload = {
+ ...rest,
+ ...(ocpp16Payload ?? ocpp20Payload),
+ } as RequestPayload
+
+ const uuid = generateUUID()
+
+ return await new Promise<CallToolResult>(resolve => {
+ const timeout = setTimeout(() => {
+ this.pendingMcpRequests.delete(uuid)
+ resolve(UIMCPServer.createToolErrorResponse(`Tool '${procedureName}' timed out`))
+ }, MCP_TOOL_TIMEOUT_MS)
+
+ this.pendingMcpRequests.set(uuid, {
+ reject: (error: Error) => {
+ resolve(UIMCPServer.createToolErrorResponse(error.message))
+ },
+ resolve: (payload: ResponsePayload) => {
+ resolve(UIMCPServer.createToolResponse(payload))
+ },
+ timeout,
+ })
+
+ const request = this.buildProtocolRequest(uuid, procedureName, flatPayload)
+ service
+ .requestHandler(request)
+ .then(directResponse => {
+ if (directResponse != null) {
+ const pending = this.pendingMcpRequests.get(uuid)
+ if (pending != null) {
+ clearTimeout(pending.timeout)
+ this.pendingMcpRequests.delete(uuid)
+ const [, payload] = directResponse
+ resolve(UIMCPServer.createToolResponse(payload))
+ }
+ }
+ return undefined
+ })
+ .catch((error: unknown) => {
+ const pending = this.pendingMcpRequests.get(uuid)
+ if (pending != null) {
+ clearTimeout(pending.timeout)
+ this.pendingMcpRequests.delete(uuid)
+ }
+ resolve(
+ UIMCPServer.createToolErrorResponse(
+ error instanceof Error ? error.message : String(error)
+ )
+ )
+ })
+ })
+ }
+
+ private loadOcppSchemas (): Map<string, { ocpp16?: unknown; ocpp20?: unknown }> {
+ const cache = new Map<string, { ocpp16?: unknown; ocpp20?: unknown }>()
+ const baseDir = this.getSchemaBaseDir()
+ for (const [procedureName, mapping] of ocppSchemaMapping) {
+ const entry: { ocpp16?: unknown; ocpp20?: unknown } = {}
+ if (mapping.ocpp16 != null) {
+ try {
+ entry.ocpp16 = JSON.parse(
+ readFileSync(join(baseDir, OCPPVersion.VERSION_16, `${mapping.ocpp16}.json`), 'utf8')
+ )
+ } catch {
+ logger.warn(
+ `${this.logPrefix(moduleName, 'loadOcppSchemas')} Failed to load OCPP 1.6 schema for ${procedureName}`
+ )
+ }
+ }
+ if (mapping.ocpp20 != null) {
+ try {
+ entry.ocpp20 = JSON.parse(
+ readFileSync(join(baseDir, OCPPVersion.VERSION_20, `${mapping.ocpp20}.json`), 'utf8')
+ )
+ } catch {
+ logger.warn(
+ `${this.logPrefix(moduleName, 'loadOcppSchemas')} Failed to load OCPP 2.0 schema for ${procedureName}`
+ )
+ }
+ }
+ if (entry.ocpp16 != null || entry.ocpp20 != null) {
+ cache.set(procedureName, entry)
+ }
+ }
+ if (cache.size > 0) {
+ logger.info(
+ `${this.logPrefix(moduleName, 'loadOcppSchemas')} OCPP JSON schema injection enabled for ${cache.size.toString()} tool(s)`
+ )
+ }
+ return cache
+ }
+
+ private async readRequestBody (req: IncomingMessage): Promise<unknown> {
+ const chunks: Buffer[] = []
+ let received = 0
+ for await (const chunk of req) {
+ received += (chunk as Buffer).length
+ if (received > DEFAULT_MAX_PAYLOAD_SIZE) {
+ throw new BaseError('Payload too large')
+ }
+ chunks.push(chunk as Buffer)
+ }
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
+ }
+
+ private sendErrorResponse (res: ServerResponse, statusCode: number): void {
+ if (res.headersSent) return
+ const messages: Record<number, string> = {
+ 400: '400 Bad Request',
+ 405: '405 Method Not Allowed',
+ 500: '500 Internal Server Error',
+ }
+ res
+ .writeHead(statusCode, { 'Content-Type': 'text/plain' })
+ .end(messages[statusCode] ?? `${statusCode.toString()} Error`)
+ }
+}
} from '../../types/index.js'
import { logger, logPrefix } from '../../utils/index.js'
import { UIHttpServer } from './UIHttpServer.js'
+import { UIMCPServer } from './UIMCPServer.js'
import { isLoopback } from './UIServerUtils.js'
import { UIWebSocketServer } from './UIWebSocketServer.js'
logger.warn(`${UIServerFactory.logPrefix()} ${logMsg}`)
}
if (
- uiServerConfiguration.type === ApplicationProtocol.WS &&
+ (uiServerConfiguration.type === ApplicationProtocol.WS ||
+ uiServerConfiguration.type === ApplicationProtocol.MCP) &&
uiServerConfiguration.version !== ApplicationProtocolVersion.VERSION_11
) {
const logMsg = `Only version ${ApplicationProtocolVersion.VERSION_11} with application protocol type '${uiServerConfiguration.type}' is supported in '${ConfigurationSection.uiServer}' configuration section. Falling back to version ${ApplicationProtocolVersion.VERSION_11}`
logger.warn(`${UIServerFactory.logPrefix()} ${logMsg}`)
uiServerConfiguration.version = ApplicationProtocolVersion.VERSION_11
}
+ if (
+ uiServerConfiguration.type === ApplicationProtocol.MCP &&
+ uiServerConfiguration.authentication?.enabled === true &&
+ uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH
+ ) {
+ throw new BaseError(
+ `'${uiServerConfiguration.authentication.type}' authentication type with application protocol type '${uiServerConfiguration.type}' is not supported in '${ConfigurationSection.uiServer}' configuration section`
+ )
+ }
switch (uiServerConfiguration.type) {
- case ApplicationProtocol.HTTP:
+ case ApplicationProtocol.HTTP: {
+ const logMsg = `Application protocol type '${uiServerConfiguration.type}' is deprecated in '${ConfigurationSection.uiServer}' configuration section. Use '${ApplicationProtocol.MCP}' instead`
+ logger.warn(`${UIServerFactory.logPrefix()} ${logMsg}`)
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
return new UIHttpServer(uiServerConfiguration)
+ }
+ case ApplicationProtocol.MCP:
+ return new UIMCPServer(uiServerConfiguration)
case ApplicationProtocol.WS:
default:
if (
import { Protocol, ProtocolVersion } from '../../types/index.js'
import { getErrorMessage, isEmpty, logger, logPrefix } from '../../utils/index.js'
+export enum HttpMethod {
+ DELETE = 'DELETE',
+ GET = 'GET',
+ PATCH = 'PATCH',
+ POST = 'POST',
+ PUT = 'PUT',
+}
+
export const getUsernameAndPasswordFromAuthorizationToken = (
authorizationToken: string,
next: (err?: Error) => void
--- /dev/null
+import { type McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
+import { open, readdir, readFile, stat } from 'node:fs/promises'
+import { dirname, join, resolve } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { z } from 'zod'
+
+import type { AbstractUIServer } from '../AbstractUIServer.js'
+
+import { ConfigurationSection, type LogConfiguration, OCPPVersion } from '../../../types/index.js'
+import { Configuration } from '../../../utils/Configuration.js'
+
+const MAX_TAIL_LINES = 5000
+const DEFAULT_TAIL_LINES = 200
+const TAIL_BYTES = 65_536
+
+const getLogFilePath = (configField: 'errorFile' | 'file', date?: string): string | undefined => {
+ const logConfig = Configuration.getConfigurationSection<LogConfiguration>(
+ ConfigurationSection.log
+ )
+ const relativePath = logConfig[configField]
+ if (relativePath == null) {
+ return undefined
+ }
+ if (logConfig.rotate !== true) {
+ return resolve(relativePath)
+ }
+ const now = new Date()
+ const localDate =
+ date ??
+ `${now.getFullYear().toString()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
+ const dir = dirname(resolve(relativePath))
+ const baseName = configField === 'file' ? `combined-${localDate}.log` : `error-${localDate}.log`
+ return join(dir, baseName)
+}
+
+const tailFile = async (
+ filePath: string,
+ maxLines: number
+): Promise<{ lines: string[]; totalLines: number }> => {
+ const fileStat = await stat(filePath)
+ const fileHandle = await open(filePath, 'r')
+ try {
+ const fullContent = fileStat.size <= TAIL_BYTES
+ const readSize = Math.min(TAIL_BYTES, fileStat.size)
+ const position = Math.max(0, fileStat.size - readSize)
+ const buffer = Buffer.alloc(readSize)
+ await fileHandle.read(buffer, 0, readSize, position)
+ const allLines = buffer.toString('utf8').split('\n')
+ if (position > 0) {
+ allLines.shift()
+ }
+ const totalLines = fullContent ? allLines.length : -1
+ return { lines: allLines.slice(-maxLines), totalLines }
+ } finally {
+ await fileHandle.close()
+ }
+}
+
+export const registerMCPResources = (server: McpServer, uiServer: AbstractUIServer): void => {
+ server.registerResource(
+ 'station-list',
+ 'station://list',
+ {
+ description: 'List all charging stations with their current status and info',
+ mimeType: 'application/json',
+ },
+ _uri => ({
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify(uiServer.listChargingStationData(), null, 2),
+ uri: 'station://list',
+ },
+ ],
+ })
+ )
+
+ server.registerResource(
+ 'station-by-id',
+ new ResourceTemplate('station://{hashId}', { list: undefined }),
+ {
+ description: 'Get data for a specific charging station by its hash ID',
+ mimeType: 'application/json',
+ },
+ (uri, { hashId }) => {
+ const data = uiServer.getChargingStationData(hashId as string)
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text:
+ data != null
+ ? JSON.stringify(data, null, 2)
+ : JSON.stringify({ error: `Station '${hashId as string}' not found` }),
+ uri: uri.href,
+ },
+ ],
+ }
+ }
+ )
+
+ server.registerResource(
+ 'template-list',
+ 'template://list',
+ {
+ description: 'List all available charging station configuration templates',
+ mimeType: 'application/json',
+ },
+ _uri => ({
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify(uiServer.getChargingStationTemplates(), null, 2),
+ uri: 'template://list',
+ },
+ ],
+ })
+ )
+}
+
+const OCPP_SCHEMA_VERSIONS = [OCPPVersion.VERSION_16, OCPPVersion.VERSION_20] as const
+
+const getSchemaBaseDir = (): string => {
+ const currentDir = dirname(fileURLToPath(import.meta.url))
+ return join(currentDir, 'assets', 'json-schemas', 'ocpp')
+}
+
+// Path traversal guard: validate that the resolved path stays within the expected base directory.
+const isPathWithinBase = (candidatePath: string, baseDir: string): boolean => {
+ const resolvedBase = resolve(baseDir)
+ const resolvedCandidate = resolve(candidatePath)
+ return resolvedCandidate.startsWith(`${resolvedBase}/`) || resolvedCandidate === resolvedBase
+}
+
+export const registerMCPSchemaResources = (server: McpServer): void => {
+ for (const version of OCPP_SCHEMA_VERSIONS) {
+ server.registerResource(
+ `ocpp-${version}-schema-list`,
+ `schema://ocpp/${version}`,
+ {
+ description: `List all available OCPP ${version} JSON command schemas`,
+ mimeType: 'application/json',
+ },
+ async _uri => {
+ try {
+ const baseDir = getSchemaBaseDir()
+ const schemaDir = join(baseDir, version)
+ if (!isPathWithinBase(schemaDir, baseDir)) {
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify({ error: `Invalid OCPP version '${version}'` }),
+ uri: `schema://ocpp/${version}`,
+ },
+ ],
+ }
+ }
+ const files = await readdir(schemaDir)
+ const commands = files
+ .filter(f => f.endsWith('.json'))
+ .map(f => f.replace('.json', ''))
+ .sort((a, b) => a.localeCompare(b))
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify({ commands, count: commands.length, version }, null, 2),
+ uri: `schema://ocpp/${version}`,
+ },
+ ],
+ }
+ } catch {
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify({ error: `OCPP ${version} schemas not available` }),
+ uri: `schema://ocpp/${version}`,
+ },
+ ],
+ }
+ }
+ }
+ )
+ }
+
+ server.registerResource(
+ 'ocpp-schema-by-command',
+ new ResourceTemplate('schema://ocpp/{version}/{command}', { list: undefined }),
+ {
+ description:
+ 'Full OCPP JSON schema for a specific command (e.g., schema://ocpp/1.6/Authorize or schema://ocpp/2.0/AuthorizeRequest)',
+ mimeType: 'application/json',
+ },
+ async (uri, { command, version }) => {
+ try {
+ const versionStr = version as string
+ const commandStr = command as string
+ const baseDir = getSchemaBaseDir()
+ const schemaPath = join(baseDir, versionStr, `${commandStr}.json`)
+ if (!isPathWithinBase(schemaPath, baseDir)) {
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify({
+ error: `Invalid schema path for '${commandStr}' in OCPP ${versionStr}`,
+ }),
+ uri: uri.href,
+ },
+ ],
+ }
+ }
+ const content = await readFile(schemaPath, 'utf8')
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: content,
+ uri: uri.href,
+ },
+ ],
+ }
+ } catch {
+ return {
+ contents: [
+ {
+ mimeType: 'application/json',
+ text: JSON.stringify({
+ error: `Schema '${command as string}' not found for OCPP ${version as string}`,
+ }),
+ uri: uri.href,
+ },
+ ],
+ }
+ }
+ }
+ )
+}
+
+const registerLogReadTool = (
+ server: McpServer,
+ name: string,
+ configField: 'errorFile' | 'file',
+ description: string
+): void => {
+ const label = configField === 'file' ? 'Log' : 'Error log'
+ server.registerTool(
+ name,
+ {
+ annotations: { readOnlyHint: true },
+ description,
+ inputSchema: {
+ date: z
+ .string()
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
+ .optional()
+ .describe('Log file date in YYYY-MM-DD format. Defaults to current local date'),
+ tail: z
+ .number()
+ .int()
+ .positive()
+ .max(MAX_TAIL_LINES)
+ .default(DEFAULT_TAIL_LINES)
+ .describe('Number of lines to return from the end of the log'),
+ },
+ },
+ async ({ date, tail }) => {
+ try {
+ const logPath = getLogFilePath(configField, date)
+ if (logPath == null) {
+ return {
+ content: [{ text: `${label} file not configured`, type: 'text' as const }],
+ isError: true,
+ }
+ }
+ const { lines, totalLines } = await tailFile(logPath, tail)
+ const meta =
+ totalLines >= 0
+ ? `Showing last ${String(lines.length)} of ${String(totalLines)} lines`
+ : `Showing last ${String(lines.length)} lines`
+ return {
+ content: [{ text: `${meta}\n\n${lines.join('\n')}`, type: 'text' as const }],
+ }
+ } catch {
+ return {
+ content: [{ text: `${label} file not available`, type: 'text' as const }],
+ isError: true,
+ }
+ }
+ }
+ )
+}
+
+export const registerMCPLogTools = (server: McpServer): void => {
+ registerLogReadTool(
+ server,
+ 'readCombinedLog',
+ 'file',
+ 'Read recent entries from the combined simulator log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
+ )
+ registerLogReadTool(
+ server,
+ 'readErrorLog',
+ 'errorFile',
+ 'Read recent entries from the error log file. Returns the last N lines (default 200, max 5000). Optionally specify a date (YYYY-MM-DD) for rotated log files.'
+ )
+}
--- /dev/null
+import { z } from 'zod'
+
+import { ProcedureName } from '../../../types/index.js'
+
+export interface MCPToolSchema {
+ description: string
+ inputSchema: z.ZodObject<z.ZodRawShape>
+}
+
+const hashIds = z
+ .array(z.string())
+ .optional()
+ .describe('Target station hash IDs (omit for all stations)')
+
+const connectorIds = z
+ .array(z.number().int().positive())
+ .optional()
+ .describe('Target connector IDs')
+
+const broadcastInputSchema = z.object({
+ connectorIds,
+ hashIds,
+})
+
+const emptyInputSchema = z.object({})
+
+const chargingStationOptionsSchema = z.object({
+ autoRegister: z.boolean().optional().describe('Set stations as registered at boot notification'),
+ autoStart: z.boolean().optional().describe('Enable automatic start of added charging station'),
+ enableStatistics: z.boolean().optional().describe('Enable charging station statistics'),
+ ocppStrictCompliance: z
+ .boolean()
+ .optional()
+ .describe('Enable strict OCPP specifications adherence'),
+ persistentConfiguration: z
+ .boolean()
+ .optional()
+ .describe('Enable persistent OCPP parameters storage'),
+ stopTransactionsOnStopped: z
+ .boolean()
+ .optional()
+ .describe('Enable stop transactions on station stop'),
+ supervisionUrls: z
+ .union([z.url(), z.array(z.url())])
+ .optional()
+ .describe('OCPP server supervision URL(s)'),
+})
+
+/** Maps ProcedureName to OCPP JSON Schema file base names per version */
+export const ocppSchemaMapping = new Map<ProcedureName, { ocpp16?: string; ocpp20?: string }>([
+ [ProcedureName.AUTHORIZE, { ocpp16: 'Authorize', ocpp20: 'AuthorizeRequest' }],
+ [
+ ProcedureName.BOOT_NOTIFICATION,
+ { ocpp16: 'BootNotification', ocpp20: 'BootNotificationRequest' },
+ ],
+ [ProcedureName.DATA_TRANSFER, { ocpp16: 'DataTransfer', ocpp20: 'DataTransferRequest' }],
+ [ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION, { ocpp16: 'DiagnosticsStatusNotification' }],
+ [
+ ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+ { ocpp16: 'FirmwareStatusNotification', ocpp20: 'FirmwareStatusNotificationRequest' },
+ ],
+ [ProcedureName.GET_15118_EV_CERTIFICATE, { ocpp20: 'Get15118EVCertificateRequest' }],
+ [ProcedureName.GET_CERTIFICATE_STATUS, { ocpp20: 'GetCertificateStatusRequest' }],
+ [ProcedureName.LOG_STATUS_NOTIFICATION, { ocpp20: 'LogStatusNotificationRequest' }],
+ [ProcedureName.METER_VALUES, { ocpp16: 'MeterValues', ocpp20: 'MeterValuesRequest' }],
+ [ProcedureName.NOTIFY_CUSTOMER_INFORMATION, { ocpp20: 'NotifyCustomerInformationRequest' }],
+ [ProcedureName.NOTIFY_REPORT, { ocpp20: 'NotifyReportRequest' }],
+ [ProcedureName.SECURITY_EVENT_NOTIFICATION, { ocpp20: 'SecurityEventNotificationRequest' }],
+ [ProcedureName.SIGN_CERTIFICATE, { ocpp20: 'SignCertificateRequest' }],
+ [ProcedureName.START_TRANSACTION, { ocpp16: 'StartTransaction' }],
+ [
+ ProcedureName.STATUS_NOTIFICATION,
+ { ocpp16: 'StatusNotification', ocpp20: 'StatusNotificationRequest' },
+ ],
+ [ProcedureName.STOP_TRANSACTION, { ocpp16: 'StopTransaction' }],
+ [ProcedureName.TRANSACTION_EVENT, { ocpp20: 'TransactionEventRequest' }],
+])
+
+const ocpp16PayloadField = z
+ .record(z.string(), z.unknown())
+ .optional()
+ .describe('OCPP 1.6 request payload')
+
+const ocpp20PayloadField = z
+ .record(z.string(), z.unknown())
+ .optional()
+ .describe('OCPP 2.0.1 request payload')
+
+const buildOcppInputSchema = (mapping: {
+ ocpp16?: string
+ ocpp20?: string
+}): z.ZodObject<z.ZodRawShape> => {
+ const fields: Record<string, z.ZodType> = { connectorIds, hashIds }
+ if (mapping.ocpp16 != null) {
+ fields.ocpp16Payload = ocpp16PayloadField
+ }
+ if (mapping.ocpp20 != null) {
+ fields.ocpp20Payload = ocpp20PayloadField
+ }
+ return z.object(fields)
+}
+
+const buildVersionAffinity = (mapping: { ocpp16?: string; ocpp20?: string }): string => {
+ if (mapping.ocpp16 != null && mapping.ocpp20 != null) return '(OCPP 1.6 & 2.0.x)'
+ if (mapping.ocpp16 != null) return '(OCPP 1.6 only)'
+ return '(OCPP 2.0.x only)'
+}
+
+const getMapping = (name: ProcedureName): { ocpp16?: string; ocpp20?: string } =>
+ ocppSchemaMapping.get(name) ?? {}
+
+const ocppDescription = (base: string, name: ProcedureName): string => {
+ const mapping = getMapping(name)
+ const affinity = buildVersionAffinity(mapping)
+ const hint =
+ mapping.ocpp16 != null && mapping.ocpp20 != null
+ ? '. Provide ocpp16Payload for 1.6 stations, ocpp20Payload for 2.0 stations.'
+ : ''
+ return `${base} ${affinity}${hint}`
+}
+
+const ocppInputSchema = (name: ProcedureName): z.ZodObject<z.ZodRawShape> =>
+ buildOcppInputSchema(getMapping(name))
+
+export const mcpToolSchemas = new Map<ProcedureName, MCPToolSchema>([
+ [
+ ProcedureName.ADD_CHARGING_STATIONS,
+ {
+ description: 'Add new charging stations from a configuration template',
+ inputSchema: z.object({
+ numberOfStations: z
+ .number()
+ .int()
+ .positive()
+ .describe('Number of charging stations to add'),
+ options: chargingStationOptionsSchema
+ .optional()
+ .describe('Configuration overrides for the new stations'),
+ template: z.string().describe('Name of the charging station template to use'),
+ }),
+ },
+ ],
+ [
+ ProcedureName.AUTHORIZE,
+ {
+ description: ocppDescription('Send an Authorize request', ProcedureName.AUTHORIZE),
+ inputSchema: ocppInputSchema(ProcedureName.AUTHORIZE),
+ },
+ ],
+ [
+ ProcedureName.BOOT_NOTIFICATION,
+ {
+ description: ocppDescription(
+ 'Send a BootNotification request',
+ ProcedureName.BOOT_NOTIFICATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.BOOT_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.CLOSE_CONNECTION,
+ {
+ description:
+ 'Close the WebSocket connection to the OCPP server for one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.DATA_TRANSFER,
+ {
+ description: ocppDescription('Send a DataTransfer request', ProcedureName.DATA_TRANSFER),
+ inputSchema: ocppInputSchema(ProcedureName.DATA_TRANSFER),
+ },
+ ],
+ [
+ ProcedureName.DELETE_CHARGING_STATIONS,
+ {
+ description: 'Delete one or more charging stations from the simulator',
+ inputSchema: z.object({
+ deleteConfiguration: z
+ .boolean()
+ .optional()
+ .describe('Whether to delete persistent configuration files'),
+ hashIds,
+ }),
+ },
+ ],
+ [
+ ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+ {
+ description: ocppDescription(
+ 'Send a DiagnosticsStatusNotification',
+ ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+ {
+ description: ocppDescription(
+ 'Send a FirmwareStatusNotification',
+ ProcedureName.FIRMWARE_STATUS_NOTIFICATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.FIRMWARE_STATUS_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.GET_15118_EV_CERTIFICATE,
+ {
+ description: ocppDescription(
+ 'Request an ISO 15118 EV certificate',
+ ProcedureName.GET_15118_EV_CERTIFICATE
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.GET_15118_EV_CERTIFICATE),
+ },
+ ],
+ [
+ ProcedureName.GET_CERTIFICATE_STATUS,
+ {
+ description: ocppDescription(
+ 'Get the certificate status',
+ ProcedureName.GET_CERTIFICATE_STATUS
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.GET_CERTIFICATE_STATUS),
+ },
+ ],
+ [
+ ProcedureName.HEARTBEAT,
+ {
+ description: 'Send a Heartbeat request (OCPP 1.6 & 2.0.x)',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.LIST_CHARGING_STATIONS,
+ {
+ description: 'List all charging stations with their current data and connection status',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.LIST_TEMPLATES,
+ {
+ description: 'List available charging station configuration templates',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.LOG_STATUS_NOTIFICATION,
+ {
+ description: ocppDescription(
+ 'Send a LogStatusNotification',
+ ProcedureName.LOG_STATUS_NOTIFICATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.LOG_STATUS_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.METER_VALUES,
+ {
+ description: ocppDescription('Send MeterValues', ProcedureName.METER_VALUES),
+ inputSchema: ocppInputSchema(ProcedureName.METER_VALUES),
+ },
+ ],
+ [
+ ProcedureName.NOTIFY_CUSTOMER_INFORMATION,
+ {
+ description: ocppDescription(
+ 'Send a NotifyCustomerInformation',
+ ProcedureName.NOTIFY_CUSTOMER_INFORMATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.NOTIFY_CUSTOMER_INFORMATION),
+ },
+ ],
+ [
+ ProcedureName.NOTIFY_REPORT,
+ {
+ description: ocppDescription('Send a NotifyReport', ProcedureName.NOTIFY_REPORT),
+ inputSchema: ocppInputSchema(ProcedureName.NOTIFY_REPORT),
+ },
+ ],
+ [
+ ProcedureName.OPEN_CONNECTION,
+ {
+ description:
+ 'Open the WebSocket connection to the OCPP server for one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.PERFORMANCE_STATISTICS,
+ {
+ description:
+ 'Get performance statistics of the charging stations simulator when storage is enabled',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.SECURITY_EVENT_NOTIFICATION,
+ {
+ description: ocppDescription(
+ 'Send a SecurityEventNotification',
+ ProcedureName.SECURITY_EVENT_NOTIFICATION
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.SECURITY_EVENT_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.SET_SUPERVISION_URL,
+ {
+ description: 'Set the OCPP server supervision URL for one or more charging stations',
+ inputSchema: z.object({
+ hashIds,
+ url: z.url().describe('The OCPP server supervision URL to set'),
+ }),
+ },
+ ],
+ [
+ ProcedureName.SIGN_CERTIFICATE,
+ {
+ description: ocppDescription(
+ 'Send a SignCertificate request',
+ ProcedureName.SIGN_CERTIFICATE
+ ),
+ inputSchema: ocppInputSchema(ProcedureName.SIGN_CERTIFICATE),
+ },
+ ],
+ [
+ ProcedureName.SIMULATOR_STATE,
+ {
+ description:
+ 'Get the current state of the simulator including version, configuration, started status, and template statistics',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+ {
+ description: 'Start the automatic transaction generator on one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.START_CHARGING_STATION,
+ {
+ description: 'Start one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.START_SIMULATOR,
+ {
+ description: 'Start the charging stations simulator',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.START_TRANSACTION,
+ {
+ description: ocppDescription('Start a charging transaction', ProcedureName.START_TRANSACTION),
+ inputSchema: ocppInputSchema(ProcedureName.START_TRANSACTION),
+ },
+ ],
+ [
+ ProcedureName.STATUS_NOTIFICATION,
+ {
+ description: ocppDescription('Send a StatusNotification', ProcedureName.STATUS_NOTIFICATION),
+ inputSchema: ocppInputSchema(ProcedureName.STATUS_NOTIFICATION),
+ },
+ ],
+ [
+ ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+ {
+ description: 'Stop the automatic transaction generator on one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.STOP_CHARGING_STATION,
+ {
+ description: 'Stop one or more charging stations',
+ inputSchema: broadcastInputSchema,
+ },
+ ],
+ [
+ ProcedureName.STOP_SIMULATOR,
+ {
+ description: 'Stop the charging stations simulator',
+ inputSchema: emptyInputSchema,
+ },
+ ],
+ [
+ ProcedureName.STOP_TRANSACTION,
+ {
+ description: ocppDescription('Stop a charging transaction', ProcedureName.STOP_TRANSACTION),
+ inputSchema: z.object({
+ hashIds,
+ ocpp16Payload: z
+ .record(z.string(), z.unknown())
+ .optional()
+ .describe('OCPP 1.6 StopTransaction payload'),
+ transactionId: z.number().int().optional().describe('Transaction ID to stop'),
+ }),
+ },
+ ],
+ [
+ ProcedureName.TRANSACTION_EVENT,
+ {
+ description: ocppDescription('Send a TransactionEvent', ProcedureName.TRANSACTION_EVENT),
+ inputSchema: ocppInputSchema(ProcedureName.TRANSACTION_EVENT),
+ },
+ ],
+])
--- /dev/null
+export {
+ registerMCPLogTools,
+ registerMCPResources,
+ registerMCPSchemaResources,
+} from './MCPResourceHandlers.js'
+export { mcpToolSchemas, ocppSchemaMapping } from './MCPToolSchemas.js'
+export type { MCPToolSchema } from './MCPToolSchemas.js'
import type { ConnectorStatus } from './ConnectorStatus.js'
+import type { SampledValueTemplate } from './MeasurandPerPhaseSampledValueTemplates.js'
import type { AvailabilityType } from './ocpp/Requests.js'
export interface EvseStatus {
availability: AvailabilityType
connectors: Map<number, ConnectorStatus>
+ MeterValues?: SampledValueTemplate[]
}
export interface EvseTemplate {
Connectors: Record<string, ConnectorStatus>
+ MeterValues?: SampledValueTemplate[]
}
export enum ApplicationProtocol {
HTTP = 'http',
+ MCP = 'mcp',
WS = 'ws',
}
type OCPP16StopTransactionRequest,
type OCPP16StopTransactionResponse,
} from './1.6/Transaction.js'
+import { type OCPP20AuthorizeRequest } from './2.0/Requests.js'
+import { type OCPP20AuthorizeResponse } from './2.0/Responses.js'
import { OCPP20AuthorizationStatusEnumType, OCPP20ReasonEnumType } from './2.0/Transaction.js'
export const AuthorizationStatus = {
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type AuthorizationStatus = OCPP16AuthorizationStatus | OCPP20AuthorizationStatusEnumType
-export type AuthorizeRequest = OCPP16AuthorizeRequest
+export type AuthorizeRequest = OCPP16AuthorizeRequest | OCPP20AuthorizeRequest
-export type AuthorizeResponse = OCPP16AuthorizeResponse
+export type AuthorizeResponse = OCPP16AuthorizeResponse | OCPP20AuthorizeResponse
export type StartTransactionRequest = OCPP16StartTransactionRequest
* Test values for transaction-related operations
*/
export const TEST_TRANSACTION_ID = 1
+export const TEST_TRANSACTION_ID_STRING = 'tx-ocpp20-1'
export const TEST_TRANSACTION_ENERGY_WH = 5000
/**
} from '../../../src/types/index.js'
import { Constants } from '../../../src/utils/index.js'
import { flushMicrotasks, standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import { TEST_TRANSACTION_ID_STRING } from '../ChargingStationTestConstants.js'
import { createMockChargingStation } from '../ChargingStationTestUtils.js'
import { createMockStationWithRequestTracking } from '../ocpp/2.0/OCPP20TestUtils.js'
value: 0,
},
]
+ connectorStatus.transactionId = TEST_TRANSACTION_ID_STRING
}
instance = new ChargingStationWorkerBroadcastChannel(station)
assert.notStrictEqual(payload, undefined)
assert.ok('evseId' in payload, 'Expected payload to include evseId')
assert.ok('connectorId' in payload, 'Expected payload to include connectorId')
- assert.ok('status' in payload, 'Expected payload to include status')
+ assert.ok('connectorStatus' in payload, 'Expected payload to include connectorStatus')
assert.ok(
payload.evseId != null && payload.evseId > 0,
'Expected evseId > 0 (EVSE 0 excluded)'
assert.strictEqual(command, OCPP20RequestCommand.STATUS_NOTIFICATION)
assert.strictEqual(payload.evseId, 1)
assert.strictEqual(payload.connectorId, 1)
- assert.ok('status' in payload)
+ assert.ok('connectorStatus' in payload)
assert.strictEqual(options.skipBufferingOnError, true)
assert.strictEqual(options.triggerMessage, true)
})
await it('should build complete StatusNotificationRequest from connectorId + status', async () => {
await service.requestHandler(station, OCPP20RequestCommand.STATUS_NOTIFICATION, {
connectorId: 1,
+ connectorStatus: ConnectorStatusEnum.Available,
evseId: 1,
- status: ConnectorStatusEnum.Available,
})
assert.strictEqual(sendMessageMock.mock.calls.length, 1)
await it('should resolve evseId from station when not provided', async () => {
await service.requestHandler(station, OCPP20RequestCommand.STATUS_NOTIFICATION, {
connectorId: 1,
- status: ConnectorStatusEnum.Occupied,
+ connectorStatus: ConnectorStatusEnum.Occupied,
})
assert.strictEqual(sendMessageMock.mock.calls.length, 1)
import type { ChargingStation } from '../../../../src/charging-station/index.js'
import {
- ConnectorStatusEnum,
OCPP20ConnectorStatusEnumType,
OCPP20RequestCommand,
type OCPP20StatusNotificationRequest,
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Available,
evseId: 1,
- status: ConnectorStatusEnum.Available,
}
) as OCPP20StatusNotificationRequest
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 2,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Occupied,
evseId: 2,
- status: ConnectorStatusEnum.Occupied,
}
) as OCPP20StatusNotificationRequest
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Faulted,
evseId: 1,
- status: ConnectorStatusEnum.Faulted,
}
) as OCPP20StatusNotificationRequest
// FR: G01.FR.04
await it('should handle all OCPP20ConnectorStatusEnumType values correctly', () => {
- const statusValues: [ConnectorStatusEnum, OCPP20ConnectorStatusEnumType][] = [
- [ConnectorStatusEnum.Available, OCPP20ConnectorStatusEnumType.Available],
- [ConnectorStatusEnum.Faulted, OCPP20ConnectorStatusEnumType.Faulted],
- [ConnectorStatusEnum.Occupied, OCPP20ConnectorStatusEnumType.Occupied],
- [ConnectorStatusEnum.Reserved, OCPP20ConnectorStatusEnumType.Reserved],
- [ConnectorStatusEnum.Unavailable, OCPP20ConnectorStatusEnumType.Unavailable],
+ const statusValues: OCPP20ConnectorStatusEnumType[] = [
+ OCPP20ConnectorStatusEnumType.Available,
+ OCPP20ConnectorStatusEnumType.Faulted,
+ OCPP20ConnectorStatusEnumType.Occupied,
+ OCPP20ConnectorStatusEnumType.Reserved,
+ OCPP20ConnectorStatusEnumType.Unavailable,
]
- statusValues.forEach(([inputStatus, expectedConnectorStatus], index) => {
+ statusValues.forEach((connectorStatus, index) => {
const payload = testableRequestService.buildRequestPayload(
station,
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: index + 1,
+ connectorStatus,
evseId: index + 1,
- status: inputStatus,
}
) as OCPP20StatusNotificationRequest
assert.notStrictEqual(payload, undefined)
- assert.strictEqual(payload.connectorStatus, expectedConnectorStatus)
+ assert.strictEqual(payload.connectorStatus, connectorStatus)
assert.strictEqual(payload.connectorId, index + 1)
assert.strictEqual(payload.evseId, index + 1)
assert.ok(payload.timestamp instanceof Date)
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 3,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Reserved,
evseId: 2,
- status: ConnectorStatusEnum.Reserved,
}
) as OCPP20StatusNotificationRequest
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 0,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Available,
evseId: 1,
- status: ConnectorStatusEnum.Available,
}
) as OCPP20StatusNotificationRequest
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Unavailable,
evseId: 0,
- status: ConnectorStatusEnum.Unavailable,
}
) as OCPP20StatusNotificationRequest
// buildRequestPayload now generates its own timestamp via buildStatusNotificationRequest,
// so we verify the output always has a valid Date timestamp
const statusValues = [
- ConnectorStatusEnum.Available,
- ConnectorStatusEnum.Occupied,
- ConnectorStatusEnum.Faulted,
- ConnectorStatusEnum.Reserved,
+ OCPP20ConnectorStatusEnumType.Available,
+ OCPP20ConnectorStatusEnumType.Occupied,
+ OCPP20ConnectorStatusEnumType.Faulted,
+ OCPP20ConnectorStatusEnumType.Reserved,
]
const beforeBuild = new Date()
- statusValues.forEach(status => {
+ statusValues.forEach(connectorStatus => {
const payload = testableRequestService.buildRequestPayload(
station,
OCPP20RequestCommand.STATUS_NOTIFICATION,
{
connectorId: 1,
+ connectorStatus,
evseId: 1,
- status,
}
) as OCPP20StatusNotificationRequest
OCPP20RequestCommand,
OCPP20RequiredVariableName,
OCPP20TransactionEventEnumType,
+ type OCPP20TransactionEventRequest,
type OCPP20TransactionType,
OCPP20TriggerReasonEnumType,
OCPPVersion,
// Reset sequence number to simulate new transaction
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
- const transactionEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- triggerReason,
+ const transactionEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate required fields
assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Started)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Build first event (Started)
- const startEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Build second event (Updated)
- const updateEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ const updateEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest)
// Build third event (Ended)
- const endEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.StopAuthorized,
+ const endEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ stoppedReason: OCPP20ReasonEnumType.Local,
transactionId,
- { stoppedReason: OCPP20ReasonEnumType.Local }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate sequence number progression: 0 → 1 → 2
assert.strictEqual(startEvent.seqNo, 0)
reservationId: 67890,
}
- const transactionEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ const transactionEvent = buildTransactionEvent(mockStation, {
+ cableMaxCurrent: options.cableMaxCurrent,
+ chargingState: options.chargingState,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ idToken: options.idToken,
+ numberOfPhasesUsed: options.numberOfPhasesUsed,
+ offline: options.offline,
+ remoteStartId: options.remoteStartId,
+ reservationId: options.reservationId,
transactionId,
- options
- )
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate optional fields are included
if (transactionEvent.idToken == null) {
'this-string-is-way-too-long-for-a-valid-transaction-id-exceeds-36-chars'
try {
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ buildTransactionEvent(mockStation, {
connectorId,
- invalidTransactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: invalidTransactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
throw new Error('Should have thrown error for invalid identifier string')
} catch (error) {
assert.ok((error as Error).message.includes('Invalid transaction ID format'))
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
for (const triggerReason of triggerReasons) {
- const transactionEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- triggerReason,
+ const transactionEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(transactionEvent.triggerReason, triggerReason)
assert.strictEqual(transactionEvent.eventType, OCPP20TransactionEventEnumType.Updated)
const connectorId = 1
// First, build a transaction event to set sequence number
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ buildTransactionEvent(mockStation, {
connectorId,
- generateUUID()
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: generateUUID(),
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Verify sequence number is set
const connectorStatus = mockStation.getConnectorStatus(connectorId)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
- const transactionEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const transactionEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken: {
+ idToken: 'SCHEMA_TEST_TOKEN',
+ type: OCPP20IdTokenEnumType.ISO14443,
+ },
transactionId,
- {
- idToken: {
- idToken: 'SCHEMA_TEST_TOKEN',
- type: OCPP20IdTokenEnumType.ISO14443,
- },
- }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate all required fields exist
const requiredFields = [
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
- const transactionEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const transactionEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// For this test setup, EVSE ID should match connector ID
if (transactionEvent.evse == null) {
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Old method call should still work
- const oldEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const oldEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(oldEvent.eventType, OCPP20TransactionEventEnumType.Started)
assert.strictEqual(oldEvent.triggerReason, OCPP20TriggerReasonEnumType.Authorized)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
- const startedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- expectedStartTrigger,
+ const startedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
transactionId,
- idToken != null ? { idToken } : undefined
- )
+ triggerReason: expectedStartTrigger,
+ ...(idToken != null ? { idToken } : {}),
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(startedEvent.eventType, OCPP20TransactionEventEnumType.Started)
assert.strictEqual(startedEvent.triggerReason, expectedStartTrigger)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Step 1: Started event
- const startedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- expectedStartTrigger,
+ const startedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
transactionId,
- idToken != null ? { idToken } : undefined
- )
+ triggerReason: expectedStartTrigger,
+ ...(idToken != null ? { idToken } : {}),
+ } as unknown as OCPP20TransactionEventRequest)
// Step 2: Charging state change
- const chargingEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ const chargingEvent = buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// Step 3: Ended event
- const endedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.StopAuthorized,
+ const endedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate event sequence
assert.strictEqual(startedEvent.seqNo, 0)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connector2)
// Start transaction on connector 1
- const conn1Event1 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- expectedStartTrigger,
- connector1,
- transaction1Id
- )
+ const conn1Event1 = buildTransactionEvent(mockStation, {
+ connectorId: connector1,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transaction1Id,
+ triggerReason: expectedStartTrigger,
+ } as unknown as OCPP20TransactionEventRequest)
// Start transaction on connector 2
- const conn2Event1 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- expectedStartTrigger,
- connector2,
- transaction2Id
- )
+ const conn2Event1 = buildTransactionEvent(mockStation, {
+ connectorId: connector2,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transaction2Id,
+ triggerReason: expectedStartTrigger,
+ } as unknown as OCPP20TransactionEventRequest)
// Update connector 1
- const conn1Event2 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
- connector1,
- transaction1Id
- )
+ const conn1Event2 = buildTransactionEvent(mockStation, {
+ connectorId: connector1,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId: transaction1Id,
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// Update connector 2
- const conn2Event2 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
- connector2,
- transaction2Id
- )
+ const conn2Event2 = buildTransactionEvent(mockStation, {
+ connectorId: connector2,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId: transaction2Id,
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// Verify independent sequence numbers
assert.strictEqual(conn1Event1.seqNo, 0)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Step 1: Cable plugged in (Started)
- const cablePluggedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ const cablePluggedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest)
// Step 2: EV detected (Updated)
- const evDetectedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.EVDetected,
+ const evDetectedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.EVDetected,
+ } as unknown as OCPP20TransactionEventRequest)
// Step 3: Charging starts (Updated with ChargingStateChanged)
- const chargingStartedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ const chargingStartedEvent = buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// Assert sequence numbers follow correct order
assert.strictEqual(cablePluggedEvent.seqNo, 0)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Start transaction with cable plug
- const startEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ const startEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest)
// End transaction with EV departure (cable removal)
- const endEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.EVDeparted,
+ const endEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.EVDeparted,
+ } as unknown as OCPP20TransactionEventRequest)
// Assert proper sequencing for cable-initiated start and end
assert.strictEqual(startEvent.seqNo, 0)
// Build full cable-first flow
const events = [
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.EVDetected,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.Authorized,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.EVDetected,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- ),
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest),
]
// Assert EVDetected comes after CablePluggedIn and before authorization
// Cable-first flow with suspended state
const events = [
// 1. Cable plugged
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest),
// 2. Start charging
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- ),
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest),
// 3. Suspended by EV
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.SuspendedEV,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.SuspendedEV }
- ),
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest),
// 4. Resume charging
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- ),
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest),
// 5. EV departed
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.EVDeparted,
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.EVDeparted,
+ } as unknown as OCPP20TransactionEventRequest),
]
// Verify sequence numbers are continuous through suspend/resume
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Build Started event with idToken (E03.FR.01: IdToken must be in first event)
- const startedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
if (startedEvent.idToken == null) {
assert.fail('Expected idToken to be defined')
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// First event includes idToken
- const startedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Second event should NOT include idToken (flag is set after first inclusion)
- const updatedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ const updatedEvent = buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ idToken,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging, idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
assert.notStrictEqual(startedEvent.idToken, undefined)
assert.strictEqual(updatedEvent.idToken, undefined)
type: OCPP20IdTokenEnumType.ISO14443,
}
- const rfidEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const rfidEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken: rfidToken,
transactionId,
- { idToken: rfidToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(rfidEvent.idToken?.type, OCPP20IdTokenEnumType.ISO14443)
type: OCPP20IdTokenEnumType.eMAID,
}
- const emaidEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const emaidEvent = buildTransactionEvent(mockStation, {
connectorId,
- generateUUID(),
- { idToken: emaidToken }
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken: emaidToken,
+ transactionId: generateUUID(),
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(emaidEvent.idToken?.type, OCPP20IdTokenEnumType.eMAID)
assert.strictEqual(emaidEvent.idToken.idToken, 'DE*ABC*E123456*1')
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// E03 Step 1: IdToken presented and authorized (Started with Authorized trigger)
- const authorizedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const authorizedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// E03 Step 2: Cable connected (Updated event)
- const cableConnectedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ const cableConnectedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest)
// E03 Step 3: Charging starts
- const chargingEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ const chargingEvent = buildTransactionEvent(mockStation, {
+ chargingState: OCPP20ChargingStateEnumType.Charging,
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Updated,
transactionId,
- { chargingState: OCPP20ChargingStateEnumType.Charging }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest)
// E03 Step 4: Transaction ends
- const endedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.StopAuthorized,
+ const endedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Validate event sequence
assert.strictEqual(authorizedEvent.eventType, OCPP20TransactionEventEnumType.Started)
connectorStatus.transactionIdTokenSent = undefined
}
- const e03Start = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const e03Start = buildTransactionEvent(mockStation, {
connectorId,
- e03TransactionId,
- { idToken }
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
+ transactionId: e03TransactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// E02 Cable-First: Starts with CablePluggedIn trigger
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
connectorStatus.transactionIdTokenSent = undefined
}
- const e02Start = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ const e02Start = buildTransactionEvent(mockStation, {
connectorId,
- e02TransactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: e02TransactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest)
// Key difference: E03 starts with Authorized, E02 starts with CablePluggedIn
assert.strictEqual(e03Start.triggerReason, OCPP20TriggerReasonEnumType.Authorized)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// E03.FR.05: User authorizes with IdToken
- const authorizedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const authorizedEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// E03.FR.06: Cable not connected within timeout - transaction ends with Timeout
- const timeoutEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.EVConnectTimeout,
+ const timeoutEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.EVConnectTimeout,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(authorizedEvent.eventType, OCPP20TransactionEventEnumType.Started)
assert.strictEqual(authorizedEvent.triggerReason, OCPP20TriggerReasonEnumType.Authorized)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Transaction started with authorization
- const startEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startEvent = buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- )
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
// Transaction ended due to deauthorization (e.g., token revoked mid-session)
- const revokedEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.Deauthorized,
+ const revokedEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Deauthorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(startEvent.eventType, OCPP20TransactionEventEnumType.Started)
assert.strictEqual(revokedEvent.eventType, OCPP20TransactionEventEnumType.Ended)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
const events = [
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ buildTransactionEvent(mockStation, {
connectorId,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ idToken,
transactionId,
- { idToken }
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.CablePluggedIn,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.CablePluggedIn,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.ChargingStateChanged,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
- buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.StopAuthorized,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest),
+ buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- ),
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ } as unknown as OCPP20TransactionEventRequest),
]
// E03.FR.07: Sequence numbers must be continuous
// E03.FR.08: transactionId MUST be unique
assert.notStrictEqual(transaction1Id, transaction2Id)
- const event1 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const event1 = buildTransactionEvent(mockStation, {
connectorId,
- transaction1Id
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transaction1Id,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
- const event2 = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const event2 = buildTransactionEvent(mockStation, {
connectorId,
- transaction2Id
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transaction2Id,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(event1.transactionInfo.transactionId, transaction1Id)
assert.strictEqual(event2.transactionInfo.transactionId, transaction2Id)
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, connectorId)
// Send initial Started event
- const startEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(startEvent.seqNo, 0)
// Send multiple periodic events (simulating timer ticks)
for (let i = 1; i <= 3; i++) {
- const periodicEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ const periodicEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(periodicEvent.seqNo, i)
}
// Simulate full transaction lifecycle with periodic updates
// 1. Started event (seqNo: 0)
- const startEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
+ const startEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(startEvent.seqNo, 0)
// 2. Multiple periodic updates (seqNo: 1, 2, 3)
for (let i = 1; i <= 3; i++) {
- const updateEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ const updateEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(updateEvent.seqNo, i)
}
// 3. Ended event (seqNo: 4)
- const endEvent = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Ended,
- OCPP20TriggerReasonEnumType.StopAuthorized,
+ const endEvent = buildTransactionEvent(mockStation, {
connectorId,
- transactionId
- )
+ eventType: OCPP20TransactionEventEnumType.Ended,
+ transactionId,
+ triggerReason: OCPP20TriggerReasonEnumType.StopAuthorized,
+ } as unknown as OCPP20TransactionEventRequest)
assert.strictEqual(endEvent.seqNo, 4)
})
OCPP20ServiceUtils.resetTransactionSequenceNumber(mockStation, 2)
// Build events for connector 1
- const event1Start = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
- 1,
- transactionId1
- )
- const event1Update = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
- 1,
- transactionId1
- )
+ const event1Start = buildTransactionEvent(mockStation, {
+ connectorId: 1,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transactionId1,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
+ const event1Update = buildTransactionEvent(mockStation, {
+ connectorId: 1,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId: transactionId1,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest)
// Build events for connector 2
- const event2Start = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Started,
- OCPP20TriggerReasonEnumType.Authorized,
- 2,
- transactionId2
- )
- const event2Update = buildTransactionEvent(
- mockStation,
- OCPP20TransactionEventEnumType.Updated,
- OCPP20TriggerReasonEnumType.MeterValuePeriodic,
- 2,
- transactionId2
- )
+ const event2Start = buildTransactionEvent(mockStation, {
+ connectorId: 2,
+ eventType: OCPP20TransactionEventEnumType.Started,
+ transactionId: transactionId2,
+ triggerReason: OCPP20TriggerReasonEnumType.Authorized,
+ } as unknown as OCPP20TransactionEventRequest)
+ const event2Update = buildTransactionEvent(mockStation, {
+ connectorId: 2,
+ eventType: OCPP20TransactionEventEnumType.Updated,
+ transactionId: transactionId2,
+ triggerReason: OCPP20TriggerReasonEnumType.MeterValuePeriodic,
+ } as unknown as OCPP20TransactionEventRequest)
// Verify independent sequence numbers
assert.strictEqual(event1Start.seqNo, 0)
restoreConnectorStatus,
sendAndSetConnectorStatus,
} from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
-import { ConnectorStatusEnum, OCPPVersion } from '../../../src/types/index.js'
+import {
+ ConnectorStatusEnum,
+ type OCPP16StatusNotificationRequest,
+ type OCPP20StatusNotificationRequest,
+ OCPPVersion,
+} from '../../../src/types/index.js'
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import { createMockChargingStation } from '../ChargingStationTestUtils.js'
await it('should send StatusNotification and update connector status', async () => {
const { requestHandler, station } = createStationWithRequestHandler()
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 1,
+ status: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP16StatusNotificationRequest)
assert.strictEqual(requestHandler.mock.calls.length, 1)
assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied)
await it('should return early when connector does not exist', async () => {
const { requestHandler, station } = createStationWithRequestHandler()
- await sendAndSetConnectorStatus(station, 99, ConnectorStatusEnum.Occupied)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 99,
+ status: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP16StatusNotificationRequest)
assert.strictEqual(requestHandler.mock.calls.length, 0)
})
await it('should skip sending when options.send is false', async () => {
const { requestHandler, station } = createStationWithRequestHandler()
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied, undefined, {
- send: false,
- })
+ await sendAndSetConnectorStatus(
+ station,
+ {
+ connectorId: 1,
+ status: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP16StatusNotificationRequest,
+ {
+ send: false,
+ }
+ )
assert.strictEqual(requestHandler.mock.calls.length, 0)
assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied)
assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Available)
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Unavailable)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 1,
+ status: ConnectorStatusEnum.Unavailable,
+ } as unknown as OCPP16StatusNotificationRequest)
assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Unavailable)
})
const stationObj = station as unknown as { emitChargingStationEvent: () => void }
const emitSpy = mock.method(stationObj, 'emitChargingStationEvent')
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 1,
+ status: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP16StatusNotificationRequest)
assert.strictEqual(emitSpy.mock.calls.length, 1)
})
ocppVersion: OCPPVersion.VERSION_20,
})
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied, 1)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 1,
+ connectorStatus: ConnectorStatusEnum.Occupied,
+ evseId: 1,
+ } as unknown as OCPP20StatusNotificationRequest)
assert.strictEqual(requestHandler.mock.calls.length, 1)
assert.strictEqual(station.getConnectorStatus(1)?.status, ConnectorStatusEnum.Occupied)
await it('should default options.send to true when options not provided', async () => {
const { requestHandler, station } = createStationWithRequestHandler()
- await sendAndSetConnectorStatus(station, 1, ConnectorStatusEnum.Occupied)
+ await sendAndSetConnectorStatus(station, {
+ connectorId: 1,
+ status: ConnectorStatusEnum.Occupied,
+ } as unknown as OCPP16StatusNotificationRequest)
assert.strictEqual(requestHandler.mock.calls.length, 1)
})
--- /dev/null
+/**
+ * @file Tests for OCPPServiceUtils buildMeterValue
+ * @description Verifies buildMeterValue resolves connectorId/evseId from transactionId
+ * and that getSampledValueTemplate handles EVSE-level and connector-level templates
+ *
+ * Covers:
+ * - buildMeterValue — resolves connectorId from transactionId for OCPP 1.6
+ * - buildMeterValue — resolves connectorId + evseId from transactionId for OCPP 2.0
+ * - buildMeterValue — throws when transactionId not found
+ * - getSampledValueTemplate — EVSE-level templates take priority over connector-level
+ * - getSampledValueTemplate — merges connector templates when no EVSE-level templates
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../../src/charging-station/index.js'
+
+import { buildMeterValue } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
+import {
+ MeterValueMeasurand,
+ OCPPVersion,
+ type SampledValueTemplate,
+} from '../../../src/types/index.js'
+import { Constants } from '../../../src/utils/index.js'
+import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import {
+ TEST_CHARGING_STATION_BASE_NAME,
+ TEST_TRANSACTION_ID,
+ TEST_TRANSACTION_ID_STRING,
+} from '../ChargingStationTestConstants.js'
+import { createMockChargingStation } from '../ChargingStationTestUtils.js'
+
+const energyTemplate: SampledValueTemplate = {
+ measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+ unit: 'Wh',
+ value: '0',
+} as unknown as SampledValueTemplate
+
+await describe('buildMeterValue', async () => {
+ let station: ChargingStation
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await describe('OCPP 1.6', async () => {
+ beforeEach(() => {
+ const { station: s } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_16 },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = s
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = [energyTemplate]
+ connectorStatus.transactionId = TEST_TRANSACTION_ID
+ }
+ })
+
+ await it('should resolve connectorId from transactionId and build meter value', () => {
+ const meterValue = buildMeterValue(station, TEST_TRANSACTION_ID, 0)
+
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.ok(Array.isArray(meterValue.sampledValue))
+ })
+
+ await it('should throw when transactionId not found', () => {
+ assert.throws(
+ () => buildMeterValue(station, 999, 0),
+ (error: Error) => {
+ assert.ok(error.message.includes('no connector found'))
+ return true
+ }
+ )
+ })
+ })
+
+ await describe('OCPP 2.0', async () => {
+ beforeEach(() => {
+ const { station: s } = createMockChargingStation({
+ baseName: TEST_CHARGING_STATION_BASE_NAME,
+ connectorsCount: 1,
+ evseConfiguration: { evsesCount: 1 },
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: { ocppVersion: OCPPVersion.VERSION_201 },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+ station = s
+ const connectorStatus = station.getConnectorStatus(1)
+ if (connectorStatus != null) {
+ connectorStatus.MeterValues = [energyTemplate]
+ connectorStatus.transactionId = TEST_TRANSACTION_ID_STRING
+ }
+ })
+
+ await it('should resolve connectorId and evseId from transactionId', () => {
+ const meterValue = buildMeterValue(station, TEST_TRANSACTION_ID_STRING, 0)
+
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.ok(Array.isArray(meterValue.sampledValue))
+ })
+
+ await it('should throw when transactionId not found', () => {
+ assert.throws(
+ () => buildMeterValue(station, 'unknown-tx', 0),
+ (error: Error) => {
+ assert.ok(error.message.includes('no connector'))
+ return true
+ }
+ )
+ })
+
+ await it('should use EVSE-level MeterValues templates when available', () => {
+ const evseStatus = station.getEvseStatus(1)
+ if (evseStatus != null) {
+ evseStatus.MeterValues = [energyTemplate]
+ // Clear connector-level templates to prove EVSE-level is used
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ connectorStatus.MeterValues = []
+ }
+ }
+
+ const meterValue = buildMeterValue(station, TEST_TRANSACTION_ID_STRING, 0)
+
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.ok(Array.isArray(meterValue.sampledValue))
+ assert.ok(
+ meterValue.sampledValue.length > 0,
+ 'should have sampled values from EVSE-level template'
+ )
+ })
+
+ await it('should merge connector templates when no EVSE-level templates', () => {
+ const evseStatus = station.getEvseStatus(1)
+ if (evseStatus != null) {
+ evseStatus.MeterValues = undefined
+ }
+
+ const meterValue = buildMeterValue(station, TEST_TRANSACTION_ID_STRING, 0)
+
+ assert.ok(meterValue.timestamp instanceof Date)
+ assert.ok(Array.isArray(meterValue.sampledValue))
+ assert.ok(
+ meterValue.sampledValue.length > 0,
+ 'should have sampled values from connector templates'
+ )
+ })
+ })
+})
waitForStreamFlush,
} from './UIServerTestUtils.js'
+// eslint-disable-next-line @typescript-eslint/no-deprecated
class TestableUIHttpServer extends UIHttpServer {
public addResponseHandler (uuid: UUIDv4, res: MockServerResponse): void {
this.responseHandlers.set(uuid, res as never)
})
await it('should create server with custom host and port', () => {
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
const serverCustom = new UIHttpServer(
createMockUIServerConfiguration({
options: {
--- /dev/null
+/**
+ * @file UIMCPServer Integration Tests
+ * @description HTTP integration tests verifying MCP endpoint responds correctly
+ */
+
+import type { AddressInfo } from 'node:net'
+
+import assert from 'node:assert/strict'
+import { writeFileSync } from 'node:fs'
+import { request as httpRequest, type Server } from 'node:http'
+import { join } from 'node:path'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import { UIMCPServer } from '../../../src/charging-station/ui-server/UIMCPServer.js'
+import { HttpMethod } from '../../../src/charging-station/ui-server/UIServerUtils.js'
+import { ApplicationProtocol } from '../../../src/types/index.js'
+import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import { createMockUIServerConfiguration } from './UIServerTestUtils.js'
+
+/**
+ * Parse SSE events from raw response body.
+ * MCP Streamable HTTP transport sends responses as SSE `event: message` frames.
+ * @param raw - Raw SSE response body string
+ * @returns Array of parsed JSON objects from SSE data lines
+ */
+const parseSseEvents = (raw: string): object[] => {
+ const events: object[] = []
+ for (const block of raw.split('\n\n')) {
+ const dataLine = block.split('\n').find(line => line.startsWith('data: '))
+ if (dataLine != null) {
+ const jsonStr = dataLine.slice('data: '.length).trim()
+ if (jsonStr.length > 0) {
+ events.push(JSON.parse(jsonStr) as object)
+ }
+ }
+ }
+ return events
+}
+
+const makeMcpPost = (port: number, body: object): Promise<{ events: object[]; status: number }> => {
+ return new Promise((resolve, reject) => {
+ const payload = JSON.stringify(body)
+ const req = httpRequest(
+ {
+ headers: {
+ Accept: 'application/json, text/event-stream',
+ 'Content-Length': Buffer.byteLength(payload),
+ 'Content-Type': 'application/json',
+ },
+ hostname: 'localhost',
+ method: HttpMethod.POST,
+ path: '/mcp',
+ port,
+ },
+ res => {
+ const chunks: Buffer[] = []
+ res.on('data', (c: Buffer) => chunks.push(c))
+ res.on('end', () => {
+ const raw = Buffer.concat(chunks).toString()
+ const contentType = res.headers['content-type'] ?? ''
+ if (contentType.includes('text/event-stream')) {
+ resolve({ events: parseSseEvents(raw), status: res.statusCode ?? 0 })
+ } else {
+ try {
+ resolve({ events: [JSON.parse(raw) as object], status: res.statusCode ?? 0 })
+ } catch {
+ reject(new Error(`Invalid response: ${raw}`))
+ }
+ }
+ })
+ }
+ )
+ req.on('error', reject)
+ req.write(payload)
+ req.end()
+ })
+}
+
+const callTool = async (
+ port: number,
+ toolName: string,
+ args: Record<string, unknown> = {}
+): Promise<{ content: { text: string; type: string }[]; isError?: boolean }> => {
+ // Initialize session
+ await makeMcpPost(port, {
+ id: 'init',
+ jsonrpc: '2.0',
+ method: 'initialize',
+ params: {
+ capabilities: {},
+ clientInfo: { name: 'test-client', version: '1.0' },
+ protocolVersion: '2025-03-26',
+ },
+ })
+ // Call tool
+ const response = await makeMcpPost(port, {
+ id: 'call',
+ jsonrpc: '2.0',
+ method: 'tools/call',
+ params: { arguments: args, name: toolName },
+ })
+ assert.strictEqual(response.status, 200)
+ assert.ok(response.events.length > 0)
+ const body = response.events[response.events.length - 1] as Record<string, unknown>
+ assert.strictEqual(body.jsonrpc, '2.0')
+ assert.strictEqual(body.id, 'call')
+ return body.result as { content: { text: string; type: string }[]; isError?: boolean }
+}
+
+await describe('UIMCPServer HTTP Integration', async () => {
+ let server: UIMCPServer
+ let testPort: number
+
+ beforeEach(async () => {
+ server = new UIMCPServer(
+ createMockUIServerConfiguration({
+ options: { host: 'localhost', port: 0 },
+ type: ApplicationProtocol.MCP,
+ })
+ )
+ server.start()
+ const httpServer = Reflect.get(server, 'httpServer') as Server
+ await new Promise<void>(resolve => {
+ if (httpServer.listening) {
+ resolve()
+ } else {
+ httpServer.on('listening', resolve)
+ }
+ })
+ testPort = (httpServer.address() as AddressInfo).port
+ })
+
+ afterEach(async () => {
+ server.stop()
+ await new Promise(resolve => {
+ setTimeout(resolve, 50)
+ })
+ standardCleanup()
+ })
+
+ await it('should respond to MCP initialize request with serverInfo and capabilities', async () => {
+ const response = await makeMcpPost(testPort, {
+ id: '1',
+ jsonrpc: '2.0',
+ method: 'initialize',
+ params: {
+ capabilities: {},
+ clientInfo: { name: 'test-client', version: '1.0' },
+ protocolVersion: '2025-03-26',
+ },
+ })
+
+ assert.strictEqual(response.status, 200)
+ assert.ok(response.events.length > 0, 'Should receive at least one SSE event')
+ const body = response.events[response.events.length - 1] as Record<string, unknown>
+ assert.strictEqual(body.jsonrpc, '2.0')
+ assert.strictEqual(body.id, '1')
+ assert.ok('result' in body, 'Response should have a result field')
+ const result = body.result as Record<string, unknown>
+ assert.ok('serverInfo' in result, 'Result should have serverInfo')
+ assert.ok('capabilities' in result, 'Result should have capabilities')
+ })
+
+ await describe('readCombinedLog tool', async () => {
+ await it('should return log content with default date (current local date)', async () => {
+ // Arrange - create a log file for today's local date
+ const now = new Date()
+ const todayDate = `${now.getFullYear().toString()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`
+ const logDir = join(process.cwd(), 'logs')
+ const logFile = join(logDir, `combined-${todayDate}.log`)
+ writeFileSync(logFile, 'info: test log line 1\ninfo: test log line 2\n', { flag: 'a' })
+
+ // Act
+ const result = await callTool(testPort, 'readCombinedLog', { tail: 10 })
+
+ // Assert
+ assert.strictEqual(result.isError, undefined)
+ assert.ok(result.content.length > 0)
+ assert.strictEqual(result.content[0].type, 'text')
+ assert.ok(result.content[0].text.includes('Showing last'))
+ })
+
+ await it('should return log content for explicit date parameter', async () => {
+ // Arrange - create a log file for a specific date
+ const logDir = join(process.cwd(), 'logs')
+ const testDate = '2020-01-01'
+ const logFile = join(logDir, `combined-${testDate}.log`)
+ writeFileSync(logFile, 'info: historical log entry\n')
+
+ // Act
+ const result = await callTool(testPort, 'readCombinedLog', { date: testDate, tail: 10 })
+
+ // Assert
+ assert.strictEqual(result.isError, undefined)
+ assert.ok(result.content.length > 0)
+ assert.strictEqual(result.content[0].type, 'text')
+ assert.ok(result.content[0].text.includes('historical log entry'))
+ })
+
+ await it('should return error for non-existent date log file', async () => {
+ const result = await callTool(testPort, 'readCombinedLog', { date: '1999-01-01', tail: 10 })
+
+ assert.strictEqual(result.isError, true)
+ assert.ok(result.content[0].text.includes('not available'))
+ })
+ })
+})
--- /dev/null
+/**
+ * @file Tests for UIMCPServer
+ * @description Unit tests for MCP-based UI server transport and Promise bridge
+ */
+
+import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
+import type { IncomingMessage } from 'node:http'
+
+import assert from 'node:assert/strict'
+import { dirname, join } from 'node:path'
+import { Readable } from 'node:stream'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+import { fileURLToPath } from 'node:url'
+
+import type { ProtocolResponse, RequestPayload, ResponsePayload } from '../../../src/types/index.js'
+
+import { mcpToolSchemas } from '../../../src/charging-station/ui-server/mcp/index.js'
+import { UIMCPServer } from '../../../src/charging-station/ui-server/UIMCPServer.js'
+import { DEFAULT_MAX_PAYLOAD_SIZE } from '../../../src/charging-station/ui-server/UIServerSecurity.js'
+import { BaseError } from '../../../src/exception/index.js'
+import {
+ ApplicationProtocol,
+ OCPPVersion,
+ ProcedureName,
+ ResponseStatus,
+} from '../../../src/types/index.js'
+import { logger } from '../../../src/utils/Logger.js'
+import {
+ createLoggerMocks,
+ standardCleanup,
+ withMockTimers,
+} from '../../helpers/TestLifecycleHelpers.js'
+import { TEST_HASH_ID, TEST_HASH_ID_2, TEST_UUID, TEST_UUID_2 } from './UIServerTestConstants.js'
+import {
+ createMockChargingStationDataWithVersion,
+ createMockUIServerConfiguration,
+} from './UIServerTestUtils.js'
+
+class TestableUIMCPServer extends UIMCPServer {
+ public callCheckVersionCompatibility (
+ hashIds: string[] | undefined,
+ ocpp16Payload: Record<string, unknown> | undefined,
+ ocpp20Payload: Record<string, unknown> | undefined,
+ procedureName: ProcedureName
+ ): CallToolResult | undefined {
+ return (
+ Reflect.get(this, 'checkVersionCompatibility') as (
+ hashIds: string[] | undefined,
+ ocpp16Payload: Record<string, unknown> | undefined,
+ ocpp20Payload: Record<string, unknown> | undefined,
+ procedureName: ProcedureName
+ ) => CallToolResult | undefined
+ ).call(this, hashIds, ocpp16Payload, ocpp20Payload, procedureName)
+ }
+
+ public callInvokeProcedure (
+ procedureName: ProcedureName,
+ input: RequestPayload,
+ service?: { requestHandler: (request: unknown) => Promise<ProtocolResponse | undefined> }
+ ): Promise<CallToolResult> {
+ return (
+ Reflect.get(this, 'invokeProcedure') as (
+ procedureName: ProcedureName,
+ input: RequestPayload,
+ service:
+ | undefined
+ | { requestHandler: (request: unknown) => Promise<ProtocolResponse | undefined> }
+ ) => Promise<CallToolResult>
+ ).call(this, procedureName, input, service)
+ }
+
+ public callLoadOcppSchemas (): Map<string, { ocpp16?: unknown; ocpp20?: unknown }> {
+ return (
+ Reflect.get(this, 'loadOcppSchemas') as () => Map<
+ string,
+ { ocpp16?: unknown; ocpp20?: unknown }
+ >
+ ).call(this)
+ }
+
+ public callReadRequestBody (req: IncomingMessage): Promise<unknown> {
+ return (
+ Reflect.get(this, 'readRequestBody') as (req: IncomingMessage) => Promise<unknown>
+ ).call(this, req)
+ }
+
+ public getPendingMcpRequest (uuid: string):
+ | undefined
+ | {
+ reject: (error: Error) => void
+ resolve: (payload: ResponsePayload) => void
+ timeout: ReturnType<typeof setTimeout>
+ } {
+ return (
+ Reflect.get(this, 'pendingMcpRequests') as Map<
+ string,
+ {
+ reject: (error: Error) => void
+ resolve: (payload: ResponsePayload) => void
+ timeout: ReturnType<typeof setTimeout>
+ }
+ >
+ ).get(uuid)
+ }
+
+ public getPendingMcpRequestsMap (): Map<
+ string,
+ {
+ reject: (error: Error) => void
+ resolve: (payload: ResponsePayload) => void
+ timeout: ReturnType<typeof setTimeout>
+ }
+ > {
+ return Reflect.get(this, 'pendingMcpRequests') as Map<
+ string,
+ {
+ reject: (error: Error) => void
+ resolve: (payload: ResponsePayload) => void
+ timeout: ReturnType<typeof setTimeout>
+ }
+ >
+ }
+
+ public getPendingMcpRequestsSize (): number {
+ return (Reflect.get(this, 'pendingMcpRequests') as Map<string, unknown>).size
+ }
+
+ protected override getSchemaBaseDir (): string {
+ return join(
+ dirname(fileURLToPath(import.meta.url)),
+ '..',
+ '..',
+ '..',
+ 'src',
+ 'assets',
+ 'json-schemas',
+ 'ocpp'
+ )
+ }
+}
+
+const createMcpServerConfig = () =>
+ createMockUIServerConfiguration({ type: ApplicationProtocol.MCP })
+
+/**
+ * Assert that a CallToolResult is an error containing the expected substring.
+ * @param result - MCP tool result to validate
+ * @param expectedSubstring - Text expected in the error message
+ */
+const assertToolError = (result: CallToolResult, expectedSubstring: string): void => {
+ assert.strictEqual(result.isError, true)
+ const text = result.content[0]
+ assert.ok('text' in text)
+ assert.ok(text.text.includes(expectedSubstring))
+}
+
+await describe('UIMCPServer', async () => {
+ let server: TestableUIMCPServer
+
+ beforeEach(() => {
+ server = new TestableUIMCPServer(createMcpServerConfig())
+ })
+
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await describe('Construction and type', async () => {
+ await it('should have uiServerType of UI MCP Server', () => {
+ assert.strictEqual(Reflect.get(server, 'uiServerType'), 'UI MCP Server')
+ })
+
+ await it('should create HTTP server', () => {
+ assert.notStrictEqual(Reflect.get(server, 'httpServer'), undefined)
+ })
+ })
+
+ await describe('Tool schema registration', async () => {
+ await it('should have a tool schema for every ProcedureName', () => {
+ assert.strictEqual(mcpToolSchemas.size, Object.keys(ProcedureName).length)
+ })
+ })
+
+ await describe('hasResponseHandler override', async () => {
+ await it('should return false when no handler registered', () => {
+ assert.strictEqual(server.hasResponseHandler(TEST_UUID), false)
+ })
+
+ await it('should return true when response handler registered via base class', () => {
+ // eslint-disable-next-line @typescript-eslint/dot-notation
+ server['responseHandlers'].set(TEST_UUID, {} as never)
+ assert.strictEqual(server.hasResponseHandler(TEST_UUID), true)
+ // eslint-disable-next-line @typescript-eslint/dot-notation
+ server['responseHandlers'].delete(TEST_UUID)
+ })
+
+ await it('should return true when uuid is in pendingMcpRequests', () => {
+ const timeout = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout,
+ })
+
+ assert.strictEqual(server.hasResponseHandler(TEST_UUID), true)
+
+ clearTimeout(timeout)
+ pendingMap.delete(TEST_UUID)
+ })
+ })
+
+ await describe('sendResponse Promise bridge', async () => {
+ await it('should resolve pending Promise when sendResponse called with matching UUID', () => {
+ let resolvedPayload: ResponsePayload | undefined
+ const timeout = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (payload: ResponsePayload) => {
+ resolvedPayload = payload
+ },
+ timeout,
+ })
+
+ const expectedPayload: ResponsePayload = { status: ResponseStatus.SUCCESS }
+ server.sendResponse([TEST_UUID, expectedPayload])
+
+ assert.ok(resolvedPayload != null, 'resolvedPayload should be defined')
+ assert.deepStrictEqual(resolvedPayload, expectedPayload)
+ })
+
+ await it('should clear timeout when resolving pending request', t => {
+ const clearTimeoutMock = t.mock.method(globalThis, 'clearTimeout')
+
+ const timeout = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout,
+ })
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ assert.ok(clearTimeoutMock.mock.calls.length > 0)
+ })
+
+ await it('should delete pending entry after resolve', () => {
+ const timeout = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout,
+ })
+
+ assert.strictEqual(server.getPendingMcpRequestsSize(), 1)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ assert.strictEqual(server.getPendingMcpRequestsSize(), 0)
+ })
+
+ await it('should log error when sendResponse called for unknown UUID', t => {
+ const { errorMock } = createLoggerMocks(t, logger)
+
+ server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
+
+ assert.strictEqual(errorMock.mock.calls.length, 1)
+ })
+ })
+
+ await describe('sendRequest warning', async () => {
+ await it('should log warning when sendRequest is called in stateless mode', t => {
+ const { warnMock } = createLoggerMocks(t, logger)
+
+ server.sendRequest([TEST_UUID, ProcedureName.LIST_CHARGING_STATIONS, {}])
+
+ assert.strictEqual(warnMock.mock.calls.length, 1)
+ })
+ })
+
+ await describe('stop cleanup', async () => {
+ await it('should reject all pending requests on stop', () => {
+ const rejectedErrors: Error[] = []
+ const timeout1 = setTimeout(() => undefined, 30000)
+ const timeout2 = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+
+ pendingMap.set(TEST_UUID, {
+ reject: (error: Error) => {
+ rejectedErrors.push(error)
+ },
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout: timeout1,
+ })
+ pendingMap.set(TEST_UUID_2, {
+ reject: (error: Error) => {
+ rejectedErrors.push(error)
+ },
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout: timeout2,
+ })
+
+ assert.strictEqual(server.getPendingMcpRequestsSize(), 2)
+
+ server.stop()
+
+ assert.strictEqual(rejectedErrors.length, 2)
+ assert.ok(rejectedErrors[0] instanceof Error)
+ assert.ok(rejectedErrors[1] instanceof Error)
+ assert.strictEqual(rejectedErrors[0].message, 'Server stopping')
+ assert.strictEqual(rejectedErrors[1].message, 'Server stopping')
+ })
+
+ await it('should clear all timeouts on stop', t => {
+ const clearTimeoutMock = t.mock.method(globalThis, 'clearTimeout')
+
+ const timeout1 = setTimeout(() => undefined, 30000)
+ const timeout2 = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout: timeout1,
+ })
+ pendingMap.set(TEST_UUID_2, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout: timeout2,
+ })
+
+ server.stop()
+
+ assert.ok(clearTimeoutMock.mock.calls.length >= 2)
+ })
+
+ await it('should clear pending map on stop', () => {
+ const timeout = setTimeout(() => undefined, 30000)
+ const pendingMap = server.getPendingMcpRequestsMap()
+
+ pendingMap.set(TEST_UUID, {
+ reject: (_error: Error) => undefined,
+ resolve: (_payload?: ResponsePayload) => undefined,
+ timeout,
+ })
+
+ assert.strictEqual(server.getPendingMcpRequestsSize(), 1)
+
+ server.stop()
+
+ assert.strictEqual(server.getPendingMcpRequestsSize(), 0)
+ })
+ })
+
+ await describe('invokeProcedure', async () => {
+ await it('should return error response when service is null', async () => {
+ const result = await server.callInvokeProcedure(
+ ProcedureName.LIST_CHARGING_STATIONS,
+ {},
+ undefined
+ )
+
+ assertToolError(result, 'UI service not available')
+ })
+
+ await it('should return error response when both ocpp16Payload and ocpp20Payload are provided', async () => {
+ const mockService = {
+ requestHandler: () => Promise.resolve(undefined),
+ }
+ const input = {
+ ocpp16Payload: { idTag: 'TAG1' },
+ ocpp20Payload: { idToken: {} },
+ } as unknown as RequestPayload
+
+ const result = await server.callInvokeProcedure(ProcedureName.AUTHORIZE, input, mockService)
+
+ assertToolError(result, 'Cannot provide both')
+ })
+
+ await it('should return error response when version compatibility check fails', async () => {
+ // Arrange - station is OCPP 2.0, but sending ocpp16Payload
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_20)
+ )
+ const mockService = {
+ requestHandler: () => Promise.resolve(undefined),
+ }
+ const input = {
+ hashIds: [TEST_HASH_ID],
+ ocpp16Payload: { idTag: 'TAG1' },
+ } as unknown as RequestPayload
+
+ // Act
+ const result = await server.callInvokeProcedure(ProcedureName.AUTHORIZE, input, mockService)
+
+ // Assert
+ assertToolError(result, TEST_HASH_ID)
+ })
+
+ await it('should resolve with direct response when service returns immediately', async () => {
+ const directPayload: ResponsePayload = {
+ hashIdsSucceeded: ['station-1'],
+ status: ResponseStatus.SUCCESS,
+ }
+ const mockService = {
+ requestHandler: (request: unknown) => {
+ const [uuid] = request as [string, string, unknown]
+ return Promise.resolve([uuid, directPayload] as ProtocolResponse)
+ },
+ }
+
+ const result = await server.callInvokeProcedure(
+ ProcedureName.LIST_CHARGING_STATIONS,
+ {},
+ mockService
+ )
+
+ assert.strictEqual(result.isError, undefined)
+ const text = result.content[0]
+ assert.ok('text' in text)
+ const parsed = JSON.parse(text.text) as ResponsePayload
+ assert.strictEqual(parsed.status, ResponseStatus.SUCCESS)
+ assert.deepStrictEqual(parsed.hashIdsSucceeded, ['station-1'])
+ })
+
+ await it('should return error response when service throws', async () => {
+ const mockService = {
+ requestHandler: () => Promise.reject(new Error('Service failure')),
+ }
+
+ const result = await server.callInvokeProcedure(
+ ProcedureName.LIST_CHARGING_STATIONS,
+ {},
+ mockService
+ )
+
+ assertToolError(result, 'Service failure')
+ })
+
+ await it('should return timeout error after MCP_TOOL_TIMEOUT_MS', async t => {
+ await withMockTimers(t, ['setTimeout'], async () => {
+ // Arrange - service returns undefined (broadcast/async) and never resolves
+ const mockService = {
+ requestHandler: () => Promise.resolve(undefined),
+ }
+
+ // Act
+ const resultPromise = server.callInvokeProcedure(
+ ProcedureName.START_CHARGING_STATION,
+ {},
+ mockService
+ )
+
+ // Allow the service.requestHandler microtask to complete
+ await Promise.resolve()
+ await Promise.resolve()
+
+ // Tick past the 30s timeout
+ t.mock.timers.tick(30_000)
+
+ const result = await resultPromise
+
+ // Assert
+ assertToolError(result, 'timed out')
+ })
+ })
+ })
+
+ await describe('checkVersionCompatibility', async () => {
+ await it('should return undefined when both payloads are undefined', () => {
+ const result = server.callCheckVersionCompatibility(
+ undefined,
+ undefined,
+ undefined,
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.strictEqual(result, undefined)
+ })
+
+ await it('should return undefined when ocpp16Payload matches 1.6 station', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_16)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ { idTag: 'TAG1' },
+ undefined,
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.strictEqual(result, undefined)
+ })
+
+ await it('should return undefined when ocpp20Payload matches 2.0 station', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_20)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ undefined,
+ { idToken: {} },
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.strictEqual(result, undefined)
+ })
+
+ await it('should return undefined when ocpp20Payload matches 2.0.1 station', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_201)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ undefined,
+ { idToken: {} },
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.strictEqual(result, undefined)
+ })
+
+ await it('should return error when ocpp16Payload sent to 2.0 station', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_20)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ { idTag: 'TAG1' },
+ undefined,
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.ok(result != null, 'Expected error result')
+ assertToolError(result, TEST_HASH_ID)
+ const text = result.content[0]
+ assert.ok('text' in text)
+ assert.ok(text.text.includes('ocpp20Payload'))
+ })
+
+ await it('should return error when ocpp20Payload sent to 1.6 station', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_16)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ undefined,
+ { idToken: {} },
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.ok(result != null, 'Expected error result')
+ assertToolError(result, TEST_HASH_ID)
+ const text = result.content[0]
+ assert.ok('text' in text)
+ assert.ok(text.text.includes('ocpp16Payload'))
+ })
+
+ await it('should check only specified hashIds when provided', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_16)
+ )
+ server.setChargingStationData(
+ TEST_HASH_ID_2,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID_2, OCPPVersion.VERSION_20)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ [TEST_HASH_ID],
+ { idTag: 'TAG1' },
+ undefined,
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.strictEqual(result, undefined)
+ })
+
+ await it('should check all stations when hashIds is undefined', () => {
+ server.setChargingStationData(
+ TEST_HASH_ID,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID, OCPPVersion.VERSION_16)
+ )
+ server.setChargingStationData(
+ TEST_HASH_ID_2,
+ createMockChargingStationDataWithVersion(TEST_HASH_ID_2, OCPPVersion.VERSION_20)
+ )
+
+ const result = server.callCheckVersionCompatibility(
+ undefined,
+ { idTag: 'TAG1' },
+ undefined,
+ ProcedureName.AUTHORIZE
+ )
+
+ assert.ok(result != null, 'Expected error result')
+ assertToolError(result, TEST_HASH_ID_2)
+ })
+ })
+
+ await describe('readRequestBody', async () => {
+ await it('should resolve with parsed JSON on valid body', async () => {
+ const expected = { jsonrpc: '2.0', method: 'tools/list' }
+ const mockReq = Readable.from([Buffer.from(JSON.stringify(expected))])
+
+ const result = await server.callReadRequestBody(mockReq as unknown as IncomingMessage)
+ assert.deepStrictEqual(result, expected)
+ })
+
+ await it('should reject with BaseError when payload too large', async () => {
+ const oversizedChunk = Buffer.alloc(DEFAULT_MAX_PAYLOAD_SIZE + 1)
+ const mockReq = Readable.from([oversizedChunk])
+
+ await assert.rejects(
+ server.callReadRequestBody(mockReq as unknown as IncomingMessage),
+ (error: Error) => {
+ assert.ok(error instanceof BaseError)
+ assert.ok(error.message.includes('Payload too large'))
+ return true
+ }
+ )
+ })
+
+ await it('should reject with error on invalid JSON', async () => {
+ const mockReq = Readable.from([Buffer.from('not valid json {{{')])
+
+ await assert.rejects(server.callReadRequestBody(mockReq as unknown as IncomingMessage))
+ })
+
+ await it('should reject with error on stream error', async () => {
+ const mockReq = new Readable({
+ read () {
+ this.destroy(new Error('Connection reset'))
+ },
+ })
+
+ await assert.rejects(
+ server.callReadRequestBody(mockReq as unknown as IncomingMessage),
+ (error: Error) => {
+ assert.strictEqual(error.message, 'Connection reset')
+ return true
+ }
+ )
+ })
+ })
+
+ await describe('loadOcppSchemas', async () => {
+ await it('should load and cache OCPP schemas from disk', () => {
+ const cache = server.callLoadOcppSchemas()
+
+ assert.ok(cache.size > 0, 'Schema cache should not be empty')
+ const authorizeSchemas = cache.get(ProcedureName.AUTHORIZE)
+ assert.ok(authorizeSchemas != null, 'Should have schemas for authorize')
+ assert.ok(authorizeSchemas.ocpp16 != null, 'Should have OCPP 1.6 schema for authorize')
+ assert.ok(authorizeSchemas.ocpp20 != null, 'Should have OCPP 2.0 schema for authorize')
+ })
+
+ await it('should only cache entries that have at least one schema loaded', () => {
+ const cache = server.callLoadOcppSchemas()
+
+ for (const [, entry] of cache) {
+ assert.ok(
+ entry.ocpp16 != null || entry.ocpp20 != null,
+ 'Cached entry should have at least one schema'
+ )
+ }
+ })
+ })
+})
--- /dev/null
+/**
+ * @file Tests for UIServerFactory
+ * @description Unit tests for UI server factory and protocol-specific server creation
+ */
+
+import assert from 'node:assert/strict'
+import { afterEach, describe, it } from 'node:test'
+
+import { UIHttpServer } from '../../../src/charging-station/ui-server/UIHttpServer.js'
+import { UIMCPServer } from '../../../src/charging-station/ui-server/UIMCPServer.js'
+import { UIServerFactory } from '../../../src/charging-station/ui-server/UIServerFactory.js'
+import { UIWebSocketServer } from '../../../src/charging-station/ui-server/UIWebSocketServer.js'
+import { ApplicationProtocol, ApplicationProtocolVersion } from '../../../src/types/index.js'
+import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
+import { createMockUIServerConfiguration } from './UIServerTestUtils.js'
+
+await describe('UIServerFactory', async () => {
+ afterEach(() => {
+ standardCleanup()
+ })
+
+ await it('should create UIHttpServer for HTTP protocol', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.HTTP })
+ const server = UIServerFactory.getUIServerImplementation(config)
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ assert.ok(server instanceof UIHttpServer)
+ server.stop()
+ })
+
+ await it('should create UIWebSocketServer for WS protocol', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.WS })
+ const server = UIServerFactory.getUIServerImplementation(config)
+ assert.ok(server instanceof UIWebSocketServer)
+ server.stop()
+ })
+
+ await it('should create UIMCPServer for MCP protocol', () => {
+ const config = createMockUIServerConfiguration({ type: ApplicationProtocol.MCP })
+ const server = UIServerFactory.getUIServerImplementation(config)
+ assert.ok(server instanceof UIMCPServer)
+ server.stop()
+ })
+
+ await it('should fall back to VERSION_11 for MCP with VERSION_20', () => {
+ const config = createMockUIServerConfiguration({
+ type: ApplicationProtocol.MCP,
+ version: ApplicationProtocolVersion.VERSION_20,
+ })
+ const server = UIServerFactory.getUIServerImplementation(config)
+ assert.strictEqual(config.version, ApplicationProtocolVersion.VERSION_11)
+ server.stop()
+ })
+
+ await it('should fall back to VERSION_11 for WS with VERSION_20', () => {
+ const config = createMockUIServerConfiguration({
+ type: ApplicationProtocol.WS,
+ version: ApplicationProtocolVersion.VERSION_20,
+ })
+ const server = UIServerFactory.getUIServerImplementation(config)
+ assert.strictEqual(config.version, ApplicationProtocolVersion.VERSION_11)
+ server.stop()
+ })
+})
UUIDv4,
} from '../../../src/types/index.js'
+import { HttpMethod } from '../../../src/charging-station/ui-server/UIServerUtils.js'
import { UIWebSocketServer } from '../../../src/charging-station/ui-server/UIWebSocketServer.js'
import {
ApplicationProtocol,
ApplicationProtocolVersion,
AuthenticationType,
+ type OCPPVersion,
ResponseStatus,
} from '../../../src/types/index.js'
import { MockWebSocket } from '../mocks/MockWebSocket.js'
): IncomingMessage => {
return {
headers: {},
- method: 'POST',
+ method: HttpMethod.POST,
url: '/ui',
...overrides,
} as IncomingMessage
setTimeout(resolve, delayMs)
})
}
+
+/**
+ * Create mock charging station data with a specific OCPP version.
+ * @param hashId - Unique identifier for the charging station
+ * @param ocppVersion - OCPP protocol version
+ * @returns ChargingStationData with the specified OCPP version
+ */
+export const createMockChargingStationDataWithVersion = (
+ hashId: string,
+ ocppVersion: OCPPVersion
+): ChargingStationData =>
+ createMockChargingStationData(hashId, {
+ stationInfo: {
+ baseName: 'test',
+ chargePointModel: 'TestModel',
+ chargePointVendor: 'TestVendor',
+ chargingStationId: hashId,
+ hashId,
+ ocppVersion,
+ templateIndex: 0,
+ templateName: 'test-template',
+ },
+ })