From c4a89082ac9187b541c083a0df58471b36388fd6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 20 Nov 2024 13:05:33 +0100 Subject: [PATCH] chore(deps-dev): apply updates MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- package.json | 8 +- pnpm-lock.yaml | 199 +- scripts/build-requirements.js | 2 +- .../AutomaticTransactionGenerator.ts | 145 +- src/charging-station/Bootstrap.ts | 535 ++- src/charging-station/ChargingStation.ts | 3790 ++++++++--------- src/charging-station/ConfigurationKeyUtils.ts | 8 +- src/charging-station/IdTagsCache.ts | 100 +- src/charging-station/SharedLRUCache.ts | 78 +- .../ChargingStationWorkerBroadcastChannel.ts | 10 +- .../WorkerBroadcastChannel.ts | 12 +- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 186 +- .../ocpp/1.6/OCPP16RequestService.ts | 72 +- .../ocpp/1.6/OCPP16ResponseService.ts | 127 +- .../ocpp/1.6/OCPP16ServiceUtils.ts | 294 +- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 28 +- .../ocpp/2.0/OCPP20RequestService.ts | 54 +- .../ocpp/2.0/OCPP20ResponseService.ts | 105 +- .../ocpp/OCPPIncomingRequestService.ts | 16 +- .../ocpp/OCPPRequestService.ts | 126 +- .../ocpp/OCPPResponseService.ts | 25 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 30 +- .../ui-server/AbstractUIServer.ts | 98 +- .../ui-server/UIHttpServer.ts | 80 +- .../ui-server/UIServerFactory.ts | 18 +- .../ui-server/UIWebSocketServer.ts | 148 +- .../ui-services/AbstractUIService.ts | 186 +- src/performance/PerformanceStatistics.ts | 186 +- src/performance/storage/JsonFileStorage.ts | 16 +- src/performance/storage/MikroOrmStorage.ts | 50 +- src/performance/storage/MongoDBStorage.ts | 38 +- src/performance/storage/Storage.ts | 22 +- src/types/AutomaticTransactionGenerator.ts | 10 +- src/types/ChargingStationConfiguration.ts | 23 +- src/types/ChargingStationInfo.ts | 28 +- src/types/ChargingStationOcppConfiguration.ts | 8 +- src/types/ChargingStationTemplate.ts | 46 +- src/types/ChargingStationWorker.ts | 50 +- src/types/ConfigurationData.ts | 116 +- src/types/Evse.ts | 8 +- src/types/JsonType.ts | 4 +- .../MeasurandPerPhaseSampledValueTemplates.ts | 10 +- src/types/Statistics.ts | 24 +- src/types/Storage.ts | 14 +- src/types/UIProtocol.ts | 54 +- src/types/WebSocket.ts | 3 +- src/types/WorkerBroadcastChannel.ts | 21 +- src/types/ocpp/1.6/ChargingProfile.ts | 44 +- src/types/ocpp/1.6/Configuration.ts | 18 +- src/types/ocpp/1.6/MeterValues.ts | 74 +- src/types/ocpp/1.6/Requests.ts | 210 +- src/types/ocpp/1.6/Responses.ts | 130 +- src/types/ocpp/1.6/Transaction.ts | 28 +- src/types/ocpp/2.0/Common.ts | 92 +- src/types/ocpp/2.0/Requests.ts | 46 +- src/types/ocpp/2.0/Responses.ts | 16 +- src/types/ocpp/2.0/Variables.ts | 96 +- src/types/ocpp/Common.ts | 8 +- src/types/ocpp/Configuration.ts | 38 +- src/types/ocpp/MessageType.ts | 3 +- src/types/ocpp/MeterValues.ts | 16 +- src/types/ocpp/Requests.ts | 72 +- src/types/ocpp/Reservation.ts | 8 +- src/types/ocpp/Responses.ts | 32 +- src/types/ocpp/Transaction.ts | 8 +- src/utils/AsyncLock.ts | 26 +- src/utils/Configuration.ts | 152 +- src/utils/Logger.ts | 2 +- src/utils/Utils.ts | 2 +- src/worker/WorkerAbstract.ts | 5 +- src/worker/WorkerConstants.ts | 2 +- src/worker/WorkerDynamicPool.ts | 32 +- src/worker/WorkerFixedPool.ts | 32 +- src/worker/WorkerSet.ts | 169 +- src/worker/WorkerTypes.ts | 54 +- tests/utils/Utils.test.ts | 8 +- ui/web/package.json | 2 +- ui/web/src/composables/UIClient.ts | 240 +- ui/web/src/types/ChargingStationType.ts | 314 +- ui/web/src/types/JsonType.ts | 2 +- ui/web/src/types/UIProtocol.ts | 62 +- 81 files changed, 4626 insertions(+), 4628 deletions(-) diff --git a/package.json b/package.json index 93c46874..0d9222d2 100644 --- a/package.json +++ b/package.json @@ -111,13 +111,13 @@ "utf-8-validate": "^6.0.5" }, "devDependencies": { - "@commitlint/cli": "^19.5.0", - "@commitlint/config-conventional": "^19.5.0", + "@commitlint/cli": "^19.6.0", + "@commitlint/config-conventional": "^19.6.0", "@cspell/eslint-plugin": "^8.16.0", "@eslint/js": "^9.15.0", "@mikro-orm/cli": "^6.4.0", "@std/expect": "npm:@jsr/std__expect@^1.0.8", - "@types/node": "^22.9.0", + "@types/node": "^22.9.1", "@types/semver": "^7.5.8", "@types/ws": "^8.5.13", "c8": "^10.1.2", @@ -129,7 +129,7 @@ "eslint": "^9.15.0", "eslint-define-config": "^2.1.0", "eslint-plugin-jsdoc": "^50.5.0", - "eslint-plugin-perfectionist": "^3.9.1", + "eslint-plugin-perfectionist": "^4.0.3", "eslint-plugin-vue": "^9.31.0", "glob": "^11.0.0", "husky": "^9.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64531661..07337066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,11 +83,11 @@ importers: version: 6.0.5 devDependencies: '@commitlint/cli': - specifier: ^19.5.0 - version: 19.5.0(@types/node@22.9.0)(typescript@5.6.3) + specifier: ^19.6.0 + version: 19.6.0(@types/node@22.9.1)(typescript@5.6.3) '@commitlint/config-conventional': - specifier: ^19.5.0 - version: 19.5.0 + specifier: ^19.6.0 + version: 19.6.0 '@cspell/eslint-plugin': specifier: ^8.16.0 version: 8.16.0(eslint@9.15.0(jiti@1.21.6)) @@ -101,8 +101,8 @@ importers: specifier: npm:@jsr/std__expect@^1.0.8 version: '@jsr/std__expect@1.0.8' '@types/node': - specifier: ^22.9.0 - version: 22.9.0 + specifier: ^22.9.1 + version: 22.9.1 '@types/semver': specifier: ^7.5.8 version: 7.5.8 @@ -137,8 +137,8 @@ importers: specifier: ^50.5.0 version: 50.5.0(eslint@9.15.0(jiti@1.21.6)) eslint-plugin-perfectionist: - specifier: ^3.9.1 - version: 3.9.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vue-eslint-parser@9.4.3(eslint@9.15.0(jiti@1.21.6))) + specifier: ^4.0.3 + version: 4.0.3(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint-plugin-vue: specifier: ^9.31.0 version: 9.31.0(eslint@9.15.0(jiti@1.21.6)) @@ -165,7 +165,7 @@ importers: version: 7.6.3 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) + version: 10.9.2(@types/node@22.9.1)(typescript@5.6.3) tsx: specifier: ^4.19.2 version: 4.19.2 @@ -198,17 +198,17 @@ importers: specifier: ^21.1.7 version: 21.1.7 '@types/node': - specifier: ^22.9.0 - version: 22.9.0 + specifier: ^22.9.1 + version: 22.9.1 '@vitejs/plugin-vue': specifier: ^5.2.0 - version: 5.2.0(vite@5.4.11(@types/node@22.9.0))(vue@3.5.13(typescript@5.6.3)) + version: 5.2.0(vite@5.4.11(@types/node@22.9.1))(vue@3.5.13(typescript@5.6.3)) '@vitejs/plugin-vue-jsx': specifier: ^4.1.0 - version: 4.1.0(vite@5.4.11(@types/node@22.9.0))(vue@3.5.13(typescript@5.6.3)) + version: 4.1.0(vite@5.4.11(@types/node@22.9.1))(vue@3.5.13(typescript@5.6.3)) '@vitest/coverage-v8': specifier: ^2.1.5 - version: 2.1.5(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5))) + version: 2.1.5(vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5))) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.6 @@ -232,10 +232,10 @@ importers: version: 5.6.3 vite: specifier: ^5.4.11 - version: 5.4.11(@types/node@22.9.0) + version: 5.4.11(@types/node@22.9.1) vitest: specifier: ^2.1.5 - version: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)) + version: 2.1.5(@types/node@22.9.1)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)) packages: @@ -398,13 +398,13 @@ packages: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@commitlint/cli@19.5.0': - resolution: {integrity: sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ==} + '@commitlint/cli@19.6.0': + resolution: {integrity: sha512-v17BgGD9w5KnthaKxXnEg6KLq6DYiAxyiN44TpiRtqyW8NSq+Kx99mkEG8Qo6uu6cI5eMzMojW2muJxjmPnF8w==} engines: {node: '>=v18'} hasBin: true - '@commitlint/config-conventional@19.5.0': - resolution: {integrity: sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg==} + '@commitlint/config-conventional@19.6.0': + resolution: {integrity: sha512-DJT40iMnTYtBtUfw9ApbsLZFke1zKh6llITVJ+x9mtpHD08gsNXaIRqHTmwTZL3dNX5+WoyK7pCN/5zswvkBCQ==} engines: {node: '>=v18'} '@commitlint/config-validator@19.5.0': @@ -423,12 +423,12 @@ packages: resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==} engines: {node: '>=v18'} - '@commitlint/is-ignored@19.5.0': - resolution: {integrity: sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w==} + '@commitlint/is-ignored@19.6.0': + resolution: {integrity: sha512-Ov6iBgxJQFR9koOupDPHvcHU9keFupDgtB3lObdEZDroiG4jj1rzky60fbQozFKVYRTUdrBGICHG0YVmRuAJmw==} engines: {node: '>=v18'} - '@commitlint/lint@19.5.0': - resolution: {integrity: sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg==} + '@commitlint/lint@19.6.0': + resolution: {integrity: sha512-LRo7zDkXtcIrpco9RnfhOKeg8PAnE3oDDoalnrVU/EVaKHYBWYL1DlRR7+3AWn0JiBqD8yKOfetVxJGdEtZ0tg==} engines: {node: '>=v18'} '@commitlint/load@19.5.0': @@ -451,8 +451,8 @@ packages: resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==} engines: {node: '>=v18'} - '@commitlint/rules@19.5.0': - resolution: {integrity: sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw==} + '@commitlint/rules@19.6.0': + resolution: {integrity: sha512-1f2reW7lbrI0X0ozZMesS/WZxgPa4/wi56vFuJENBmed6mWq5KsheN/nxqnl/C23ioxpPO/PL6tXpiiFy5Bhjw==} engines: {node: '>=v18'} '@commitlint/to-lines@19.5.0': @@ -1399,8 +1399,8 @@ packages: resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} engines: {node: '>=14.16'} - '@stylistic/eslint-plugin@2.10.1': - resolution: {integrity: sha512-U+4yzNXElTf9q0kEfnloI9XbOyD4cnEQCxjUI94q0+W++0GAEQvJ/slwEj9lwjDHfGADRSr+Tco/z0XJvmDfCQ==} + '@stylistic/eslint-plugin@2.11.0': + resolution: {integrity: sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=8.40.0' @@ -1465,8 +1465,8 @@ packages: '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/node@22.9.0': - resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + '@types/node@22.9.1': + resolution: {integrity: sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg==} '@types/offscreencanvas@2019.3.0': resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==} @@ -2016,8 +2016,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bson@6.9.0: - resolution: {integrity: sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==} + bson@6.10.0: + resolution: {integrity: sha512-ROchNosXMJD2cbQGm84KoP7vOGPO6/bOAW0veMMbzhXLqoZptcaYRVLitwvuhwhjjpU1qP4YZRWLhgETdgqUQw==} engines: {node: '>=16.20.1'} buffer-equal@0.0.1: @@ -2913,24 +2913,11 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-perfectionist@3.9.1: - resolution: {integrity: sha512-9WRzf6XaAxF4Oi5t/3TqKP5zUjERhasHmLFHin2Yw6ZAp/EP/EVA2dr3BhQrrHWCm5SzTMZf0FcjDnBkO2xFkA==} + eslint-plugin-perfectionist@4.0.3: + resolution: {integrity: sha512-CyafnreF6boy4lf1XaF72U8NbkwrfjU/mOf1y6doaDMS9zGXhUU1DSk+ZPf/rVwCf1PL1m+rhHqFs+IcB8kDmA==} engines: {node: ^18.0.0 || >=20.0.0} peerDependencies: - astro-eslint-parser: ^1.0.2 eslint: '>=8.0.0' - svelte: '>=3.0.0' - svelte-eslint-parser: ^0.41.1 - vue-eslint-parser: '>=9.0.0' - peerDependenciesMeta: - astro-eslint-parser: - optional: true - svelte: - optional: true - svelte-eslint-parser: - optional: true - vue-eslint-parser: - optional: true eslint-plugin-promise@7.1.0: resolution: {integrity: sha512-8trNmPxdAy3W620WKDpaS65NlM5yAumod6XeC4LOb+jxlkG4IVcp68c6dXY2ev+uT4U1PtG57YDV6EGAXN0GbQ==} @@ -4450,12 +4437,13 @@ packages: napi-build-utils@1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} - natural-compare-lite@1.4.0: - resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + natural-orderby@5.0.0: + resolution: {integrity: sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==} + engines: {node: '>=18'} + ndarray-blas-level1@1.1.3: resolution: {integrity: sha512-g0Qzf+W0J2S/w1GeYlGuFjGRGGE+f+u8x4O8lhBtsrCaf++n/+YLTPKk1tovYmciL3zUePmwi/szoP5oj8zQvg==} @@ -4516,8 +4504,8 @@ packages: encoding: optional: true - node-gyp-build@4.8.3: - resolution: {integrity: sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true node-gyp@8.4.1: @@ -6149,8 +6137,8 @@ packages: engines: {node: '>= 14'} hasBin: true - yaml@2.6.0: - resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} engines: {node: '>= 14'} hasBin: true @@ -6511,11 +6499,11 @@ snapshots: '@colors/colors@1.6.0': {} - '@commitlint/cli@19.5.0(@types/node@22.9.0)(typescript@5.6.3)': + '@commitlint/cli@19.6.0(@types/node@22.9.1)(typescript@5.6.3)': dependencies: '@commitlint/format': 19.5.0 - '@commitlint/lint': 19.5.0 - '@commitlint/load': 19.5.0(@types/node@22.9.0)(typescript@5.6.3) + '@commitlint/lint': 19.6.0 + '@commitlint/load': 19.5.0(@types/node@22.9.1)(typescript@5.6.3) '@commitlint/read': 19.5.0 '@commitlint/types': 19.5.0 tinyexec: 0.3.1 @@ -6524,7 +6512,7 @@ snapshots: - '@types/node' - typescript - '@commitlint/config-conventional@19.5.0': + '@commitlint/config-conventional@19.6.0': dependencies: '@commitlint/types': 19.5.0 conventional-changelog-conventionalcommits: 7.0.2 @@ -6550,19 +6538,19 @@ snapshots: '@commitlint/types': 19.5.0 chalk: 5.3.0 - '@commitlint/is-ignored@19.5.0': + '@commitlint/is-ignored@19.6.0': dependencies: '@commitlint/types': 19.5.0 semver: 7.6.3 - '@commitlint/lint@19.5.0': + '@commitlint/lint@19.6.0': dependencies: - '@commitlint/is-ignored': 19.5.0 + '@commitlint/is-ignored': 19.6.0 '@commitlint/parse': 19.5.0 - '@commitlint/rules': 19.5.0 + '@commitlint/rules': 19.6.0 '@commitlint/types': 19.5.0 - '@commitlint/load@19.5.0(@types/node@22.9.0)(typescript@5.6.3)': + '@commitlint/load@19.5.0(@types/node@22.9.1)(typescript@5.6.3)': dependencies: '@commitlint/config-validator': 19.5.0 '@commitlint/execute-rule': 19.5.0 @@ -6570,7 +6558,7 @@ snapshots: '@commitlint/types': 19.5.0 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@5.6.3) - cosmiconfig-typescript-loader: 5.1.0(@types/node@22.9.0)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + cosmiconfig-typescript-loader: 5.1.0(@types/node@22.9.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -6603,7 +6591,7 @@ snapshots: lodash.mergewith: 4.6.2 resolve-from: 5.0.0 - '@commitlint/rules@19.5.0': + '@commitlint/rules@19.6.0': dependencies: '@commitlint/ensure': 19.5.0 '@commitlint/message': 19.5.0 @@ -7350,7 +7338,7 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@stylistic/eslint-plugin@2.10.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + '@stylistic/eslint-plugin@2.11.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) @@ -7406,7 +7394,7 @@ snapshots: '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 '@types/estree@1.0.6': {} @@ -7418,7 +7406,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 '@types/tough-cookie': 4.0.5 parse5: 7.2.1 @@ -7426,7 +7414,7 @@ snapshots: '@types/long@4.0.2': {} - '@types/node@22.9.0': + '@types/node@22.9.1': dependencies: undici-types: 6.19.8 @@ -7450,7 +7438,7 @@ snapshots: '@types/ws@8.5.13': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': dependencies: @@ -7534,22 +7522,22 @@ snapshots: '@typescript-eslint/types': 8.15.0 eslint-visitor-keys: 4.2.0 - '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.11(@types/node@22.9.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue-jsx@4.1.0(vite@5.4.11(@types/node@22.9.1))(vue@3.5.13(typescript@5.6.3))': dependencies: '@babel/core': 7.26.0 '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0) '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0) - vite: 5.4.11(@types/node@22.9.0) + vite: 5.4.11(@types/node@22.9.1) vue: 3.5.13(typescript@5.6.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.9.0))(vue@3.5.13(typescript@5.6.3))': + '@vitejs/plugin-vue@5.2.0(vite@5.4.11(@types/node@22.9.1))(vue@3.5.13(typescript@5.6.3))': dependencies: - vite: 5.4.11(@types/node@22.9.0) + vite: 5.4.11(@types/node@22.9.1) vue: 3.5.13(typescript@5.6.3) - '@vitest/coverage-v8@2.1.5(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)))': + '@vitest/coverage-v8@2.1.5(vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -7563,7 +7551,7 @@ snapshots: std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)) + vitest: 2.1.5(@types/node@22.9.1)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)) transitivePeerDependencies: - supports-color @@ -7574,13 +7562,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.9.0))': + '@vitest/mocker@2.1.5(vite@5.4.11(@types/node@22.9.1))': dependencies: '@vitest/spy': 2.1.5 estree-walker: 3.0.3 magic-string: 0.30.13 optionalDependencies: - vite: 5.4.11(@types/node@22.9.0) + vite: 5.4.11(@types/node@22.9.1) '@vitest/pretty-format@2.1.5': dependencies: @@ -8152,7 +8140,7 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) - bson@6.9.0: {} + bson@6.10.0: {} buffer-equal@0.0.1: {} @@ -8172,7 +8160,7 @@ snapshots: bufferutil@4.0.8: dependencies: - node-gyp-build: 4.8.3 + node-gyp-build: 4.8.4 optional: true builtin-status-codes@3.0.0: {} @@ -8547,9 +8535,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@5.1.0(@types/node@22.9.0)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + cosmiconfig-typescript-loader@5.1.0(@types/node@22.9.1)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): dependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 cosmiconfig: 9.0.0(typescript@5.6.3) jiti: 1.21.6 typescript: 5.6.3 @@ -8622,7 +8610,7 @@ snapshots: dependencies: '@cspell/cspell-types': 8.16.0 comment-json: 4.2.5 - yaml: 2.6.0 + yaml: 2.6.1 cspell-dictionary@8.16.0: dependencies: @@ -9328,15 +9316,12 @@ snapshots: minimatch: 9.0.5 semver: 7.6.3 - eslint-plugin-perfectionist@3.9.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)(vue-eslint-parser@9.4.3(eslint@9.15.0(jiti@1.21.6))): + eslint-plugin-perfectionist@4.0.3(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): dependencies: '@typescript-eslint/types': 8.15.0 '@typescript-eslint/utils': 8.15.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) - minimatch: 9.0.5 - natural-compare-lite: 1.4.0 - optionalDependencies: - vue-eslint-parser: 9.4.3(eslint@9.15.0(jiti@1.21.6)) + natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color - typescript @@ -10692,7 +10677,7 @@ snapshots: mariadb@3.4.0: dependencies: '@types/geojson': 7946.0.14 - '@types/node': 22.9.0 + '@types/node': 22.9.1 denque: 2.1.0 iconv-lite: 0.6.3 lru-cache: 10.4.3 @@ -10903,7 +10888,7 @@ snapshots: mongodb@6.10.0(socks@2.8.3): dependencies: '@mongodb-js/saslprep': 1.1.9 - bson: 6.9.0 + bson: 6.10.0 mongodb-connection-string-url: 3.0.1 optionalDependencies: socks: 2.8.3 @@ -10954,10 +10939,10 @@ snapshots: napi-build-utils@1.0.2: {} - natural-compare-lite@1.4.0: {} - natural-compare@1.4.0: {} + natural-orderby@5.0.0: {} + ndarray-blas-level1@1.1.3: {} ndarray-cholesky-factorization@1.0.2: @@ -11005,7 +10990,7 @@ snapshots: neostandard@0.11.8(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): dependencies: '@humanwhocodes/gitignore-to-minimatch': 1.0.2 - '@stylistic/eslint-plugin': 2.10.1(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@stylistic/eslint-plugin': 2.11.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) eslint: 9.15.0(jiti@1.21.6) eslint-plugin-n: 17.13.2(eslint@9.15.0(jiti@1.21.6)) eslint-plugin-promise: 7.1.0(eslint@9.15.0(jiti@1.21.6)) @@ -11036,7 +11021,7 @@ snapshots: optionalDependencies: encoding: 0.1.13 - node-gyp-build@4.8.3: + node-gyp-build@4.8.4: optional: true node-gyp@8.4.1: @@ -12326,14 +12311,14 @@ snapshots: '@ts-morph/common': 0.25.0 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3): + ts-node@10.9.2(@types/node@22.9.1)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.9.0 + '@types/node': 22.9.1 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -12532,7 +12517,7 @@ snapshots: utf-8-validate@6.0.5: dependencies: - node-gyp-build: 4.8.3 + node-gyp-build: 4.8.4 optional: true util-deprecate@1.0.2: {} @@ -12573,13 +12558,13 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-node@2.1.5(@types/node@22.9.0): + vite-node@2.1.5(@types/node@22.9.1): dependencies: cac: 6.7.14 debug: 4.3.7 es-module-lexer: 1.5.4 pathe: 1.1.2 - vite: 5.4.11(@types/node@22.9.0) + vite: 5.4.11(@types/node@22.9.1) transitivePeerDependencies: - '@types/node' - less @@ -12591,19 +12576,19 @@ snapshots: - supports-color - terser - vite@5.4.11(@types/node@22.9.0): + vite@5.4.11(@types/node@22.9.1): dependencies: esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.27.3 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 fsevents: 2.3.3 - vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)): + vitest@2.1.5(@types/node@22.9.1)(jsdom@25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5)): dependencies: '@vitest/expect': 2.1.5 - '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.9.0)) + '@vitest/mocker': 2.1.5(vite@5.4.11(@types/node@22.9.1)) '@vitest/pretty-format': 2.1.5 '@vitest/runner': 2.1.5 '@vitest/snapshot': 2.1.5 @@ -12619,11 +12604,11 @@ snapshots: tinyexec: 0.3.1 tinypool: 1.0.2 tinyrainbow: 1.2.0 - vite: 5.4.11(@types/node@22.9.0) - vite-node: 2.1.5(@types/node@22.9.0) + vite: 5.4.11(@types/node@22.9.1) + vite-node: 2.1.5(@types/node@22.9.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 22.9.1 jsdom: 25.0.1(bufferutil@4.0.8)(utf-8-validate@6.0.5) transitivePeerDependencies: - less @@ -12865,7 +12850,7 @@ snapshots: yaml@2.5.1: {} - yaml@2.6.0: {} + yaml@2.6.1: {} yargs-parser@15.0.3: dependencies: diff --git a/scripts/build-requirements.js b/scripts/build-requirements.js index 495edb9e..ed5459f1 100644 --- a/scripts/build-requirements.js +++ b/scripts/build-requirements.js @@ -27,10 +27,10 @@ switch (runtime) { case JSRuntime.node: checkNodeVersion() break + case JSRuntime.browser: case JSRuntime.bun: case JSRuntime.deno: case JSRuntime.workerd: - case JSRuntime.browser: default: console.warn(chalk.yellow(`Unsupported '${runtime}' runtime detected`)) break diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index ece8e243..61be7d76 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -38,20 +38,12 @@ export class AutomaticTransactionGenerator { AutomaticTransactionGenerator >() - private readonly chargingStation: ChargingStation - private readonly logPrefix = (connectorId?: number): string => { - return logPrefix( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${ - connectorId != null ? ` on connector #${connectorId.toString()}` : '' - }:` - ) - } + public readonly connectorsStatus: Map + public started: boolean + private readonly chargingStation: ChargingStation private starting: boolean private stopping: boolean - public readonly connectorsStatus: Map - public started: boolean private constructor (chargingStation: ChargingStation) { this.started = false @@ -82,6 +74,67 @@ export class AutomaticTransactionGenerator { return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId) } + public start (stopAbsoluteDuration?: boolean): void { + if (!checkChargingStationState(this.chargingStation, this.logPrefix())) { + return + } + if (this.started) { + logger.warn(`${this.logPrefix()} is already started`) + return + } + if (this.starting) { + logger.warn(`${this.logPrefix()} is already starting`) + return + } + this.starting = true + this.startConnectors(stopAbsoluteDuration) + this.started = true + this.starting = false + } + + public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void { + if (!checkChargingStationState(this.chargingStation, this.logPrefix(connectorId))) { + return + } + if (!this.connectorsStatus.has(connectorId)) { + logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`) + throw new BaseError(`Connector ${connectorId.toString()} does not exist`) + } + if (this.connectorsStatus.get(connectorId)?.start === false) { + this.internalStartConnector(connectorId, stopAbsoluteDuration).catch(Constants.EMPTY_FUNCTION) + } else if (this.connectorsStatus.get(connectorId)?.start === true) { + logger.warn(`${this.logPrefix(connectorId)} is already started on connector`) + } + } + + public stop (): void { + if (!this.started) { + logger.warn(`${this.logPrefix()} is already stopped`) + return + } + if (this.stopping) { + logger.warn(`${this.logPrefix()} is already stopping`) + return + } + this.stopping = true + this.stopConnectors() + this.started = false + this.stopping = false + } + + public stopConnector (connectorId: number): void { + if (!this.connectorsStatus.has(connectorId)) { + logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`) + throw new BaseError(`Connector ${connectorId.toString()} does not exist`) + } + if (this.connectorsStatus.get(connectorId)?.start === true) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.connectorsStatus.get(connectorId)!.start = false + } else if (this.connectorsStatus.get(connectorId)?.start === false) { + logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`) + } + } + private canStartConnector (connectorId: number): boolean { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) { @@ -317,6 +370,15 @@ export class AutomaticTransactionGenerator { this.chargingStation.emit(ChargingStationEvents.updated) } + private readonly logPrefix = (connectorId?: number): string => { + return logPrefix( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${ + connectorId != null ? ` on connector #${connectorId.toString()}` : '' + }:` + ) + } + private setStartConnectorStatus ( connectorId: number, stopAbsoluteDuration = this.chargingStation.getAutomaticTransactionGeneratorConfiguration() @@ -535,65 +597,4 @@ export class AutomaticTransactionGenerator { await sleep(Constants.DEFAULT_ATG_WAIT_TIME) } } - - public start (stopAbsoluteDuration?: boolean): void { - if (!checkChargingStationState(this.chargingStation, this.logPrefix())) { - return - } - if (this.started) { - logger.warn(`${this.logPrefix()} is already started`) - return - } - if (this.starting) { - logger.warn(`${this.logPrefix()} is already starting`) - return - } - this.starting = true - this.startConnectors(stopAbsoluteDuration) - this.started = true - this.starting = false - } - - public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void { - if (!checkChargingStationState(this.chargingStation, this.logPrefix(connectorId))) { - return - } - if (!this.connectorsStatus.has(connectorId)) { - logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`) - throw new BaseError(`Connector ${connectorId.toString()} does not exist`) - } - if (this.connectorsStatus.get(connectorId)?.start === false) { - this.internalStartConnector(connectorId, stopAbsoluteDuration).catch(Constants.EMPTY_FUNCTION) - } else if (this.connectorsStatus.get(connectorId)?.start === true) { - logger.warn(`${this.logPrefix(connectorId)} is already started on connector`) - } - } - - public stop (): void { - if (!this.started) { - logger.warn(`${this.logPrefix()} is already stopped`) - return - } - if (this.stopping) { - logger.warn(`${this.logPrefix()} is already stopping`) - return - } - this.stopping = true - this.stopConnectors() - this.started = false - this.stopping = false - } - - public stopConnector (connectorId: number): void { - if (!this.connectorsStatus.has(connectorId)) { - logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`) - throw new BaseError(`Connector ${connectorId.toString()} does not exist`) - } - if (this.connectorsStatus.get(connectorId)?.start === true) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.connectorsStatus.get(connectorId)!.start = false - } else if (this.connectorsStatus.get(connectorId)?.start === false) { - logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`) - } - } } diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index e791ed01..1a016b69 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -50,21 +50,34 @@ import { UIServerFactory } from './ui-server/UIServerFactory.js' const moduleName = 'Bootstrap' +/* eslint-disable perfectionist/sort-enums */ enum exitCodes { succeeded = 0, - // eslint-disable-next-line perfectionist/sort-enums missingChargingStationsConfiguration = 1, - // eslint-disable-next-line perfectionist/sort-enums duplicateChargingStationTemplateUrls = 2, noChargingStationTemplates = 3, - // eslint-disable-next-line perfectionist/sort-enums gracefulShutdownError = 4, } +/* eslint-enable perfectionist/sort-enums */ export class Bootstrap extends EventEmitter { private static instance: Bootstrap | null = null - private readonly logPrefix = (): string => { - return logPrefix(' Bootstrap |') + public get numberOfChargingStationTemplates (): number { + return this.templateStatistics.size + } + + public get numberOfConfiguredChargingStations (): number { + return [...this.templateStatistics.values()].reduce( + (accumulator, value) => accumulator + value.configured, + 0 + ) + } + + public get numberOfProvisionedChargingStations (): number { + return [...this.templateStatistics.values()].reduce( + (accumulator, value) => accumulator + value.provisioned, + 0 + ) } private started: boolean @@ -75,76 +88,21 @@ export class Bootstrap extends EventEmitter { private readonly uiServer: AbstractUIServer private uiServerStarted: boolean private readonly version: string = version - - private readonly workerEventAdded = (data: ChargingStationData): void => { - this.uiServer.chargingStations.set(data.stationInfo.hashId, data) - logger.info( - `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - data.stationInfo.chargingStationId - } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` - ) - } - - private readonly workerEventDeleted = (data: ChargingStationData): void => { - this.uiServer.chargingStations.delete(data.stationInfo.hashId) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)! - --templateStatistics.added - templateStatistics.indexes.delete(data.stationInfo.templateIndex) - logger.info( - `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - data.stationInfo.chargingStationId - } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` - ) - } - - private readonly workerEventPerformanceStatistics = (data: Statistics): void => { - // eslint-disable-next-line @typescript-eslint/unbound-method - if (isAsyncFunction(this.storage?.storePerformanceStatistics)) { - ;( - this.storage.storePerformanceStatistics as ( - performanceStatistics: Statistics - ) => Promise - )(data).catch(Constants.EMPTY_FUNCTION) - } else { - ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)( - data - ) - } - } - - private readonly workerEventStarted = (data: ChargingStationData): void => { - this.uiServer.chargingStations.set(data.stationInfo.hashId, data) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++this.templateStatistics.get(data.stationInfo.templateName)!.started - logger.info( - `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - data.stationInfo.chargingStationId - } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))` + private workerImplementation?: WorkerAbstract + private get numberOfAddedChargingStations (): number { + return [...this.templateStatistics.values()].reduce( + (accumulator, value) => accumulator + value.added, + 0 ) } - private readonly workerEventStopped = (data: ChargingStationData): void => { - this.uiServer.chargingStations.set(data.stationInfo.hashId, data) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - --this.templateStatistics.get(data.stationInfo.templateName)!.started - logger.info( - `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - data.stationInfo.chargingStationId - } (hashId: ${data.stationInfo.hashId}) stopped (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))` + private get numberOfStartedChargingStations (): number { + return [...this.templateStatistics.values()].reduce( + (accumulator, value) => accumulator + value.started, + 0 ) } - private readonly workerEventUpdated = (data: ChargingStationData): void => { - this.uiServer.chargingStations.set(data.stationInfo.hashId, data) - } - - private workerImplementation?: WorkerAbstract - private constructor () { super() for (const signal of ['SIGINT', 'SIGQUIT', 'SIGTERM']) { @@ -176,7 +134,191 @@ export class Bootstrap extends EventEmitter { if (Bootstrap.instance === null) { Bootstrap.instance = new Bootstrap() } - return Bootstrap.instance + return Bootstrap.instance + } + + public async addChargingStation ( + index: number, + templateFile: string, + options?: ChargingStationOptions + ): Promise { + if (!this.started && !this.starting) { + throw new BaseError( + 'Cannot add charging station while the charging stations simulator is not started' + ) + } + const stationInfo = await this.workerImplementation?.addElement({ + index, + options, + templateFile: join( + dirname(fileURLToPath(import.meta.url)), + 'assets', + 'station-templates', + templateFile + ), + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))! + ++templateStatistics.added + templateStatistics.indexes.add(index) + return stationInfo + } + + public getLastIndex (templateName: string): number { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const indexes = [...this.templateStatistics.get(templateName)!.indexes] + .concat(0) + .sort((a, b) => a - b) + for (let i = 0; i < indexes.length - 1; i++) { + if (indexes[i + 1] - indexes[i] !== 1) { + return indexes[i] + } + } + return indexes[indexes.length - 1] + } + + public getPerformanceStatistics (): IterableIterator | undefined { + return this.storage?.getPerformanceStatistics() + } + + public getState (): SimulatorState { + return { + configuration: Configuration.getConfigurationData(), + started: this.started, + templateStatistics: this.templateStatistics, + version: this.version, + } + } + + public async start (): Promise { + if (!this.started) { + if (!this.starting) { + this.starting = true + this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded) + this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted) + this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted) + this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped) + this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated) + this.on( + ChargingStationWorkerMessageEvents.performanceStatistics, + this.workerEventPerformanceStatistics + ) + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isAsyncFunction(this.workerImplementation?.start)) { + await this.workerImplementation.start() + } else { + ;(this.workerImplementation?.start as () => void)() + } + const performanceStorageConfiguration = + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ) + if (performanceStorageConfiguration.enabled === true) { + this.storage = StorageFactory.getStorage( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + performanceStorageConfiguration.type!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + performanceStorageConfiguration.uri!, + this.logPrefix() + ) + await this.storage?.open() + } + if ( + !this.uiServerStarted && + Configuration.getConfigurationSection( + ConfigurationSection.uiServer + ).enabled === true + ) { + this.uiServer.start() + this.uiServerStarted = true + } + // Start ChargingStation object instance in worker thread + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) { + try { + const nbStations = stationTemplateUrl.numberOfStations + for (let index = 1; index <= nbStations; index++) { + await this.addChargingStation(index, stationTemplateUrl.file) + } + } catch (error) { + console.error( + chalk.red( + `Error at starting charging station with template file ${stationTemplateUrl.file}: ` + ), + error + ) + } + } + const workerConfiguration = Configuration.getConfigurationSection( + ConfigurationSection.worker + ) + console.info( + chalk.green( + `Charging stations simulator ${this.version} started with ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s) from ${this.numberOfChargingStationTemplates.toString()} charging station template(s) and ${ + Configuration.workerDynamicPoolInUse() + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${workerConfiguration.poolMinSize?.toString()}/` + : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }${this.workerImplementation?.size.toString()}${ + Configuration.workerPoolInUse() + ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `/${workerConfiguration.poolMaxSize?.toString()}` + : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + } worker(s) concurrently running in '${workerConfiguration.processType}' mode${ + this.workerImplementation?.maxElementsPerWorker != null + ? ` (${this.workerImplementation.maxElementsPerWorker.toString()} charging station(s) per worker)` + : '' + }` + ) + ) + Configuration.workerDynamicPoolInUse() && + console.warn( + chalk.yellow( + 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead' + ) + ) + console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info) + this.started = true + this.starting = false + } else { + console.error(chalk.red('Cannot start an already starting charging stations simulator')) + } + } else { + console.error(chalk.red('Cannot start an already started charging stations simulator')) + } + } + + public async stop (): Promise { + if (this.started) { + if (!this.stopping) { + this.stopping = true + await this.uiServer.sendInternalRequest( + this.uiServer.buildProtocolRequest( + generateUUID(), + ProcedureName.STOP_CHARGING_STATION, + Constants.EMPTY_FROZEN_OBJECT + ) + ) + try { + await this.waitChargingStationsStopped() + } catch (error) { + console.error(chalk.red('Error while waiting for charging stations to stop: '), error) + } + await this.workerImplementation?.stop() + this.removeAllListeners() + this.uiServer.clearCaches() + await this.storage?.close() + delete this.storage + this.started = false + this.stopping = false + } else { + console.error(chalk.red('Cannot stop an already stopping charging stations simulator')) + } + } else { + console.error(chalk.red('Cannot stop an already stopped charging stations simulator')) + } } private gracefulShutdown (): void { @@ -299,6 +441,10 @@ export class Bootstrap extends EventEmitter { ) } + private readonly logPrefix = (): string => { + return logPrefix(' Bootstrap |') + } + private messageHandler ( msg: ChargingStationWorkerMessage ): void { @@ -395,219 +541,70 @@ export class Bootstrap extends EventEmitter { }) } - public async addChargingStation ( - index: number, - templateFile: string, - options?: ChargingStationOptions - ): Promise { - if (!this.started && !this.starting) { - throw new BaseError( - 'Cannot add charging station while the charging stations simulator is not started' - ) - } - const stationInfo = await this.workerImplementation?.addElement({ - index, - options, - templateFile: join( - dirname(fileURLToPath(import.meta.url)), - 'assets', - 'station-templates', - templateFile - ), - }) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))! - ++templateStatistics.added - templateStatistics.indexes.add(index) - return stationInfo + private readonly workerEventAdded = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` + ) } - public getLastIndex (templateName: string): number { + private readonly workerEventDeleted = (data: ChargingStationData): void => { + this.uiServer.chargingStations.delete(data.stationInfo.hashId) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const indexes = [...this.templateStatistics.get(templateName)!.indexes] - .concat(0) - .sort((a, b) => a - b) - for (let i = 0; i < indexes.length - 1; i++) { - if (indexes[i + 1] - indexes[i] !== 1) { - return indexes[i] - } - } - return indexes[indexes.length - 1] - } - - public getPerformanceStatistics (): IterableIterator | undefined { - return this.storage?.getPerformanceStatistics() - } - - public getState (): SimulatorState { - return { - configuration: Configuration.getConfigurationData(), - started: this.started, - templateStatistics: this.templateStatistics, - version: this.version, - } - } - - public async start (): Promise { - if (!this.started) { - if (!this.starting) { - this.starting = true - this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded) - this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted) - this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted) - this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped) - this.on(ChargingStationWorkerMessageEvents.updated, this.workerEventUpdated) - this.on( - ChargingStationWorkerMessageEvents.performanceStatistics, - this.workerEventPerformanceStatistics - ) - // eslint-disable-next-line @typescript-eslint/unbound-method - if (isAsyncFunction(this.workerImplementation?.start)) { - await this.workerImplementation.start() - } else { - ;(this.workerImplementation?.start as () => void)() - } - const performanceStorageConfiguration = - Configuration.getConfigurationSection( - ConfigurationSection.performanceStorage - ) - if (performanceStorageConfiguration.enabled === true) { - this.storage = StorageFactory.getStorage( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - performanceStorageConfiguration.type!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - performanceStorageConfiguration.uri!, - this.logPrefix() - ) - await this.storage?.open() - } - if ( - !this.uiServerStarted && - Configuration.getConfigurationSection( - ConfigurationSection.uiServer - ).enabled === true - ) { - this.uiServer.start() - this.uiServerStarted = true - } - // Start ChargingStation object instance in worker thread - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const stationTemplateUrl of Configuration.getStationTemplateUrls()!) { - try { - const nbStations = stationTemplateUrl.numberOfStations - for (let index = 1; index <= nbStations; index++) { - await this.addChargingStation(index, stationTemplateUrl.file) - } - } catch (error) { - console.error( - chalk.red( - `Error at starting charging station with template file ${stationTemplateUrl.file}: ` - ), - error - ) - } - } - const workerConfiguration = Configuration.getConfigurationSection( - ConfigurationSection.worker - ) - console.info( - chalk.green( - `Charging stations simulator ${this.version} started with ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s) from ${this.numberOfChargingStationTemplates.toString()} charging station template(s) and ${ - Configuration.workerDynamicPoolInUse() - ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${workerConfiguration.poolMinSize?.toString()}/` - : '' - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }${this.workerImplementation?.size.toString()}${ - Configuration.workerPoolInUse() - ? // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `/${workerConfiguration.poolMaxSize?.toString()}` - : '' - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - } worker(s) concurrently running in '${workerConfiguration.processType}' mode${ - this.workerImplementation?.maxElementsPerWorker != null - ? ` (${this.workerImplementation.maxElementsPerWorker.toString()} charging station(s) per worker)` - : '' - }` - ) - ) - Configuration.workerDynamicPoolInUse() && - console.warn( - chalk.yellow( - 'Charging stations simulator is using dynamic pool mode. This is an experimental feature with known issues.\nPlease consider using fixed pool or worker set mode instead' - ) - ) - console.info(chalk.green('Worker set/pool information:'), this.workerImplementation?.info) - this.started = true - this.starting = false - } else { - console.error(chalk.red('Cannot start an already starting charging stations simulator')) - } - } else { - console.error(chalk.red('Cannot start an already started charging stations simulator')) - } + const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)! + --templateStatistics.added + templateStatistics.indexes.delete(data.stationInfo.templateIndex) + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` + ) } - public async stop (): Promise { - if (this.started) { - if (!this.stopping) { - this.stopping = true - await this.uiServer.sendInternalRequest( - this.uiServer.buildProtocolRequest( - generateUUID(), - ProcedureName.STOP_CHARGING_STATION, - Constants.EMPTY_FROZEN_OBJECT - ) - ) - try { - await this.waitChargingStationsStopped() - } catch (error) { - console.error(chalk.red('Error while waiting for charging stations to stop: '), error) - } - await this.workerImplementation?.stop() - this.removeAllListeners() - this.uiServer.clearCaches() - await this.storage?.close() - delete this.storage - this.started = false - this.stopping = false - } else { - console.error(chalk.red('Cannot stop an already stopping charging stations simulator')) - } + private readonly workerEventPerformanceStatistics = (data: Statistics): void => { + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isAsyncFunction(this.storage?.storePerformanceStatistics)) { + ;( + this.storage.storePerformanceStatistics as ( + performanceStatistics: Statistics + ) => Promise + )(data).catch(Constants.EMPTY_FUNCTION) } else { - console.error(chalk.red('Cannot stop an already stopped charging stations simulator')) + ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)( + data + ) } } - private get numberOfAddedChargingStations (): number { - return [...this.templateStatistics.values()].reduce( - (accumulator, value) => accumulator + value.added, - 0 - ) - } - - public get numberOfChargingStationTemplates (): number { - return this.templateStatistics.size - } - - public get numberOfConfiguredChargingStations (): number { - return [...this.templateStatistics.values()].reduce( - (accumulator, value) => accumulator + value.configured, - 0 + private readonly workerEventStarted = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++this.templateStatistics.get(data.stationInfo.templateName)!.started + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))` ) } - public get numberOfProvisionedChargingStations (): number { - return [...this.templateStatistics.values()].reduce( - (accumulator, value) => accumulator + value.provisioned, - 0 + private readonly workerEventStopped = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + --this.templateStatistics.get(data.stationInfo.templateName)!.started + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) stopped (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))` ) } - private get numberOfStartedChargingStations (): number { - return [...this.templateStatistics.values()].reduce( - (accumulator, value) => accumulator + value.started, - 0 - ) + private readonly workerEventUpdated = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) } } diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index 5757e47d..45a2f94e 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -156,22 +156,6 @@ import { import { SharedLRUCache } from './SharedLRUCache.js' export class ChargingStation extends EventEmitter { - private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration - private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel - private configurationFile!: string - private configurationFileHash!: string - private configuredSupervisionUrl!: URL - private connectorsConfigurationHash!: string - private evsesConfigurationHash!: string - private flushMessageBufferSetInterval?: NodeJS.Timeout - private readonly messageBuffer: Set - private ocppIncomingRequestService!: OCPPIncomingRequestService - private readonly sharedLRUCache: SharedLRUCache - private stopping: boolean - private templateFileHash!: string - private templateFileWatcher?: FSWatcher - private wsConnectionRetryCount: number - private wsPingSetInterval?: NodeJS.Timeout public automaticTransactionGenerator?: AutomaticTransactionGenerator public bootNotificationRequest?: BootNotificationRequest public bootNotificationResponse?: BootNotificationResponse @@ -180,25 +164,6 @@ export class ChargingStation extends EventEmitter { public heartbeatSetInterval?: NodeJS.Timeout public idTagsCache: IdTagsCache public readonly index: number - public logPrefix = (): string => { - if ( - this instanceof ChargingStation && - this.stationInfo != null && - isNotEmptyString(this.stationInfo.chargingStationId) - ) { - return logPrefix(` ${this.stationInfo.chargingStationId} |`) - } - let stationTemplate: ChargingStationTemplate | undefined - try { - stationTemplate = JSON.parse( - readFileSync(this.templateFile, 'utf8') - ) as ChargingStationTemplate - } catch { - // Ignore - } - return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`) - } - public ocppConfiguration?: ChargingStationOcppConfiguration public ocppRequestService!: OCPPRequestService public performanceStatistics?: PerformanceStatistics @@ -209,6 +174,43 @@ export class ChargingStation extends EventEmitter { public stationInfo?: ChargingStationInfo public readonly templateFile: string public wsConnection: null | WebSocket + public get hasEvses (): boolean { + return this.connectors.size === 0 && this.evses.size > 0 + } + + public get wsConnectionUrl (): URL { + const wsConnectionBaseUrlStr = `${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value + : this.configuredSupervisionUrl.href + }` + return new URL( + `${wsConnectionBaseUrlStr}${ + !wsConnectionBaseUrlStr.endsWith('/') ? '/' : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }${this.stationInfo?.chargingStationId}` + ) + } + + private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration + private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel + private configurationFile!: string + private configurationFileHash!: string + private configuredSupervisionUrl!: URL + private connectorsConfigurationHash!: string + private evsesConfigurationHash!: string + private flushMessageBufferSetInterval?: NodeJS.Timeout + private readonly messageBuffer: Set + private ocppIncomingRequestService!: OCPPIncomingRequestService + private readonly sharedLRUCache: SharedLRUCache + private stopping: boolean + private templateFileHash!: string + private templateFileWatcher?: FSWatcher + private wsConnectionRetryCount: number + private wsPingSetInterval?: NodeJS.Timeout constructor (index: number, templateFile: string, options?: ChargingStationOptions) { super() @@ -280,2204 +282,2202 @@ export class ChargingStation extends EventEmitter { } } - private add (): void { - this.emit(ChargingStationEvents.added) + public async addReservation (reservation: Reservation): Promise { + const reservationFound = this.getReservationBy('reservationId', reservation.reservationId) + if (reservationFound != null) { + await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getConnectorStatus(reservation.connectorId)!.reservation = reservation + await sendAndSetConnectorStatus( + this, + reservation.connectorId, + ConnectorStatusEnum.Reserved, + undefined, + { send: reservation.connectorId !== 0 } + ) } - private clearIntervalFlushMessageBuffer (): void { - if (this.flushMessageBufferSetInterval != null) { - clearInterval(this.flushMessageBufferSetInterval) - delete this.flushMessageBufferSetInterval - } + public bufferMessage (message: string): void { + this.messageBuffer.add(message) + this.setIntervalFlushMessageBuffer() } - private flushMessageBuffer (): void { - if (this.messageBuffer.size > 0) { - for (const message of this.messageBuffer.values()) { - let beginId: string | undefined - let commandName: RequestCommand | undefined - const [messageType] = JSON.parse(message) as ErrorResponse | OutgoingRequest | Response - const isRequest = messageType === MessageType.CALL_MESSAGE - if (isRequest) { - ;[, , commandName] = JSON.parse(message) as OutgoingRequest - beginId = PerformanceStatistics.beginMeasure(commandName) - } - this.wsConnection?.send(message, (error?: Error) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!) - if (error == null) { - logger.debug( - `${this.logPrefix()} >> Buffered ${getMessageTypeString( - messageType - )} OCPP message sent '${JSON.stringify(message)}'` - ) - this.messageBuffer.delete(message) - } else { - logger.debug( - `${this.logPrefix()} >> Buffered ${getMessageTypeString( - messageType - )} OCPP message '${JSON.stringify(message)}' send failed:`, - error - ) - } - }) - } + public closeWSConnection (): void { + if (this.isWebSocketConnectionOpened()) { + this.wsConnection?.close() + this.wsConnection = null } } - private getAmperageLimitation (): number | undefined { - if ( - isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && - getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null - ) { - return ( - convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) / - getAmperageLimitationUnitDivider(this.stationInfo) - ) + public async delete (deleteConfiguration = true): Promise { + if (this.started) { + await this.stop() } + AutomaticTransactionGenerator.deleteInstance(this) + PerformanceStatistics.deleteInstance(this.stationInfo?.hashId) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) + this.requests.clear() + this.connectors.clear() + this.evses.clear() + this.templateFileWatcher?.unref() + deleteConfiguration && rmSync(this.configurationFile, { force: true }) + this.chargingStationWorkerBroadcastChannel.unref() + this.emit(ChargingStationEvents.deleted) + this.removeAllListeners() } - private getCachedRequest ( - messageType: MessageType | undefined, - messageId: string - ): CachedRequest | undefined { - const cachedRequest = this.requests.get(messageId) - if (Array.isArray(cachedRequest)) { - return cachedRequest - } - throw new OCPPError( - ErrorType.PROTOCOL_ERROR, - `Cached request for message id '${messageId}' ${getMessageTypeString( - messageType - )} is not an array`, - undefined, - cachedRequest + public getAuthorizeRemoteTxRequests (): boolean { + const authorizeRemoteTxRequests = getConfigurationKey( + this, + StandardParametersKey.AuthorizeRemoteTxRequests ) + return authorizeRemoteTxRequests != null + ? convertToBoolean(authorizeRemoteTxRequests.value) + : false } - private getConfigurationFromFile (): ChargingStationConfiguration | undefined { - let configuration: ChargingStationConfiguration | undefined - if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) { - try { - if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) { - configuration = this.sharedLRUCache.getChargingStationConfiguration( - this.configurationFileHash - ) - } else { - const measureId = `${FileType.ChargingStationConfiguration} read` - const beginId = PerformanceStatistics.beginMeasure(measureId) - configuration = JSON.parse( - readFileSync(this.configurationFile, 'utf8') - ) as ChargingStationConfiguration - PerformanceStatistics.endMeasure(measureId, beginId) - this.sharedLRUCache.setChargingStationConfiguration(configuration) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.configurationFileHash = configuration.configurationHash! + public getAutomaticTransactionGeneratorConfiguration (): + | AutomaticTransactionGeneratorConfiguration + | undefined { + if (this.automaticTransactionGeneratorConfiguration == null) { + let automaticTransactionGeneratorConfiguration: + | AutomaticTransactionGeneratorConfiguration + | undefined + const stationTemplate = this.getTemplateFromFile() + const stationConfiguration = this.getConfigurationFromFile() + if ( + this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true && + stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && + stationConfiguration?.automaticTransactionGenerator != null + ) { + automaticTransactionGeneratorConfiguration = + stationConfiguration.automaticTransactionGenerator + } else { + automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator + } + this.automaticTransactionGeneratorConfiguration = { + ...Constants.DEFAULT_ATG_CONFIGURATION, + ...automaticTransactionGeneratorConfiguration, + } + } + return this.automaticTransactionGeneratorConfiguration + } + + public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined { + return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses + } + + public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined { + if (transactionId == null) { + return undefined + } else if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (connectorStatus.transactionId === transactionId) { + return connectorId + } + } + } + } else { + for (const connectorId of this.connectors.keys()) { + if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { + return connectorId } - } catch (error) { - handleFileException( - this.configurationFile, - FileType.ChargingStationConfiguration, - error as NodeJS.ErrnoException, - this.logPrefix() - ) } } - return configuration } - private getConfiguredSupervisionUrl (): URL { - let configuredSupervisionUrl: string | undefined - const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls() - if (isNotEmptyArray(supervisionUrls)) { - let configuredSupervisionUrlIndex: number - switch (Configuration.getSupervisionUrlDistribution()) { - case SupervisionUrlDistribution.RANDOM: - configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length) - break - case SupervisionUrlDistribution.ROUND_ROBIN: - case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY: - default: - !Object.values(SupervisionUrlDistribution).includes( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Configuration.getSupervisionUrlDistribution()! - ) && - logger.warn( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string - `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${ - SupervisionUrlDistribution.CHARGING_STATION_AFFINITY - }'` - ) - configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length - break - } - configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex] - } else if (typeof supervisionUrls === 'string') { + public getConnectorMaximumAvailablePower (connectorId: number): number { + let connectorAmperageLimitationLimit: number | undefined + const amperageLimitation = this.getAmperageLimitation() + if ( + amperageLimitation != null && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - configuredSupervisionUrl = supervisionUrls! - } - if (isNotEmptyString(configuredSupervisionUrl)) { - return new URL(configuredSupervisionUrl) + amperageLimitation < this.stationInfo!.maximumAmperage! + ) { + connectorAmperageLimitationLimit = + (this.stationInfo?.currentOutType === CurrentType.AC + ? ACElectricUtils.powerTotal( + this.getNumberOfPhases(), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.stationInfo.voltageOut!, + amperageLimitation * + (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()) + ) + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) / + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.powerDivider! } - const errorMsg = 'No supervision url(s) configured' - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider! + const chargingStationChargingProfilesLimit = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getChargingStationChargingProfilesLimit(this)! / this.powerDivider! + const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId) + return min( + Number.isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Number.isNaN(connectorAmperageLimitationLimit!) + ? Number.POSITIVE_INFINITY + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorAmperageLimitationLimit!, + Number.isNaN(chargingStationChargingProfilesLimit) + ? Number.POSITIVE_INFINITY + : chargingStationChargingProfilesLimit, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Number.isNaN(connectorChargingProfilesLimit!) + ? Number.POSITIVE_INFINITY + : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + connectorChargingProfilesLimit! + ) } - // 0 for disabling - private getConnectionTimeout (): number { - if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) { - return convertToInt( - getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ?? - Constants.DEFAULT_CONNECTION_TIMEOUT - ) + public getConnectorStatus (connectorId: number): ConnectorStatus | undefined { + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + if (evseStatus.connectors.has(connectorId)) { + return evseStatus.connectors.get(connectorId) + } + } + return undefined } - return Constants.DEFAULT_CONNECTION_TIMEOUT + return this.connectors.get(connectorId) } - private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType { - return ( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (stationInfo ?? this.stationInfo!).currentOutType ?? - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Constants.DEFAULT_STATION_INFO.currentOutType! - ) + public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number { + return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded) } - private getEnergyActiveImportRegister ( - connectorStatus: ConnectorStatus | undefined, + public getEnergyActiveImportRegisterByTransactionId ( + transactionId: number | undefined, rounded = false ): number { - if (this.stationInfo?.meteringPerTransaction === true) { - return ( - (rounded - ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null - ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue) - : undefined - : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0 - ) + return this.getEnergyActiveImportRegister( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!), + rounded + ) + } + + public getHeartbeatInterval (): number { + const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) + if (HeartbeatInterval != null) { + return secondsToMilliseconds(convertToInt(HeartbeatInterval.value)) } - return ( - (rounded - ? connectorStatus?.energyActiveImportRegisterValue != null - ? Math.round(connectorStatus.energyActiveImportRegisterValue) - : undefined - : connectorStatus?.energyActiveImportRegisterValue) ?? 0 + const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) + if (HeartBeatInterval != null) { + return secondsToMilliseconds(convertToInt(HeartBeatInterval.value)) + } + this.stationInfo?.autoRegister === false && + logger.warn( + `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${Constants.DEFAULT_HEARTBEAT_INTERVAL.toString()}` + ) + return Constants.DEFAULT_HEARTBEAT_INTERVAL + } + + public getLocalAuthListEnabled (): boolean { + const localAuthListEnabled = getConfigurationKey( + this, + StandardParametersKey.LocalAuthListEnabled ) + return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false } - private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined { + public getNumberOfConnectors (): number { + if (this.hasEvses) { + let numberOfConnectors = 0 + for (const [evseId, evseStatus] of this.evses) { + if (evseId > 0) { + numberOfConnectors += evseStatus.connectors.size + } + } + return numberOfConnectors + } + return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size + } + + public getNumberOfEvses (): number { + return this.evses.has(0) ? this.evses.size - 1 : this.evses.size + } + + public getNumberOfPhases (stationInfo?: ChargingStationInfo): number { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower! + const localStationInfo = stationInfo ?? this.stationInfo! switch (this.getCurrentOutType(stationInfo)) { case CurrentType.AC: - return ACElectricUtils.amperagePerPhaseFromPower( - this.getNumberOfPhases(stationInfo), - maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()), - this.getVoltageOut(stationInfo) - ) + return localStationInfo.numberOfPhases ?? 3 case CurrentType.DC: - return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo)) + return 0 } } - private getNumberOfReservableConnectors (): number { - let numberOfReservableConnectors = 0 + public getNumberOfRunningTransactions (): number { + let numberOfRunningTransactions = 0 if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors) + for (const [evseId, evseStatus] of this.evses) { + if (evseId === 0) { + continue + } + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus.transactionStarted === true) { + ++numberOfRunningTransactions + } + } } } else { - numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors) + for (const connectorId of this.connectors.keys()) { + if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { + ++numberOfRunningTransactions + } + } } - return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero() + return numberOfRunningTransactions } - private getNumberOfReservationsOnConnectorZero (): number { - if ( - (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) || - (!this.hasEvses && this.connectors.get(0)?.reservation != null) - ) { - return 1 + public getReservationBy ( + filterKey: ReservationKey, + value: number | string + ): Reservation | undefined { + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus.reservation?.[filterKey] === value) { + return connectorStatus.reservation + } + } + } + } else { + for (const connectorStatus of this.connectors.values()) { + if (connectorStatus.reservation?.[filterKey] === value) { + return connectorStatus.reservation + } + } } - return 0 } - private getOcppConfiguration ( - ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration - ): ChargingStationOcppConfiguration | undefined { - let ocppConfiguration: ChargingStationOcppConfiguration | undefined = - this.getOcppConfigurationFromFile(ocppPersistentConfiguration) - if (ocppConfiguration == null) { - ocppConfiguration = this.getOcppConfigurationFromTemplate() - } - return ocppConfiguration + public getReserveConnectorZeroSupported (): boolean { + return convertToBoolean( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value + ) } - private getOcppConfigurationFromFile ( - ocppPersistentConfiguration?: boolean - ): ChargingStationOcppConfiguration | undefined { - const configurationKey = this.getConfigurationFromFile()?.configurationKey - if (ocppPersistentConfiguration && Array.isArray(configurationKey)) { - return { configurationKey } + public getTransactionIdTag (transactionId: number): string | undefined { + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const connectorStatus of evseStatus.connectors.values()) { + if (connectorStatus.transactionId === transactionId) { + return connectorStatus.transactionIdTag + } + } + } + } else { + for (const connectorId of this.connectors.keys()) { + if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { + return this.getConnectorStatus(connectorId)?.transactionIdTag + } + } } - return undefined } - private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined { - return this.getTemplateFromFile()?.Configuration - } - - private getPowerDivider (): number { - let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors() - if (this.stationInfo?.powerSharedByConnectors === true) { - powerDivider = this.getNumberOfRunningTransactions() - } - return powerDivider - } - - private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo { - const stationInfoFromTemplate = this.getStationInfoFromTemplate() - options?.persistentConfiguration != null && - (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration) - const stationInfoFromFile = this.getStationInfoFromFile( - stationInfoFromTemplate.stationInfoPersistentConfiguration - ) - let stationInfo: ChargingStationInfo - // Priority: - // 1. charging station info from template - // 2. charging station info from configuration file - if ( - stationInfoFromFile != null && - stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash - ) { - stationInfo = stationInfoFromFile - } else { - stationInfo = stationInfoFromTemplate - stationInfoFromFile != null && - propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo) - } - return setChargingStationOptions( - mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo), - options - ) - } - - private getStationInfoFromFile ( - stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO - .stationInfoPersistentConfiguration - ): ChargingStationInfo | undefined { - let stationInfo: ChargingStationInfo | undefined - if (stationInfoPersistentConfiguration) { - stationInfo = this.getConfigurationFromFile()?.stationInfo - if (stationInfo != null) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - delete stationInfo.infoHash - delete (stationInfo as ChargingStationTemplate).numberOfConnectors - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (stationInfo.templateIndex == null) { - stationInfo.templateIndex = this.index - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (stationInfo.templateName == null) { - stationInfo.templateName = buildTemplateName(this.templateFile) + public hasConnector (connectorId: number): boolean { + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + if (evseStatus.connectors.has(connectorId)) { + return true } } + return false } - return stationInfo + return this.connectors.has(connectorId) } - private getStationInfoFromTemplate (): ChargingStationInfo { + public hasIdTags (): boolean { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stationTemplate = this.getTemplateFromFile()! - checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) - const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation) - warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile) - if (stationTemplate.Connectors != null) { - checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) - } - const stationInfo = stationTemplateToStationInfo(stationTemplate) - stationInfo.hashId = getHashId(this.index, stationTemplate) - stationInfo.templateIndex = this.index - stationInfo.templateName = buildTemplateName(this.templateFile) - stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate) - createSerialNumber(stationTemplate, stationInfo) - stationInfo.voltageOut = this.getVoltageOut(stationInfo) - if (isNotEmptyArray(stationTemplate.power)) { - const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length) - stationInfo.maximumPower = - stationTemplate.powerUnit === PowerUnits.KILO_WATT - ? stationTemplate.power[powerArrayRandomIndex] * 1000 - : stationTemplate.power[powerArrayRandomIndex] - } else if (typeof stationTemplate.power === 'number') { - stationInfo.maximumPower = - stationTemplate.powerUnit === PowerUnits.KILO_WATT - ? stationTemplate.power * 1000 - : stationTemplate.power - } - stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo) - if ( - isNotEmptyString(stationInfo.firmwareVersionPattern) && - isNotEmptyString(stationInfo.firmwareVersion) && - !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) - ) { - logger.warn( - `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${ - this.templateFile - } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` - ) - } - if (stationTemplate.resetTime != null) { - stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime) - } - return stationInfo + return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!)) } - private getTemplateFromFile (): ChargingStationTemplate | undefined { - let template: ChargingStationTemplate | undefined - try { - if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) { - template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash) - } else { - const measureId = `${FileType.ChargingStationTemplate} read` - const beginId = PerformanceStatistics.beginMeasure(measureId) - template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate - PerformanceStatistics.endMeasure(measureId, beginId) - template.templateHash = hash( - Constants.DEFAULT_HASH_ALGORITHM, - JSON.stringify(template), - 'hex' - ) - this.sharedLRUCache.setChargingStationTemplate(template) - this.templateFileHash = template.templateHash - } - } catch (error) { - handleFileException( - this.templateFile, - FileType.ChargingStationTemplate, - error as NodeJS.ErrnoException, - this.logPrefix() - ) - } - return template + public inAcceptedState (): boolean { + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED } - private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0! + public inPendingState (): boolean { + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING } - private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage { - return ( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (stationInfo ?? this.stationInfo!).voltageOut ?? - getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile) - ) + public inRejectedState (): boolean { + return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED } - private getWebSocketPingInterval (): number { - return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null - ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value) - : 0 + public inUnknownState (): boolean { + return this.bootNotificationResponse?.status == null } - private handleErrorMessage (errorResponse: ErrorResponse): void { - const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse - if (!this.requests.has(messageId)) { - // Error - throw new OCPPError( - ErrorType.INTERNAL_ERROR, - `Error response for unknown message id '${messageId}'`, - undefined, - { errorDetails, errorMessage, errorType } - ) - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)! - logger.debug( - `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify( - errorResponse - )}` - ) - errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails)) + public isChargingStationAvailable (): boolean { + return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative } - private async handleIncomingMessage (request: IncomingRequest): Promise { - const [messageType, messageId, commandName, commandPayload] = request - if (this.requests.has(messageId)) { - throw new OCPPError( - ErrorType.SECURITY_ERROR, - `Received message with duplicate message id '${messageId}'`, - commandName, - commandPayload - ) - } - if (this.stationInfo?.enableStatistics === true) { - this.performanceStatistics?.addRequestStatistic(commandName, messageType) - } - logger.debug( - `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify( - request - )}` - ) - // Process the message - await this.ocppIncomingRequestService.incomingRequestHandler( - this, - messageId, - commandName, - commandPayload + public isConnectorAvailable (connectorId: number): boolean { + return ( + connectorId > 0 && + this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative ) - this.emit(ChargingStationEvents.updated) } - private handleResponseMessage (response: Response): void { - const [messageType, messageId, commandPayload] = response - if (!this.requests.has(messageId)) { - // Error - throw new OCPPError( - ErrorType.INTERNAL_ERROR, - `Response for unknown message id '${messageId}'`, - undefined, - commandPayload + public isConnectorReservable ( + reservationId: number, + idTag?: string, + connectorId?: number + ): boolean { + const reservation = this.getReservationBy('reservationId', reservationId) + const reservationExists = reservation != null && !hasReservationExpired(reservation) + if (arguments.length === 1) { + return !reservationExists + } else if (arguments.length > 1) { + const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined + const userReservationExists = + userReservation != null && !hasReservationExpired(userReservation) + const notConnectorZero = connectorId == null ? true : connectorId > 0 + const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0 + return ( + !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable ) } - // Respond - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest( - messageType, - messageId - )! - logger.debug( - `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify( - response - )}` - ) - responseCallback(commandPayload, requestPayload) + return false } - private handleUnsupportedVersion (version: OCPPVersion | undefined): void { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + public isRegistered (): boolean { + return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState()) } - private initialize (options?: ChargingStationOptions): void { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stationTemplate = this.getTemplateFromFile()! - checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) - this.configurationFile = join( - dirname(this.templateFile.replace('station-templates', 'configurations')), - `${getHashId(this.index, stationTemplate)}.json` - ) - const stationConfiguration = this.getConfigurationFromFile() + public isWebSocketConnectionOpened (): boolean { + return this.wsConnection?.readyState === WebSocket.OPEN + } + + public logPrefix = (): string => { if ( - stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash && - (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null) + this instanceof ChargingStation && + this.stationInfo != null && + isNotEmptyString(this.stationInfo.chargingStationId) ) { - checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile) - this.initializeConnectorsOrEvsesFromFile(stationConfiguration) - } else { - this.initializeConnectorsOrEvsesFromTemplate(stationTemplate) + return logPrefix(` ${this.stationInfo.chargingStationId} |`) } - this.stationInfo = this.getStationInfo(options) - validateStationInfo(this) - if ( - this.stationInfo.firmwareStatus === FirmwareStatus.Installing && - isNotEmptyString(this.stationInfo.firmwareVersionPattern) && - isNotEmptyString(this.stationInfo.firmwareVersion) - ) { - const patternGroup = - this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? - this.stationInfo.firmwareVersion.split('.').length - const match = new RegExp(this.stationInfo.firmwareVersionPattern) - .exec(this.stationInfo.firmwareVersion) - ?.slice(1, patternGroup + 1) - if (match != null) { - const patchLevelIndex = match.length - 1 - match[patchLevelIndex] = ( - convertToInt(match[patchLevelIndex]) + - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo.firmwareUpgrade!.versionUpgrade!.step! - ).toString() - this.stationInfo.firmwareVersion = match.join('.') - } + let stationTemplate: ChargingStationTemplate | undefined + try { + stationTemplate = JSON.parse( + readFileSync(this.templateFile, 'utf8') + ) as ChargingStationTemplate + } catch { + // Ignore } - this.saveStationInfo() - this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() - if (this.stationInfo.enableStatistics === true) { - this.performanceStatistics = PerformanceStatistics.getInstance( - this.stationInfo.hashId, - this.stationInfo.chargingStationId, - this.configuredSupervisionUrl - ) + return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`) + } + + public openWSConnection ( + options?: WsOptions, + params?: { closeOpened?: boolean; terminateOpened?: boolean } + ): void { + options = { + handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), + ...this.stationInfo?.wsOptions, + ...options, } - const bootNotificationRequest = createBootNotificationRequest(this.stationInfo) - if (bootNotificationRequest == null) { - const errorMsg = 'Error while creating boot notification request' - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + params = { ...{ closeOpened: false, terminateOpened: false }, ...params } + if (!checkChargingStationState(this, this.logPrefix())) { + return } - this.bootNotificationRequest = bootNotificationRequest - this.powerDivider = this.getPowerDivider() - // OCPP configuration - this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration) - this.initializeOcppConfiguration() - this.initializeOcppServices() - if (this.stationInfo.autoRegister === true) { - this.bootNotificationResponse = { - currentTime: new Date(), - interval: millisecondsToSeconds(this.getHeartbeatInterval()), - status: RegistrationStatusEnumType.ACCEPTED, - } + if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) { + options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}` } - } - - private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate.Connectors == null && this.connectors.size === 0) { - const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + if (params.closeOpened) { + this.closeWSConnection() } - if (stationTemplate.Connectors?.[0] == null) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no connector id 0 configuration` - ) + if (params.terminateOpened) { + this.terminateWSConnection() } - if (stationTemplate.Connectors != null) { - const { configuredMaxConnectors, templateMaxAvailableConnectors, templateMaxConnectors } = - checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) - const connectorsConfigHash = hash( - Constants.DEFAULT_HASH_ALGORITHM, - `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`, - 'hex' - ) - const connectorsConfigChanged = - this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash - if (this.connectors.size === 0 || connectorsConfigChanged) { - connectorsConfigChanged && this.connectors.clear() - this.connectorsConfigurationHash = connectorsConfigHash - if (templateMaxConnectors > 0) { - for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) { - if ( - connectorId === 0 && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - (stationTemplate.Connectors[connectorId] == null || - !this.getUseConnectorId0(stationTemplate)) - ) { - continue - } - const templateConnectorId = - connectorId > 0 && stationTemplate.randomConnectors === true - ? randomInt(1, templateMaxAvailableConnectors) - : connectorId - const connectorStatus = stationTemplate.Connectors[templateConnectorId] - checkStationInfoConnectorStatus( - templateConnectorId, - connectorStatus, - this.logPrefix(), - this.templateFile - ) - this.connectors.set(connectorId, clone(connectorStatus)) - } - initializeConnectorsMapStatus(this.connectors, this.logPrefix()) - this.saveConnectorsStatus() - } else { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no connectors configuration defined, cannot create connectors` - ) - } - } - } else { + + if (this.isWebSocketConnectionOpened()) { logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no connectors configuration defined, using already defined connectors` + `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened` ) + return } + + logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`) + + this.wsConnection = new WebSocket( + this.wsConnectionUrl, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `ocpp${this.stationInfo?.ocppVersion}`, + options + ) + + // Handle WebSocket message + this.wsConnection.on('message', data => { + this.onMessage(data).catch(Constants.EMPTY_FUNCTION) + }) + // Handle WebSocket error + this.wsConnection.on('error', this.onError.bind(this)) + // Handle WebSocket close + this.wsConnection.on('close', this.onClose.bind(this)) + // Handle WebSocket open + this.wsConnection.on('open', () => { + this.onOpen().catch((error: unknown) => + logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error) + ) + }) + // Handle WebSocket ping + this.wsConnection.on('ping', this.onPing.bind(this)) + // Handle WebSocket pong + this.wsConnection.on('pong', this.onPong.bind(this)) } - private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { - if (configuration.connectorsStatus != null && configuration.evsesStatus == null) { - for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) { - this.connectors.set( - connectorId, - prepareConnectorStatus(clone(connectorStatus)) + public async removeReservation ( + reservation: Reservation, + reason: ReservationTerminationReason + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const connector = this.getConnectorStatus(reservation.connectorId)! + switch (reason) { + case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: + case ReservationTerminationReason.TRANSACTION_STARTED: + delete connector.reservation + break + case ReservationTerminationReason.EXPIRED: + case ReservationTerminationReason.REPLACE_EXISTING: + case ReservationTerminationReason.RESERVATION_CANCELED: + await sendAndSetConnectorStatus( + this, + reservation.connectorId, + ConnectorStatusEnum.Available, + undefined, + { send: reservation.connectorId !== 0 } ) - } - } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { - for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { - const evseStatus = clone(evseStatusConfiguration) - delete evseStatus.connectorsStatus - this.evses.set(evseId, { - ...(evseStatus as EvseStatus), - connectors: new Map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [ - connectorId, - prepareConnectorStatus(connectorStatus), - ]) - ), - }) - } - } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) { - const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) - } else { - const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + delete connector.reservation + break + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new BaseError(`Unknown reservation termination reason '${reason}'`) } } - private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate.Connectors != null && stationTemplate.Evses == null) { - this.initializeConnectorsFromTemplate(stationTemplate) - } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) { - this.initializeEvsesFromTemplate(stationTemplate) - } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) { - const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) - } else { - const errorMsg = `No connectors or evses defined in template file ${this.templateFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + public async reset (reason?: StopTransactionReason): Promise { + await this.stop(reason) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await sleep(this.stationInfo!.resetTime!) + this.initialize() + this.start() + } + + public restartHeartbeat (): void { + // Stop heartbeat + this.stopHeartbeat() + // Start heartbeat + this.startHeartbeat() + } + + public restartMeterValues (connectorId: number, interval: number): void { + this.stopMeterValues(connectorId) + this.startMeterValues(connectorId, interval) + } + + public restartWebSocketPing (): void { + // Stop WebSocket ping + this.stopWebSocketPing() + // Start WebSocket ping + this.startWebSocketPing() + } + + public saveOcppConfiguration (): void { + if (this.stationInfo?.ocppPersistentConfiguration === true) { + this.saveConfiguration() } } - private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { - if (stationTemplate.Evses == null && this.evses.size === 0) { - const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + public setSupervisionUrl (url: string): void { + if ( + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) + ) { + setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url) + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.stationInfo!.supervisionUrls = url + this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() + this.saveStationInfo() } - if (stationTemplate.Evses?.[0] == null) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no evse id 0 configuration` - ) + } + + public start (): void { + if (!this.started) { + if (!this.starting) { + this.starting = true + if (this.stationInfo?.enableStatistics === true) { + this.performanceStatistics?.start() + } + this.openWSConnection() + // Monitor charging station template file + this.templateFileWatcher = watchJsonFile( + this.templateFile, + FileType.ChargingStationTemplate, + this.logPrefix(), + undefined, + (event, filename): void => { + if (isNotEmptyString(filename) && event === 'change') { + try { + logger.debug( + `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ + this.templateFile + } file have changed, reload` + ) + this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) + // Initialize + this.initialize() + // Restart the ATG + const ATGStarted = this.automaticTransactionGenerator?.started + if (ATGStarted === true) { + this.stopAutomaticTransactionGenerator() + } + delete this.automaticTransactionGeneratorConfiguration + if ( + this.getAutomaticTransactionGeneratorConfiguration()?.enable === true && + ATGStarted === true + ) { + this.startAutomaticTransactionGenerator(undefined, true) + } + if (this.stationInfo?.enableStatistics === true) { + this.performanceStatistics?.restart() + } else { + this.performanceStatistics?.stop() + } + // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed + } catch (error) { + logger.error( + `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`, + error + ) + } + } + } + ) + this.started = true + this.emit(ChargingStationEvents.started) + this.starting = false + } else { + logger.warn(`${this.logPrefix()} Charging station is already starting...`) + } + } else { + logger.warn(`${this.logPrefix()} Charging station is already started...`) } - if (stationTemplate.Evses?.[0]?.Connectors[0] == null) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with evse id 0 with no connector id 0 configuration` - ) + } + + public startAutomaticTransactionGenerator ( + connectorIds?: number[], + stopAbsoluteDuration?: boolean + ): void { + this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this) + if (isNotEmptyArray(connectorIds)) { + for (const connectorId of connectorIds) { + this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration) + } + } else { + this.automaticTransactionGenerator?.start(stopAbsoluteDuration) } - if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used` + this.saveAutomaticTransactionGeneratorConfiguration() + this.emit(ChargingStationEvents.updated) + } + + public startHeartbeat (): void { + const heartbeatInterval = this.getHeartbeatInterval() + if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) { + this.heartbeatSetInterval = setInterval(() => { + this.ocppRequestService + .requestHandler(this, RequestCommand.HEARTBEAT) + .catch((error: unknown) => { + logger.error( + `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`, + error + ) + }) + }, heartbeatInterval) + logger.info( + `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds( + heartbeatInterval + )}` ) - } - if (stationTemplate.Evses != null) { - const evsesConfigHash = hash( - Constants.DEFAULT_HASH_ALGORITHM, - JSON.stringify(stationTemplate.Evses), - 'hex' + } else if (this.heartbeatSetInterval != null) { + logger.info( + `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds( + heartbeatInterval + )}` ) - const evsesConfigChanged = - this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash - if (this.evses.size === 0 || evsesConfigChanged) { - evsesConfigChanged && this.evses.clear() - this.evsesConfigurationHash = evsesConfigHash - const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses) - if (templateMaxEvses > 0) { - for (const evseKey in stationTemplate.Evses) { - const evseId = convertToInt(evseKey) - this.evses.set(evseId, { - availability: AvailabilityType.Operative, - connectors: buildConnectorsMap( - stationTemplate.Evses[evseKey].Connectors, - this.logPrefix(), - this.templateFile - ), - }) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix()) - } - this.saveEvsesStatus() - } else { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no evses configuration defined, cannot create evses` - ) - } - } } else { - logger.warn( - `${this.logPrefix()} Charging station information from template ${ - this.templateFile - } with no evses configuration defined, using already defined evses` + logger.error( + `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat` ) } } - private initializeOcppConfiguration (): void { - if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) { - addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0') + public startMeterValues (connectorId: number, interval: number): void { + if (connectorId === 0) { + logger.error( + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}` + ) + return } - if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) { - addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { - visible: false, - }) + const connectorStatus = this.getConnectorStatus(connectorId) + if (connectorStatus == null) { + logger.error( + `${this.logPrefix()} Trying to start MeterValues on non existing connector id + ${connectorId.toString()}` + ) + return } - if ( - this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null - ) { - addConfigurationKey( - this, - this.stationInfo.supervisionUrlOcppKey, - this.configuredSupervisionUrl.href, - { reboot: true } + if (connectorStatus.transactionStarted === false) { + logger.error( + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction started` ) + return } else if ( - this.stationInfo?.supervisionUrlOcppConfiguration === false && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null + connectorStatus.transactionStarted === true && + connectorStatus.transactionId == null ) { - deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { - save: false, - }) + logger.error( + `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id` + ) + return } - if ( - isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && - getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null - ) { - addConfigurationKey( - this, - this.stationInfo.amperageLimitationOcppKey, - // prettier-ignore - ( + if (interval > 0) { + connectorStatus.transactionSetInterval = setInterval(() => { + const meterValue = buildMeterValue( + this, + connectorId, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo) - ).toString() - ) - } - if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) { - addConfigurationKey( - this, - StandardParametersKey.SupportedFeatureProfiles, - `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}` - ) - } - addConfigurationKey( - this, - StandardParametersKey.NumberOfConnectors, - this.getNumberOfConnectors().toString(), - { readonly: true }, - { overwrite: true } - ) - if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) { - addConfigurationKey( - this, - StandardParametersKey.MeterValuesSampledData, - MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + connectorStatus.transactionId!, + interval + ) + this.ocppRequestService + .requestHandler( + this, + RequestCommand.METER_VALUES, + { + connectorId, + meterValue: [meterValue], + transactionId: connectorStatus.transactionId, + } + ) + .catch((error: unknown) => { + logger.error( + `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`, + error + ) + }) + }, interval) + } else { + logger.error( + `${this.logPrefix()} Charging station ${ + StandardParametersKey.MeterValueSampleInterval + } configuration set to ${interval.toString()}, not sending MeterValues` ) } - if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) { - const connectorsPhaseRotation: string[] = [] - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorId of evseStatus.connectors.keys()) { - connectorsPhaseRotation.push( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getPhaseRotationValue(connectorId, this.getNumberOfPhases())! - ) - } + } + + public async stop ( + reason?: StopTransactionReason, + stopTransactions = this.stationInfo?.stopTransactionsOnStopped + ): Promise { + if (this.started) { + if (!this.stopping) { + this.stopping = true + await this.stopMessageSequence(reason, stopTransactions) + this.closeWSConnection() + if (this.stationInfo?.enableStatistics === true) { + this.performanceStatistics?.stop() } + this.templateFileWatcher?.close() + delete this.bootNotificationResponse + this.started = false + this.saveConfiguration() + this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) + this.emit(ChargingStationEvents.stopped) + this.stopping = false } else { - for (const connectorId of this.connectors.keys()) { - connectorsPhaseRotation.push( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getPhaseRotationValue(connectorId, this.getNumberOfPhases())! - ) - } + logger.warn(`${this.logPrefix()} Charging station is already stopping...`) } - addConfigurationKey( - this, - StandardParametersKey.ConnectorPhaseRotation, - connectorsPhaseRotation.toString() - ) + } else { + logger.warn(`${this.logPrefix()} Charging station is already stopped...`) } - if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) { - addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true') + } + + public stopAutomaticTransactionGenerator (connectorIds?: number[]): void { + if (isNotEmptyArray(connectorIds)) { + for (const connectorId of connectorIds) { + this.automaticTransactionGenerator?.stopConnector(connectorId) + } + } else { + this.automaticTransactionGenerator?.stop() + } + this.saveAutomaticTransactionGeneratorConfiguration() + this.emit(ChargingStationEvents.updated) + } + + public stopMeterValues (connectorId: number): void { + const connectorStatus = this.getConnectorStatus(connectorId) + if (connectorStatus?.transactionSetInterval != null) { + clearInterval(connectorStatus.transactionSetInterval) } + } + + public async stopTransactionOnConnector ( + connectorId: number, + reason?: StopTransactionReason + ): Promise { + const transactionId = this.getConnectorStatus(connectorId)?.transactionId if ( - getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null && - hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true + this.stationInfo?.beginEndMeterValues === true && + this.stationInfo.ocppStrictCompliance === true && + this.stationInfo.outOfOrderEndMeterValues === false ) { - addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false') - } - if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) { - addConfigurationKey( + const transactionEndMeterValue = buildTransactionEndMeterValue( this, - StandardParametersKey.ConnectionTimeOut, - Constants.DEFAULT_CONNECTION_TIMEOUT.toString() + connectorId, + this.getEnergyActiveImportRegisterByTransactionId(transactionId) + ) + await this.ocppRequestService.requestHandler( + this, + RequestCommand.METER_VALUES, + { + connectorId, + meterValue: [transactionEndMeterValue], + transactionId, + } ) } - this.saveOcppConfiguration() + return await this.ocppRequestService.requestHandler< + Partial, + StopTransactionResponse + >(this, RequestCommand.STOP_TRANSACTION, { + meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true), + transactionId, + ...(reason != null && { reason }), + }) } - private initializeOcppServices (): void { - const ocppVersion = this.stationInfo?.ocppVersion - switch (ocppVersion) { - case OCPPVersion.VERSION_16: - this.ocppIncomingRequestService = - OCPP16IncomingRequestService.getInstance() - this.ocppRequestService = OCPP16RequestService.getInstance( - OCPP16ResponseService.getInstance() - ) - break - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: - this.ocppIncomingRequestService = - OCPP20IncomingRequestService.getInstance() - this.ocppRequestService = OCPP20RequestService.getInstance( - OCPP20ResponseService.getInstance() - ) - break - default: - this.handleUnsupportedVersion(ocppVersion) - break - } + private add (): void { + this.emit(ChargingStationEvents.added) } - private internalStopMessageSequence (): void { - // Stop WebSocket ping - this.stopWebSocketPing() - // Stop heartbeat - this.stopHeartbeat() - // Stop the ATG - if (this.automaticTransactionGenerator?.started === true) { - this.stopAutomaticTransactionGenerator() + private clearIntervalFlushMessageBuffer (): void { + if (this.flushMessageBufferSetInterval != null) { + clearInterval(this.flushMessageBufferSetInterval) + delete this.flushMessageBufferSetInterval } } - private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { - this.emit(ChargingStationEvents.disconnected) - this.emit(ChargingStationEvents.updated) - switch (code) { - // Normal close - case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS: - case WebSocketCloseEventStatusCode.CLOSE_NORMAL: - logger.info( - `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString( - code - )}' and reason '${reason.toString()}'` - ) - this.wsConnectionRetryCount = 0 - break - // Abnormal close - default: - logger.error( - `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString( - code - )}' and reason '${reason.toString()}'` - ) - this.started && - this.reconnect() - .then(() => { - this.emit(ChargingStationEvents.updated) - return undefined - }) - .catch((error: unknown) => - logger.error(`${this.logPrefix()} Error while reconnecting:`, error) + private flushMessageBuffer (): void { + if (this.messageBuffer.size > 0) { + for (const message of this.messageBuffer.values()) { + let beginId: string | undefined + let commandName: RequestCommand | undefined + const [messageType] = JSON.parse(message) as ErrorResponse | OutgoingRequest | Response + const isRequest = messageType === MessageType.CALL_MESSAGE + if (isRequest) { + ;[, , commandName] = JSON.parse(message) as OutgoingRequest + beginId = PerformanceStatistics.beginMeasure(commandName) + } + this.wsConnection?.send(message, (error?: Error) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!) + if (error == null) { + logger.debug( + `${this.logPrefix()} >> Buffered ${getMessageTypeString( + messageType + )} OCPP message sent '${JSON.stringify(message)}'` ) - break - } - } - - private onError (error: WSError): void { - this.closeWSConnection() - logger.error(`${this.logPrefix()} WebSocket error:`, error) - } - - private async onMessage (data: RawData): Promise { - let request: ErrorResponse | IncomingRequest | Response | undefined - let messageType: MessageType | undefined - let errorMsg: string - try { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - request = JSON.parse(data.toString()) as ErrorResponse | IncomingRequest | Response - if (Array.isArray(request)) { - ;[messageType] = request - // Check the type of message - switch (messageType) { - // Error Message - case MessageType.CALL_ERROR_MESSAGE: - this.handleErrorMessage(request as ErrorResponse) - break - // Incoming Message - case MessageType.CALL_MESSAGE: - await this.handleIncomingMessage(request as IncomingRequest) - break - // Response Message - case MessageType.CALL_RESULT_MESSAGE: - this.handleResponseMessage(request as Response) - break - // Unknown Message - default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - errorMsg = `Wrong message type ${messageType}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg) - } - } else { - throw new OCPPError( - ErrorType.PROTOCOL_ERROR, - 'Incoming message is not an array', - undefined, - { - request, - } - ) - } - } catch (error) { - if (!Array.isArray(request)) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error) - return - } - let commandName: IncomingRequestCommand | undefined - let requestCommandName: IncomingRequestCommand | RequestCommand | undefined - let errorCallback: ErrorCallback - const [, messageId] = request - switch (messageType) { - case MessageType.CALL_ERROR_MESSAGE: - case MessageType.CALL_RESULT_MESSAGE: - if (this.requests.has(messageId)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ;[, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)! - // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op) - errorCallback(error as OCPPError, false) + this.messageBuffer.delete(message) } else { - // Remove the request from the cache in case of error at response handling - this.requests.delete(messageId) - } - break - case MessageType.CALL_MESSAGE: - ;[, , commandName] = request as IncomingRequest - // Send error - await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName) - break - } - if (!(error instanceof OCPPError)) { - logger.warn( - `${this.logPrefix()} Error thrown at incoming OCPP command ${ - commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND - // eslint-disable-next-line @typescript-eslint/no-base-to-string - } message '${data.toString()}' handling is not an OCPPError:`, - error - ) - } - logger.error( - `${this.logPrefix()} Incoming OCPP command '${ - commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND - // eslint-disable-next-line @typescript-eslint/no-base-to-string - }' message '${data.toString()}'${ - this.requests.has(messageId) - ? ` matching cached request '${JSON.stringify( - this.getCachedRequest(messageType, messageId) - )}'` - : '' - } processing error:`, - error - ) - } - } - - private async onOpen (): Promise { - if (this.isWebSocketConnectionOpened()) { - this.emit(ChargingStationEvents.connected) - this.emit(ChargingStationEvents.updated) - logger.info( - `${this.logPrefix()} Connection to OCPP server through ${ - this.wsConnectionUrl.href - } succeeded` - ) - let registrationRetryCount = 0 - if (!this.isRegistered()) { - // Send BootNotification - do { - await this.ocppRequestService.requestHandler< - BootNotificationRequest, - BootNotificationResponse - >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { - skipBufferingOnError: true, - }) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.bootNotificationResponse!.currentTime = convertToDate( - this.bootNotificationResponse?.currentTime - )! - if (!this.isRegistered()) { - this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount - await sleep( - this.bootNotificationResponse?.interval != null - ? secondsToMilliseconds(this.bootNotificationResponse.interval) - : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL + logger.debug( + `${this.logPrefix()} >> Buffered ${getMessageTypeString( + messageType + )} OCPP message '${JSON.stringify(message)}' send failed:`, + error ) } - } while ( - !this.isRegistered() && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! || - this.stationInfo?.registrationMaxRetries === -1) - ) - } - if (!this.isRegistered()) { - logger.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount.toString()}) or retry disabled (${this.stationInfo?.registrationMaxRetries?.toString()})` - ) + }) } - this.emit(ChargingStationEvents.updated) - } else { - logger.warn( - `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed` - ) } } - private onPing (): void { - logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`) - } - - private onPong (): void { - logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`) - } - - private async reconnect (): Promise { + private getAmperageLimitation (): number | undefined { if ( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! || - this.stationInfo?.autoReconnectMaxRetries === -1 + isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && + getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null ) { - ++this.wsConnectionRetryCount - const reconnectDelay = - this.stationInfo?.reconnectExponentialDelay === true - ? exponentialDelay(this.wsConnectionRetryCount) - : secondsToMilliseconds(this.getConnectionTimeout()) - const reconnectDelayWithdraw = 1000 - const reconnectTimeout = - reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0 - logger.error( - `${this.logPrefix()} WebSocket connection retry in ${roundTo( - reconnectDelay, - 2 - ).toString()}ms, timeout ${reconnectTimeout.toString()}ms` - ) - await sleep(reconnectDelay) - logger.error( - `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}` - ) - this.openWSConnection( - { - handshakeTimeout: reconnectTimeout, - }, - { closeOpened: true } - ) - } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) { - logger.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})` + return ( + convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) / + getAmperageLimitationUnitDivider(this.stationInfo) ) } } - private saveAutomaticTransactionGeneratorConfiguration (): void { - if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) { - this.saveConfiguration() + private getCachedRequest ( + messageType: MessageType | undefined, + messageId: string + ): CachedRequest | undefined { + const cachedRequest = this.requests.get(messageId) + if (Array.isArray(cachedRequest)) { + return cachedRequest } + throw new OCPPError( + ErrorType.PROTOCOL_ERROR, + `Cached request for message id '${messageId}' ${getMessageTypeString( + messageType + )} is not an array`, + undefined, + cachedRequest + ) } - private saveConfiguration (): void { - if (isNotEmptyString(this.configurationFile)) { + private getConfigurationFromFile (): ChargingStationConfiguration | undefined { + let configuration: ChargingStationConfiguration | undefined + if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) { try { - if (!existsSync(dirname(this.configurationFile))) { - mkdirSync(dirname(this.configurationFile), { recursive: true }) - } - const configurationFromFile = this.getConfigurationFromFile() - let configurationData: ChargingStationConfiguration = - configurationFromFile != null - ? clone(configurationFromFile) - : {} - if (this.stationInfo?.stationInfoPersistentConfiguration === true) { - configurationData.stationInfo = this.stationInfo - } else { - delete configurationData.stationInfo - } - if ( - this.stationInfo?.ocppPersistentConfiguration === true && - Array.isArray(this.ocppConfiguration?.configurationKey) - ) { - configurationData.configurationKey = this.ocppConfiguration.configurationKey + if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) { + configuration = this.sharedLRUCache.getChargingStationConfiguration( + this.configurationFileHash + ) } else { - delete configurationData.configurationKey + const measureId = `${FileType.ChargingStationConfiguration} read` + const beginId = PerformanceStatistics.beginMeasure(measureId) + configuration = JSON.parse( + readFileSync(this.configurationFile, 'utf8') + ) as ChargingStationConfiguration + PerformanceStatistics.endMeasure(measureId, beginId) + this.sharedLRUCache.setChargingStationConfiguration(configuration) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.configurationFileHash = configuration.configurationHash! } - configurationData = mergeDeepRight( - configurationData, - buildChargingStationAutomaticTransactionGeneratorConfiguration(this) + } catch (error) { + handleFileException( + this.configurationFile, + FileType.ChargingStationConfiguration, + error as NodeJS.ErrnoException, + this.logPrefix() ) - if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) { - delete configurationData.automaticTransactionGenerator - } - if (this.connectors.size > 0) { - configurationData.connectorsStatus = buildConnectorsStatus(this) - } else { - delete configurationData.connectorsStatus - } - if (this.evses.size > 0) { - configurationData.evsesStatus = buildEvsesStatus(this) - } else { - delete configurationData.evsesStatus - } - delete configurationData.configurationHash - const configurationHash = hash( - Constants.DEFAULT_HASH_ALGORITHM, - JSON.stringify({ - automaticTransactionGenerator: configurationData.automaticTransactionGenerator, - configurationKey: configurationData.configurationKey, - stationInfo: configurationData.stationInfo, - ...(this.connectors.size > 0 && { - connectorsStatus: configurationData.connectorsStatus, - }), - ...(this.evses.size > 0 && { - evsesStatus: configurationData.evsesStatus, - }), - } satisfies ChargingStationConfiguration), - 'hex' - ) - if (this.configurationFileHash !== configurationHash) { - AsyncLock.runExclusive(AsyncLockType.configuration, () => { - configurationData.configurationHash = configurationHash - const measureId = `${FileType.ChargingStationConfiguration} write` - const beginId = PerformanceStatistics.beginMeasure(measureId) - writeFileSync( - this.configurationFile, - JSON.stringify(configurationData, undefined, 2), - 'utf8' - ) - PerformanceStatistics.endMeasure(measureId, beginId) - this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) - this.sharedLRUCache.setChargingStationConfiguration(configurationData) - this.configurationFileHash = configurationHash - }).catch((error: unknown) => { - handleFileException( - this.configurationFile, - FileType.ChargingStationConfiguration, - error as NodeJS.ErrnoException, - this.logPrefix() + } + } + return configuration + } + + private getConfiguredSupervisionUrl (): URL { + let configuredSupervisionUrl: string | undefined + const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls() + if (isNotEmptyArray(supervisionUrls)) { + let configuredSupervisionUrlIndex: number + switch (Configuration.getSupervisionUrlDistribution()) { + case SupervisionUrlDistribution.RANDOM: + configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length) + break + case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY: + case SupervisionUrlDistribution.ROUND_ROBIN: + default: + !Object.values(SupervisionUrlDistribution).includes( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Configuration.getSupervisionUrlDistribution()! + ) && + logger.warn( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string + `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${ + SupervisionUrlDistribution.CHARGING_STATION_AFFINITY + }'` ) - }) - } else { - logger.debug( - `${this.logPrefix()} Not saving unchanged charging station configuration file ${ - this.configurationFile - }` - ) - } - } catch (error) { - handleFileException( - this.configurationFile, - FileType.ChargingStationConfiguration, - error as NodeJS.ErrnoException, - this.logPrefix() - ) + configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length + break } - } else { - logger.error( - `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file` - ) + configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex] + } else if (typeof supervisionUrls === 'string') { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + configuredSupervisionUrl = supervisionUrls! + } + if (isNotEmptyString(configuredSupervisionUrl)) { + return new URL(configuredSupervisionUrl) } + const errorMsg = 'No supervision url(s) configured' + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) } - private saveConnectorsStatus (): void { - this.saveConfiguration() + // 0 for disabling + private getConnectionTimeout (): number { + if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) { + return convertToInt( + getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ?? + Constants.DEFAULT_CONNECTION_TIMEOUT + ) + } + return Constants.DEFAULT_CONNECTION_TIMEOUT } - private saveEvsesStatus (): void { - this.saveConfiguration() + private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType { + return ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (stationInfo ?? this.stationInfo!).currentOutType ?? + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Constants.DEFAULT_STATION_INFO.currentOutType! + ) } - private saveStationInfo (): void { - if (this.stationInfo?.stationInfoPersistentConfiguration === true) { - this.saveConfiguration() + private getEnergyActiveImportRegister ( + connectorStatus: ConnectorStatus | undefined, + rounded = false + ): number { + if (this.stationInfo?.meteringPerTransaction === true) { + return ( + (rounded + ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null + ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue) + : undefined + : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0 + ) } + return ( + (rounded + ? connectorStatus?.energyActiveImportRegisterValue != null + ? Math.round(connectorStatus.energyActiveImportRegisterValue) + : undefined + : connectorStatus?.energyActiveImportRegisterValue) ?? 0 + ) } - private setIntervalFlushMessageBuffer (): void { - if (this.flushMessageBufferSetInterval == null) { - this.flushMessageBufferSetInterval = setInterval(() => { - if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) { - this.flushMessageBuffer() - } - if (this.messageBuffer.size === 0) { - this.clearIntervalFlushMessageBuffer() - } - }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL) + private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower! + switch (this.getCurrentOutType(stationInfo)) { + case CurrentType.AC: + return ACElectricUtils.amperagePerPhaseFromPower( + this.getNumberOfPhases(stationInfo), + maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()), + this.getVoltageOut(stationInfo) + ) + case CurrentType.DC: + return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo)) } } - private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise { - if (this.stationInfo?.autoRegister === true) { - await this.ocppRequestService.requestHandler< - BootNotificationRequest, - BootNotificationResponse - >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { - skipBufferingOnError: true, - }) - } - // Start WebSocket ping - if (this.wsPingSetInterval == null) { - this.startWebSocketPing() - } - // Start heartbeat - if (this.heartbeatSetInterval == null) { - this.startHeartbeat() - } - // Initialize connectors status + private getNumberOfReservableConnectors (): number { + let numberOfReservableConnectors = 0 if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - await sendAndSetConnectorStatus( - this, - connectorId, - getBootConnectorStatus(this, connectorId, connectorStatus), - evseId - ) - } - } + for (const evseStatus of this.evses.values()) { + numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors) } } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0) { - await sendAndSetConnectorStatus( - this, - connectorId, - getBootConnectorStatus( - this, - connectorId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(connectorId)! - ) - ) - } - } + numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors) } - if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { - await this.ocppRequestService.requestHandler< - FirmwareStatusNotificationRequest, - FirmwareStatusNotificationResponse - >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { - status: FirmwareStatus.Installed, - }) - this.stationInfo.firmwareStatus = FirmwareStatus.Installed + return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero() + } + + private getNumberOfReservationsOnConnectorZero (): number { + if ( + (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) || + (!this.hasEvses && this.connectors.get(0)?.reservation != null) + ) { + return 1 } + return 0 + } - // Start the ATG - if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { - this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration) + private getOcppConfiguration ( + ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration + ): ChargingStationOcppConfiguration | undefined { + let ocppConfiguration: ChargingStationOcppConfiguration | undefined = + this.getOcppConfigurationFromFile(ocppPersistentConfiguration) + if (ocppConfiguration == null) { + ocppConfiguration = this.getOcppConfigurationFromTemplate() } - this.flushMessageBuffer() + return ocppConfiguration } - private startWebSocketPing (): void { - const webSocketPingInterval = this.getWebSocketPingInterval() - if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) { - this.wsPingSetInterval = setInterval(() => { - if (this.isWebSocketConnectionOpened()) { - this.wsConnection?.ping() - } - }, secondsToMilliseconds(webSocketPingInterval)) - logger.info( - `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds( - webSocketPingInterval - )}` - ) - } else if (this.wsPingSetInterval != null) { - logger.info( - `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds( - webSocketPingInterval - )}` - ) - } else { - logger.error( - `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping` - ) + private getOcppConfigurationFromFile ( + ocppPersistentConfiguration?: boolean + ): ChargingStationOcppConfiguration | undefined { + const configurationKey = this.getConfigurationFromFile()?.configurationKey + if (ocppPersistentConfiguration && Array.isArray(configurationKey)) { + return { configurationKey } } + return undefined } - private stopHeartbeat (): void { - if (this.heartbeatSetInterval != null) { - clearInterval(this.heartbeatSetInterval) - delete this.heartbeatSetInterval + private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined { + return this.getTemplateFromFile()?.Configuration + } + + private getPowerDivider (): number { + let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors() + if (this.stationInfo?.powerSharedByConnectors === true) { + powerDivider = this.getNumberOfRunningTransactions() } + return powerDivider } - private async stopMessageSequence ( - reason?: StopTransactionReason, - stopTransactions?: boolean - ): Promise { - this.internalStopMessageSequence() - // Stop ongoing transactions - stopTransactions && (await this.stopRunningTransactions(reason)) - if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId > 0) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - await sendAndSetConnectorStatus( - this, - connectorId, - ConnectorStatusEnum.Unavailable, - evseId - ) - delete connectorStatus.status - } - } - } + private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo { + const stationInfoFromTemplate = this.getStationInfoFromTemplate() + options?.persistentConfiguration != null && + (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration) + const stationInfoFromFile = this.getStationInfoFromFile( + stationInfoFromTemplate.stationInfoPersistentConfiguration + ) + let stationInfo: ChargingStationInfo + // Priority: + // 1. charging station info from template + // 2. charging station info from configuration file + if ( + stationInfoFromFile != null && + stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash + ) { + stationInfo = stationInfoFromFile } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0) { - await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable) - delete this.getConnectorStatus(connectorId)?.status - } - } + stationInfo = stationInfoFromTemplate + stationInfoFromFile != null && + propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo) } + return setChargingStationOptions( + mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo), + options + ) } - private async stopRunningTransactions (reason?: StopTransactionReason): Promise { - if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId === 0) { - continue - } - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionStarted === true) { - await this.stopTransactionOnConnector(connectorId, reason) - } + private getStationInfoFromFile ( + stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO + .stationInfoPersistentConfiguration + ): ChargingStationInfo | undefined { + let stationInfo: ChargingStationInfo | undefined + if (stationInfoPersistentConfiguration) { + stationInfo = this.getConfigurationFromFile()?.stationInfo + if (stationInfo != null) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + delete stationInfo.infoHash + delete (stationInfo as ChargingStationTemplate).numberOfConnectors + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (stationInfo.templateIndex == null) { + stationInfo.templateIndex = this.index } - } - } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { - await this.stopTransactionOnConnector(connectorId, reason) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (stationInfo.templateName == null) { + stationInfo.templateName = buildTemplateName(this.templateFile) } } } + return stationInfo } - private stopWebSocketPing (): void { - if (this.wsPingSetInterval != null) { - clearInterval(this.wsPingSetInterval) - delete this.wsPingSetInterval + private getStationInfoFromTemplate (): ChargingStationInfo { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const stationTemplate = this.getTemplateFromFile()! + checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) + const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation) + warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile) + if (stationTemplate.Connectors != null) { + checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) + } + const stationInfo = stationTemplateToStationInfo(stationTemplate) + stationInfo.hashId = getHashId(this.index, stationTemplate) + stationInfo.templateIndex = this.index + stationInfo.templateName = buildTemplateName(this.templateFile) + stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate) + createSerialNumber(stationTemplate, stationInfo) + stationInfo.voltageOut = this.getVoltageOut(stationInfo) + if (isNotEmptyArray(stationTemplate.power)) { + const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length) + stationInfo.maximumPower = + stationTemplate.powerUnit === PowerUnits.KILO_WATT + ? stationTemplate.power[powerArrayRandomIndex] * 1000 + : stationTemplate.power[powerArrayRandomIndex] + } else if (typeof stationTemplate.power === 'number') { + stationInfo.maximumPower = + stationTemplate.powerUnit === PowerUnits.KILO_WATT + ? stationTemplate.power * 1000 + : stationTemplate.power + } + stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo) + if ( + isNotEmptyString(stationInfo.firmwareVersionPattern) && + isNotEmptyString(stationInfo.firmwareVersion) && + !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) + ) { + logger.warn( + `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${ + this.templateFile + } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'` + ) + } + if (stationTemplate.resetTime != null) { + stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime) } + return stationInfo } - private terminateWSConnection (): void { - if (this.isWebSocketConnectionOpened()) { - this.wsConnection?.terminate() - this.wsConnection = null + private getTemplateFromFile (): ChargingStationTemplate | undefined { + let template: ChargingStationTemplate | undefined + try { + if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) { + template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash) + } else { + const measureId = `${FileType.ChargingStationTemplate} read` + const beginId = PerformanceStatistics.beginMeasure(measureId) + template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate + PerformanceStatistics.endMeasure(measureId, beginId) + template.templateHash = hash( + Constants.DEFAULT_HASH_ALGORITHM, + JSON.stringify(template), + 'hex' + ) + this.sharedLRUCache.setChargingStationTemplate(template) + this.templateFileHash = template.templateHash + } + } catch (error) { + handleFileException( + this.templateFile, + FileType.ChargingStationTemplate, + error as NodeJS.ErrnoException, + this.logPrefix() + ) } + return template } - public async addReservation (reservation: Reservation): Promise { - const reservationFound = this.getReservationBy('reservationId', reservation.reservationId) - if (reservationFound != null) { - await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING) - } + private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(reservation.connectorId)!.reservation = reservation - await sendAndSetConnectorStatus( - this, - reservation.connectorId, - ConnectorStatusEnum.Reserved, - undefined, - { send: reservation.connectorId !== 0 } - ) + return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0! } - public bufferMessage (message: string): void { - this.messageBuffer.add(message) - this.setIntervalFlushMessageBuffer() + private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage { + return ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (stationInfo ?? this.stationInfo!).voltageOut ?? + getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile) + ) } - public closeWSConnection (): void { - if (this.isWebSocketConnectionOpened()) { - this.wsConnection?.close() - this.wsConnection = null - } + private getWebSocketPingInterval (): number { + return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null + ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value) + : 0 } - public async delete (deleteConfiguration = true): Promise { - if (this.started) { - await this.stop() + private handleErrorMessage (errorResponse: ErrorResponse): void { + const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse + if (!this.requests.has(messageId)) { + // Error + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + `Error response for unknown message id '${messageId}'`, + undefined, + { errorDetails, errorMessage, errorType } + ) } - AutomaticTransactionGenerator.deleteInstance(this) - PerformanceStatistics.deleteInstance(this.stationInfo?.hashId) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) - this.requests.clear() - this.connectors.clear() - this.evses.clear() - this.templateFileWatcher?.unref() - deleteConfiguration && rmSync(this.configurationFile, { force: true }) - this.chargingStationWorkerBroadcastChannel.unref() - this.emit(ChargingStationEvents.deleted) - this.removeAllListeners() - } - - public getAuthorizeRemoteTxRequests (): boolean { - const authorizeRemoteTxRequests = getConfigurationKey( + const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)! + logger.debug( + `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify( + errorResponse + )}` + ) + errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails)) + } + + private async handleIncomingMessage (request: IncomingRequest): Promise { + const [messageType, messageId, commandName, commandPayload] = request + if (this.requests.has(messageId)) { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `Received message with duplicate message id '${messageId}'`, + commandName, + commandPayload + ) + } + if (this.stationInfo?.enableStatistics === true) { + this.performanceStatistics?.addRequestStatistic(commandName, messageType) + } + logger.debug( + `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify( + request + )}` + ) + // Process the message + await this.ocppIncomingRequestService.incomingRequestHandler( this, - StandardParametersKey.AuthorizeRemoteTxRequests + messageId, + commandName, + commandPayload ) - return authorizeRemoteTxRequests != null - ? convertToBoolean(authorizeRemoteTxRequests.value) - : false + this.emit(ChargingStationEvents.updated) } - public getAutomaticTransactionGeneratorConfiguration (): - | AutomaticTransactionGeneratorConfiguration - | undefined { - if (this.automaticTransactionGeneratorConfiguration == null) { - let automaticTransactionGeneratorConfiguration: - | AutomaticTransactionGeneratorConfiguration - | undefined - const stationTemplate = this.getTemplateFromFile() - const stationConfiguration = this.getConfigurationFromFile() - if ( - this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true && - stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash && - stationConfiguration?.automaticTransactionGenerator != null - ) { - automaticTransactionGeneratorConfiguration = - stationConfiguration.automaticTransactionGenerator - } else { - automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator - } - this.automaticTransactionGeneratorConfiguration = { - ...Constants.DEFAULT_ATG_CONFIGURATION, - ...automaticTransactionGeneratorConfiguration, - } + private handleResponseMessage (response: Response): void { + const [messageType, messageId, commandPayload] = response + if (!this.requests.has(messageId)) { + // Error + throw new OCPPError( + ErrorType.INTERNAL_ERROR, + `Response for unknown message id '${messageId}'`, + undefined, + commandPayload + ) } - return this.automaticTransactionGeneratorConfiguration + // Respond + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest( + messageType, + messageId + )! + logger.debug( + `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify( + response + )}` + ) + responseCallback(commandPayload, requestPayload) } - public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined { - return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses + private handleUnsupportedVersion (version: OCPPVersion | undefined): void { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) } - public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined { - if (transactionId == null) { - return undefined - } else if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const [connectorId, connectorStatus] of evseStatus.connectors) { - if (connectorStatus.transactionId === transactionId) { - return connectorId - } - } - } + private initialize (options?: ChargingStationOptions): void { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const stationTemplate = this.getTemplateFromFile()! + checkTemplate(stationTemplate, this.logPrefix(), this.templateFile) + this.configurationFile = join( + dirname(this.templateFile.replace('station-templates', 'configurations')), + `${getHashId(this.index, stationTemplate)}.json` + ) + const stationConfiguration = this.getConfigurationFromFile() + if ( + stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash && + (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null) + ) { + checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile) + this.initializeConnectorsOrEvsesFromFile(stationConfiguration) } else { - for (const connectorId of this.connectors.keys()) { - if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { - return connectorId - } - } + this.initializeConnectorsOrEvsesFromTemplate(stationTemplate) } - } - - public getConnectorMaximumAvailablePower (connectorId: number): number { - let connectorAmperageLimitationLimit: number | undefined - const amperageLimitation = this.getAmperageLimitation() + this.stationInfo = this.getStationInfo(options) + validateStationInfo(this) if ( - amperageLimitation != null && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - amperageLimitation < this.stationInfo!.maximumAmperage! + this.stationInfo.firmwareStatus === FirmwareStatus.Installing && + isNotEmptyString(this.stationInfo.firmwareVersionPattern) && + isNotEmptyString(this.stationInfo.firmwareVersion) ) { - connectorAmperageLimitationLimit = - (this.stationInfo?.currentOutType === CurrentType.AC - ? ACElectricUtils.powerTotal( - this.getNumberOfPhases(), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo.voltageOut!, - amperageLimitation * - (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()) - ) - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) / - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.powerDivider! + const patternGroup = + this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ?? + this.stationInfo.firmwareVersion.split('.').length + const match = new RegExp(this.stationInfo.firmwareVersionPattern) + .exec(this.stationInfo.firmwareVersion) + ?.slice(1, patternGroup + 1) + if (match != null) { + const patchLevelIndex = match.length - 1 + match[patchLevelIndex] = ( + convertToInt(match[patchLevelIndex]) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.stationInfo.firmwareUpgrade!.versionUpgrade!.step! + ).toString() + this.stationInfo.firmwareVersion = match.join('.') + } } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider! - const chargingStationChargingProfilesLimit = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getChargingStationChargingProfilesLimit(this)! / this.powerDivider! - const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId) - return min( - Number.isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Number.isNaN(connectorAmperageLimitationLimit!) - ? Number.POSITIVE_INFINITY - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectorAmperageLimitationLimit!, - Number.isNaN(chargingStationChargingProfilesLimit) - ? Number.POSITIVE_INFINITY - : chargingStationChargingProfilesLimit, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Number.isNaN(connectorChargingProfilesLimit!) - ? Number.POSITIVE_INFINITY - : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectorChargingProfilesLimit! - ) - } - - public getConnectorStatus (connectorId: number): ConnectorStatus | undefined { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - if (evseStatus.connectors.has(connectorId)) { - return evseStatus.connectors.get(connectorId) - } + this.saveStationInfo() + this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() + if (this.stationInfo.enableStatistics === true) { + this.performanceStatistics = PerformanceStatistics.getInstance( + this.stationInfo.hashId, + this.stationInfo.chargingStationId, + this.configuredSupervisionUrl + ) + } + const bootNotificationRequest = createBootNotificationRequest(this.stationInfo) + if (bootNotificationRequest == null) { + const errorMsg = 'Error while creating boot notification request' + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } + this.bootNotificationRequest = bootNotificationRequest + this.powerDivider = this.getPowerDivider() + // OCPP configuration + this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration) + this.initializeOcppConfiguration() + this.initializeOcppServices() + if (this.stationInfo.autoRegister === true) { + this.bootNotificationResponse = { + currentTime: new Date(), + interval: millisecondsToSeconds(this.getHeartbeatInterval()), + status: RegistrationStatusEnumType.ACCEPTED, } - return undefined } - return this.connectors.get(connectorId) - } - - public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number { - return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded) - } - - public getEnergyActiveImportRegisterByTransactionId ( - transactionId: number | undefined, - rounded = false - ): number { - return this.getEnergyActiveImportRegister( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!), - rounded - ) } - public getHeartbeatInterval (): number { - const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) - if (HeartbeatInterval != null) { - return secondsToMilliseconds(convertToInt(HeartbeatInterval.value)) - } - const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) - if (HeartBeatInterval != null) { - return secondsToMilliseconds(convertToInt(HeartBeatInterval.value)) + private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void { + if (stationTemplate.Connectors == null && this.connectors.size === 0) { + const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) } - this.stationInfo?.autoRegister === false && + if (stationTemplate.Connectors?.[0] == null) { logger.warn( - `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${Constants.DEFAULT_HEARTBEAT_INTERVAL.toString()}` + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no connector id 0 configuration` ) - return Constants.DEFAULT_HEARTBEAT_INTERVAL - } - - public getLocalAuthListEnabled (): boolean { - const localAuthListEnabled = getConfigurationKey( - this, - StandardParametersKey.LocalAuthListEnabled - ) - return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false - } - - public getNumberOfConnectors (): number { - if (this.hasEvses) { - let numberOfConnectors = 0 - for (const [evseId, evseStatus] of this.evses) { - if (evseId > 0) { - numberOfConnectors += evseStatus.connectors.size - } - } - return numberOfConnectors - } - return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size - } - - public getNumberOfEvses (): number { - return this.evses.has(0) ? this.evses.size - 1 : this.evses.size - } - - public getNumberOfPhases (stationInfo?: ChargingStationInfo): number { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const localStationInfo = stationInfo ?? this.stationInfo! - switch (this.getCurrentOutType(stationInfo)) { - case CurrentType.AC: - return localStationInfo.numberOfPhases ?? 3 - case CurrentType.DC: - return 0 } - } - - public getNumberOfRunningTransactions (): number { - let numberOfRunningTransactions = 0 - if (this.hasEvses) { - for (const [evseId, evseStatus] of this.evses) { - if (evseId === 0) { - continue - } - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionStarted === true) { - ++numberOfRunningTransactions + if (stationTemplate.Connectors != null) { + const { configuredMaxConnectors, templateMaxAvailableConnectors, templateMaxConnectors } = + checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile) + const connectorsConfigHash = hash( + Constants.DEFAULT_HASH_ALGORITHM, + `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`, + 'hex' + ) + const connectorsConfigChanged = + this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash + if (this.connectors.size === 0 || connectorsConfigChanged) { + connectorsConfigChanged && this.connectors.clear() + this.connectorsConfigurationHash = connectorsConfigHash + if (templateMaxConnectors > 0) { + for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) { + if ( + connectorId === 0 && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + (stationTemplate.Connectors[connectorId] == null || + !this.getUseConnectorId0(stationTemplate)) + ) { + continue + } + const templateConnectorId = + connectorId > 0 && stationTemplate.randomConnectors === true + ? randomInt(1, templateMaxAvailableConnectors) + : connectorId + const connectorStatus = stationTemplate.Connectors[templateConnectorId] + checkStationInfoConnectorStatus( + templateConnectorId, + connectorStatus, + this.logPrefix(), + this.templateFile + ) + this.connectors.set(connectorId, clone(connectorStatus)) } + initializeConnectorsMapStatus(this.connectors, this.logPrefix()) + this.saveConnectorsStatus() + } else { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no connectors configuration defined, cannot create connectors` + ) } } } else { - for (const connectorId of this.connectors.keys()) { - if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { - ++numberOfRunningTransactions - } - } + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no connectors configuration defined, using already defined connectors` + ) } - return numberOfRunningTransactions } - public getReservationBy ( - filterKey: ReservationKey, - value: number | string - ): Reservation | undefined { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.reservation?.[filterKey] === value) { - return connectorStatus.reservation - } - } + private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void { + if (configuration.connectorsStatus != null && configuration.evsesStatus == null) { + for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) { + this.connectors.set( + connectorId, + prepareConnectorStatus(clone(connectorStatus)) + ) } - } else { - for (const connectorStatus of this.connectors.values()) { - if (connectorStatus.reservation?.[filterKey] === value) { - return connectorStatus.reservation - } + } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { + for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { + const evseStatus = clone(evseStatusConfiguration) + delete evseStatus.connectorsStatus + this.evses.set(evseId, { + ...(evseStatus as EvseStatus), + connectors: new Map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [ + connectorId, + prepareConnectorStatus(connectorStatus), + ]) + ), + }) } + } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) { + const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } else { + const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) } } - public getReserveConnectorZeroSupported (): boolean { - return convertToBoolean( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value - ) + private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { + if (stationTemplate.Connectors != null && stationTemplate.Evses == null) { + this.initializeConnectorsFromTemplate(stationTemplate) + } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) { + this.initializeEvsesFromTemplate(stationTemplate) + } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) { + const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } else { + const errorMsg = `No connectors or evses defined in template file ${this.templateFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } } - public getTransactionIdTag (transactionId: number): string | undefined { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - for (const connectorStatus of evseStatus.connectors.values()) { - if (connectorStatus.transactionId === transactionId) { - return connectorStatus.transactionIdTag + private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void { + if (stationTemplate.Evses == null && this.evses.size === 0) { + const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) + } + if (stationTemplate.Evses?.[0] == null) { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no evse id 0 configuration` + ) + } + if (stationTemplate.Evses?.[0]?.Connectors[0] == null) { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with evse id 0 with no connector id 0 configuration` + ) + } + if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used` + ) + } + if (stationTemplate.Evses != null) { + const evsesConfigHash = hash( + Constants.DEFAULT_HASH_ALGORITHM, + JSON.stringify(stationTemplate.Evses), + 'hex' + ) + const evsesConfigChanged = + this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash + if (this.evses.size === 0 || evsesConfigChanged) { + evsesConfigChanged && this.evses.clear() + this.evsesConfigurationHash = evsesConfigHash + const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses) + if (templateMaxEvses > 0) { + for (const evseKey in stationTemplate.Evses) { + const evseId = convertToInt(evseKey) + this.evses.set(evseId, { + availability: AvailabilityType.Operative, + connectors: buildConnectorsMap( + stationTemplate.Evses[evseKey].Connectors, + this.logPrefix(), + this.templateFile + ), + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix()) } + this.saveEvsesStatus() + } else { + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no evses configuration defined, cannot create evses` + ) } } } else { - for (const connectorId of this.connectors.keys()) { - if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) { - return this.getConnectorStatus(connectorId)?.transactionIdTag - } - } + logger.warn( + `${this.logPrefix()} Charging station information from template ${ + this.templateFile + } with no evses configuration defined, using already defined evses` + ) } } - public hasConnector (connectorId: number): boolean { - if (this.hasEvses) { - for (const evseStatus of this.evses.values()) { - if (evseStatus.connectors.has(connectorId)) { - return true - } - } - return false + private initializeOcppConfiguration (): void { + if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) { + addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0') } - return this.connectors.has(connectorId) - } - - public hasIdTags (): boolean { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!)) - } - - public inAcceptedState (): boolean { - return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED - } - - public inPendingState (): boolean { - return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING + if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) { + addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', { + visible: false, + }) + } + if ( + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null + ) { + addConfigurationKey( + this, + this.stationInfo.supervisionUrlOcppKey, + this.configuredSupervisionUrl.href, + { reboot: true } + ) + } else if ( + this.stationInfo?.supervisionUrlOcppConfiguration === false && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null + ) { + deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, { + save: false, + }) + } + if ( + isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) && + getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null + ) { + addConfigurationKey( + this, + this.stationInfo.amperageLimitationOcppKey, + // prettier-ignore + ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo) + ).toString() + ) + } + if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) { + addConfigurationKey( + this, + StandardParametersKey.SupportedFeatureProfiles, + `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}` + ) + } + addConfigurationKey( + this, + StandardParametersKey.NumberOfConnectors, + this.getNumberOfConnectors().toString(), + { readonly: true }, + { overwrite: true } + ) + if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) { + addConfigurationKey( + this, + StandardParametersKey.MeterValuesSampledData, + MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER + ) + } + if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) { + const connectorsPhaseRotation: string[] = [] + if (this.hasEvses) { + for (const evseStatus of this.evses.values()) { + for (const connectorId of evseStatus.connectors.keys()) { + connectorsPhaseRotation.push( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getPhaseRotationValue(connectorId, this.getNumberOfPhases())! + ) + } + } + } else { + for (const connectorId of this.connectors.keys()) { + connectorsPhaseRotation.push( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + getPhaseRotationValue(connectorId, this.getNumberOfPhases())! + ) + } + } + addConfigurationKey( + this, + StandardParametersKey.ConnectorPhaseRotation, + connectorsPhaseRotation.toString() + ) + } + if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) { + addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true') + } + if ( + getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null && + hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true + ) { + addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false') + } + if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) { + addConfigurationKey( + this, + StandardParametersKey.ConnectionTimeOut, + Constants.DEFAULT_CONNECTION_TIMEOUT.toString() + ) + } + this.saveOcppConfiguration() } - public inRejectedState (): boolean { - return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED + private initializeOcppServices (): void { + const ocppVersion = this.stationInfo?.ocppVersion + switch (ocppVersion) { + case OCPPVersion.VERSION_16: + this.ocppIncomingRequestService = + OCPP16IncomingRequestService.getInstance() + this.ocppRequestService = OCPP16RequestService.getInstance( + OCPP16ResponseService.getInstance() + ) + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + this.ocppIncomingRequestService = + OCPP20IncomingRequestService.getInstance() + this.ocppRequestService = OCPP20RequestService.getInstance( + OCPP20ResponseService.getInstance() + ) + break + default: + this.handleUnsupportedVersion(ocppVersion) + break + } } - public inUnknownState (): boolean { - return this.bootNotificationResponse?.status == null + private internalStopMessageSequence (): void { + // Stop WebSocket ping + this.stopWebSocketPing() + // Stop heartbeat + this.stopHeartbeat() + // Stop the ATG + if (this.automaticTransactionGenerator?.started === true) { + this.stopAutomaticTransactionGenerator() + } } - public isChargingStationAvailable (): boolean { - return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative + private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void { + this.emit(ChargingStationEvents.disconnected) + this.emit(ChargingStationEvents.updated) + switch (code) { + // Normal close + case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS: + case WebSocketCloseEventStatusCode.CLOSE_NORMAL: + logger.info( + `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString( + code + )}' and reason '${reason.toString()}'` + ) + this.wsConnectionRetryCount = 0 + break + // Abnormal close + default: + logger.error( + `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString( + code + )}' and reason '${reason.toString()}'` + ) + this.started && + this.reconnect() + .then(() => { + this.emit(ChargingStationEvents.updated) + return undefined + }) + .catch((error: unknown) => + logger.error(`${this.logPrefix()} Error while reconnecting:`, error) + ) + break + } } - public isConnectorAvailable (connectorId: number): boolean { - return ( - connectorId > 0 && - this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative - ) + private onError (error: WSError): void { + this.closeWSConnection() + logger.error(`${this.logPrefix()} WebSocket error:`, error) } - public isConnectorReservable ( - reservationId: number, - idTag?: string, - connectorId?: number - ): boolean { - const reservation = this.getReservationBy('reservationId', reservationId) - const reservationExists = reservation != null && !hasReservationExpired(reservation) - if (arguments.length === 1) { - return !reservationExists - } else if (arguments.length > 1) { - const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined - const userReservationExists = - userReservation != null && !hasReservationExpired(userReservation) - const notConnectorZero = connectorId == null ? true : connectorId > 0 - const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0 - return ( - !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable + private async onMessage (data: RawData): Promise { + let request: ErrorResponse | IncomingRequest | Response | undefined + let messageType: MessageType | undefined + let errorMsg: string + try { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + request = JSON.parse(data.toString()) as ErrorResponse | IncomingRequest | Response + if (Array.isArray(request)) { + ;[messageType] = request + // Check the type of message + switch (messageType) { + // Error Message + case MessageType.CALL_ERROR_MESSAGE: + this.handleErrorMessage(request as ErrorResponse) + break + // Incoming Message + case MessageType.CALL_MESSAGE: + await this.handleIncomingMessage(request as IncomingRequest) + break + // Response Message + case MessageType.CALL_RESULT_MESSAGE: + this.handleResponseMessage(request as Response) + break + // Unknown Message + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + errorMsg = `Wrong message type ${messageType}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg) + } + } else { + throw new OCPPError( + ErrorType.PROTOCOL_ERROR, + 'Incoming message is not an array', + undefined, + { + request, + } + ) + } + } catch (error) { + if (!Array.isArray(request)) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error) + return + } + let commandName: IncomingRequestCommand | undefined + let requestCommandName: IncomingRequestCommand | RequestCommand | undefined + let errorCallback: ErrorCallback + const [, messageId] = request + switch (messageType) { + case MessageType.CALL_ERROR_MESSAGE: + case MessageType.CALL_RESULT_MESSAGE: + if (this.requests.has(messageId)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ;[, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)! + // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op) + errorCallback(error as OCPPError, false) + } else { + // Remove the request from the cache in case of error at response handling + this.requests.delete(messageId) + } + break + case MessageType.CALL_MESSAGE: + ;[, , commandName] = request as IncomingRequest + // Send error + await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName) + break + } + if (!(error instanceof OCPPError)) { + logger.warn( + `${this.logPrefix()} Error thrown at incoming OCPP command ${ + commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND + // eslint-disable-next-line @typescript-eslint/no-base-to-string + } message '${data.toString()}' handling is not an OCPPError:`, + error + ) + } + logger.error( + `${this.logPrefix()} Incoming OCPP command '${ + commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND + // eslint-disable-next-line @typescript-eslint/no-base-to-string + }' message '${data.toString()}'${ + this.requests.has(messageId) + ? ` matching cached request '${JSON.stringify( + this.getCachedRequest(messageType, messageId) + )}'` + : '' + } processing error:`, + error ) } - return false - } - - public isRegistered (): boolean { - return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState()) - } - - public isWebSocketConnectionOpened (): boolean { - return this.wsConnection?.readyState === WebSocket.OPEN } - public openWSConnection ( - options?: WsOptions, - params?: { closeOpened?: boolean; terminateOpened?: boolean } - ): void { - options = { - handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()), - ...this.stationInfo?.wsOptions, - ...options, - } - params = { ...{ closeOpened: false, terminateOpened: false }, ...params } - if (!checkChargingStationState(this, this.logPrefix())) { - return - } - if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) { - options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}` - } - if (params.closeOpened) { - this.closeWSConnection() - } - if (params.terminateOpened) { - this.terminateWSConnection() - } - + private async onOpen (): Promise { if (this.isWebSocketConnectionOpened()) { + this.emit(ChargingStationEvents.connected) + this.emit(ChargingStationEvents.updated) + logger.info( + `${this.logPrefix()} Connection to OCPP server through ${ + this.wsConnectionUrl.href + } succeeded` + ) + let registrationRetryCount = 0 + if (!this.isRegistered()) { + // Send BootNotification + do { + await this.ocppRequestService.requestHandler< + BootNotificationRequest, + BootNotificationResponse + >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { + skipBufferingOnError: true, + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.bootNotificationResponse!.currentTime = convertToDate( + this.bootNotificationResponse?.currentTime + )! + if (!this.isRegistered()) { + this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount + await sleep( + this.bootNotificationResponse?.interval != null + ? secondsToMilliseconds(this.bootNotificationResponse.interval) + : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL + ) + } + } while ( + !this.isRegistered() && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! || + this.stationInfo?.registrationMaxRetries === -1) + ) + } + if (!this.isRegistered()) { + logger.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount.toString()}) or retry disabled (${this.stationInfo?.registrationMaxRetries?.toString()})` + ) + } + this.emit(ChargingStationEvents.updated) + } else { logger.warn( - `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened` + `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed` ) - return } + } - logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`) - - this.wsConnection = new WebSocket( - this.wsConnectionUrl, - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `ocpp${this.stationInfo?.ocppVersion}`, - options - ) + private onPing (): void { + logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`) + } - // Handle WebSocket message - this.wsConnection.on('message', data => { - this.onMessage(data).catch(Constants.EMPTY_FUNCTION) - }) - // Handle WebSocket error - this.wsConnection.on('error', this.onError.bind(this)) - // Handle WebSocket close - this.wsConnection.on('close', this.onClose.bind(this)) - // Handle WebSocket open - this.wsConnection.on('open', () => { - this.onOpen().catch((error: unknown) => - logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error) - ) - }) - // Handle WebSocket ping - this.wsConnection.on('ping', this.onPing.bind(this)) - // Handle WebSocket pong - this.wsConnection.on('pong', this.onPong.bind(this)) + private onPong (): void { + logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`) } - public async removeReservation ( - reservation: Reservation, - reason: ReservationTerminationReason - ): Promise { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const connector = this.getConnectorStatus(reservation.connectorId)! - switch (reason) { - case ReservationTerminationReason.CONNECTOR_STATE_CHANGED: - case ReservationTerminationReason.TRANSACTION_STARTED: - delete connector.reservation - break - case ReservationTerminationReason.EXPIRED: - case ReservationTerminationReason.REPLACE_EXISTING: - case ReservationTerminationReason.RESERVATION_CANCELED: - await sendAndSetConnectorStatus( - this, - reservation.connectorId, - ConnectorStatusEnum.Available, - undefined, - { send: reservation.connectorId !== 0 } - ) - delete connector.reservation - break - default: + private async reconnect (): Promise { + if ( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! || + this.stationInfo?.autoReconnectMaxRetries === -1 + ) { + ++this.wsConnectionRetryCount + const reconnectDelay = + this.stationInfo?.reconnectExponentialDelay === true + ? exponentialDelay(this.wsConnectionRetryCount) + : secondsToMilliseconds(this.getConnectionTimeout()) + const reconnectDelayWithdraw = 1000 + const reconnectTimeout = + reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0 + logger.error( + `${this.logPrefix()} WebSocket connection retry in ${roundTo( + reconnectDelay, + 2 + ).toString()}ms, timeout ${reconnectTimeout.toString()}ms` + ) + await sleep(reconnectDelay) + logger.error( + `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}` + ) + this.openWSConnection( + { + handshakeTimeout: reconnectTimeout, + }, + { closeOpened: true } + ) + } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) { + logger.error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new BaseError(`Unknown reservation termination reason '${reason}'`) + `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})` + ) } } - public async reset (reason?: StopTransactionReason): Promise { - await this.stop(reason) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await sleep(this.stationInfo!.resetTime!) - this.initialize() - this.start() + private saveAutomaticTransactionGeneratorConfiguration (): void { + if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) { + this.saveConfiguration() + } } - public restartHeartbeat (): void { - // Stop heartbeat - this.stopHeartbeat() - // Start heartbeat - this.startHeartbeat() + private saveConfiguration (): void { + if (isNotEmptyString(this.configurationFile)) { + try { + if (!existsSync(dirname(this.configurationFile))) { + mkdirSync(dirname(this.configurationFile), { recursive: true }) + } + const configurationFromFile = this.getConfigurationFromFile() + let configurationData: ChargingStationConfiguration = + configurationFromFile != null + ? clone(configurationFromFile) + : {} + if (this.stationInfo?.stationInfoPersistentConfiguration === true) { + configurationData.stationInfo = this.stationInfo + } else { + delete configurationData.stationInfo + } + if ( + this.stationInfo?.ocppPersistentConfiguration === true && + Array.isArray(this.ocppConfiguration?.configurationKey) + ) { + configurationData.configurationKey = this.ocppConfiguration.configurationKey + } else { + delete configurationData.configurationKey + } + configurationData = mergeDeepRight( + configurationData, + buildChargingStationAutomaticTransactionGeneratorConfiguration(this) + ) + if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) { + delete configurationData.automaticTransactionGenerator + } + if (this.connectors.size > 0) { + configurationData.connectorsStatus = buildConnectorsStatus(this) + } else { + delete configurationData.connectorsStatus + } + if (this.evses.size > 0) { + configurationData.evsesStatus = buildEvsesStatus(this) + } else { + delete configurationData.evsesStatus + } + delete configurationData.configurationHash + const configurationHash = hash( + Constants.DEFAULT_HASH_ALGORITHM, + JSON.stringify({ + automaticTransactionGenerator: configurationData.automaticTransactionGenerator, + configurationKey: configurationData.configurationKey, + stationInfo: configurationData.stationInfo, + ...(this.connectors.size > 0 && { + connectorsStatus: configurationData.connectorsStatus, + }), + ...(this.evses.size > 0 && { + evsesStatus: configurationData.evsesStatus, + }), + } satisfies ChargingStationConfiguration), + 'hex' + ) + if (this.configurationFileHash !== configurationHash) { + AsyncLock.runExclusive(AsyncLockType.configuration, () => { + configurationData.configurationHash = configurationHash + const measureId = `${FileType.ChargingStationConfiguration} write` + const beginId = PerformanceStatistics.beginMeasure(measureId) + writeFileSync( + this.configurationFile, + JSON.stringify(configurationData, undefined, 2), + 'utf8' + ) + PerformanceStatistics.endMeasure(measureId, beginId) + this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) + this.sharedLRUCache.setChargingStationConfiguration(configurationData) + this.configurationFileHash = configurationHash + }).catch((error: unknown) => { + handleFileException( + this.configurationFile, + FileType.ChargingStationConfiguration, + error as NodeJS.ErrnoException, + this.logPrefix() + ) + }) + } else { + logger.debug( + `${this.logPrefix()} Not saving unchanged charging station configuration file ${ + this.configurationFile + }` + ) + } + } catch (error) { + handleFileException( + this.configurationFile, + FileType.ChargingStationConfiguration, + error as NodeJS.ErrnoException, + this.logPrefix() + ) + } + } else { + logger.error( + `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file` + ) + } } - public restartMeterValues (connectorId: number, interval: number): void { - this.stopMeterValues(connectorId) - this.startMeterValues(connectorId, interval) + private saveConnectorsStatus (): void { + this.saveConfiguration() } - public restartWebSocketPing (): void { - // Stop WebSocket ping - this.stopWebSocketPing() - // Start WebSocket ping - this.startWebSocketPing() + private saveEvsesStatus (): void { + this.saveConfiguration() } - public saveOcppConfiguration (): void { - if (this.stationInfo?.ocppPersistentConfiguration === true) { + private saveStationInfo (): void { + if (this.stationInfo?.stationInfoPersistentConfiguration === true) { this.saveConfiguration() } } - public setSupervisionUrl (url: string): void { - if ( - this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) - ) { - setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url) - } else { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.stationInfo!.supervisionUrls = url - this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl() - this.saveStationInfo() + private setIntervalFlushMessageBuffer (): void { + if (this.flushMessageBufferSetInterval == null) { + this.flushMessageBufferSetInterval = setInterval(() => { + if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) { + this.flushMessageBuffer() + } + if (this.messageBuffer.size === 0) { + this.clearIntervalFlushMessageBuffer() + } + }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL) } } - public start (): void { - if (!this.started) { - if (!this.starting) { - this.starting = true - if (this.stationInfo?.enableStatistics === true) { - this.performanceStatistics?.start() - } - this.openWSConnection() - // Monitor charging station template file - this.templateFileWatcher = watchJsonFile( - this.templateFile, - FileType.ChargingStationTemplate, - this.logPrefix(), - undefined, - (event, filename): void => { - if (isNotEmptyString(filename) && event === 'change') { - try { - logger.debug( - `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${ - this.templateFile - } file have changed, reload` - ) - this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!) - // Initialize - this.initialize() - // Restart the ATG - const ATGStarted = this.automaticTransactionGenerator?.started - if (ATGStarted === true) { - this.stopAutomaticTransactionGenerator() - } - delete this.automaticTransactionGeneratorConfiguration - if ( - this.getAutomaticTransactionGeneratorConfiguration()?.enable === true && - ATGStarted === true - ) { - this.startAutomaticTransactionGenerator(undefined, true) - } - if (this.stationInfo?.enableStatistics === true) { - this.performanceStatistics?.restart() - } else { - this.performanceStatistics?.stop() - } - // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed - } catch (error) { - logger.error( - `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`, - error - ) - } - } + private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise { + if (this.stationInfo?.autoRegister === true) { + await this.ocppRequestService.requestHandler< + BootNotificationRequest, + BootNotificationResponse + >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, { + skipBufferingOnError: true, + }) + } + // Start WebSocket ping + if (this.wsPingSetInterval == null) { + this.startWebSocketPing() + } + // Start heartbeat + if (this.heartbeatSetInterval == null) { + this.startHeartbeat() + } + // Initialize connectors status + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId > 0) { + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + await sendAndSetConnectorStatus( + this, + connectorId, + getBootConnectorStatus(this, connectorId, connectorStatus), + evseId + ) } - ) - this.started = true - this.emit(ChargingStationEvents.started) - this.starting = false - } else { - logger.warn(`${this.logPrefix()} Charging station is already starting...`) + } } } else { - logger.warn(`${this.logPrefix()} Charging station is already started...`) + for (const connectorId of this.connectors.keys()) { + if (connectorId > 0) { + await sendAndSetConnectorStatus( + this, + connectorId, + getBootConnectorStatus( + this, + connectorId, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getConnectorStatus(connectorId)! + ) + ) + } + } + } + if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) { + await this.ocppRequestService.requestHandler< + FirmwareStatusNotificationRequest, + FirmwareStatusNotificationResponse + >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, { + status: FirmwareStatus.Installed, + }) + this.stationInfo.firmwareStatus = FirmwareStatus.Installed } - } - public startAutomaticTransactionGenerator ( - connectorIds?: number[], - stopAbsoluteDuration?: boolean - ): void { - this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this) - if (isNotEmptyArray(connectorIds)) { - for (const connectorId of connectorIds) { - this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration) - } - } else { - this.automaticTransactionGenerator?.start(stopAbsoluteDuration) + // Start the ATG + if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) { + this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration) } - this.saveAutomaticTransactionGeneratorConfiguration() - this.emit(ChargingStationEvents.updated) + this.flushMessageBuffer() } - public startHeartbeat (): void { - const heartbeatInterval = this.getHeartbeatInterval() - if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) { - this.heartbeatSetInterval = setInterval(() => { - this.ocppRequestService - .requestHandler(this, RequestCommand.HEARTBEAT) - .catch((error: unknown) => { - logger.error( - `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`, - error - ) - }) - }, heartbeatInterval) + private startWebSocketPing (): void { + const webSocketPingInterval = this.getWebSocketPingInterval() + if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) { + this.wsPingSetInterval = setInterval(() => { + if (this.isWebSocketConnectionOpened()) { + this.wsConnection?.ping() + } + }, secondsToMilliseconds(webSocketPingInterval)) logger.info( - `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds( - heartbeatInterval + `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds( + webSocketPingInterval )}` ) - } else if (this.heartbeatSetInterval != null) { + } else if (this.wsPingSetInterval != null) { logger.info( - `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds( - heartbeatInterval + `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds( + webSocketPingInterval )}` ) } else { logger.error( - `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat` + `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping` ) } } - public startMeterValues (connectorId: number, interval: number): void { - if (connectorId === 0) { - logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}` - ) - return - } - const connectorStatus = this.getConnectorStatus(connectorId) - if (connectorStatus == null) { - logger.error( - `${this.logPrefix()} Trying to start MeterValues on non existing connector id - ${connectorId.toString()}` - ) - return - } - if (connectorStatus.transactionStarted === false) { - logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction started` - ) - return - } else if ( - connectorStatus.transactionStarted === true && - connectorStatus.transactionId == null - ) { - logger.error( - `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id` - ) - return - } - if (interval > 0) { - connectorStatus.transactionSetInterval = setInterval(() => { - const meterValue = buildMeterValue( - this, - connectorId, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - connectorStatus.transactionId!, - interval - ) - this.ocppRequestService - .requestHandler( - this, - RequestCommand.METER_VALUES, - { - connectorId, - meterValue: [meterValue], - transactionId: connectorStatus.transactionId, - } - ) - .catch((error: unknown) => { - logger.error( - `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`, - error - ) - }) - }, interval) - } else { - logger.error( - `${this.logPrefix()} Charging station ${ - StandardParametersKey.MeterValueSampleInterval - } configuration set to ${interval.toString()}, not sending MeterValues` - ) + private stopHeartbeat (): void { + if (this.heartbeatSetInterval != null) { + clearInterval(this.heartbeatSetInterval) + delete this.heartbeatSetInterval } } - public async stop ( + private async stopMessageSequence ( reason?: StopTransactionReason, - stopTransactions = this.stationInfo?.stopTransactionsOnStopped + stopTransactions?: boolean ): Promise { - if (this.started) { - if (!this.stopping) { - this.stopping = true - await this.stopMessageSequence(reason, stopTransactions) - this.closeWSConnection() - if (this.stationInfo?.enableStatistics === true) { - this.performanceStatistics?.stop() + this.internalStopMessageSequence() + // Stop ongoing transactions + stopTransactions && (await this.stopRunningTransactions(reason)) + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId > 0) { + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + await sendAndSetConnectorStatus( + this, + connectorId, + ConnectorStatusEnum.Unavailable, + evseId + ) + delete connectorStatus.status + } } - this.templateFileWatcher?.close() - delete this.bootNotificationResponse - this.started = false - this.saveConfiguration() - this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash) - this.emit(ChargingStationEvents.stopped) - this.stopping = false - } else { - logger.warn(`${this.logPrefix()} Charging station is already stopping...`) } } else { - logger.warn(`${this.logPrefix()} Charging station is already stopped...`) + for (const connectorId of this.connectors.keys()) { + if (connectorId > 0) { + await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable) + delete this.getConnectorStatus(connectorId)?.status + } + } } } - public stopAutomaticTransactionGenerator (connectorIds?: number[]): void { - if (isNotEmptyArray(connectorIds)) { - for (const connectorId of connectorIds) { - this.automaticTransactionGenerator?.stopConnector(connectorId) + private async stopRunningTransactions (reason?: StopTransactionReason): Promise { + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId === 0) { + continue + } + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (connectorStatus.transactionStarted === true) { + await this.stopTransactionOnConnector(connectorId, reason) + } + } } } else { - this.automaticTransactionGenerator?.stop() + for (const connectorId of this.connectors.keys()) { + if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) { + await this.stopTransactionOnConnector(connectorId, reason) + } + } } - this.saveAutomaticTransactionGeneratorConfiguration() - this.emit(ChargingStationEvents.updated) } - public stopMeterValues (connectorId: number): void { - const connectorStatus = this.getConnectorStatus(connectorId) - if (connectorStatus?.transactionSetInterval != null) { - clearInterval(connectorStatus.transactionSetInterval) + private stopWebSocketPing (): void { + if (this.wsPingSetInterval != null) { + clearInterval(this.wsPingSetInterval) + delete this.wsPingSetInterval } } - public async stopTransactionOnConnector ( - connectorId: number, - reason?: StopTransactionReason - ): Promise { - const transactionId = this.getConnectorStatus(connectorId)?.transactionId - if ( - this.stationInfo?.beginEndMeterValues === true && - this.stationInfo.ocppStrictCompliance === true && - this.stationInfo.outOfOrderEndMeterValues === false - ) { - const transactionEndMeterValue = buildTransactionEndMeterValue( - this, - connectorId, - this.getEnergyActiveImportRegisterByTransactionId(transactionId) - ) - await this.ocppRequestService.requestHandler( - this, - RequestCommand.METER_VALUES, - { - connectorId, - meterValue: [transactionEndMeterValue], - transactionId, - } - ) + private terminateWSConnection (): void { + if (this.isWebSocketConnectionOpened()) { + this.wsConnection?.terminate() + this.wsConnection = null } - return await this.ocppRequestService.requestHandler< - Partial, - StopTransactionResponse - >(this, RequestCommand.STOP_TRANSACTION, { - meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true), - transactionId, - ...(reason != null && { reason }), - }) - } - - public get hasEvses (): boolean { - return this.connectors.size === 0 && this.evses.size > 0 - } - - public get wsConnectionUrl (): URL { - const wsConnectionBaseUrlStr = `${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - this.stationInfo?.supervisionUrlOcppConfiguration === true && - isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && - isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) - ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value - : this.configuredSupervisionUrl.href - }` - return new URL( - `${wsConnectionBaseUrlStr}${ - !wsConnectionBaseUrlStr.endsWith('/') ? '/' : '' - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }${this.stationInfo?.chargingStationId}` - ) } } diff --git a/src/charging-station/ConfigurationKeyUtils.ts b/src/charging-station/ConfigurationKeyUtils.ts index b182cc4f..037f75eb 100644 --- a/src/charging-station/ConfigurationKeyUtils.ts +++ b/src/charging-station/ConfigurationKeyUtils.ts @@ -3,6 +3,10 @@ import type { ChargingStation } from './ChargingStation.js' import { logger } from '../utils/index.js' +interface AddConfigurationKeyParams { + overwrite?: boolean + save?: boolean +} interface ConfigurationKeyOptions { readonly?: boolean reboot?: boolean @@ -12,10 +16,6 @@ interface DeleteConfigurationKeyParams { caseInsensitive?: boolean save?: boolean } -interface AddConfigurationKeyParams { - overwrite?: boolean - save?: boolean -} export const getConfigurationKey = ( chargingStation: ChargingStation, diff --git a/src/charging-station/IdTagsCache.ts b/src/charging-station/IdTagsCache.ts index b1993ab8..c10a5e0f 100644 --- a/src/charging-station/IdTagsCache.ts +++ b/src/charging-station/IdTagsCache.ts @@ -23,10 +23,6 @@ export class IdTagsCache { private readonly idTagsCaches: Map private readonly idTagsCachesAddressableIndexes: Map - private readonly logPrefix = (file: string): string => { - return logPrefix(` Id tags cache for id tags file '${file}' |`) - } - private constructor () { this.idTagsCaches = new Map() this.idTagsCachesAddressableIndexes = new Map() @@ -39,6 +35,52 @@ export class IdTagsCache { return IdTagsCache.instance } + public deleteIdTags (file: string): boolean { + return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file) + } + + /** + * Gets one idtag from the cache given the distribution + * Must be called after checking the cache is not an empty array + * @param distribution - + * @param chargingStation - + * @param connectorId - + * @returns string + */ + public getIdTag ( + distribution: IdTagDistribution, + chargingStation: ChargingStation, + connectorId: number + ): string { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const hashId = chargingStation.stationInfo!.hashId + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const idTagsFile = getIdTagsFile(chargingStation.stationInfo!)! + switch (distribution) { + case IdTagDistribution.CONNECTOR_AFFINITY: + return this.getConnectorAffinityIdTag(chargingStation, connectorId) + case IdTagDistribution.RANDOM: + return this.getRandomIdTag(hashId, idTagsFile) + case IdTagDistribution.ROUND_ROBIN: + return this.getRoundRobinIdTag(hashId, idTagsFile) + default: + return this.getRoundRobinIdTag(hashId, idTagsFile) + } + } + + /** + * Gets all idtags from the cache + * Must be called after checking the cache is not an empty array + * @param file - + * @returns string[] | undefined + */ + public getIdTags (file: string): string[] | undefined { + if (!this.hasIdTagsCache(file)) { + this.setIdTagsCache(file, this.getIdTagsFromFile(file)) + } + return this.getIdTagsCache(file) + } + private deleteIdTagsCache (file: string): boolean { this.idTagsCaches.get(file)?.idTagsFileWatcher?.close() return this.idTagsCaches.delete(file) @@ -125,6 +167,10 @@ export class IdTagsCache { return this.idTagsCaches.has(file) } + private readonly logPrefix = (file: string): string => { + return logPrefix(` Id tags cache for id tags file '${file}' |`) + } + private setIdTagsCache (file: string, idTags: string[]): Map { return this.idTagsCaches.set(file, { idTags, @@ -156,50 +202,4 @@ export class IdTagsCache { ), }) } - - public deleteIdTags (file: string): boolean { - return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file) - } - - /** - * Gets one idtag from the cache given the distribution - * Must be called after checking the cache is not an empty array - * @param distribution - - * @param chargingStation - - * @param connectorId - - * @returns string - */ - public getIdTag ( - distribution: IdTagDistribution, - chargingStation: ChargingStation, - connectorId: number - ): string { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const hashId = chargingStation.stationInfo!.hashId - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const idTagsFile = getIdTagsFile(chargingStation.stationInfo!)! - switch (distribution) { - case IdTagDistribution.CONNECTOR_AFFINITY: - return this.getConnectorAffinityIdTag(chargingStation, connectorId) - case IdTagDistribution.RANDOM: - return this.getRandomIdTag(hashId, idTagsFile) - case IdTagDistribution.ROUND_ROBIN: - return this.getRoundRobinIdTag(hashId, idTagsFile) - default: - return this.getRoundRobinIdTag(hashId, idTagsFile) - } - } - - /** - * Gets all idtags from the cache - * Must be called after checking the cache is not an empty array - * @param file - - * @returns string[] | undefined - */ - public getIdTags (file: string): string[] | undefined { - if (!this.hasIdTagsCache(file)) { - this.setIdTagsCache(file, this.getIdTagsFromFile(file)) - } - return this.getIdTagsCache(file) - } } diff --git a/src/charging-station/SharedLRUCache.ts b/src/charging-station/SharedLRUCache.ts index 1023ab8c..12cbc430 100644 --- a/src/charging-station/SharedLRUCache.ts +++ b/src/charging-station/SharedLRUCache.ts @@ -32,45 +32,6 @@ export class SharedLRUCache { return SharedLRUCache.instance } - private delete (key: string): void { - this.lruCache.delete(key) - } - - private get (key: string): CacheValueType | undefined { - return this.lruCache.get(key) - } - - private getChargingStationConfigurationKey (hash: string): string { - return `${CacheType.chargingStationConfiguration}${hash}` - } - - private getChargingStationTemplateKey (hash: string): string { - return `${CacheType.chargingStationTemplate}${hash}` - } - - private has (key: string): boolean { - return this.lruCache.has(key) - } - - private isChargingStationConfigurationCacheable ( - chargingStationConfiguration: ChargingStationConfiguration - ): boolean { - return ( - chargingStationConfiguration.configurationKey != null && - chargingStationConfiguration.stationInfo != null && - chargingStationConfiguration.automaticTransactionGenerator != null && - chargingStationConfiguration.configurationHash != null && - isNotEmptyArray(chargingStationConfiguration.configurationKey) && - !isEmpty(chargingStationConfiguration.stationInfo) && - !isEmpty(chargingStationConfiguration.automaticTransactionGenerator) && - isNotEmptyString(chargingStationConfiguration.configurationHash) - ) - } - - private set (key: string, value: CacheValueType): void { - this.lruCache.set(key, value) - } - public clear (): void { this.lruCache.clear() } @@ -124,4 +85,43 @@ export class SharedLRUCache { chargingStationTemplate ) } + + private delete (key: string): void { + this.lruCache.delete(key) + } + + private get (key: string): CacheValueType | undefined { + return this.lruCache.get(key) + } + + private getChargingStationConfigurationKey (hash: string): string { + return `${CacheType.chargingStationConfiguration}${hash}` + } + + private getChargingStationTemplateKey (hash: string): string { + return `${CacheType.chargingStationTemplate}${hash}` + } + + private has (key: string): boolean { + return this.lruCache.has(key) + } + + private isChargingStationConfigurationCacheable ( + chargingStationConfiguration: ChargingStationConfiguration + ): boolean { + return ( + chargingStationConfiguration.configurationKey != null && + chargingStationConfiguration.stationInfo != null && + chargingStationConfiguration.automaticTransactionGenerator != null && + chargingStationConfiguration.configurationHash != null && + isNotEmptyArray(chargingStationConfiguration.configurationKey) && + !isEmpty(chargingStationConfiguration.stationInfo) && + !isEmpty(chargingStationConfiguration.automaticTransactionGenerator) && + isNotEmptyString(chargingStationConfiguration.configurationHash) + ) + } + + private set (key: string, value: CacheValueType): void { + this.lruCache.set(key, value) + } } diff --git a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts index e70e128d..a0eea259 100644 --- a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts @@ -46,6 +46,11 @@ import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js' const moduleName = 'ChargingStationWorkerBroadcastChannel' +type CommandHandler = ( + requestPayload?: BroadcastChannelRequestPayload + // eslint-disable-next-line @typescript-eslint/no-invalid-void-type +) => CommandResponse | Promise | void + type CommandResponse = | AuthorizeResponse | BootNotificationResponse @@ -55,11 +60,6 @@ type CommandResponse = | StartTransactionResponse | StopTransactionResponse -type CommandHandler = ( - requestPayload?: BroadcastChannelRequestPayload - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -) => CommandResponse | Promise | void - export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel { private readonly chargingStation: ChargingStation private readonly commandHandlers: Map diff --git a/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts index 66c4c0de..cb371457 100644 --- a/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts @@ -12,14 +12,14 @@ import { logger, logPrefix, validateUUID } from '../../utils/index.js' const moduleName = 'WorkerBroadcastChannel' export abstract class WorkerBroadcastChannel extends BroadcastChannel { - private readonly logPrefix = (modName: string, methodName: string): string => { - return logPrefix(` Worker Broadcast Channel | ${modName}.${methodName}:`) - } - protected constructor () { super('worker') } + public sendRequest (request: BroadcastChannelRequest): void { + this.postMessage(request) + } + protected isRequest (message: JsonType[]): boolean { return Array.isArray(message) && message.length === 3 } @@ -54,7 +54,7 @@ export abstract class WorkerBroadcastChannel extends BroadcastChannel { return messageEvent } - public sendRequest (request: BroadcastChannelRequest): void { - this.postMessage(request) + private readonly logPrefix = (modName: string, methodName: string): string => { + return logPrefix(` Worker Broadcast Channel | ${modName}.${methodName}:`) } } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index bcc96e6e..ec163158 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -571,6 +571,94 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { this.validatePayload = this.validatePayload.bind(this) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async incomingRequestHandler( + chargingStation: ChargingStation, + messageId: string, + commandName: OCPP16IncomingRequestCommand, + commandPayload: ReqType + ): Promise { + let response: ResType + if ( + chargingStation.stationInfo?.ocppStrictCompliance === true && + chargingStation.inPendingState() && + (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || + commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION) + ) { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `${commandName} cannot be issued to handle request PDU ${JSON.stringify( + commandPayload, + undefined, + 2 + )} while the charging station is in pending state on the central server`, + commandName, + commandPayload + ) + } + if ( + chargingStation.isRegistered() || + (chargingStation.stationInfo?.ocppStrictCompliance === false && + chargingStation.inUnknownState()) + ) { + if ( + this.incomingRequestHandlers.has(commandName) && + OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) + ) { + try { + this.validatePayload(chargingStation, commandName, commandPayload) + // Call the method to build the response + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)! + if (isAsyncFunction(incomingRequestHandler)) { + response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType + } else { + response = incomingRequestHandler(chargingStation, commandPayload) as ResType + } + } catch (error) { + // Log + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`, + error + ) + throw error + } + } else { + // Throw exception + throw new OCPPError( + ErrorType.NOT_IMPLEMENTED, + `${commandName} is not implemented to handle request PDU ${JSON.stringify( + commandPayload, + undefined, + 2 + )}`, + commandName, + commandPayload + ) + } + } else { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `${commandName} cannot be issued to handle request PDU ${JSON.stringify( + commandPayload, + undefined, + 2 + )} while the charging station is not registered on the central server`, + commandName, + commandPayload + ) + } + // Send the built response + await chargingStation.ocppRequestService.sendResponse( + chargingStation, + messageId, + response, + commandName + ) + // Emit command name event to allow delayed handling + this.emit(commandName, chargingStation, commandPayload, response) + } + private async handleRequestCancelReservation ( chargingStation: ChargingStation, commandPayload: OCPP16CancelReservationRequest @@ -1245,16 +1333,16 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { try { await removeExpiredReservations(chargingStation) switch (connectorStatus.status) { - case OCPP16ChargePointStatus.Faulted: - response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED - break - case OCPP16ChargePointStatus.Preparing: case OCPP16ChargePointStatus.Charging: + case OCPP16ChargePointStatus.Finishing: + case OCPP16ChargePointStatus.Preparing: case OCPP16ChargePointStatus.SuspendedEV: case OCPP16ChargePointStatus.SuspendedEVSE: - case OCPP16ChargePointStatus.Finishing: response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED break + case OCPP16ChargePointStatus.Faulted: + response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED + break case OCPP16ChargePointStatus.Unavailable: response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE break @@ -1694,92 +1782,4 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ) return false } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public async incomingRequestHandler( - chargingStation: ChargingStation, - messageId: string, - commandName: OCPP16IncomingRequestCommand, - commandPayload: ReqType - ): Promise { - let response: ResType - if ( - chargingStation.stationInfo?.ocppStrictCompliance === true && - chargingStation.inPendingState() && - (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION || - commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION) - ) { - throw new OCPPError( - ErrorType.SECURITY_ERROR, - `${commandName} cannot be issued to handle request PDU ${JSON.stringify( - commandPayload, - undefined, - 2 - )} while the charging station is in pending state on the central server`, - commandName, - commandPayload - ) - } - if ( - chargingStation.isRegistered() || - (chargingStation.stationInfo?.ocppStrictCompliance === false && - chargingStation.inUnknownState()) - ) { - if ( - this.incomingRequestHandlers.has(commandName) && - OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) - ) { - try { - this.validatePayload(chargingStation, commandName, commandPayload) - // Call the method to build the response - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)! - if (isAsyncFunction(incomingRequestHandler)) { - response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType - } else { - response = incomingRequestHandler(chargingStation, commandPayload) as ResType - } - } catch (error) { - // Log - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`, - error - ) - throw error - } - } else { - // Throw exception - throw new OCPPError( - ErrorType.NOT_IMPLEMENTED, - `${commandName} is not implemented to handle request PDU ${JSON.stringify( - commandPayload, - undefined, - 2 - )}`, - commandName, - commandPayload - ) - } - } else { - throw new OCPPError( - ErrorType.SECURITY_ERROR, - `${commandName} cannot be issued to handle request PDU ${JSON.stringify( - commandPayload, - undefined, - 2 - )} while the charging station is not registered on the central server`, - commandName, - commandPayload - ) - } - // Send the built response - await chargingStation.ocppRequestService.sendResponse( - chargingStation, - messageId, - response, - commandName - ) - // Emit command name event to allow delayed handling - this.emit(commandName, chargingStation, commandPayload, response) - } } diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 655ca809..5c096532 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -145,6 +145,42 @@ export class OCPP16RequestService extends OCPPRequestService { this.buildRequestPayload = this.buildRequestPayload.bind(this) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async requestHandler( + chargingStation: ChargingStation, + commandName: OCPP16RequestCommand, + commandParams?: RequestType, + params?: RequestParams + ): Promise { + // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. + if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + // Pre request actions hook + switch (commandName) { + case OCPP16RequestCommand.START_TRANSACTION: + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + (commandParams as OCPP16StartTransactionRequest).connectorId, + OCPP16ChargePointStatus.Preparing + ) + break + } + return (await this.sendMessage( + chargingStation, + generateUUID(), + this.buildRequestPayload(chargingStation, commandName, commandParams), + commandName, + params + )) as ResponseType + } + // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). + throw new OCPPError( + ErrorType.NOT_SUPPORTED, + `Unsupported OCPP command ${commandName}`, + commandName, + commandParams + ) + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters private buildRequestPayload( chargingStation: ChargingStation, @@ -230,40 +266,4 @@ export class OCPP16RequestService extends OCPPRequestService { ) } } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public async requestHandler( - chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - commandParams?: RequestType, - params?: RequestParams - ): Promise { - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. - if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { - // Pre request actions hook - switch (commandName) { - case OCPP16RequestCommand.START_TRANSACTION: - await OCPP16ServiceUtils.sendAndSetConnectorStatus( - chargingStation, - (commandParams as OCPP16StartTransactionRequest).connectorId, - OCPP16ChargePointStatus.Preparing - ) - break - } - return (await this.sendMessage( - chargingStation, - generateUUID(), - this.buildRequestPayload(chargingStation, commandName, commandParams), - commandName, - params - )) as ResponseType - } - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ErrorType.NOT_SUPPORTED, - `Unsupported OCPP command ${commandName}`, - commandName, - commandParams - ) - } } diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 06fc0cd4..bac3f9fa 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -59,13 +59,14 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js' const moduleName = 'OCPP16ResponseService' export class OCPP16ResponseService extends OCPPResponseService { - protected payloadValidateFunctions: Map> - private readonly responseHandlers: Map public incomingRequestResponsePayloadValidateFunctions: Map< OCPP16IncomingRequestCommand, ValidateFunction > + protected payloadValidateFunctions: Map> + private readonly responseHandlers: Map + public constructor () { // if (new.target.name === moduleName) { // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) @@ -378,6 +379,67 @@ export class OCPP16ResponseService extends OCPPResponseService { this.validatePayload = this.validatePayload.bind(this) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async responseHandler( + chargingStation: ChargingStation, + commandName: OCPP16RequestCommand, + payload: ResType, + requestPayload: ReqType + ): Promise { + if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) { + if ( + this.responseHandlers.has(commandName) && + OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) + ) { + try { + this.validatePayload(chargingStation, commandName, payload) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const responseHandler = this.responseHandlers.get(commandName)! + if (isAsyncFunction(responseHandler)) { + await responseHandler(chargingStation, payload, requestPayload) + } else { + ;( + responseHandler as ( + chargingStation: ChargingStation, + payload: JsonType, + requestPayload?: JsonType + ) => void + )(chargingStation, payload, requestPayload) + } + } catch (error) { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`, + error + ) + throw error + } + } else { + // Throw exception + throw new OCPPError( + ErrorType.NOT_IMPLEMENTED, + `${commandName} is not implemented to handle response PDU ${JSON.stringify( + payload, + undefined, + 2 + )}`, + commandName, + payload + ) + } + } else { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `${commandName} cannot be issued to handle response PDU ${JSON.stringify( + payload, + undefined, + 2 + )} while the charging station is not registered on the central server`, + commandName, + payload + ) + } + } + private handleResponseAuthorize ( chargingStation: ChargingStation, payload: OCPP16AuthorizeResponse, @@ -781,65 +843,4 @@ export class OCPP16ResponseService extends OCPPResponseService { ) return false } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public async responseHandler( - chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - payload: ResType, - requestPayload: ReqType - ): Promise { - if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) { - if ( - this.responseHandlers.has(commandName) && - OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) - ) { - try { - this.validatePayload(chargingStation, commandName, payload) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const responseHandler = this.responseHandlers.get(commandName)! - if (isAsyncFunction(responseHandler)) { - await responseHandler(chargingStation, payload, requestPayload) - } else { - ;( - responseHandler as ( - chargingStation: ChargingStation, - payload: JsonType, - requestPayload?: JsonType - ) => void - )(chargingStation, payload, requestPayload) - } - } catch (error) { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`, - error - ) - throw error - } - } else { - // Throw exception - throw new OCPPError( - ErrorType.NOT_IMPLEMENTED, - `${commandName} is not implemented to handle response PDU ${JSON.stringify( - payload, - undefined, - 2 - )}`, - commandName, - payload - ) - } - } else { - throw new OCPPError( - ErrorType.SECURITY_ERROR, - `${commandName} cannot be issued to handle response PDU ${JSON.stringify( - payload, - undefined, - 2 - )} while the charging station is not registered on the central server`, - commandName, - payload - ) - } - } } diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 9582e171..010bd5b2 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -43,6 +43,43 @@ import { OCPPServiceUtils } from '../OCPPServiceUtils.js' import { OCPP16Constants } from './OCPP16Constants.js' export class OCPP16ServiceUtils extends OCPPServiceUtils { + public static buildTransactionBeginMeterValue ( + chargingStation: ChargingStation, + connectorId: number, + meterStart: number | undefined + ): OCPP16MeterValue { + const meterValue: OCPP16MeterValue = { + sampledValue: [], + timestamp: new Date(), + } + // Energy.Active.Import.Register measurand (default) + const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( + chargingStation, + connectorId + ) + const unitDivider = + sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 + meterValue.sampledValue.push( + OCPP16ServiceUtils.buildSampledValue( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + sampledValueTemplate!, + roundTo((meterStart ?? 0) / unitDivider, 4), + OCPP16MeterValueContext.TRANSACTION_BEGIN + ) + ) + return meterValue + } + + public static buildTransactionDataMeterValues ( + transactionBeginMeterValue: OCPP16MeterValue, + transactionEndMeterValue: OCPP16MeterValue + ): OCPP16MeterValue[] { + const meterValues: OCPP16MeterValue[] = [] + meterValues.push(transactionBeginMeterValue) + meterValues.push(transactionEndMeterValue) + return meterValues + } + public static changeAvailability = async ( chargingStation: ChargingStation, connectorIds: number[], @@ -74,6 +111,22 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED } + public static checkFeatureProfile ( + chargingStation: ChargingStation, + featureProfile: OCPP16SupportedFeatureProfiles, + command: OCPP16IncomingRequestCommand | OCPP16RequestCommand + ): boolean { + if (hasFeatureProfile(chargingStation, featureProfile) === false) { + logger.warn( + `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ + OCPP16StandardParametersKey.SupportedFeatureProfiles + } in configuration` + ) + return false + } + return true + } + public static clearChargingProfiles = ( chargingStation: ChargingStation, commandPayload: OCPP16ClearChargingProfileRequest, @@ -115,81 +168,6 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return clearedCP } - private static readonly composeChargingSchedule = ( - chargingSchedule: OCPP16ChargingSchedule, - compositeInterval: Interval - ): OCPP16ChargingSchedule | undefined => { - const chargingScheduleInterval: Interval = { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - start: chargingSchedule.startSchedule!, - } - if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) { - chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod) - if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) { - return { - ...chargingSchedule, - chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod - .filter((schedulePeriod, index) => { - if ( - isWithinInterval( - addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), - compositeInterval - ) - ) { - return true - } - if ( - index < chargingSchedule.chargingSchedulePeriod.length - 1 && - !isWithinInterval( - addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), - compositeInterval - ) && - isWithinInterval( - addSeconds( - chargingScheduleInterval.start, - chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod - ), - compositeInterval - ) - ) { - return true - } - return false - }) - .map((schedulePeriod, index) => { - if (index === 0 && schedulePeriod.startPeriod !== 0) { - schedulePeriod.startPeriod = 0 - } - return schedulePeriod - }), - duration: differenceInSeconds( - chargingScheduleInterval.end, - compositeInterval.start as Date - ), - startSchedule: compositeInterval.start as Date, - } - } - if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) { - return { - ...chargingSchedule, - chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(schedulePeriod => - isWithinInterval( - addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), - compositeInterval - ) - ), - duration: differenceInSeconds( - compositeInterval.end as Date, - chargingScheduleInterval.start - ), - } - } - return chargingSchedule - } - } - public static composeChargingSchedules = ( chargingScheduleHigher: OCPP16ChargingSchedule | undefined, chargingScheduleLower: OCPP16ChargingSchedule | undefined, @@ -425,78 +403,6 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { return false } - public static remoteStopTransaction = async ( - chargingStation: ChargingStation, - connectorId: number - ): Promise => { - await OCPP16ServiceUtils.sendAndSetConnectorStatus( - chargingStation, - connectorId, - OCPP16ChargePointStatus.Finishing - ) - const stopResponse = await chargingStation.stopTransactionOnConnector( - connectorId, - OCPP16StopTransactionReason.REMOTE - ) - if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { - return OCPP16Constants.OCPP_RESPONSE_ACCEPTED - } - return OCPP16Constants.OCPP_RESPONSE_REJECTED - } - - public static buildTransactionBeginMeterValue ( - chargingStation: ChargingStation, - connectorId: number, - meterStart: number | undefined - ): OCPP16MeterValue { - const meterValue: OCPP16MeterValue = { - sampledValue: [], - timestamp: new Date(), - } - // Energy.Active.Import.Register measurand (default) - const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate( - chargingStation, - connectorId - ) - const unitDivider = - sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1 - meterValue.sampledValue.push( - OCPP16ServiceUtils.buildSampledValue( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - sampledValueTemplate!, - roundTo((meterStart ?? 0) / unitDivider, 4), - OCPP16MeterValueContext.TRANSACTION_BEGIN - ) - ) - return meterValue - } - - public static buildTransactionDataMeterValues ( - transactionBeginMeterValue: OCPP16MeterValue, - transactionEndMeterValue: OCPP16MeterValue - ): OCPP16MeterValue[] { - const meterValues: OCPP16MeterValue[] = [] - meterValues.push(transactionBeginMeterValue) - meterValues.push(transactionEndMeterValue) - return meterValues - } - - public static checkFeatureProfile ( - chargingStation: ChargingStation, - featureProfile: OCPP16SupportedFeatureProfiles, - command: OCPP16IncomingRequestCommand | OCPP16RequestCommand - ): boolean { - if (hasFeatureProfile(chargingStation, featureProfile) === false) { - logger.warn( - `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${ - OCPP16StandardParametersKey.SupportedFeatureProfiles - } in configuration` - ) - return false - } - return true - } - public static isConfigurationKeyVisible (key: ConfigurationKey): boolean { if (key.visible == null) { return true @@ -517,6 +423,25 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { ) } + public static remoteStopTransaction = async ( + chargingStation: ChargingStation, + connectorId: number + ): Promise => { + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + connectorId, + OCPP16ChargePointStatus.Finishing + ) + const stopResponse = await chargingStation.stopTransactionOnConnector( + connectorId, + OCPP16StopTransactionReason.REMOTE + ) + if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) { + return OCPP16Constants.OCPP_RESPONSE_ACCEPTED + } + return OCPP16Constants.OCPP_RESPONSE_REJECTED + } + public static setChargingProfile ( chargingStation: ChargingStation, connectorId: number, @@ -558,4 +483,79 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { } !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp) } + + private static readonly composeChargingSchedule = ( + chargingSchedule: OCPP16ChargingSchedule, + compositeInterval: Interval + ): OCPP16ChargingSchedule | undefined => { + const chargingScheduleInterval: Interval = { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + start: chargingSchedule.startSchedule!, + } + if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) { + chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod) + if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) { + return { + ...chargingSchedule, + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod + .filter((schedulePeriod, index) => { + if ( + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), + compositeInterval + ) + ) { + return true + } + if ( + index < chargingSchedule.chargingSchedulePeriod.length - 1 && + !isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), + compositeInterval + ) && + isWithinInterval( + addSeconds( + chargingScheduleInterval.start, + chargingSchedule.chargingSchedulePeriod[index + 1].startPeriod + ), + compositeInterval + ) + ) { + return true + } + return false + }) + .map((schedulePeriod, index) => { + if (index === 0 && schedulePeriod.startPeriod !== 0) { + schedulePeriod.startPeriod = 0 + } + return schedulePeriod + }), + duration: differenceInSeconds( + chargingScheduleInterval.end, + compositeInterval.start as Date + ), + startSchedule: compositeInterval.start as Date, + } + } + if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) { + return { + ...chargingSchedule, + chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(schedulePeriod => + isWithinInterval( + addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod), + compositeInterval + ) + ), + duration: differenceInSeconds( + compositeInterval.end as Date, + chargingScheduleInterval.start + ), + } + } + return chargingSchedule + } + } } diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 9324fea9..39075970 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -53,20 +53,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { this.validatePayload = this.validatePayload.bind(this) } - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP20IncomingRequestCommand, - commandPayload: JsonType - ): boolean { - if (this.payloadValidateFunctions.has(commandName)) { - return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters public async incomingRequestHandler( chargingStation: ChargingStation, @@ -154,4 +140,18 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { // Emit command name event to allow delayed handling this.emit(commandName, chargingStation, commandPayload, response) } + + private validatePayload ( + chargingStation: ChargingStation, + commandName: OCPP20IncomingRequestCommand, + commandPayload: JsonType + ): boolean { + if (this.payloadValidateFunctions.has(commandName)) { + return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) + } + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` + ) + return false + } } diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 4d519d42..e4469365 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -67,6 +67,33 @@ export class OCPP20RequestService extends OCPPRequestService { this.buildRequestPayload = this.buildRequestPayload.bind(this) } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async requestHandler( + chargingStation: ChargingStation, + commandName: OCPP20RequestCommand, + commandParams?: RequestType, + params?: RequestParams + ): Promise { + // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. + if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + // TODO: pre request actions hook + return (await this.sendMessage( + chargingStation, + generateUUID(), + this.buildRequestPayload(chargingStation, commandName, commandParams), + commandName, + params + )) as ResponseType + } + // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). + throw new OCPPError( + ErrorType.NOT_SUPPORTED, + `Unsupported OCPP command ${commandName}`, + commandName, + commandParams + ) + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters private buildRequestPayload( chargingStation: ChargingStation, @@ -95,31 +122,4 @@ export class OCPP20RequestService extends OCPPRequestService { ) } } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public async requestHandler( - chargingStation: ChargingStation, - commandName: OCPP20RequestCommand, - commandParams?: RequestType, - params?: RequestParams - ): Promise { - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. - if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { - // TODO: pre request actions hook - return (await this.sendMessage( - chargingStation, - generateUUID(), - this.buildRequestPayload(chargingStation, commandName, commandParams), - commandName, - params - )) as ResponseType - } - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ErrorType.NOT_SUPPORTED, - `Unsupported OCPP command ${commandName}`, - commandName, - commandParams - ) - } } diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index a3735d83..0b0c32a7 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -26,13 +26,14 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20ResponseService' export class OCPP20ResponseService extends OCPPResponseService { - protected payloadValidateFunctions: Map> - private readonly responseHandlers: Map public incomingRequestResponsePayloadValidateFunctions: Map< OCPP20IncomingRequestCommand, ValidateFunction > + protected payloadValidateFunctions: Map> + private readonly responseHandlers: Map + public constructor () { // if (new.target.name === moduleName) { // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) @@ -96,56 +97,6 @@ export class OCPP20ResponseService extends OCPPResponseService { this.validatePayload = this.validatePayload.bind(this) } - private handleResponseBootNotification ( - chargingStation: ChargingStation, - payload: OCPP20BootNotificationResponse - ): void { - if (Object.values(RegistrationStatusEnumType).includes(payload.status)) { - chargingStation.bootNotificationResponse = payload - if (chargingStation.isRegistered()) { - chargingStation.emit(ChargingStationEvents.registered) - if (chargingStation.inAcceptedState()) { - addConfigurationKey( - chargingStation, - OCPP20OptionalVariableName.HeartbeatInterval, - payload.interval.toString(), - {}, - { overwrite: true, save: true } - ) - chargingStation.emit(ChargingStationEvents.accepted) - } - } else if (chargingStation.inRejectedState()) { - chargingStation.emit(ChargingStationEvents.rejected) - } - const logMsg = `${chargingStation.logPrefix()} Charging station in '${ - payload.status - }' state on the central server` - payload.status === RegistrationStatusEnumType.REJECTED - ? logger.warn(logMsg) - : logger.info(logMsg) - } else { - delete chargingStation.bootNotificationResponse - logger.error( - `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`, - payload - ) - } - } - - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP20RequestCommand, - payload: JsonType - ): boolean { - if (this.payloadValidateFunctions.has(commandName)) { - return this.validateResponsePayload(chargingStation, commandName, payload) - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters public async responseHandler( chargingStation: ChargingStation, @@ -206,4 +157,54 @@ export class OCPP20ResponseService extends OCPPResponseService { ) } } + + private handleResponseBootNotification ( + chargingStation: ChargingStation, + payload: OCPP20BootNotificationResponse + ): void { + if (Object.values(RegistrationStatusEnumType).includes(payload.status)) { + chargingStation.bootNotificationResponse = payload + if (chargingStation.isRegistered()) { + chargingStation.emit(ChargingStationEvents.registered) + if (chargingStation.inAcceptedState()) { + addConfigurationKey( + chargingStation, + OCPP20OptionalVariableName.HeartbeatInterval, + payload.interval.toString(), + {}, + { overwrite: true, save: true } + ) + chargingStation.emit(ChargingStationEvents.accepted) + } + } else if (chargingStation.inRejectedState()) { + chargingStation.emit(ChargingStationEvents.rejected) + } + const logMsg = `${chargingStation.logPrefix()} Charging station in '${ + payload.status + }' state on the central server` + payload.status === RegistrationStatusEnumType.REJECTED + ? logger.warn(logMsg) + : logger.info(logMsg) + } else { + delete chargingStation.bootNotificationResponse + logger.error( + `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`, + payload + ) + } + } + + private validatePayload ( + chargingStation: ChargingStation, + commandName: OCPP20RequestCommand, + payload: JsonType + ): boolean { + if (this.payloadValidateFunctions.has(commandName)) { + return this.validateResponsePayload(chargingStation, commandName, payload) + } + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` + ) + return false + } } diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index 01eab722..b142b223 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -50,6 +50,14 @@ export abstract class OCPPIncomingRequestService extends EventEmitter { return OCPPIncomingRequestService.instance as T } + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unnecessary-type-parameters + public abstract incomingRequestHandler( + chargingStation: ChargingStation, + messageId: string, + commandName: IncomingRequestCommand, + commandPayload: ReqType + ): Promise + protected handleRequestClearCache (chargingStation: ChargingStation): ClearCacheResponse { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (chargingStation.idTagsCache.deleteIdTags(getIdTagsFile(chargingStation.stationInfo!)!)) { @@ -82,12 +90,4 @@ export abstract class OCPPIncomingRequestService extends EventEmitter { JSON.stringify(validate?.errors, undefined, 2) ) } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unnecessary-type-parameters - public abstract incomingRequestHandler( - chargingStation: ChargingStation, - messageId: string, - commandName: IncomingRequestCommand, - commandPayload: ReqType - ): Promise } diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index a8696bd5..554f0a37 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -83,6 +83,69 @@ export abstract class OCPPRequestService { return OCPPRequestService.instance as T } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public abstract requestHandler( + chargingStation: ChargingStation, + commandName: RequestCommand, + commandParams?: ReqType, + params?: RequestParams + ): Promise + + public async sendError ( + chargingStation: ChargingStation, + messageId: string, + ocppError: OCPPError, + commandName: IncomingRequestCommand | RequestCommand + ): Promise { + try { + // Send error message + return await this.internalSendMessage( + chargingStation, + messageId, + ocppError, + MessageType.CALL_ERROR_MESSAGE, + commandName + ) + } catch (error) { + handleSendMessageError( + chargingStation, + commandName, + MessageType.CALL_ERROR_MESSAGE, + error as Error + ) + return null + } + } + + public async sendResponse ( + chargingStation: ChargingStation, + messageId: string, + messagePayload: JsonType, + commandName: IncomingRequestCommand + ): Promise { + try { + // Send response message + return await this.internalSendMessage( + chargingStation, + messageId, + messagePayload, + MessageType.CALL_RESULT_MESSAGE, + commandName + ) + } catch (error) { + handleSendMessageError( + chargingStation, + commandName, + MessageType.CALL_RESULT_MESSAGE, + error as Error, + { + throwError: true, + } + ) + return null + } + } + protected async sendMessage ( chargingStation: ChargingStation, messageId: string, @@ -449,67 +512,4 @@ export abstract class OCPPRequestService { JSON.stringify(validate?.errors, undefined, 2) ) } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public abstract requestHandler( - chargingStation: ChargingStation, - commandName: RequestCommand, - commandParams?: ReqType, - params?: RequestParams - ): Promise - - public async sendError ( - chargingStation: ChargingStation, - messageId: string, - ocppError: OCPPError, - commandName: IncomingRequestCommand | RequestCommand - ): Promise { - try { - // Send error message - return await this.internalSendMessage( - chargingStation, - messageId, - ocppError, - MessageType.CALL_ERROR_MESSAGE, - commandName - ) - } catch (error) { - handleSendMessageError( - chargingStation, - commandName, - MessageType.CALL_ERROR_MESSAGE, - error as Error - ) - return null - } - } - - public async sendResponse ( - chargingStation: ChargingStation, - messageId: string, - messagePayload: JsonType, - commandName: IncomingRequestCommand - ): Promise { - try { - // Send response message - return await this.internalSendMessage( - chargingStation, - messageId, - messagePayload, - MessageType.CALL_RESULT_MESSAGE, - commandName - ) - } catch (error) { - handleSendMessageError( - chargingStation, - commandName, - MessageType.CALL_RESULT_MESSAGE, - error as Error, - { - throwError: true, - } - ) - return null - } - } } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 0eaf22f8..6868b4ef 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -21,15 +21,16 @@ const moduleName = 'OCPPResponseService' export abstract class OCPPResponseService { private static instance: null | OCPPResponseService = null + public abstract incomingRequestResponsePayloadValidateFunctions: Map< + IncomingRequestCommand, + ValidateFunction + > + protected readonly ajv: Ajv protected readonly ajvIncomingRequest: Ajv protected emptyResponseHandler = Constants.EMPTY_FUNCTION protected abstract payloadValidateFunctions: Map> private readonly version: OCPPVersion - public abstract incomingRequestResponsePayloadValidateFunctions: Map< - IncomingRequestCommand, - ValidateFunction - > protected constructor (version: OCPPVersion) { this.version = version @@ -54,6 +55,14 @@ export abstract class OCPPResponseService { return OCPPResponseService.instance as T } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public abstract responseHandler( + chargingStation: ChargingStation, + commandName: RequestCommand, + payload: ResType, + requestPayload: ReqType + ): Promise + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters protected validateResponsePayload( chargingStation: ChargingStation, @@ -78,12 +87,4 @@ export abstract class OCPPResponseService { JSON.stringify(validate?.errors, undefined, 2) ) } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public abstract responseHandler( - chargingStation: ChargingStation, - commandName: RequestCommand, - payload: ResType, - requestPayload: ReqType - ): Promise } diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 226bffb2..c3e0047d 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -1299,25 +1299,13 @@ const getMeasurandDefaultLocation = ( // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class OCPPServiceUtils { - protected static buildSampledValue = buildSampledValue public static readonly buildTransactionEndMeterValue = buildTransactionEndMeterValue - protected static getSampledValueTemplate = getSampledValueTemplate public static readonly isIdTagAuthorized = isIdTagAuthorized - private static readonly logPrefix = ( - ocppVersion: OCPPVersion, - moduleName?: string, - methodName?: string - ): string => { - const logMsg = - isNotEmptyString(moduleName) && isNotEmptyString(methodName) - ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` - : ` OCPP ${ocppVersion} |` - return logPrefix(logMsg) - } - public static readonly restoreConnectorStatus = restoreConnectorStatus - public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus + protected static buildSampledValue = buildSampledValue + + protected static getSampledValueTemplate = getSampledValueTemplate protected constructor () { // This is intentional @@ -1417,4 +1405,16 @@ export class OCPPServiceUtils { return {} as JSONSchemaType } } + + private static readonly logPrefix = ( + ocppVersion: OCPPVersion, + moduleName?: string, + methodName?: string + ): string => { + const logMsg = + isNotEmptyString(moduleName) && isNotEmptyString(methodName) + ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` + : ` OCPP ${ocppVersion} |` + return logPrefix(logMsg) + } } diff --git a/src/charging-station/ui-server/AbstractUIServer.ts b/src/charging-station/ui-server/AbstractUIServer.ts index a639883d..9068d474 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -26,6 +26,9 @@ import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js const moduleName = 'AbstractUIServer' export abstract class AbstractUIServer { + public readonly chargingStations: Map + public readonly chargingStationTemplates: Set + protected readonly httpServer: Http2Server | Server protected readonly responseHandlers: Map< `${string}-${string}-${string}-${string}-${string}`, @@ -33,8 +36,6 @@ export abstract class AbstractUIServer { > protected readonly uiServices: Map - public readonly chargingStations: Map - public readonly chargingStationTemplates: Set public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) { this.chargingStations = new Map() @@ -59,6 +60,54 @@ export abstract class AbstractUIServer { this.uiServices = new Map() } + public buildProtocolRequest ( + uuid: `${string}-${string}-${string}-${string}-${string}`, + procedureName: ProcedureName, + requestPayload: RequestPayload + ): ProtocolRequest { + return [uuid, procedureName, requestPayload] + } + + public buildProtocolResponse ( + uuid: `${string}-${string}-${string}-${string}-${string}`, + responsePayload: ResponsePayload + ): ProtocolResponse { + return [uuid, responsePayload] + } + + public clearCaches (): void { + this.chargingStations.clear() + this.chargingStationTemplates.clear() + } + + public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean { + return this.responseHandlers.has(uuid) + } + + public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string + + public async sendInternalRequest (request: ProtocolRequest): Promise { + const protocolVersion = ProtocolVersion['0.0.1'] + this.registerProtocolVersionUIService(protocolVersion) + return await (this.uiServices + .get(protocolVersion) + ?.requestHandler(request) as Promise) + } + + public abstract sendRequest (request: ProtocolRequest): void + + public abstract sendResponse (response: ProtocolResponse): void + + public abstract start (): void + + public stop (): void { + this.stopHttpServer() + for (const uiService of this.uiServices.values()) { + uiService.stop() + } + this.clearCaches() + } + protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void { const authorizationError = new BaseError('Unauthorized') if (this.isBasicAuthEnabled()) { @@ -144,49 +193,4 @@ export abstract class AbstractUIServer { this.httpServer.removeAllListeners() } } - - public buildProtocolRequest ( - uuid: `${string}-${string}-${string}-${string}-${string}`, - procedureName: ProcedureName, - requestPayload: RequestPayload - ): ProtocolRequest { - return [uuid, procedureName, requestPayload] - } - - public buildProtocolResponse ( - uuid: `${string}-${string}-${string}-${string}-${string}`, - responsePayload: ResponsePayload - ): ProtocolResponse { - return [uuid, responsePayload] - } - - public clearCaches (): void { - this.chargingStations.clear() - this.chargingStationTemplates.clear() - } - - public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean { - return this.responseHandlers.has(uuid) - } - - public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string - - public async sendInternalRequest (request: ProtocolRequest): Promise { - const protocolVersion = ProtocolVersion['0.0.1'] - this.registerProtocolVersionUIService(protocolVersion) - return await (this.uiServices - .get(protocolVersion) - ?.requestHandler(request) as Promise) - } - - public abstract sendRequest (request: ProtocolRequest): void - public abstract sendResponse (response: ProtocolResponse): void - public abstract start (): void - public stop (): void { - this.stopHttpServer() - for (const uiService of this.uiServices.values()) { - uiService.stop() - } - this.clearCaches() - } } diff --git a/src/charging-station/ui-server/UIHttpServer.ts b/src/charging-station/ui-server/UIHttpServer.ts index 0b26ce1d..b1357822 100644 --- a/src/charging-station/ui-server/UIHttpServer.ts +++ b/src/charging-station/ui-server/UIHttpServer.ts @@ -36,6 +36,10 @@ enum HttpMethods { } export class UIHttpServer extends AbstractUIServer { + public constructor (protected override readonly uiServerConfiguration: UIServerConfiguration) { + super(uiServerConfiguration) + } + public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => { const logMsgPrefix = prefixSuffix != null ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server' const logMsg = @@ -45,8 +49,42 @@ export class UIHttpServer extends AbstractUIServer { return logPrefix(logMsg) } - public constructor (protected override readonly uiServerConfiguration: UIServerConfiguration) { - super(uiServerConfiguration) + public sendRequest (request: ProtocolRequest): void { + switch (this.uiServerConfiguration.version) { + case ApplicationProtocolVersion.VERSION_20: + this.httpServer.emit('request', request) + break + } + } + + public sendResponse (response: ProtocolResponse): void { + const [uuid, payload] = response + try { + if (this.hasResponseHandler(uuid)) { + const res = this.responseHandlers.get(uuid) as ServerResponse + res + .writeHead(this.responseStatusToStatusCode(payload.status), { + 'Content-Type': 'application/json', + }) + .end(JSONStringify(payload, undefined, MapStringifyFormat.object)) + } else { + logger.error( + `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` + ) + } + } catch (error) { + logger.error( + `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`, + error + ) + } finally { + this.responseHandlers.delete(uuid) + } + } + + public start (): void { + this.httpServer.on('request', this.requestListener.bind(this)) + this.startHttpServer() } private requestListener (req: IncomingMessage, res: ServerResponse): void { @@ -136,42 +174,4 @@ export class UIHttpServer extends AbstractUIServer { return StatusCodes.INTERNAL_SERVER_ERROR } } - - public sendRequest (request: ProtocolRequest): void { - switch (this.uiServerConfiguration.version) { - case ApplicationProtocolVersion.VERSION_20: - this.httpServer.emit('request', request) - break - } - } - - public sendResponse (response: ProtocolResponse): void { - const [uuid, payload] = response - try { - if (this.hasResponseHandler(uuid)) { - const res = this.responseHandlers.get(uuid) as ServerResponse - res - .writeHead(this.responseStatusToStatusCode(payload.status), { - 'Content-Type': 'application/json', - }) - .end(JSONStringify(payload, undefined, MapStringifyFormat.object)) - } else { - logger.error( - `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` - ) - } - } catch (error) { - logger.error( - `${this.logPrefix(moduleName, 'sendResponse')} Error at sending response id '${uuid}':`, - error - ) - } finally { - this.responseHandlers.delete(uuid) - } - } - - public start (): void { - this.httpServer.on('request', this.requestListener.bind(this)) - this.startHttpServer() - } } diff --git a/src/charging-station/ui-server/UIServerFactory.ts b/src/charging-station/ui-server/UIServerFactory.ts index 830476b4..3bdf68f6 100644 --- a/src/charging-station/ui-server/UIServerFactory.ts +++ b/src/charging-station/ui-server/UIServerFactory.ts @@ -17,15 +17,6 @@ import { UIWebSocketServer } from './UIWebSocketServer.js' // eslint-disable-next-line @typescript-eslint/no-extraneous-class export class UIServerFactory { - private static readonly logPrefix = (modName?: string, methodName?: string): string => { - const logMsgPrefix = 'UI Server' - const logMsg = - modName != null && methodName != null - ? ` ${logMsgPrefix} | ${modName}.${methodName}:` - : ` ${logMsgPrefix} |` - return logPrefix(logMsg) - } - private constructor () { // This is intentional } @@ -91,4 +82,13 @@ export class UIServerFactory { return new UIWebSocketServer(uiServerConfiguration) } } + + private static readonly logPrefix = (modName?: string, methodName?: string): string => { + const logMsgPrefix = 'UI Server' + const logMsg = + modName != null && methodName != null + ? ` ${logMsgPrefix} | ${modName}.${methodName}:` + : ` ${logMsgPrefix} |` + return logPrefix(logMsg) + } } diff --git a/src/charging-station/ui-server/UIWebSocketServer.ts b/src/charging-station/ui-server/UIWebSocketServer.ts index ba557644..dd57883e 100644 --- a/src/charging-station/ui-server/UIWebSocketServer.ts +++ b/src/charging-station/ui-server/UIWebSocketServer.ts @@ -32,16 +32,6 @@ const moduleName = 'UIWebSocketServer' export class UIWebSocketServer extends AbstractUIServer { private readonly webSocketServer: WebSocketServer - public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => { - const logMsgPrefix = - prefixSuffix != null ? `UI WebSocket Server ${prefixSuffix}` : 'UI WebSocket Server' - const logMsg = - isNotEmptyString(modName) && isNotEmptyString(methodName) - ? ` ${logMsgPrefix} | ${modName}.${methodName}:` - : ` ${logMsgPrefix} |` - return logPrefix(logMsg) - } - public constructor (protected override readonly uiServerConfiguration: UIServerConfiguration) { super(uiServerConfiguration) this.webSocketServer = new WebSocketServer({ @@ -50,70 +40,14 @@ export class UIWebSocketServer extends AbstractUIServer { }) } - private broadcastToClients (message: string): void { - for (const client of this.webSocketServer.clients) { - if (client.readyState === WebSocket.OPEN) { - client.send(message) - } - } - } - - private validateRawDataRequest (rawData: RawData): false | ProtocolRequest { - // logger.debug( - // `${this.logPrefix( - // moduleName, - // 'validateRawDataRequest' - // // eslint-disable-next-line @typescript-eslint/no-base-to-string - // )} Raw data received in string format: ${rawData.toString()}` - // ) - - let request: ProtocolRequest - try { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - request = JSON.parse(rawData.toString()) as ProtocolRequest - } catch (error) { - logger.error( - `${this.logPrefix( - moduleName, - 'validateRawDataRequest' - // eslint-disable-next-line @typescript-eslint/no-base-to-string - )} UI protocol request is not valid JSON: ${rawData.toString()}` - ) - return false - } - - if (!Array.isArray(request)) { - logger.error( - `${this.logPrefix( - moduleName, - 'validateRawDataRequest' - )} UI protocol request is not an array:`, - request - ) - return false - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (request.length !== 3) { - logger.error( - `${this.logPrefix(moduleName, 'validateRawDataRequest')} UI protocol request is malformed:`, - request - ) - return false - } - - if (!validateUUID(request[0])) { - logger.error( - `${this.logPrefix( - moduleName, - 'validateRawDataRequest' - )} UI protocol request UUID field is invalid:`, - request - ) - return false - } - - return request + public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => { + const logMsgPrefix = + prefixSuffix != null ? `UI WebSocket Server ${prefixSuffix}` : 'UI WebSocket Server' + const logMsg = + isNotEmptyString(modName) && isNotEmptyString(methodName) + ? ` ${logMsgPrefix} | ${modName}.${methodName}:` + : ` ${logMsgPrefix} |` + return logPrefix(logMsg) } public sendRequest (request: ProtocolRequest): void { @@ -243,4 +177,70 @@ export class UIWebSocketServer extends AbstractUIServer { }) this.startHttpServer() } + + private broadcastToClients (message: string): void { + for (const client of this.webSocketServer.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(message) + } + } + } + + private validateRawDataRequest (rawData: RawData): false | ProtocolRequest { + // logger.debug( + // `${this.logPrefix( + // moduleName, + // 'validateRawDataRequest' + // // eslint-disable-next-line @typescript-eslint/no-base-to-string + // )} Raw data received in string format: ${rawData.toString()}` + // ) + + let request: ProtocolRequest + try { + // eslint-disable-next-line @typescript-eslint/no-base-to-string + request = JSON.parse(rawData.toString()) as ProtocolRequest + } catch (error) { + logger.error( + `${this.logPrefix( + moduleName, + 'validateRawDataRequest' + // eslint-disable-next-line @typescript-eslint/no-base-to-string + )} UI protocol request is not valid JSON: ${rawData.toString()}` + ) + return false + } + + if (!Array.isArray(request)) { + logger.error( + `${this.logPrefix( + moduleName, + 'validateRawDataRequest' + )} UI protocol request is not an array:`, + request + ) + return false + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (request.length !== 3) { + logger.error( + `${this.logPrefix(moduleName, 'validateRawDataRequest')} UI protocol request is malformed:`, + request + ) + return false + } + + if (!validateUUID(request[0])) { + logger.error( + `${this.logPrefix( + moduleName, + 'validateRawDataRequest' + )} UI protocol request UUID field is invalid:`, + request + ) + return false + } + + return request + } } diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index 78ca0650..0ca837e8 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -81,10 +81,6 @@ export abstract class AbstractUIService { private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel private readonly version: ProtocolVersion - public logPrefix = (modName: string, methodName: string): string => { - return this.uiServer.logPrefix(modName, methodName, this.version) - } - constructor (uiServer: AbstractUIServer, version: ProtocolVersion) { this.uiServer = uiServer this.version = version @@ -104,6 +100,89 @@ export abstract class AbstractUIService { >() } + public deleteBroadcastChannelRequest ( + uuid: `${string}-${string}-${string}-${string}-${string}` + ): void { + this.broadcastChannelRequests.delete(uuid) + } + + public getBroadcastChannelExpectedResponses ( + uuid: `${string}-${string}-${string}-${string}-${string}` + ): number { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.broadcastChannelRequests.get(uuid)! + } + + public logPrefix = (modName: string, methodName: string): string => { + return this.uiServer.logPrefix(modName, methodName, this.version) + } + + public async requestHandler (request: ProtocolRequest): Promise { + let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined + let command: ProcedureName | undefined + let requestPayload: RequestPayload | undefined + let responsePayload: ResponsePayload | undefined + try { + ;[uuid, command, requestPayload] = request + + if (!this.requestHandlers.has(command)) { + throw new BaseError( + `'${command}' is not implemented to handle message payload ${JSON.stringify( + requestPayload, + undefined, + 2 + )}` + ) + } + + // Call the request handler to build the response payload + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const requestHandler = this.requestHandlers.get(command)! + if (isAsyncFunction(requestHandler)) { + responsePayload = await requestHandler(uuid, command, requestPayload) + } else { + responsePayload = ( + requestHandler as ( + uuid?: string, + procedureName?: ProcedureName, + payload?: RequestPayload + ) => ResponsePayload | undefined + )(uuid, command, requestPayload) + } + } catch (error) { + // Log + logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error) + responsePayload = { + command, + errorDetails: (error as OCPPError).details, + errorMessage: (error as OCPPError).message, + errorStack: (error as OCPPError).stack, + hashIds: requestPayload?.hashIds, + requestPayload, + responsePayload, + status: ResponseStatus.FAILURE, + } satisfies ResponsePayload + } + if (responsePayload != null) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.uiServer.buildProtocolResponse(uuid!, responsePayload) + } + } + + public sendResponse ( + uuid: `${string}-${string}-${string}-${string}-${string}`, + responsePayload: ResponsePayload + ): void { + if (this.uiServer.hasResponseHandler(uuid)) { + this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(uuid, responsePayload)) + } + } + + public stop (): void { + this.broadcastChannelRequests.clear() + this.uiServiceWorkerBroadcastChannel.close() + } + protected handleProtocolRequest ( uuid: `${string}-${string}-${string}-${string}-${string}`, procedureName: ProcedureName, @@ -246,6 +325,16 @@ export abstract class AbstractUIService { } } + // public sendRequest ( + // uuid: `${string}-${string}-${string}-${string}-${string}`, + // procedureName: ProcedureName, + // requestPayload: RequestPayload + // ): void { + // this.uiServer.sendRequest( + // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload) + // ) + // } + private async handleStopSimulator (): Promise { try { await Bootstrap.getInstance().stop() @@ -293,93 +382,4 @@ export abstract class AbstractUIService { this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload]) this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses) } - - public deleteBroadcastChannelRequest ( - uuid: `${string}-${string}-${string}-${string}-${string}` - ): void { - this.broadcastChannelRequests.delete(uuid) - } - - public getBroadcastChannelExpectedResponses ( - uuid: `${string}-${string}-${string}-${string}-${string}` - ): number { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.broadcastChannelRequests.get(uuid)! - } - - public async requestHandler (request: ProtocolRequest): Promise { - let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined - let command: ProcedureName | undefined - let requestPayload: RequestPayload | undefined - let responsePayload: ResponsePayload | undefined - try { - ;[uuid, command, requestPayload] = request - - if (!this.requestHandlers.has(command)) { - throw new BaseError( - `'${command}' is not implemented to handle message payload ${JSON.stringify( - requestPayload, - undefined, - 2 - )}` - ) - } - - // Call the request handler to build the response payload - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const requestHandler = this.requestHandlers.get(command)! - if (isAsyncFunction(requestHandler)) { - responsePayload = await requestHandler(uuid, command, requestPayload) - } else { - responsePayload = ( - requestHandler as ( - uuid?: string, - procedureName?: ProcedureName, - payload?: RequestPayload - ) => ResponsePayload | undefined - )(uuid, command, requestPayload) - } - } catch (error) { - // Log - logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error) - responsePayload = { - command, - errorDetails: (error as OCPPError).details, - errorMessage: (error as OCPPError).message, - errorStack: (error as OCPPError).stack, - hashIds: requestPayload?.hashIds, - requestPayload, - responsePayload, - status: ResponseStatus.FAILURE, - } satisfies ResponsePayload - } - if (responsePayload != null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return this.uiServer.buildProtocolResponse(uuid!, responsePayload) - } - } - - // public sendRequest ( - // uuid: `${string}-${string}-${string}-${string}-${string}`, - // procedureName: ProcedureName, - // requestPayload: RequestPayload - // ): void { - // this.uiServer.sendRequest( - // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload) - // ) - // } - - public sendResponse ( - uuid: `${string}-${string}-${string}-${string}-${string}`, - responsePayload: ResponsePayload - ): void { - if (this.uiServer.hasResponseHandler(uuid)) { - this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(uuid, responsePayload)) - } - } - - public stop (): void { - this.broadcastChannelRequests.clear() - this.uiServiceWorkerBroadcastChannel.close() - } } diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index 15b33881..9c16208b 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -43,21 +43,10 @@ export class PerformanceStatistics { PerformanceStatistics >() - private static readonly logPrefix = (): string => { - return logPrefix(' Performance statistics') - } - private displayInterval?: NodeJS.Timeout - private readonly logPrefix = (): string => { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return logPrefix(` ${this.objName} | Performance statistics`) - } - private readonly objId: string | undefined private readonly objName: string | undefined - private performanceObserver!: PerformanceObserver - private readonly statistics: Statistics private constructor (objId: string, objName: string, uri: URL) { @@ -128,6 +117,93 @@ export class PerformanceStatistics { return PerformanceStatistics.instances.get(objId) } + private static readonly logPrefix = (): string => { + return logPrefix(' Performance statistics') + } + + public addRequestStatistic ( + command: IncomingRequestCommand | RequestCommand, + messageType: MessageType + ): void { + switch (messageType) { + case MessageType.CALL_ERROR_MESSAGE: + if ( + this.statistics.statisticsData.has(command) && + this.statistics.statisticsData.get(command)?.errorCount != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++this.statistics.statisticsData.get(command)!.errorCount! + } else { + this.statistics.statisticsData.set(command, { + ...this.statistics.statisticsData.get(command), + errorCount: 1, + }) + } + break + case MessageType.CALL_MESSAGE: + if ( + this.statistics.statisticsData.has(command) && + this.statistics.statisticsData.get(command)?.requestCount != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++this.statistics.statisticsData.get(command)!.requestCount! + } else { + this.statistics.statisticsData.set(command, { + ...this.statistics.statisticsData.get(command), + requestCount: 1, + }) + } + break + case MessageType.CALL_RESULT_MESSAGE: + if ( + this.statistics.statisticsData.has(command) && + this.statistics.statisticsData.get(command)?.responseCount != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++this.statistics.statisticsData.get(command)!.responseCount! + } else { + this.statistics.statisticsData.set(command, { + ...this.statistics.statisticsData.get(command), + responseCount: 1, + }) + } + break + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + logger.error(`${this.logPrefix()} wrong message type ${messageType}`) + break + } + } + + public restart (): void { + this.stop() + this.start() + } + + public start (): void { + this.startLogStatisticsInterval() + const performanceStorageConfiguration = + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ) + if (performanceStorageConfiguration.enabled === true) { + logger.info( + `${this.logPrefix()} storage enabled: type ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + performanceStorageConfiguration.type + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }, uri: ${performanceStorageConfiguration.uri}` + ) + } + } + + public stop (): void { + this.stopLogStatisticsInterval() + performance.clearMarks() + performance.clearMeasures() + this.performanceObserver.disconnect() + } + private addPerformanceEntryToStatistics (entry: PerformanceEntry): void { // Initialize command statistics if (!this.statistics.statisticsData.has(entry.name)) { @@ -210,6 +286,11 @@ export class PerformanceStatistics { this.performanceObserver.observe({ entryTypes: ['measure'] }) } + private readonly logPrefix = (): string => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + return logPrefix(` ${this.objName} | Performance statistics`) + } + private logStatistics (): void { logger.info(this.logPrefix(), { ...this.statistics, @@ -252,87 +333,4 @@ export class PerformanceStatistics { delete this.displayInterval } } - - public addRequestStatistic ( - command: IncomingRequestCommand | RequestCommand, - messageType: MessageType - ): void { - switch (messageType) { - case MessageType.CALL_ERROR_MESSAGE: - if ( - this.statistics.statisticsData.has(command) && - this.statistics.statisticsData.get(command)?.errorCount != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++this.statistics.statisticsData.get(command)!.errorCount! - } else { - this.statistics.statisticsData.set(command, { - ...this.statistics.statisticsData.get(command), - errorCount: 1, - }) - } - break - case MessageType.CALL_MESSAGE: - if ( - this.statistics.statisticsData.has(command) && - this.statistics.statisticsData.get(command)?.requestCount != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++this.statistics.statisticsData.get(command)!.requestCount! - } else { - this.statistics.statisticsData.set(command, { - ...this.statistics.statisticsData.get(command), - requestCount: 1, - }) - } - break - case MessageType.CALL_RESULT_MESSAGE: - if ( - this.statistics.statisticsData.has(command) && - this.statistics.statisticsData.get(command)?.responseCount != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ++this.statistics.statisticsData.get(command)!.responseCount! - } else { - this.statistics.statisticsData.set(command, { - ...this.statistics.statisticsData.get(command), - responseCount: 1, - }) - } - break - default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - logger.error(`${this.logPrefix()} wrong message type ${messageType}`) - break - } - } - - public restart (): void { - this.stop() - this.start() - } - - public start (): void { - this.startLogStatisticsInterval() - const performanceStorageConfiguration = - Configuration.getConfigurationSection( - ConfigurationSection.performanceStorage - ) - if (performanceStorageConfiguration.enabled === true) { - logger.info( - `${this.logPrefix()} storage enabled: type ${ - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - performanceStorageConfiguration.type - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }, uri: ${performanceStorageConfiguration.uri}` - ) - } - } - - public stop (): void { - this.stopLogStatisticsInterval() - performance.clearMarks() - performance.clearMeasures() - this.performanceObserver.disconnect() - } } diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index e76bdb2f..bc1d4538 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -16,14 +16,6 @@ export class JsonFileStorage extends Storage { this.dbName = this.storageUri.pathname } - private checkPerformanceRecordsFile (): void { - if (this.fd == null) { - throw new BaseError( - `${this.logPrefix} Performance records '${this.dbName}' file descriptor not found` - ) - } - } - public close (): void { this.clearPerformanceStatistics() try { @@ -79,4 +71,12 @@ export class JsonFileStorage extends Storage { ) }) } + + private checkPerformanceRecordsFile (): void { + if (this.fd == null) { + throw new BaseError( + `${this.logPrefix} Performance records '${this.dbName}' file descriptor not found` + ) + } + } } diff --git a/src/performance/storage/MikroOrmStorage.ts b/src/performance/storage/MikroOrmStorage.ts index 2eb8644c..f9998a0d 100644 --- a/src/performance/storage/MikroOrmStorage.ts +++ b/src/performance/storage/MikroOrmStorage.ts @@ -17,31 +17,6 @@ export class MikroOrmStorage extends Storage { this.dbName = this.getDBName() } - private getClientUrl (): string | undefined { - switch (this.storageType) { - case StorageType.MARIA_DB: - case StorageType.MYSQL: - case StorageType.SQLITE: - return this.storageUri.toString() - } - } - - private getDBName (): string { - if (this.storageType === StorageType.SQLITE) { - return `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db` - } - return this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '') - } - - private getOptions (): MariaDbOptions | SqliteOptions { - return { - clientUrl: this.getClientUrl(), - dbName: this.dbName, - entities: ['./dist/types/orm/entities/*.js'], - entitiesTs: ['./src/types/orm/entities/*.ts'], - } - } - public async close (): Promise { this.clearPerformanceStatistics() try { @@ -90,4 +65,29 @@ export class MikroOrmStorage extends Storage { ) } } + + private getClientUrl (): string | undefined { + switch (this.storageType) { + case StorageType.MARIA_DB: + case StorageType.MYSQL: + case StorageType.SQLITE: + return this.storageUri.toString() + } + } + + private getDBName (): string { + if (this.storageType === StorageType.SQLITE) { + return `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db` + } + return this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '') + } + + private getOptions (): MariaDbOptions | SqliteOptions { + return { + clientUrl: this.getClientUrl(), + dbName: this.dbName, + entities: ['./dist/types/orm/entities/*.js'], + entitiesTs: ['./src/types/orm/entities/*.ts'], + } + } } diff --git a/src/performance/storage/MongoDBStorage.ts b/src/performance/storage/MongoDBStorage.ts index ef48f3bd..f092351a 100644 --- a/src/performance/storage/MongoDBStorage.ts +++ b/src/performance/storage/MongoDBStorage.ts @@ -18,25 +18,6 @@ export class MongoDBStorage extends Storage { this.dbName = this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '') } - private checkDBConnection (): void { - if (this.client == null) { - throw new BaseError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix} ${this.getDBNameFromStorageType( - StorageType.MONGO_DB - )} client initialization failed while trying to issue a request` - ) - } - if (!this.connected) { - throw new BaseError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix} ${this.getDBNameFromStorageType( - StorageType.MONGO_DB - )} connection not opened while trying to issue a request` - ) - } - } - public async close (): Promise { this.clearPerformanceStatistics() try { @@ -78,4 +59,23 @@ export class MongoDBStorage extends Storage { ) } } + + private checkDBConnection (): void { + if (this.client == null) { + throw new BaseError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${this.logPrefix} ${this.getDBNameFromStorageType( + StorageType.MONGO_DB + )} client initialization failed while trying to issue a request` + ) + } + if (!this.connected) { + throw new BaseError( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${this.logPrefix} ${this.getDBNameFromStorageType( + StorageType.MONGO_DB + )} connection not opened while trying to issue a request` + ) + } + } } diff --git a/src/performance/storage/Storage.ts b/src/performance/storage/Storage.ts index 0e21515c..5abe052d 100644 --- a/src/performance/storage/Storage.ts +++ b/src/performance/storage/Storage.ts @@ -22,6 +22,18 @@ export abstract class Storage { this.logPrefix = logPrefix } + public abstract close (): Promise | void + + public getPerformanceStatistics (): IterableIterator { + return Storage.performanceStatistics.values() + } + + public abstract open (): Promise | void + + public abstract storePerformanceStatistics ( + performanceStatistics: Statistics + ): Promise | void + protected clearPerformanceStatistics (): void { Storage.performanceStatistics.clear() } @@ -72,14 +84,4 @@ export abstract class Storage { protected setPerformanceStatistics (performanceStatistics: Statistics): void { Storage.performanceStatistics.set(performanceStatistics.id, performanceStatistics) } - - public abstract close (): Promise | void - - public getPerformanceStatistics (): IterableIterator { - return Storage.performanceStatistics.values() - } - public abstract open (): Promise | void - public abstract storePerformanceStatistics ( - performanceStatistics: Statistics - ): Promise | void } diff --git a/src/types/AutomaticTransactionGenerator.ts b/src/types/AutomaticTransactionGenerator.ts index e1fb9667..61bb9bc3 100644 --- a/src/types/AutomaticTransactionGenerator.ts +++ b/src/types/AutomaticTransactionGenerator.ts @@ -19,6 +19,11 @@ export interface AutomaticTransactionGeneratorConfiguration extends JsonObject { stopAfterHours: number } +export interface ChargingStationAutomaticTransactionGeneratorConfiguration { + automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration + automaticTransactionGeneratorStatuses?: Status[] +} + export interface Status { acceptedAuthorizeRequests: number acceptedStartTransactionRequests: number @@ -37,8 +42,3 @@ export interface Status { stoppedDate?: Date stopTransactionRequests: number } - -export interface ChargingStationAutomaticTransactionGeneratorConfiguration { - automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration - automaticTransactionGeneratorStatuses?: Status[] -} diff --git a/src/types/ChargingStationConfiguration.ts b/src/types/ChargingStationConfiguration.ts index 1363ccbf..bd7bf905 100644 --- a/src/types/ChargingStationConfiguration.ts +++ b/src/types/ChargingStationConfiguration.ts @@ -4,22 +4,23 @@ import type { ChargingStationOcppConfiguration } from './ChargingStationOcppConf import type { ConnectorStatus } from './ConnectorStatus.js' import type { EvseStatus } from './Evse.js' -interface ConnectorsConfiguration { +export type ChargingStationConfiguration = + ChargingStationAutomaticTransactionGeneratorConfiguration & + ChargingStationInfoConfiguration & + ChargingStationOcppConfiguration & + ConnectorsConfiguration & + EvsesConfiguration & { + configurationHash?: string + } + +export type EvseStatusConfiguration = Omit & { connectorsStatus?: ConnectorStatus[] } -export type EvseStatusConfiguration = { +interface ConnectorsConfiguration { connectorsStatus?: ConnectorStatus[] -} & Omit +} interface EvsesConfiguration { evsesStatus?: EvseStatusConfiguration[] } - -export type ChargingStationConfiguration = { - configurationHash?: string -} & ChargingStationAutomaticTransactionGeneratorConfiguration & - ChargingStationInfoConfiguration & - ChargingStationOcppConfiguration & - ConnectorsConfiguration & - EvsesConfiguration diff --git a/src/types/ChargingStationInfo.ts b/src/types/ChargingStationInfo.ts index b1fb3454..09550060 100644 --- a/src/types/ChargingStationInfo.ts +++ b/src/types/ChargingStationInfo.ts @@ -1,7 +1,19 @@ import type { ChargingStationTemplate } from './ChargingStationTemplate.js' import type { FirmwareStatus } from './ocpp/Requests.js' -export type ChargingStationInfo = { +export type ChargingStationInfo = Omit< + ChargingStationTemplate, + | 'AutomaticTransactionGenerator' + | 'chargeBoxSerialNumberPrefix' + | 'chargePointSerialNumberPrefix' + | 'Configuration' + | 'Connectors' + | 'Evses' + | 'meterSerialNumberPrefix' + | 'numberOfConnectors' + | 'power' + | 'powerUnit' +> & { chargeBoxSerialNumber?: string chargePointSerialNumber?: string chargingStationId?: string @@ -14,19 +26,7 @@ export type ChargingStationInfo = { meterSerialNumber?: string templateIndex: number templateName: string -} & Omit< - ChargingStationTemplate, - | 'AutomaticTransactionGenerator' - | 'chargeBoxSerialNumberPrefix' - | 'chargePointSerialNumberPrefix' - | 'Configuration' - | 'Connectors' - | 'Evses' - | 'meterSerialNumberPrefix' - | 'numberOfConnectors' - | 'power' - | 'powerUnit' -> +} export interface ChargingStationInfoConfiguration { stationInfo?: ChargingStationInfo diff --git a/src/types/ChargingStationOcppConfiguration.ts b/src/types/ChargingStationOcppConfiguration.ts index efa2f35d..cdd1d3ff 100644 --- a/src/types/ChargingStationOcppConfiguration.ts +++ b/src/types/ChargingStationOcppConfiguration.ts @@ -1,11 +1,11 @@ import type { JsonObject } from './JsonType.js' import type { OCPPConfigurationKey } from './ocpp/Configuration.js' +export interface ChargingStationOcppConfiguration extends JsonObject { + configurationKey?: ConfigurationKey[] +} + export interface ConfigurationKey extends OCPPConfigurationKey { reboot?: boolean visible?: boolean } - -export interface ChargingStationOcppConfiguration extends JsonObject { - configurationKey?: ConfigurationKey[] -} diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index 571e4458..c17544f7 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -15,6 +15,13 @@ import type { RequestCommand, } from './ocpp/Requests.js' +export enum AmpereUnits { + AMPERE = 'A', + CENTI_AMPERE = 'cA', + DECI_AMPERE = 'dA', + MILLI_AMPERE = 'mA', +} + export enum CurrentType { AC = 'AC', DC = 'DC', @@ -25,13 +32,6 @@ export enum PowerUnits { WATT = 'W', } -export enum AmpereUnits { - AMPERE = 'A', - CENTI_AMPERE = 'cA', - DECI_AMPERE = 'dA', - MILLI_AMPERE = 'mA', -} - export enum Voltage { VOLTAGE_110 = 110, VOLTAGE_230 = 230, @@ -39,22 +39,6 @@ export enum Voltage { VOLTAGE_800 = 800, } -export type WsOptions = ClientOptions & ClientRequestArgs - -export interface FirmwareUpgrade extends JsonObject { - failureStatus?: FirmwareStatus - reset?: boolean - versionUpgrade?: { - patternGroup?: number - step?: number - } -} - -interface CommandsSupport extends JsonObject { - incomingCommands: Record - outgoingCommands?: Record -} - enum x509CertificateType { ChargingStationCertificate = 'ChargingStationCertificate', CSMSRootCertificate = 'CSMSRootCertificate', @@ -132,3 +116,19 @@ export interface ChargingStationTemplate { wsOptions?: WsOptions x509Certificates?: Record } + +export interface FirmwareUpgrade extends JsonObject { + failureStatus?: FirmwareStatus + reset?: boolean + versionUpgrade?: { + patternGroup?: number + step?: number + } +} + +export type WsOptions = ClientOptions & ClientRequestArgs + +interface CommandsSupport extends JsonObject { + incomingCommands: Record + outgoingCommands?: Record +} diff --git a/src/types/ChargingStationWorker.ts b/src/types/ChargingStationWorker.ts index a732471a..05e3a85f 100644 --- a/src/types/ChargingStationWorker.ts +++ b/src/types/ChargingStationWorker.ts @@ -12,26 +12,10 @@ import type { Statistics } from './Statistics.js' import { ChargingStationEvents } from './ChargingStationEvents.js' -export interface ChargingStationOptions extends JsonObject { - autoRegister?: boolean - autoStart?: boolean - enableStatistics?: boolean - ocppStrictCompliance?: boolean - persistentConfiguration?: boolean - stopTransactionsOnStopped?: boolean - supervisionUrls?: string | string[] -} - -export interface ChargingStationWorkerData extends WorkerData { - index: number - options?: ChargingStationOptions - templateFile: string +enum ChargingStationMessageEvents { + performanceStatistics = 'performanceStatistics', } -export type EvseStatusWorkerType = { - connectors?: ConnectorStatus[] -} & Omit - export interface ChargingStationData extends WorkerData { automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration bootNotificationResponse?: BootNotificationResponse @@ -48,10 +32,29 @@ export interface ChargingStationData extends WorkerData { | typeof WebSocket.OPEN } -enum ChargingStationMessageEvents { - performanceStatistics = 'performanceStatistics', +export interface ChargingStationOptions extends JsonObject { + autoRegister?: boolean + autoStart?: boolean + enableStatistics?: boolean + ocppStrictCompliance?: boolean + persistentConfiguration?: boolean + stopTransactionsOnStopped?: boolean + supervisionUrls?: string | string[] +} + +export interface ChargingStationWorkerData extends WorkerData { + index: number + options?: ChargingStationOptions + templateFile: string } +export interface ChargingStationWorkerMessage { + data: T + event: ChargingStationWorkerMessageEvents +} + +export type ChargingStationWorkerMessageData = ChargingStationData | Statistics + export const ChargingStationWorkerMessageEvents = { ...ChargingStationEvents, ...ChargingStationMessageEvents, @@ -61,9 +64,6 @@ export type ChargingStationWorkerMessageEvents = | ChargingStationEvents | ChargingStationMessageEvents -export type ChargingStationWorkerMessageData = ChargingStationData | Statistics - -export interface ChargingStationWorkerMessage { - data: T - event: ChargingStationWorkerMessageEvents +export type EvseStatusWorkerType = Omit & { + connectors?: ConnectorStatus[] } diff --git a/src/types/ConfigurationData.ts b/src/types/ConfigurationData.ts index 19d93248..d6605024 100644 --- a/src/types/ConfigurationData.ts +++ b/src/types/ConfigurationData.ts @@ -6,7 +6,10 @@ import type { WorkerProcessType } from '../worker/index.js' import type { StorageType } from './Storage.js' import type { ApplicationProtocol, AuthenticationType } from './UIProtocol.js' -type ServerOptions = ListenOptions +export enum ApplicationProtocolVersion { + VERSION_11 = '1.1', + VERSION_20 = '2.0', +} export enum ConfigurationSection { log = 'log', @@ -21,63 +24,6 @@ export enum SupervisionUrlDistribution { ROUND_ROBIN = 'round-robin', } -export interface StationTemplateUrl { - file: string - numberOfStations: number - provisionedNumberOfStations?: number -} - -export interface LogConfiguration { - console?: boolean - enabled?: boolean - errorFile?: string - file?: string - format?: string - level?: string - maxFiles?: number | string - maxSize?: number | string - rotate?: boolean - statisticsInterval?: number -} - -export enum ApplicationProtocolVersion { - VERSION_11 = '1.1', - VERSION_20 = '2.0', -} - -export interface UIServerConfiguration { - authentication?: { - enabled: boolean - password?: string - type: AuthenticationType - username?: string - } - enabled?: boolean - options?: ServerOptions - type?: ApplicationProtocol - version?: ApplicationProtocolVersion -} - -export interface StorageConfiguration { - enabled?: boolean - type?: StorageType - uri?: string -} - -export type ElementsPerWorkerType = 'all' | 'auto' | number - -export interface WorkerConfiguration { - elementAddDelay?: number - elementsPerWorker?: ElementsPerWorkerType - /** @deprecated Use `elementAddDelay` instead. */ - elementStartDelay?: number - poolMaxSize?: number - poolMinSize?: number - processType?: WorkerProcessType - resourceLimits?: ResourceLimits - startDelay?: number -} - export interface ConfigurationData { /** @deprecated Moved to charging station template. */ autoReconnectMaxRetries?: number @@ -123,3 +69,57 @@ export interface ConfigurationData { /** @deprecated Moved to worker configuration section. */ workerStartDelay?: number } + +export type ElementsPerWorkerType = 'all' | 'auto' | number + +export interface LogConfiguration { + console?: boolean + enabled?: boolean + errorFile?: string + file?: string + format?: string + level?: string + maxFiles?: number | string + maxSize?: number | string + rotate?: boolean + statisticsInterval?: number +} + +export interface StationTemplateUrl { + file: string + numberOfStations: number + provisionedNumberOfStations?: number +} + +export interface StorageConfiguration { + enabled?: boolean + type?: StorageType + uri?: string +} + +export interface UIServerConfiguration { + authentication?: { + enabled: boolean + password?: string + type: AuthenticationType + username?: string + } + enabled?: boolean + options?: ServerOptions + type?: ApplicationProtocol + version?: ApplicationProtocolVersion +} + +export interface WorkerConfiguration { + elementAddDelay?: number + elementsPerWorker?: ElementsPerWorkerType + /** @deprecated Use `elementAddDelay` instead. */ + elementStartDelay?: number + poolMaxSize?: number + poolMinSize?: number + processType?: WorkerProcessType + resourceLimits?: ResourceLimits + startDelay?: number +} + +type ServerOptions = ListenOptions diff --git a/src/types/Evse.ts b/src/types/Evse.ts index dcb00ad5..96eba54d 100644 --- a/src/types/Evse.ts +++ b/src/types/Evse.ts @@ -1,11 +1,11 @@ import type { ConnectorStatus } from './ConnectorStatus.js' import type { AvailabilityType } from './ocpp/Requests.js' -export interface EvseTemplate { - Connectors: Record -} - export interface EvseStatus { availability: AvailabilityType connectors: Map } + +export interface EvseTemplate { + Connectors: Record +} diff --git a/src/types/JsonType.ts b/src/types/JsonType.ts index bca5d958..a7a6103e 100644 --- a/src/types/JsonType.ts +++ b/src/types/JsonType.ts @@ -1,8 +1,8 @@ -type JsonPrimitive = boolean | Date | null | number | string - // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style export type JsonObject = { [key in string]?: JsonType } export type JsonType = JsonObject | JsonPrimitive | JsonType[] + +type JsonPrimitive = boolean | Date | null | number | string diff --git a/src/types/MeasurandPerPhaseSampledValueTemplates.ts b/src/types/MeasurandPerPhaseSampledValueTemplates.ts index 99572673..f35f4f64 100644 --- a/src/types/MeasurandPerPhaseSampledValueTemplates.ts +++ b/src/types/MeasurandPerPhaseSampledValueTemplates.ts @@ -1,12 +1,12 @@ import type { SampledValue } from './ocpp/MeterValues.js' -export interface SampledValueTemplate extends SampledValue { - fluctuationPercent?: number - minimumValue?: number -} - export interface MeasurandPerPhaseSampledValueTemplates { L1?: SampledValueTemplate L2?: SampledValueTemplate L3?: SampledValueTemplate } + +export interface SampledValueTemplate extends SampledValue { + fluctuationPercent?: number + minimumValue?: number +} diff --git a/src/types/Statistics.ts b/src/types/Statistics.ts index 7df512e2..83da1905 100644 --- a/src/types/Statistics.ts +++ b/src/types/Statistics.ts @@ -3,9 +3,13 @@ import type { CircularBuffer } from 'mnemonist' import type { WorkerData } from '../worker/index.js' import type { IncomingRequestCommand, RequestCommand } from './ocpp/Requests.js' -export interface TimestampedData { - timestamp: number - value: number +export interface Statistics extends WorkerData { + createdAt: Date + id: string + name: string + statisticsData: Map + updatedAt?: Date + uri: string } export type StatisticsData = Partial<{ @@ -24,15 +28,6 @@ export type StatisticsData = Partial<{ totalTimeMeasurement: number }> -export interface Statistics extends WorkerData { - createdAt: Date - id: string - name: string - statisticsData: Map - updatedAt?: Date - uri: string -} - export interface TemplateStatistics { added: number configured: number @@ -40,3 +35,8 @@ export interface TemplateStatistics { provisioned: number started: number } + +export interface TimestampedData { + timestamp: number + value: number +} diff --git a/src/types/Storage.ts b/src/types/Storage.ts index f942670a..f2d73330 100644 --- a/src/types/Storage.ts +++ b/src/types/Storage.ts @@ -1,3 +1,10 @@ +export enum DBName { + MARIA_DB = 'MariaDB', + MONGO_DB = 'MongoDB', + MYSQL = 'MySQL', + SQLITE = 'SQLite', +} + export enum StorageType { JSON_FILE = 'jsonfile', MARIA_DB = 'mariadb', @@ -6,10 +13,3 @@ export enum StorageType { NONE = 'none', SQLITE = 'sqlite', } - -export enum DBName { - MARIA_DB = 'MariaDB', - MONGO_DB = 'MongoDB', - MYSQL = 'MySQL', - SQLITE = 'SQLite', -} diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index 50e70ed9..76e3ee7e 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -1,10 +1,6 @@ import type { JsonObject } from './JsonType.js' import type { BroadcastChannelResponsePayload } from './WorkerBroadcastChannel.js' -export enum Protocol { - UI = 'ui', -} - export enum ApplicationProtocol { HTTP = 'http', WS = 'ws', @@ -15,26 +11,6 @@ export enum AuthenticationType { PROTOCOL_BASIC_AUTH = 'protocol-basic-auth', } -export enum ProtocolVersion { - '0.0.1' = '0.0.1', -} - -export type ProtocolRequest = [ - `${string}-${string}-${string}-${string}-${string}`, - ProcedureName, - RequestPayload -] -export type ProtocolResponse = [ - `${string}-${string}-${string}-${string}-${string}`, - ResponsePayload -] - -export type ProtocolRequestHandler = ( - uuid?: `${string}-${string}-${string}-${string}-${string}`, - procedureName?: ProcedureName, - payload?: RequestPayload -) => Promise | Promise | ResponsePayload | undefined - export enum ProcedureName { ADD_CHARGING_STATIONS = 'addChargingStations', AUTHORIZE = 'authorize', @@ -63,16 +39,40 @@ export enum ProcedureName { STOP_TRANSACTION = 'stopTransaction', } -export interface RequestPayload extends JsonObject { - connectorIds?: number[] - hashIds?: string[] +export enum Protocol { + UI = 'ui', } +export enum ProtocolVersion { + '0.0.1' = '0.0.1', +} export enum ResponseStatus { FAILURE = 'failure', SUCCESS = 'success', } +export type ProtocolRequest = [ + `${string}-${string}-${string}-${string}-${string}`, + ProcedureName, + RequestPayload +] + +export type ProtocolRequestHandler = ( + uuid?: `${string}-${string}-${string}-${string}-${string}`, + procedureName?: ProcedureName, + payload?: RequestPayload +) => Promise | Promise | ResponsePayload | undefined + +export type ProtocolResponse = [ + `${string}-${string}-${string}-${string}-${string}`, + ResponsePayload +] + +export interface RequestPayload extends JsonObject { + connectorIds?: number[] + hashIds?: string[] +} + export interface ResponsePayload extends JsonObject { hashIdsFailed?: string[] hashIdsSucceeded?: string[] diff --git a/src/types/WebSocket.ts b/src/types/WebSocket.ts index 396c768b..65fffc57 100644 --- a/src/types/WebSocket.ts +++ b/src/types/WebSocket.ts @@ -1,4 +1,3 @@ -/* eslint-disable perfectionist/sort-enums */ export const WebSocketCloseEventStatusString: Record = Object.freeze({ 1000: 'Normal Closure', @@ -19,6 +18,7 @@ export const WebSocketCloseEventStatusString: Record { hashId: string | undefined diff --git a/src/types/ocpp/1.6/ChargingProfile.ts b/src/types/ocpp/1.6/ChargingProfile.ts index cea7d569..8a26dbd2 100644 --- a/src/types/ocpp/1.6/ChargingProfile.ts +++ b/src/types/ocpp/1.6/ChargingProfile.ts @@ -1,5 +1,27 @@ import type { JsonObject } from '../../JsonType.js' +export enum OCPP16ChargingProfileKindType { + ABSOLUTE = 'Absolute', + RECURRING = 'Recurring', + RELATIVE = 'Relative', +} + +export enum OCPP16ChargingProfilePurposeType { + CHARGE_POINT_MAX_PROFILE = 'ChargePointMaxProfile', + TX_DEFAULT_PROFILE = 'TxDefaultProfile', + TX_PROFILE = 'TxProfile', +} + +export enum OCPP16ChargingRateUnitType { + AMPERE = 'A', + WATT = 'W', +} + +export enum OCPP16RecurrencyKindType { + DAILY = 'Daily', + WEEKLY = 'Weekly', +} + export interface OCPP16ChargingProfile extends JsonObject { chargingProfileId: number chargingProfileKind: OCPP16ChargingProfileKindType @@ -25,25 +47,3 @@ export interface OCPP16ChargingSchedulePeriod extends JsonObject { numberPhases?: number startPeriod: number } - -export enum OCPP16ChargingRateUnitType { - AMPERE = 'A', - WATT = 'W', -} - -export enum OCPP16ChargingProfileKindType { - ABSOLUTE = 'Absolute', - RECURRING = 'Recurring', - RELATIVE = 'Relative', -} - -export enum OCPP16ChargingProfilePurposeType { - CHARGE_POINT_MAX_PROFILE = 'ChargePointMaxProfile', - TX_DEFAULT_PROFILE = 'TxDefaultProfile', - TX_PROFILE = 'TxProfile', -} - -export enum OCPP16RecurrencyKindType { - DAILY = 'Daily', - WEEKLY = 'Weekly', -} diff --git a/src/types/ocpp/1.6/Configuration.ts b/src/types/ocpp/1.6/Configuration.ts index 22658ee6..12074b23 100644 --- a/src/types/ocpp/1.6/Configuration.ts +++ b/src/types/ocpp/1.6/Configuration.ts @@ -1,12 +1,3 @@ -export enum OCPP16SupportedFeatureProfiles { - Core = 'Core', - FirmwareManagement = 'FirmwareManagement', - LocalAuthListManagement = 'LocalAuthListManagement', - RemoteTrigger = 'RemoteTrigger', - Reservation = 'Reservation', - SmartCharging = 'SmartCharging', -} - export enum OCPP16StandardParametersKey { AllowOfflineTxForUnknownId = 'AllowOfflineTxForUnknownId', AuthorizationCacheEnabled = 'AuthorizationCacheEnabled', @@ -54,6 +45,15 @@ export enum OCPP16StandardParametersKey { WebSocketPingInterval = 'WebSocketPingInterval', } +export enum OCPP16SupportedFeatureProfiles { + Core = 'Core', + FirmwareManagement = 'FirmwareManagement', + LocalAuthListManagement = 'LocalAuthListManagement', + RemoteTrigger = 'RemoteTrigger', + Reservation = 'Reservation', + SmartCharging = 'SmartCharging', +} + export enum OCPP16VendorParametersKey { ConnectionUrl = 'ConnectionUrl', } diff --git a/src/types/ocpp/1.6/MeterValues.ts b/src/types/ocpp/1.6/MeterValues.ts index ff4f304a..9630f062 100644 --- a/src/types/ocpp/1.6/MeterValues.ts +++ b/src/types/ocpp/1.6/MeterValues.ts @@ -1,25 +1,6 @@ import type { EmptyObject } from '../../EmptyObject.js' import type { JsonObject } from '../../JsonType.js' -export enum OCPP16MeterValueUnit { - AMP = 'A', - KILO_VAR = 'kvar', - KILO_VAR_HOUR = 'kvarh', - KILO_VOLT_AMP = 'kVA', - KILO_WATT = 'kW', - KILO_WATT_HOUR = 'kWh', - PERCENT = 'Percent', - TEMP_CELSIUS = 'Celsius', - TEMP_FAHRENHEIT = 'Fahrenheit', - TEMP_KELVIN = 'K', - VAR = 'var', - VAR_HOUR = 'varh', - VOLT = 'V', - VOLT_AMP = 'VA', - WATT = 'W', - WATT_HOUR = 'Wh', -} - export enum OCPP16MeterValueContext { INTERRUPTION_BEGIN = 'Interruption.Begin', INTERRUPTION_END = 'Interruption.End', @@ -31,6 +12,14 @@ export enum OCPP16MeterValueContext { TRIGGER = 'Trigger', } +export enum OCPP16MeterValueLocation { + BODY = 'Body', + CABLE = 'Cable', + EV = 'EV', + INLET = 'Inlet', + OUTLET = 'Outlet', +} + export enum OCPP16MeterValueMeasurand { CURRENT_EXPORT = 'Current.Export', CURRENT_IMPORT = 'Current.Import', @@ -56,14 +45,6 @@ export enum OCPP16MeterValueMeasurand { VOLTAGE = 'Voltage', } -export enum OCPP16MeterValueLocation { - BODY = 'Body', - CABLE = 'Cable', - EV = 'EV', - INLET = 'Inlet', - OUTLET = 'Outlet', -} - export enum OCPP16MeterValuePhase { L1 = 'L1', L1_L2 = 'L1-L2', @@ -77,21 +58,30 @@ export enum OCPP16MeterValuePhase { N = 'N', } +export enum OCPP16MeterValueUnit { + AMP = 'A', + KILO_VAR = 'kvar', + KILO_VAR_HOUR = 'kvarh', + KILO_VOLT_AMP = 'kVA', + KILO_WATT = 'kW', + KILO_WATT_HOUR = 'kWh', + PERCENT = 'Percent', + TEMP_CELSIUS = 'Celsius', + TEMP_FAHRENHEIT = 'Fahrenheit', + TEMP_KELVIN = 'K', + VAR = 'var', + VAR_HOUR = 'varh', + VOLT = 'V', + VOLT_AMP = 'VA', + WATT = 'W', + WATT_HOUR = 'Wh', +} + enum OCPP16MeterValueFormat { RAW = 'Raw', SIGNED_DATA = 'SignedData', } -export interface OCPP16SampledValue extends JsonObject { - context?: OCPP16MeterValueContext - format?: OCPP16MeterValueFormat - location?: OCPP16MeterValueLocation - measurand?: OCPP16MeterValueMeasurand - phase?: OCPP16MeterValuePhase - unit?: OCPP16MeterValueUnit - value: string -} - export interface OCPP16MeterValue extends JsonObject { sampledValue: OCPP16SampledValue[] timestamp: Date @@ -104,3 +94,13 @@ export interface OCPP16MeterValuesRequest extends JsonObject { } export type OCPP16MeterValuesResponse = EmptyObject + +export interface OCPP16SampledValue extends JsonObject { + context?: OCPP16MeterValueContext + format?: OCPP16MeterValueFormat + location?: OCPP16MeterValueLocation + measurand?: OCPP16MeterValueMeasurand + phase?: OCPP16MeterValuePhase + unit?: OCPP16MeterValueUnit + value: string +} diff --git a/src/types/ocpp/1.6/Requests.ts b/src/types/ocpp/1.6/Requests.ts index 5715dd12..44327c8b 100644 --- a/src/types/ocpp/1.6/Requests.ts +++ b/src/types/ocpp/1.6/Requests.ts @@ -10,17 +10,21 @@ import type { import type { OCPP16StandardParametersKey, OCPP16VendorParametersKey } from './Configuration.js' import type { OCPP16DiagnosticsStatus } from './DiagnosticsStatus.js' -export enum OCPP16RequestCommand { - AUTHORIZE = 'Authorize', - BOOT_NOTIFICATION = 'BootNotification', - DATA_TRANSFER = 'DataTransfer', - DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification', - FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification', - HEARTBEAT = 'Heartbeat', - METER_VALUES = 'MeterValues', - START_TRANSACTION = 'StartTransaction', - STATUS_NOTIFICATION = 'StatusNotification', - STOP_TRANSACTION = 'StopTransaction', +export enum OCPP16AvailabilityType { + Inoperative = 'Inoperative', + Operative = 'Operative', +} + +export enum OCPP16DataTransferVendorId {} + +export enum OCPP16FirmwareStatus { + Downloaded = 'Downloaded', + DownloadFailed = 'DownloadFailed', + Downloading = 'Downloading', + Idle = 'Idle', + InstallationFailed = 'InstallationFailed', + Installed = 'Installed', + Installing = 'Installing', } export enum OCPP16IncomingRequestCommand { @@ -43,7 +47,49 @@ export enum OCPP16IncomingRequestCommand { UPDATE_FIRMWARE = 'UpdateFirmware', } -export type OCPP16HeartbeatRequest = EmptyObject +export enum OCPP16MessageTrigger { + BootNotification = 'BootNotification', + DiagnosticsStatusNotification = 'DiagnosticsStatusNotification', + FirmwareStatusNotification = 'FirmwareStatusNotification', + Heartbeat = 'Heartbeat', + MeterValues = 'MeterValues', + StatusNotification = 'StatusNotification', +} + +export enum OCPP16RequestCommand { + AUTHORIZE = 'Authorize', + BOOT_NOTIFICATION = 'BootNotification', + DATA_TRANSFER = 'DataTransfer', + DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification', + FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification', + HEARTBEAT = 'Heartbeat', + METER_VALUES = 'MeterValues', + START_TRANSACTION = 'StartTransaction', + STATUS_NOTIFICATION = 'StatusNotification', + STOP_TRANSACTION = 'StopTransaction', +} + +enum ResetType { + HARD = 'Hard', + SOFT = 'Soft', +} + +export interface ChangeConfigurationRequest extends JsonObject { + key: OCPP16ConfigurationKey + value: string +} + +export interface GetConfigurationRequest extends JsonObject { + key?: OCPP16ConfigurationKey[] +} + +export interface GetDiagnosticsRequest extends JsonObject { + location: string + retries?: number + retryInterval?: number + startTime?: Date + stopTime?: Date +} export interface OCPP16BootNotificationRequest extends JsonObject { chargeBoxSerialNumber?: string @@ -57,50 +103,36 @@ export interface OCPP16BootNotificationRequest extends JsonObject { meterType?: string } -export interface OCPP16StatusNotificationRequest extends JsonObject { +export interface OCPP16CancelReservationRequest extends JsonObject { + reservationId: number +} + +export interface OCPP16ChangeAvailabilityRequest extends JsonObject { connectorId: number - errorCode: OCPP16ChargePointErrorCode - info?: string - status: OCPP16ChargePointStatus - timestamp?: Date - vendorErrorCode?: string - vendorId?: string + type: OCPP16AvailabilityType } export type OCPP16ClearCacheRequest = EmptyObject -type OCPP16ConfigurationKey = OCPP16StandardParametersKey | OCPP16VendorParametersKey | string - -export interface ChangeConfigurationRequest extends JsonObject { - key: OCPP16ConfigurationKey - value: string -} - -export interface RemoteStartTransactionRequest extends JsonObject { - chargingProfile?: OCPP16ChargingProfile +export interface OCPP16ClearChargingProfileRequest extends JsonObject { + chargingProfilePurpose?: OCPP16ChargingProfilePurposeType connectorId?: number - idTag: string -} - -export interface RemoteStopTransactionRequest extends JsonObject { - transactionId: number -} - -export interface UnlockConnectorRequest extends JsonObject { - connectorId: number + id?: number + stackLevel?: number } -export interface GetConfigurationRequest extends JsonObject { - key?: OCPP16ConfigurationKey[] +export interface OCPP16DataTransferRequest extends JsonObject { + data?: string + messageId?: string + vendorId: string } -enum ResetType { - HARD = 'Hard', - SOFT = 'Soft', +export interface OCPP16DiagnosticsStatusNotificationRequest extends JsonObject { + status: OCPP16DiagnosticsStatus } -export interface ResetRequest extends JsonObject { - type: ResetType +export interface OCPP16FirmwareStatusNotificationRequest extends JsonObject { + status: OCPP16FirmwareStatus } export interface OCPP16GetCompositeScheduleRequest extends JsonObject { @@ -109,26 +141,29 @@ export interface OCPP16GetCompositeScheduleRequest extends JsonObject { duration: number } -export interface SetChargingProfileRequest extends JsonObject { - connectorId: number - csChargingProfiles: OCPP16ChargingProfile -} +export type OCPP16HeartbeatRequest = EmptyObject -export enum OCPP16AvailabilityType { - Inoperative = 'Inoperative', - Operative = 'Operative', +export interface OCPP16ReserveNowRequest extends JsonObject { + connectorId: number + expiryDate: Date + idTag: string + parentIdTag?: string + reservationId: number } -export interface OCPP16ChangeAvailabilityRequest extends JsonObject { +export interface OCPP16StatusNotificationRequest extends JsonObject { connectorId: number - type: OCPP16AvailabilityType + errorCode: OCPP16ChargePointErrorCode + info?: string + status: OCPP16ChargePointStatus + timestamp?: Date + vendorErrorCode?: string + vendorId?: string } -export interface OCPP16ClearChargingProfileRequest extends JsonObject { - chargingProfilePurpose?: OCPP16ChargingProfilePurposeType +export interface OCPP16TriggerMessageRequest extends JsonObject { connectorId?: number - id?: number - stackLevel?: number + requestedMessage: OCPP16MessageTrigger } export interface OCPP16UpdateFirmwareRequest extends JsonObject { @@ -138,62 +173,27 @@ export interface OCPP16UpdateFirmwareRequest extends JsonObject { retryInterval?: number } -export enum OCPP16FirmwareStatus { - Downloaded = 'Downloaded', - DownloadFailed = 'DownloadFailed', - Downloading = 'Downloading', - Idle = 'Idle', - InstallationFailed = 'InstallationFailed', - Installed = 'Installed', - Installing = 'Installing', -} - -export interface OCPP16FirmwareStatusNotificationRequest extends JsonObject { - status: OCPP16FirmwareStatus -} - -export interface GetDiagnosticsRequest extends JsonObject { - location: string - retries?: number - retryInterval?: number - startTime?: Date - stopTime?: Date -} - -export interface OCPP16DiagnosticsStatusNotificationRequest extends JsonObject { - status: OCPP16DiagnosticsStatus +export interface RemoteStartTransactionRequest extends JsonObject { + chargingProfile?: OCPP16ChargingProfile + connectorId?: number + idTag: string } -export enum OCPP16MessageTrigger { - BootNotification = 'BootNotification', - DiagnosticsStatusNotification = 'DiagnosticsStatusNotification', - FirmwareStatusNotification = 'FirmwareStatusNotification', - Heartbeat = 'Heartbeat', - MeterValues = 'MeterValues', - StatusNotification = 'StatusNotification', +export interface RemoteStopTransactionRequest extends JsonObject { + transactionId: number } -export interface OCPP16TriggerMessageRequest extends JsonObject { - connectorId?: number - requestedMessage: OCPP16MessageTrigger +export interface ResetRequest extends JsonObject { + type: ResetType } -export enum OCPP16DataTransferVendorId {} - -export interface OCPP16DataTransferRequest extends JsonObject { - data?: string - messageId?: string - vendorId: string +export interface SetChargingProfileRequest extends JsonObject { + connectorId: number + csChargingProfiles: OCPP16ChargingProfile } -export interface OCPP16ReserveNowRequest extends JsonObject { +export interface UnlockConnectorRequest extends JsonObject { connectorId: number - expiryDate: Date - idTag: string - parentIdTag?: string - reservationId: number } -export interface OCPP16CancelReservationRequest extends JsonObject { - reservationId: number -} +type OCPP16ConfigurationKey = OCPP16StandardParametersKey | OCPP16VendorParametersKey | string diff --git a/src/types/ocpp/1.6/Responses.ts b/src/types/ocpp/1.6/Responses.ts index 18eddc9a..6dc03580 100644 --- a/src/types/ocpp/1.6/Responses.ts +++ b/src/types/ocpp/1.6/Responses.ts @@ -4,18 +4,21 @@ import type { GenericStatus, RegistrationStatusEnumType } from '../Common.js' import type { OCPPConfigurationKey } from '../Configuration.js' import type { OCPP16ChargingSchedule } from './ChargingProfile.js' -export interface OCPP16HeartbeatResponse extends JsonObject { - currentTime: Date +export enum OCPP16AvailabilityStatus { + ACCEPTED = 'Accepted', + REJECTED = 'Rejected', + SCHEDULED = 'Scheduled', } -export enum OCPP16UnlockStatus { +export enum OCPP16ChargingProfileStatus { + ACCEPTED = 'Accepted', NOT_SUPPORTED = 'NotSupported', - UNLOCK_FAILED = 'UnlockFailed', - UNLOCKED = 'Unlocked', + REJECTED = 'Rejected', } -export interface UnlockConnectorResponse extends JsonObject { - status: OCPP16UnlockStatus +export enum OCPP16ClearChargingProfileStatus { + ACCEPTED = 'Accepted', + UNKNOWN = 'Unknown', } export enum OCPP16ConfigurationStatus { @@ -25,100 +28,97 @@ export enum OCPP16ConfigurationStatus { REJECTED = 'Rejected', } -export interface ChangeConfigurationResponse extends JsonObject { - status: OCPP16ConfigurationStatus +export enum OCPP16DataTransferStatus { + ACCEPTED = 'Accepted', + REJECTED = 'Rejected', + UNKNOWN_MESSAGE_ID = 'UnknownMessageId', + UNKNOWN_VENDOR_ID = 'UnknownVendorId', } -export interface OCPP16BootNotificationResponse extends JsonObject { - currentTime: Date - interval: number - status: RegistrationStatusEnumType +export enum OCPP16ReservationStatus { + ACCEPTED = 'Accepted', + FAULTED = 'Faulted', + NOT_SUPPORTED = 'NotSupported', + OCCUPIED = 'Occupied', + REJECTED = 'Rejected', + UNAVAILABLE = 'Unavailable', } -export type OCPP16StatusNotificationResponse = EmptyObject - -export interface GetConfigurationResponse extends JsonObject { - configurationKey: OCPPConfigurationKey[] - unknownKey: string[] +export enum OCPP16TriggerMessageStatus { + ACCEPTED = 'Accepted', + NOT_IMPLEMENTED = 'NotImplemented', + REJECTED = 'Rejected', } -export enum OCPP16ChargingProfileStatus { - ACCEPTED = 'Accepted', +export enum OCPP16UnlockStatus { NOT_SUPPORTED = 'NotSupported', - REJECTED = 'Rejected', + UNLOCK_FAILED = 'UnlockFailed', + UNLOCKED = 'Unlocked', } -export interface OCPP16GetCompositeScheduleResponse extends JsonObject { - chargingSchedule?: OCPP16ChargingSchedule - connectorId?: number - scheduleStart?: Date - status: GenericStatus +export interface ChangeConfigurationResponse extends JsonObject { + status: OCPP16ConfigurationStatus } -export interface SetChargingProfileResponse extends JsonObject { - status: OCPP16ChargingProfileStatus +export interface GetConfigurationResponse extends JsonObject { + configurationKey: OCPPConfigurationKey[] + unknownKey: string[] } -export enum OCPP16AvailabilityStatus { - ACCEPTED = 'Accepted', - REJECTED = 'Rejected', - SCHEDULED = 'Scheduled', +export interface GetDiagnosticsResponse extends JsonObject { + fileName?: string } -export interface OCPP16ChangeAvailabilityResponse extends JsonObject { - status: OCPP16AvailabilityStatus +export interface OCPP16BootNotificationResponse extends JsonObject { + currentTime: Date + interval: number + status: RegistrationStatusEnumType } -export enum OCPP16ClearChargingProfileStatus { - ACCEPTED = 'Accepted', - UNKNOWN = 'Unknown', +export interface OCPP16ChangeAvailabilityResponse extends JsonObject { + status: OCPP16AvailabilityStatus } export interface OCPP16ClearChargingProfileResponse extends JsonObject { status: OCPP16ClearChargingProfileStatus } -export type OCPP16UpdateFirmwareResponse = EmptyObject +export interface OCPP16DataTransferResponse extends JsonObject { + data?: string + status: OCPP16DataTransferStatus +} + +export type OCPP16DiagnosticsStatusNotificationResponse = EmptyObject export type OCPP16FirmwareStatusNotificationResponse = EmptyObject -export interface GetDiagnosticsResponse extends JsonObject { - fileName?: string +export interface OCPP16GetCompositeScheduleResponse extends JsonObject { + chargingSchedule?: OCPP16ChargingSchedule + connectorId?: number + scheduleStart?: Date + status: GenericStatus } -export type OCPP16DiagnosticsStatusNotificationResponse = EmptyObject +export interface OCPP16HeartbeatResponse extends JsonObject { + currentTime: Date +} -export enum OCPP16TriggerMessageStatus { - ACCEPTED = 'Accepted', - NOT_IMPLEMENTED = 'NotImplemented', - REJECTED = 'Rejected', +export interface OCPP16ReserveNowResponse extends JsonObject { + status: OCPP16ReservationStatus } +export type OCPP16StatusNotificationResponse = EmptyObject + export interface OCPP16TriggerMessageResponse extends JsonObject { status: OCPP16TriggerMessageStatus } -export enum OCPP16DataTransferStatus { - ACCEPTED = 'Accepted', - REJECTED = 'Rejected', - UNKNOWN_MESSAGE_ID = 'UnknownMessageId', - UNKNOWN_VENDOR_ID = 'UnknownVendorId', -} - -export interface OCPP16DataTransferResponse extends JsonObject { - data?: string - status: OCPP16DataTransferStatus -} +export type OCPP16UpdateFirmwareResponse = EmptyObject -export enum OCPP16ReservationStatus { - ACCEPTED = 'Accepted', - FAULTED = 'Faulted', - NOT_SUPPORTED = 'NotSupported', - OCCUPIED = 'Occupied', - REJECTED = 'Rejected', - UNAVAILABLE = 'Unavailable', +export interface SetChargingProfileResponse extends JsonObject { + status: OCPP16ChargingProfileStatus } -export interface OCPP16ReserveNowResponse extends JsonObject { - status: OCPP16ReservationStatus +export interface UnlockConnectorResponse extends JsonObject { + status: OCPP16UnlockStatus } diff --git a/src/types/ocpp/1.6/Transaction.ts b/src/types/ocpp/1.6/Transaction.ts index beec7cd8..2aa5db38 100644 --- a/src/types/ocpp/1.6/Transaction.ts +++ b/src/types/ocpp/1.6/Transaction.ts @@ -1,6 +1,14 @@ import type { JsonObject } from '../../JsonType.js' import type { OCPP16MeterValue } from './MeterValues.js' +export enum OCPP16AuthorizationStatus { + ACCEPTED = 'Accepted', + BLOCKED = 'Blocked', + CONCURRENT_TX = 'ConcurrentTx', + EXPIRED = 'Expired', + INVALID = 'Invalid', +} + export enum OCPP16StopTransactionReason { DE_AUTHORIZED = 'DeAuthorized', EMERGENCY_STOP = 'EmergencyStop', @@ -15,20 +23,6 @@ export enum OCPP16StopTransactionReason { UNLOCK_COMMAND = 'UnlockCommand', } -export enum OCPP16AuthorizationStatus { - ACCEPTED = 'Accepted', - BLOCKED = 'Blocked', - CONCURRENT_TX = 'ConcurrentTx', - EXPIRED = 'Expired', - INVALID = 'Invalid', -} - -interface IdTagInfo extends JsonObject { - expiryDate?: Date - parentIdTag?: string - status: OCPP16AuthorizationStatus -} - export interface OCPP16AuthorizeRequest extends JsonObject { idTag: string } @@ -62,3 +56,9 @@ export interface OCPP16StopTransactionRequest extends JsonObject { export interface OCPP16StopTransactionResponse extends JsonObject { idTagInfo?: IdTagInfo } + +interface IdTagInfo extends JsonObject { + expiryDate?: Date + parentIdTag?: string + status: OCPP16AuthorizationStatus +} diff --git a/src/types/ocpp/2.0/Common.ts b/src/types/ocpp/2.0/Common.ts index d13279e9..1bd577be 100644 --- a/src/types/ocpp/2.0/Common.ts +++ b/src/types/ocpp/2.0/Common.ts @@ -1,17 +1,6 @@ import type { JsonObject } from '../../JsonType.js' import type { GenericStatus } from '../Common.js' -export enum DataEnumType { - boolean = 'boolean', - dateTime = 'dateTime', - decimal = 'decimal', - integer = 'integer', - MemberList = 'MemberList', - OptionList = 'OptionList', - SequenceList = 'SequenceList', - string = 'string', -} - export enum BootReasonEnumType { ApplicationReset = 'ApplicationReset', FirmwareUpdate = 'FirmwareUpdate', @@ -24,25 +13,31 @@ export enum BootReasonEnumType { Watchdog = 'Watchdog', } -export enum OperationalStatusEnumType { - Inoperative = 'Inoperative', - Operative = 'Operative', +export enum CertificateActionEnumType { + Install = 'Install', + Update = 'Update', } -export enum OCPP20ConnectorStatusEnumType { - Available = 'Available', - Faulted = 'Faulted', - Occupied = 'Occupied', - Reserved = 'Reserved', - Unavailable = 'Unavailable', +export enum CertificateSigningUseEnumType { + ChargingStationCertificate = 'ChargingStationCertificate', + V2GCertificate = 'V2GCertificate', } -export type GenericStatusEnumType = GenericStatus +export enum DataEnumType { + boolean = 'boolean', + dateTime = 'dateTime', + decimal = 'decimal', + integer = 'integer', + MemberList = 'MemberList', + OptionList = 'OptionList', + SequenceList = 'SequenceList', + string = 'string', +} -export enum HashAlgorithmEnumType { - SHA256 = 'SHA256', - SHA384 = 'SHA384', - SHA512 = 'SHA512', +export enum DeleteCertificateStatusEnumType { + Accepted = 'Accepted', + Failed = 'Failed', + NotFound = 'NotFound', } export enum GetCertificateIdUseEnumType { @@ -63,6 +58,12 @@ export enum GetInstalledCertificateStatusEnumType { NotFound = 'NotFound', } +export enum HashAlgorithmEnumType { + SHA256 = 'SHA256', + SHA384 = 'SHA384', + SHA512 = 'SHA512', +} + export enum InstallCertificateStatusEnumType { Accepted = 'Accepted', Failed = 'Failed', @@ -76,24 +77,25 @@ export enum InstallCertificateUseEnumType { V2GRootCertificate = 'V2GRootCertificate', } -export enum DeleteCertificateStatusEnumType { - Accepted = 'Accepted', - Failed = 'Failed', - NotFound = 'NotFound', +export enum OCPP20ConnectorStatusEnumType { + Available = 'Available', + Faulted = 'Faulted', + Occupied = 'Occupied', + Reserved = 'Reserved', + Unavailable = 'Unavailable', } -export enum CertificateActionEnumType { - Install = 'Install', - Update = 'Update', +export enum OperationalStatusEnumType { + Inoperative = 'Inoperative', + Operative = 'Operative', } -export enum CertificateSigningUseEnumType { - ChargingStationCertificate = 'ChargingStationCertificate', - V2GCertificate = 'V2GCertificate', +export interface CertificateHashDataChainType extends JsonObject { + certificateHashData: CertificateHashDataType + certificateType: GetCertificateIdUseEnumType + childCertificateHashData?: CertificateHashDataType } -export type CertificateSignedStatusEnumType = GenericStatusEnumType - export interface CertificateHashDataType extends JsonObject { hashAlgorithm: HashAlgorithmEnumType issuerKeyHash: string @@ -101,12 +103,15 @@ export interface CertificateHashDataType extends JsonObject { serialNumber: string } -export interface CertificateHashDataChainType extends JsonObject { - certificateHashData: CertificateHashDataType - certificateType: GetCertificateIdUseEnumType - childCertificateHashData?: CertificateHashDataType +export type CertificateSignedStatusEnumType = GenericStatusEnumType + +export interface EVSEType extends JsonObject { + connectorId?: string + id: number } +export type GenericStatusEnumType = GenericStatus + export interface OCSPRequestDataType extends JsonObject { hashAlgorithm: HashAlgorithmEnumType issuerKeyHash: string @@ -119,8 +124,3 @@ export interface StatusInfoType extends JsonObject { additionalInfo?: string reasonCode: string } - -export interface EVSEType extends JsonObject { - connectorId?: string - id: number -} diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts index 6fd56088..0cf30f92 100644 --- a/src/types/ocpp/2.0/Requests.ts +++ b/src/types/ocpp/2.0/Requests.ts @@ -7,29 +7,16 @@ import type { } from './Common.js' import type { OCPP20SetVariableDataType } from './Variables.js' -export enum OCPP20RequestCommand { - BOOT_NOTIFICATION = 'BootNotification', - HEARTBEAT = 'Heartbeat', - STATUS_NOTIFICATION = 'StatusNotification', -} - export enum OCPP20IncomingRequestCommand { CLEAR_CACHE = 'ClearCache', REQUEST_START_TRANSACTION = 'RequestStartTransaction', REQUEST_STOP_TRANSACTION = 'RequestStopTransaction', } -interface ModemType extends JsonObject { - iccid?: string - imsi?: string -} - -interface ChargingStationType extends JsonObject { - firmwareVersion?: string - model: string - modem?: ModemType - serialNumber?: string - vendorName: string +export enum OCPP20RequestCommand { + BOOT_NOTIFICATION = 'BootNotification', + HEARTBEAT = 'Heartbeat', + STATUS_NOTIFICATION = 'StatusNotification', } export interface OCPP20BootNotificationRequest extends JsonObject { @@ -37,9 +24,18 @@ export interface OCPP20BootNotificationRequest extends JsonObject { reason: BootReasonEnumType } +export type OCPP20ClearCacheRequest = EmptyObject + export type OCPP20HeartbeatRequest = EmptyObject -export type OCPP20ClearCacheRequest = EmptyObject +export interface OCPP20InstallCertificateRequest extends JsonObject { + certificate: string + certificateType: InstallCertificateUseEnumType +} + +export interface OCPP20SetVariablesRequest extends JsonObject { + setVariableData: OCPP20SetVariableDataType[] +} export interface OCPP20StatusNotificationRequest extends JsonObject { connectorId: number @@ -48,11 +44,15 @@ export interface OCPP20StatusNotificationRequest extends JsonObject { timestamp: Date } -export interface OCPP20SetVariablesRequest extends JsonObject { - setVariableData: OCPP20SetVariableDataType[] +interface ChargingStationType extends JsonObject { + firmwareVersion?: string + model: string + modem?: ModemType + serialNumber?: string + vendorName: string } -export interface OCPP20InstallCertificateRequest extends JsonObject { - certificate: string - certificateType: InstallCertificateUseEnumType +interface ModemType extends JsonObject { + iccid?: string + imsi?: string } diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts index 816eea0b..e735c788 100644 --- a/src/types/ocpp/2.0/Responses.ts +++ b/src/types/ocpp/2.0/Responses.ts @@ -15,22 +15,22 @@ export interface OCPP20BootNotificationResponse extends JsonObject { statusInfo?: StatusInfoType } -export interface OCPP20HeartbeatResponse extends JsonObject { - currentTime: Date -} - export interface OCPP20ClearCacheResponse extends JsonObject { status: GenericStatusEnumType statusInfo?: StatusInfoType } -export type OCPP20StatusNotificationResponse = EmptyObject - -export interface OCPP20SetVariablesResponse extends JsonObject { - setVariableResult: OCPP20SetVariableResultType[] +export interface OCPP20HeartbeatResponse extends JsonObject { + currentTime: Date } export interface OCPP20InstallCertificateResponse extends JsonObject { status: InstallCertificateStatusEnumType statusInfo?: StatusInfoType } + +export interface OCPP20SetVariablesResponse extends JsonObject { + setVariableResult: OCPP20SetVariableResultType[] +} + +export type OCPP20StatusNotificationResponse = EmptyObject diff --git a/src/types/ocpp/2.0/Variables.ts b/src/types/ocpp/2.0/Variables.ts index dac4a3a6..e31da220 100644 --- a/src/types/ocpp/2.0/Variables.ts +++ b/src/types/ocpp/2.0/Variables.ts @@ -1,25 +1,9 @@ import type { JsonObject } from '../../JsonType.js' import type { EVSEType, StatusInfoType } from './Common.js' -enum OCPP20ComponentName { - AlignedDataCtrlr = 'AlignedDataCtrlr', - AuthCacheCtrlr = 'AuthCacheCtrlr', - AuthCtrlr = 'AuthCtrlr', - CHAdeMOCtrlr = 'CHAdeMOCtrlr', - ClockCtrlr = 'ClockCtrlr', - CustomizationCtrlr = 'CustomizationCtrlr', - DeviceDataCtrlr = 'DeviceDataCtrlr', - DisplayMessageCtrlr = 'DisplayMessageCtrlr', - ISO15118Ctrlr = 'ISO15118Ctrlr', - LocalAuthListCtrlr = 'LocalAuthListCtrlr', - MonitoringCtrlr = 'MonitoringCtrlr', - OCPPCommCtrlr = 'OCPPCommCtrlr', - ReservationCtrlr = 'ReservationCtrlr', - SampledDataCtrlr = 'SampledDataCtrlr', - SecurityCtrlr = 'SecurityCtrlr', - SmartChargingCtrlr = 'SmartChargingCtrlr', - TariffCostCtrlr = 'TariffCostCtrlr', - TxCtrlr = 'TxCtrlr', +export enum OCPP20OptionalVariableName { + HeartbeatInterval = 'HeartbeatInterval', + WebSocketPingInterval = 'WebSocketPingInterval', } export enum OCPP20RequiredVariableName { @@ -53,11 +37,6 @@ export enum OCPP20RequiredVariableName { UnlockOnEVSideDisconnect = 'UnlockOnEVSideDisconnect', } -export enum OCPP20OptionalVariableName { - HeartbeatInterval = 'HeartbeatInterval', - WebSocketPingInterval = 'WebSocketPingInterval', -} - export enum OCPP20VendorVariableName { ConnectionUrl = 'ConnectionUrl', } @@ -69,21 +48,39 @@ enum AttributeEnumType { Target = 'Target', } -interface ComponentType extends JsonObject { - evse?: EVSEType - instance?: string - name: OCPP20ComponentName | string +enum OCPP20ComponentName { + AlignedDataCtrlr = 'AlignedDataCtrlr', + AuthCacheCtrlr = 'AuthCacheCtrlr', + AuthCtrlr = 'AuthCtrlr', + CHAdeMOCtrlr = 'CHAdeMOCtrlr', + ClockCtrlr = 'ClockCtrlr', + CustomizationCtrlr = 'CustomizationCtrlr', + DeviceDataCtrlr = 'DeviceDataCtrlr', + DisplayMessageCtrlr = 'DisplayMessageCtrlr', + ISO15118Ctrlr = 'ISO15118Ctrlr', + LocalAuthListCtrlr = 'LocalAuthListCtrlr', + MonitoringCtrlr = 'MonitoringCtrlr', + OCPPCommCtrlr = 'OCPPCommCtrlr', + ReservationCtrlr = 'ReservationCtrlr', + SampledDataCtrlr = 'SampledDataCtrlr', + SecurityCtrlr = 'SecurityCtrlr', + SmartChargingCtrlr = 'SmartChargingCtrlr', + TariffCostCtrlr = 'TariffCostCtrlr', + TxCtrlr = 'TxCtrlr', } -type VariableName = - | OCPP20OptionalVariableName - | OCPP20RequiredVariableName - | OCPP20VendorVariableName - | string +enum SetVariableStatusEnumType { + Accepted = 'Accepted', + NotSupportedAttributeType = 'NotSupportedAttributeType', + RebootRequired = 'RebootRequired', + Rejected = 'Rejected', + UnknownComponent = 'UnknownComponent', + UnknownVariable = 'UnknownVariable', +} -interface VariableType extends JsonObject { - instance?: string - name: VariableName +export interface OCPP20ComponentVariableType extends JsonObject { + component: ComponentType + variable?: VariableType } export interface OCPP20SetVariableDataType extends JsonObject { @@ -93,15 +90,6 @@ export interface OCPP20SetVariableDataType extends JsonObject { variable: VariableType } -enum SetVariableStatusEnumType { - Accepted = 'Accepted', - NotSupportedAttributeType = 'NotSupportedAttributeType', - RebootRequired = 'RebootRequired', - Rejected = 'Rejected', - UnknownComponent = 'UnknownComponent', - UnknownVariable = 'UnknownVariable', -} - export interface OCPP20SetVariableResultType extends JsonObject { attributeStatus: SetVariableStatusEnumType attributeStatusInfo?: StatusInfoType @@ -110,7 +98,19 @@ export interface OCPP20SetVariableResultType extends JsonObject { variable: VariableType } -export interface OCPP20ComponentVariableType extends JsonObject { - component: ComponentType - variable?: VariableType +interface ComponentType extends JsonObject { + evse?: EVSEType + instance?: string + name: OCPP20ComponentName | string +} + +type VariableName = + | OCPP20OptionalVariableName + | OCPP20RequiredVariableName + | OCPP20VendorVariableName + | string + +interface VariableType extends JsonObject { + instance?: string + name: VariableName } diff --git a/src/types/ocpp/Common.ts b/src/types/ocpp/Common.ts index e8923973..7190d9a4 100644 --- a/src/types/ocpp/Common.ts +++ b/src/types/ocpp/Common.ts @@ -5,12 +5,12 @@ export enum GenericStatus { Rejected = 'Rejected', } -export interface GenericResponse extends JsonObject { - status: GenericStatus -} - export enum RegistrationStatusEnumType { ACCEPTED = 'Accepted', PENDING = 'Pending', REJECTED = 'Rejected', } + +export interface GenericResponse extends JsonObject { + status: GenericStatus +} diff --git a/src/types/ocpp/Configuration.ts b/src/types/ocpp/Configuration.ts index c0de4370..f0760742 100644 --- a/src/types/ocpp/Configuration.ts +++ b/src/types/ocpp/Configuration.ts @@ -11,6 +11,25 @@ import { OCPP20VendorVariableName, } from './2.0/Variables.js' +export enum ConnectorPhaseRotation { + NotApplicable = 'NotApplicable', + RST = 'RST', + RTS = 'RTS', + SRT = 'SRT', + STR = 'STR', + TRS = 'TRS', + TSR = 'TSR', + Unknown = 'Unknown', +} + +export type ConfigurationKeyType = StandardParametersKey | string | VendorParametersKey + +export interface OCPPConfigurationKey extends JsonObject { + key: ConfigurationKeyType + readonly: boolean + value?: string +} + export const StandardParametersKey = { ...OCPP16StandardParametersKey, ...OCPP20RequiredVariableName, @@ -31,22 +50,3 @@ export const SupportedFeatureProfiles = { } as const // eslint-disable-next-line @typescript-eslint/no-redeclare export type SupportedFeatureProfiles = OCPP16SupportedFeatureProfiles - -export enum ConnectorPhaseRotation { - NotApplicable = 'NotApplicable', - RST = 'RST', - RTS = 'RTS', - SRT = 'SRT', - STR = 'STR', - TRS = 'TRS', - TSR = 'TSR', - Unknown = 'Unknown', -} - -export type ConfigurationKeyType = StandardParametersKey | string | VendorParametersKey - -export interface OCPPConfigurationKey extends JsonObject { - key: ConfigurationKeyType - readonly: boolean - value?: string -} diff --git a/src/types/ocpp/MessageType.ts b/src/types/ocpp/MessageType.ts index c44cb88f..d8573eee 100644 --- a/src/types/ocpp/MessageType.ts +++ b/src/types/ocpp/MessageType.ts @@ -1,6 +1,7 @@ +/* eslint-disable perfectionist/sort-enums */ export enum MessageType { CALL_MESSAGE = 2, // Caller to Callee CALL_RESULT_MESSAGE = 3, // Callee to Caller - // eslint-disable-next-line perfectionist/sort-enums CALL_ERROR_MESSAGE = 4, // Callee to Caller } +/* eslint-enable perfectionist/sort-enums */ diff --git a/src/types/ocpp/MeterValues.ts b/src/types/ocpp/MeterValues.ts index 6a44bb74..7c222e3a 100644 --- a/src/types/ocpp/MeterValues.ts +++ b/src/types/ocpp/MeterValues.ts @@ -8,6 +8,8 @@ import { type OCPP16SampledValue, } from './1.6/MeterValues.js' +export type MeterValue = OCPP16MeterValue + export const MeterValueUnit = { ...OCPP16MeterValueUnit, } as const @@ -20,18 +22,18 @@ export const MeterValueContext = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type MeterValueContext = OCPP16MeterValueContext -export const MeterValueMeasurand = { - ...OCPP16MeterValueMeasurand, -} as const -// eslint-disable-next-line @typescript-eslint/no-redeclare -export type MeterValueMeasurand = OCPP16MeterValueMeasurand - export const MeterValueLocation = { ...OCPP16MeterValueLocation, } as const // eslint-disable-next-line @typescript-eslint/no-redeclare export type MeterValueLocation = OCPP16MeterValueLocation +export const MeterValueMeasurand = { + ...OCPP16MeterValueMeasurand, +} as const +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type MeterValueMeasurand = OCPP16MeterValueMeasurand + export const MeterValuePhase = { ...OCPP16MeterValuePhase, } as const @@ -39,5 +41,3 @@ export const MeterValuePhase = { export type MeterValuePhase = OCPP16MeterValuePhase export type SampledValue = OCPP16SampledValue - -export type MeterValue = OCPP16MeterValue diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index a6171b2d..79bd00b3 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -28,20 +28,28 @@ import { type OCPP20StatusNotificationRequest, } from './2.0/Requests.js' -export const RequestCommand = { - ...OCPP16RequestCommand, - ...OCPP20RequestCommand, -} as const -// eslint-disable-next-line @typescript-eslint/no-redeclare -export type RequestCommand = OCPP16RequestCommand | OCPP20RequestCommand +export type BootNotificationRequest = OCPP16BootNotificationRequest | OCPP20BootNotificationRequest -export type OutgoingRequest = [MessageType.CALL_MESSAGE, string, RequestCommand, JsonType] +export type CachedRequest = [ + ResponseCallback, + ErrorCallback, + IncomingRequestCommand | RequestCommand, + JsonType +] -export interface RequestParams { - skipBufferingOnError?: boolean - throwError?: boolean - triggerMessage?: boolean -} +export type DataTransferRequest = OCPP16DataTransferRequest + +export type DiagnosticsStatusNotificationRequest = OCPP16DiagnosticsStatusNotificationRequest + +export type ErrorCallback = (ocppError: OCPPError, requestStatistic?: boolean) => void + +export type FirmwareStatusNotificationRequest = OCPP16FirmwareStatusNotificationRequest + +export type HeartbeatRequest = OCPP16HeartbeatRequest + +export type IncomingRequest = [MessageType.CALL_MESSAGE, string, IncomingRequestCommand, JsonType] + +export type OutgoingRequest = [MessageType.CALL_MESSAGE, string, RequestCommand, JsonType] export const IncomingRequestCommand = { ...OCPP16IncomingRequestCommand, @@ -50,23 +58,23 @@ export const IncomingRequestCommand = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type IncomingRequestCommand = OCPP16IncomingRequestCommand | OCPP20IncomingRequestCommand -export type IncomingRequest = [MessageType.CALL_MESSAGE, string, IncomingRequestCommand, JsonType] - export type IncomingRequestHandler = ( chargingStation: ChargingStation, commandPayload: JsonType ) => JsonType | Promise -export type ResponseCallback = (payload: JsonType, requestPayload: JsonType) => void - -export type ErrorCallback = (ocppError: OCPPError, requestStatistic?: boolean) => void +export const RequestCommand = { + ...OCPP16RequestCommand, + ...OCPP20RequestCommand, +} as const +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type RequestCommand = OCPP16RequestCommand | OCPP20RequestCommand -export type CachedRequest = [ - ResponseCallback, - ErrorCallback, - IncomingRequestCommand | RequestCommand, - JsonType -] +export interface RequestParams { + skipBufferingOnError?: boolean + throwError?: boolean + triggerMessage?: boolean +} export const MessageTrigger = { ...OCPP16MessageTrigger, @@ -74,22 +82,14 @@ export const MessageTrigger = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type MessageTrigger = OCPP16MessageTrigger -export type BootNotificationRequest = OCPP16BootNotificationRequest | OCPP20BootNotificationRequest +export type MeterValuesRequest = OCPP16MeterValuesRequest -export type HeartbeatRequest = OCPP16HeartbeatRequest +export type ResponseCallback = (payload: JsonType, requestPayload: JsonType) => void export type StatusNotificationRequest = | OCPP16StatusNotificationRequest | OCPP20StatusNotificationRequest -export type MeterValuesRequest = OCPP16MeterValuesRequest - -export type DataTransferRequest = OCPP16DataTransferRequest - -export type DiagnosticsStatusNotificationRequest = OCPP16DiagnosticsStatusNotificationRequest - -export type FirmwareStatusNotificationRequest = OCPP16FirmwareStatusNotificationRequest - export const AvailabilityType = { ...OCPP16AvailabilityType, ...OperationalStatusEnumType, @@ -97,6 +97,8 @@ export const AvailabilityType = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type AvailabilityType = OCPP16AvailabilityType | OperationalStatusEnumType +export type CancelReservationRequest = OCPP16CancelReservationRequest + export const DiagnosticsStatus = { ...OCPP16DiagnosticsStatus, } as const @@ -109,8 +111,6 @@ export const FirmwareStatus = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type FirmwareStatus = OCPP16FirmwareStatus -export type ResponseType = JsonType | OCPPError - export type ReserveNowRequest = OCPP16ReserveNowRequest -export type CancelReservationRequest = OCPP16CancelReservationRequest +export type ResponseType = JsonType | OCPPError diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts index 1a1b95c1..bc4fa266 100644 --- a/src/types/ocpp/Reservation.ts +++ b/src/types/ocpp/Reservation.ts @@ -1,9 +1,5 @@ import type { OCPP16ReserveNowRequest } from './1.6/Requests.js' -export type Reservation = OCPP16ReserveNowRequest - -export type ReservationKey = keyof Reservation - export enum ReservationTerminationReason { CONNECTOR_STATE_CHANGED = 'ConnectorStateChanged', EXPIRED = 'Expired', @@ -11,3 +7,7 @@ export enum ReservationTerminationReason { RESERVATION_CANCELED = 'ReservationCanceled', TRANSACTION_STARTED = 'TransactionStarted', } + +export type Reservation = OCPP16ReserveNowRequest + +export type ReservationKey = keyof Reservation diff --git a/src/types/ocpp/Responses.ts b/src/types/ocpp/Responses.ts index 9676ff74..ec444fcf 100644 --- a/src/types/ocpp/Responses.ts +++ b/src/types/ocpp/Responses.ts @@ -23,34 +23,34 @@ import { } from './1.6/Responses.js' import { type GenericResponse, GenericStatus } from './Common.js' -export type Response = [MessageType.CALL_RESULT_MESSAGE, string, JsonType] - -export type ErrorResponse = [MessageType.CALL_ERROR_MESSAGE, string, ErrorType, string, JsonType] - -export type ResponseHandler = ( - chargingStation: ChargingStation, - payload: JsonType, - requestPayload?: JsonType -) => Promise | void - export type BootNotificationResponse = | OCPP16BootNotificationResponse | OCPP20BootNotificationResponse -export type HeartbeatResponse = OCPP16HeartbeatResponse - export type ClearCacheResponse = GenericResponse | OCPP20ClearCacheResponse -export type StatusNotificationResponse = OCPP16StatusNotificationResponse - -export type MeterValuesResponse = OCPP16MeterValuesResponse - export type DataTransferResponse = OCPP16DataTransferResponse export type DiagnosticsStatusNotificationResponse = OCPP16DiagnosticsStatusNotificationResponse +export type ErrorResponse = [MessageType.CALL_ERROR_MESSAGE, string, ErrorType, string, JsonType] + export type FirmwareStatusNotificationResponse = OCPP16FirmwareStatusNotificationResponse +export type HeartbeatResponse = OCPP16HeartbeatResponse + +export type MeterValuesResponse = OCPP16MeterValuesResponse + +export type Response = [MessageType.CALL_RESULT_MESSAGE, string, JsonType] + +export type ResponseHandler = ( + chargingStation: ChargingStation, + payload: JsonType, + requestPayload?: JsonType +) => Promise | void + +export type StatusNotificationResponse = OCPP16StatusNotificationResponse + export const AvailabilityStatus = { ...OCPP16AvailabilityStatus, } as const diff --git a/src/types/ocpp/Transaction.ts b/src/types/ocpp/Transaction.ts index 5379f111..83f16acc 100644 --- a/src/types/ocpp/Transaction.ts +++ b/src/types/ocpp/Transaction.ts @@ -19,16 +19,16 @@ export type AuthorizeRequest = OCPP16AuthorizeRequest export type AuthorizeResponse = OCPP16AuthorizeResponse +export type StartTransactionRequest = OCPP16StartTransactionRequest + +export type StartTransactionResponse = OCPP16StartTransactionResponse + export const StopTransactionReason = { ...OCPP16StopTransactionReason, } as const // eslint-disable-next-line @typescript-eslint/no-redeclare export type StopTransactionReason = OCPP16StopTransactionReason -export type StartTransactionRequest = OCPP16StartTransactionRequest - -export type StartTransactionResponse = OCPP16StartTransactionResponse - export type StopTransactionRequest = OCPP16StopTransactionRequest export type StopTransactionResponse = OCPP16StopTransactionResponse diff --git a/src/utils/AsyncLock.ts b/src/utils/AsyncLock.ts index e8c66f95..49565046 100644 --- a/src/utils/AsyncLock.ts +++ b/src/utils/AsyncLock.ts @@ -21,6 +21,19 @@ export class AsyncLock { this.resolveQueue = new Queue() } + public static async runExclusive(type: AsyncLockType, fn: () => Promise | T): Promise { + try { + await AsyncLock.acquire(type) + if (isAsyncFunction(fn)) { + return await fn() + } else { + return fn() as T + } + } finally { + await AsyncLock.release(type) + } + } + private static async acquire (type: AsyncLockType): Promise { const asyncLock = AsyncLock.getAsyncLock(type) if (!asyncLock.acquired) { @@ -52,17 +65,4 @@ export class AsyncLock { resolve() }) } - - public static async runExclusive(type: AsyncLockType, fn: () => Promise | T): Promise { - try { - await AsyncLock.acquire(type) - if (isAsyncFunction(fn)) { - return await fn() - } else { - return fn() as T - } - } finally { - await AsyncLock.release(type) - } - } } diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index d9b19b87..c9708323 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -127,6 +127,82 @@ export class Configuration { // This is intentional } + public static getConfigurationData (): ConfigurationData | undefined { + if ( + Configuration.configurationData == null && + Configuration.configurationFile != null && + Configuration.configurationFile.length > 0 + ) { + try { + Configuration.configurationData = JSON.parse( + readFileSync(Configuration.configurationFile, 'utf8') + ) as ConfigurationData + if (Configuration.configurationFileWatcher == null) { + Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher() + } + } catch (error) { + handleFileException( + Configuration.configurationFile, + FileType.Configuration, + error as NodeJS.ErrnoException, + logPrefix() + ) + } + } + return Configuration.configurationData + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public static getConfigurationSection( + sectionName: ConfigurationSection + ): T { + if (!Configuration.isConfigurationSectionCached(sectionName)) { + Configuration.cacheConfigurationSection(sectionName) + } + return Configuration.configurationSectionCache.get(sectionName) as T + } + + public static getStationTemplateUrls (): StationTemplateUrl[] | undefined { + const checkDeprecatedConfigurationKeysOnce = once( + Configuration.checkDeprecatedConfigurationKeys.bind(Configuration) + ) + checkDeprecatedConfigurationKeysOnce() + return Configuration.getConfigurationData()?.stationTemplateUrls + } + + public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined { + return has(Configuration.getConfigurationData(), 'supervisionUrlDistribution') + ? Configuration.getConfigurationData()?.supervisionUrlDistribution + : SupervisionUrlDistribution.ROUND_ROBIN + } + + public static getSupervisionUrls (): string | string[] | undefined { + if ( + Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![ + 'supervisionURLs' as keyof ConfigurationData + ] as string | string[] + } + return Configuration.getConfigurationData()?.supervisionUrls + } + + public static workerDynamicPoolInUse (): boolean { + return ( + Configuration.getConfigurationSection(ConfigurationSection.worker) + .processType === WorkerProcessType.dynamicPool + ) + } + + public static workerPoolInUse (): boolean { + return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Configuration.getConfigurationSection(ConfigurationSection.worker) + .processType! + ) + } + private static buildLogSection (): LogConfiguration { const deprecatedLogConfiguration: LogConfiguration = { ...(has('logEnabled', Configuration.getConfigurationData()) && { @@ -501,31 +577,6 @@ export class Configuration { } } - public static getConfigurationData (): ConfigurationData | undefined { - if ( - Configuration.configurationData == null && - Configuration.configurationFile != null && - Configuration.configurationFile.length > 0 - ) { - try { - Configuration.configurationData = JSON.parse( - readFileSync(Configuration.configurationFile, 'utf8') - ) as ConfigurationData - if (Configuration.configurationFileWatcher == null) { - Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher() - } - } catch (error) { - handleFileException( - Configuration.configurationFile, - FileType.Configuration, - error as NodeJS.ErrnoException, - logPrefix() - ) - } - } - return Configuration.configurationData - } - private static getConfigurationFileWatcher (): FSWatcher | undefined { if (Configuration.configurationFile == null || Configuration.configurationFile.length === 0) { return @@ -571,42 +622,6 @@ export class Configuration { } } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - public static getConfigurationSection( - sectionName: ConfigurationSection - ): T { - if (!Configuration.isConfigurationSectionCached(sectionName)) { - Configuration.cacheConfigurationSection(sectionName) - } - return Configuration.configurationSectionCache.get(sectionName) as T - } - - public static getStationTemplateUrls (): StationTemplateUrl[] | undefined { - const checkDeprecatedConfigurationKeysOnce = once( - Configuration.checkDeprecatedConfigurationKeys.bind(Configuration) - ) - checkDeprecatedConfigurationKeysOnce() - return Configuration.getConfigurationData()?.stationTemplateUrls - } - - public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined { - return has(Configuration.getConfigurationData(), 'supervisionUrlDistribution') - ? Configuration.getConfigurationData()?.supervisionUrlDistribution - : SupervisionUrlDistribution.ROUND_ROBIN - } - - public static getSupervisionUrls (): string | string[] | undefined { - if ( - Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null - ) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![ - 'supervisionURLs' as keyof ConfigurationData - ] as string | string[] - } - return Configuration.getConfigurationData()?.supervisionUrls - } - private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean { return Configuration.configurationSectionCache.has(sectionName) } @@ -643,19 +658,4 @@ export class Configuration { ) } } - - public static workerDynamicPoolInUse (): boolean { - return ( - Configuration.getConfigurationSection(ConfigurationSection.worker) - .processType === WorkerProcessType.dynamicPool - ) - } - - public static workerPoolInUse (): boolean { - return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - Configuration.getConfigurationSection(ConfigurationSection.worker) - .processType! - ) - } } diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 8dfd29db..8ee1d8ab 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,8 +1,8 @@ import type { FormatWrap } from 'logform' import { createLogger, format, type transport } from 'winston' -import TransportType from 'winston/lib/winston/transports/index.js' import DailyRotateFile from 'winston-daily-rotate-file' +import TransportType from 'winston/lib/winston/transports/index.js' import { ConfigurationSection, type LogConfiguration } from '../types/index.js' import { Configuration } from './Configuration.js' diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 6f0e6b9f..57e7f7e3 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -228,7 +228,7 @@ export const isCFEnvironment = (): boolean => { } declare const nonEmptyString: unique symbol -type NonEmptyString = { [nonEmptyString]: true } & string +type NonEmptyString = string & { [nonEmptyString]: true } export const isNotEmptyString = (value: unknown): value is NonEmptyString => { return typeof value === 'string' && value.trim().length > 0 } diff --git a/src/worker/WorkerAbstract.ts b/src/worker/WorkerAbstract.ts index d985209e..d4282e0c 100644 --- a/src/worker/WorkerAbstract.ts +++ b/src/worker/WorkerAbstract.ts @@ -6,13 +6,14 @@ import { existsSync } from 'node:fs' import type { SetInfo, WorkerData, WorkerOptions } from './WorkerTypes.js' export abstract class WorkerAbstract { - protected readonly workerOptions: WorkerOptions - protected readonly workerScript: string public abstract readonly emitter: EventEmitterAsyncResource | undefined public abstract readonly info: PoolInfo | SetInfo public abstract readonly maxElementsPerWorker: number | undefined public abstract readonly size: number + protected readonly workerOptions: WorkerOptions + protected readonly workerScript: string + /** * `WorkerAbstract` constructor. * @param workerScript - diff --git a/src/worker/WorkerConstants.ts b/src/worker/WorkerConstants.ts index 07c55757..0d7407dc 100644 --- a/src/worker/WorkerConstants.ts +++ b/src/worker/WorkerConstants.ts @@ -16,7 +16,7 @@ export const DEFAULT_POOL_MIN_SIZE = Math.floor(availableParallelism() / 2) export const DEFAULT_POOL_MAX_SIZE = Math.round(availableParallelism() * 1.5) export const DEFAULT_ELEMENTS_PER_WORKER = 1 -export const DEFAULT_WORKER_OPTIONS: WorkerOptions = Object.freeze({ +export const DEFAULT_WORKER_OPTIONS: Readonly = Object.freeze({ elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY, elementsPerWorker: DEFAULT_ELEMENTS_PER_WORKER, poolMaxSize: DEFAULT_POOL_MAX_SIZE, diff --git a/src/worker/WorkerDynamicPool.ts b/src/worker/WorkerDynamicPool.ts index 38115c0b..e5808d30 100644 --- a/src/worker/WorkerDynamicPool.ts +++ b/src/worker/WorkerDynamicPool.ts @@ -11,6 +11,22 @@ export class WorkerDynamicPool exten D, R > { + get emitter (): EventEmitterAsyncResource | undefined { + return this.pool.emitter + } + + get info (): PoolInfo { + return this.pool.info + } + + get maxElementsPerWorker (): number | undefined { + return undefined + } + + get size (): number { + return this.pool.info.workerNodes + } + private readonly pool: DynamicThreadPool /** @@ -48,20 +64,4 @@ export class WorkerDynamicPool exten public async stop (): Promise { await this.pool.destroy() } - - get emitter (): EventEmitterAsyncResource | undefined { - return this.pool.emitter - } - - get info (): PoolInfo { - return this.pool.info - } - - get maxElementsPerWorker (): number | undefined { - return undefined - } - - get size (): number { - return this.pool.info.workerNodes - } } diff --git a/src/worker/WorkerFixedPool.ts b/src/worker/WorkerFixedPool.ts index 22d6fcd1..8bddad7d 100644 --- a/src/worker/WorkerFixedPool.ts +++ b/src/worker/WorkerFixedPool.ts @@ -11,6 +11,22 @@ export class WorkerFixedPool extends D, R > { + get emitter (): EventEmitterAsyncResource | undefined { + return this.pool.emitter + } + + get info (): PoolInfo { + return this.pool.info + } + + get maxElementsPerWorker (): number | undefined { + return undefined + } + + get size (): number { + return this.pool.info.workerNodes + } + private readonly pool: FixedThreadPool /** @@ -47,20 +63,4 @@ export class WorkerFixedPool extends public async stop (): Promise { await this.pool.destroy() } - - get emitter (): EventEmitterAsyncResource | undefined { - return this.pool.emitter - } - - get info (): PoolInfo { - return this.pool.info - } - - get maxElementsPerWorker (): number | undefined { - return undefined - } - - get size (): number { - return this.pool.info.workerNodes - } } diff --git a/src/worker/WorkerSet.ts b/src/worker/WorkerSet.ts index 2cec2d5f..babf6932 100644 --- a/src/worker/WorkerSet.ts +++ b/src/worker/WorkerSet.ts @@ -24,6 +24,32 @@ interface ResponseWrapper { } export class WorkerSet extends WorkerAbstract { + public readonly emitter: EventEmitterAsyncResource | undefined + + get info (): SetInfo { + return { + elementsExecuting: [...this.workerSet].reduce( + (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements, + 0 + ), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + elementsPerWorker: this.maxElementsPerWorker!, + size: this.size, + started: this.started, + type: 'set', + version: workerSetVersion, + worker: 'thread', + } + } + + get maxElementsPerWorker (): number | undefined { + return this.workerOptions.elementsPerWorker + } + + get size (): number { + return this.workerSet.size + } + private readonly promiseResponseMap: Map< `${string}-${string}-${string}-${string}`, ResponseWrapper @@ -32,7 +58,6 @@ export class WorkerSet extends Worke private started: boolean private readonly workerSet: Set private workerStartup: boolean - public readonly emitter: EventEmitterAsyncResource | undefined /** * Creates a new `WorkerSet`. @@ -62,6 +87,65 @@ export class WorkerSet extends Worke this.workerStartup = false } + /** @inheritDoc */ + public async addElement (elementData: D): Promise { + if (!this.started) { + throw new Error('Cannot add a WorkerSet element: not started') + } + const workerSetElement = await this.getWorkerSetElement() + const sendMessageToWorker = new Promise((resolve, reject) => { + const message = { + data: elementData, + event: WorkerMessageEvents.addWorkerElement, + uuid: randomUUID(), + } satisfies WorkerMessage + workerSetElement.worker.postMessage(message) + this.promiseResponseMap.set(message.uuid, { + reject, + resolve, + workerSetElement, + }) + }) + const response = await sendMessageToWorker + // Add element sequentially to optimize memory at startup + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (this.workerOptions.elementAddDelay! > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)) + } + return response + } + + /** @inheritDoc */ + public async start (): Promise { + this.addWorkerSetElement() + // Add worker set element sequentially to optimize memory at startup + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.workerOptions.workerStartDelay! > 0 && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!))) + this.emitter?.emit(WorkerSetEvents.started, this.info) + this.started = true + } + + /** @inheritDoc */ + public async stop (): Promise { + for (const workerSetElement of this.workerSet) { + const worker = workerSetElement.worker + const waitWorkerExit = new Promise(resolve => { + worker.once('exit', () => { + resolve() + }) + }) + worker.unref() + await worker.terminate() + await waitWorkerExit + } + this.emitter?.emit(WorkerSetEvents.stopped, this.info) + this.started = false + this.emitter?.emitDestroy() + } + /** * Adds a new `WorkerSetElement`. * @returns The new `WorkerSetElement`. @@ -167,87 +251,4 @@ export class WorkerSet extends Worke } this.workerSet.delete(workerSetElement) } - - /** @inheritDoc */ - public async addElement (elementData: D): Promise { - if (!this.started) { - throw new Error('Cannot add a WorkerSet element: not started') - } - const workerSetElement = await this.getWorkerSetElement() - const sendMessageToWorker = new Promise((resolve, reject) => { - const message = { - data: elementData, - event: WorkerMessageEvents.addWorkerElement, - uuid: randomUUID(), - } satisfies WorkerMessage - workerSetElement.worker.postMessage(message) - this.promiseResponseMap.set(message.uuid, { - reject, - resolve, - workerSetElement, - }) - }) - const response = await sendMessageToWorker - // Add element sequentially to optimize memory at startup - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (this.workerOptions.elementAddDelay! > 0) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)) - } - return response - } - - /** @inheritDoc */ - public async start (): Promise { - this.addWorkerSetElement() - // Add worker set element sequentially to optimize memory at startup - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.workerOptions.workerStartDelay! > 0 && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!))) - this.emitter?.emit(WorkerSetEvents.started, this.info) - this.started = true - } - - /** @inheritDoc */ - public async stop (): Promise { - for (const workerSetElement of this.workerSet) { - const worker = workerSetElement.worker - const waitWorkerExit = new Promise(resolve => { - worker.once('exit', () => { - resolve() - }) - }) - worker.unref() - await worker.terminate() - await waitWorkerExit - } - this.emitter?.emit(WorkerSetEvents.stopped, this.info) - this.started = false - this.emitter?.emitDestroy() - } - - get info (): SetInfo { - return { - elementsExecuting: [...this.workerSet].reduce( - (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements, - 0 - ), - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - elementsPerWorker: this.maxElementsPerWorker!, - size: this.size, - started: this.started, - type: 'set', - version: workerSetVersion, - worker: 'thread', - } - } - - get maxElementsPerWorker (): number | undefined { - return this.workerOptions.elementsPerWorker - } - - get size (): number { - return this.workerSet.size - } } diff --git a/src/worker/WorkerTypes.ts b/src/worker/WorkerTypes.ts index 336c5c62..0f272fe9 100644 --- a/src/worker/WorkerTypes.ts +++ b/src/worker/WorkerTypes.ts @@ -2,6 +2,12 @@ import type { Worker } from 'node:worker_threads' import { type PoolEvent, PoolEvents, type ThreadPoolOptions } from 'poolifier' +export enum WorkerMessageEvents { + addedWorkerElement = 'addedWorkerElement', + addWorkerElement = 'addWorkerElement', + workerElementError = 'workerElementError', +} + export enum WorkerProcessType { /** @experimental */ dynamicPool = 'dynamicPool', @@ -9,6 +15,14 @@ export enum WorkerProcessType { workerSet = 'workerSet', } +export enum WorkerSetEvents { + elementAdded = 'elementAdded', + elementError = 'elementError', + error = 'error', + started = 'started', + stopped = 'stopped', +} + export interface SetInfo { elementsExecuting: number elementsPerWorker: number @@ -19,12 +33,13 @@ export interface SetInfo { worker: string } -export enum WorkerSetEvents { - elementAdded = 'elementAdded', - elementError = 'elementError', - error = 'error', - started = 'started', - stopped = 'stopped', +export type WorkerData = Record + +export interface WorkerDataError extends WorkerData { + event: WorkerMessageEvents + message: string + name: string + stack?: string } export const WorkerEvents = { @@ -34,6 +49,12 @@ export const WorkerEvents = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type WorkerEvents = PoolEvent | WorkerSetEvents +export interface WorkerMessage { + data: T + event: WorkerMessageEvents + uuid: `${string}-${string}-${string}-${string}` +} + export interface WorkerOptions { elementAddDelay?: number elementsPerWorker?: number @@ -43,28 +64,7 @@ export interface WorkerOptions { workerStartDelay?: number } -export type WorkerData = Record - -export interface WorkerDataError extends WorkerData { - event: WorkerMessageEvents - message: string - name: string - stack?: string -} - export interface WorkerSetElement { numberOfWorkerElements: number worker: Worker } - -export interface WorkerMessage { - data: T - event: WorkerMessageEvents - uuid: `${string}-${string}-${string}-${string}` -} - -export enum WorkerMessageEvents { - addedWorkerElement = 'addedWorkerElement', - addWorkerElement = 'addWorkerElement', - workerElementError = 'workerElementError', -} diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index c88baa0f..bf874330 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -254,15 +254,15 @@ await describe('Utils test suite', async () => { // eslint-disable-next-line @typescript-eslint/no-empty-function expect(isAsyncFunction(async function named () {})).toBe(true) class TestClass { - // eslint-disable-next-line @typescript-eslint/no-empty-function - public testArrowAsync = async (): Promise => {} - // eslint-disable-next-line @typescript-eslint/no-empty-function - public testArrowSync = (): void => {} // eslint-disable-next-line @typescript-eslint/no-empty-function public static async testStaticAsync (): Promise {} // eslint-disable-next-line @typescript-eslint/no-empty-function public static testStaticSync (): void {} // eslint-disable-next-line @typescript-eslint/no-empty-function + public testArrowAsync = async (): Promise => {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + public testArrowSync = (): void => {} + // eslint-disable-next-line @typescript-eslint/no-empty-function public async testAsync (): Promise {} // eslint-disable-next-line @typescript-eslint/no-empty-function public testSync (): void {} diff --git a/ui/web/package.json b/ui/web/package.json index 73817b53..cae83074 100644 --- a/ui/web/package.json +++ b/ui/web/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@tsconfig/node22": "^22.0.0", "@types/jsdom": "^21.1.7", - "@types/node": "^22.9.0", + "@types/node": "^22.9.1", "@vitejs/plugin-vue": "^5.2.0", "@vitejs/plugin-vue-jsx": "^4.1.0", "@vitest/coverage-v8": "^2.1.5", diff --git a/ui/web/src/composables/UIClient.ts b/ui/web/src/composables/UIClient.ts index 398edd81..4bb33ab3 100644 --- a/ui/web/src/composables/UIClient.ts +++ b/ui/web/src/composables/UIClient.ts @@ -46,126 +46,6 @@ export class UIClient { return UIClient.instance } - private openWS (): void { - const protocols = - this.uiServerConfiguration.authentication?.enabled === true && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH - ? [ - `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`, - `authorization.basic.${btoa( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}` - ).replace(/={1,2}$/, '')}`, - ] - : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}` - this.ws = new WebSocket( - `${ - this.uiServerConfiguration.secure === true - ? ApplicationProtocol.WSS - : ApplicationProtocol.WS - }://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}`, - protocols - ) - this.ws.onopen = () => { - useToast().success( - `WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}' successfully opened` - ) - } - this.ws.onmessage = this.responseHandler.bind(this) - this.ws.onerror = errorEvent => { - useToast().error( - `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'` - ) - console.error( - `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'`, - errorEvent - ) - } - this.ws.onclose = () => { - useToast().info('WebSocket to UI server closed') - } - } - - private responseHandler (messageEvent: MessageEvent): void { - let response: ProtocolResponse - try { - response = JSON.parse(messageEvent.data) as ProtocolResponse - } catch (error) { - useToast().error('Invalid response JSON format') - console.error('Invalid response JSON format', error) - return - } - - if (!Array.isArray(response)) { - useToast().error('Response not an array') - console.error('Response not an array:', response) - return - } - - const [uuid, responsePayload] = response - - if (!validateUUID(uuid)) { - useToast().error('Response UUID field is invalid') - console.error('Response UUID field is invalid:', response) - return - } - - if (this.responseHandlers.has(uuid)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { procedureName, reject, resolve } = this.responseHandlers.get(uuid)! - switch (responsePayload.status) { - case ResponseStatus.FAILURE: - reject(responsePayload) - break - case ResponseStatus.SUCCESS: - resolve(responsePayload) - break - default: - reject( - new Error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'` - ) - ) - } - this.responseHandlers.delete(uuid) - } else { - throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`) - } - } - - private async sendRequest ( - procedureName: ProcedureName, - payload: RequestPayload - ): Promise { - return new Promise((resolve, reject) => { - if (this.ws?.readyState === WebSocket.OPEN) { - const uuid = randomUUID() - const msg = JSON.stringify([uuid, procedureName, payload]) - const sendTimeout = setTimeout(() => { - this.responseHandlers.delete(uuid) - reject(new Error(`Send request '${procedureName}' message: connection timeout`)) - }, 60000) - try { - this.ws.send(msg) - this.responseHandlers.set(uuid, { procedureName, reject, resolve }) - } catch (error) { - this.responseHandlers.delete(uuid) - reject( - new Error( - `Send request '${procedureName}' message: error ${(error as Error).toString()}` - ) - ) - } finally { - clearTimeout(sendTimeout) - } - } else { - reject(new Error(`Send request '${procedureName}' message: connection closed`)) - } - }) - } - public async addChargingStations ( template: string, numberOfStations: number, @@ -301,4 +181,124 @@ export class UIClient { ) { this.ws?.removeEventListener(event, listener, options) } + + private openWS (): void { + const protocols = + this.uiServerConfiguration.authentication?.enabled === true && + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH + ? [ + `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`, + `authorization.basic.${btoa( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}` + ).replace(/={1,2}$/, '')}`, + ] + : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}` + this.ws = new WebSocket( + `${ + this.uiServerConfiguration.secure === true + ? ApplicationProtocol.WSS + : ApplicationProtocol.WS + }://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}`, + protocols + ) + this.ws.onopen = () => { + useToast().success( + `WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}' successfully opened` + ) + } + this.ws.onmessage = this.responseHandler.bind(this) + this.ws.onerror = errorEvent => { + useToast().error( + `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'` + ) + console.error( + `Error in WebSocket to UI server '${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port.toString()}'`, + errorEvent + ) + } + this.ws.onclose = () => { + useToast().info('WebSocket to UI server closed') + } + } + + private responseHandler (messageEvent: MessageEvent): void { + let response: ProtocolResponse + try { + response = JSON.parse(messageEvent.data) as ProtocolResponse + } catch (error) { + useToast().error('Invalid response JSON format') + console.error('Invalid response JSON format', error) + return + } + + if (!Array.isArray(response)) { + useToast().error('Response not an array') + console.error('Response not an array:', response) + return + } + + const [uuid, responsePayload] = response + + if (!validateUUID(uuid)) { + useToast().error('Response UUID field is invalid') + console.error('Response UUID field is invalid:', response) + return + } + + if (this.responseHandlers.has(uuid)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const { procedureName, reject, resolve } = this.responseHandlers.get(uuid)! + switch (responsePayload.status) { + case ResponseStatus.FAILURE: + reject(responsePayload) + break + case ResponseStatus.SUCCESS: + resolve(responsePayload) + break + default: + reject( + new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'` + ) + ) + } + this.responseHandlers.delete(uuid) + } else { + throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`) + } + } + + private async sendRequest ( + procedureName: ProcedureName, + payload: RequestPayload + ): Promise { + return new Promise((resolve, reject) => { + if (this.ws?.readyState === WebSocket.OPEN) { + const uuid = randomUUID() + const msg = JSON.stringify([uuid, procedureName, payload]) + const sendTimeout = setTimeout(() => { + this.responseHandlers.delete(uuid) + reject(new Error(`Send request '${procedureName}' message: connection timeout`)) + }, 60000) + try { + this.ws.send(msg) + this.responseHandlers.set(uuid, { procedureName, reject, resolve }) + } catch (error) { + this.responseHandlers.delete(uuid) + reject( + new Error( + `Send request '${procedureName}' message: error ${(error as Error).toString()}` + ) + ) + } finally { + clearTimeout(sendTimeout) + } + } else { + reject(new Error(`Send request '${procedureName}' message: connection closed`)) + } + }) + } } diff --git a/ui/web/src/types/ChargingStationType.ts b/ui/web/src/types/ChargingStationType.ts index a074f159..357d177d 100644 --- a/ui/web/src/types/ChargingStationType.ts +++ b/ui/web/src/types/ChargingStationType.ts @@ -1,11 +1,109 @@ import type { JsonObject } from './JsonType' +export enum AmpereUnits { + AMPERE = 'A', + CENTI_AMPERE = 'cA', + DECI_AMPERE = 'dA', + MILLI_AMPERE = 'mA', +} + +export enum CurrentType { + AC = 'AC', + DC = 'DC', +} + export enum IdTagDistribution { CONNECTOR_AFFINITY = 'connector-affinity', RANDOM = 'random', ROUND_ROBIN = 'round-robin', } +export enum OCPP16AvailabilityType { + INOPERATIVE = 'Inoperative', + OPERATIVE = 'Operative', +} + +export enum OCPP16ChargePointStatus { + AVAILABLE = 'Available', + CHARGING = 'Charging', + FAULTED = 'Faulted', + FINISHING = 'Finishing', + OCCUPIED = 'Occupied', + PREPARING = 'Preparing', + RESERVED = 'Reserved', + SUSPENDED_EV = 'SuspendedEV', + SUSPENDED_EVSE = 'SuspendedEVSE', + UNAVAILABLE = 'Unavailable', +} + +export enum OCPP16FirmwareStatus { + Downloaded = 'Downloaded', + DownloadFailed = 'DownloadFailed', + Downloading = 'Downloading', + Idle = 'Idle', + InstallationFailed = 'InstallationFailed', + Installed = 'Installed', + Installing = 'Installing', +} + +export enum OCPP16IncomingRequestCommand { + CHANGE_AVAILABILITY = 'ChangeAvailability', + CHANGE_CONFIGURATION = 'ChangeConfiguration', + CLEAR_CACHE = 'ClearCache', + CLEAR_CHARGING_PROFILE = 'ClearChargingProfile', + GET_CONFIGURATION = 'GetConfiguration', + GET_DIAGNOSTICS = 'GetDiagnostics', + REMOTE_START_TRANSACTION = 'RemoteStartTransaction', + REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction', + RESET = 'Reset', + SET_CHARGING_PROFILE = 'SetChargingProfile', + TRIGGER_MESSAGE = 'TriggerMessage', + UNLOCK_CONNECTOR = 'UnlockConnector', +} + +export enum OCPP16MessageTrigger { + BootNotification = 'BootNotification', + DiagnosticsStatusNotification = 'DiagnosticsStatusNotification', + FirmwareStatusNotification = 'FirmwareStatusNotification', + Heartbeat = 'Heartbeat', + MeterValues = 'MeterValues', + StatusNotification = 'StatusNotification', +} + +export enum OCPP16RegistrationStatus { + ACCEPTED = 'Accepted', + PENDING = 'Pending', + REJECTED = 'Rejected', +} + +export enum OCPP16RequestCommand { + AUTHORIZE = 'Authorize', + BOOT_NOTIFICATION = 'BootNotification', + DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification', + HEARTBEAT = 'Heartbeat', + METER_VALUES = 'MeterValues', + START_TRANSACTION = 'StartTransaction', + STATUS_NOTIFICATION = 'StatusNotification', + STOP_TRANSACTION = 'StopTransaction', +} + +export enum OCPPProtocol { + JSON = 'json', +} + +export enum OCPPVersion { + VERSION_16 = '1.6', + VERSION_20 = '2.0', + VERSION_201 = '2.0.1', +} + +export enum Voltage { + VOLTAGE_110 = 110, + VOLTAGE_230 = 230, + VOLTAGE_400 = 400, + VOLTAGE_800 = 800, +} + export interface AutomaticTransactionGeneratorConfiguration extends JsonObject { enable: boolean idTagDistribution?: IdTagDistribution @@ -19,6 +117,12 @@ export interface AutomaticTransactionGeneratorConfiguration extends JsonObject { stopAfterHours: number } +export type AvailabilityType = OCPP16AvailabilityType + +export type BootNotificationResponse = OCPP16BootNotificationResponse + +export type ChargePointStatus = OCPP16ChargePointStatus + export interface ChargingStationAutomaticTransactionGeneratorConfiguration extends JsonObject { automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration automaticTransactionGeneratorStatuses?: Status[] @@ -40,41 +144,6 @@ export interface ChargingStationData extends JsonObject { | typeof WebSocket.OPEN } -export enum OCPP16FirmwareStatus { - Downloaded = 'Downloaded', - DownloadFailed = 'DownloadFailed', - Downloading = 'Downloading', - Idle = 'Idle', - InstallationFailed = 'InstallationFailed', - Installed = 'Installed', - Installing = 'Installing', -} - -export interface FirmwareUpgrade extends JsonObject { - failureStatus?: FirmwareStatus - reset?: boolean - versionUpgrade?: { - patternGroup?: number - step?: number - } -} - -export const FirmwareStatus = { - ...OCPP16FirmwareStatus, -} as const -// eslint-disable-next-line @typescript-eslint/no-redeclare -export type FirmwareStatus = OCPP16FirmwareStatus - -export interface ChargingStationOptions extends JsonObject { - autoRegister?: boolean - autoStart?: boolean - enableStatistics?: boolean - ocppStrictCompliance?: boolean - persistentConfiguration?: boolean - stopTransactionsOnStopped?: boolean - supervisionUrls?: string | string[] -} - export interface ChargingStationInfo extends JsonObject { amperageLimitationOcppKey?: string amperageLimitationUnit?: AmpereUnits @@ -142,62 +211,62 @@ export interface ChargingStationOcppConfiguration extends JsonObject { configurationKey?: ConfigurationKey[] } +export interface ChargingStationOptions extends JsonObject { + autoRegister?: boolean + autoStart?: boolean + enableStatistics?: boolean + ocppStrictCompliance?: boolean + persistentConfiguration?: boolean + stopTransactionsOnStopped?: boolean + supervisionUrls?: string | string[] +} + export interface ConfigurationKey extends OCPPConfigurationKey { reboot?: boolean visible?: boolean } -export interface OCPPConfigurationKey extends JsonObject { - key: string - readonly: boolean - value?: string +export interface ConnectorStatus extends JsonObject { + authorizeIdTag?: string + availability: AvailabilityType + bootStatus?: ChargePointStatus + energyActiveImportRegisterValue?: number // In Wh + idTagAuthorized?: boolean + idTagLocalAuthorized?: boolean + localAuthorizeIdTag?: string + status?: ChargePointStatus + transactionEnergyActiveImportRegisterValue?: number // In Wh + transactionId?: number + transactionIdTag?: string + transactionRemoteStarted?: boolean + transactionStarted?: boolean } -export enum OCPP16IncomingRequestCommand { - CHANGE_AVAILABILITY = 'ChangeAvailability', - CHANGE_CONFIGURATION = 'ChangeConfiguration', - CLEAR_CACHE = 'ClearCache', - CLEAR_CHARGING_PROFILE = 'ClearChargingProfile', - GET_CONFIGURATION = 'GetConfiguration', - GET_DIAGNOSTICS = 'GetDiagnostics', - REMOTE_START_TRANSACTION = 'RemoteStartTransaction', - REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction', - RESET = 'Reset', - SET_CHARGING_PROFILE = 'SetChargingProfile', - TRIGGER_MESSAGE = 'TriggerMessage', - UNLOCK_CONNECTOR = 'UnlockConnector', +export interface EvseStatus extends JsonObject { + availability: AvailabilityType + connectors?: ConnectorStatus[] } -export const IncomingRequestCommand = { - ...OCPP16IncomingRequestCommand, +export const FirmwareStatus = { + ...OCPP16FirmwareStatus, } as const // eslint-disable-next-line @typescript-eslint/no-redeclare -export type IncomingRequestCommand = OCPP16IncomingRequestCommand +export type FirmwareStatus = OCPP16FirmwareStatus -export enum OCPP16RequestCommand { - AUTHORIZE = 'Authorize', - BOOT_NOTIFICATION = 'BootNotification', - DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification', - HEARTBEAT = 'Heartbeat', - METER_VALUES = 'MeterValues', - START_TRANSACTION = 'StartTransaction', - STATUS_NOTIFICATION = 'StatusNotification', - STOP_TRANSACTION = 'StopTransaction', +export interface FirmwareUpgrade extends JsonObject { + failureStatus?: FirmwareStatus + reset?: boolean + versionUpgrade?: { + patternGroup?: number + step?: number + } } -export const RequestCommand = { - ...OCPP16RequestCommand, +export const IncomingRequestCommand = { + ...OCPP16IncomingRequestCommand, } as const // eslint-disable-next-line @typescript-eslint/no-redeclare -export type RequestCommand = OCPP16RequestCommand - -export type BootNotificationResponse = OCPP16BootNotificationResponse - -export enum OCPP16RegistrationStatus { - ACCEPTED = 'Accepted', - PENDING = 'Pending', - REJECTED = 'Rejected', -} +export type IncomingRequestCommand = OCPP16IncomingRequestCommand export interface OCPP16BootNotificationResponse extends JsonObject { currentTime: Date @@ -205,13 +274,10 @@ export interface OCPP16BootNotificationResponse extends JsonObject { status: OCPP16RegistrationStatus } -export enum OCPP16MessageTrigger { - BootNotification = 'BootNotification', - DiagnosticsStatusNotification = 'DiagnosticsStatusNotification', - FirmwareStatusNotification = 'FirmwareStatusNotification', - Heartbeat = 'Heartbeat', - MeterValues = 'MeterValues', - StatusNotification = 'StatusNotification', +export interface OCPPConfigurationKey extends JsonObject { + key: string + readonly: boolean + value?: string } export const MessageTrigger = { @@ -220,80 +286,11 @@ export const MessageTrigger = { // eslint-disable-next-line @typescript-eslint/no-redeclare export type MessageTrigger = OCPP16MessageTrigger -interface CommandsSupport extends JsonObject { - incomingCommands: Record - outgoingCommands?: Record -} - -export enum OCPPVersion { - VERSION_16 = '1.6', - VERSION_20 = '2.0', - VERSION_201 = '2.0.1', -} - -export enum OCPPProtocol { - JSON = 'json', -} - -export enum CurrentType { - AC = 'AC', - DC = 'DC', -} - -export enum Voltage { - VOLTAGE_110 = 110, - VOLTAGE_230 = 230, - VOLTAGE_400 = 400, - VOLTAGE_800 = 800, -} - -export enum AmpereUnits { - AMPERE = 'A', - CENTI_AMPERE = 'cA', - DECI_AMPERE = 'dA', - MILLI_AMPERE = 'mA', -} - -export interface ConnectorStatus extends JsonObject { - authorizeIdTag?: string - availability: AvailabilityType - bootStatus?: ChargePointStatus - energyActiveImportRegisterValue?: number // In Wh - idTagAuthorized?: boolean - idTagLocalAuthorized?: boolean - localAuthorizeIdTag?: string - status?: ChargePointStatus - transactionEnergyActiveImportRegisterValue?: number // In Wh - transactionId?: number - transactionIdTag?: string - transactionRemoteStarted?: boolean - transactionStarted?: boolean -} - -export interface EvseStatus extends JsonObject { - availability: AvailabilityType - connectors?: ConnectorStatus[] -} - -export enum OCPP16AvailabilityType { - INOPERATIVE = 'Inoperative', - OPERATIVE = 'Operative', -} -export type AvailabilityType = OCPP16AvailabilityType - -export enum OCPP16ChargePointStatus { - AVAILABLE = 'Available', - CHARGING = 'Charging', - FAULTED = 'Faulted', - FINISHING = 'Finishing', - OCCUPIED = 'Occupied', - PREPARING = 'Preparing', - RESERVED = 'Reserved', - SUSPENDED_EV = 'SuspendedEV', - SUSPENDED_EVSE = 'SuspendedEVSE', - UNAVAILABLE = 'Unavailable', -} -export type ChargePointStatus = OCPP16ChargePointStatus +export const RequestCommand = { + ...OCPP16RequestCommand, +} as const +// eslint-disable-next-line @typescript-eslint/no-redeclare +export type RequestCommand = OCPP16RequestCommand export interface Status extends JsonObject { acceptedAuthorizeRequests?: number @@ -313,3 +310,8 @@ export interface Status extends JsonObject { stoppedDate?: Date stopTransactionRequests?: number } + +interface CommandsSupport extends JsonObject { + incomingCommands: Record + outgoingCommands?: Record +} diff --git a/ui/web/src/types/JsonType.ts b/ui/web/src/types/JsonType.ts index 163bb955..6acff3c7 100644 --- a/ui/web/src/types/JsonType.ts +++ b/ui/web/src/types/JsonType.ts @@ -1,4 +1,4 @@ -type JsonPrimitive = boolean | Date | null | number | string // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style export type JsonObject = { [key in string]?: JsonType } export type JsonType = JsonObject | JsonPrimitive | JsonType[] +type JsonPrimitive = boolean | Date | null | number | string diff --git a/ui/web/src/types/UIProtocol.ts b/ui/web/src/types/UIProtocol.ts index 44df63d2..169a713e 100644 --- a/ui/web/src/types/UIProtocol.ts +++ b/ui/web/src/types/UIProtocol.ts @@ -1,36 +1,14 @@ import type { JsonObject } from './JsonType' -export enum Protocol { - UI = 'ui', -} - export enum ApplicationProtocol { WS = 'ws', WSS = 'wss', } -export enum ProtocolVersion { - '0.0.1' = '0.0.1', -} - export enum AuthenticationType { PROTOCOL_BASIC_AUTH = 'protocol-basic-auth', } -export type ProtocolRequest = [ - `${string}-${string}-${string}-${string}-${string}`, - ProcedureName, - RequestPayload -] -export type ProtocolResponse = [ - `${string}-${string}-${string}-${string}-${string}`, - ResponsePayload -] - -export type ProtocolRequestHandler = ( - payload: RequestPayload -) => Promise | ResponsePayload - export enum ProcedureName { ADD_CHARGING_STATIONS = 'addChargingStations', CLOSE_CONNECTION = 'closeConnection', @@ -50,30 +28,52 @@ export enum ProcedureName { STOP_TRANSACTION = 'stopTransaction', } -export interface RequestPayload extends JsonObject { - connectorIds?: number[] - hashIds?: string[] +export enum Protocol { + UI = 'ui', } +export enum ProtocolVersion { + '0.0.1' = '0.0.1', +} export enum ResponseStatus { FAILURE = 'failure', SUCCESS = 'success', } +export type ProtocolRequest = [ + `${string}-${string}-${string}-${string}-${string}`, + ProcedureName, + RequestPayload +] + +export type ProtocolRequestHandler = ( + payload: RequestPayload +) => Promise | ResponsePayload + +export type ProtocolResponse = [ + `${string}-${string}-${string}-${string}-${string}`, + ResponsePayload +] + +export interface RequestPayload extends JsonObject { + connectorIds?: number[] + hashIds?: string[] +} + export interface ResponsePayload extends JsonObject { hashIds?: string[] status: ResponseStatus } +export interface SimulatorState extends JsonObject { + started: boolean + templateStatistics: Record + version: string +} + interface TemplateStatistics extends JsonObject { added: number configured: number indexes: number[] started: number } - -export interface SimulatorState extends JsonObject { - started: boolean - templateStatistics: Record - version: string -} -- 2.34.1