refactor: switch to eslint-plugin-perfectionist
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 21 Aug 2024 21:29:22 +0000 (23:29 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 21 Aug 2024 21:29:22 +0000 (23:29 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
137 files changed:
eslint.config.js
mikro-orm.config-template.ts
package.json
pnpm-lock.yaml
scripts/build-requirements.js
scripts/bundle.js
scripts/runtime.js
src/charging-station/AutomaticTransactionGenerator.ts
src/charging-station/Bootstrap.ts
src/charging-station/ChargingStation.ts
src/charging-station/ChargingStationWorker.ts
src/charging-station/ConfigurationKeyUtils.ts
src/charging-station/Helpers.ts
src/charging-station/IdTagsCache.ts
src/charging-station/SharedLRUCache.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts
src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
src/charging-station/ocpp/OCPPConstants.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPRequestService.ts
src/charging-station/ocpp/OCPPResponseService.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIServerFactory.ts
src/charging-station/ui-server/UIServerUtils.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/charging-station/ui-server/ui-services/UIService001.ts
src/charging-station/ui-server/ui-services/UIServiceFactory.ts
src/exception/OCPPError.ts
src/performance/PerformanceStatistics.ts
src/performance/storage/JsonFileStorage.ts
src/performance/storage/MikroOrmStorage.ts
src/performance/storage/MongoDBStorage.ts
src/performance/storage/None.ts
src/performance/storage/Storage.ts
src/performance/storage/StorageFactory.ts
src/scripts/deleteChargingStations.cjs
src/scripts/setCSPublicFlag.cjs
src/types/AutomaticTransactionGenerator.ts
src/types/ChargingStationConfiguration.ts
src/types/ChargingStationEvents.ts
src/types/ChargingStationInfo.ts
src/types/ChargingStationOcppConfiguration.ts
src/types/ChargingStationTemplate.ts
src/types/ChargingStationWorker.ts
src/types/ConfigurationData.ts
src/types/ConnectorStatus.ts
src/types/Error.ts
src/types/Evse.ts
src/types/FileType.ts
src/types/JsonType.ts
src/types/MeasurandValues.ts
src/types/SimulatorState.ts
src/types/Statistics.ts
src/types/Storage.ts
src/types/UIProtocol.ts
src/types/WebSocket.ts
src/types/WorkerBroadcastChannel.ts
src/types/ocpp/1.6/ChargePointStatus.ts
src/types/ocpp/1.6/ChargingProfile.ts
src/types/ocpp/1.6/Configuration.ts
src/types/ocpp/1.6/MeterValues.ts
src/types/ocpp/1.6/Requests.ts
src/types/ocpp/1.6/Responses.ts
src/types/ocpp/1.6/Transaction.ts
src/types/ocpp/2.0/Common.ts
src/types/ocpp/2.0/Requests.ts
src/types/ocpp/2.0/Responses.ts
src/types/ocpp/2.0/Variables.ts
src/types/ocpp/Configuration.ts
src/types/ocpp/ErrorType.ts
src/types/ocpp/MessageType.ts
src/types/ocpp/Requests.ts
src/types/ocpp/Reservation.ts
src/types/ocpp/Responses.ts
src/types/orm/entities/PerformanceRecord.ts
src/utils/AsyncLock.ts
src/utils/ChargingStationConfigurationUtils.ts
src/utils/Configuration.ts
src/utils/ConfigurationUtils.ts
src/utils/Constants.ts
src/utils/ElectricUtils.ts
src/utils/ErrorUtils.ts
src/utils/FileUtils.ts
src/utils/Logger.ts
src/utils/MessageChannelUtils.ts
src/utils/Utils.ts
src/worker/WorkerAbstract.ts
src/worker/WorkerConstants.ts
src/worker/WorkerDynamicPool.ts
src/worker/WorkerFactory.ts
src/worker/WorkerFixedPool.ts
src/worker/WorkerSet.ts
src/worker/WorkerTypes.ts
src/worker/WorkerUtils.ts
tests/charging-station/Helpers.test.ts
tests/exception/BaseError.test.ts
tests/exception/OCPPError.test.ts
tests/types/ConfigurationData.test.ts
tests/utils/AsyncLock.test.ts
tests/utils/ConfigurationUtils.test.ts
tests/utils/ElectricUtils.test.ts
tests/utils/ErrorUtils.test.ts
tests/utils/StatisticUtils.test.ts
tests/utils/Utils.test.ts
ui/web/package.json
ui/web/src/components/actions/AddChargingStations.vue
ui/web/src/components/actions/SetSupervisionUrl.vue
ui/web/src/components/actions/StartTransaction.vue
ui/web/src/components/buttons/Button.vue
ui/web/src/components/buttons/ToggleButton.vue
ui/web/src/components/charging-stations/CSConnector.vue
ui/web/src/components/charging-stations/CSData.vue
ui/web/src/components/charging-stations/CSTable.vue
ui/web/src/composables/UIClient.ts
ui/web/src/main.ts
ui/web/src/router/index.ts
ui/web/src/shims-vue.d.ts
ui/web/src/types/ChargingStationType.ts
ui/web/src/types/ConfigurationType.ts
ui/web/src/types/JsonType.ts
ui/web/src/types/UIProtocol.ts
ui/web/src/views/ChargingStationsView.vue
ui/web/start.js
ui/web/tests/unit/CSTable.spec.ts
ui/web/vite.config.ts
ui/web/vitest.config.ts

index 925bd2fe4e21c3f66df24b9f4822ea8c2c01d568..36e270744abeb24a4fea56064da9082bfb5e1a0a 100644 (file)
@@ -2,7 +2,7 @@
 import js from '@eslint/js'
 import { defineFlatConfig } from 'eslint-define-config'
 import jsdoc from 'eslint-plugin-jsdoc'
-import simpleImportSort from 'eslint-plugin-simple-import-sort'
+import perfectionist from 'eslint-plugin-perfectionist'
 import pluginVue from 'eslint-plugin-vue'
 import neostandard, { plugins } from 'neostandard'
 
@@ -19,8 +19,8 @@ export default defineFlatConfig([
       'jsdoc/check-tag-names': [
         'warn',
         {
-          typed: true,
           definedTags: ['defaultValue', 'experimental', 'typeParam'],
+          typed: true,
         },
       ],
     },
@@ -36,11 +36,11 @@ export default defineFlatConfig([
   },
   ...plugins['typescript-eslint'].config(
     {
-      files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '*/**.vue'],
       extends: [
         ...plugins['typescript-eslint'].configs.strictTypeChecked,
         ...plugins['typescript-eslint'].configs.stylisticTypeChecked,
       ],
+      files: ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts', '*/**.vue'],
       languageOptions: {
         parserOptions: {
           projectService: true,
@@ -53,13 +53,11 @@ export default defineFlatConfig([
       ...plugins['typescript-eslint'].configs.disableTypeChecked,
     }
   ),
+  perfectionist.configs['recommended-natural'],
   {
-    plugins: {
-      'simple-import-sort': simpleImportSort,
-    },
+    files: ['**/*.vue'],
     rules: {
-      'simple-import-sort/imports': 'error',
-      'simple-import-sort/exports': 'error',
+      'perfectionist/sort-vue-attributes': 'off',
     },
   },
   ...neostandard({
index af4b08dd6be5b71e895fdc7b215ab97f7620b5f9..d2796255b90658960a17afbf5952ad2940154538 100644 (file)
@@ -4,7 +4,7 @@ import { Constants } from './src/utils/index.js'
 
 export default defineConfig({
   dbName: `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`,
+  debug: true,
   entities: ['./dist/types/orm/entities/*.js'],
   entitiesTs: ['./src/types/orm/entities/*.ts'],
-  debug: true,
 })
index 62a6c3eb7ea1c9298c62f879f27bc481710ef663..72e0159648bd8c9f8263b16dbf7262158bd23254 100644 (file)
     "@commitlint/config-conventional": "^19.2.2",
     "@eslint/js": "^9.9.0",
     "@mikro-orm/cli": "^6.3.6",
-    "@types/node": "^22.4.1",
+    "@types/node": "^22.5.0",
     "@types/semver": "^7.5.8",
     "@types/ws": "^8.5.12",
     "c8": "^10.1.2",
     "eslint": "^9.9.0",
     "eslint-define-config": "^2.1.0",
     "eslint-plugin-jsdoc": "^50.2.2",
-    "eslint-plugin-simple-import-sort": "^12.1.1",
+    "eslint-plugin-perfectionist": "^3.2.0",
     "eslint-plugin-vue": "^9.27.0",
     "expect": "^29.7.0",
     "glob": "^11.0.0",
index b80e48e1fd518c25b2f87ecff32e1e745d438372..11ad1cf2bd179083a56f8f0c811cf179d757debe 100644 (file)
@@ -84,7 +84,7 @@ importers:
     devDependencies:
       '@commitlint/cli':
         specifier: ^19.4.0
-        version: 19.4.0(@types/node@22.4.1)(typescript@5.5.4)
+        version: 19.4.0(@types/node@22.5.0)(typescript@5.5.4)
       '@commitlint/config-conventional':
         specifier: ^19.2.2
         version: 19.2.2
@@ -95,8 +95,8 @@ importers:
         specifier: ^6.3.6
         version: 6.3.6(mariadb@3.3.1)
       '@types/node':
-        specifier: ^22.4.1
-        version: 22.4.1
+        specifier: ^22.5.0
+        version: 22.5.0
       '@types/semver':
         specifier: ^7.5.8
         version: 7.5.8
@@ -130,9 +130,9 @@ importers:
       eslint-plugin-jsdoc:
         specifier: ^50.2.2
         version: 50.2.2(eslint@9.9.0(jiti@1.21.6))
-      eslint-plugin-simple-import-sort:
-        specifier: ^12.1.1
-        version: 12.1.1(eslint@9.9.0(jiti@1.21.6))
+      eslint-plugin-perfectionist:
+        specifier: ^3.2.0
+        version: 3.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4)(vue-eslint-parser@9.4.3(eslint@9.9.0(jiti@1.21.6)))
       eslint-plugin-vue:
         specifier: ^9.27.0
         version: 9.27.0(eslint@9.9.0(jiti@1.21.6))
@@ -162,7 +162,7 @@ importers:
         version: 7.6.3
       ts-node:
         specifier: ^10.9.2
-        version: 10.9.2(@types/node@22.4.1)(typescript@5.5.4)
+        version: 10.9.2(@types/node@22.5.0)(typescript@5.5.4)
       tsx:
         specifier: ^4.17.0
         version: 4.17.0
@@ -195,17 +195,17 @@ importers:
         specifier: ^21.1.7
         version: 21.1.7
       '@types/node':
-        specifier: ^22.4.1
-        version: 22.4.1
+        specifier: ^22.5.0
+        version: 22.5.0
       '@vitejs/plugin-vue':
         specifier: ^5.1.2
-        version: 5.1.2(vite@5.4.2(@types/node@22.4.1))(vue@3.4.38(typescript@5.5.4))
+        version: 5.1.2(vite@5.4.2(@types/node@22.5.0))(vue@3.4.38(typescript@5.5.4))
       '@vitejs/plugin-vue-jsx':
         specifier: ^4.0.1
-        version: 4.0.1(vite@5.4.2(@types/node@22.4.1))(vue@3.4.38(typescript@5.5.4))
+        version: 4.0.1(vite@5.4.2(@types/node@22.5.0))(vue@3.4.38(typescript@5.5.4))
       '@vitest/coverage-v8':
         specifier: ^2.0.5
-        version: 2.0.5(vitest@2.0.5(@types/node@22.4.1)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)))
+        version: 2.0.5(vitest@2.0.5(@types/node@22.5.0)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)))
       '@vue/test-utils':
         specifier: ^2.4.6
         version: 2.4.6
@@ -229,10 +229,10 @@ importers:
         version: 5.5.4
       vite:
         specifier: ^5.4.2
-        version: 5.4.2(@types/node@22.4.1)
+        version: 5.4.2(@types/node@22.5.0)
       vitest:
         specifier: ^2.0.5
-        version: 2.0.5(@types/node@22.4.1)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+        version: 2.0.5(@types/node@22.5.0)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
 
 packages:
 
@@ -1126,8 +1126,8 @@ packages:
   '@types/node@20.16.1':
     resolution: {integrity: sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==}
 
-  '@types/node@22.4.1':
-    resolution: {integrity: sha512-1tbpb9325+gPnKK0dMm+/LMriX0vKxf6RnB0SZUqfyVkQ4fMgUSySqhxE/y8Jvs4NyF1yHzTfG9KlnkIODxPKg==}
+  '@types/node@22.5.0':
+    resolution: {integrity: sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==}
 
   '@types/offscreencanvas@2019.3.0':
     resolution: {integrity: sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==}
@@ -2323,8 +2323,8 @@ packages:
   ee-first@1.1.1:
     resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
 
-  electron-to-chromium@1.5.12:
-    resolution: {integrity: sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==}
+  electron-to-chromium@1.5.13:
+    resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==}
 
   elliptic@6.5.7:
     resolution: {integrity: sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==}
@@ -2515,6 +2515,25 @@ packages:
     peerDependencies:
       eslint: '>=8.23.0'
 
+  eslint-plugin-perfectionist@3.2.0:
+    resolution: {integrity: sha512-cX1aztMbSfRWPKJH8CD+gadrbkS+RNH1OGWuNGws8J6rHzYYhawxWTU/yzMYjq2IRJCpBCfhgfa7BHRXQYxLHA==}
+    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.0
+      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==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2527,11 +2546,6 @@ packages:
     peerDependencies:
       eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
 
-  eslint-plugin-simple-import-sort@12.1.1:
-    resolution: {integrity: sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==}
-    peerDependencies:
-      eslint: '>=5.0.0'
-
   eslint-plugin-vue@9.27.0:
     resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==}
     engines: {node: ^14.17.0 || >=16.0.0}
@@ -3259,8 +3273,8 @@ packages:
     resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==}
     hasBin: true
 
-  is-core-module@2.15.0:
-    resolution: {integrity: sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==}
+  is-core-module@2.15.1:
+    resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
     engines: {node: '>= 0.4'}
 
   is-data-view@1.0.1:
@@ -4044,6 +4058,9 @@ 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==}
 
@@ -4091,8 +4108,8 @@ packages:
   no-case@2.3.2:
     resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==}
 
-  node-abi@3.65.0:
-    resolution: {integrity: sha512-ThjYBfoDNr08AWx6hGaRbfPwxKV9kVzAzOzlLKbk2CuqXE2xnCh+cbAGnwM3t8Lq4v9rUB7VfondlkBckcJrVA==}
+  node-abi@3.67.0:
+    resolution: {integrity: sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==}
     engines: {node: '>=10'}
 
   node-addon-api@7.1.1:
@@ -4929,8 +4946,8 @@ packages:
   spdx-expression-parse@4.0.0:
     resolution: {integrity: sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==}
 
-  spdx-license-ids@3.0.18:
-    resolution: {integrity: sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==}
+  spdx-license-ids@3.0.20:
+    resolution: {integrity: sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==}
 
   split2@4.2.0:
     resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
@@ -6110,11 +6127,11 @@ snapshots:
 
   '@colors/colors@1.6.0': {}
 
-  '@commitlint/cli@19.4.0(@types/node@22.4.1)(typescript@5.5.4)':
+  '@commitlint/cli@19.4.0(@types/node@22.5.0)(typescript@5.5.4)':
     dependencies:
       '@commitlint/format': 19.3.0
       '@commitlint/lint': 19.2.2
-      '@commitlint/load': 19.4.0(@types/node@22.4.1)(typescript@5.5.4)
+      '@commitlint/load': 19.4.0(@types/node@22.5.0)(typescript@5.5.4)
       '@commitlint/read': 19.4.0
       '@commitlint/types': 19.0.3
       execa: 8.0.1
@@ -6161,7 +6178,7 @@ snapshots:
       '@commitlint/rules': 19.0.3
       '@commitlint/types': 19.0.3
 
-  '@commitlint/load@19.4.0(@types/node@22.4.1)(typescript@5.5.4)':
+  '@commitlint/load@19.4.0(@types/node@22.5.0)(typescript@5.5.4)':
     dependencies:
       '@commitlint/config-validator': 19.0.3
       '@commitlint/execute-rule': 19.0.0
@@ -6169,7 +6186,7 @@ snapshots:
       '@commitlint/types': 19.0.3
       chalk: 5.3.0
       cosmiconfig: 9.0.0(typescript@5.5.4)
-      cosmiconfig-typescript-loader: 5.0.0(@types/node@22.4.1)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4)
+      cosmiconfig-typescript-loader: 5.0.0(@types/node@22.5.0)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4)
       lodash.isplainobject: 4.0.6
       lodash.merge: 4.6.2
       lodash.uniq: 4.5.0
@@ -6454,7 +6471,7 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       '@types/yargs': 17.0.33
       chalk: 4.1.2
 
@@ -6750,7 +6767,7 @@ snapshots:
 
   '@types/conventional-commits-parser@5.0.0':
     dependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
 
   '@types/eslint@9.6.0':
     dependencies:
@@ -6775,7 +6792,7 @@ snapshots:
 
   '@types/jsdom@21.1.7':
     dependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       '@types/tough-cookie': 4.0.5
       parse5: 7.1.2
 
@@ -6787,7 +6804,7 @@ snapshots:
     dependencies:
       undici-types: 6.19.8
 
-  '@types/node@22.4.1':
+  '@types/node@22.5.0':
     dependencies:
       undici-types: 6.19.8
 
@@ -6813,7 +6830,7 @@ snapshots:
 
   '@types/ws@8.5.12':
     dependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
 
   '@types/yargs-parser@21.0.3': {}
 
@@ -6902,22 +6919,22 @@ snapshots:
       '@typescript-eslint/types': 8.2.0
       eslint-visitor-keys: 3.4.3
 
-  '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.2(@types/node@22.4.1))(vue@3.4.38(typescript@5.5.4))':
+  '@vitejs/plugin-vue-jsx@4.0.1(vite@5.4.2(@types/node@22.5.0))(vue@3.4.38(typescript@5.5.4))':
     dependencies:
       '@babel/core': 7.25.2
       '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2)
       '@vue/babel-plugin-jsx': 1.2.2(@babel/core@7.25.2)
-      vite: 5.4.2(@types/node@22.4.1)
+      vite: 5.4.2(@types/node@22.5.0)
       vue: 3.4.38(typescript@5.5.4)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@5.1.2(vite@5.4.2(@types/node@22.4.1))(vue@3.4.38(typescript@5.5.4))':
+  '@vitejs/plugin-vue@5.1.2(vite@5.4.2(@types/node@22.5.0))(vue@3.4.38(typescript@5.5.4))':
     dependencies:
-      vite: 5.4.2(@types/node@22.4.1)
+      vite: 5.4.2(@types/node@22.5.0)
       vue: 3.4.38(typescript@5.5.4)
 
-  '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@22.4.1)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)))':
+  '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@22.5.0)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)))':
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 0.2.3
@@ -6931,7 +6948,7 @@ snapshots:
       std-env: 3.7.0
       test-exclude: 7.0.1
       tinyrainbow: 1.2.0
-      vitest: 2.0.5(@types/node@22.4.1)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      vitest: 2.0.5(@types/node@22.5.0)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))
     transitivePeerDependencies:
       - supports-color
 
@@ -7504,7 +7521,7 @@ snapshots:
   browserslist@4.23.3:
     dependencies:
       caniuse-lite: 1.0.30001651
-      electron-to-chromium: 1.5.12
+      electron-to-chromium: 1.5.13
       node-releases: 2.0.18
       update-browserslist-db: 1.1.0(browserslist@4.23.3)
 
@@ -7892,9 +7909,9 @@ snapshots:
 
   core-util-is@1.0.3: {}
 
-  cosmiconfig-typescript-loader@5.0.0(@types/node@22.4.1)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4):
+  cosmiconfig-typescript-loader@5.0.0(@types/node@22.5.0)(cosmiconfig@9.0.0(typescript@5.5.4))(typescript@5.5.4):
     dependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       cosmiconfig: 9.0.0(typescript@5.5.4)
       jiti: 1.21.6
       typescript: 5.5.4
@@ -8257,7 +8274,7 @@ snapshots:
 
   ee-first@1.1.1: {}
 
-  electron-to-chromium@1.5.12: {}
+  electron-to-chromium@1.5.13: {}
 
   elliptic@6.5.7:
     dependencies:
@@ -8583,6 +8600,19 @@ snapshots:
       minimatch: 9.0.5
       semver: 7.6.3
 
+  eslint-plugin-perfectionist@3.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4)(vue-eslint-parser@9.4.3(eslint@9.9.0(jiti@1.21.6))):
+    dependencies:
+      '@typescript-eslint/types': 8.2.0
+      '@typescript-eslint/utils': 8.2.0(eslint@9.9.0(jiti@1.21.6))(typescript@5.5.4)
+      eslint: 9.9.0(jiti@1.21.6)
+      minimatch: 10.0.1
+      natural-compare-lite: 1.4.0
+    optionalDependencies:
+      vue-eslint-parser: 9.4.3(eslint@9.9.0(jiti@1.21.6))
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
   eslint-plugin-promise@7.1.0(eslint@9.9.0(jiti@1.21.6)):
     dependencies:
       eslint: 9.9.0(jiti@1.21.6)
@@ -8609,10 +8639,6 @@ snapshots:
       string.prototype.matchall: 4.0.11
       string.prototype.repeat: 1.0.0
 
-  eslint-plugin-simple-import-sort@12.1.1(eslint@9.9.0(jiti@1.21.6)):
-    dependencies:
-      eslint: 9.9.0(jiti@1.21.6)
-
   eslint-plugin-vue@9.27.0(eslint@9.9.0(jiti@1.21.6)):
     dependencies:
       '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.0(jiti@1.21.6))
@@ -9442,7 +9468,7 @@ snapshots:
     dependencies:
       ci-info: 2.0.0
 
-  is-core-module@2.15.0:
+  is-core-module@2.15.1:
     dependencies:
       hasown: 2.0.2
 
@@ -9650,7 +9676,7 @@ snapshots:
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       chalk: 4.1.2
       ci-info: 3.9.0
       graceful-fs: 4.2.11
@@ -10245,6 +10271,8 @@ snapshots:
 
   napi-build-utils@1.0.2: {}
 
+  natural-compare-lite@1.4.0: {}
+
   natural-compare@1.4.0: {}
 
   ndarray-blas-level1@1.1.3: {}
@@ -10314,7 +10342,7 @@ snapshots:
     dependencies:
       lower-case: 1.1.4
 
-  node-abi@3.65.0:
+  node-abi@3.67.0:
     dependencies:
       semver: 7.6.3
 
@@ -10677,7 +10705,7 @@ snapshots:
       minimist: 1.2.8
       mkdirp-classic: 0.5.3
       napi-build-utils: 1.0.2
-      node-abi: 3.65.0
+      node-abi: 3.67.0
       pump: 3.0.0
       rc: 1.2.8
       simple-get: 4.0.1
@@ -10918,13 +10946,13 @@ snapshots:
 
   resolve@1.22.8:
     dependencies:
-      is-core-module: 2.15.0
+      is-core-module: 2.15.1
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
 
   resolve@2.0.0-next.5:
     dependencies:
-      is-core-module: 2.15.0
+      is-core-module: 2.15.1
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
 
@@ -11217,9 +11245,9 @@ snapshots:
   spdx-expression-parse@4.0.0:
     dependencies:
       spdx-exceptions: 2.5.0
-      spdx-license-ids: 3.0.18
+      spdx-license-ids: 3.0.20
 
-  spdx-license-ids@3.0.18: {}
+  spdx-license-ids@3.0.20: {}
 
   split2@4.2.0: {}
 
@@ -11617,14 +11645,14 @@ snapshots:
       '@ts-morph/common': 0.24.0
       code-block-writer: 13.0.2
 
-  ts-node@10.9.2(@types/node@22.4.1)(typescript@5.5.4):
+  ts-node@10.9.2(@types/node@22.5.0)(typescript@5.5.4):
     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.4.1
+      '@types/node': 22.5.0
       acorn: 8.12.1
       acorn-walk: 8.3.3
       arg: 4.1.3
@@ -11864,13 +11892,13 @@ snapshots:
       core-util-is: 1.0.2
       extsprintf: 1.3.0
 
-  vite-node@2.0.5(@types/node@22.4.1):
+  vite-node@2.0.5(@types/node@22.5.0):
     dependencies:
       cac: 6.7.14
       debug: 4.3.6
       pathe: 1.1.2
       tinyrainbow: 1.2.0
-      vite: 5.4.2(@types/node@22.4.1)
+      vite: 5.4.2(@types/node@22.5.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -11882,16 +11910,16 @@ snapshots:
       - supports-color
       - terser
 
-  vite@5.4.2(@types/node@22.4.1):
+  vite@5.4.2(@types/node@22.5.0):
     dependencies:
       esbuild: 0.21.5
       postcss: 8.4.41
       rollup: 4.21.0
     optionalDependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       fsevents: 2.3.3
 
-  vitest@2.0.5(@types/node@22.4.1)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
+  vitest@2.0.5(@types/node@22.5.0)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@vitest/expect': 2.0.5
@@ -11909,11 +11937,11 @@ snapshots:
       tinybench: 2.9.0
       tinypool: 1.0.1
       tinyrainbow: 1.2.0
-      vite: 5.4.2(@types/node@22.4.1)
-      vite-node: 2.0.5(@types/node@22.4.1)
+      vite: 5.4.2(@types/node@22.5.0)
+      vite-node: 2.0.5(@types/node@22.5.0)
       why-is-node-running: 2.3.0
     optionalDependencies:
-      '@types/node': 22.4.1
+      '@types/node': 22.5.0
       jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - less
index 75617fd62a390cbfa5281ac6404728325ed29e55..4e4aafbc0cac9a23b893f3242c7a50baef8ee634 100644 (file)
@@ -1,7 +1,6 @@
+import chalk from 'chalk'
 import { readFileSync } from 'node:fs'
 import { exit, version } from 'node:process'
-
-import chalk from 'chalk'
 // eslint-disable-next-line n/no-unpublished-import
 import { satisfies } from 'semver'
 
index 6f419e18287d3c0547e63fe6e91d6e251cc0afaf..35caeb5759d6eed953a4c2b89be82832027e98ee 100644 (file)
@@ -1,20 +1,18 @@
 /* eslint-disable n/no-unpublished-import */
-import { env } from 'node:process'
-
 import chalk from 'chalk'
 import { build } from 'esbuild'
 import { clean } from 'esbuild-plugin-clean'
 import { copy } from 'esbuild-plugin-copy'
+import { env } from 'node:process'
 
 const isDevelopmentBuild = env.BUILD === 'development'
 const sourcemap = !!isDevelopmentBuild
 console.info(chalk.green(`Building in ${isDevelopmentBuild ? 'development' : 'production'} mode`))
 console.time('Build time')
 await build({
-  entryPoints: ['./src/start.ts', './src/charging-station/ChargingStationWorker.ts'],
   bundle: true,
-  platform: 'node',
-  format: 'esm',
+  entryNames: '[name]',
+  entryPoints: ['./src/start.ts', './src/charging-station/ChargingStationWorker.ts'],
   external: [
     '@mikro-orm/*',
     'ajv',
@@ -36,11 +34,10 @@ await build({
     'winston-daily-rotate-file',
     'ws',
   ],
-  treeShaking: true,
+  format: 'esm',
   minify: true,
-  sourcemap,
-  entryNames: '[name]',
   outdir: './dist',
+  platform: 'node',
   plugins: [
     clean({
       patterns: [
@@ -78,5 +75,7 @@ await build({
       ],
     }),
   ],
+  sourcemap,
+  treeShaking: true,
 })
 console.timeEnd('Build time')
index d32efe7f66b980569c5de69e5d8d7c5e943ef62f..ed1bf141ba56d1480256d7ef67fd044e5f9656ef 100644 (file)
@@ -1,9 +1,9 @@
 export const runtimes = {
+  browser: 'browser',
   bun: 'bun',
   deno: 'deno',
   node: 'node',
   workerd: 'workerd',
-  browser: 'browser',
 }
 
 const isBun = !!globalThis.Bun || !!globalThis.process?.versions?.bun
index e760b5f0623150e81ee0ea478043e52193588c47..9b3bcaaf7f4ec79dd3c7e20038cbe59680fa6d00 100644 (file)
@@ -1,8 +1,9 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
+import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns'
 import { randomInt } from 'node:crypto'
 
-import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns'
+import type { ChargingStation } from './ChargingStation.js'
 
 import { BaseError } from '../exception/index.js'
 import { PerformanceStatistics } from '../performance/index.js'
@@ -27,7 +28,6 @@ import {
   secureRandom,
   sleep,
 } from '../utils/index.js'
-import type { ChargingStation } from './ChargingStation.js'
 import { checkChargingStationState } from './Helpers.js'
 import { IdTagsCache } from './IdTagsCache.js'
 import { isIdTagAuthorized } from './ocpp/index.js'
@@ -38,11 +38,21 @@ export class AutomaticTransactionGenerator {
     AutomaticTransactionGenerator
   >()
 
-  public readonly connectorsStatus: Map<number, Status>
-  public started: boolean
+  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()}` : ''
+      }:`
+    )
+  }
+
   private starting: boolean
   private stopping: boolean
-  private readonly chargingStation: ChargingStation
+  public readonly connectorsStatus: Map<number, Status>
+
+  public started: boolean
 
   private constructor (chargingStation: ChargingStation) {
     this.started = false
@@ -53,6 +63,11 @@ export class AutomaticTransactionGenerator {
     this.initializeConnectorsStatus()
   }
 
+  public static deleteInstance (chargingStation: ChargingStation): boolean {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return AutomaticTransactionGenerator.instances.delete(chargingStation.stationInfo!.hashId)
+  }
+
   public static getInstance (
     chargingStation: ChargingStation
   ): AutomaticTransactionGenerator | undefined {
@@ -68,110 +83,136 @@ export class AutomaticTransactionGenerator {
     return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId)
   }
 
-  public static deleteInstance (chargingStation: ChargingStation): boolean {
+  private canStartConnector (connectorId: number): boolean {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return AutomaticTransactionGenerator.instances.delete(chargingStation.stationInfo!.hashId)
-  }
-
-  public start (stopAbsoluteDuration?: boolean): void {
-    if (!checkChargingStationState(this.chargingStation, this.logPrefix())) {
-      return
+    if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
+      logger.info(
+        `${this.logPrefix(
+          connectorId
+        )} entered in transaction loop while the ATG stop date has been reached`
+      )
+      return false
     }
-    if (this.started) {
-      logger.warn(`${this.logPrefix()} is already started`)
-      return
+    if (!this.chargingStation.inAcceptedState()) {
+      logger.error(
+        `${this.logPrefix(
+          connectorId
+        )} entered in transaction loop while the charging station is not in accepted state`
+      )
+      return false
     }
-    if (this.starting) {
-      logger.warn(`${this.logPrefix()} is already starting`)
-      return
+    if (!this.chargingStation.isChargingStationAvailable()) {
+      logger.info(
+        `${this.logPrefix(
+          connectorId
+        )} entered in transaction loop while the charging station is unavailable`
+      )
+      return false
     }
-    this.starting = true
-    this.startConnectors(stopAbsoluteDuration)
-    this.started = true
-    this.starting = false
-  }
-
-  public stop (): void {
-    if (!this.started) {
-      logger.warn(`${this.logPrefix()} is already stopped`)
-      return
+    if (!this.chargingStation.isConnectorAvailable(connectorId)) {
+      logger.info(
+        `${this.logPrefix(
+          connectorId
+        )} entered in transaction loop while the connector ${connectorId.toString()} is unavailable`
+      )
+      return false
     }
-    if (this.stopping) {
-      logger.warn(`${this.logPrefix()} is already stopping`)
-      return
+    const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
+    if (connectorStatus?.transactionStarted === true) {
+      logger.info(
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${connectorStatus.transactionId?.toString()} is already started on connector ${connectorId.toString()}`
+      )
+      return false
     }
-    this.stopping = true
-    this.stopConnectors()
-    this.started = false
-    this.stopping = false
+    return true
   }
 
-  public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void {
-    if (!checkChargingStationState(this.chargingStation, this.logPrefix(connectorId))) {
-      return
+  private getConnectorStatus (connectorId: number): Status {
+    const statusIndex = connectorId - 1
+    if (statusIndex < 0) {
+      logger.error(`${this.logPrefix(connectorId)} invalid connector id`)
+      throw new BaseError(`Invalid connector id ${connectorId.toString()}`)
     }
-    if (!this.connectorsStatus.has(connectorId)) {
-      logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
-      throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
+    let connectorStatus: Status | undefined
+    if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
+      connectorStatus = clone<Status>(
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
+      )
+    } else {
+      logger.warn(
+        `${this.logPrefix(
+          connectorId
+        )} no status found for connector #${connectorId.toString()} in charging station configuration file. New status will be created`
+      )
     }
-    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`)
+    if (connectorStatus != null) {
+      connectorStatus.startDate = convertToDate(connectorStatus.startDate)
+      connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
+      connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
+      connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
+      if (
+        !this.started &&
+        (connectorStatus.start ||
+          this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
+      ) {
+        connectorStatus.start = false
+      }
     }
+    return (
+      connectorStatus ?? {
+        acceptedAuthorizeRequests: 0,
+        acceptedStartTransactionRequests: 0,
+        acceptedStopTransactionRequests: 0,
+        authorizeRequests: 0,
+        rejectedAuthorizeRequests: 0,
+        rejectedStartTransactionRequests: 0,
+        rejectedStopTransactionRequests: 0,
+        skippedConsecutiveTransactions: 0,
+        skippedTransactions: 0,
+        start: false,
+        startTransactionRequests: 0,
+        stopTransactionRequests: 0,
+      }
+    )
   }
 
-  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 getRequireAuthorize (): boolean {
+    return (
+      this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
+    )
   }
 
-  private startConnectors (stopAbsoluteDuration?: boolean): void {
-    if (
-      this.connectorsStatus.size > 0 &&
-      this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
-    ) {
-      this.connectorsStatus.clear()
-      this.initializeConnectorsStatus()
-    }
-    if (this.chargingStation.hasEvses) {
-      for (const [evseId, evseStatus] of this.chargingStation.evses) {
-        if (evseId > 0) {
-          for (const connectorId of evseStatus.connectors.keys()) {
-            this.startConnector(connectorId, stopAbsoluteDuration)
-          }
-        }
-      }
+  private handleStartTransactionResponse (
+    connectorId: number,
+    startResponse: StartTransactionResponse
+  ): void {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
+    if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
     } else {
-      for (const connectorId of this.chargingStation.connectors.keys()) {
-        if (connectorId > 0) {
-          this.startConnector(connectorId, stopAbsoluteDuration)
-        }
-      }
+      logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests
     }
   }
 
-  private stopConnectors (): void {
+  private initializeConnectorsStatus (): void {
     if (this.chargingStation.hasEvses) {
       for (const [evseId, evseStatus] of this.chargingStation.evses) {
         if (evseId > 0) {
           for (const connectorId of evseStatus.connectors.keys()) {
-            this.stopConnector(connectorId)
+            this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
           }
         }
       }
     } else {
       for (const connectorId of this.chargingStation.connectors.keys()) {
         if (connectorId > 0) {
-          this.stopConnector(connectorId)
+          this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
         }
       }
     }
@@ -307,164 +348,31 @@ export class AutomaticTransactionGenerator {
     this.chargingStation.emit(ChargingStationEvents.updated)
   }
 
-  private canStartConnector (connectorId: number): boolean {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
-      logger.info(
-        `${this.logPrefix(
-          connectorId
-        )} entered in transaction loop while the ATG stop date has been reached`
-      )
-      return false
-    }
-    if (!this.chargingStation.inAcceptedState()) {
-      logger.error(
-        `${this.logPrefix(
-          connectorId
-        )} entered in transaction loop while the charging station is not in accepted state`
-      )
-      return false
-    }
-    if (!this.chargingStation.isChargingStationAvailable()) {
-      logger.info(
-        `${this.logPrefix(
-          connectorId
-        )} entered in transaction loop while the charging station is unavailable`
-      )
-      return false
-    }
-    if (!this.chargingStation.isConnectorAvailable(connectorId)) {
-      logger.info(
-        `${this.logPrefix(
-          connectorId
-        )} entered in transaction loop while the connector ${connectorId.toString()} is unavailable`
-      )
-      return false
-    }
-    const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
-    if (connectorStatus?.transactionStarted === true) {
-      logger.info(
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${connectorStatus.transactionId?.toString()} is already started on connector ${connectorId.toString()}`
-      )
-      return false
-    }
-    return true
-  }
-
-  private async waitChargingStationAvailable (connectorId: number): Promise<void> {
-    let logged = false
-    while (!this.chargingStation.isChargingStationAvailable()) {
-      if (!logged) {
-        logger.info(
-          `${this.logPrefix(
-            connectorId
-          )} transaction loop waiting for charging station to be available`
-        )
-        logged = true
-      }
-      await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
-    }
-  }
-
-  private async waitConnectorAvailable (connectorId: number): Promise<void> {
-    let logged = false
-    while (!this.chargingStation.isConnectorAvailable(connectorId)) {
-      if (!logged) {
-        logger.info(
-          `${this.logPrefix(
-            connectorId
-          )} transaction loop waiting for connector ${connectorId.toString()} to be available`
-        )
-        logged = true
-      }
-      await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
-    }
-  }
-
-  private async waitRunningTransactionStopped (connectorId: number): Promise<void> {
-    const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
-    let logged = false
-    while (connectorStatus?.transactionStarted === true) {
-      if (!logged) {
-        logger.info(
-          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-          `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${connectorStatus.transactionId?.toString()} on connector ${connectorId.toString()} to be stopped`
-        )
-        logged = true
-      }
-      await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+  private startConnectors (stopAbsoluteDuration?: boolean): void {
+    if (
+      this.connectorsStatus.size > 0 &&
+      this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
+    ) {
+      this.connectorsStatus.clear()
+      this.initializeConnectorsStatus()
     }
-  }
-
-  private initializeConnectorsStatus (): void {
     if (this.chargingStation.hasEvses) {
       for (const [evseId, evseStatus] of this.chargingStation.evses) {
         if (evseId > 0) {
           for (const connectorId of evseStatus.connectors.keys()) {
-            this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
+            this.startConnector(connectorId, stopAbsoluteDuration)
           }
         }
       }
     } else {
       for (const connectorId of this.chargingStation.connectors.keys()) {
         if (connectorId > 0) {
-          this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
+          this.startConnector(connectorId, stopAbsoluteDuration)
         }
       }
     }
   }
 
-  private getConnectorStatus (connectorId: number): Status {
-    const statusIndex = connectorId - 1
-    if (statusIndex < 0) {
-      logger.error(`${this.logPrefix(connectorId)} invalid connector id`)
-      throw new BaseError(`Invalid connector id ${connectorId.toString()}`)
-    }
-    let connectorStatus: Status | undefined
-    if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
-      connectorStatus = clone<Status>(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
-      )
-    } else {
-      logger.warn(
-        `${this.logPrefix(
-          connectorId
-        )} no status found for connector #${connectorId.toString()} in charging station configuration file. New status will be created`
-      )
-    }
-    if (connectorStatus != null) {
-      connectorStatus.startDate = convertToDate(connectorStatus.startDate)
-      connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
-      connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
-      connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
-      if (
-        !this.started &&
-        (connectorStatus.start ||
-          this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
-      ) {
-        connectorStatus.start = false
-      }
-    }
-    return (
-      connectorStatus ?? {
-        start: false,
-        authorizeRequests: 0,
-        acceptedAuthorizeRequests: 0,
-        rejectedAuthorizeRequests: 0,
-        startTransactionRequests: 0,
-        acceptedStartTransactionRequests: 0,
-        rejectedStartTransactionRequests: 0,
-        stopTransactionRequests: 0,
-        acceptedStopTransactionRequests: 0,
-        rejectedStopTransactionRequests: 0,
-        skippedConsecutiveTransactions: 0,
-        skippedTransactions: 0,
-      }
-    )
-  }
-
   private async startTransaction (
     connectorId: number
   ): Promise<StartTransactionResponse | undefined> {
@@ -530,6 +438,24 @@ export class AutomaticTransactionGenerator {
     return startResponse
   }
 
+  private stopConnectors (): void {
+    if (this.chargingStation.hasEvses) {
+      for (const [evseId, evseStatus] of this.chargingStation.evses) {
+        if (evseId > 0) {
+          for (const connectorId of evseStatus.connectors.keys()) {
+            this.stopConnector(connectorId)
+          }
+        }
+      }
+    } else {
+      for (const connectorId of this.chargingStation.connectors.keys()) {
+        if (connectorId > 0) {
+          this.stopConnector(connectorId)
+        }
+      }
+    }
+  }
+
   private async stopTransaction (
     connectorId: number,
     reason = StopTransactionReason.LOCAL
@@ -566,34 +492,109 @@ export class AutomaticTransactionGenerator {
     return stopResponse
   }
 
-  private getRequireAuthorize (): boolean {
-    return (
-      this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
-    )
+  private async waitChargingStationAvailable (connectorId: number): Promise<void> {
+    let logged = false
+    while (!this.chargingStation.isChargingStationAvailable()) {
+      if (!logged) {
+        logger.info(
+          `${this.logPrefix(
+            connectorId
+          )} transaction loop waiting for charging station to be available`
+        )
+        logged = true
+      }
+      await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+    }
   }
 
-  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 async waitConnectorAvailable (connectorId: number): Promise<void> {
+    let logged = false
+    while (!this.chargingStation.isConnectorAvailable(connectorId)) {
+      if (!logged) {
+        logger.info(
+          `${this.logPrefix(
+            connectorId
+          )} transaction loop waiting for connector ${connectorId.toString()} to be available`
+        )
+        logged = true
+      }
+      await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+    }
   }
 
-  private handleStartTransactionResponse (
-    connectorId: number,
-    startResponse: StartTransactionResponse
-  ): void {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
-    if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
-    } else {
-      logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
+  private async waitRunningTransactionStopped (connectorId: number): Promise<void> {
+    const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
+    let logged = false
+    while (connectorStatus?.transactionStarted === true) {
+      if (!logged) {
+        logger.info(
+          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+          `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${connectorStatus.transactionId?.toString()} on connector ${connectorId.toString()} to be stopped`
+        )
+        logged = true
+      }
+      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)!.rejectedStartTransactionRequests
+      this.connectorsStatus.get(connectorId)!.start = false
+    } else if (this.connectorsStatus.get(connectorId)?.start === false) {
+      logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
     }
   }
 }
index dfd91916bb4abbce6c531584de2861766af77e11..2603de82de31e29b749c72874821244fd3cc16f4 100644 (file)
@@ -1,14 +1,16 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
+import type { Worker } from 'worker_threads'
+
+import chalk from 'chalk'
 import { EventEmitter } from 'node:events'
 import { dirname, extname, join } from 'node:path'
 import process, { exit } from 'node:process'
 import { fileURLToPath } from 'node:url'
 import { isMainThread } from 'node:worker_threads'
-
-import chalk from 'chalk'
 import { availableParallelism, type MessageHandler } from 'poolifier'
-import type { Worker } from 'worker_threads'
+
+import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
 
 import { version } from '../../package.json'
 import { BaseError } from '../exception/index.js'
@@ -44,30 +46,101 @@ import {
 } from '../utils/index.js'
 import { DEFAULT_ELEMENTS_PER_WORKER, type WorkerAbstract, WorkerFactory } from '../worker/index.js'
 import { buildTemplateName, waitChargingStationEvents } from './Helpers.js'
-import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
 import { UIServerFactory } from './ui-server/UIServerFactory.js'
 
 const moduleName = 'Bootstrap'
 
 enum exitCodes {
-  succeeded = 0,
-  missingChargingStationsConfiguration = 1,
   duplicateChargingStationTemplateUrls = 2,
+  gracefulShutdownError = 4,
+  missingChargingStationsConfiguration = 1,
   noChargingStationTemplates = 3,
-  gracefulShutdownError = 4
+  succeeded = 0
 }
 
 export class Bootstrap extends EventEmitter {
   private static instance: Bootstrap | null = null
-  private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
-  private readonly uiServer: AbstractUIServer
-  private storage?: Storage
-  private readonly templateStatistics: Map<string, TemplateStatistics>
-  private readonly version: string = version
+  private readonly logPrefix = (): string => {
+    return logPrefix(' Bootstrap |')
+  }
+
   private started: boolean
   private starting: boolean
   private stopping: boolean
+  private storage?: Storage
+  private readonly templateStatistics: Map<string, TemplateStatistics>
+  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<void>
+      )(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 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 readonly workerEventUpdated = (data: ChargingStationData): void => {
+    this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
+  }
+
+  private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
 
   private constructor () {
     super()
@@ -103,69 +176,280 @@ export class Bootstrap extends EventEmitter {
     return Bootstrap.instance
   }
 
-  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
-    )
-  }
-
-  public getState (): SimulatorState {
-    return {
-      version: this.version,
-      configuration: Configuration.getConfigurationData(),
-      started: this.started,
-      templateStatistics: this.templateStatistics,
-    }
+  private gracefulShutdown (): void {
+    this.stop()
+      .then(() => {
+        console.info(chalk.green('Graceful shutdown'))
+        this.uiServer.stop()
+        this.uiServerStarted = false
+        this.waitChargingStationsStopped()
+          // eslint-disable-next-line promise/no-nesting
+          .then(() => {
+            return exit(exitCodes.succeeded)
+          })
+          // eslint-disable-next-line promise/no-nesting
+          .catch(() => {
+            exit(exitCodes.gracefulShutdownError)
+          })
+        return undefined
+      })
+      .catch((error: unknown) => {
+        console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
+        exit(exitCodes.gracefulShutdownError)
+      })
   }
 
-  public getLastIndex (templateName: string): number {
+  private initializeCounters (): void {
     // 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]
+    const stationTemplateUrls = Configuration.getStationTemplateUrls()!
+    if (isNotEmptyArray(stationTemplateUrls)) {
+      for (const stationTemplateUrl of stationTemplateUrls) {
+        const templateName = buildTemplateName(stationTemplateUrl.file)
+        this.templateStatistics.set(templateName, {
+          added: 0,
+          configured: stationTemplateUrl.numberOfStations,
+          indexes: new Set<number>(),
+          provisioned: stationTemplateUrl.provisionedNumberOfStations ?? 0,
+          started: 0,
+        })
+        this.uiServer.chargingStationTemplates.add(templateName)
+      }
+      if (this.templateStatistics.size !== stationTemplateUrls.length) {
+        console.error(
+          chalk.red(
+            "'stationTemplateUrls' contains duplicate entries, please check your configuration"
+          )
+        )
+        exit(exitCodes.duplicateChargingStationTemplateUrls)
       }
+    } else {
+      console.error(
+        chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
+      )
+      exit(exitCodes.missingChargingStationsConfiguration)
+    }
+    if (
+      this.numberOfConfiguredChargingStations === 0 &&
+      Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
+        .enabled !== true
+    ) {
+      console.error(
+        chalk.red(
+          "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
+        )
+      )
+      exit(exitCodes.noChargingStationTemplates)
     }
-    return indexes[indexes.length - 1]
-  }
-
-  public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
-    return this.storage?.getPerformanceStatistics()
-  }
-
-  private get numberOfAddedChargingStations (): number {
-    return [...this.templateStatistics.values()].reduce(
-      (accumulator, value) => accumulator + value.added,
-      0
-    )
   }
 
-  private get numberOfStartedChargingStations (): number {
-    return [...this.templateStatistics.values()].reduce(
-      (accumulator, value) => accumulator + value.started,
-      0
+  private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
+    if (!isMainThread) {
+      return
+    }
+    let elementsPerWorker: number
+    switch (workerConfiguration.elementsPerWorker) {
+      case 'all':
+        elementsPerWorker =
+          this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations
+        break
+      case 'auto':
+        elementsPerWorker =
+          this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations >
+          availableParallelism()
+            ? Math.round(
+              (this.numberOfConfiguredChargingStations +
+                  this.numberOfProvisionedChargingStations) /
+                  (availableParallelism() * 1.5)
+            )
+            : 1
+        break
+      default:
+        elementsPerWorker = workerConfiguration.elementsPerWorker ?? DEFAULT_ELEMENTS_PER_WORKER
+    }
+    this.workerImplementation = WorkerFactory.getWorkerImplementation<
+      ChargingStationWorkerData,
+      ChargingStationInfo
+    >(
+      join(
+        dirname(fileURLToPath(import.meta.url)),
+        `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
+      ),
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      workerConfiguration.processType!,
+      {
+        elementAddDelay: workerConfiguration.elementAddDelay,
+        elementsPerWorker,
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        poolMaxSize: workerConfiguration.poolMaxSize!,
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        poolMinSize: workerConfiguration.poolMinSize!,
+        poolOptions: {
+          messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
+          ...(workerConfiguration.resourceLimits != null && {
+            workerOptions: {
+              resourceLimits: workerConfiguration.resourceLimits,
+            },
+          }),
+        },
+        workerStartDelay: workerConfiguration.startDelay,
+      }
     )
   }
 
-  public async start (): Promise<void> {
-    if (!this.started) {
-      if (!this.starting) {
-        this.starting = true
-        this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
+  private messageHandler (
+    msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
+  ): void {
+    // logger.debug(
+    //   `${this.logPrefix()} ${moduleName}.messageHandler: Charging station worker message received: ${JSON.stringify(
+    //     msg,
+    //     undefined,
+    //     2
+    //   )}`
+    // )
+    // Skip worker message events processing
+    // eslint-disable-next-line @typescript-eslint/dot-notation
+    if (msg['uuid'] != null) {
+      return
+    }
+    const { data, event } = msg
+    try {
+      switch (event) {
+        case ChargingStationWorkerMessageEvents.added:
+          this.emit(ChargingStationWorkerMessageEvents.added, data)
+          break
+        case ChargingStationWorkerMessageEvents.deleted:
+          this.emit(ChargingStationWorkerMessageEvents.deleted, data)
+          break
+        case ChargingStationWorkerMessageEvents.performanceStatistics:
+          this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, data)
+          break
+        case ChargingStationWorkerMessageEvents.started:
+          this.emit(ChargingStationWorkerMessageEvents.started, data)
+          break
+        case ChargingStationWorkerMessageEvents.stopped:
+          this.emit(ChargingStationWorkerMessageEvents.stopped, data)
+          break
+        case ChargingStationWorkerMessageEvents.updated:
+          this.emit(ChargingStationWorkerMessageEvents.updated, data)
+          break
+        default:
+          throw new BaseError(
+            `Unknown charging station worker message event: '${event}' received with data: ${JSON.stringify(
+              data,
+              undefined,
+              2
+            )}`
+          )
+      }
+    } catch (error) {
+      logger.error(
+        `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling charging station worker message event '${event}':`,
+        error
+      )
+    }
+  }
+
+  private async restart (): Promise<void> {
+    await this.stop()
+    if (
+      this.uiServerStarted &&
+      Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
+        .enabled !== true
+    ) {
+      this.uiServer.stop()
+      this.uiServerStarted = false
+    }
+    this.initializeCounters()
+    // FIXME: initialize worker implementation only if the worker section has changed
+    this.initializeWorkerImplementation(
+      Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
+    )
+    await this.start()
+  }
+
+  private async waitChargingStationsStopped (): Promise<string> {
+    return await new Promise<string>((resolve, reject: (reason?: unknown) => void) => {
+      const waitTimeout = setTimeout(() => {
+        const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
+          Constants.STOP_CHARGING_STATIONS_TIMEOUT
+        )} reached at stopping charging stations`
+        console.warn(chalk.yellow(timeoutMessage))
+        reject(new Error(timeoutMessage))
+      }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
+      waitChargingStationEvents(
+        this,
+        ChargingStationWorkerMessageEvents.stopped,
+        this.numberOfStartedChargingStations
+      )
+        .then(events => {
+          resolve('Charging stations stopped')
+          return events
+        })
+        .finally(() => {
+          clearTimeout(waitTimeout)
+        })
+        .catch(reject)
+    })
+  }
+
+  public async addChargingStation (
+    index: number,
+    templateFile: string,
+    options?: ChargingStationOptions
+  ): Promise<ChargingStationInfo | undefined> {
+    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<Statistics> | 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<void> {
+    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)
@@ -292,317 +576,35 @@ export class Bootstrap extends EventEmitter {
     }
   }
 
-  private async restart (): Promise<void> {
-    await this.stop()
-    if (
-      this.uiServerStarted &&
-      Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
-        .enabled !== true
-    ) {
-      this.uiServer.stop()
-      this.uiServerStarted = false
-    }
-    this.initializeCounters()
-    // FIXME: initialize worker implementation only if the worker section has changed
-    this.initializeWorkerImplementation(
-      Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
-    )
-    await this.start()
-  }
-
-  private async waitChargingStationsStopped (): Promise<string> {
-    return await new Promise<string>((resolve, reject: (reason?: unknown) => void) => {
-      const waitTimeout = setTimeout(() => {
-        const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
-          Constants.STOP_CHARGING_STATIONS_TIMEOUT
-        )} reached at stopping charging stations`
-        console.warn(chalk.yellow(timeoutMessage))
-        reject(new Error(timeoutMessage))
-      }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
-      waitChargingStationEvents(
-        this,
-        ChargingStationWorkerMessageEvents.stopped,
-        this.numberOfStartedChargingStations
-      )
-        .then(events => {
-          resolve('Charging stations stopped')
-          return events
-        })
-        .finally(() => {
-          clearTimeout(waitTimeout)
-        })
-        .catch(reject)
-    })
-  }
-
-  private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
-    if (!isMainThread) {
-      return
-    }
-    let elementsPerWorker: number
-    switch (workerConfiguration.elementsPerWorker) {
-      case 'all':
-        elementsPerWorker =
-          this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations
-        break
-      case 'auto':
-        elementsPerWorker =
-          this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations >
-          availableParallelism()
-            ? Math.round(
-              (this.numberOfConfiguredChargingStations +
-                  this.numberOfProvisionedChargingStations) /
-                  (availableParallelism() * 1.5)
-            )
-            : 1
-        break
-      default:
-        elementsPerWorker = workerConfiguration.elementsPerWorker ?? DEFAULT_ELEMENTS_PER_WORKER
-    }
-    this.workerImplementation = WorkerFactory.getWorkerImplementation<
-      ChargingStationWorkerData,
-      ChargingStationInfo
-    >(
-      join(
-        dirname(fileURLToPath(import.meta.url)),
-        `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
-      ),
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      workerConfiguration.processType!,
-      {
-        workerStartDelay: workerConfiguration.startDelay,
-        elementAddDelay: workerConfiguration.elementAddDelay,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        poolMaxSize: workerConfiguration.poolMaxSize!,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        poolMinSize: workerConfiguration.poolMinSize!,
-        elementsPerWorker,
-        poolOptions: {
-          messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
-          ...(workerConfiguration.resourceLimits != null && {
-            workerOptions: {
-              resourceLimits: workerConfiguration.resourceLimits,
-            },
-          }),
-        },
-      }
+  private get numberOfAddedChargingStations (): number {
+    return [...this.templateStatistics.values()].reduce(
+      (accumulator, value) => accumulator + value.added,
+      0
     )
   }
 
-  private messageHandler (
-    msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
-  ): void {
-    // logger.debug(
-    //   `${this.logPrefix()} ${moduleName}.messageHandler: Charging station worker message received: ${JSON.stringify(
-    //     msg,
-    //     undefined,
-    //     2
-    //   )}`
-    // )
-    // Skip worker message events processing
-    // eslint-disable-next-line @typescript-eslint/dot-notation
-    if (msg['uuid'] != null) {
-      return
-    }
-    const { event, data } = msg
-    try {
-      switch (event) {
-        case ChargingStationWorkerMessageEvents.added:
-          this.emit(ChargingStationWorkerMessageEvents.added, data)
-          break
-        case ChargingStationWorkerMessageEvents.deleted:
-          this.emit(ChargingStationWorkerMessageEvents.deleted, data)
-          break
-        case ChargingStationWorkerMessageEvents.started:
-          this.emit(ChargingStationWorkerMessageEvents.started, data)
-          break
-        case ChargingStationWorkerMessageEvents.stopped:
-          this.emit(ChargingStationWorkerMessageEvents.stopped, data)
-          break
-        case ChargingStationWorkerMessageEvents.updated:
-          this.emit(ChargingStationWorkerMessageEvents.updated, data)
-          break
-        case ChargingStationWorkerMessageEvents.performanceStatistics:
-          this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, data)
-          break
-        default:
-          throw new BaseError(
-            `Unknown charging station worker message event: '${event}' received with data: ${JSON.stringify(
-              data,
-              undefined,
-              2
-            )}`
-          )
-      }
-    } catch (error) {
-      logger.error(
-        `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling charging station worker message event '${event}':`,
-        error
-      )
-    }
-  }
-
-  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 get numberOfChargingStationTemplates (): number {
+    return this.templateStatistics.size
   }
 
-  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))`
+  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)
-  }
-
-  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<void>
-      )(data).catch(Constants.EMPTY_FUNCTION)
-    } else {
-      ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
-        data
-      )
-    }
-  }
-
-  private initializeCounters (): void {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const stationTemplateUrls = Configuration.getStationTemplateUrls()!
-    if (isNotEmptyArray(stationTemplateUrls)) {
-      for (const stationTemplateUrl of stationTemplateUrls) {
-        const templateName = buildTemplateName(stationTemplateUrl.file)
-        this.templateStatistics.set(templateName, {
-          configured: stationTemplateUrl.numberOfStations,
-          provisioned: stationTemplateUrl.provisionedNumberOfStations ?? 0,
-          added: 0,
-          started: 0,
-          indexes: new Set<number>(),
-        })
-        this.uiServer.chargingStationTemplates.add(templateName)
-      }
-      if (this.templateStatistics.size !== stationTemplateUrls.length) {
-        console.error(
-          chalk.red(
-            "'stationTemplateUrls' contains duplicate entries, please check your configuration"
-          )
-        )
-        exit(exitCodes.duplicateChargingStationTemplateUrls)
-      }
-    } else {
-      console.error(
-        chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
-      )
-      exit(exitCodes.missingChargingStationsConfiguration)
-    }
-    if (
-      this.numberOfConfiguredChargingStations === 0 &&
-      Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
-        .enabled !== true
-    ) {
-      console.error(
-        chalk.red(
-          "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
-        )
-      )
-      exit(exitCodes.noChargingStationTemplates)
-    }
-  }
-
-  public async addChargingStation (
-    index: number,
-    templateFile: string,
-    options?: ChargingStationOptions
-  ): Promise<ChargingStationInfo | undefined> {
-    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,
-      templateFile: join(
-        dirname(fileURLToPath(import.meta.url)),
-        'assets',
-        'station-templates',
-        templateFile
-      ),
-      options,
-    })
-    // 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 gracefulShutdown (): void {
-    this.stop()
-      .then(() => {
-        console.info(chalk.green('Graceful shutdown'))
-        this.uiServer.stop()
-        this.uiServerStarted = false
-        this.waitChargingStationsStopped()
-          // eslint-disable-next-line promise/no-nesting
-          .then(() => {
-            return exit(exitCodes.succeeded)
-          })
-          // eslint-disable-next-line promise/no-nesting
-          .catch(() => {
-            exit(exitCodes.gracefulShutdownError)
-          })
-        return undefined
-      })
-      .catch((error: unknown) => {
-        console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
-        exit(exitCodes.gracefulShutdownError)
-      })
-  }
-
-  private readonly logPrefix = (): string => {
-    return logPrefix(' Bootstrap |')
-  }
 }
index cdf6930493a6b87de466ee2960943e7ae9c192e2..83e5517b537b696ac6b61c7d984c50347e31b8cf 100644 (file)
@@ -1,13 +1,12 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
+import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
 import { createHash, randomInt } from 'node:crypto'
 import { EventEmitter } from 'node:events'
 import { existsSync, type FSWatcher, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
 import { dirname, join } from 'node:path'
 import { URL } from 'node:url'
 import { parentPort } from 'node:worker_threads'
-
-import { millisecondsToSeconds, secondsToMilliseconds } from 'date-fns'
 import { mergeDeepRight, once } from 'rambda'
 import { type RawData, WebSocket } from 'ws'
 
@@ -157,40 +156,60 @@ import {
 import { SharedLRUCache } from './SharedLRUCache.js'
 
 export class ChargingStation extends EventEmitter {
-  public readonly index: number
-  public readonly templateFile: string
-  public stationInfo?: ChargingStationInfo
-  public started: boolean
-  public starting: boolean
-  public idTagsCache: IdTagsCache
-  public automaticTransactionGenerator?: AutomaticTransactionGenerator
-  public ocppConfiguration?: ChargingStationOcppConfiguration
-  public wsConnection: WebSocket | null
-  public readonly connectors: Map<number, ConnectorStatus>
-  public readonly evses: Map<number, EvseStatus>
-  public readonly requests: Map<string, CachedRequest>
-  public performanceStatistics?: PerformanceStatistics
-  public heartbeatSetInterval?: NodeJS.Timeout
-  public ocppRequestService!: OCPPRequestService
-  public bootNotificationRequest?: BootNotificationRequest
-  public bootNotificationResponse?: BootNotificationResponse
-  public powerDivider?: number
-  private stopping: boolean
+  private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
+  private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
   private configurationFile!: string
   private configurationFileHash!: string
+  private configuredSupervisionUrl!: URL
   private connectorsConfigurationHash!: string
   private evsesConfigurationHash!: string
-  private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
-  private ocppIncomingRequestService!: OCPPIncomingRequestService
+  private flushMessageBufferSetInterval?: NodeJS.Timeout
   private readonly messageBuffer: Set<string>
-  private configuredSupervisionUrl!: URL
-  private wsConnectionRetryCount: number
-  private templateFileWatcher?: FSWatcher
-  private templateFileHash!: string
+  private ocppIncomingRequestService!: OCPPIncomingRequestService
   private readonly sharedLRUCache: SharedLRUCache
+  private stopping: boolean
+  private templateFileHash!: string
+  private templateFileWatcher?: FSWatcher
+  private wsConnectionRetryCount: number
   private wsPingSetInterval?: NodeJS.Timeout
-  private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
-  private flushMessageBufferSetInterval?: NodeJS.Timeout
+  public automaticTransactionGenerator?: AutomaticTransactionGenerator
+  public bootNotificationRequest?: BootNotificationRequest
+  public bootNotificationResponse?: BootNotificationResponse
+  public readonly connectors: Map<number, ConnectorStatus>
+  public readonly evses: Map<number, EvseStatus>
+  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
+  public powerDivider?: number
+  public readonly requests: Map<string, CachedRequest>
+  public started: boolean
+  public starting: boolean
+  public stationInfo?: ChargingStationInfo
+  public readonly templateFile: string
+
+  public wsConnection: null | WebSocket
 
   constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
     super()
@@ -262,1015 +281,1229 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  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 add (): void {
+    this.emit(ChargingStationEvents.added)
   }
 
-  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
+  private clearIntervalFlushMessageBuffer (): void {
+    if (this.flushMessageBufferSetInterval != null) {
+      clearInterval(this.flushMessageBufferSetInterval)
+      delete this.flushMessageBufferSetInterval
     }
-    return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
-  }
-
-  public hasIdTags (): boolean {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
   }
 
-  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
+  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 isWebSocketConnectionOpened (): boolean {
-    return this.wsConnection?.readyState === WebSocket.OPEN
-  }
-
-  public inUnknownState (): boolean {
-    return this.bootNotificationResponse?.status == null
-  }
-
-  public inPendingState (): boolean {
-    return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
-  }
-
-  public inAcceptedState (): boolean {
-    return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
-  }
-
-  public inRejectedState (): boolean {
-    return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
-  }
-
-  public isRegistered (): boolean {
-    return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
-  }
-
-  public isChargingStationAvailable (): boolean {
-    return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
-  }
-
-  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 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)
+      )
     }
-    return this.connectors.has(connectorId)
   }
 
-  public isConnectorAvailable (connectorId: number): boolean {
-    return (
-      connectorId > 0 &&
-      this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
+  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 getNumberOfConnectors (): number {
-    if (this.hasEvses) {
-      let numberOfConnectors = 0
-      for (const [evseId, evseStatus] of this.evses) {
-        if (evseId > 0) {
-          numberOfConnectors += evseStatus.connectors.size
+  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!
         }
+      } catch (error) {
+        handleFileException(
+          this.configurationFile,
+          FileType.ChargingStationConfiguration,
+          error as NodeJS.ErrnoException,
+          this.logPrefix()
+        )
       }
-      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
+    return configuration
   }
 
-  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)
-        }
+  private getConfiguredSupervisionUrl (): URL {
+    let configuredSupervisionUrl: string
+    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
       }
-      return undefined
+      configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
+    } else {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      configuredSupervisionUrl = supervisionUrls!
     }
-    return this.connectors.get(connectorId)
+    if (isNotEmptyString(configuredSupervisionUrl)) {
+      return new URL(configuredSupervisionUrl)
+    }
+    const errorMsg = 'No supervision url(s) configured'
+    logger.error(`${this.logPrefix()} ${errorMsg}`)
+    throw new BaseError(errorMsg)
   }
 
-  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
-      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!
+  // 0 for disabling
+  private getConnectionTimeout (): number {
+    if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
+      return convertToInt(
+        getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
+          Constants.DEFAULT_CONNECTION_TIMEOUT
+      )
     }
-    // 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(
-      isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
+    return Constants.DEFAULT_CONNECTION_TIMEOUT
+  }
+
+  private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
+    return (
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      isNaN(connectorAmperageLimitationLimit!)
-        ? Number.POSITIVE_INFINITY
-        : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        connectorAmperageLimitationLimit!,
-      isNaN(chargingStationChargingProfilesLimit)
-        ? Number.POSITIVE_INFINITY
-        : chargingStationChargingProfilesLimit,
+      (stationInfo ?? this.stationInfo!).currentOutType ??
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      isNaN(connectorChargingProfilesLimit!)
-        ? Number.POSITIVE_INFINITY
-        : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        connectorChargingProfilesLimit!
+      Constants.DEFAULT_STATION_INFO.currentOutType!
     )
   }
 
-  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
-        }
-      }
+  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
+    )
   }
 
-  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
-          }
-        }
-      }
-    } else {
-      for (const connectorId of this.connectors.keys()) {
-        if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
-          ++numberOfRunningTransactions
-        }
-      }
+  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))
     }
-    return numberOfRunningTransactions
   }
 
-  public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
-    if (transactionId == null) {
-      return undefined
-    } else if (this.hasEvses) {
+  private getNumberOfReservableConnectors (): number {
+    let numberOfReservableConnectors = 0
+    if (this.hasEvses) {
       for (const evseStatus of this.evses.values()) {
-        for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-          if (connectorStatus.transactionId === transactionId) {
-            return connectorId
-          }
-        }
+        numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
       }
     } else {
-      for (const connectorId of this.connectors.keys()) {
-        if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
-          return connectorId
-        }
-      }
+      numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
     }
+    return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
   }
 
-  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
-    )
+  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
   }
 
-  public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
-    return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
+  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 getAuthorizeRemoteTxRequests (): boolean {
-    const authorizeRemoteTxRequests = getConfigurationKey(
-      this,
-      StandardParametersKey.AuthorizeRemoteTxRequests
-    )
-    return authorizeRemoteTxRequests != null
-      ? convertToBoolean(authorizeRemoteTxRequests.value)
-      : false
+  private getOcppConfigurationFromFile (
+    ocppPersistentConfiguration?: boolean
+  ): ChargingStationOcppConfiguration | undefined {
+    const configurationKey = this.getConfigurationFromFile()?.configurationKey
+    if (ocppPersistentConfiguration && Array.isArray(configurationKey)) {
+      return { configurationKey }
+    }
+    return undefined
   }
 
-  public getLocalAuthListEnabled (): boolean {
-    const localAuthListEnabled = getConfigurationKey(
-      this,
-      StandardParametersKey.LocalAuthListEnabled
-    )
-    return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
+  private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
+    return this.getTemplateFromFile()?.Configuration
   }
 
-  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 getPowerDivider (): number {
+    let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
+    if (this.stationInfo?.powerSharedByConnectors === true) {
+      powerDivider = this.getNumberOfRunningTransactions()
     }
-    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
+    return powerDivider
   }
 
-  public setSupervisionUrl (url: string): void {
+  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 (
-      this.stationInfo?.supervisionUrlOcppConfiguration === true &&
-      isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
+      stationInfoFromFile != null &&
+      stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
     ) {
-      setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
+      stationInfo = stationInfoFromFile
     } else {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.stationInfo!.supervisionUrls = url
-      this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
-      this.saveStationInfo()
+      stationInfo = stationInfoFromTemplate
+      stationInfoFromFile != null &&
+        propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
     }
+    return setChargingStationOptions(
+      mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
+      options
+    )
   }
 
-  public startHeartbeat (): void {
-    const heartbeatInterval = this.getHeartbeatInterval()
-    if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) {
-      this.heartbeatSetInterval = setInterval(() => {
-        this.ocppRequestService
-          .requestHandler<HeartbeatRequest, HeartbeatResponse>(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
-        )}`
-      )
-    } else if (this.heartbeatSetInterval != null) {
-      logger.info(
-        `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
-          heartbeatInterval
-        )}`
-      )
-    } else {
-      logger.error(
-        `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat`
-      )
+  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) {
+        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)
+        }
+      }
     }
+    return stationInfo
   }
 
-  public restartHeartbeat (): void {
-    // Stop heartbeat
-    this.stopHeartbeat()
-    // Start heartbeat
-    this.startHeartbeat()
-  }
-
-  public restartWebSocketPing (): void {
-    // Stop WebSocket ping
-    this.stopWebSocketPing()
-    // Start WebSocket ping
-    this.startWebSocketPing()
-  }
-
-  public startMeterValues (connectorId: number, interval: number): void {
-    if (connectorId === 0) {
-      logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
-      )
-      return
+  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 connectorStatus = this.getConnectorStatus(connectorId)
-    if (connectorStatus == null) {
-      logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on non existing connector id
-          ${connectorId.toString()}`
-      )
-      return
+    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 {
+      stationInfo.maximumPower =
+        stationTemplate.powerUnit === PowerUnits.KILO_WATT
+          ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          stationTemplate.power! * 1000
+          : stationTemplate.power
     }
-    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
+    stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
+    if (
+      isNotEmptyString(stationInfo.firmwareVersionPattern) &&
+      isNotEmptyString(stationInfo.firmwareVersion) &&
+      !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
     ) {
-      logger.error(
-        `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id`
+      logger.warn(
+        `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
+          this.templateFile
+        } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
       )
-      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<MeterValuesRequest, MeterValuesResponse>(
-            this,
-            RequestCommand.METER_VALUES,
-            {
-              connectorId,
-              transactionId: connectorStatus.transactionId,
-              meterValue: [meterValue],
-            }
-          )
-          .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 (stationTemplate.resetTime != null) {
+      stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
     }
+    return stationInfo
   }
 
-  public stopMeterValues (connectorId: number): void {
-    const connectorStatus = this.getConnectorStatus(connectorId)
-    if (connectorStatus?.transactionSetInterval != null) {
-      clearInterval(connectorStatus.transactionSetInterval)
+  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 = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+          .update(JSON.stringify(template))
+          .digest('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 restartMeterValues (connectorId: number, interval: number): void {
-    this.stopMeterValues(connectorId)
-    this.startMeterValues(connectorId, interval)
+  private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
   }
 
-  private add (): void {
-    this.emit(ChargingStationEvents.added)
+  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 async delete (deleteConfiguration = true): Promise<void> {
-    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 getWebSocketPingInterval (): number {
+    return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
+      ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
+      : 0
   }
 
-  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...`)
+  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 async stop (
-    reason?: StopTransactionReason,
-    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
-  ): Promise<void> {
-    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 {
-        logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
-      }
-    } else {
-      logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
+  private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
+    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
+    )
+    this.emit(ChargingStationEvents.updated)
   }
 
-  public async reset (reason?: StopTransactionReason): Promise<void> {
-    await this.stop(reason)
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    await sleep(this.stationInfo!.resetTime!)
-    this.initialize()
-    this.start()
-  }
-
-  public saveOcppConfiguration (): void {
-    if (this.stationInfo?.ocppPersistentConfiguration === true) {
-      this.saveConfiguration()
+  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
+      )
     }
+    // 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 bufferMessage (message: string): void {
-    this.messageBuffer.add(message)
-    this.setIntervalFlushMessageBuffer()
+  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 openWSConnection (
-    options?: WsOptions,
-    params?: { closeOpened?: boolean; terminateOpened?: boolean }
-  ): void {
-    options = {
-      handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
-      ...this.stationInfo?.wsOptions,
-      ...options,
+  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 {
+      this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
     }
-    params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
-    if (!checkChargingStationState(this, this.logPrefix())) {
-      return
+    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('.')
+      }
     }
-    if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
-      options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
+    this.saveStationInfo()
+    this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+    if (this.stationInfo.enableStatistics === true) {
+      this.performanceStatistics = PerformanceStatistics.getInstance(
+        this.stationInfo.hashId,
+        this.stationInfo.chargingStationId,
+        this.configuredSupervisionUrl
+      )
     }
-    if (params.closeOpened) {
-      this.closeWSConnection()
+    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)
     }
-    if (params.terminateOpened) {
-      this.terminateWSConnection()
+    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.isWebSocketConnectionOpened()) {
-      logger.warn(
-        `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
-      )
-      return
+  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)
     }
-
-    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)
+    if (stationTemplate.Connectors?.[0] == null) {
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with no connector id 0 configuration`
       )
-    })
-    // Handle WebSocket ping
-    this.wsConnection.on('ping', this.onPing.bind(this))
-    // Handle WebSocket pong
-    this.wsConnection.on('pong', this.onPong.bind(this))
-  }
-
-  public closeWSConnection (): void {
-    if (this.isWebSocketConnectionOpened()) {
-      this.wsConnection?.close()
-      this.wsConnection = null
     }
-  }
-
-  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,
+    if (stationTemplate.Connectors != null) {
+      const { configuredMaxConnectors, templateMaxAvailableConnectors, templateMaxConnectors } =
+        checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
+      const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        .update(
+          `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
+        )
+        .digest('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>(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 {
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with no connectors configuration defined, using already defined connectors`
+      )
     }
-    return this.automaticTransactionGeneratorConfiguration
   }
 
-  public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
-    return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
-  }
-
-  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)
+  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>(connectorStatus))
+        )
+      }
+    } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
+      for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
+        const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
+        delete evseStatus.connectorsStatus
+        this.evses.set(evseId, {
+          ...(evseStatus as EvseStatus),
+          connectors: new Map<number, ConnectorStatus>(
+            // 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 {
-      this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
+      const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
+      logger.error(`${this.logPrefix()} ${errorMsg}`)
+      throw new BaseError(errorMsg)
     }
-    this.saveAutomaticTransactionGeneratorConfiguration()
-    this.emit(ChargingStationEvents.updated)
   }
 
-  public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
-    if (isNotEmptyArray(connectorIds)) {
-      for (const connectorId of connectorIds) {
-        this.automaticTransactionGenerator?.stopConnector(connectorId)
-      }
+  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 {
-      this.automaticTransactionGenerator?.stop()
+      const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
+      logger.error(`${this.logPrefix()} ${errorMsg}`)
+      throw new BaseError(errorMsg)
     }
-    this.saveAutomaticTransactionGeneratorConfiguration()
-    this.emit(ChargingStationEvents.updated)
   }
 
-  public async stopTransactionOnConnector (
-    connectorId: number,
-    reason?: StopTransactionReason
-  ): Promise<StopTransactionResponse> {
-    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)
+  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`
       )
-      await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
-        this,
-        RequestCommand.METER_VALUES,
-        {
-          connectorId,
-          transactionId,
-          meterValue: [transactionEndMeterValue],
-        }
+    }
+    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`
       )
     }
-    return await this.ocppRequestService.requestHandler<
-      Partial<StopTransactionRequest>,
-      StopTransactionResponse
-    >(this, RequestCommand.STOP_TRANSACTION, {
-      transactionId,
-      meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
-      ...(reason != null && { reason }),
-    })
-  }
-
-  public getReserveConnectorZeroSupported (): boolean {
-    return convertToBoolean(
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
-    )
-  }
-
-  public async addReservation (reservation: Reservation): Promise<void> {
-    const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
-    if (reservationFound != null) {
-      await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
+    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`
+      )
     }
-    // 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 }
-    )
-  }
-
-  public async removeReservation (
-    reservation: Reservation,
-    reason: ReservationTerminationReason
-  ): Promise<void> {
-    // 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.RESERVATION_CANCELED:
-      case ReservationTerminationReason.REPLACE_EXISTING:
-      case ReservationTerminationReason.EXPIRED:
-        await sendAndSetConnectorStatus(
-          this,
-          reservation.connectorId,
-          ConnectorStatusEnum.Available,
-          undefined,
-          { send: reservation.connectorId !== 0 }
-        )
-        delete connector.reservation
-        break
-      default:
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        throw new BaseError(`Unknown reservation termination reason '${reason}'`)
-    }
-  }
-
-  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
+    if (stationTemplate.Evses != null) {
+      const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+        .update(JSON.stringify(stationTemplate.Evses))
+        .digest('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 connectorStatus of this.connectors.values()) {
-        if (connectorStatus.reservation?.[filterKey] === value) {
-          return connectorStatus.reservation
-        }
-      }
+      logger.warn(
+        `${this.logPrefix()} Charging station information from template ${
+          this.templateFile
+        } with no evses configuration defined, using already defined evses`
+      )
     }
   }
 
-  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 initializeOcppConfiguration (): void {
+    if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
+      addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
+    }
+    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,
+      })
     }
-    return false
-  }
-
-  private setIntervalFlushMessageBuffer (): void {
-    if (this.flushMessageBufferSetInterval == null) {
-      this.flushMessageBufferSetInterval = setInterval(() => {
-        if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
-          this.flushMessageBuffer()
+    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())!
+            )
+          }
         }
-        if (this.messageBuffer.size === 0) {
-          this.clearIntervalFlushMessageBuffer()
+      } else {
+        for (const connectorId of this.connectors.keys()) {
+          connectorsPhaseRotation.push(
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
+          )
         }
-      }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
+      }
+      addConfigurationKey(
+        this,
+        StandardParametersKey.ConnectorPhaseRotation,
+        connectorsPhaseRotation.toString()
+      )
     }
-  }
-
-  private clearIntervalFlushMessageBuffer (): void {
-    if (this.flushMessageBufferSetInterval != null) {
-      clearInterval(this.flushMessageBufferSetInterval)
-      delete this.flushMessageBufferSetInterval
+    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()
   }
 
-  private getNumberOfReservableConnectors (): number {
-    let numberOfReservableConnectors = 0
-    if (this.hasEvses) {
-      for (const evseStatus of this.evses.values()) {
-        numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
-      }
-    } else {
-      numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
+  private initializeOcppServices (): void {
+    const ocppVersion = this.stationInfo?.ocppVersion
+    switch (ocppVersion) {
+      case OCPPVersion.VERSION_16:
+        this.ocppIncomingRequestService =
+          OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
+        this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
+          OCPP16ResponseService.getInstance<OCPP16ResponseService>()
+        )
+        break
+      case OCPPVersion.VERSION_20:
+      case OCPPVersion.VERSION_201:
+        this.ocppIncomingRequestService =
+          OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
+        this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
+          OCPP20ResponseService.getInstance<OCPP20ResponseService>()
+        )
+        break
+      default:
+        this.handleUnsupportedVersion(ocppVersion)
+        break
     }
-    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
+  private internalStopMessageSequence (): void {
+    // Stop WebSocket ping
+    this.stopWebSocketPing()
+    // Stop heartbeat
+    this.stopHeartbeat()
+    // Stop the ATG
+    if (this.automaticTransactionGenerator?.started === true) {
+      this.stopAutomaticTransactionGenerator()
     }
-    return 0
   }
 
-  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 OutgoingRequest | Response | ErrorResponse
-        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
+  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
     }
   }
 
-  private getTemplateFromFile (): ChargingStationTemplate | undefined {
-    let template: ChargingStationTemplate | undefined
+  private onError (error: WSError): void {
+    this.closeWSConnection()
+    logger.error(`${this.logPrefix()} WebSocket error:`, error)
+  }
+
+  private async onMessage (data: RawData): Promise<void> {
+    let request: ErrorResponse | IncomingRequest | Response | undefined
+    let messageType: MessageType | undefined
+    let errorMsg: string
     try {
-      if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
-        template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
+      // 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 {
-        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 = createHash(Constants.DEFAULT_HASH_ALGORITHM)
-          .update(JSON.stringify(template))
-          .digest('hex')
-        this.sharedLRUCache.setChargingStationTemplate(template)
-        this.templateFileHash = template.templateHash
+        throw new OCPPError(
+          ErrorType.PROTOCOL_ERROR,
+          'Incoming message is not an array',
+          undefined,
+          {
+            request,
+          }
+        )
       }
     } catch (error) {
-      handleFileException(
-        this.templateFile,
-        FileType.ChargingStationTemplate,
-        error as NodeJS.ErrnoException,
-        this.logPrefix()
+      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 template
   }
 
-  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]
+  private async onOpen (): Promise<void> {
+    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 {
-      stationInfo.maximumPower =
-        stationTemplate.powerUnit === PowerUnits.KILO_WATT
-          ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          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}'`
+        `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
       )
     }
-    if (stationTemplate.resetTime != null) {
-      stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
-    }
-    return stationInfo
   }
 
-  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) {
-        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)
-        }
-      }
-    }
-    return stationInfo
+  private onPing (): void {
+    logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
   }
 
-  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
+  private onPong (): void {
+    logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
+  }
+
+  private async reconnect (): Promise<void> {
     if (
-      stationInfoFromFile != null &&
-      stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
+      this.stationInfo?.autoReconnectMaxRetries === -1
     ) {
-      stationInfo = stationInfoFromFile
+      ++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()})`
+      )
+    }
+  }
+
+  private saveAutomaticTransactionGeneratorConfiguration (): void {
+    if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
+      this.saveConfiguration()
+    }
+  }
+
+  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<ChargingStationConfiguration>(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 = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+          .update(
+            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)
+          )
+          .digest('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 {
-      stationInfo = stationInfoFromTemplate
-      stationInfoFromFile != null &&
-        propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
+      logger.error(
+        `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
+      )
     }
-    return setChargingStationOptions(
-      mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
-      options
-    )
+  }
+
+  private saveConnectorsStatus (): void {
+    this.saveConfiguration()
+  }
+
+  private saveEvsesStatus (): void {
+    this.saveConfiguration()
   }
 
   private saveStationInfo (): void {
@@ -1279,1200 +1512,967 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  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)
+  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 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 {
-      this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
+  private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
+    if (this.stationInfo?.autoRegister === true) {
+      await this.ocppRequestService.requestHandler<
+        BootNotificationRequest,
+        BootNotificationResponse
+      >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
+        skipBufferingOnError: true,
+      })
     }
-    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('.')
-      }
+    // Start WebSocket ping
+    if (this.wsPingSetInterval == null) {
+      this.startWebSocketPing()
     }
-    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)
+    // Start heartbeat
+    if (this.heartbeatSetInterval == null) {
+      this.startHeartbeat()
     }
-    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,
+    // 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
+            )
+          }
+        }
+      }
+    } 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)!
+            )
+          )
+        }
       }
     }
-  }
+    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
+    }
 
-  private initializeOcppServices (): void {
-    const ocppVersion = this.stationInfo?.ocppVersion
-    switch (ocppVersion) {
-      case OCPPVersion.VERSION_16:
-        this.ocppIncomingRequestService =
-          OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
-        this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
-          OCPP16ResponseService.getInstance<OCPP16ResponseService>()
-        )
-        break
-      case OCPPVersion.VERSION_20:
-      case OCPPVersion.VERSION_201:
-        this.ocppIncomingRequestService =
-          OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
-        this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
-          OCPP20ResponseService.getInstance<OCPP20ResponseService>()
-        )
-        break
-      default:
-        this.handleUnsupportedVersion(ocppVersion)
-        break
+    // Start the ATG
+    if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+      this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
     }
+    this.flushMessageBuffer()
   }
 
-  private initializeOcppConfiguration (): void {
-    if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
-      addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
-    }
-    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 }
+  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.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()
+    } else if (this.wsPingSetInterval != null) {
+      logger.info(
+        `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
+          webSocketPingInterval
+        )}`
       )
-    }
-    if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
-      addConfigurationKey(
-        this,
-        StandardParametersKey.SupportedFeatureProfiles,
-        `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
+    } else {
+      logger.error(
+        `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping`
       )
     }
-    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
-      )
+  }
+
+  private stopHeartbeat (): void {
+    if (this.heartbeatSetInterval != null) {
+      clearInterval(this.heartbeatSetInterval)
+      delete this.heartbeatSetInterval
     }
-    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())!
+  }
+
+  private async stopMessageSequence (
+    reason?: StopTransactionReason,
+    stopTransactions?: boolean
+  ): Promise<void> {
+    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
           }
         }
-      } else {
-        for (const connectorId of this.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()) {
+        if (connectorId > 0) {
+          await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
+          delete this.getConnectorStatus(connectorId)?.status
         }
       }
-      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()
   }
 
-  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>(connectorStatus))
-        )
+  private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
+    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 if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
-      for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
-        const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
-        delete evseStatus.connectorsStatus
-        this.evses.set(evseId, {
-          ...(evseStatus as EvseStatus),
-          connectors: new Map<number, ConnectorStatus>(
-            // 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)
+      for (const connectorId of this.connectors.keys()) {
+        if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
+          await this.stopTransactionOnConnector(connectorId, 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)
+  private stopWebSocketPing (): void {
+    if (this.wsPingSetInterval != null) {
+      clearInterval(this.wsPingSetInterval)
+      delete this.wsPingSetInterval
     }
   }
 
-  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 (stationTemplate.Connectors?.[0] == null) {
-      logger.warn(
-        `${this.logPrefix()} Charging station information from template ${
-          this.templateFile
-        } with no connector id 0 configuration`
-      )
-    }
-    if (stationTemplate.Connectors != null) {
-      const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
-        checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
-      const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
-        .update(
-          `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
-        )
-        .digest('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>(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 {
-      logger.warn(
-        `${this.logPrefix()} Charging station information from template ${
-          this.templateFile
-        } with no connectors configuration defined, using already defined connectors`
-      )
+  private terminateWSConnection (): void {
+    if (this.isWebSocketConnectionOpened()) {
+      this.wsConnection?.terminate()
+      this.wsConnection = null
     }
   }
 
-  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 = createHash(Constants.DEFAULT_HASH_ALGORITHM)
-        .update(JSON.stringify(stationTemplate.Evses))
-        .digest('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, {
-              connectors: buildConnectorsMap(
-                stationTemplate.Evses[evseKey].Connectors,
-                this.logPrefix(),
-                this.templateFile
-              ),
-              availability: AvailabilityType.Operative,
-            })
-            // 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`
-      )
+  public async addReservation (reservation: Reservation): Promise<void> {
+    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 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!
-        }
-      } catch (error) {
-        handleFileException(
-          this.configurationFile,
-          FileType.ChargingStationConfiguration,
-          error as NodeJS.ErrnoException,
-          this.logPrefix()
-        )
-      }
-    }
-    return configuration
+  public bufferMessage (message: string): void {
+    this.messageBuffer.add(message)
+    this.setIntervalFlushMessageBuffer()
   }
 
-  private saveAutomaticTransactionGeneratorConfiguration (): void {
-    if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
-      this.saveConfiguration()
+  public closeWSConnection (): void {
+    if (this.isWebSocketConnectionOpened()) {
+      this.wsConnection?.close()
+      this.wsConnection = null
     }
   }
 
-  private saveConnectorsStatus (): void {
-    this.saveConfiguration()
+  public async delete (deleteConfiguration = true): Promise<void> {
+    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 saveEvsesStatus (): void {
-    this.saveConfiguration()
+  public getAuthorizeRemoteTxRequests (): boolean {
+    const authorizeRemoteTxRequests = getConfigurationKey(
+      this,
+      StandardParametersKey.AuthorizeRemoteTxRequests
+    )
+    return authorizeRemoteTxRequests != null
+      ? convertToBoolean(authorizeRemoteTxRequests.value)
+      : false
   }
 
-  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<ChargingStationConfiguration>(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 = createHash(Constants.DEFAULT_HASH_ALGORITHM)
-          .update(
-            JSON.stringify({
-              stationInfo: configurationData.stationInfo,
-              configurationKey: configurationData.configurationKey,
-              automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
-              ...(this.connectors.size > 0 && {
-                connectorsStatus: configurationData.connectorsStatus,
-              }),
-              ...(this.evses.size > 0 && {
-                evsesStatus: configurationData.evsesStatus,
-              }),
-            } satisfies ChargingStationConfiguration)
-          )
-          .digest('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()
-        )
+  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,
       }
-    } else {
-      logger.error(
-        `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
-      )
-    }
-  }
-
-  private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
-    return this.getTemplateFromFile()?.Configuration
-  }
-
-  private getOcppConfigurationFromFile (
-    ocppPersistentConfiguration?: boolean
-  ): ChargingStationOcppConfiguration | undefined {
-    const configurationKey = this.getConfigurationFromFile()?.configurationKey
-    if (ocppPersistentConfiguration && Array.isArray(configurationKey)) {
-      return { configurationKey }
     }
-    return undefined
+    return this.automaticTransactionGeneratorConfiguration
   }
 
-  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 getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
+    return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
   }
 
-  private async onOpen (): Promise<void> {
-    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
-            )
+  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
           }
-        } 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 onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
-    this.emit(ChargingStationEvents.disconnected)
-    this.emit(ChargingStationEvents.updated)
-    switch (code) {
-      // Normal close
-      case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
-      case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
-        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
+      for (const connectorId of this.connectors.keys()) {
+        if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
+          return connectorId
+        }
+      }
     }
   }
 
-  private getCachedRequest (
-    messageType: MessageType | undefined,
-    messageId: string
-  ): CachedRequest | undefined {
-    const cachedRequest = this.requests.get(messageId)
-    if (Array.isArray(cachedRequest)) {
-      return cachedRequest
+  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
+      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!
     }
-    throw new OCPPError(
-      ErrorType.PROTOCOL_ERROR,
-      `Cached request for message id '${messageId}' ${getMessageTypeString(
-        messageType
-      )} is not an array`,
-      undefined,
-      cachedRequest
+    // 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(
+      isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      isNaN(connectorAmperageLimitationLimit!)
+        ? Number.POSITIVE_INFINITY
+        : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        connectorAmperageLimitationLimit!,
+      isNaN(chargingStationChargingProfilesLimit)
+        ? Number.POSITIVE_INFINITY
+        : chargingStationChargingProfilesLimit,
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      isNaN(connectorChargingProfilesLimit!)
+        ? Number.POSITIVE_INFINITY
+        : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        connectorChargingProfilesLimit!
     )
   }
 
-  private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
-    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)
+  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
     }
-    logger.debug(
-      `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
-        request
-      )}`
-    )
-    // Process the message
-    await this.ocppIncomingRequestService.incomingRequestHandler(
-      this,
-      messageId,
-      commandName,
-      commandPayload
-    )
-    this.emit(ChargingStationEvents.updated)
+    return this.connectors.get(connectorId)
   }
 
-  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
-      )
-    }
-    // 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 getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
+    return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
   }
 
-  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,
-        { errorType, errorMessage, errorDetails }
-      )
-    }
-    // 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
-      )}`
+  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
     )
-    errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
   }
 
-  private async onMessage (data: RawData): Promise<void> {
-    let request: IncomingRequest | Response | ErrorResponse | 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 IncomingRequest | Response | ErrorResponse
-      if (Array.isArray(request)) {
-        ;[messageType] = request
-        // Check the type of message
-        switch (messageType) {
-          // 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
-          // Error Message
-          case MessageType.CALL_ERROR_MESSAGE:
-            this.handleErrorMessage(request as ErrorResponse)
-            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: RequestCommand | IncomingRequestCommand | undefined
-      let errorCallback: ErrorCallback
-      const [, messageId] = request
-      switch (messageType) {
-        case MessageType.CALL_MESSAGE:
-          ;[, , commandName] = request as IncomingRequest
-          // Send error
-          await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
-          break
-        case MessageType.CALL_RESULT_MESSAGE:
-        case MessageType.CALL_ERROR_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
-      }
-      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
-      )
+  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))
     }
+    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
   }
 
-  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`)
+  public getLocalAuthListEnabled (): boolean {
+    const localAuthListEnabled = getConfigurationKey(
+      this,
+      StandardParametersKey.LocalAuthListEnabled
+    )
+    return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
   }
 
-  private onError (error: WSError): void {
-    this.closeWSConnection()
-    logger.error(`${this.logPrefix()} WebSocket error:`, error)
+  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
   }
 
-  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
-    )
+  public getNumberOfEvses (): number {
+    return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
   }
 
-  private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
+  public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
+    const localStationInfo = stationInfo ?? this.stationInfo!
+    switch (this.getCurrentOutType(stationInfo)) {
+      case CurrentType.AC:
+        return localStationInfo.numberOfPhases ?? 3
+      case CurrentType.DC:
+        return 0
+    }
   }
 
-  private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
+  public getNumberOfRunningTransactions (): number {
+    let numberOfRunningTransactions = 0
     if (this.hasEvses) {
       for (const [evseId, evseStatus] of this.evses) {
         if (evseId === 0) {
           continue
         }
-        for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+        for (const connectorStatus of evseStatus.connectors.values()) {
           if (connectorStatus.transactionStarted === true) {
-            await this.stopTransactionOnConnector(connectorId, reason)
+            ++numberOfRunningTransactions
           }
         }
       }
     } else {
       for (const connectorId of this.connectors.keys()) {
         if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
-          await this.stopTransactionOnConnector(connectorId, reason)
+          ++numberOfRunningTransactions
         }
       }
     }
+    return numberOfRunningTransactions
   }
 
-  // 0 for disabling
-  private getConnectionTimeout (): number {
-    if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
-      return convertToInt(
-        getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
-          Constants.DEFAULT_CONNECTION_TIMEOUT
+  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
+        }
+      }
+    }
+  }
+
+  public getReserveConnectorZeroSupported (): boolean {
+    return convertToBoolean(
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
+    )
+  }
+
+  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
+        }
+      }
+    }
+  }
+
+  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 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
+  }
+
+  public inRejectedState (): boolean {
+    return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
+  }
+
+  public inUnknownState (): boolean {
+    return this.bootNotificationResponse?.status == null
+  }
+
+  public isChargingStationAvailable (): boolean {
+    return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
+  }
+
+  public isConnectorAvailable (connectorId: number): boolean {
+    return (
+      connectorId > 0 &&
+      this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
+    )
+  }
+
+  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
       )
     }
-    return Constants.DEFAULT_CONNECTION_TIMEOUT
+    return false
   }
 
-  private getPowerDivider (): number {
-    let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
-    if (this.stationInfo?.powerSharedByConnectors === true) {
-      powerDivider = this.getNumberOfRunningTransactions()
+  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,
     }
-    return powerDivider
+    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()
+    }
+
+    if (this.isWebSocketConnectionOpened()) {
+      logger.warn(
+        `${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 getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
+  public async removeReservation (
+    reservation: Reservation,
+    reason: ReservationTerminationReason
+  ): Promise<void> {
     // 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)
+    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 }
         )
-      case CurrentType.DC:
-        return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
+        delete connector.reservation
+        break
+      default:
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        throw new BaseError(`Unknown reservation termination reason '${reason}'`)
     }
   }
 
-  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 async reset (reason?: StopTransactionReason): Promise<void> {
+    await this.stop(reason)
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    await sleep(this.stationInfo!.resetTime!)
+    this.initialize()
+    this.start()
   }
 
-  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 restartHeartbeat (): void {
+    // Stop heartbeat
+    this.stopHeartbeat()
+    // Start heartbeat
+    this.startHeartbeat()
   }
 
-  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 restartMeterValues (connectorId: number, interval: number): void {
+    this.stopMeterValues(connectorId)
+    this.startMeterValues(connectorId, interval)
   }
 
-  private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
-    if (this.stationInfo?.autoRegister === true) {
-      await this.ocppRequestService.requestHandler<
-        BootNotificationRequest,
-        BootNotificationResponse
-      >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
-        skipBufferingOnError: true,
-      })
-    }
+  public restartWebSocketPing (): void {
+    // Stop WebSocket ping
+    this.stopWebSocketPing()
     // 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
-            )
-          }
-        }
-      }
-    } 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)!
-            )
-          )
-        }
-      }
-    }
-    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
-    }
+    this.startWebSocketPing()
+  }
 
-    // Start the ATG
-    if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
-      this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
+  public saveOcppConfiguration (): void {
+    if (this.stationInfo?.ocppPersistentConfiguration === true) {
+      this.saveConfiguration()
     }
-    this.flushMessageBuffer()
   }
 
-  private internalStopMessageSequence (): void {
-    // Stop WebSocket ping
-    this.stopWebSocketPing()
-    // Stop heartbeat
-    this.stopHeartbeat()
-    // Stop the ATG
-    if (this.automaticTransactionGenerator?.started === true) {
-      this.stopAutomaticTransactionGenerator()
+  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 async stopMessageSequence (
-    reason?: StopTransactionReason,
-    stopTransactions?: boolean
-  ): Promise<void> {
-    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
-          }
+  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 {
-      for (const connectorId of this.connectors.keys()) {
-        if (connectorId > 0) {
-          await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
-          delete this.getConnectorStatus(connectorId)?.status
-        }
-      }
+      logger.warn(`${this.logPrefix()} Charging station is already started...`)
     }
   }
 
-  private getWebSocketPingInterval (): number {
-    return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
-      ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
-      : 0
+  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)
+    }
+    this.saveAutomaticTransactionGeneratorConfiguration()
+    this.emit(ChargingStationEvents.updated)
   }
 
-  private startWebSocketPing (): void {
-    const webSocketPingInterval = this.getWebSocketPingInterval()
-    if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
-      this.wsPingSetInterval = setInterval(() => {
-        if (this.isWebSocketConnectionOpened()) {
-          this.wsConnection?.ping()
-        }
-      }, secondsToMilliseconds(webSocketPingInterval))
+  public startHeartbeat (): void {
+    const heartbeatInterval = this.getHeartbeatInterval()
+    if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) {
+      this.heartbeatSetInterval = setInterval(() => {
+        this.ocppRequestService
+          .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
+          .catch((error: unknown) => {
+            logger.error(
+              `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
+              error
+            )
+          })
+      }, heartbeatInterval)
       logger.info(
-        `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
-          webSocketPingInterval
+        `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
+          heartbeatInterval
         )}`
       )
-    } else if (this.wsPingSetInterval != null) {
+    } else if (this.heartbeatSetInterval != null) {
       logger.info(
-        `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
-          webSocketPingInterval
+        `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
+          heartbeatInterval
         )}`
       )
     } else {
       logger.error(
-        `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping`
+        `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat`
       )
     }
   }
 
-  private stopWebSocketPing (): void {
-    if (this.wsPingSetInterval != null) {
-      clearInterval(this.wsPingSetInterval)
-      delete this.wsPingSetInterval
+  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<MeterValuesRequest, MeterValuesResponse>(
+            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 getConfiguredSupervisionUrl (): URL {
-    let configuredSupervisionUrl: string
-    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
+  public async stop (
+    reason?: StopTransactionReason,
+    stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+  ): Promise<void> {
+    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 {
+        logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
       }
-      configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
     } else {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      configuredSupervisionUrl = supervisionUrls!
-    }
-    if (isNotEmptyString(configuredSupervisionUrl)) {
-      return new URL(configuredSupervisionUrl)
+      logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
     }
-    const errorMsg = 'No supervision url(s) configured'
-    logger.error(`${this.logPrefix()} ${errorMsg}`)
-    throw new BaseError(errorMsg)
   }
 
-  private stopHeartbeat (): void {
-    if (this.heartbeatSetInterval != null) {
-      clearInterval(this.heartbeatSetInterval)
-      delete this.heartbeatSetInterval
+  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)
   }
 
-  private terminateWSConnection (): void {
-    if (this.isWebSocketConnectionOpened()) {
-      this.wsConnection?.terminate()
-      this.wsConnection = null
+  public stopMeterValues (connectorId: number): void {
+    const connectorStatus = this.getConnectorStatus(connectorId)
+    if (connectorStatus?.transactionSetInterval != null) {
+      clearInterval(connectorStatus.transactionSetInterval)
     }
   }
 
-  private async reconnect (): Promise<void> {
+  public async stopTransactionOnConnector (
+    connectorId: number,
+    reason?: StopTransactionReason
+  ): Promise<StopTransactionResponse> {
+    const transactionId = this.getConnectorStatus(connectorId)?.transactionId
     if (
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
-      this.stationInfo?.autoReconnectMaxRetries === -1
+      this.stationInfo?.beginEndMeterValues === true &&
+      this.stationInfo.ocppStrictCompliance === true &&
+      this.stationInfo.outOfOrderEndMeterValues === false
     ) {
-      ++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()}`
+      const transactionEndMeterValue = buildTransactionEndMeterValue(
+        this,
+        connectorId,
+        this.getEnergyActiveImportRegisterByTransactionId(transactionId)
       )
-      this.openWSConnection(
+      await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
+        this,
+        RequestCommand.METER_VALUES,
         {
-          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()})`
+          connectorId,
+          meterValue: [transactionEndMeterValue],
+          transactionId,
+        }
       )
     }
+    return await this.ocppRequestService.requestHandler<
+      Partial<StopTransactionRequest>,
+      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}`
+    )
   }
 }
index 581cd9422751c94c08d11c8ffe0cd9a2dce6e744..b8ec0afc6308343fde19cab82916ed8905eac4a7 100644 (file)
@@ -1,11 +1,11 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 import { parentPort } from 'node:worker_threads'
-
 import { ThreadWorker } from 'poolifier'
 
-import { BaseError } from '../exception/index.js'
 import type { ChargingStationInfo, ChargingStationWorkerData } from '../types/index.js'
+
+import { BaseError } from '../exception/index.js'
 import { Configuration } from '../utils/index.js'
 import { type WorkerDataError, type WorkerMessage, WorkerMessageEvents } from '../worker/index.js'
 import { ChargingStation } from './ChargingStation.js'
@@ -17,7 +17,7 @@ if (Configuration.workerPoolInUse()) {
     ChargingStationInfo | undefined
   >((data?: ChargingStationWorkerData): ChargingStationInfo | undefined => {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const { index, templateFile, options } = data!
+    const { index, options, templateFile } = data!
     return new ChargingStation(index, templateFile, options).stationInfo
   })
 } else {
@@ -25,7 +25,7 @@ if (Configuration.workerPoolInUse()) {
   class ChargingStationWorker<Data extends ChargingStationWorkerData> {
     constructor () {
       parentPort?.on('message', (message: WorkerMessage<Data>) => {
-        const { uuid, event, data } = message
+        const { data, event, uuid } = message
         // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
         if (uuid != null) {
           switch (event) {
@@ -37,21 +37,21 @@ if (Configuration.workerPoolInUse()) {
                   data.options
                 )
                 parentPort?.postMessage({
-                  uuid,
-                  event: WorkerMessageEvents.addedWorkerElement,
                   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                   data: chargingStation.stationInfo!,
+                  event: WorkerMessageEvents.addedWorkerElement,
+                  uuid,
                 } satisfies WorkerMessage<ChargingStationInfo>)
               } catch (error) {
                 parentPort?.postMessage({
-                  uuid,
-                  event: WorkerMessageEvents.workerElementError,
                   data: {
                     event,
-                    name: (error as Error).name,
                     message: (error as Error).message,
+                    name: (error as Error).name,
                     stack: (error as Error).stack,
                   },
+                  event: WorkerMessageEvents.workerElementError,
+                  uuid,
                 } satisfies WorkerMessage<WorkerDataError>)
               }
               break
index 072ec9a93d99e6a7b5e7738c914b8867057cfd99..b182cc4f1678888e0a812e118ab3a7d0ee29c86d 100644 (file)
@@ -1,15 +1,16 @@
 import type { ConfigurationKey, ConfigurationKeyType } from '../types/index.js'
-import { logger } from '../utils/index.js'
 import type { ChargingStation } from './ChargingStation.js'
 
+import { logger } from '../utils/index.js'
+
 interface ConfigurationKeyOptions {
   readonly?: boolean
-  visible?: boolean
   reboot?: boolean
+  visible?: boolean
 }
 interface DeleteConfigurationKeyParams {
-  save?: boolean
   caseInsensitive?: boolean
+  save?: boolean
 }
 interface AddConfigurationKeyParams {
   overwrite?: boolean
@@ -39,8 +40,8 @@ export const addConfigurationKey = (
   options = {
     ...{
       readonly: false,
-      visible: true,
       reboot: false,
+      visible: true,
     },
     ...options,
   }
@@ -57,9 +58,9 @@ export const addConfigurationKey = (
       key,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       readonly: options.readonly!,
+      reboot: options.reboot,
       value,
       visible: options.visible,
-      reboot: options.reboot,
     })
     params.save && chargingStation.saveOcppConfiguration()
   } else {
@@ -98,7 +99,7 @@ export const deleteConfigurationKey = (
   key: ConfigurationKeyType,
   params?: DeleteConfigurationKeyParams
 ): ConfigurationKey[] | undefined => {
-  params = { ...{ save: true, caseInsensitive: false }, ...params }
+  params = { ...{ caseInsensitive: false, save: true }, ...params }
   const keyFound = getConfigurationKey(chargingStation, key, params.caseInsensitive)
   if (keyFound != null) {
     const deletedConfigurationKey = chargingStation.ocppConfiguration?.configurationKey?.splice(
index 27d397a37dfdc1594d1c2087d86b5a15461841f9..240a54a4f6ff1046d9524eaf4797d202d2ec9f0d 100644 (file)
@@ -1,8 +1,4 @@
-import { createHash, randomBytes } from 'node:crypto'
 import type { EventEmitter } from 'node:events'
-import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
-import { env } from 'node:process'
-import { fileURLToPath } from 'node:url'
 
 import chalk from 'chalk'
 import {
@@ -21,8 +17,14 @@ import {
   toDate,
 } from 'date-fns'
 import { maxTime } from 'date-fns/constants'
+import { createHash, randomBytes } from 'node:crypto'
+import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path'
+import { env } from 'node:process'
+import { fileURLToPath } from 'node:url'
 import { isEmpty } from 'rambda'
 
+import type { ChargingStation } from './ChargingStation.js'
+
 import { BaseError } from '../exception/index.js'
 import {
   AmpereUnits,
@@ -68,7 +70,6 @@ import {
   logger,
   secureRandom,
 } from '../utils/index.js'
-import type { ChargingStation } from './ChargingStation.js'
 import { getConfigurationKey } from './ConfigurationKeyUtils.js'
 
 const moduleName = 'Helpers'
@@ -350,8 +351,8 @@ export const checkConnectorsConfiguration = (
   templateFile: string
 ): {
   configuredMaxConnectors: number
-  templateMaxConnectors: number
   templateMaxAvailableConnectors: number
+  templateMaxConnectors: number
 } => {
   const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
   checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
@@ -370,8 +371,8 @@ export const checkConnectorsConfiguration = (
   }
   return {
     configuredMaxConnectors,
-    templateMaxConnectors,
     templateMaxAvailableConnectors,
+    templateMaxConnectors,
   }
 }
 
@@ -553,7 +554,6 @@ export const createBootNotificationRequest = (
     case OCPPVersion.VERSION_20:
     case OCPPVersion.VERSION_201:
       return {
-        reason: bootReason,
         chargingStation: {
           model: stationInfo.chargePointModel,
           vendorName: stationInfo.chargePointVendor,
@@ -570,6 +570,7 @@ export const createBootNotificationRequest = (
             },
           }),
         },
+        reason: bootReason,
       } satisfies OCPP20BootNotificationRequest
   }
 }
@@ -618,12 +619,12 @@ export const createSerialNumber = (
   stationTemplate: ChargingStationTemplate,
   stationInfo: ChargingStationInfo,
   params?: {
-    randomSerialNumberUpperCase?: boolean
     randomSerialNumber?: boolean
+    randomSerialNumberUpperCase?: boolean
   }
 ): void => {
   params = {
-    ...{ randomSerialNumberUpperCase: true, randomSerialNumber: true },
+    ...{ randomSerialNumber: true, randomSerialNumberUpperCase: true },
     ...params,
   }
   const serialNumberSuffix = params.randomSerialNumber
@@ -795,7 +796,7 @@ const buildChargingProfilesLimit = (
 ): number => {
   // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
   const errorMsg = `Unknown ${chargingStation.stationInfo?.currentOutType} currentOutType in charging station information, cannot build charging profiles limit`
-  const { limit, chargingProfile } = chargingProfilesLimit
+  const { chargingProfile, limit } = chargingProfilesLimit
   switch (chargingStation.stationInfo?.currentOutType) {
     case CurrentType.AC:
       return chargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
@@ -966,8 +967,8 @@ const convertDeprecatedTemplateKey = (
 }
 
 interface ChargingProfilesLimit {
-  limit: number
   chargingProfile: ChargingProfile
+  limit: number
 }
 
 /**
@@ -1026,8 +1027,8 @@ const getChargingProfilesLimit = (
     // Check if the charging profile is active
     if (
       isWithinInterval(currentDate, {
-        start: chargingSchedule.startSchedule,
         end: addSeconds(chargingSchedule.startSchedule, chargingSchedule.duration),
+        start: chargingSchedule.startSchedule,
       })
     ) {
       if (isNotEmptyArray(chargingSchedule.chargingSchedulePeriod)) {
@@ -1056,8 +1057,8 @@ const getChargingProfilesLimit = (
         // Handle only one schedule period
         if (chargingSchedule.chargingSchedulePeriod.length === 1) {
           const chargingProfilesLimit: ChargingProfilesLimit = {
-            limit: chargingSchedule.chargingSchedulePeriod[0].limit,
             chargingProfile,
+            limit: chargingSchedule.chargingSchedulePeriod[0].limit,
           }
           logger.debug(debugLogMsg, chargingProfilesLimit)
           return chargingProfilesLimit
@@ -1077,8 +1078,8 @@ const getChargingProfilesLimit = (
           ) {
             // Found the schedule period: previous is the correct one
             const chargingProfilesLimit: ChargingProfilesLimit = {
-              limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
               chargingProfile: previousActiveChargingProfile ?? chargingProfile,
+              limit: previousChargingSchedulePeriod?.limit ?? chargingSchedulePeriod.limit,
             }
             logger.debug(debugLogMsg, chargingProfilesLimit)
             return chargingProfilesLimit
@@ -1096,8 +1097,8 @@ const getChargingProfilesLimit = (
               ) > chargingSchedule.duration)
           ) {
             const chargingProfilesLimit: ChargingProfilesLimit = {
-              limit: chargingSchedulePeriod.limit,
               chargingProfile,
+              limit: chargingSchedulePeriod.limit,
             }
             logger.debug(debugLogMsg, chargingProfilesLimit)
             return chargingProfilesLimit
@@ -1115,7 +1116,7 @@ const getChargingProfilesLimit = (
 export const prepareChargingProfileKind = (
   connectorStatus: ConnectorStatus | undefined,
   chargingProfile: ChargingProfile,
-  currentDate: string | number | Date,
+  currentDate: Date | number | string,
   logPrefix: string
 ): boolean => {
   switch (chargingProfile.chargingProfileKind) {
@@ -1143,7 +1144,7 @@ export const prepareChargingProfileKind = (
 
 export const canProceedChargingProfile = (
   chargingProfile: ChargingProfile,
-  currentDate: string | number | Date,
+  currentDate: Date | number | string,
   logPrefix: string
 ): boolean => {
   if (
@@ -1215,7 +1216,7 @@ const canProceedRecurringChargingProfile = (
  */
 const prepareRecurringChargingProfile = (
   chargingProfile: ChargingProfile,
-  currentDate: string | number | Date,
+  currentDate: Date | number | string,
   logPrefix: string
 ): boolean => {
   const chargingSchedule = chargingProfile.chargingSchedule
@@ -1224,10 +1225,10 @@ const prepareRecurringChargingProfile = (
   switch (chargingProfile.recurrencyKind) {
     case RecurrencyKindType.DAILY:
       recurringInterval = {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        start: chargingSchedule.startSchedule!,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         end: addDays(chargingSchedule.startSchedule!, 1),
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        start: chargingSchedule.startSchedule!,
       }
       checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
       if (
@@ -1239,18 +1240,18 @@ const prepareRecurringChargingProfile = (
           differenceInDays(currentDate, recurringInterval.start)
         )
         recurringInterval = {
-          start: chargingSchedule.startSchedule,
           end: addDays(chargingSchedule.startSchedule, 1),
+          start: chargingSchedule.startSchedule,
         }
         recurringIntervalTranslated = true
       }
       break
     case RecurrencyKindType.WEEKLY:
       recurringInterval = {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        start: chargingSchedule.startSchedule!,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         end: addWeeks(chargingSchedule.startSchedule!, 1),
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        start: chargingSchedule.startSchedule!,
       }
       checkRecurringChargingProfileDuration(chargingProfile, recurringInterval, logPrefix)
       if (
@@ -1262,8 +1263,8 @@ const prepareRecurringChargingProfile = (
           differenceInWeeks(currentDate, recurringInterval.start)
         )
         recurringInterval = {
-          start: chargingSchedule.startSchedule,
           end: addWeeks(chargingSchedule.startSchedule, 1),
+          start: chargingSchedule.startSchedule,
         }
         recurringIntervalTranslated = true
       }
index 71e038be5a2868267b40edabb2847352240eecbc..b1993ab84d51fd9701241bbee552d2b7dc309be9 100644 (file)
@@ -1,5 +1,7 @@
 import { type FSWatcher, readFileSync } from 'node:fs'
 
+import type { ChargingStation } from './ChargingStation.js'
+
 import { FileType, IdTagDistribution } from '../types/index.js'
 import {
   handleFileException,
@@ -9,7 +11,6 @@ import {
   secureRandom,
   watchJsonFile,
 } from '../utils/index.js'
-import type { ChargingStation } from './ChargingStation.js'
 import { getIdTagsFile } from './Helpers.js'
 
 interface IdTagsCacheValueType {
@@ -22,6 +23,10 @@ export class IdTagsCache {
   private readonly idTagsCaches: Map<string, IdTagsCacheValueType>
   private readonly idTagsCachesAddressableIndexes: Map<string, number>
 
+  private readonly logPrefix = (file: string): string => {
+    return logPrefix(` Id tags cache for id tags file '${file}' |`)
+  }
+
   private constructor () {
     this.idTagsCaches = new Map<string, IdTagsCacheValueType>()
     this.idTagsCachesAddressableIndexes = new Map<string, number>()
@@ -34,50 +39,61 @@ export class IdTagsCache {
     return IdTagsCache.instance
   }
 
-  /**
-   * 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 {
+  private deleteIdTagsCache (file: string): boolean {
+    this.idTagsCaches.get(file)?.idTagsFileWatcher?.close()
+    return this.idTagsCaches.delete(file)
+  }
+
+  private deleteIdTagsCacheIndexes (file: string): boolean {
+    const deleted: boolean[] = []
+    for (const [key] of this.idTagsCachesAddressableIndexes) {
+      if (key.startsWith(file)) {
+        deleted.push(this.idTagsCachesAddressableIndexes.delete(key))
+      }
+    }
+    return !deleted.some(value => !value)
+  }
+
+  private getConnectorAffinityIdTag (chargingStation: ChargingStation, connectorId: number): string {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const hashId = chargingStation.stationInfo!.hashId
+    const file = getIdTagsFile(chargingStation.stationInfo!)!
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const idTagsFile = getIdTagsFile(chargingStation.stationInfo!)!
-    switch (distribution) {
-      case IdTagDistribution.RANDOM:
-        return this.getRandomIdTag(hashId, idTagsFile)
-      case IdTagDistribution.ROUND_ROBIN:
-        return this.getRoundRobinIdTag(hashId, idTagsFile)
-      case IdTagDistribution.CONNECTOR_AFFINITY:
-        return this.getConnectorAffinityIdTag(chargingStation, connectorId)
-      default:
-        return this.getRoundRobinIdTag(hashId, idTagsFile)
-    }
+    const idTags = this.getIdTags(file)!
+    const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
+      file,
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      chargingStation.stationInfo!.hashId
+    )
+    this.idTagsCachesAddressableIndexes.set(
+      addressableKey,
+      (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
+    )
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
   }
 
-  /**
-   * 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 getIdTagsCache (file: string): string[] | undefined {
+    return this.idTagsCaches.get(file)?.idTags
   }
 
-  public deleteIdTags (file: string): boolean {
-    return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file)
+  private getIdTagsCacheIndexesAddressableKey (prefix: string, uid: string): string {
+    return `${prefix}${uid}`
+  }
+
+  private getIdTagsFromFile (file: string): string[] {
+    if (isNotEmptyString(file)) {
+      try {
+        return JSON.parse(readFileSync(file, 'utf8')) as string[]
+      } catch (error) {
+        handleFileException(
+          file,
+          FileType.Authorization,
+          error as NodeJS.ErrnoException,
+          this.logPrefix(file)
+        )
+      }
+    }
+    return []
   }
 
   private getRandomIdTag (hashId: string, file: string): string {
@@ -105,24 +121,6 @@ export class IdTagsCache {
     return idTag
   }
 
-  private getConnectorAffinityIdTag (chargingStation: ChargingStation, connectorId: number): string {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const file = getIdTagsFile(chargingStation.stationInfo!)!
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const idTags = this.getIdTags(file)!
-    const addressableKey = this.getIdTagsCacheIndexesAddressableKey(
-      file,
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      chargingStation.stationInfo!.hashId
-    )
-    this.idTagsCachesAddressableIndexes.set(
-      addressableKey,
-      (chargingStation.index - 1 + (connectorId - 1)) % idTags.length
-    )
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return idTags[this.idTagsCachesAddressableIndexes.get(addressableKey)!]
-  }
-
   private hasIdTagsCache (file: string): boolean {
     return this.idTagsCaches.has(file)
   }
@@ -159,46 +157,49 @@ export class IdTagsCache {
     })
   }
 
-  private getIdTagsCache (file: string): string[] | undefined {
-    return this.idTagsCaches.get(file)?.idTags
-  }
-
-  private deleteIdTagsCache (file: string): boolean {
-    this.idTagsCaches.get(file)?.idTagsFileWatcher?.close()
-    return this.idTagsCaches.delete(file)
+  public deleteIdTags (file: string): boolean {
+    return this.deleteIdTagsCache(file) && this.deleteIdTagsCacheIndexes(file)
   }
 
-  private deleteIdTagsCacheIndexes (file: string): boolean {
-    const deleted: boolean[] = []
-    for (const [key] of this.idTagsCachesAddressableIndexes) {
-      if (key.startsWith(file)) {
-        deleted.push(this.idTagsCachesAddressableIndexes.delete(key))
-      }
+  /**
+   * 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)
     }
-    return !deleted.some(value => !value)
   }
 
-  private getIdTagsCacheIndexesAddressableKey (prefix: string, uid: string): string {
-    return `${prefix}${uid}`
-  }
-
-  private getIdTagsFromFile (file: string): string[] {
-    if (isNotEmptyString(file)) {
-      try {
-        return JSON.parse(readFileSync(file, 'utf8')) as string[]
-      } catch (error) {
-        handleFileException(
-          file,
-          FileType.Authorization,
-          error as NodeJS.ErrnoException,
-          this.logPrefix(file)
-        )
-      }
+  /**
+   * 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 []
-  }
-
-  private readonly logPrefix = (file: string): string => {
-    return logPrefix(` Id tags cache for id tags file '${file}' |`)
+    return this.getIdTagsCache(file)
   }
 }
index 0da4558d4d46f4c4bd3d98f92eb2a7677856517d..55cd6c2368bce90a31082b2de26c12bb32ec2a5a 100644 (file)
@@ -2,18 +2,19 @@ import { LRUMapWithDelete as LRUCache } from 'mnemonist'
 import { isEmpty } from 'rambda'
 
 import type { ChargingStationConfiguration, ChargingStationTemplate } from '../types/index.js'
+
 import { isNotEmptyArray, isNotEmptyString } from '../utils/index.js'
 import { Bootstrap } from './Bootstrap.js'
 
 enum CacheType {
-  chargingStationTemplate = 'chargingStationTemplate',
-  chargingStationConfiguration = 'chargingStationConfiguration'
+  chargingStationConfiguration = 'chargingStationConfiguration',
+  chargingStationTemplate = 'chargingStationTemplate'
 }
 
-type CacheValueType = ChargingStationTemplate | ChargingStationConfiguration
+type CacheValueType = ChargingStationConfiguration | ChargingStationTemplate
 
 export class SharedLRUCache {
-  private static instance: SharedLRUCache | null = null
+  private static instance: null | SharedLRUCache = null
   private readonly lruCache: LRUCache<string, CacheValueType>
 
   private constructor () {
@@ -31,96 +32,96 @@ export class SharedLRUCache {
     return SharedLRUCache.instance
   }
 
-  public hasChargingStationConfiguration (chargingStationConfigurationHash: string): boolean {
-    return this.has(this.getChargingStationConfigurationKey(chargingStationConfigurationHash))
+  private delete (key: string): void {
+    this.lruCache.delete(key)
   }
 
-  public setChargingStationConfiguration (
-    chargingStationConfiguration: ChargingStationConfiguration
-  ): void {
-    if (this.isChargingStationConfigurationCacheable(chargingStationConfiguration)) {
-      this.set(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.getChargingStationConfigurationKey(chargingStationConfiguration.configurationHash!),
-        chargingStationConfiguration
-      )
-    }
+  private get (key: string): CacheValueType | undefined {
+    return this.lruCache.get(key)
   }
 
-  public getChargingStationConfiguration (
-    chargingStationConfigurationHash: string
-  ): ChargingStationConfiguration {
-    return this.get(
-      this.getChargingStationConfigurationKey(chargingStationConfigurationHash)
-    ) as ChargingStationConfiguration
+  private getChargingStationConfigurationKey (hash: string): string {
+    return `${CacheType.chargingStationConfiguration}${hash}`
   }
 
-  public deleteChargingStationConfiguration (chargingStationConfigurationHash: string): void {
-    this.delete(this.getChargingStationConfigurationKey(chargingStationConfigurationHash))
+  private getChargingStationTemplateKey (hash: string): string {
+    return `${CacheType.chargingStationTemplate}${hash}`
   }
 
-  public hasChargingStationTemplate (chargingStationTemplateHash: string): boolean {
-    return this.has(this.getChargingStationTemplateKey(chargingStationTemplateHash))
+  private has (key: string): boolean {
+    return this.lruCache.has(key)
   }
 
-  public setChargingStationTemplate (chargingStationTemplate: ChargingStationTemplate): void {
-    this.set(
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.getChargingStationTemplateKey(chargingStationTemplate.templateHash!),
-      chargingStationTemplate
+  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)
     )
   }
 
-  public getChargingStationTemplate (chargingStationTemplateHash: string): ChargingStationTemplate {
-    return this.get(
-      this.getChargingStationTemplateKey(chargingStationTemplateHash)
-    ) as ChargingStationTemplate
-  }
-
-  public deleteChargingStationTemplate (chargingStationTemplateHash: string): void {
-    this.delete(this.getChargingStationTemplateKey(chargingStationTemplateHash))
+  private set (key: string, value: CacheValueType): void {
+    this.lruCache.set(key, value)
   }
 
   public clear (): void {
     this.lruCache.clear()
   }
 
-  private getChargingStationConfigurationKey (hash: string): string {
-    return `${CacheType.chargingStationConfiguration}${hash}`
+  public deleteChargingStationConfiguration (chargingStationConfigurationHash: string): void {
+    this.delete(this.getChargingStationConfigurationKey(chargingStationConfigurationHash))
   }
 
-  private getChargingStationTemplateKey (hash: string): string {
-    return `${CacheType.chargingStationTemplate}${hash}`
+  public deleteChargingStationTemplate (chargingStationTemplateHash: string): void {
+    this.delete(this.getChargingStationTemplateKey(chargingStationTemplateHash))
   }
 
-  private has (key: string): boolean {
-    return this.lruCache.has(key)
+  public getChargingStationConfiguration (
+    chargingStationConfigurationHash: string
+  ): ChargingStationConfiguration {
+    return this.get(
+      this.getChargingStationConfigurationKey(chargingStationConfigurationHash)
+    ) as ChargingStationConfiguration
   }
 
-  private get (key: string): CacheValueType | undefined {
-    return this.lruCache.get(key)
+  public getChargingStationTemplate (chargingStationTemplateHash: string): ChargingStationTemplate {
+    return this.get(
+      this.getChargingStationTemplateKey(chargingStationTemplateHash)
+    ) as ChargingStationTemplate
   }
 
-  private set (key: string, value: CacheValueType): void {
-    this.lruCache.set(key, value)
+  public hasChargingStationConfiguration (chargingStationConfigurationHash: string): boolean {
+    return this.has(this.getChargingStationConfigurationKey(chargingStationConfigurationHash))
   }
 
-  private delete (key: string): void {
-    this.lruCache.delete(key)
+  public hasChargingStationTemplate (chargingStationTemplateHash: string): boolean {
+    return this.has(this.getChargingStationTemplateKey(chargingStationTemplateHash))
   }
 
-  private isChargingStationConfigurationCacheable (
+  public setChargingStationConfiguration (
     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)
+  ): void {
+    if (this.isChargingStationConfigurationCacheable(chargingStationConfiguration)) {
+      this.set(
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        this.getChargingStationConfigurationKey(chargingStationConfiguration.configurationHash!),
+        chargingStationConfiguration
+      )
+    }
+  }
+
+  public setChargingStationTemplate (chargingStationTemplate: ChargingStationTemplate): void {
+    this.set(
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      this.getChargingStationTemplateKey(chargingStationTemplate.templateHash!),
+      chargingStationTemplate
     )
   }
 }
index 60e6d9efd7cfc0ea912b46d336e1cd5ac34a740b..e70e128db294374d50f8dde1cc89372f6e2b3a59 100644 (file)
@@ -1,6 +1,8 @@
 import { secondsToMilliseconds } from 'date-fns'
 import { isEmpty } from 'rambda'
 
+import type { ChargingStation } from '../ChargingStation.js'
+
 import { BaseError, type OCPPError } from '../../exception/index.js'
 import {
   AuthorizationStatus,
@@ -38,7 +40,6 @@ import {
   type StopTransactionResponse,
 } from '../../types/index.js'
 import { Constants, convertToInt, isAsyncFunction, logger } from '../../utils/index.js'
-import type { ChargingStation } from '../ChargingStation.js'
 import { getConfigurationKey } from '../ConfigurationKeyUtils.js'
 import { buildMeterValue } from '../ocpp/index.js'
 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
@@ -46,22 +47,22 @@ import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
 const moduleName = 'ChargingStationWorkerBroadcastChannel'
 
 type CommandResponse =
-  | EmptyObject
-  | StartTransactionResponse
-  | StopTransactionResponse
   | AuthorizeResponse
   | BootNotificationResponse
-  | HeartbeatResponse
   | DataTransferResponse
+  | EmptyObject
+  | HeartbeatResponse
+  | StartTransactionResponse
+  | StopTransactionResponse
 
 type CommandHandler = (
   requestPayload?: BroadcastChannelRequestPayload
   // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
-) => Promise<CommandResponse | void> | CommandResponse | void
+) => CommandResponse | Promise<CommandResponse | void> | void
 
 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
-  private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
   private readonly chargingStation: ChargingStation
+  private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
 
   constructor (chargingStation: ChargingStation) {
     super()
@@ -69,86 +70,6 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
       throwError: true,
     }
     this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
-      [
-        BroadcastChannelProcedureName.START_CHARGING_STATION,
-        () => {
-          this.chargingStation.start()
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.STOP_CHARGING_STATION,
-        async () => {
-          await this.chargingStation.stop()
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
-        async (requestPayload?: BroadcastChannelRequestPayload) => {
-          await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.OPEN_CONNECTION,
-        () => {
-          this.chargingStation.openWSConnection()
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.CLOSE_CONNECTION,
-        () => {
-          this.chargingStation.closeWSConnection()
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
-        (requestPayload?: BroadcastChannelRequestPayload) => {
-          this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
-        (requestPayload?: BroadcastChannelRequestPayload) => {
-          this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.SET_SUPERVISION_URL,
-        (requestPayload?: BroadcastChannelRequestPayload) => {
-          this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
-        },
-      ],
-      [
-        BroadcastChannelProcedureName.START_TRANSACTION,
-        async (requestPayload?: BroadcastChannelRequestPayload) =>
-          await this.chargingStation.ocppRequestService.requestHandler<
-            StartTransactionRequest,
-            StartTransactionResponse
-          >(
-            this.chargingStation,
-            RequestCommand.START_TRANSACTION,
-            requestPayload as StartTransactionRequest,
-            requestParams
-          ),
-      ],
-      [
-        BroadcastChannelProcedureName.STOP_TRANSACTION,
-        async (requestPayload?: BroadcastChannelRequestPayload) =>
-          await this.chargingStation.ocppRequestService.requestHandler<
-            StopTransactionRequest,
-            StartTransactionResponse
-          >(
-            this.chargingStation,
-            RequestCommand.STOP_TRANSACTION,
-            {
-              meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
-                requestPayload?.transactionId,
-                true
-              ),
-              ...requestPayload,
-            } as StopTransactionRequest,
-            requestParams
-          ),
-      ],
       [
         BroadcastChannelProcedureName.AUTHORIZE,
         async (requestPayload?: BroadcastChannelRequestPayload) =>
@@ -183,15 +104,53 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
         },
       ],
       [
-        BroadcastChannelProcedureName.STATUS_NOTIFICATION,
+        BroadcastChannelProcedureName.CLOSE_CONNECTION,
+        () => {
+          this.chargingStation.closeWSConnection()
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.DATA_TRANSFER,
         async (requestPayload?: BroadcastChannelRequestPayload) =>
           await this.chargingStation.ocppRequestService.requestHandler<
-            StatusNotificationRequest,
-            StatusNotificationResponse
+            DataTransferRequest,
+            DataTransferResponse
           >(
             this.chargingStation,
-            RequestCommand.STATUS_NOTIFICATION,
-            requestPayload as StatusNotificationRequest,
+            RequestCommand.DATA_TRANSFER,
+            requestPayload as DataTransferRequest,
+            requestParams
+          ),
+      ],
+      [
+        BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
+        async (requestPayload?: BroadcastChannelRequestPayload) => {
+          await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+        async (requestPayload?: BroadcastChannelRequestPayload) =>
+          await this.chargingStation.ocppRequestService.requestHandler<
+            DiagnosticsStatusNotificationRequest,
+            DiagnosticsStatusNotificationResponse
+          >(
+            this.chargingStation,
+            RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+            requestPayload as DiagnosticsStatusNotificationRequest,
+            requestParams
+          ),
+      ],
+      [
+        BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+        async (requestPayload?: BroadcastChannelRequestPayload) =>
+          await this.chargingStation.ocppRequestService.requestHandler<
+            FirmwareStatusNotificationRequest,
+            FirmwareStatusNotificationResponse
+          >(
+            this.chargingStation,
+            RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+            requestPayload as FirmwareStatusNotificationRequest,
             requestParams
           ),
       ],
@@ -242,41 +201,83 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
         },
       ],
       [
-        BroadcastChannelProcedureName.DATA_TRANSFER,
+        BroadcastChannelProcedureName.OPEN_CONNECTION,
+        () => {
+          this.chargingStation.openWSConnection()
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.SET_SUPERVISION_URL,
+        (requestPayload?: BroadcastChannelRequestPayload) => {
+          this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+        (requestPayload?: BroadcastChannelRequestPayload) => {
+          this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.START_CHARGING_STATION,
+        () => {
+          this.chargingStation.start()
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.START_TRANSACTION,
         async (requestPayload?: BroadcastChannelRequestPayload) =>
           await this.chargingStation.ocppRequestService.requestHandler<
-            DataTransferRequest,
-            DataTransferResponse
+            StartTransactionRequest,
+            StartTransactionResponse
           >(
             this.chargingStation,
-            RequestCommand.DATA_TRANSFER,
-            requestPayload as DataTransferRequest,
+            RequestCommand.START_TRANSACTION,
+            requestPayload as StartTransactionRequest,
             requestParams
           ),
       ],
       [
-        BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+        BroadcastChannelProcedureName.STATUS_NOTIFICATION,
         async (requestPayload?: BroadcastChannelRequestPayload) =>
           await this.chargingStation.ocppRequestService.requestHandler<
-            DiagnosticsStatusNotificationRequest,
-            DiagnosticsStatusNotificationResponse
+            StatusNotificationRequest,
+            StatusNotificationResponse
           >(
             this.chargingStation,
-            RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
-            requestPayload as DiagnosticsStatusNotificationRequest,
+            RequestCommand.STATUS_NOTIFICATION,
+            requestPayload as StatusNotificationRequest,
             requestParams
           ),
       ],
       [
-        BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+        BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+        (requestPayload?: BroadcastChannelRequestPayload) => {
+          this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.STOP_CHARGING_STATION,
+        async () => {
+          await this.chargingStation.stop()
+        },
+      ],
+      [
+        BroadcastChannelProcedureName.STOP_TRANSACTION,
         async (requestPayload?: BroadcastChannelRequestPayload) =>
           await this.chargingStation.ocppRequestService.requestHandler<
-            FirmwareStatusNotificationRequest,
-            FirmwareStatusNotificationResponse
+            StopTransactionRequest,
+            StartTransactionResponse
           >(
             this.chargingStation,
-            RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
-            requestPayload as FirmwareStatusNotificationRequest,
+            RequestCommand.STOP_TRANSACTION,
+            {
+              meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
+                requestPayload?.transactionId,
+                true
+              ),
+              ...requestPayload,
+            } as StopTransactionRequest,
             requestParams
           ),
       ],
@@ -286,71 +287,16 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
     this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void
   }
 
-  private requestHandler (messageEvent: MessageEvent): void {
-    const validatedMessageEvent = this.validateMessageEvent(messageEvent)
-    if (validatedMessageEvent === false) {
-      return
-    }
-    if (this.isResponse(validatedMessageEvent.data)) {
-      return
-    }
-    const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
-    if (
-      requestPayload.hashIds != null &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId)
-    ) {
-      return
-    }
-    if (requestPayload.hashId != null) {
-      logger.error(
-        `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
-      )
-      return
-    }
-    let responsePayload: BroadcastChannelResponsePayload | undefined
-    this.commandHandler(command, requestPayload)
-      .then(commandResponse => {
-        if (commandResponse == null || isEmpty(commandResponse)) {
-          responsePayload = {
-            hashId: this.chargingStation.stationInfo?.hashId,
-            status: ResponseStatus.SUCCESS,
-          }
-        } else {
-          responsePayload = this.commandResponseToResponsePayload(
-            command,
-            requestPayload,
-            commandResponse
-          )
-        }
-        return undefined
-      })
-      .finally(() => {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.sendResponse([uuid, responsePayload!])
-      })
-      .catch((error: unknown) => {
-        logger.error(
-          `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
-          error
-        )
-        responsePayload = {
-          hashId: this.chargingStation.stationInfo?.hashId,
-          status: ResponseStatus.FAILURE,
-          command,
-          requestPayload,
-          errorMessage: (error as OCPPError).message,
-          errorStack: (error as OCPPError).stack,
-          errorDetails: (error as OCPPError).details,
-        } satisfies BroadcastChannelResponsePayload
-      })
-  }
-
-  private messageErrorHandler (messageEvent: MessageEvent): void {
-    logger.error(
-      `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
-      messageEvent
-    )
+  private cleanRequestPayload (
+    command: BroadcastChannelProcedureName,
+    requestPayload: BroadcastChannelRequestPayload
+  ): void {
+    delete requestPayload.hashId
+    delete requestPayload.hashIds
+    ![
+      BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+      BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+    ].includes(command) && delete requestPayload.connectorIds
   }
 
   private async commandHandler (
@@ -375,18 +321,6 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
     throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
   }
 
-  private cleanRequestPayload (
-    command: BroadcastChannelProcedureName,
-    requestPayload: BroadcastChannelRequestPayload
-  ): void {
-    delete requestPayload.hashId
-    delete requestPayload.hashIds
-    ![
-      BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
-      BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
-    ].includes(command) && delete requestPayload.connectorIds
-  }
-
   private commandResponseToResponsePayload (
     command: BroadcastChannelProcedureName,
     requestPayload: BroadcastChannelRequestPayload,
@@ -400,11 +334,11 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
       }
     }
     return {
-      hashId: this.chargingStation.stationInfo?.hashId,
-      status: responseStatus,
       command,
-      requestPayload,
       commandResponse,
+      hashId: this.chargingStation.stationInfo?.hashId,
+      requestPayload,
+      status: responseStatus,
     }
   }
 
@@ -413,15 +347,15 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
     commandResponse: CommandResponse
   ): ResponseStatus {
     switch (command) {
+      case BroadcastChannelProcedureName.AUTHORIZE:
       case BroadcastChannelProcedureName.START_TRANSACTION:
       case BroadcastChannelProcedureName.STOP_TRANSACTION:
-      case BroadcastChannelProcedureName.AUTHORIZE:
         if (
           (
             commandResponse as
+              | AuthorizeResponse
               | StartTransactionResponse
               | StopTransactionResponse
-              | AuthorizeResponse
           ).idTagInfo?.status === AuthorizationStatus.ACCEPTED
         ) {
           return ResponseStatus.SUCCESS
@@ -437,14 +371,14 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
           return ResponseStatus.SUCCESS
         }
         return ResponseStatus.FAILURE
-      case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
-      case BroadcastChannelProcedureName.METER_VALUES:
-        if (isEmpty(commandResponse)) {
+      case BroadcastChannelProcedureName.HEARTBEAT:
+        if ('currentTime' in commandResponse) {
           return ResponseStatus.SUCCESS
         }
         return ResponseStatus.FAILURE
-      case BroadcastChannelProcedureName.HEARTBEAT:
-        if ('currentTime' in commandResponse) {
+      case BroadcastChannelProcedureName.METER_VALUES:
+      case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
+        if (isEmpty(commandResponse)) {
           return ResponseStatus.SUCCESS
         }
         return ResponseStatus.FAILURE
@@ -452,4 +386,71 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
         return ResponseStatus.FAILURE
     }
   }
+
+  private messageErrorHandler (messageEvent: MessageEvent): void {
+    logger.error(
+      `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
+      messageEvent
+    )
+  }
+
+  private requestHandler (messageEvent: MessageEvent): void {
+    const validatedMessageEvent = this.validateMessageEvent(messageEvent)
+    if (validatedMessageEvent === false) {
+      return
+    }
+    if (this.isResponse(validatedMessageEvent.data)) {
+      return
+    }
+    const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
+    if (
+      requestPayload.hashIds != null &&
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      !requestPayload.hashIds.includes(this.chargingStation.stationInfo!.hashId)
+    ) {
+      return
+    }
+    if (requestPayload.hashId != null) {
+      logger.error(
+        `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
+      )
+      return
+    }
+    let responsePayload: BroadcastChannelResponsePayload | undefined
+    this.commandHandler(command, requestPayload)
+      .then(commandResponse => {
+        if (commandResponse == null || isEmpty(commandResponse)) {
+          responsePayload = {
+            hashId: this.chargingStation.stationInfo?.hashId,
+            status: ResponseStatus.SUCCESS,
+          }
+        } else {
+          responsePayload = this.commandResponseToResponsePayload(
+            command,
+            requestPayload,
+            commandResponse
+          )
+        }
+        return undefined
+      })
+      .finally(() => {
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        this.sendResponse([uuid, responsePayload!])
+      })
+      .catch((error: unknown) => {
+        logger.error(
+          `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
+          error
+        )
+        responsePayload = {
+          command,
+          errorDetails: (error as OCPPError).details,
+          errorMessage: (error as OCPPError).message,
+          errorStack: (error as OCPPError).stack,
+          hashId: this.chargingStation.stationInfo?.hashId,
+          requestPayload,
+          status: ResponseStatus.FAILURE,
+        } satisfies BroadcastChannelResponsePayload
+      })
+  }
 }
index 958c9375c1401bd3ea83adcfe1649a60e15156ae..48bf16915d39f17cfddc0ac39b6a4f03941326c8 100644 (file)
@@ -1,3 +1,5 @@
+import type { AbstractUIService } from '../ui-server/ui-services/AbstractUIService.js'
+
 import {
   type BroadcastChannelResponse,
   type BroadcastChannelResponsePayload,
@@ -6,20 +8,19 @@ import {
   ResponseStatus,
 } from '../../types/index.js'
 import { logger } from '../../utils/index.js'
-import type { AbstractUIService } from '../ui-server/ui-services/AbstractUIService.js'
 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
 
 const moduleName = 'UIServiceWorkerBroadcastChannel'
 
 interface Responses {
+  responses: BroadcastChannelResponsePayload[]
   responsesExpected: number
   responsesReceived: number
-  responses: BroadcastChannelResponsePayload[]
 }
 
 export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
-  private readonly uiService: AbstractUIService
   private readonly responses: Map<string, Responses>
+  private readonly uiService: AbstractUIService
 
   constructor (uiService: AbstractUIService) {
     super()
@@ -29,38 +30,6 @@ export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
     this.responses = new Map<string, Responses>()
   }
 
-  private responseHandler (messageEvent: MessageEvent): void {
-    const validatedMessageEvent = this.validateMessageEvent(messageEvent)
-    if (validatedMessageEvent === false) {
-      return
-    }
-    if (this.isRequest(validatedMessageEvent.data)) {
-      return
-    }
-    const [uuid, responsePayload] = validatedMessageEvent.data as BroadcastChannelResponse
-    if (!this.responses.has(uuid)) {
-      this.responses.set(uuid, {
-        responsesExpected: this.uiService.getBroadcastChannelExpectedResponses(uuid),
-        responsesReceived: 1,
-        responses: [responsePayload],
-      })
-    } else if (
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      this.responses.get(uuid)!.responsesReceived <= this.responses.get(uuid)!.responsesExpected
-    ) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      ++this.responses.get(uuid)!.responsesReceived
-      this.responses.get(uuid)?.responses.push(responsePayload)
-    }
-    if (
-      this.responses.get(uuid)?.responsesReceived === this.responses.get(uuid)?.responsesExpected
-    ) {
-      this.uiService.sendResponse(uuid, this.buildResponsePayload(uuid))
-      this.responses.delete(uuid)
-      this.uiService.deleteBroadcastChannelRequest(uuid)
-    }
-  }
-
   private buildResponsePayload (uuid: string): ResponsePayload {
     const responsesStatus =
       this.responses
@@ -69,22 +38,22 @@ export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
         ? ResponseStatus.SUCCESS
         : ResponseStatus.FAILURE
     return {
-      status: responsesStatus,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
       hashIdsSucceeded: this.responses
         .get(uuid)
-        ?.responses.map(({ status, hashId }) => {
+        ?.responses.map(({ hashId, status }) => {
           if (hashId != null && status === ResponseStatus.SUCCESS) {
             return hashId
           }
           return undefined
         })
         .filter(hashId => hashId != null)!,
+      status: responsesStatus,
       ...(responsesStatus === ResponseStatus.FAILURE && {
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
         hashIdsFailed: this.responses
           .get(uuid)
-          ?.responses.map(({ status, hashId }) => {
+          ?.responses.map(({ hashId, status }) => {
             if (hashId != null && status === ResponseStatus.FAILURE) {
               return hashId
             }
@@ -113,4 +82,36 @@ export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
       messageEvent
     )
   }
+
+  private responseHandler (messageEvent: MessageEvent): void {
+    const validatedMessageEvent = this.validateMessageEvent(messageEvent)
+    if (validatedMessageEvent === false) {
+      return
+    }
+    if (this.isRequest(validatedMessageEvent.data)) {
+      return
+    }
+    const [uuid, responsePayload] = validatedMessageEvent.data as BroadcastChannelResponse
+    if (!this.responses.has(uuid)) {
+      this.responses.set(uuid, {
+        responses: [responsePayload],
+        responsesExpected: this.uiService.getBroadcastChannelExpectedResponses(uuid),
+        responsesReceived: 1,
+      })
+    } else if (
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      this.responses.get(uuid)!.responsesReceived <= this.responses.get(uuid)!.responsesExpected
+    ) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      ++this.responses.get(uuid)!.responsesReceived
+      this.responses.get(uuid)?.responses.push(responsePayload)
+    }
+    if (
+      this.responses.get(uuid)?.responsesReceived === this.responses.get(uuid)?.responsesExpected
+    ) {
+      this.uiService.sendResponse(uuid, this.buildResponsePayload(uuid))
+      this.responses.delete(uuid)
+      this.uiService.deleteBroadcastChannelRequest(uuid)
+    }
+  }
 }
index 7dfa92651d30c188416be09347cb6e184dd3a38f..66c4c0de6aa266580a9be0b77250dd62e2acabe9 100644 (file)
@@ -6,21 +6,18 @@ import type {
   JsonType,
   MessageEvent,
 } from '../../types/index.js'
+
 import { logger, logPrefix, validateUUID } from '../../utils/index.js'
 
 const moduleName = 'WorkerBroadcastChannel'
 
 export abstract class WorkerBroadcastChannel extends BroadcastChannel {
-  protected constructor () {
-    super('worker')
-  }
-
-  public sendRequest (request: BroadcastChannelRequest): void {
-    this.postMessage(request)
+  private readonly logPrefix = (modName: string, methodName: string): string => {
+    return logPrefix(` Worker Broadcast Channel | ${modName}.${methodName}:`)
   }
 
-  protected sendResponse (response: BroadcastChannelResponse): void {
-    this.postMessage(response)
+  protected constructor () {
+    super('worker')
   }
 
   protected isRequest (message: JsonType[]): boolean {
@@ -31,7 +28,11 @@ export abstract class WorkerBroadcastChannel extends BroadcastChannel {
     return Array.isArray(message) && message.length === 2
   }
 
-  protected validateMessageEvent (messageEvent: MessageEvent): MessageEvent | false {
+  protected sendResponse (response: BroadcastChannelResponse): void {
+    this.postMessage(response)
+  }
+
+  protected validateMessageEvent (messageEvent: MessageEvent): false | MessageEvent {
     if (!Array.isArray(messageEvent.data)) {
       logger.error(
         `${this.logPrefix(
@@ -53,7 +54,7 @@ export abstract class WorkerBroadcastChannel extends BroadcastChannel {
     return messageEvent
   }
 
-  private readonly logPrefix = (modName: string, methodName: string): string => {
-    return logPrefix(` Worker Broadcast Channel | ${modName}.${methodName}:`)
+  public sendRequest (request: BroadcastChannelRequest): void {
+    this.postMessage(request)
   }
 }
index a03b5be6a829822b894081590bd3255697cd53ac..bcc96e6e16ce624c5199cec550a9abf7baf97165 100644 (file)
@@ -1,11 +1,7 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
-import { randomInt } from 'node:crypto'
-import { createWriteStream, readdirSync } from 'node:fs'
-import { dirname, extname, join, resolve } from 'node:path'
-import { fileURLToPath, URL } from 'node:url'
-
 import type { ValidateFunction } from 'ajv'
+
 import { Client, type FTPResponse } from 'basic-ftp'
 import {
   addSeconds,
@@ -15,6 +11,10 @@ import {
   secondsToMilliseconds,
 } from 'date-fns'
 import { maxTime } from 'date-fns/constants'
+import { randomInt } from 'node:crypto'
+import { createWriteStream, readdirSync } from 'node:fs'
+import { dirname, extname, join, resolve } from 'node:path'
+import { fileURLToPath, URL } from 'node:url'
 import { isEmpty } from 'rambda'
 import { create } from 'tar'
 
@@ -134,40 +134,40 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     super(OCPPVersion.VERSION_16)
     this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
       [
-        OCPP16IncomingRequestCommand.RESET,
-        this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CACHE,
-        this.handleRequestClearCache.bind(this) as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+        this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
-        this.handleRequestUnlockConnector.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+        this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
-        this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.CLEAR_CACHE,
+        this.handleRequestClearCache.bind(this) as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
-        this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+        this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
-        this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.DATA_TRANSFER,
+        this.handleRequestDataTransfer.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
-        this.handleRequestSetChargingProfile.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
+        this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
-        this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+        this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
-        this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+        this.handleRequestGetDiagnostics.bind(this) as IncomingRequestHandler,
       ],
       [
         OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
@@ -178,28 +178,28 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         this.handleRequestRemoteStopTransaction.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
-        this.handleRequestGetDiagnostics.bind(this) as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        this.handleRequestReserveNow.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
-        this.handleRequestTriggerMessage.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.RESET,
+        this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.DATA_TRANSFER,
-        this.handleRequestDataTransfer.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+        this.handleRequestSetChargingProfile.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
-        this.handleRequestUpdateFirmware.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        this.handleRequestTriggerMessage.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.RESERVE_NOW,
-        this.handleRequestReserveNow.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+        this.handleRequestUnlockConnector.bind(this) as unknown as IncomingRequestHandler,
       ],
       [
-        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
-        this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler,
+        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+        this.handleRequestUpdateFirmware.bind(this) as unknown as IncomingRequestHandler,
       ],
     ])
     this.payloadValidateFunctions = new Map<
@@ -207,60 +207,60 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
       ValidateFunction<JsonType>
     >([
       [
-        OCPP16IncomingRequestCommand.RESET,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
-            'assets/json-schemas/ocpp/1.6/Reset.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+            'assets/json-schemas/ocpp/1.6/CancelReservation.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CACHE,
+        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
-            'assets/json-schemas/ocpp/1.6/ClearCache.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityRequest>(
+            'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
-            'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
+            'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+        OCPP16IncomingRequestCommand.CLEAR_CACHE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
-            'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
+            'assets/json-schemas/ocpp/1.6/ClearCache.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
-            'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileRequest>(
+            'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+        OCPP16IncomingRequestCommand.DATA_TRANSFER,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
-            'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
+            'assets/json-schemas/ocpp/1.6/DataTransfer.json',
             moduleName,
             'constructor'
           )
@@ -277,100 +277,100 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
-            'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
+            'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileRequest>(
-            'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
+            'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+        OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityRequest>(
-            'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
+            'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+        OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
-            'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
+            'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
-            'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+            'assets/json-schemas/ocpp/1.6/ReserveNow.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        OCPP16IncomingRequestCommand.RESET,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
-            'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
+            'assets/json-schemas/ocpp/1.6/Reset.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.DATA_TRANSFER,
+        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
-            'assets/json-schemas/ocpp/1.6/DataTransfer.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
+            'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
-            'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
+            'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
-            'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
+            'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
-            'assets/json-schemas/ocpp/1.6/CancelReservation.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
+            'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
             moduleName,
             'constructor'
           )
@@ -476,7 +476,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         if (response.status !== OCPP16TriggerMessageStatus.ACCEPTED) {
           return
         }
-        const { requestedMessage, connectorId } = request
+        const { connectorId, requestedMessage } = request
         const errorHandler = (error: unknown): void => {
           logger.error(
             `${chargingStation.logPrefix()} ${moduleName}.constructor: Trigger ${requestedMessage} error:`,
@@ -571,201 +571,103 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     this.validatePayload = this.validatePayload.bind(this)
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
+  private async handleRequestCancelReservation (
     chargingStation: ChargingStation,
-    messageId: string,
-    commandName: OCPP16IncomingRequestCommand,
-    commandPayload: ReqType
-  ): Promise<void> {
-    let response: ResType
+    commandPayload: OCPP16CancelReservationRequest
+  ): Promise<GenericResponse> {
     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
+      !OCPP16ServiceUtils.checkFeatureProfile(
+        chargingStation,
+        OCPP16SupportedFeatureProfiles.Reservation,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION
       )
-    }
-    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
+      return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
+    }
+    try {
+      const { reservationId } = commandPayload
+      const reservation = chargingStation.getReservationBy('reservationId', reservationId)
+      if (reservation == null) {
+        logger.debug(
+          `${chargingStation.logPrefix()} Reservation with id ${reservationId.toString()} does not exist on charging station`
         )
+        return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
       }
-    } 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
+      await chargingStation.removeReservation(
+        reservation,
+        ReservationTerminationReason.RESERVATION_CANCELED
       )
+      return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED
+    } catch (error) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      return handleIncomingRequestError<GenericResponse>(
+        chargingStation,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        error as Error,
+        {
+          errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED,
+        }
+      )!
     }
-    // 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 validatePayload (
-    chargingStation: ChargingStation,
-    commandName: OCPP16IncomingRequestCommand,
-    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
   }
 
-  // Simulate charging station restart
-  private handleRequestReset (
-    chargingStation: ChargingStation,
-    commandPayload: ResetRequest
-  ): GenericResponse {
-    const { type } = commandPayload
-    chargingStation
-      .reset(`${type}Reset` as OCPP16StopTransactionReason)
-      .catch(Constants.EMPTY_FUNCTION)
-    logger.info(
-      `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        chargingStation.stationInfo!.resetTime!
-      )}`
-    )
-    return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
-  }
-
-  private async handleRequestUnlockConnector (
+  private async handleRequestChangeAvailability (
     chargingStation: ChargingStation,
-    commandPayload: UnlockConnectorRequest
-  ): Promise<UnlockConnectorResponse> {
-    const { connectorId } = commandPayload
+    commandPayload: OCPP16ChangeAvailabilityRequest
+  ): Promise<OCPP16ChangeAvailabilityResponse> {
+    const { connectorId, type } = commandPayload
     if (!chargingStation.hasConnector(connectorId)) {
       logger.error(
-        `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}`
+        `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}`
       )
-      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
     }
+    const chargePointStatus: OCPP16ChargePointStatus =
+      type === OCPP16AvailabilityType.Operative
+        ? OCPP16ChargePointStatus.Available
+        : OCPP16ChargePointStatus.Unavailable
     if (connectorId === 0) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId.toString()}`
-      )
-      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
-    }
-    if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
-      const stopResponse = await chargingStation.stopTransactionOnConnector(
-        connectorId,
-        OCPP16StopTransactionReason.UNLOCK_COMMAND
-      )
-      if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
-        return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
-      }
-      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
-    }
-    await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-      chargingStation,
-      connectorId,
-      OCPP16ChargePointStatus.Available
-    )
-    return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
-  }
-
-  private handleRequestGetConfiguration (
-    chargingStation: ChargingStation,
-    commandPayload: GetConfigurationRequest
-  ): GetConfigurationResponse {
-    const { key } = commandPayload
-    const configurationKey: OCPPConfigurationKey[] = []
-    const unknownKey: string[] = []
-    if (key == null) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) {
-        if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) {
-          continue
+      let response: OCPP16ChangeAvailabilityResponse | undefined
+      if (chargingStation.hasEvses) {
+        for (const evseStatus of chargingStation.evses.values()) {
+          response = await OCPP16ServiceUtils.changeAvailability(
+            chargingStation,
+            [...evseStatus.connectors.keys()],
+            chargePointStatus,
+            type
+          )
         }
-        configurationKey.push({
-          key: configKey.key,
-          readonly: configKey.readonly,
-          value: configKey.value,
-        })
+      } else {
+        response = await OCPP16ServiceUtils.changeAvailability(
+          chargingStation,
+          [...chargingStation.connectors.keys()],
+          chargePointStatus,
+          type
+        )
       }
-    } else if (isNotEmptyArray(key)) {
-      for (const k of key) {
-        const keyFound = getConfigurationKey(chargingStation, k, true)
-        if (keyFound != null) {
-          if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) {
-            continue
-          }
-          configurationKey.push({
-            key: keyFound.key,
-            readonly: keyFound.readonly,
-            value: keyFound.value,
-          })
-        } else {
-          unknownKey.push(k)
-        }
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      return response!
+    } else if (
+      connectorId > 0 &&
+      (chargingStation.isChargingStationAvailable() ||
+        (!chargingStation.isChargingStationAvailable() &&
+          type === OCPP16AvailabilityType.Inoperative))
+    ) {
+      if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        chargingStation.getConnectorStatus(connectorId)!.availability = type
+        return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
       }
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      chargingStation.getConnectorStatus(connectorId)!.availability = type
+      await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+        chargingStation,
+        connectorId,
+        chargePointStatus
+      )
+      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
     }
-    return {
-      configurationKey,
-      unknownKey,
-    }
+    return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
   }
 
   private handleRequestChangeConfiguration (
@@ -844,74 +746,88 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED
   }
 
-  private handleRequestSetChargingProfile (
+  private handleRequestClearChargingProfile (
     chargingStation: ChargingStation,
-    commandPayload: SetChargingProfileRequest
-  ): SetChargingProfileResponse {
+    commandPayload: OCPP16ClearChargingProfileRequest
+  ): OCPP16ClearChargingProfileResponse {
     if (
       !OCPP16ServiceUtils.checkFeatureProfile(
         chargingStation,
         OCPP16SupportedFeatureProfiles.SmartCharging,
-        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
       )
     ) {
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED
+      return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
     }
-    const { connectorId, csChargingProfiles } = commandPayload
-    if (!chargingStation.hasConnector(connectorId)) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId.toString()}`
-      )
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+    const { connectorId } = commandPayload
+    if (connectorId != null) {
+      if (!chargingStation.hasConnector(connectorId)) {
+        logger.error(
+          `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId.toString()}`
+        )
+        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+      }
+      const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+      if (isNotEmptyArray(connectorStatus?.chargingProfiles)) {
+        connectorStatus.chargingProfiles = []
+        logger.debug(
+          `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId.toString()}`
+        )
+        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
+      }
+    } else {
+      let clearedCP = false
+      if (chargingStation.hasEvses) {
+        for (const evseStatus of chargingStation.evses.values()) {
+          for (const status of evseStatus.connectors.values()) {
+            const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
+              chargingStation,
+              commandPayload,
+              status.chargingProfiles
+            )
+            if (clearedConnectorCP && !clearedCP) {
+              clearedCP = true
+            }
+          }
+        }
+      } else {
+        for (const id of chargingStation.connectors.keys()) {
+          const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
+            chargingStation,
+            commandPayload,
+            chargingStation.getConnectorStatus(id)?.chargingProfiles
+          )
+          if (clearedConnectorCP && !clearedCP) {
+            clearedCP = true
+          }
+        }
+      }
+      if (clearedCP) {
+        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
+      }
     }
-    if (
-      csChargingProfiles.chargingProfilePurpose ===
-        OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
-      connectorId !== 0
-    ) {
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+    return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+  }
+
+  private handleRequestDataTransfer (
+    chargingStation: ChargingStation,
+    commandPayload: OCPP16DataTransferRequest
+  ): OCPP16DataTransferResponse {
+    const { vendorId } = commandPayload
+    try {
+      if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) {
+        return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED
+      }
+      return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID
+    } catch (error) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      return handleIncomingRequestError<OCPP16DataTransferResponse>(
+        chargingStation,
+        OCPP16IncomingRequestCommand.DATA_TRANSFER,
+        error as Error,
+        { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED }
+      )!
     }
-    if (
-      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
-      connectorId === 0
-    ) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()}`
-      )
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
-    }
-    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-    if (
-      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
-      connectorId > 0 &&
-      connectorStatus?.transactionStarted === false
-    ) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} without a started transaction`
-      )
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
-    }
-    if (
-      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
-      connectorId > 0 &&
-      connectorStatus?.transactionStarted === true &&
-      csChargingProfiles.transactionId != null &&
-      csChargingProfiles.transactionId !== connectorStatus.transactionId
-    ) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} with a different transaction id ${
-          csChargingProfiles.transactionId.toString()
-          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        } than the started transaction id ${connectorStatus.transactionId?.toString()}`
-      )
-      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
-    }
-    OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles)
-    logger.debug(
-      `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId.toString()}: %j`,
-      csChargingProfiles
-    )
-    return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED
   }
 
   private handleRequestGetCompositeSchedule (
@@ -927,7 +843,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     ) {
       return OCPP16Constants.OCPP_RESPONSE_REJECTED
     }
-    const { connectorId, duration, chargingRateUnit } = commandPayload
+    const { chargingRateUnit, connectorId, duration } = commandPayload
     if (!chargingStation.hasConnector(connectorId)) {
       logger.error(
         `${chargingStation.logPrefix()} Trying to get composite schedule to a non existing connector id ${connectorId.toString()}`
@@ -954,8 +870,8 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     const currentDate = new Date()
     const compositeScheduleInterval: Interval = {
-      start: currentDate,
       end: addSeconds(currentDate, duration),
+      start: currentDate,
     }
     // FIXME: add and handle charging station charging profiles
     const chargingProfiles: OCPP16ChargingProfile[] = getConnectorChargingProfiles(
@@ -1019,138 +935,182 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
     if (compositeSchedule != null) {
       return {
-        status: GenericStatus.Accepted,
-        scheduleStart: compositeSchedule.startSchedule,
-        connectorId,
         chargingSchedule: compositeSchedule,
+        connectorId,
+        scheduleStart: compositeSchedule.startSchedule,
+        status: GenericStatus.Accepted,
       }
     }
     return OCPP16Constants.OCPP_RESPONSE_REJECTED
   }
 
-  private handleRequestClearChargingProfile (
+  private handleRequestGetConfiguration (
     chargingStation: ChargingStation,
-    commandPayload: OCPP16ClearChargingProfileRequest
-  ): OCPP16ClearChargingProfileResponse {
+    commandPayload: GetConfigurationRequest
+  ): GetConfigurationResponse {
+    const { key } = commandPayload
+    const configurationKey: OCPPConfigurationKey[] = []
+    const unknownKey: string[] = []
+    if (key == null) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) {
+        if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) {
+          continue
+        }
+        configurationKey.push({
+          key: configKey.key,
+          readonly: configKey.readonly,
+          value: configKey.value,
+        })
+      }
+    } else if (isNotEmptyArray(key)) {
+      for (const k of key) {
+        const keyFound = getConfigurationKey(chargingStation, k, true)
+        if (keyFound != null) {
+          if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) {
+            continue
+          }
+          configurationKey.push({
+            key: keyFound.key,
+            readonly: keyFound.readonly,
+            value: keyFound.value,
+          })
+        } else {
+          unknownKey.push(k)
+        }
+      }
+    }
+    return {
+      configurationKey,
+      unknownKey,
+    }
+  }
+
+  private async handleRequestGetDiagnostics (
+    chargingStation: ChargingStation,
+    commandPayload: GetDiagnosticsRequest
+  ): Promise<GetDiagnosticsResponse> {
     if (
       !OCPP16ServiceUtils.checkFeatureProfile(
         chargingStation,
-        OCPP16SupportedFeatureProfiles.SmartCharging,
-        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
+        OCPP16SupportedFeatureProfiles.FirmwareManagement,
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
       )
     ) {
-      return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
+      )
+      return OCPP16Constants.OCPP_RESPONSE_EMPTY
     }
-    const { connectorId } = commandPayload
-    if (connectorId != null) {
-      if (!chargingStation.hasConnector(connectorId)) {
-        logger.error(
-          `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId.toString()}`
+    const { location } = commandPayload
+    const uri = new URL(location)
+    if (uri.protocol.startsWith('ftp:')) {
+      let ftpClient: Client | undefined
+      try {
+        const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+          ConfigurationSection.log
         )
-        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
-      }
-      const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-      if (isNotEmptyArray(connectorStatus?.chargingProfiles)) {
-        connectorStatus.chargingProfiles = []
-        logger.debug(
-          `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId.toString()}`
+        const logFiles = readdirSync(
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          resolve((fileURLToPath(import.meta.url), '../', dirname(logConfiguration.file!)))
         )
-        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
-      }
-    } else {
-      let clearedCP = false
-      if (chargingStation.hasEvses) {
-        for (const evseStatus of chargingStation.evses.values()) {
-          for (const status of evseStatus.connectors.values()) {
-            const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
-              chargingStation,
-              commandPayload,
-              status.chargingProfiles
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          .filter(file => file.endsWith(extname(logConfiguration.file!)))
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          .map(file => join(dirname(logConfiguration.file!), file))
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
+        create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
+        ftpClient = new Client()
+        const accessResponse = await ftpClient.access({
+          host: uri.hostname,
+          ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
+          ...(isNotEmptyString(uri.username) && { user: uri.username }),
+          ...(isNotEmptyString(uri.password) && { password: uri.password }),
+        })
+        let uploadResponse: FTPResponse | undefined
+        if (accessResponse.code === 220) {
+          ftpClient.trackProgress(info => {
+            logger.info(
+              `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${(
+                info.bytes / 1024
+              ).toString()} bytes transferred from diagnostics archive ${info.name}`
             )
-            if (clearedConnectorCP && !clearedCP) {
-              clearedCP = true
-            }
-          }
-        }
-      } else {
-        for (const id of chargingStation.connectors.keys()) {
-          const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
-            chargingStation,
-            commandPayload,
-            chargingStation.getConnectorStatus(id)?.chargingProfiles
+            chargingStation.ocppRequestService
+              .requestHandler<
+                OCPP16DiagnosticsStatusNotificationRequest,
+                OCPP16DiagnosticsStatusNotificationResponse
+              >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+                status: OCPP16DiagnosticsStatus.Uploading,
+              })
+              .catch((error: unknown) => {
+                logger.error(
+                  `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
+                    OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
+                  }'`,
+                  error
+                )
+              })
+          })
+          uploadResponse = await ftpClient.uploadFrom(
+            join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive),
+            `${uri.pathname}${diagnosticsArchive}`
           )
-          if (clearedConnectorCP && !clearedCP) {
-            clearedCP = true
+          if (uploadResponse.code === 226) {
+            await chargingStation.ocppRequestService.requestHandler<
+              OCPP16DiagnosticsStatusNotificationRequest,
+              OCPP16DiagnosticsStatusNotificationResponse
+            >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+              status: OCPP16DiagnosticsStatus.Uploaded,
+            })
+            ftpClient.close()
+            return { fileName: diagnosticsArchive }
           }
+          throw new OCPPError(
+            ErrorType.GENERIC_ERROR,
+            `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse.code.toString()}`,
+            OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+          )
         }
+        throw new OCPPError(
+          ErrorType.GENERIC_ERROR,
+          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+          `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse?.code.toString()}`,
+          OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+        )
+      } catch (error) {
+        await chargingStation.ocppRequestService.requestHandler<
+          OCPP16DiagnosticsStatusNotificationRequest,
+          OCPP16DiagnosticsStatusNotificationResponse
+        >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+          status: OCPP16DiagnosticsStatus.UploadFailed,
+        })
+        ftpClient?.close()
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        return handleIncomingRequestError<GetDiagnosticsResponse>(
+          chargingStation,
+          OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+          error as Error,
+          { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY }
+        )!
       }
-      if (clearedCP) {
-        return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
-      }
+    } else {
+      logger.error(
+        `${chargingStation.logPrefix()} Unsupported protocol ${
+          uri.protocol
+        } to transfer the diagnostic logs archive`
+      )
+      await chargingStation.ocppRequestService.requestHandler<
+        OCPP16DiagnosticsStatusNotificationRequest,
+        OCPP16DiagnosticsStatusNotificationResponse
+      >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+        status: OCPP16DiagnosticsStatus.UploadFailed,
+      })
+      return OCPP16Constants.OCPP_RESPONSE_EMPTY
     }
-    return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
   }
 
-  private async handleRequestChangeAvailability (
-    chargingStation: ChargingStation,
-    commandPayload: OCPP16ChangeAvailabilityRequest
-  ): Promise<OCPP16ChangeAvailabilityResponse> {
-    const { connectorId, type } = commandPayload
-    if (!chargingStation.hasConnector(connectorId)) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}`
-      )
-      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
-    }
-    const chargePointStatus: OCPP16ChargePointStatus =
-      type === OCPP16AvailabilityType.Operative
-        ? OCPP16ChargePointStatus.Available
-        : OCPP16ChargePointStatus.Unavailable
-    if (connectorId === 0) {
-      let response: OCPP16ChangeAvailabilityResponse | undefined
-      if (chargingStation.hasEvses) {
-        for (const evseStatus of chargingStation.evses.values()) {
-          response = await OCPP16ServiceUtils.changeAvailability(
-            chargingStation,
-            [...evseStatus.connectors.keys()],
-            chargePointStatus,
-            type
-          )
-        }
-      } else {
-        response = await OCPP16ServiceUtils.changeAvailability(
-          chargingStation,
-          [...chargingStation.connectors.keys()],
-          chargePointStatus,
-          type
-        )
-      }
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      return response!
-    } else if (
-      connectorId > 0 &&
-      (chargingStation.isChargingStationAvailable() ||
-        (!chargingStation.isChargingStationAvailable() &&
-          type === OCPP16AvailabilityType.Inoperative))
-    ) {
-      if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        chargingStation.getConnectorStatus(connectorId)!.availability = type
-        return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
-      }
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      chargingStation.getConnectorStatus(connectorId)!.availability = type
-      await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-        chargingStation,
-        connectorId,
-        chargePointStatus
-      )
-      return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
-    }
-    return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
-  }
-
-  private async handleRequestRemoteStartTransaction (
+  private async handleRequestRemoteStartTransaction (
     chargingStation: ChargingStation,
     commandPayload: RemoteStartTransactionRequest
   ): Promise<GenericResponse> {
@@ -1178,7 +1138,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
         return OCPP16Constants.OCPP_RESPONSE_REJECTED
       }
     }
-    const { connectorId: transactionConnectorId, idTag, chargingProfile } = commandPayload
+    const { chargingProfile, connectorId: transactionConnectorId, idTag } = commandPayload
     if (!chargingStation.hasConnector(transactionConnectorId)) {
       return this.notifyRemoteStartTransactionRejected(
         chargingStation,
@@ -1230,52 +1190,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
   }
 
-  private notifyRemoteStartTransactionRejected (
-    chargingStation: ChargingStation,
-    connectorId: number,
-    idTag: string
-  ): GenericResponse {
-    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-    logger.debug(
-      `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        chargingStation.stationInfo?.chargingStationId
-      }#${connectorId.toString()}, idTag '${idTag}', availability '${
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        connectorStatus?.availability
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-      }', status '${connectorStatus?.status}'`
-    )
-    return OCPP16Constants.OCPP_RESPONSE_REJECTED
-  }
-
-  private setRemoteStartTransactionChargingProfile (
-    chargingStation: ChargingStation,
-    connectorId: number,
-    chargingProfile: OCPP16ChargingProfile
-  ): boolean {
-    if (
-      chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
-      chargingProfile.transactionId == null
-    ) {
-      OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile)
-      logger.debug(
-        `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on ${
-          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-          chargingStation.stationInfo?.chargingStationId
-        }#${connectorId.toString()}`,
-        chargingProfile
-      )
-      return true
-    }
-    logger.debug(
-      `${chargingStation.logPrefix()} Not allowed to set ${
-        chargingProfile.chargingProfilePurpose
-      } charging profile(s)${chargingProfile.transactionId != null ? ' with transactionId set' : ''} at remote start transaction`
-    )
-    return false
-  }
-
   private handleRequestRemoteStopTransaction (
     chargingStation: ChargingStation,
     commandPayload: RemoteStopTransactionRequest
@@ -1293,322 +1207,182 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     return OCPP16Constants.OCPP_RESPONSE_REJECTED
   }
 
-  private handleRequestUpdateFirmware (
+  private async handleRequestReserveNow (
     chargingStation: ChargingStation,
-    commandPayload: OCPP16UpdateFirmwareRequest
-  ): OCPP16UpdateFirmwareResponse {
+    commandPayload: OCPP16ReserveNowRequest
+  ): Promise<OCPP16ReserveNowResponse> {
     if (
       !OCPP16ServiceUtils.checkFeatureProfile(
         chargingStation,
-        OCPP16SupportedFeatureProfiles.FirmwareManagement,
-        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE
+        OCPP16SupportedFeatureProfiles.Reservation,
+        OCPP16IncomingRequestCommand.RESERVE_NOW
       )
     ) {
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
-      )
-      return OCPP16Constants.OCPP_RESPONSE_EMPTY
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
     }
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
-    const { retrieveDate } = commandPayload
-    if (
-      chargingStation.stationInfo?.firmwareStatus != null &&
-      chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
-    ) {
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
+    commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
+    const { connectorId, idTag, reservationId } = commandPayload
+    if (!chargingStation.hasConnector(connectorId)) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to reserve a non existing connector id ${connectorId.toString()}`
       )
-      return OCPP16Constants.OCPP_RESPONSE_EMPTY
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
     }
-    const now = Date.now()
-    if (retrieveDate.getTime() <= now) {
-      this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
-    } else {
-      setTimeout(() => {
-        this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
-      }, retrieveDate.getTime() - now)
+    if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) {
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
     }
-    return OCPP16Constants.OCPP_RESPONSE_EMPTY
-  }
-
-  private async updateFirmwareSimulation (
-    chargingStation: ChargingStation,
-    maxDelay = 30,
-    minDelay = 15
-  ): Promise<void> {
-    if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
-      return
+    if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) {
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
     }
-    if (chargingStation.hasEvses) {
-      for (const [evseId, evseStatus] of chargingStation.evses) {
-        if (evseId > 0) {
-          for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-            if (connectorStatus.transactionStarted === false) {
-              await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-                chargingStation,
-                connectorId,
-                OCPP16ChargePointStatus.Unavailable
-              )
-            }
+    if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
+      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+    }
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
+    resetAuthorizeConnectorStatus(connectorStatus)
+    let response: OCPP16ReserveNowResponse
+    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.SuspendedEV:
+        case OCPP16ChargePointStatus.SuspendedEVSE:
+        case OCPP16ChargePointStatus.Finishing:
+          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+          break
+        case OCPP16ChargePointStatus.Unavailable:
+          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE
+          break
+        case OCPP16ChargePointStatus.Reserved:
+          if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) {
+            response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+            break
           }
-        }
-      }
-    } else {
-      for (const connectorId of chargingStation.connectors.keys()) {
-        if (
-          connectorId > 0 &&
-          chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
-        ) {
-          await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-            chargingStation,
-            connectorId,
-            OCPP16ChargePointStatus.Unavailable
-          )
-        }
+        // eslint-disable-next-line no-fallthrough
+        default:
+          if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
+            response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+            break
+          }
+          await chargingStation.addReservation({
+            id: commandPayload.reservationId,
+            ...commandPayload,
+          })
+          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED
+          break
       }
+      return response
+    } catch (error) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      return handleIncomingRequestError<OCPP16ReserveNowResponse>(
+        chargingStation,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        error as Error,
+        { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
+      )!
     }
-    await chargingStation.ocppRequestService.requestHandler<
-      OCPP16FirmwareStatusNotificationRequest,
-      OCPP16FirmwareStatusNotificationResponse
-    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
-      status: OCPP16FirmwareStatus.Downloading,
-    })
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading
+  }
+
+  // Simulate charging station restart
+  private handleRequestReset (
+    chargingStation: ChargingStation,
+    commandPayload: ResetRequest
+  ): GenericResponse {
+    const { type } = commandPayload
+    chargingStation
+      .reset(`${type}Reset` as OCPP16StopTransactionReason)
+      .catch(Constants.EMPTY_FUNCTION)
+    logger.info(
+      `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        chargingStation.stationInfo!.resetTime!
+      )}`
+    )
+    return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
+  }
+
+  private handleRequestSetChargingProfile (
+    chargingStation: ChargingStation,
+    commandPayload: SetChargingProfileRequest
+  ): SetChargingProfileResponse {
     if (
-      chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
-      OCPP16FirmwareStatus.DownloadFailed
+      !OCPP16ServiceUtils.checkFeatureProfile(
+        chargingStation,
+        OCPP16SupportedFeatureProfiles.SmartCharging,
+        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
+      )
     ) {
-      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
-      await chargingStation.ocppRequestService.requestHandler<
-        OCPP16FirmwareStatusNotificationRequest,
-        OCPP16FirmwareStatusNotificationResponse
-      >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
-        status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
-      })
-      chargingStation.stationInfo.firmwareStatus =
-        chargingStation.stationInfo.firmwareUpgrade.failureStatus
-      return
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED
     }
-    await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
-    await chargingStation.ocppRequestService.requestHandler<
-      OCPP16FirmwareStatusNotificationRequest,
-      OCPP16FirmwareStatusNotificationResponse
-    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
-      status: OCPP16FirmwareStatus.Downloaded,
-    })
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded
-    let wasTransactionsStarted = false
-    let transactionsStarted: boolean
-    do {
-      const runningTransactions = chargingStation.getNumberOfRunningTransactions()
-      if (runningTransactions > 0) {
-        const waitTime = secondsToMilliseconds(15)
-        logger.debug(
-          `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions.toString()} transaction(s) in progress, waiting ${formatDurationMilliSeconds(
-            waitTime
-          )} before continuing firmware update simulation`
-        )
-        await sleep(waitTime)
-        transactionsStarted = true
-        wasTransactionsStarted = true
-      } else {
-        if (chargingStation.hasEvses) {
-          for (const [evseId, evseStatus] of chargingStation.evses) {
-            if (evseId > 0) {
-              for (const [connectorId, connectorStatus] of evseStatus.connectors) {
-                if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
-                  await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-                    chargingStation,
-                    connectorId,
-                    OCPP16ChargePointStatus.Unavailable
-                  )
-                }
-              }
-            }
-          }
-        } else {
-          for (const connectorId of chargingStation.connectors.keys()) {
-            if (
-              connectorId > 0 &&
-              chargingStation.getConnectorStatus(connectorId)?.status !==
-                OCPP16ChargePointStatus.Unavailable
-            ) {
-              await OCPP16ServiceUtils.sendAndSetConnectorStatus(
-                chargingStation,
-                connectorId,
-                OCPP16ChargePointStatus.Unavailable
-              )
-            }
-          }
-        }
-        transactionsStarted = false
-      }
-    } while (transactionsStarted)
-    !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
-    if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
-      return
+    const { connectorId, csChargingProfiles } = commandPayload
+    if (!chargingStation.hasConnector(connectorId)) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId.toString()}`
+      )
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
     }
-    await chargingStation.ocppRequestService.requestHandler<
-      OCPP16FirmwareStatusNotificationRequest,
-      OCPP16FirmwareStatusNotificationResponse
-    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
-      status: OCPP16FirmwareStatus.Installing,
-    })
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing
     if (
-      chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
-      OCPP16FirmwareStatus.InstallationFailed
+      csChargingProfiles.chargingProfilePurpose ===
+        OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
+      connectorId !== 0
     ) {
-      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
-      await chargingStation.ocppRequestService.requestHandler<
-        OCPP16FirmwareStatusNotificationRequest,
-        OCPP16FirmwareStatusNotificationResponse
-      >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
-        status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
-      })
-      chargingStation.stationInfo.firmwareStatus =
-        chargingStation.stationInfo.firmwareUpgrade.failureStatus
-      return
-    }
-    if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
-      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
-      await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
     }
-  }
-
-  private async handleRequestGetDiagnostics (
-    chargingStation: ChargingStation,
-    commandPayload: GetDiagnosticsRequest
-  ): Promise<GetDiagnosticsResponse> {
     if (
-      !OCPP16ServiceUtils.checkFeatureProfile(
-        chargingStation,
-        OCPP16SupportedFeatureProfiles.FirmwareManagement,
-        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+      connectorId === 0
+    ) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()}`
       )
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+    }
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+    if (
+      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+      connectorId > 0 &&
+      connectorStatus?.transactionStarted === false
     ) {
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} without a started transaction`
       )
-      return OCPP16Constants.OCPP_RESPONSE_EMPTY
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
     }
-    const { location } = commandPayload
-    const uri = new URL(location)
-    if (uri.protocol.startsWith('ftp:')) {
-      let ftpClient: Client | undefined
-      try {
-        const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
-          ConfigurationSection.log
-        )
-        const logFiles = readdirSync(
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          resolve((fileURLToPath(import.meta.url), '../', dirname(logConfiguration.file!)))
-        )
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          .filter(file => file.endsWith(extname(logConfiguration.file!)))
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          .map(file => join(dirname(logConfiguration.file!), file))
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
-        create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
-        ftpClient = new Client()
-        const accessResponse = await ftpClient.access({
-          host: uri.hostname,
-          ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
-          ...(isNotEmptyString(uri.username) && { user: uri.username }),
-          ...(isNotEmptyString(uri.password) && { password: uri.password }),
-        })
-        let uploadResponse: FTPResponse | undefined
-        if (accessResponse.code === 220) {
-          ftpClient.trackProgress(info => {
-            logger.info(
-              `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${(
-                info.bytes / 1024
-              ).toString()} bytes transferred from diagnostics archive ${info.name}`
-            )
-            chargingStation.ocppRequestService
-              .requestHandler<
-                OCPP16DiagnosticsStatusNotificationRequest,
-                OCPP16DiagnosticsStatusNotificationResponse
-              >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
-                status: OCPP16DiagnosticsStatus.Uploading,
-              })
-              .catch((error: unknown) => {
-                logger.error(
-                  `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
-                    OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
-                  }'`,
-                  error
-                )
-              })
-          })
-          uploadResponse = await ftpClient.uploadFrom(
-            join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive),
-            `${uri.pathname}${diagnosticsArchive}`
-          )
-          if (uploadResponse.code === 226) {
-            await chargingStation.ocppRequestService.requestHandler<
-              OCPP16DiagnosticsStatusNotificationRequest,
-              OCPP16DiagnosticsStatusNotificationResponse
-            >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
-              status: OCPP16DiagnosticsStatus.Uploaded,
-            })
-            ftpClient.close()
-            return { fileName: diagnosticsArchive }
-          }
-          throw new OCPPError(
-            ErrorType.GENERIC_ERROR,
-            `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse.code.toString()}`,
-            OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
-          )
-        }
-        throw new OCPPError(
-          ErrorType.GENERIC_ERROR,
-          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-          `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse?.code.toString()}`,
-          OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
-        )
-      } catch (error) {
-        await chargingStation.ocppRequestService.requestHandler<
-          OCPP16DiagnosticsStatusNotificationRequest,
-          OCPP16DiagnosticsStatusNotificationResponse
-        >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
-          status: OCPP16DiagnosticsStatus.UploadFailed,
-        })
-        ftpClient?.close()
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        return handleIncomingRequestError<GetDiagnosticsResponse>(
-          chargingStation,
-          OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
-          error as Error,
-          { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY }
-        )!
-      }
-    } else {
+    if (
+      csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+      connectorId > 0 &&
+      connectorStatus?.transactionStarted === true &&
+      csChargingProfiles.transactionId != null &&
+      csChargingProfiles.transactionId !== connectorStatus.transactionId
+    ) {
       logger.error(
-        `${chargingStation.logPrefix()} Unsupported protocol ${
-          uri.protocol
-        } to transfer the diagnostic logs archive`
+        `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} with a different transaction id ${
+          csChargingProfiles.transactionId.toString()
+          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        } than the started transaction id ${connectorStatus.transactionId?.toString()}`
       )
-      await chargingStation.ocppRequestService.requestHandler<
-        OCPP16DiagnosticsStatusNotificationRequest,
-        OCPP16DiagnosticsStatusNotificationResponse
-      >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
-        status: OCPP16DiagnosticsStatus.UploadFailed,
-      })
-      return OCPP16Constants.OCPP_RESPONSE_EMPTY
+      return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
     }
+    OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles)
+    logger.debug(
+      `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId.toString()}: %j`,
+      csChargingProfiles
+    )
+    return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED
   }
 
   private handleRequestTriggerMessage (
     chargingStation: ChargingStation,
     commandPayload: OCPP16TriggerMessageRequest
   ): OCPP16TriggerMessageResponse {
-    const { requestedMessage, connectorId } = commandPayload
+    const { connectorId, requestedMessage } = commandPayload
     if (
       !OCPP16ServiceUtils.checkFeatureProfile(
         chargingStation,
@@ -1639,147 +1413,373 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
     }
   }
 
-  private handleRequestDataTransfer (
+  private async handleRequestUnlockConnector (
     chargingStation: ChargingStation,
-    commandPayload: OCPP16DataTransferRequest
-  ): OCPP16DataTransferResponse {
-    const { vendorId } = commandPayload
-    try {
-      if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) {
-        return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED
+    commandPayload: UnlockConnectorRequest
+  ): Promise<UnlockConnectorResponse> {
+    const { connectorId } = commandPayload
+    if (!chargingStation.hasConnector(connectorId)) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}`
+      )
+      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+    }
+    if (connectorId === 0) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId.toString()}`
+      )
+      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+    }
+    if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
+      const stopResponse = await chargingStation.stopTransactionOnConnector(
+        connectorId,
+        OCPP16StopTransactionReason.UNLOCK_COMMAND
+      )
+      if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+        return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
       }
-      return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID
-    } catch (error) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      return handleIncomingRequestError<OCPP16DataTransferResponse>(
-        chargingStation,
-        OCPP16IncomingRequestCommand.DATA_TRANSFER,
-        error as Error,
-        { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED }
-      )!
+      return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
     }
+    await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+      chargingStation,
+      connectorId,
+      OCPP16ChargePointStatus.Available
+    )
+    return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
   }
 
-  private async handleRequestReserveNow (
+  private handleRequestUpdateFirmware (
     chargingStation: ChargingStation,
-    commandPayload: OCPP16ReserveNowRequest
-  ): Promise<OCPP16ReserveNowResponse> {
+    commandPayload: OCPP16UpdateFirmwareRequest
+  ): OCPP16UpdateFirmwareResponse {
     if (
       !OCPP16ServiceUtils.checkFeatureProfile(
         chargingStation,
-        OCPP16SupportedFeatureProfiles.Reservation,
-        OCPP16IncomingRequestCommand.RESERVE_NOW
+        OCPP16SupportedFeatureProfiles.FirmwareManagement,
+        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE
       )
     ) {
-      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
+      )
+      return OCPP16Constants.OCPP_RESPONSE_EMPTY
     }
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
-    const { reservationId, idTag, connectorId } = commandPayload
-    if (!chargingStation.hasConnector(connectorId)) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to reserve a non existing connector id ${connectorId.toString()}`
+    commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
+    const { retrieveDate } = commandPayload
+    if (
+      chargingStation.stationInfo?.firmwareStatus != null &&
+      chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
+    ) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
       )
-      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+      return OCPP16Constants.OCPP_RESPONSE_EMPTY
     }
-    if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) {
-      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+    const now = Date.now()
+    if (retrieveDate.getTime() <= now) {
+      this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
+    } else {
+      setTimeout(() => {
+        this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
+      }, retrieveDate.getTime() - now)
     }
-    if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) {
-      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+    return OCPP16Constants.OCPP_RESPONSE_EMPTY
+  }
+
+  private notifyRemoteStartTransactionRejected (
+    chargingStation: ChargingStation,
+    connectorId: number,
+    idTag: string
+  ): GenericResponse {
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+    logger.debug(
+      `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        chargingStation.stationInfo?.chargingStationId
+      }#${connectorId.toString()}, idTag '${idTag}', availability '${
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        connectorStatus?.availability
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+      }', status '${connectorStatus?.status}'`
+    )
+    return OCPP16Constants.OCPP_RESPONSE_REJECTED
+  }
+
+  private setRemoteStartTransactionChargingProfile (
+    chargingStation: ChargingStation,
+    connectorId: number,
+    chargingProfile: OCPP16ChargingProfile
+  ): boolean {
+    if (
+      chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+      chargingProfile.transactionId == null
+    ) {
+      OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile)
+      logger.debug(
+        `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on ${
+          // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+          chargingStation.stationInfo?.chargingStationId
+        }#${connectorId.toString()}`,
+        chargingProfile
+      )
+      return true
     }
-    if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
-      return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+    logger.debug(
+      `${chargingStation.logPrefix()} Not allowed to set ${
+        chargingProfile.chargingProfilePurpose
+      } charging profile(s)${chargingProfile.transactionId != null ? ' with transactionId set' : ''} at remote start transaction`
+    )
+    return false
+  }
+
+  private async updateFirmwareSimulation (
+    chargingStation: ChargingStation,
+    maxDelay = 30,
+    minDelay = 15
+  ): Promise<void> {
+    if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
+      return
     }
+    if (chargingStation.hasEvses) {
+      for (const [evseId, evseStatus] of chargingStation.evses) {
+        if (evseId > 0) {
+          for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+            if (connectorStatus.transactionStarted === false) {
+              await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+                chargingStation,
+                connectorId,
+                OCPP16ChargePointStatus.Unavailable
+              )
+            }
+          }
+        }
+      }
+    } else {
+      for (const connectorId of chargingStation.connectors.keys()) {
+        if (
+          connectorId > 0 &&
+          chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
+        ) {
+          await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+            chargingStation,
+            connectorId,
+            OCPP16ChargePointStatus.Unavailable
+          )
+        }
+      }
+    }
+    await chargingStation.ocppRequestService.requestHandler<
+      OCPP16FirmwareStatusNotificationRequest,
+      OCPP16FirmwareStatusNotificationResponse
+    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+      status: OCPP16FirmwareStatus.Downloading,
+    })
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
-    resetAuthorizeConnectorStatus(connectorStatus)
-    let response: OCPP16ReserveNowResponse
-    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.SuspendedEV:
-        case OCPP16ChargePointStatus.SuspendedEVSE:
-        case OCPP16ChargePointStatus.Finishing:
-          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
-          break
-        case OCPP16ChargePointStatus.Unavailable:
-          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE
-          break
-        case OCPP16ChargePointStatus.Reserved:
-          if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) {
-            response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
-            break
+    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading
+    if (
+      chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
+      OCPP16FirmwareStatus.DownloadFailed
+    ) {
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+      await chargingStation.ocppRequestService.requestHandler<
+        OCPP16FirmwareStatusNotificationRequest,
+        OCPP16FirmwareStatusNotificationResponse
+      >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+        status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
+      })
+      chargingStation.stationInfo.firmwareStatus =
+        chargingStation.stationInfo.firmwareUpgrade.failureStatus
+      return
+    }
+    await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+    await chargingStation.ocppRequestService.requestHandler<
+      OCPP16FirmwareStatusNotificationRequest,
+      OCPP16FirmwareStatusNotificationResponse
+    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+      status: OCPP16FirmwareStatus.Downloaded,
+    })
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded
+    let wasTransactionsStarted = false
+    let transactionsStarted: boolean
+    do {
+      const runningTransactions = chargingStation.getNumberOfRunningTransactions()
+      if (runningTransactions > 0) {
+        const waitTime = secondsToMilliseconds(15)
+        logger.debug(
+          `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions.toString()} transaction(s) in progress, waiting ${formatDurationMilliSeconds(
+            waitTime
+          )} before continuing firmware update simulation`
+        )
+        await sleep(waitTime)
+        transactionsStarted = true
+        wasTransactionsStarted = true
+      } else {
+        if (chargingStation.hasEvses) {
+          for (const [evseId, evseStatus] of chargingStation.evses) {
+            if (evseId > 0) {
+              for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+                if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
+                  await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+                    chargingStation,
+                    connectorId,
+                    OCPP16ChargePointStatus.Unavailable
+                  )
+                }
+              }
+            }
           }
-        // eslint-disable-next-line no-fallthrough
-        default:
-          if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
-            response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
-            break
+        } else {
+          for (const connectorId of chargingStation.connectors.keys()) {
+            if (
+              connectorId > 0 &&
+              chargingStation.getConnectorStatus(connectorId)?.status !==
+                OCPP16ChargePointStatus.Unavailable
+            ) {
+              await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+                chargingStation,
+                connectorId,
+                OCPP16ChargePointStatus.Unavailable
+              )
+            }
           }
-          await chargingStation.addReservation({
-            id: commandPayload.reservationId,
-            ...commandPayload,
-          })
-          response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED
-          break
+        }
+        transactionsStarted = false
       }
-      return response
-    } catch (error) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      return handleIncomingRequestError<OCPP16ReserveNowResponse>(
-        chargingStation,
-        OCPP16IncomingRequestCommand.RESERVE_NOW,
-        error as Error,
-        { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
-      )!
+    } while (transactionsStarted)
+    !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
+    if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
+      return
+    }
+    await chargingStation.ocppRequestService.requestHandler<
+      OCPP16FirmwareStatusNotificationRequest,
+      OCPP16FirmwareStatusNotificationResponse
+    >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+      status: OCPP16FirmwareStatus.Installing,
+    })
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing
+    if (
+      chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
+      OCPP16FirmwareStatus.InstallationFailed
+    ) {
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+      await chargingStation.ocppRequestService.requestHandler<
+        OCPP16FirmwareStatusNotificationRequest,
+        OCPP16FirmwareStatusNotificationResponse
+      >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+        status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
+      })
+      chargingStation.stationInfo.firmwareStatus =
+        chargingStation.stationInfo.firmwareUpgrade.failureStatus
+      return
+    }
+    if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
+      await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+      await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
     }
   }
 
-  private async handleRequestCancelReservation (
+  private validatePayload (
     chargingStation: ChargingStation,
-    commandPayload: OCPP16CancelReservationRequest
-  ): Promise<GenericResponse> {
+    commandName: OCPP16IncomingRequestCommand,
+    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<ReqType extends JsonType, ResType extends JsonType>(
+    chargingStation: ChargingStation,
+    messageId: string,
+    commandName: OCPP16IncomingRequestCommand,
+    commandPayload: ReqType
+  ): Promise<void> {
+    let response: ResType
     if (
-      !OCPP16ServiceUtils.checkFeatureProfile(
-        chargingStation,
-        OCPP16SupportedFeatureProfiles.Reservation,
-        OCPP16IncomingRequestCommand.CANCEL_RESERVATION
-      )
+      chargingStation.stationInfo?.ocppStrictCompliance === true &&
+      chargingStation.inPendingState() &&
+      (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
+        commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
     ) {
-      return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
+      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
+      )
     }
-    try {
-      const { reservationId } = commandPayload
-      const reservation = chargingStation.getReservationBy('reservationId', reservationId)
-      if (reservation == null) {
-        logger.debug(
-          `${chargingStation.logPrefix()} Reservation with id ${reservationId.toString()} does not exist on charging station`
+    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
         )
-        return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
       }
-      await chargingStation.removeReservation(
-        reservation,
-        ReservationTerminationReason.RESERVATION_CANCELED
+    } 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
       )
-      return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED
-    } catch (error) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      return handleIncomingRequestError<GenericResponse>(
-        chargingStation,
-        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
-        error as Error,
-        {
-          errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED,
-        }
-      )!
     }
+    // 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)
   }
 }
index 83c9608be26fafaf423d8fd5484750050c3e4bc4..655ca809f5d52aea945d5497aacb96a1da67552f 100644 (file)
@@ -3,6 +3,8 @@
 import type { ValidateFunction } from 'ajv'
 
 import type { ChargingStation } from '../../../charging-station/index.js'
+import type { OCPPResponseService } from '../OCPPResponseService.js'
+
 import { OCPPError } from '../../../exception/index.js'
 import {
   ErrorType,
@@ -25,7 +27,6 @@ import {
 } from '../../../types/index.js'
 import { Constants, generateUUID } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
-import type { OCPPResponseService } from '../OCPPResponseService.js'
 import { OCPP16Constants } from './OCPP16Constants.js'
 import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 
@@ -61,80 +62,80 @@ export class OCPP16RequestService extends OCPPRequestService {
         ),
       ],
       [
-        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+        OCPP16RequestCommand.DATA_TRANSFER,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationRequest>(
-            'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
+            'assets/json-schemas/ocpp/1.6/DataTransfer.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.HEARTBEAT,
+        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatRequest>(
-            'assets/json-schemas/ocpp/1.6/Heartbeat.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationRequest>(
+            'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.METER_VALUES,
+        OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16MeterValuesRequest>(
-            'assets/json-schemas/ocpp/1.6/MeterValues.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationRequest>(
+            'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.STATUS_NOTIFICATION,
+        OCPP16RequestCommand.HEARTBEAT,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationRequest>(
-            'assets/json-schemas/ocpp/1.6/StatusNotification.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatRequest>(
+            'assets/json-schemas/ocpp/1.6/Heartbeat.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.START_TRANSACTION,
+        OCPP16RequestCommand.METER_VALUES,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionRequest>(
-            'assets/json-schemas/ocpp/1.6/StartTransaction.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16MeterValuesRequest>(
+            'assets/json-schemas/ocpp/1.6/MeterValues.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.STOP_TRANSACTION,
+        OCPP16RequestCommand.START_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionRequest>(
-            'assets/json-schemas/ocpp/1.6/StopTransaction.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionRequest>(
+            'assets/json-schemas/ocpp/1.6/StartTransaction.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.DATA_TRANSFER,
+        OCPP16RequestCommand.STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
-            'assets/json-schemas/ocpp/1.6/DataTransfer.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationRequest>(
+            'assets/json-schemas/ocpp/1.6/StatusNotification.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+        OCPP16RequestCommand.STOP_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationRequest>(
-            'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionRequest>(
+            'assets/json-schemas/ocpp/1.6/StopTransaction.json',
             moduleName,
             'constructor'
           )
@@ -144,42 +145,6 @@ export class OCPP16RequestService extends OCPPRequestService {
     this.buildRequestPayload = this.buildRequestPayload.bind(this)
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: OCPP16RequestCommand,
-    commandParams?: RequestType,
-    params?: RequestParams
-  ): Promise<ResponseType> {
-    // 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<RequestType>(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<Request extends JsonType>(
     chargingStation: ChargingStation,
@@ -190,18 +155,18 @@ export class OCPP16RequestService extends OCPPRequestService {
     let energyActiveImportRegister: number
     commandParams = commandParams as JsonObject
     switch (commandName) {
+      case OCPP16RequestCommand.AUTHORIZE:
+        return {
+          idTag: Constants.DEFAULT_IDTAG,
+          ...commandParams,
+        } as unknown as Request
       case OCPP16RequestCommand.BOOT_NOTIFICATION:
+      case OCPP16RequestCommand.DATA_TRANSFER:
       case OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION:
       case OCPP16RequestCommand.METER_VALUES:
       case OCPP16RequestCommand.STATUS_NOTIFICATION:
-      case OCPP16RequestCommand.DATA_TRANSFER:
         return commandParams as unknown as Request
-      case OCPP16RequestCommand.AUTHORIZE:
-        return {
-          idTag: Constants.DEFAULT_IDTAG,
-          ...commandParams,
-        } as unknown as Request
       case OCPP16RequestCommand.HEARTBEAT:
         return OCPP16Constants.OCPP_REQUEST_EMPTY as unknown as Request
       case OCPP16RequestCommand.START_TRANSACTION:
@@ -265,4 +230,40 @@ export class OCPP16RequestService extends OCPPRequestService {
         )
     }
   }
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP16RequestCommand,
+    commandParams?: RequestType,
+    params?: RequestParams
+  ): Promise<ResponseType> {
+    // 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<RequestType>(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
+    )
+  }
 }
index 71c15c91681ab024cb0c92e3d69bf30385cf5d7e..0e73811661715c4fb85155422ce8e150626b8a41 100644 (file)
@@ -1,6 +1,7 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 import type { ValidateFunction } from 'ajv'
+
 import { secondsToMilliseconds } from 'date-fns'
 
 import {
@@ -58,102 +59,102 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 const moduleName = 'OCPP16ResponseService'
 
 export class OCPP16ResponseService extends OCPPResponseService {
+  protected payloadValidateFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
+
+  private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
   public incomingRequestResponsePayloadValidateFunctions: Map<
     OCPP16IncomingRequestCommand,
     ValidateFunction<JsonType>
   >
 
-  protected payloadValidateFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
-  private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
-
   public constructor () {
     // if (new.target.name === moduleName) {
     //   throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
     // }
     super(OCPPVersion.VERSION_16)
     this.responseHandlers = new Map<OCPP16RequestCommand, ResponseHandler>([
+      [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
       [
         OCPP16RequestCommand.BOOT_NOTIFICATION,
         this.handleResponseBootNotification.bind(this) as ResponseHandler,
       ],
+      [OCPP16RequestCommand.DATA_TRANSFER, this.emptyResponseHandler],
+      [
+        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+        this.emptyResponseHandler.bind(this) as ResponseHandler,
+      ],
+      [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.emptyResponseHandler],
       [OCPP16RequestCommand.HEARTBEAT, this.emptyResponseHandler],
-      [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
+      [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler],
       [
         OCPP16RequestCommand.START_TRANSACTION,
         this.handleResponseStartTransaction.bind(this) as ResponseHandler,
       ],
-      [
-        OCPP16RequestCommand.STOP_TRANSACTION,
-        this.handleResponseStopTransaction.bind(this) as ResponseHandler,
-      ],
       [
         OCPP16RequestCommand.STATUS_NOTIFICATION,
         this.emptyResponseHandler.bind(this) as ResponseHandler,
       ],
-      [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler],
       [
-        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
-        this.emptyResponseHandler.bind(this) as ResponseHandler,
+        OCPP16RequestCommand.STOP_TRANSACTION,
+        this.handleResponseStopTransaction.bind(this) as ResponseHandler,
       ],
-      [OCPP16RequestCommand.DATA_TRANSFER, this.emptyResponseHandler],
-      [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.emptyResponseHandler],
     ])
     this.payloadValidateFunctions = new Map<OCPP16RequestCommand, ValidateFunction<JsonType>>([
       [
-        OCPP16RequestCommand.BOOT_NOTIFICATION,
+        OCPP16RequestCommand.AUTHORIZE,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16BootNotificationResponse>(
-            'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16AuthorizeResponse>(
+            'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.HEARTBEAT,
+        OCPP16RequestCommand.BOOT_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatResponse>(
-            'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16BootNotificationResponse>(
+            'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.AUTHORIZE,
+        OCPP16RequestCommand.DATA_TRANSFER,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16AuthorizeResponse>(
-            'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
+            'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.START_TRANSACTION,
+        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionResponse>(
-            'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationResponse>(
+            'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.STOP_TRANSACTION,
+        OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionResponse>(
-            'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationResponse>(
+            'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.STATUS_NOTIFICATION,
+        OCPP16RequestCommand.HEARTBEAT,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationResponse>(
-            'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatResponse>(
+            'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json',
             moduleName,
             'constructor'
           )
@@ -170,30 +171,30 @@ export class OCPP16ResponseService extends OCPPResponseService {
         ),
       ],
       [
-        OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+        OCPP16RequestCommand.START_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationResponse>(
-            'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionResponse>(
+            'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.DATA_TRANSFER,
+        OCPP16RequestCommand.STATUS_NOTIFICATION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
-            'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationResponse>(
+            'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+        OCPP16RequestCommand.STOP_TRANSACTION,
         this.ajv.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationResponse>(
-            'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionResponse>(
+            'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json',
             moduleName,
             'constructor'
           )
@@ -205,60 +206,60 @@ export class OCPP16ResponseService extends OCPPResponseService {
       ValidateFunction<JsonType>
     >([
       [
-        OCPP16IncomingRequestCommand.RESET,
+        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
         this.ajvIncomingRequest.compile(
           OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
-            'assets/json-schemas/ocpp/1.6/ResetResponse.json',
+            'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CACHE,
+        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
-            'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityResponse>(
+            'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityResponse>(
-            'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationResponse>(
+            'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+        OCPP16IncomingRequestCommand.CLEAR_CACHE,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorResponse>(
-            'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
+            'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationResponse>(
-            'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileResponse>(
+            'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+        OCPP16IncomingRequestCommand.DATA_TRANSFER,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationResponse>(
-            'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
+            'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
             moduleName,
             'constructor'
           )
@@ -275,20 +276,20 @@ export class OCPP16ResponseService extends OCPPResponseService {
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
-            'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationResponse>(
+            'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileResponse>(
-            'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsResponse>(
+            'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json',
             moduleName,
             'constructor'
           )
@@ -315,60 +316,60 @@ export class OCPP16ResponseService extends OCPPResponseService {
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+        OCPP16IncomingRequestCommand.RESERVE_NOW,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsResponse>(
-            'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
+            'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        OCPP16IncomingRequestCommand.RESET,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageResponse>(
-            'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
+            'assets/json-schemas/ocpp/1.6/ResetResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.DATA_TRANSFER,
+        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
-            'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
+            'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareResponse>(
-            'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageResponse>(
+            'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.RESERVE_NOW,
+        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
-            'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorResponse>(
+            'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json',
             moduleName,
             'constructor'
           )
         ),
       ],
       [
-        OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+        OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
         this.ajvIncomingRequest.compile(
-          OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
-            'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
+          OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareResponse>(
+            'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json',
             moduleName,
             'constructor'
           )
@@ -378,124 +379,6 @@ export class OCPP16ResponseService extends OCPPResponseService {
     this.validatePayload = this.validatePayload.bind(this)
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: OCPP16RequestCommand,
-    payload: ResType,
-    requestPayload: ReqType
-  ): Promise<void> {
-    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 validatePayload (
-    chargingStation: ChargingStation,
-    commandName: OCPP16RequestCommand,
-    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
-  }
-
-  private handleResponseBootNotification (
-    chargingStation: ChargingStation,
-    payload: OCPP16BootNotificationResponse
-  ): void {
-    if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
-      chargingStation.bootNotificationResponse = payload
-      if (chargingStation.isRegistered()) {
-        chargingStation.emit(ChargingStationEvents.registered)
-        if (chargingStation.inAcceptedState()) {
-          addConfigurationKey(
-            chargingStation,
-            OCPP16StandardParametersKey.HeartbeatInterval,
-            payload.interval.toString(),
-            {},
-            { overwrite: true, save: true }
-          )
-          addConfigurationKey(
-            chargingStation,
-            OCPP16StandardParametersKey.HeartBeatInterval,
-            payload.interval.toString(),
-            { visible: false },
-            { 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 handleResponseAuthorize (
     chargingStation: ChargingStation,
     payload: OCPP16AuthorizeResponse,
@@ -552,6 +435,49 @@ export class OCPP16ResponseService extends OCPPResponseService {
     }
   }
 
+  private handleResponseBootNotification (
+    chargingStation: ChargingStation,
+    payload: OCPP16BootNotificationResponse
+  ): void {
+    if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
+      chargingStation.bootNotificationResponse = payload
+      if (chargingStation.isRegistered()) {
+        chargingStation.emit(ChargingStationEvents.registered)
+        if (chargingStation.inAcceptedState()) {
+          addConfigurationKey(
+            chargingStation,
+            OCPP16StandardParametersKey.HeartbeatInterval,
+            payload.interval.toString(),
+            {},
+            { overwrite: true, save: true }
+          )
+          addConfigurationKey(
+            chargingStation,
+            OCPP16StandardParametersKey.HeartBeatInterval,
+            payload.interval.toString(),
+            { visible: false },
+            { 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 async handleResponseStartTransaction (
     chargingStation: ChargingStation,
     payload: OCPP16StartTransactionResponse,
@@ -719,8 +645,8 @@ export class OCPP16ResponseService extends OCPPResponseService {
           OCPP16MeterValuesResponse
         >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
           connectorId,
-          transactionId: payload.transactionId,
           meterValue: [connectorStatus.transactionBeginMeterValue],
+          transactionId: payload.transactionId,
         } satisfies OCPP16MeterValuesRequest))
       await OCPP16ServiceUtils.sendAndSetConnectorStatus(
         chargingStation,
@@ -765,16 +691,6 @@ export class OCPP16ResponseService extends OCPPResponseService {
     }
   }
 
-  private async resetConnectorOnStartTransactionError (
-    chargingStation: ChargingStation,
-    connectorId: number
-  ): Promise<void> {
-    chargingStation.stopMeterValues(connectorId)
-    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
-    resetConnectorStatus(connectorStatus)
-    await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
-  }
-
   private async handleResponseStopTransaction (
     chargingStation: ChargingStation,
     payload: OCPP16StopTransactionResponse,
@@ -797,7 +713,6 @@ export class OCPP16ResponseService extends OCPPResponseService {
         OCPP16MeterValuesResponse
       >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
         connectorId: transactionConnectorId,
-        transactionId: requestPayload.transactionId,
         meterValue: [
           OCPP16ServiceUtils.buildTransactionEndMeterValue(
             chargingStation,
@@ -805,6 +720,7 @@ export class OCPP16ResponseService extends OCPPResponseService {
             requestPayload.meterStop
           ),
         ],
+        transactionId: requestPayload.transactionId,
       }))
     if (
       !chargingStation.isChargingStationAvailable() ||
@@ -842,4 +758,89 @@ export class OCPP16ResponseService extends OCPPResponseService {
       logger.warn(logMsg)
     }
   }
+
+  private async resetConnectorOnStartTransactionError (
+    chargingStation: ChargingStation,
+    connectorId: number
+  ): Promise<void> {
+    chargingStation.stopMeterValues(connectorId)
+    const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+    resetConnectorStatus(connectorStatus)
+    await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
+  }
+
+  private validatePayload (
+    chargingStation: ChargingStation,
+    commandName: OCPP16RequestCommand,
+    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<ReqType extends JsonType, ResType extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP16RequestCommand,
+    payload: ResType,
+    requestPayload: ReqType
+  ): Promise<void> {
+    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
+      )
+    }
+  }
 }
index 5d77bffbe2eaffd74a82640a05ebc0ee01092e4e..c99bf8ed301d22218dc10b80aef7d131f9577abb 100644 (file)
@@ -1,6 +1,7 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 import type { JSONSchemaType } from 'ajv'
+
 import {
   addSeconds,
   areIntervalsOverlapping,
@@ -42,78 +43,6 @@ import { OCPPServiceUtils } from '../OCPPServiceUtils.js'
 import { OCPP16Constants } from './OCPP16Constants.js'
 
 export class OCPP16ServiceUtils extends OCPPServiceUtils {
-  public static checkFeatureProfile (
-    chargingStation: ChargingStation,
-    featureProfile: OCPP16SupportedFeatureProfiles,
-    command: OCPP16RequestCommand | OCPP16IncomingRequestCommand
-  ): 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 buildTransactionBeginMeterValue (
-    chargingStation: ChargingStation,
-    connectorId: number,
-    meterStart: number | undefined
-  ): OCPP16MeterValue {
-    const meterValue: OCPP16MeterValue = {
-      timestamp: new Date(),
-      sampledValue: [],
-    }
-    // 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 remoteStopTransaction = async (
-    chargingStation: ChargingStation,
-    connectorId: number
-  ): Promise<GenericResponse> => {
-    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 changeAvailability = async (
     chargingStation: ChargingStation,
     connectorIds: number[],
@@ -145,54 +74,12 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
   }
 
-  public static setChargingProfile (
-    chargingStation: ChargingStation,
-    connectorId: number,
-    cp: OCPP16ChargingProfile
-  ): void {
-    if (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles == null) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an uninitialized charging profiles array attribute, applying deferred initialization`
-      )
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
-    }
-    if (!Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
-      logger.error(
-        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`
-      )
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
-    }
-    cp.chargingSchedule.startSchedule = convertToDate(cp.chargingSchedule.startSchedule)
-    cp.validFrom = convertToDate(cp.validFrom)
-    cp.validTo = convertToDate(cp.validTo)
-    let cpReplaced = false
-    if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      for (const [index, chargingProfile] of chargingStation
-        .getConnectorStatus(connectorId)!
-        .chargingProfiles!.entries()) {
-        if (
-          chargingProfile.chargingProfileId === cp.chargingProfileId ||
-          (chargingProfile.stackLevel === cp.stackLevel &&
-            chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
-        ) {
-          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-          chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp
-          cpReplaced = true
-        }
-      }
-    }
-    !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp)
-  }
-
   public static clearChargingProfiles = (
     chargingStation: ChargingStation,
     commandPayload: OCPP16ClearChargingProfileRequest,
     chargingProfiles: OCPP16ChargingProfile[] | undefined
   ): boolean => {
-    const { id, chargingProfilePurpose, stackLevel } = commandPayload
+    const { chargingProfilePurpose, id, stackLevel } = commandPayload
     let clearedCP = false
     if (isNotEmptyArray(chargingProfiles)) {
       chargingProfiles.forEach((chargingProfile: OCPP16ChargingProfile, index: number) => {
@@ -228,6 +115,81 @@ 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,
@@ -249,24 +211,24 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       OCPP16ServiceUtils.composeChargingSchedule(chargingScheduleLower!, compositeInterval)
     const compositeChargingScheduleHigherInterval: Interval = {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      start: compositeChargingScheduleHigher!.startSchedule!,
       end: addSeconds(
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         compositeChargingScheduleHigher!.startSchedule!,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         compositeChargingScheduleHigher!.duration!
       ),
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      start: compositeChargingScheduleHigher!.startSchedule!,
     }
     const compositeChargingScheduleLowerInterval: Interval = {
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      start: compositeChargingScheduleLower!.startSchedule!,
       end: addSeconds(
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         compositeChargingScheduleLower!.startSchedule!,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         compositeChargingScheduleLower!.duration!
       ),
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      start: compositeChargingScheduleLower!.startSchedule!,
     }
     const higherFirst = isBefore(
       compositeChargingScheduleHigherInterval.start,
@@ -282,18 +244,6 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
         ...compositeChargingScheduleLower,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         ...compositeChargingScheduleHigher!,
-        startSchedule: higherFirst
-          ? (compositeChargingScheduleHigherInterval.start as Date)
-          : (compositeChargingScheduleLowerInterval.start as Date),
-        duration: higherFirst
-          ? differenceInSeconds(
-            compositeChargingScheduleLowerInterval.end,
-            compositeChargingScheduleHigherInterval.start
-          )
-          : differenceInSeconds(
-            compositeChargingScheduleHigherInterval.end,
-            compositeChargingScheduleLowerInterval.start
-          ),
         chargingSchedulePeriod: [
           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
           ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map(schedulePeriod => {
@@ -322,24 +272,24 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
             }
           }),
         ].sort((a, b) => a.startPeriod - b.startPeriod),
+        duration: higherFirst
+          ? differenceInSeconds(
+            compositeChargingScheduleLowerInterval.end,
+            compositeChargingScheduleHigherInterval.start
+          )
+          : differenceInSeconds(
+            compositeChargingScheduleHigherInterval.end,
+            compositeChargingScheduleLowerInterval.start
+          ),
+        startSchedule: higherFirst
+          ? (compositeChargingScheduleHigherInterval.start as Date)
+          : (compositeChargingScheduleLowerInterval.start as Date),
       }
     }
     return {
       ...compositeChargingScheduleLower,
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       ...compositeChargingScheduleHigher!,
-      startSchedule: higherFirst
-        ? (compositeChargingScheduleHigherInterval.start as Date)
-        : (compositeChargingScheduleLowerInterval.start as Date),
-      duration: higherFirst
-        ? differenceInSeconds(
-          compositeChargingScheduleLowerInterval.end,
-          compositeChargingScheduleHigherInterval.start
-        )
-        : differenceInSeconds(
-          compositeChargingScheduleHigherInterval.end,
-          compositeChargingScheduleLowerInterval.start
-        ),
       chargingSchedulePeriod: [
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         ...compositeChargingScheduleHigher!.chargingSchedulePeriod.map(schedulePeriod => {
@@ -365,8 +315,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   schedulePeriod.startPeriod
                 ),
                 {
-                  start: compositeChargingScheduleLowerInterval.start,
                   end: compositeChargingScheduleHigherInterval.end,
+                  start: compositeChargingScheduleLowerInterval.start,
                 }
               )
             ) {
@@ -382,8 +332,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   schedulePeriod.startPeriod
                 ),
                 {
-                  start: compositeChargingScheduleLowerInterval.start,
                   end: compositeChargingScheduleHigherInterval.end,
+                  start: compositeChargingScheduleLowerInterval.start,
                 }
               ) &&
               isWithinInterval(
@@ -393,8 +343,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod
                 ),
                 {
-                  start: compositeChargingScheduleLowerInterval.start,
                   end: compositeChargingScheduleHigherInterval.end,
+                  start: compositeChargingScheduleLowerInterval.start,
                 }
               )
             ) {
@@ -408,8 +358,8 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
                   schedulePeriod.startPeriod
                 ),
                 {
-                  start: compositeChargingScheduleHigherInterval.start,
                   end: compositeChargingScheduleLowerInterval.end,
+                  start: compositeChargingScheduleHigherInterval.start,
                 }
               )
             ) {
@@ -433,16 +383,21 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
             }
           }),
       ].sort((a, b) => a.startPeriod - b.startPeriod),
+      duration: higherFirst
+        ? differenceInSeconds(
+          compositeChargingScheduleLowerInterval.end,
+          compositeChargingScheduleHigherInterval.start
+        )
+        : differenceInSeconds(
+          compositeChargingScheduleHigherInterval.end,
+          compositeChargingScheduleLowerInterval.start
+        ),
+      startSchedule: higherFirst
+        ? (compositeChargingScheduleHigherInterval.start as Date)
+        : (compositeChargingScheduleLowerInterval.start as Date),
     }
   }
 
-  public static isConfigurationKeyVisible (key: ConfigurationKey): boolean {
-    if (key.visible == null) {
-      return true
-    }
-    return key.visible
-  }
-
   public static hasReservation = (
     chargingStation: ChargingStation,
     connectorId: number,
@@ -470,6 +425,85 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     return false
   }
 
+  public static remoteStopTransaction = async (
+    chargingStation: ChargingStation,
+    connectorId: number
+  ): Promise<GenericResponse> => {
+    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
+    }
+    return key.visible
+  }
+
   public static parseJsonSchemaFile<T extends JsonType>(
     relativePath: string,
     moduleName?: string,
@@ -483,78 +517,45 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils {
     )
   }
 
-  private static readonly composeChargingSchedule = (
-    chargingSchedule: OCPP16ChargingSchedule,
-    compositeInterval: Interval
-  ): OCPP16ChargingSchedule | undefined => {
-    const chargingScheduleInterval: Interval = {
+  public static setChargingProfile (
+    chargingStation: ChargingStation,
+    connectorId: number,
+    cp: OCPP16ChargingProfile
+  ): void {
+    if (chargingStation.getConnectorStatus(connectorId)?.chargingProfiles == null) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an uninitialized charging profiles array attribute, applying deferred initialization`
+      )
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      start: chargingSchedule.startSchedule!,
+      chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
+    }
+    if (!Array.isArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+      logger.error(
+        `${chargingStation.logPrefix()} Trying to set a charging profile on connector id ${connectorId.toString()} with an improper attribute type for the charging profiles array, applying proper type deferred initialization`
+      )
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      end: addSeconds(chargingSchedule.startSchedule!, chargingSchedule.duration!),
+      chargingStation.getConnectorStatus(connectorId)!.chargingProfiles = []
     }
-    if (areIntervalsOverlapping(chargingScheduleInterval, compositeInterval)) {
-      chargingSchedule.chargingSchedulePeriod.sort((a, b) => a.startPeriod - b.startPeriod)
-      if (isBefore(chargingScheduleInterval.start, compositeInterval.start)) {
-        return {
-          ...chargingSchedule,
-          startSchedule: compositeInterval.start as Date,
-          duration: differenceInSeconds(
-            chargingScheduleInterval.end,
-            compositeInterval.start as Date
-          ),
-          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
-            }),
-        }
-      }
-      if (isAfter(chargingScheduleInterval.end, compositeInterval.end)) {
-        return {
-          ...chargingSchedule,
-          duration: differenceInSeconds(
-            compositeInterval.end as Date,
-            chargingScheduleInterval.start
-          ),
-          chargingSchedulePeriod: chargingSchedule.chargingSchedulePeriod.filter(schedulePeriod =>
-            isWithinInterval(
-              addSeconds(chargingScheduleInterval.start, schedulePeriod.startPeriod),
-              compositeInterval
-            )
-          ),
+    cp.chargingSchedule.startSchedule = convertToDate(cp.chargingSchedule.startSchedule)
+    cp.validFrom = convertToDate(cp.validFrom)
+    cp.validTo = convertToDate(cp.validTo)
+    let cpReplaced = false
+    if (isNotEmptyArray(chargingStation.getConnectorStatus(connectorId)?.chargingProfiles)) {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      for (const [index, chargingProfile] of chargingStation
+        .getConnectorStatus(connectorId)!
+        .chargingProfiles!.entries()) {
+        if (
+          chargingProfile.chargingProfileId === cp.chargingProfileId ||
+          (chargingProfile.stackLevel === cp.stackLevel &&
+            chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
+        ) {
+          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+          chargingStation.getConnectorStatus(connectorId)!.chargingProfiles![index] = cp
+          cpReplaced = true
         }
       }
-      return chargingSchedule
     }
+    !cpReplaced && chargingStation.getConnectorStatus(connectorId)?.chargingProfiles?.push(cp)
   }
 }
index 52302a0d774f9e9524d4fe35f74cb8fa8ed941ac..9324fea9c3ee8d61e0aa2322bd320176b284355c 100644 (file)
@@ -3,6 +3,7 @@
 import type { ValidateFunction } from 'ajv'
 
 import type { ChargingStation } from '../../../charging-station/index.js'
+
 import { OCPPError } from '../../../exception/index.js'
 import {
   ErrorType,
@@ -52,6 +53,20 @@ 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<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
@@ -139,18 +154,4 @@ 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
-  }
 }
index 13fb6a121f75032cb6d7c448d3eccef567ee57cf..4d519d427ac5bcfb5b74e20f8af521b260fee795 100644 (file)
@@ -3,6 +3,8 @@
 import type { ValidateFunction } from 'ajv'
 
 import type { ChargingStation } from '../../../charging-station/index.js'
+import type { OCPPResponseService } from '../OCPPResponseService.js'
+
 import { OCPPError } from '../../../exception/index.js'
 import {
   ErrorType,
@@ -17,7 +19,6 @@ import {
 } from '../../../types/index.js'
 import { generateUUID } from '../../../utils/index.js'
 import { OCPPRequestService } from '../OCPPRequestService.js'
-import type { OCPPResponseService } from '../OCPPResponseService.js'
 import { OCPP20Constants } from './OCPP20Constants.js'
 import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
 
@@ -66,33 +67,6 @@ export class OCPP20RequestService extends OCPPRequestService {
     this.buildRequestPayload = this.buildRequestPayload.bind(this)
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: OCPP20RequestCommand,
-    commandParams?: RequestType,
-    params?: RequestParams
-  ): Promise<ResponseType> {
-    // 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<RequestType>(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<Request extends JsonType>(
     chargingStation: ChargingStation,
@@ -121,4 +95,31 @@ export class OCPP20RequestService extends OCPPRequestService {
         )
     }
   }
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand,
+    commandParams?: RequestType,
+    params?: RequestParams
+  ): Promise<ResponseType> {
+    // 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<RequestType>(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
+    )
+  }
 }
index f5faf2b70ba447fcff2b043462f59943840912a7..dd16d9983983638b42f8219502fd92dc9fbbf0d3 100644 (file)
@@ -26,14 +26,14 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
 const moduleName = 'OCPP20ResponseService'
 
 export class OCPP20ResponseService extends OCPPResponseService {
+  protected payloadValidateFunctions: Map<OCPP20RequestCommand, ValidateFunction<JsonType>>
+
+  private readonly responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>
   public incomingRequestResponsePayloadValidateFunctions: Map<
     OCPP20IncomingRequestCommand,
     ValidateFunction<JsonType>
   >
 
-  protected payloadValidateFunctions: Map<OCPP20RequestCommand, ValidateFunction<JsonType>>
-  private readonly responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>
-
   public constructor () {
     // if (new.target.name === moduleName) {
     //   throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
@@ -97,6 +97,56 @@ 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<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
@@ -157,54 +207,4 @@ export class OCPP20ResponseService extends OCPPResponseService {
       )
     }
   }
-
-  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
-  }
-
-  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
-      )
-    }
-  }
 }
index 4b4d92566c81d1495ecf287c079cee8c29301158..6f530edf0bb3e05a2d99399d1b8931aa4002e557 100644 (file)
@@ -14,52 +14,26 @@ import { Constants } from '../../utils/index.js'
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class OCPPConstants {
-  static readonly OCPP_WEBSOCKET_TIMEOUT = 60000 // Ms
-
-  static readonly OCPP_MEASURANDS_SUPPORTED = Object.freeze([
-    MeterValueMeasurand.STATE_OF_CHARGE,
-    MeterValueMeasurand.VOLTAGE,
-    MeterValueMeasurand.POWER_ACTIVE_IMPORT,
-    MeterValueMeasurand.CURRENT_IMPORT,
-    MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
-  ])
-
-  static readonly OCPP_REQUEST_EMPTY = Constants.EMPTY_FROZEN_OBJECT
-  static readonly OCPP_RESPONSE_EMPTY = Constants.EMPTY_FROZEN_OBJECT
-  static readonly OCPP_RESPONSE_ACCEPTED = Object.freeze({
-    status: GenericStatus.Accepted,
-  })
-
-  static readonly OCPP_RESPONSE_REJECTED = Object.freeze({
-    status: GenericStatus.Rejected,
-  })
-
-  static readonly OCPP_CONFIGURATION_RESPONSE_ACCEPTED = Object.freeze({
-    status: ConfigurationStatus.ACCEPTED,
-  })
-
-  static readonly OCPP_CONFIGURATION_RESPONSE_REJECTED = Object.freeze({
-    status: ConfigurationStatus.REJECTED,
-  })
-
-  static readonly OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED = Object.freeze({
-    status: ConfigurationStatus.REBOOT_REQUIRED,
+  static readonly OCPP_AVAILABILITY_RESPONSE_ACCEPTED = Object.freeze({
+    status: AvailabilityStatus.ACCEPTED,
   })
 
-  static readonly OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED = Object.freeze({
-    status: ConfigurationStatus.NOT_SUPPORTED,
+  static readonly OCPP_AVAILABILITY_RESPONSE_REJECTED = Object.freeze({
+    status: AvailabilityStatus.REJECTED,
   })
 
-  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED = Object.freeze({
-    status: ChargingProfileStatus.ACCEPTED,
+  static readonly OCPP_AVAILABILITY_RESPONSE_SCHEDULED = Object.freeze({
+    status: AvailabilityStatus.SCHEDULED,
   })
 
-  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED = Object.freeze({
-    status: ChargingProfileStatus.REJECTED,
+  // Reservation for id has been cancelled
+  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({
+    status: GenericStatus.Accepted,
   })
 
-  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED = Object.freeze({
-    status: ChargingProfileStatus.NOT_SUPPORTED,
+  // Reservation could not be cancelled, because there is no reservation active for id
+  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED = Object.freeze({
+    status: GenericStatus.Rejected,
   })
 
   static readonly OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED = Object.freeze({
@@ -70,40 +44,20 @@ export class OCPPConstants {
     status: ClearChargingProfileStatus.UNKNOWN,
   })
 
-  static readonly OCPP_RESPONSE_UNLOCKED = Object.freeze({
-    status: UnlockStatus.UNLOCKED,
-  })
-
-  static readonly OCPP_RESPONSE_UNLOCK_FAILED = Object.freeze({
-    status: UnlockStatus.UNLOCK_FAILED,
-  })
-
-  static readonly OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED = Object.freeze({
-    status: UnlockStatus.NOT_SUPPORTED,
-  })
-
-  static readonly OCPP_AVAILABILITY_RESPONSE_ACCEPTED = Object.freeze({
-    status: AvailabilityStatus.ACCEPTED,
-  })
-
-  static readonly OCPP_AVAILABILITY_RESPONSE_REJECTED = Object.freeze({
-    status: AvailabilityStatus.REJECTED,
-  })
-
-  static readonly OCPP_AVAILABILITY_RESPONSE_SCHEDULED = Object.freeze({
-    status: AvailabilityStatus.SCHEDULED,
+  static readonly OCPP_CONFIGURATION_RESPONSE_ACCEPTED = Object.freeze({
+    status: ConfigurationStatus.ACCEPTED,
   })
 
-  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED = Object.freeze({
-    status: TriggerMessageStatus.ACCEPTED,
+  static readonly OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED = Object.freeze({
+    status: ConfigurationStatus.NOT_SUPPORTED,
   })
 
-  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED = Object.freeze({
-    status: TriggerMessageStatus.REJECTED,
+  static readonly OCPP_CONFIGURATION_RESPONSE_REBOOT_REQUIRED = Object.freeze({
+    status: ConfigurationStatus.REBOOT_REQUIRED,
   })
 
-  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED = Object.freeze({
-    status: TriggerMessageStatus.NOT_IMPLEMENTED,
+  static readonly OCPP_CONFIGURATION_RESPONSE_REJECTED = Object.freeze({
+    status: ConfigurationStatus.REJECTED,
   })
 
   static readonly OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED = Object.freeze({
@@ -118,6 +72,16 @@ export class OCPPConstants {
     status: DataTransferStatus.UNKNOWN_VENDOR_ID,
   })
 
+  static readonly OCPP_MEASURANDS_SUPPORTED = Object.freeze([
+    MeterValueMeasurand.STATE_OF_CHARGE,
+    MeterValueMeasurand.VOLTAGE,
+    MeterValueMeasurand.POWER_ACTIVE_IMPORT,
+    MeterValueMeasurand.CURRENT_IMPORT,
+    MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+  ])
+
+  static readonly OCPP_REQUEST_EMPTY = Constants.EMPTY_FROZEN_OBJECT
+
   // Reservation has been made
   static readonly OCPP_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({
     status: ReservationStatus.ACCEPTED,
@@ -143,16 +107,54 @@ export class OCPPConstants {
     status: ReservationStatus.UNAVAILABLE,
   })
 
-  // Reservation for id has been cancelled
-  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED = Object.freeze({
+  static readonly OCPP_RESPONSE_ACCEPTED = Object.freeze({
     status: GenericStatus.Accepted,
   })
 
-  // Reservation could not be cancelled, because there is no reservation active for id
-  static readonly OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED = Object.freeze({
+  static readonly OCPP_RESPONSE_EMPTY = Constants.EMPTY_FROZEN_OBJECT
+
+  static readonly OCPP_RESPONSE_REJECTED = Object.freeze({
     status: GenericStatus.Rejected,
   })
 
+  static readonly OCPP_RESPONSE_UNLOCK_FAILED = Object.freeze({
+    status: UnlockStatus.UNLOCK_FAILED,
+  })
+
+  static readonly OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED = Object.freeze({
+    status: UnlockStatus.NOT_SUPPORTED,
+  })
+
+  static readonly OCPP_RESPONSE_UNLOCKED = Object.freeze({
+    status: UnlockStatus.UNLOCKED,
+  })
+
+  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED = Object.freeze({
+    status: ChargingProfileStatus.ACCEPTED,
+  })
+
+  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED = Object.freeze({
+    status: ChargingProfileStatus.NOT_SUPPORTED,
+  })
+
+  static readonly OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED = Object.freeze({
+    status: ChargingProfileStatus.REJECTED,
+  })
+
+  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_ACCEPTED = Object.freeze({
+    status: TriggerMessageStatus.ACCEPTED,
+  })
+
+  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_NOT_IMPLEMENTED = Object.freeze({
+    status: TriggerMessageStatus.NOT_IMPLEMENTED,
+  })
+
+  static readonly OCPP_TRIGGER_MESSAGE_RESPONSE_REJECTED = Object.freeze({
+    status: TriggerMessageStatus.REJECTED,
+  })
+
+  static readonly OCPP_WEBSOCKET_TIMEOUT = 60000 // Ms
+
   protected constructor () {
     // This is intentional
   }
index a5f207f18914379b46a2216c2c71961007603bb7..01eab722a3d9dbebc01234635b3919e02526a7a5 100644 (file)
@@ -1,16 +1,16 @@
-import { EventEmitter } from 'node:events'
-
 import _Ajv, { type ValidateFunction } from 'ajv'
 import _ajvFormats from 'ajv-formats'
+import { EventEmitter } from 'node:events'
 
-import { type ChargingStation, getIdTagsFile } from '../../charging-station/index.js'
-import { OCPPError } from '../../exception/index.js'
 import type {
   ClearCacheResponse,
   IncomingRequestCommand,
   JsonType,
   OCPPVersion,
 } from '../../types/index.js'
+
+import { type ChargingStation, getIdTagsFile } from '../../charging-station/index.js'
+import { OCPPError } from '../../exception/index.js'
 import { logger } from '../../utils/index.js'
 import { OCPPConstants } from './OCPPConstants.js'
 import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
@@ -22,14 +22,15 @@ const ajvFormats = _ajvFormats.default
 const moduleName = 'OCPPIncomingRequestService'
 
 export abstract class OCPPIncomingRequestService extends EventEmitter {
-  private static instance: OCPPIncomingRequestService | null = null
-  private readonly version: OCPPVersion
+  private static instance: null | OCPPIncomingRequestService = null
   protected readonly ajv: Ajv
   protected abstract payloadValidateFunctions: Map<
     IncomingRequestCommand,
     ValidateFunction<JsonType>
   >
 
+  private readonly version: OCPPVersion
+
   protected constructor (version: OCPPVersion) {
     super()
     this.version = version
@@ -49,6 +50,14 @@ export abstract class OCPPIncomingRequestService extends EventEmitter {
     return OCPPIncomingRequestService.instance as T
   }
 
+  protected handleRequestClearCache (chargingStation: ChargingStation): ClearCacheResponse {
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    if (chargingStation.idTagsCache.deleteIdTags(getIdTagsFile(chargingStation.stationInfo!)!)) {
+      return OCPPConstants.OCPP_RESPONSE_ACCEPTED
+    }
+    return OCPPConstants.OCPP_RESPONSE_REJECTED
+  }
+
   // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
   protected validateIncomingRequestPayload<T extends JsonType>(
     chargingStation: ChargingStation,
@@ -74,14 +83,6 @@ export abstract class OCPPIncomingRequestService extends EventEmitter {
     )
   }
 
-  protected handleRequestClearCache (chargingStation: ChargingStation): ClearCacheResponse {
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    if (chargingStation.idTagsCache.deleteIdTags(getIdTagsFile(chargingStation.stationInfo!)!)) {
-      return OCPPConstants.OCPP_RESPONSE_ACCEPTED
-    }
-    return OCPPConstants.OCPP_RESPONSE_REJECTED
-  }
-
   // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unnecessary-type-parameters
   public abstract incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
index 490f9daee79f7a8efcff595e3e54544736b2602e..a8696bd581d00403bff588fadbf6c0b6378d7f5f 100644 (file)
@@ -2,6 +2,8 @@ import _Ajv, { type ValidateFunction } from 'ajv'
 import _ajvFormats from 'ajv-formats'
 
 import type { ChargingStation } from '../../charging-station/index.js'
+import type { OCPPResponseService } from './OCPPResponseService.js'
+
 import { OCPPError } from '../../exception/index.js'
 import { PerformanceStatistics } from '../../performance/index.js'
 import {
@@ -27,7 +29,6 @@ import {
   logger,
 } from '../../utils/index.js'
 import { OCPPConstants } from './OCPPConstants.js'
-import type { OCPPResponseService } from './OCPPResponseService.js'
 import {
   ajvErrorsToErrorType,
   convertDateToISOString,
@@ -42,16 +43,16 @@ const moduleName = 'OCPPRequestService'
 
 const defaultRequestParams: RequestParams = {
   skipBufferingOnError: false,
-  triggerMessage: false,
   throwError: false,
+  triggerMessage: false,
 }
 
 export abstract class OCPPRequestService {
-  private static instance: OCPPRequestService | null = null
-  private readonly version: OCPPVersion
-  private readonly ocppResponseService: OCPPResponseService
+  private static instance: null | OCPPRequestService = null
   protected readonly ajv: Ajv
   protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
+  private readonly ocppResponseService: OCPPResponseService
+  private readonly version: OCPPVersion
 
   protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) {
     this.version = version
@@ -82,61 +83,6 @@ export abstract class OCPPRequestService {
     return OCPPRequestService.instance as T
   }
 
-  public async sendResponse (
-    chargingStation: ChargingStation,
-    messageId: string,
-    messagePayload: JsonType,
-    commandName: IncomingRequestCommand
-  ): Promise<ResponseType> {
-    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
-    }
-  }
-
-  public async sendError (
-    chargingStation: ChargingStation,
-    messageId: string,
-    ocppError: OCPPError,
-    commandName: RequestCommand | IncomingRequestCommand
-  ): Promise<ResponseType> {
-    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
-    }
-  }
-
   protected async sendMessage (
     chargingStation: ChargingStation,
     messageId: string,
@@ -171,78 +117,56 @@ export abstract class OCPPRequestService {
     }
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  private validateRequestPayload<T extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: RequestCommand | IncomingRequestCommand,
-    payload: T
-  ): boolean {
-    if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
-      return true
-    }
-    if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) {
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
-      )
-      return true
-    }
-    const validate = this.payloadValidateFunctions.get(commandName as RequestCommand)
-    payload = clone<T>(payload)
-    convertDateToISOString<T>(payload)
-    if (validate?.(payload) === true) {
-      return true
-    }
-    logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
-      validate?.errors
-    )
-    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
-    throw new OCPPError(
-      ajvErrorsToErrorType(validate?.errors),
-      'Request PDU is invalid',
-      commandName,
-      JSON.stringify(validate?.errors, undefined, 2)
-    )
-  }
-
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  private validateIncomingRequestResponsePayload<T extends JsonType>(
+  private buildMessageToSend (
     chargingStation: ChargingStation,
-    commandName: RequestCommand | IncomingRequestCommand,
-    payload: T
-  ): boolean {
-    if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
-      return true
-    }
-    if (
-      !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has(
-        commandName as IncomingRequestCommand
-      )
-    ) {
-      logger.warn(
-        `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
-      )
-      return true
-    }
-    const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
-      commandName as IncomingRequestCommand
-    )
-    payload = clone<T>(payload)
-    convertDateToISOString<T>(payload)
-    if (validate?.(payload) === true) {
-      return true
+    messageId: string,
+    messagePayload: JsonType | OCPPError,
+    messageType: MessageType,
+    commandName: IncomingRequestCommand | RequestCommand
+  ): string {
+    let messageToSend: string
+    // Type of message
+    switch (messageType) {
+      // Error Message
+      case MessageType.CALL_ERROR_MESSAGE:
+        // Build Error Message
+        messageToSend = JSON.stringify([
+          messageType,
+          messageId,
+          (messagePayload as OCPPError).code,
+          (messagePayload as OCPPError).message,
+          (messagePayload as OCPPError).details ?? {
+            command: (messagePayload as OCPPError).command,
+          },
+        ] satisfies ErrorResponse)
+        break
+      // Request
+      case MessageType.CALL_MESSAGE:
+        // Build request
+        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType)
+        messageToSend = JSON.stringify([
+          messageType,
+          messageId,
+          commandName as RequestCommand,
+          messagePayload as JsonType,
+        ] satisfies OutgoingRequest)
+        break
+      // Response
+      case MessageType.CALL_RESULT_MESSAGE:
+        // Build response
+        this.validateIncomingRequestResponsePayload(
+          chargingStation,
+          commandName,
+          messagePayload as JsonType
+        )
+        messageToSend = JSON.stringify([
+          messageType,
+          messageId,
+          messagePayload as JsonType,
+        ] satisfies Response)
+        break
     }
-    logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
-      validate?.errors
-    )
-    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
-    throw new OCPPError(
-      ajvErrorsToErrorType(validate?.errors),
-      'Incoming request response PDU is invalid',
-      commandName,
-      JSON.stringify(validate?.errors, undefined, 2)
-    )
+    return messageToSend
   }
 
   private async internalSendMessage (
@@ -250,7 +174,7 @@ export abstract class OCPPRequestService {
     messageId: string,
     messagePayload: JsonType | OCPPError,
     messageType: MessageType,
-    commandName: RequestCommand | IncomingRequestCommand,
+    commandName: IncomingRequestCommand | RequestCommand,
     params?: RequestParams
   ): Promise<ResponseType> {
     params = {
@@ -406,8 +330,8 @@ export abstract class OCPPRequestService {
                   }buffered message id '${messageId}' with content '${messageToSend}'`,
                   commandName,
                   {
-                    name: error.name,
                     message: error.message,
+                    name: error.name,
                     stack: error.stack,
                   }
                 )
@@ -436,63 +360,11 @@ export abstract class OCPPRequestService {
     )
   }
 
-  private buildMessageToSend (
-    chargingStation: ChargingStation,
-    messageId: string,
-    messagePayload: JsonType | OCPPError,
-    messageType: MessageType,
-    commandName: RequestCommand | IncomingRequestCommand
-  ): string {
-    let messageToSend: string
-    // Type of message
-    switch (messageType) {
-      // Request
-      case MessageType.CALL_MESSAGE:
-        // Build request
-        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType)
-        messageToSend = JSON.stringify([
-          messageType,
-          messageId,
-          commandName as RequestCommand,
-          messagePayload as JsonType,
-        ] satisfies OutgoingRequest)
-        break
-      // Response
-      case MessageType.CALL_RESULT_MESSAGE:
-        // Build response
-        this.validateIncomingRequestResponsePayload(
-          chargingStation,
-          commandName,
-          messagePayload as JsonType
-        )
-        messageToSend = JSON.stringify([
-          messageType,
-          messageId,
-          messagePayload as JsonType,
-        ] satisfies Response)
-        break
-      // Error Message
-      case MessageType.CALL_ERROR_MESSAGE:
-        // Build Error Message
-        messageToSend = JSON.stringify([
-          messageType,
-          messageId,
-          (messagePayload as OCPPError).code,
-          (messagePayload as OCPPError).message,
-          (messagePayload as OCPPError).details ?? {
-            command: (messagePayload as OCPPError).command,
-          },
-        ] satisfies ErrorResponse)
-        break
-    }
-    return messageToSend
-  }
-
   private setCachedRequest (
     chargingStation: ChargingStation,
     messageId: string,
     messagePayload: JsonType,
-    commandName: RequestCommand | IncomingRequestCommand,
+    commandName: IncomingRequestCommand | RequestCommand,
     responseCallback: ResponseCallback,
     errorCallback: ErrorCallback
   ): void {
@@ -504,6 +376,80 @@ export abstract class OCPPRequestService {
     ])
   }
 
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  private validateIncomingRequestResponsePayload<T extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: IncomingRequestCommand | RequestCommand,
+    payload: T
+  ): boolean {
+    if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
+      return true
+    }
+    if (
+      !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has(
+        commandName as IncomingRequestCommand
+      )
+    ) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+      )
+      return true
+    }
+    const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
+      commandName as IncomingRequestCommand
+    )
+    payload = clone<T>(payload)
+    convertDateToISOString<T>(payload)
+    if (validate?.(payload) === true) {
+      return true
+    }
+    logger.error(
+      `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
+      validate?.errors
+    )
+    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+    throw new OCPPError(
+      ajvErrorsToErrorType(validate?.errors),
+      'Incoming request response PDU is invalid',
+      commandName,
+      JSON.stringify(validate?.errors, undefined, 2)
+    )
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  private validateRequestPayload<T extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: IncomingRequestCommand | RequestCommand,
+    payload: T
+  ): boolean {
+    if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
+      return true
+    }
+    if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
+      )
+      return true
+    }
+    const validate = this.payloadValidateFunctions.get(commandName as RequestCommand)
+    payload = clone<T>(payload)
+    convertDateToISOString<T>(payload)
+    if (validate?.(payload) === true) {
+      return true
+    }
+    logger.error(
+      `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
+      validate?.errors
+    )
+    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+    throw new OCPPError(
+      ajvErrorsToErrorType(validate?.errors),
+      'Request PDU is invalid',
+      commandName,
+      JSON.stringify(validate?.errors, undefined, 2)
+    )
+  }
+
   // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
   public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
@@ -511,4 +457,59 @@ export abstract class OCPPRequestService {
     commandParams?: ReqType,
     params?: RequestParams
   ): Promise<ResType>
+
+  public async sendError (
+    chargingStation: ChargingStation,
+    messageId: string,
+    ocppError: OCPPError,
+    commandName: IncomingRequestCommand | RequestCommand
+  ): Promise<ResponseType> {
+    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<ResponseType> {
+    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
+    }
+  }
 }
index 71e62083239d5177f899a83f7fff9b9ba58bde83..16bf140dc9237ff4c7190ecdfc54fa90083550b2 100644 (file)
@@ -2,13 +2,14 @@ import _Ajv, { type ValidateFunction } from 'ajv'
 import _ajvFormats from 'ajv-formats'
 
 import type { ChargingStation } from '../../charging-station/index.js'
-import { OCPPError } from '../../exception/index.js'
 import type {
   IncomingRequestCommand,
   JsonType,
   OCPPVersion,
   RequestCommand,
 } from '../../types/index.js'
+
+import { OCPPError } from '../../exception/index.js'
 import { Constants, logger } from '../../utils/index.js'
 import { ajvErrorsToErrorType } from './OCPPServiceUtils.js'
 type Ajv = _Ajv.default
@@ -19,11 +20,13 @@ const ajvFormats = _ajvFormats.default
 const moduleName = 'OCPPResponseService'
 
 export abstract class OCPPResponseService {
-  private static instance: OCPPResponseService | null = null
-  private readonly version: OCPPVersion
+  private static instance: null | OCPPResponseService = null
   protected readonly ajv: Ajv
   protected readonly ajvIncomingRequest: Ajv
+  protected emptyResponseHandler = Constants.EMPTY_FUNCTION
   protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
+  private readonly version: OCPPVersion
+
   public abstract incomingRequestResponsePayloadValidateFunctions: Map<
     IncomingRequestCommand,
     ValidateFunction<JsonType>
@@ -77,8 +80,6 @@ export abstract class OCPPResponseService {
     )
   }
 
-  protected emptyResponseHandler = Constants.EMPTY_FUNCTION
-
   // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
   public abstract responseHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
index 8737521b84a94d8ec16fa16b04c2b013e045b7e1..4fa8bbfb7bcdc95c7666c903968eedaf9ef76f5c 100644 (file)
@@ -1,11 +1,11 @@
+import type { ErrorObject, JSONSchemaType } from 'ajv'
+
+import { isDate } from 'date-fns'
 import { randomInt } from 'node:crypto'
 import { readFileSync } from 'node:fs'
 import { dirname, join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 
-import type { ErrorObject, JSONSchemaType } from 'ajv'
-import { isDate } from 'date-fns'
-
 import {
   type ChargingStation,
   getConfigurationKey,
@@ -70,12 +70,12 @@ import { OCPPConstants } from './OCPPConstants.js'
 
 export const getMessageTypeString = (messageType: MessageType | undefined): string => {
   switch (messageType) {
+    case MessageType.CALL_ERROR_MESSAGE:
+      return 'error'
     case MessageType.CALL_MESSAGE:
       return 'request'
     case MessageType.CALL_RESULT_MESSAGE:
       return 'response'
-    case MessageType.CALL_ERROR_MESSAGE:
-      return 'error'
     default:
       return 'unknown'
   }
@@ -91,17 +91,17 @@ const buildStatusNotificationRequest = (
     case OCPPVersion.VERSION_16:
       return {
         connectorId,
-        status: status as OCPP16ChargePointStatus,
         errorCode: ChargePointErrorCode.NO_ERROR,
+        status: status as OCPP16ChargePointStatus,
       } satisfies OCPP16StatusNotificationRequest
     case OCPPVersion.VERSION_20:
     case OCPPVersion.VERSION_201:
       return {
-        timestamp: new Date(),
-        connectorStatus: status as OCPP20ConnectorStatusEnumType,
         connectorId,
+        connectorStatus: status as OCPP20ConnectorStatusEnumType,
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
         evseId: evseId!,
+        timestamp: new Date(),
       } satisfies OCPP20StatusNotificationRequest
     default:
       throw new BaseError('Cannot build status notification payload: OCPP version not supported')
@@ -266,7 +266,7 @@ const checkConnectorStatusTransition = (
   return transitionAllowed
 }
 
-export const ajvErrorsToErrorType = (errors: ErrorObject[] | undefined | null): ErrorType => {
+export const ajvErrorsToErrorType = (errors: ErrorObject[] | null | undefined): ErrorType => {
   if (isNotEmptyArray(errors)) {
     for (const error of errors) {
       switch (error.keyword) {
@@ -317,8 +317,8 @@ export const buildMeterValue = (
   switch (chargingStation.stationInfo?.ocppVersion) {
     case OCPPVersion.VERSION_16:
       meterValue = {
-        timestamp: new Date(),
         sampledValue: [],
+        timestamp: new Date(),
       }
       // SoC measurand
       socSampledValueTemplate = getSampledValueTemplate(
@@ -527,9 +527,9 @@ export const buildMeterValue = (
                     connectorMaximumPower / unitDivider,
                     connectorMinimumPower / unitDivider,
                     {
+                      fallbackValue: connectorMinimumPower / unitDivider,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumPower / unitDivider,
                     }
                   ) / chargingStation.getNumberOfPhases(),
                   powerSampledValueTemplate.fluctuationPercent ??
@@ -545,9 +545,9 @@ export const buildMeterValue = (
                     connectorMaximumPowerPerPhase / unitDivider,
                     connectorMinimumPowerPerPhase / unitDivider,
                     {
+                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                     }
                   ),
                   powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
@@ -563,9 +563,9 @@ export const buildMeterValue = (
                     connectorMaximumPowerPerPhase / unitDivider,
                     connectorMinimumPowerPerPhase / unitDivider,
                     {
+                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                     }
                   ),
                   powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
@@ -581,9 +581,9 @@ export const buildMeterValue = (
                     connectorMaximumPowerPerPhase / unitDivider,
                     connectorMinimumPowerPerPhase / unitDivider,
                     {
+                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
                     }
                   ),
                   powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
@@ -619,9 +619,9 @@ export const buildMeterValue = (
                     connectorMaximumPower / unitDivider,
                     connectorMinimumPower / unitDivider,
                     {
+                      fallbackValue: connectorMinimumPower / unitDivider,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumPower / unitDivider,
                     }
                   ),
                   powerSampledValueTemplate.fluctuationPercent ??
@@ -647,9 +647,9 @@ export const buildMeterValue = (
                   connectorMaximumPower / unitDivider,
                   connectorMinimumPower / unitDivider,
                   {
+                    fallbackValue: connectorMinimumPower / unitDivider,
                     limitationEnabled:
                         chargingStation.stationInfo.customValueLimitationMeterValues,
-                    fallbackValue: connectorMinimumPower / unitDivider,
                   }
                 ),
                 powerSampledValueTemplate.fluctuationPercent ??
@@ -798,9 +798,9 @@ export const buildMeterValue = (
                     connectorMaximumAmperage,
                     connectorMinimumAmperage,
                     {
+                      fallbackValue: connectorMinimumAmperage,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumAmperage,
                     }
                   ),
                   currentSampledValueTemplate.fluctuationPercent ??
@@ -816,9 +816,9 @@ export const buildMeterValue = (
                     connectorMaximumAmperage,
                     connectorMinimumAmperage,
                     {
+                      fallbackValue: connectorMinimumAmperage,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumAmperage,
                     }
                   ),
                   currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
@@ -834,9 +834,9 @@ export const buildMeterValue = (
                     connectorMaximumAmperage,
                     connectorMinimumAmperage,
                     {
+                      fallbackValue: connectorMinimumAmperage,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumAmperage,
                     }
                   ),
                   currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
@@ -852,9 +852,9 @@ export const buildMeterValue = (
                     connectorMaximumAmperage,
                     connectorMinimumAmperage,
                     {
+                      fallbackValue: connectorMinimumAmperage,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumAmperage,
                     }
                   ),
                   currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
@@ -881,9 +881,9 @@ export const buildMeterValue = (
                     connectorMaximumAmperage,
                     connectorMinimumAmperage,
                     {
+                      fallbackValue: connectorMinimumAmperage,
                       limitationEnabled:
                           chargingStation.stationInfo.customValueLimitationMeterValues,
-                      fallbackValue: connectorMinimumAmperage,
                     }
                   ),
                   currentSampledValueTemplate.fluctuationPercent ??
@@ -912,9 +912,9 @@ export const buildMeterValue = (
                   connectorMaximumAmperage,
                   connectorMinimumAmperage,
                   {
+                    fallbackValue: connectorMinimumAmperage,
                     limitationEnabled:
                         chargingStation.stationInfo.customValueLimitationMeterValues,
-                    fallbackValue: connectorMinimumAmperage,
                   }
                 ),
                 currentSampledValueTemplate.fluctuationPercent ??
@@ -1010,8 +1010,8 @@ export const buildMeterValue = (
               connectorMaximumEnergyRounded,
               connectorMinimumEnergyRounded,
               {
-                limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
                 fallbackValue: connectorMinimumEnergyRounded,
+                limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
                 unitMultiplier: unitDivider,
               }
             ),
@@ -1080,8 +1080,8 @@ export const buildTransactionEndMeterValue = (
   switch (chargingStation.stationInfo?.ocppVersion) {
     case OCPPVersion.VERSION_16:
       meterValue = {
-        timestamp: new Date(),
         sampledValue: [],
+        timestamp: new Date(),
       }
       // Energy.Active.Import.Register measurand (default)
       sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
@@ -1129,16 +1129,16 @@ const getLimitFromSampledValueTemplateCustomValue = (
   maxLimit: number,
   minLimit: number,
   options?: {
-    limitationEnabled?: boolean
     fallbackValue?: number
+    limitationEnabled?: boolean
     unitMultiplier?: number
   }
 ): number => {
   options = {
     ...{
+      fallbackValue: 0,
       limitationEnabled: false,
       unitMultiplier: 1,
-      fallbackValue: 0,
     },
     ...options,
   }
@@ -1296,35 +1296,42 @@ const getMeasurandDefaultLocation = (
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class OCPPServiceUtils {
-  public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus
-  public static readonly restoreConnectorStatus = restoreConnectorStatus
-  public static readonly isIdTagAuthorized = isIdTagAuthorized
+  protected static buildSampledValue = buildSampledValue
   public static readonly buildTransactionEndMeterValue = buildTransactionEndMeterValue
   protected static getSampledValueTemplate = getSampledValueTemplate
-  protected static buildSampledValue = buildSampledValue
+  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 constructor () {
     // This is intentional
   }
 
-  public static isRequestCommandSupported (
+  public static isConnectorIdValid (
     chargingStation: ChargingStation,
-    command: RequestCommand
+    ocppCommand: IncomingRequestCommand,
+    connectorId: number
   ): boolean {
-    const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
-    if (
-      isRequestCommand &&
-      chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
-    ) {
-      return true
-    } else if (
-      isRequestCommand &&
-      chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
-    ) {
-      return chargingStation.stationInfo.commandsSupport.outgoingCommands[command]
+    if (connectorId < 0) {
+      logger.error(
+        `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}`
+      )
+      return false
     }
-    logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
-    return false
+    return true
   }
 
   public static isIncomingRequestCommandSupported (
@@ -1367,18 +1374,24 @@ export class OCPPServiceUtils {
     return false
   }
 
-  public static isConnectorIdValid (
+  public static isRequestCommandSupported (
     chargingStation: ChargingStation,
-    ocppCommand: IncomingRequestCommand,
-    connectorId: number
+    command: RequestCommand
   ): boolean {
-    if (connectorId < 0) {
-      logger.error(
-        `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}`
-      )
-      return false
+    const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
+    if (
+      isRequestCommand &&
+      chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
+    ) {
+      return true
+    } else if (
+      isRequestCommand &&
+      chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
+    ) {
+      return chargingStation.stationInfo.commandsSupport.outgoingCommands[command]
     }
-    return true
+    logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
+    return false
   }
 
   protected static parseJsonSchemaFile<T extends JsonType>(
@@ -1401,16 +1414,4 @@ export class OCPPServiceUtils {
       return {} as JSONSchemaType<T>
     }
   }
-
-  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)
-  }
 }
index 40be3687e6f78d395855fb9de565fc10b3904755..e9696c95956bedb7546741764880428f68b93d0f 100644 (file)
@@ -1,7 +1,9 @@
+import type { WebSocket } from 'ws'
+
 import { type IncomingMessage, Server, type ServerResponse } from 'node:http'
 import { createServer, type Http2Server } from 'node:http2'
 
-import type { WebSocket } from 'ws'
+import type { AbstractUIService } from './ui-services/AbstractUIService.js'
 
 import { BaseError } from '../../exception/index.js'
 import {
@@ -18,22 +20,22 @@ import {
   type UIServerConfiguration,
 } from '../../types/index.js'
 import { logger } from '../../utils/index.js'
-import type { AbstractUIService } from './ui-services/AbstractUIService.js'
 import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
 import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
 
 const moduleName = 'AbstractUIServer'
 
 export abstract class AbstractUIServer {
-  public readonly chargingStations: Map<string, ChargingStationData>
-  public readonly chargingStationTemplates: Set<string>
-  protected readonly httpServer: Server | Http2Server
+  protected readonly httpServer: Http2Server | Server
   protected readonly responseHandlers: Map<
     `${string}-${string}-${string}-${string}-${string}`,
     ServerResponse | WebSocket
   >
 
   protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
+  public readonly chargingStations: Map<string, ChargingStationData>
+
+  public readonly chargingStationTemplates: Set<string>
 
   public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
     this.chargingStations = new Map<string, ChargingStationData>()
@@ -58,64 +60,6 @@ export abstract class AbstractUIServer {
     this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
   }
 
-  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 stop (): void {
-    this.stopHttpServer()
-    for (const uiService of this.uiServices.values()) {
-      uiService.stop()
-    }
-    this.clearCaches()
-  }
-
-  public clearCaches (): void {
-    this.chargingStations.clear()
-    this.chargingStationTemplates.clear()
-  }
-
-  public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
-    const protocolVersion = ProtocolVersion['0.0.1']
-    this.registerProtocolVersionUIService(protocolVersion)
-    return await (this.uiServices
-      .get(protocolVersion)
-      ?.requestHandler(request) as Promise<ProtocolResponse>)
-  }
-
-  public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
-    return this.responseHandlers.has(uuid)
-  }
-
-  protected startHttpServer (): void {
-    this.httpServer.on('error', error => {
-      logger.error(
-        `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
-        error
-      )
-    })
-    if (!this.httpServer.listening) {
-      this.httpServer.listen(this.uiServerConfiguration.options)
-    }
-  }
-
-  protected registerProtocolVersionUIService (version: ProtocolVersion): void {
-    if (!this.uiServices.has(version)) {
-      this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
-    }
-  }
-
   protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
     const authorizationError = new BaseError('Unauthorized')
     if (this.isBasicAuthEnabled()) {
@@ -134,10 +78,21 @@ export abstract class AbstractUIServer {
     next()
   }
 
-  private stopHttpServer (): void {
-    if (this.httpServer.listening) {
-      this.httpServer.close()
-      this.httpServer.removeAllListeners()
+  protected registerProtocolVersionUIService (version: ProtocolVersion): void {
+    if (!this.uiServices.has(version)) {
+      this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
+    }
+  }
+
+  protected startHttpServer (): void {
+    this.httpServer.on('error', error => {
+      logger.error(
+        `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
+        error
+      )
+    })
+    if (!this.httpServer.listening) {
+      this.httpServer.listen(this.uiServerConfiguration.options)
     }
   }
 
@@ -184,8 +139,55 @@ export abstract class AbstractUIServer {
     )
   }
 
-  public abstract start (): void
+  private stopHttpServer (): void {
+    if (this.httpServer.listening) {
+      this.httpServer.close()
+      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<ProtocolResponse> {
+    const protocolVersion = ProtocolVersion['0.0.1']
+    this.registerProtocolVersionUIService(protocolVersion)
+    return await (this.uiServices
+      .get(protocolVersion)
+      ?.requestHandler(request) as Promise<ProtocolResponse>)
+  }
+
   public abstract sendRequest (request: ProtocolRequest): void
   public abstract sendResponse (response: ProtocolResponse): void
-  public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
+  public abstract start (): void
+  public stop (): void {
+    this.stopHttpServer()
+    for (const uiService of this.uiServices.values()) {
+      uiService.stop()
+    }
+    this.clearCaches()
+  }
 }
index 1682c0def3c79c25634bc4c10c8071f590a9103c..f33099b73f1bb587e3e13a21a71fe92d1a5a1572 100644 (file)
@@ -30,54 +30,12 @@ const moduleName = 'UIHttpServer'
 
 enum HttpMethods {
   GET = 'GET',
-  PUT = 'PUT',
+  PATCH = 'PATCH',
   POST = 'POST',
-  PATCH = 'PATCH'
+  PUT = 'PUT'
 }
 
 export class UIHttpServer extends AbstractUIServer {
-  public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
-    super(uiServerConfiguration)
-  }
-
-  public start (): void {
-    this.httpServer.on('request', this.requestListener.bind(this))
-    this.startHttpServer()
-  }
-
-  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 logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
     const logMsgPrefix = prefixSuffix != null ? `UI HTTP Server ${prefixSuffix}` : 'UI HTTP Server'
     const logMsg =
@@ -87,6 +45,10 @@ export class UIHttpServer extends AbstractUIServer {
     return logPrefix(logMsg)
   }
 
+  public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
+    super(uiServerConfiguration)
+  }
+
   private requestListener (req: IncomingMessage, res: ServerResponse): void {
     this.authenticate(req, err => {
       if (err != null) {
@@ -133,9 +95,9 @@ export class UIHttpServer extends AbstractUIServer {
             } catch (error) {
               this.sendResponse(
                 this.buildProtocolResponse(uuid, {
-                  status: ResponseStatus.FAILURE,
                   errorMessage: (error as Error).message,
                   errorStack: (error as Error).stack,
+                  status: ResponseStatus.FAILURE,
                 })
               )
               return
@@ -166,12 +128,50 @@ export class UIHttpServer extends AbstractUIServer {
 
   private responseStatusToStatusCode (status: ResponseStatus): StatusCodes {
     switch (status) {
-      case ResponseStatus.SUCCESS:
-        return StatusCodes.OK
       case ResponseStatus.FAILURE:
         return StatusCodes.BAD_REQUEST
+      case ResponseStatus.SUCCESS:
+        return StatusCodes.OK
       default:
         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()
+  }
 }
index f25c3aae67f9d8abf8498897a2dbb18da259632c..830476b41ec88b2d8d1bf2b8937462f9796724cc 100644 (file)
@@ -1,5 +1,7 @@
 import chalk from 'chalk'
 
+import type { AbstractUIServer } from './AbstractUIServer.js'
+
 import { BaseError } from '../../exception/index.js'
 import {
   ApplicationProtocol,
@@ -9,13 +11,21 @@ import {
   type UIServerConfiguration,
 } from '../../types/index.js'
 import { logger, logPrefix } from '../../utils/index.js'
-import type { AbstractUIServer } from './AbstractUIServer.js'
 import { UIHttpServer } from './UIHttpServer.js'
 import { isLoopback } from './UIServerUtils.js'
 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
   }
@@ -81,13 +91,4 @@ 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)
-  }
 }
index d26f59f00f04d130b72d578dfd1587658ed58b06..2e3ea7da7b437cac202be045a232ed112e1ef6c7 100644 (file)
@@ -22,7 +22,7 @@ export const getUsernameAndPasswordFromAuthorizationToken = (
 export const handleProtocols = (
   protocols: Set<string>,
   _request: IncomingMessage
-): string | false => {
+): false | string => {
   let protocol: Protocol | undefined
   let version: ProtocolVersion | undefined
   if (protocols.size === 0) {
index 4fa7972472cecb521f2150510325a60b50e2da85..2d80489f86259f052dd5f47849cff11803992815 100644 (file)
@@ -32,6 +32,16 @@ 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 readonly uiServerConfiguration: UIServerConfiguration) {
     super(uiServerConfiguration)
     this.webSocketServer = new WebSocketServer({
@@ -40,6 +50,112 @@ 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 sendRequest (request: ProtocolRequest): void {
+    this.broadcastToClients(JSON.stringify(request))
+  }
+
+  public sendResponse (response: ProtocolResponse): void {
+    const responseId = response[0]
+    try {
+      if (this.hasResponseHandler(responseId)) {
+        const ws = this.responseHandlers.get(responseId) as WebSocket
+        if (ws.readyState === WebSocket.OPEN) {
+          ws.send(JSONStringify(response, undefined, MapStringifyFormat.object))
+        } else {
+          logger.error(
+            `${this.logPrefix(
+              moduleName,
+              'sendResponse'
+            )} Error at sending response id '${responseId}', WebSocket is not open: ${ws.readyState.toString()}`
+          )
+        }
+      } else {
+        logger.error(
+          `${this.logPrefix(
+            moduleName,
+            'sendResponse'
+          )} Response for unknown request id: ${responseId}`
+        )
+      }
+    } catch (error) {
+      logger.error(
+        `${this.logPrefix(
+          moduleName,
+          'sendResponse'
+        )} Error at sending response id '${responseId}':`,
+        error
+      )
+    } finally {
+      this.responseHandlers.delete(responseId)
+    }
+  }
+
   public start (): void {
     this.webSocketServer.on('connection', (ws: WebSocket, _req: IncomingMessage): void => {
       if (!isProtocolAndVersionSupported(ws.protocol)) {
@@ -127,120 +243,4 @@ export class UIWebSocketServer extends AbstractUIServer {
     })
     this.startHttpServer()
   }
-
-  public sendRequest (request: ProtocolRequest): void {
-    this.broadcastToClients(JSON.stringify(request))
-  }
-
-  public sendResponse (response: ProtocolResponse): void {
-    const responseId = response[0]
-    try {
-      if (this.hasResponseHandler(responseId)) {
-        const ws = this.responseHandlers.get(responseId) as WebSocket
-        if (ws.readyState === WebSocket.OPEN) {
-          ws.send(JSONStringify(response, undefined, MapStringifyFormat.object))
-        } else {
-          logger.error(
-            `${this.logPrefix(
-              moduleName,
-              'sendResponse'
-            )} Error at sending response id '${responseId}', WebSocket is not open: ${ws.readyState.toString()}`
-          )
-        }
-      } else {
-        logger.error(
-          `${this.logPrefix(
-            moduleName,
-            'sendResponse'
-          )} Response for unknown request id: ${responseId}`
-        )
-      }
-    } catch (error) {
-      logger.error(
-        `${this.logPrefix(
-          moduleName,
-          'sendResponse'
-        )} Error at sending response id '${responseId}':`,
-        error
-      )
-    } finally {
-      this.responseHandlers.delete(responseId)
-    }
-  }
-
-  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)
-  }
-
-  private broadcastToClients (message: string): void {
-    for (const client of this.webSocketServer.clients) {
-      if (client.readyState === WebSocket.OPEN) {
-        client.send(message)
-      }
-    }
-  }
-
-  private validateRawDataRequest (rawData: RawData): ProtocolRequest | false {
-    // 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
-  }
 }
index baf621f1d9026523348e8317812a9342237b61de..3c3d51cbca5d47b05881981202ceb60ecd24ccaf 100644 (file)
@@ -1,3 +1,5 @@
+import type { AbstractUIServer } from '../AbstractUIServer.js'
+
 import { BaseError, type OCPPError } from '../../../exception/index.js'
 import {
   BroadcastChannelProcedureName,
@@ -20,14 +22,13 @@ import {
 import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
 import { Bootstrap } from '../../Bootstrap.js'
 import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
-import type { AbstractUIServer } from '../AbstractUIServer.js'
 
 const moduleName = 'AbstractUIService'
 
 interface AddChargingStationsRequestPayload extends RequestPayload {
-  template: string
   numberOfStations: number
   options?: ChargingStationOptions
+  template: string
 }
 
 export abstract class AbstractUIService {
@@ -35,57 +36,62 @@ export abstract class AbstractUIService {
     ProcedureName,
     BroadcastChannelProcedureName
   >([
-    [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
-    [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
+    [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
+    [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
+    [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
+    [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
     [
       ProcedureName.DELETE_CHARGING_STATIONS,
       BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
     ],
-    [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
-    [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
     [
-      ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
-      BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+      ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+      BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
     ],
     [
-      ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
-      BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+      ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+      BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
     ],
-    [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
-    [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
-    [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
-    [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
-    [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
-    [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
     [ProcedureName.HEARTBEAT, BroadcastChannelProcedureName.HEARTBEAT],
     [ProcedureName.METER_VALUES, BroadcastChannelProcedureName.METER_VALUES],
-    [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
+    [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
+    [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
     [
-      ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
-      BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+      ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+      BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
     ],
+    [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
+    [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
+    [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
     [
-      ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
-      BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+      ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+      BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
     ],
+    [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
+    [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
   ])
 
   protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>
-  private readonly version: ProtocolVersion
-  private readonly uiServer: AbstractUIServer
-  private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel
   private readonly broadcastChannelRequests: Map<
     `${string}-${string}-${string}-${string}-${string}`,
     number
   >
 
+  private readonly uiServer: AbstractUIServer
+  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
     this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
-      [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
-      [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
       [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
+      [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
+      [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
       [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
       [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
       [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
@@ -98,99 +104,6 @@ export abstract class AbstractUIService {
     >()
   }
 
-  public stop (): void {
-    this.broadcastChannelRequests.clear()
-    this.uiServiceWorkerBroadcastChannel.close()
-  }
-
-  public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
-    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
-          ) => undefined | ResponsePayload
-        )(uuid, command, requestPayload)
-      }
-    } catch (error) {
-      // Log
-      logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
-      responsePayload = {
-        hashIds: requestPayload?.hashIds,
-        status: ResponseStatus.FAILURE,
-        command,
-        requestPayload,
-        responsePayload,
-        errorMessage: (error as OCPPError).message,
-        errorStack: (error as OCPPError).stack,
-        errorDetails: (error as OCPPError).details,
-      } 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 logPrefix = (modName: string, methodName: string): string => {
-    return this.uiServer.logPrefix(modName, methodName, this.version)
-  }
-
-  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)!
-  }
-
   protected handleProtocolRequest (
     uuid: `${string}-${string}-${string}-${string}-${string}`,
     procedureName: ProcedureName,
@@ -204,79 +117,40 @@ export abstract class AbstractUIService {
     )
   }
 
-  private sendBroadcastChannelRequest (
-    uuid: `${string}-${string}-${string}-${string}-${string}`,
-    procedureName: BroadcastChannelProcedureName,
-    payload: BroadcastChannelRequestPayload
-  ): void {
-    if (isNotEmptyArray(payload.hashIds)) {
-      payload.hashIds = payload.hashIds
-        .map(hashId => {
-          if (this.uiServer.chargingStations.has(hashId)) {
-            return hashId
-          }
-          logger.warn(
-            `${this.logPrefix(
-              moduleName,
-              'sendBroadcastChannelRequest'
-            )} Charging station with hashId '${hashId}' not found`
-          )
-          return undefined
-        })
-        .filter(hashId => hashId != null)
-    } else {
-      delete payload.hashIds
-    }
-    const expectedNumberOfResponses = Array.isArray(payload.hashIds)
-      ? payload.hashIds.length
-      : this.uiServer.chargingStations.size
-    if (expectedNumberOfResponses === 0) {
-      throw new BaseError(
-        'hashIds array in the request payload does not contain any valid charging station hashId'
-      )
-    }
-    this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
-    this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
-  }
-
-  private handleListTemplates (): ResponsePayload {
-    return {
-      status: ResponseStatus.SUCCESS,
-      templates: [...this.uiServer.chargingStationTemplates.values()],
-    } satisfies ResponsePayload
-  }
-
-  private handleListChargingStations (): ResponsePayload {
-    return {
-      status: ResponseStatus.SUCCESS,
-      chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[],
-    } satisfies ResponsePayload
-  }
+  // public sendRequest (
+  //   uuid: `${string}-${string}-${string}-${string}-${string}`,
+  //   procedureName: ProcedureName,
+  //   requestPayload: RequestPayload
+  // ): void {
+  //   this.uiServer.sendRequest(
+  //     this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
+  //   )
+  // }
 
   private async handleAddChargingStations (
     _uuid?: `${string}-${string}-${string}-${string}-${string}`,
     _procedureName?: ProcedureName,
     requestPayload?: RequestPayload
   ): Promise<ResponsePayload> {
-    const { template, numberOfStations, options } =
+    const { numberOfStations, options, template } =
       requestPayload as AddChargingStationsRequestPayload
     if (!Bootstrap.getInstance().getState().started) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage:
           'Cannot add charging station(s) while the charging stations simulator is not started',
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
     if (typeof template !== 'string' || typeof numberOfStations !== 'number') {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: 'Invalid request payload',
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
     if (!this.uiServer.chargingStationTemplates.has(template)) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: `Template '${template}' not found`,
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
     const succeededStationInfos: ChargingStationInfo[] = []
@@ -312,6 +186,20 @@ export abstract class AbstractUIService {
     } satisfies ResponsePayload
   }
 
+  private handleListChargingStations (): ResponsePayload {
+    return {
+      chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[],
+      status: ResponseStatus.SUCCESS,
+    } satisfies ResponsePayload
+  }
+
+  private handleListTemplates (): ResponsePayload {
+    return {
+      status: ResponseStatus.SUCCESS,
+      templates: [...this.uiServer.chargingStationTemplates.values()],
+    } satisfies ResponsePayload
+  }
+
   private handlePerformanceStatistics (): ResponsePayload {
     if (
       Configuration.getConfigurationSection<StorageConfiguration>(
@@ -319,23 +207,23 @@ export abstract class AbstractUIService {
       ).enabled !== true
     ) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: 'Performance statistics storage is not enabled',
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
     try {
       return {
-        status: ResponseStatus.SUCCESS,
         performanceStatistics: [
           // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
           ...Bootstrap.getInstance().getPerformanceStatistics()!,
         ] as JsonType[],
+        status: ResponseStatus.SUCCESS,
       } satisfies ResponsePayload
     } catch (error) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: (error as Error).message,
         errorStack: (error as Error).stack,
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
   }
@@ -343,14 +231,14 @@ export abstract class AbstractUIService {
   private handleSimulatorState (): ResponsePayload {
     try {
       return {
-        status: ResponseStatus.SUCCESS,
         state: Bootstrap.getInstance().getState() as unknown as JsonObject,
+        status: ResponseStatus.SUCCESS,
       } satisfies ResponsePayload
     } catch (error) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: (error as Error).message,
         errorStack: (error as Error).stack,
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
   }
@@ -361,9 +249,9 @@ export abstract class AbstractUIService {
       return { status: ResponseStatus.SUCCESS }
     } catch (error) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: (error as Error).message,
         errorStack: (error as Error).stack,
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
   }
@@ -374,10 +262,124 @@ export abstract class AbstractUIService {
       return { status: ResponseStatus.SUCCESS }
     } catch (error) {
       return {
-        status: ResponseStatus.FAILURE,
         errorMessage: (error as Error).message,
         errorStack: (error as Error).stack,
+        status: ResponseStatus.FAILURE,
       } satisfies ResponsePayload
     }
   }
+
+  private sendBroadcastChannelRequest (
+    uuid: `${string}-${string}-${string}-${string}-${string}`,
+    procedureName: BroadcastChannelProcedureName,
+    payload: BroadcastChannelRequestPayload
+  ): void {
+    if (isNotEmptyArray(payload.hashIds)) {
+      payload.hashIds = payload.hashIds
+        .map(hashId => {
+          if (this.uiServer.chargingStations.has(hashId)) {
+            return hashId
+          }
+          logger.warn(
+            `${this.logPrefix(
+              moduleName,
+              'sendBroadcastChannelRequest'
+            )} Charging station with hashId '${hashId}' not found`
+          )
+          return undefined
+        })
+        .filter(hashId => hashId != null)
+    } else {
+      delete payload.hashIds
+    }
+    const expectedNumberOfResponses = Array.isArray(payload.hashIds)
+      ? payload.hashIds.length
+      : this.uiServer.chargingStations.size
+    if (expectedNumberOfResponses === 0) {
+      throw new BaseError(
+        'hashIds array in the request payload does not contain any valid charging station hashId'
+      )
+    }
+    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<ProtocolResponse | undefined> {
+    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()
+  }
 }
index 511bfa7a3de64383c7302584a5c4c6f42d155157..515536696704a4753ad5d8c3560c5ea07e4cc377 100644 (file)
@@ -1,5 +1,6 @@
-import { type ProtocolRequestHandler, ProtocolVersion } from '../../../types/index.js'
 import type { AbstractUIServer } from '../AbstractUIServer.js'
+
+import { type ProtocolRequestHandler, ProtocolVersion } from '../../../types/index.js'
 import { AbstractUIService } from './AbstractUIService.js'
 
 export class UIService001 extends AbstractUIService {
index 08d81a360bdc669dac2d6fc2fd009d09a8e306bd..de566fc7229bd632581e83abcc784ef7ae1952ce 100644 (file)
@@ -1,6 +1,7 @@
-import { ProtocolVersion } from '../../../types/index.js'
 import type { AbstractUIServer } from '../AbstractUIServer.js'
 import type { AbstractUIService } from './AbstractUIService.js'
+
+import { ProtocolVersion } from '../../../types/index.js'
 import { UIService001 } from './UIService001.js'
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
index 1e716ae4d1d8f7c530e489a7fb37151f5fba8644..ece0283b81218d2bb95f5683dbed26c3bf843a5f 100644 (file)
@@ -1,18 +1,19 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 import type { ErrorType, IncomingRequestCommand, JsonType, RequestCommand } from '../types/index.js'
+
 import { Constants } from '../utils/index.js'
 import { BaseError } from './BaseError.js'
 
 export class OCPPError extends BaseError {
   code: ErrorType
-  command: RequestCommand | IncomingRequestCommand
+  command: IncomingRequestCommand | RequestCommand
   details?: JsonType
 
   constructor (
     code: ErrorType,
     message: string,
-    command?: RequestCommand | IncomingRequestCommand,
+    command?: IncomingRequestCommand | RequestCommand,
     details?: JsonType
   ) {
     super(message)
index de5503f488dbdeec71ee8995ea5334551ce33005..15b338812f62463deea4e84a1bc9b2c589bebc5f 100644 (file)
@@ -1,11 +1,11 @@
 // Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
-import { performance, type PerformanceEntry, PerformanceObserver } from 'node:perf_hooks'
 import type { URL } from 'node:url'
-import { parentPort } from 'node:worker_threads'
 
 import { secondsToMilliseconds } from 'date-fns'
 import { CircularBuffer } from 'mnemonist'
+import { performance, type PerformanceEntry, PerformanceObserver } from 'node:perf_hooks'
+import { parentPort } from 'node:worker_threads'
 import { is, mean, median } from 'rambda'
 
 import { BaseError } from '../exception/index.js'
@@ -43,49 +43,40 @@ 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 displayInterval?: NodeJS.Timeout
 
   private constructor (objId: string, objName: string, uri: URL) {
     this.objId = objId
     this.objName = objName
     this.initializePerformanceObserver()
     this.statistics = {
+      createdAt: new Date(),
       id: this.objId,
       name: this.objName,
-      uri: uri.toString(),
-      createdAt: new Date(),
       statisticsData: new Map(),
+      uri: uri.toString(),
     }
   }
 
-  public static getInstance (
-    objId: string | undefined,
-    objName: string | undefined,
-    uri: URL | undefined
-  ): PerformanceStatistics | undefined {
-    if (objId == null) {
-      const errMsg = 'Cannot get performance statistics instance without specifying object id'
-      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
-      throw new BaseError(errMsg)
-    }
-    if (objName == null) {
-      const errMsg = 'Cannot get performance statistics instance without specifying object name'
-      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
-      throw new BaseError(errMsg)
-    }
-    if (uri == null) {
-      const errMsg = 'Cannot get performance statistics instance without specifying object uri'
-      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
-      throw new BaseError(errMsg)
-    }
-    if (!PerformanceStatistics.instances.has(objId)) {
-      PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri))
-    }
-    return PerformanceStatistics.instances.get(objId)
+  public static beginMeasure (id: string): string {
+    const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
+    performance.mark(markId)
+    return markId
   }
 
   public static deleteInstance (objId: string | undefined): boolean {
@@ -97,12 +88,6 @@ export class PerformanceStatistics {
     return PerformanceStatistics.instances.delete(objId)
   }
 
-  public static beginMeasure (id: string): string {
-    const markId = `${id.charAt(0).toUpperCase()}${id.slice(1)}~${generateUUID()}`
-    performance.mark(markId)
-    return markId
-  }
-
   public static endMeasure (name: string, markId: string): void {
     try {
       performance.measure(name, markId)
@@ -117,142 +102,30 @@ export class PerformanceStatistics {
     performance.clearMeasures(name)
   }
 
-  public addRequestStatistic (
-    command: RequestCommand | IncomingRequestCommand,
-    messageType: MessageType
-  ): void {
-    switch (messageType) {
-      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
-      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
-      default:
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        logger.error(`${this.logPrefix()} wrong message type ${messageType}`)
-        break
+  public static getInstance (
+    objId: string | undefined,
+    objName: string | undefined,
+    uri: undefined | URL
+  ): PerformanceStatistics | undefined {
+    if (objId == null) {
+      const errMsg = 'Cannot get performance statistics instance without specifying object id'
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+      throw new BaseError(errMsg)
     }
-  }
-
-  public start (): void {
-    this.startLogStatisticsInterval()
-    const performanceStorageConfiguration =
-      Configuration.getConfigurationSection<StorageConfiguration>(
-        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}`
-      )
+    if (objName == null) {
+      const errMsg = 'Cannot get performance statistics instance without specifying object name'
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+      throw new BaseError(errMsg)
     }
-  }
-
-  public stop (): void {
-    this.stopLogStatisticsInterval()
-    performance.clearMarks()
-    performance.clearMeasures()
-    this.performanceObserver.disconnect()
-  }
-
-  public restart (): void {
-    this.stop()
-    this.start()
-  }
-
-  private initializePerformanceObserver (): void {
-    this.performanceObserver = new PerformanceObserver(performanceObserverList => {
-      const lastPerformanceEntry = performanceObserverList.getEntries()[0]
-      // logger.debug(
-      //   `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
-      //   lastPerformanceEntry
-      // )
-      this.addPerformanceEntryToStatistics(lastPerformanceEntry)
-    })
-    this.performanceObserver.observe({ entryTypes: ['measure'] })
-  }
-
-  private logStatistics (): void {
-    logger.info(this.logPrefix(), {
-      ...this.statistics,
-      statisticsData: JSON.parse(
-        JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
-      ) as Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>,
-    })
-  }
-
-  private startLogStatisticsInterval (): void {
-    const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
-      ConfigurationSection.log
-    )
-    const logStatisticsInterval =
-      logConfiguration.enabled === true
-        ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        logConfiguration.statisticsInterval!
-        : 0
-    if (logStatisticsInterval > 0 && this.displayInterval == null) {
-      this.displayInterval = setInterval(() => {
-        this.logStatistics()
-      }, secondsToMilliseconds(logStatisticsInterval))
-      logger.info(
-        `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
-      )
-    } else if (this.displayInterval != null) {
-      logger.info(
-        `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
-      )
-    } else if (logConfiguration.enabled === true) {
-      logger.info(
-        `${this.logPrefix()} log interval is set to ${logStatisticsInterval.toString()}. Not logging statistics`
-      )
+    if (uri == null) {
+      const errMsg = 'Cannot get performance statistics instance without specifying object uri'
+      logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+      throw new BaseError(errMsg)
     }
-  }
-
-  private stopLogStatisticsInterval (): void {
-    if (this.displayInterval != null) {
-      clearInterval(this.displayInterval)
-      delete this.displayInterval
+    if (!PerformanceStatistics.instances.has(objId)) {
+      PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri))
     }
+    return PerformanceStatistics.instances.get(objId)
   }
 
   private addPerformanceEntryToStatistics (entry: PerformanceEntry): void {
@@ -325,12 +198,141 @@ export class PerformanceStatistics {
     }
   }
 
-  private static readonly logPrefix = (): string => {
-    return logPrefix(' Performance statistics')
+  private initializePerformanceObserver (): void {
+    this.performanceObserver = new PerformanceObserver(performanceObserverList => {
+      const lastPerformanceEntry = performanceObserverList.getEntries()[0]
+      // logger.debug(
+      //   `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
+      //   lastPerformanceEntry
+      // )
+      this.addPerformanceEntryToStatistics(lastPerformanceEntry)
+    })
+    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,
+      statisticsData: JSON.parse(
+        JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
+      ) as Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>,
+    })
+  }
+
+  private startLogStatisticsInterval (): void {
+    const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+      ConfigurationSection.log
+    )
+    const logStatisticsInterval =
+      logConfiguration.enabled === true
+        ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        logConfiguration.statisticsInterval!
+        : 0
+    if (logStatisticsInterval > 0 && this.displayInterval == null) {
+      this.displayInterval = setInterval(() => {
+        this.logStatistics()
+      }, secondsToMilliseconds(logStatisticsInterval))
+      logger.info(
+        `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
+      )
+    } else if (this.displayInterval != null) {
+      logger.info(
+        `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
+      )
+    } else if (logConfiguration.enabled === true) {
+      logger.info(
+        `${this.logPrefix()} log interval is set to ${logStatisticsInterval.toString()}. Not logging statistics`
+      )
+    }
+  }
+
+  private stopLogStatisticsInterval (): void {
+    if (this.displayInterval != null) {
+      clearInterval(this.displayInterval)
+      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<StorageConfiguration>(
+        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()
   }
 }
index 97ab104ccfb73a11fb52aafd8ca408eeaf1ff84f..e76bdb2fce31b2e6f3dde72c5477a5806f301f4c 100644 (file)
@@ -16,25 +16,29 @@ export class JsonFileStorage extends Storage {
     this.dbName = this.storageUri.pathname
   }
 
-  public storePerformanceStatistics (performanceStatistics: Statistics): void {
-    this.setPerformanceStatistics(performanceStatistics)
-    this.checkPerformanceRecordsFile()
-    AsyncLock.runExclusive(AsyncLockType.performance, () => {
-      writeSync(
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        this.fd!,
-        JSONStringify([...this.getPerformanceStatistics()], 2, MapStringifyFormat.object),
-        0,
-        'utf8'
+  private checkPerformanceRecordsFile (): void {
+    if (this.fd == null) {
+      throw new BaseError(
+        `${this.logPrefix} Performance records '${this.dbName}' file descriptor not found`
       )
-    }).catch((error: unknown) => {
+    }
+  }
+
+  public close (): void {
+    this.clearPerformanceStatistics()
+    try {
+      if (this.fd != null) {
+        closeSync(this.fd)
+        delete this.fd
+      }
+    } catch (error) {
       handleFileException(
         this.dbName,
         FileType.PerformanceRecords,
         error as NodeJS.ErrnoException,
         this.logPrefix
       )
-    })
+    }
   }
 
   public open (): void {
@@ -55,28 +59,24 @@ export class JsonFileStorage extends Storage {
     }
   }
 
-  public close (): void {
-    this.clearPerformanceStatistics()
-    try {
-      if (this.fd != null) {
-        closeSync(this.fd)
-        delete this.fd
-      }
-    } catch (error) {
+  public storePerformanceStatistics (performanceStatistics: Statistics): void {
+    this.setPerformanceStatistics(performanceStatistics)
+    this.checkPerformanceRecordsFile()
+    AsyncLock.runExclusive(AsyncLockType.performance, () => {
+      writeSync(
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        this.fd!,
+        JSONStringify([...this.getPerformanceStatistics()], 2, MapStringifyFormat.object),
+        0,
+        'utf8'
+      )
+    }).catch((error: unknown) => {
       handleFileException(
         this.dbName,
         FileType.PerformanceRecords,
         error as NodeJS.ErrnoException,
         this.logPrefix
       )
-    }
-  }
-
-  private checkPerformanceRecordsFile (): void {
-    if (this.fd == null) {
-      throw new BaseError(
-        `${this.logPrefix} Performance records '${this.dbName}' file descriptor not found`
-      )
-    }
+    })
   }
 }
index 7d7beedea4b695cffb1831d659f797cfdf281cef..970ac57ce28ca16fc711a58ca012c7cc66394a5d 100644 (file)
@@ -1,15 +1,15 @@
 // Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
-import { MikroORM as MariaDbORM, type Options as MariaDbOptions } from '@mikro-orm/mariadb'
-import { MikroORM as SqliteORM, type Options as SqliteOptions } from '@mikro-orm/sqlite'
+import { type Options as MariaDbOptions, MikroORM as MariaDbORM } from '@mikro-orm/mariadb'
+import { type Options as SqliteOptions, MikroORM as SqliteORM } from '@mikro-orm/sqlite'
 
 import { type PerformanceRecord, type Statistics, StorageType } from '../../types/index.js'
 import { Constants } from '../../utils/index.js'
 import { Storage } from './Storage.js'
 
 export class MikroOrmStorage extends Storage {
+  private orm?: MariaDbORM | SqliteORM
   private readonly storageType: StorageType
-  private orm?: SqliteORM | MariaDbORM
 
   constructor (storageUri: string, logPrefix: string, storageType: StorageType) {
     super(storageUri, logPrefix)
@@ -17,22 +17,40 @@ export class MikroOrmStorage extends Storage {
     this.dbName = this.getDBName()
   }
 
-  public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
+  private getClientUrl (): string | undefined {
+    switch (this.storageType) {
+      case StorageType.SQLITE:
+      case StorageType.MARIA_DB:
+      case StorageType.MYSQL:
+        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<void> {
+    this.clearPerformanceStatistics()
     try {
-      this.setPerformanceStatistics(performanceStatistics)
-      await this.orm?.em.upsert({
-        ...performanceStatistics,
-        statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({
-          name,
-          ...value,
-        })),
-      } satisfies PerformanceRecord)
+      if (this.orm != null) {
+        await this.orm.close()
+        delete this.orm
+      }
     } catch (error) {
-      this.handleDBStorageError(
-        this.storageType,
-        error as Error,
-        Constants.PERFORMANCE_RECORDS_TABLE
-      )
+      this.handleDBStorageError(this.storageType, error as Error)
     }
   }
 
@@ -54,40 +72,22 @@ export class MikroOrmStorage extends Storage {
     }
   }
 
-  public async close (): Promise<void> {
-    this.clearPerformanceStatistics()
+  public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
     try {
-      if (this.orm != null) {
-        await this.orm.close()
-        delete this.orm
-      }
+      this.setPerformanceStatistics(performanceStatistics)
+      await this.orm?.em.upsert({
+        ...performanceStatistics,
+        statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({
+          name,
+          ...value,
+        })),
+      } satisfies PerformanceRecord)
     } catch (error) {
-      this.handleDBStorageError(this.storageType, error as Error)
-    }
-  }
-
-  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 (): SqliteOptions | MariaDbOptions {
-    return {
-      dbName: this.dbName,
-      entities: ['./dist/types/orm/entities/*.js'],
-      entitiesTs: ['./src/types/orm/entities/*.ts'],
-      clientUrl: this.getClientUrl(),
-    }
-  }
-
-  private getClientUrl (): string | undefined {
-    switch (this.storageType) {
-      case StorageType.SQLITE:
-      case StorageType.MARIA_DB:
-      case StorageType.MYSQL:
-        return this.storageUri.toString()
+      this.handleDBStorageError(
+        this.storageType,
+        error as Error,
+        Constants.PERFORMANCE_RECORDS_TABLE
+      )
     }
   }
 }
index 5635e2b29cfc20ae65d0991757201fb6342ba038..ef48f3bd33a44f381e806ce662a3ce953d470d0c 100644 (file)
@@ -18,33 +18,22 @@ export class MongoDBStorage extends Storage {
     this.dbName = this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '')
   }
 
-  public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
-    try {
-      this.setPerformanceStatistics(performanceStatistics)
-      this.checkDBConnection()
-      await this.client
-        ?.db(this.dbName)
-        .collection<Statistics>(Constants.PERFORMANCE_RECORDS_TABLE)
-        .replaceOne({ id: performanceStatistics.id }, performanceStatistics, {
-          upsert: true,
-        })
-    } catch (error) {
-      this.handleDBStorageError(
-        StorageType.MONGO_DB,
-        error as Error,
-        Constants.PERFORMANCE_RECORDS_TABLE
+  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`
       )
     }
-  }
-
-  public async open (): Promise<void> {
-    try {
-      if (!this.connected && this.client != null) {
-        await this.client.connect()
-        this.connected = true
-      }
-    } catch (error) {
-      this.handleDBStorageError(StorageType.MONGO_DB, error as Error)
+    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`
+      )
     }
   }
 
@@ -60,21 +49,32 @@ 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`
-      )
+  public async open (): Promise<void> {
+    try {
+      if (!this.connected && this.client != null) {
+        await this.client.connect()
+        this.connected = true
+      }
+    } catch (error) {
+      this.handleDBStorageError(StorageType.MONGO_DB, error as Error)
     }
-    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 storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
+    try {
+      this.setPerformanceStatistics(performanceStatistics)
+      this.checkDBConnection()
+      await this.client
+        ?.db(this.dbName)
+        .collection<Statistics>(Constants.PERFORMANCE_RECORDS_TABLE)
+        .replaceOne({ id: performanceStatistics.id }, performanceStatistics, {
+          upsert: true,
+        })
+    } catch (error) {
+      this.handleDBStorageError(
+        StorageType.MONGO_DB,
+        error as Error,
+        Constants.PERFORMANCE_RECORDS_TABLE
       )
     }
   }
index aae7547f5b32afb656c023106c01babd846205b4..ceda1ab57c8b6c7ad33e90dca5d1b0a9cbabe0c7 100644 (file)
@@ -1,6 +1,7 @@
 // Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
 import type { Statistics } from '../../types/index.js'
+
 import { Storage } from './Storage.js'
 
 export class None extends Storage {
@@ -8,15 +9,15 @@ export class None extends Storage {
     super('none://none', 'none')
   }
 
-  public storePerformanceStatistics (performanceStatistics: Statistics): void {
-    this.setPerformanceStatistics(performanceStatistics)
+  public close (): void {
+    this.clearPerformanceStatistics()
   }
 
   public open (): void {
     /** Intentionally empty */
   }
 
-  public close (): void {
-    this.clearPerformanceStatistics()
+  public storePerformanceStatistics (performanceStatistics: Statistics): void {
+    this.setPerformanceStatistics(performanceStatistics)
   }
 }
index 475c3f5d589dc47d5595423732a05817d6d9d227..0e21515c1c0a80987e47e4ad37b38e711e4a6a1c 100644 (file)
@@ -12,29 +12,46 @@ import {
 import { logger } from '../../utils/index.js'
 
 export abstract class Storage {
-  protected readonly storageUri: URL
-  protected readonly logPrefix: string
-  protected dbName!: string
   private static readonly performanceStatistics = new Map<string, Statistics>()
+  protected dbName!: string
+  protected readonly logPrefix: string
+  protected readonly storageUri: URL
 
   constructor (storageUri: string, logPrefix: string) {
     this.storageUri = new URL(storageUri)
     this.logPrefix = logPrefix
   }
 
+  protected clearPerformanceStatistics (): void {
+    Storage.performanceStatistics.clear()
+  }
+
+  protected getDBNameFromStorageType (type: StorageType): DBName | undefined {
+    switch (type) {
+      case StorageType.MARIA_DB:
+        return DBName.MARIA_DB
+      case StorageType.MONGO_DB:
+        return DBName.MONGO_DB
+      case StorageType.MYSQL:
+        return DBName.MYSQL
+      case StorageType.SQLITE:
+        return DBName.SQLITE
+    }
+  }
+
   protected handleDBStorageError (
     type: StorageType,
     error: Error,
     table?: string,
     params: HandleErrorParams<EmptyObject> = {
-      throwError: false,
       consoleOut: false,
+      throwError: false,
     }
   ): void {
     params = {
       ...{
-        throwError: false,
         consoleOut: false,
+        throwError: false,
       },
       ...params,
     }
@@ -52,34 +69,17 @@ export abstract class Storage {
     }
   }
 
-  protected getDBNameFromStorageType (type: StorageType): DBName | undefined {
-    switch (type) {
-      case StorageType.SQLITE:
-        return DBName.SQLITE
-      case StorageType.MARIA_DB:
-        return DBName.MARIA_DB
-      case StorageType.MYSQL:
-        return DBName.MYSQL
-      case StorageType.MONGO_DB:
-        return DBName.MONGO_DB
-    }
-  }
-
-  public getPerformanceStatistics (): IterableIterator<Statistics> {
-    return Storage.performanceStatistics.values()
-  }
-
   protected setPerformanceStatistics (performanceStatistics: Statistics): void {
     Storage.performanceStatistics.set(performanceStatistics.id, performanceStatistics)
   }
 
-  protected clearPerformanceStatistics (): void {
-    Storage.performanceStatistics.clear()
-  }
+  public abstract close (): Promise<void> | void
 
-  public abstract open (): void | Promise<void>
-  public abstract close (): void | Promise<void>
+  public getPerformanceStatistics (): IterableIterator<Statistics> {
+    return Storage.performanceStatistics.values()
+  }
+  public abstract open (): Promise<void> | void
   public abstract storePerformanceStatistics (
     performanceStatistics: Statistics
-  ): void | Promise<void>
+  ): Promise<void> | void
 }
index 2b6b84ffe5b8ad75da53fc7c5fa9687b0e0089cd..277e6420420135f98b87729f637b3f29bf127787 100644 (file)
@@ -1,12 +1,13 @@
 // Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
 
+import type { Storage } from './Storage.js'
+
 import { BaseError } from '../../exception/index.js'
 import { StorageType } from '../../types/index.js'
 import { JsonFileStorage } from './JsonFileStorage.js'
 import { MikroOrmStorage } from './MikroOrmStorage.js'
 import { MongoDBStorage } from './MongoDBStorage.js'
 import { None } from './None.js'
-import type { Storage } from './Storage.js'
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class StorageFactory {
@@ -24,14 +25,14 @@ export class StorageFactory {
       case StorageType.JSON_FILE:
         storageInstance = new JsonFileStorage(connectionUri, logPrefix)
         break
-      case StorageType.MONGO_DB:
-        storageInstance = new MongoDBStorage(connectionUri, logPrefix)
-        break
-      case StorageType.SQLITE:
       case StorageType.MARIA_DB:
       case StorageType.MYSQL:
+      case StorageType.SQLITE:
         storageInstance = new MikroOrmStorage(connectionUri, logPrefix, type)
         break
+      case StorageType.MONGO_DB:
+        storageInstance = new MongoDBStorage(connectionUri, logPrefix)
+        break
       case StorageType.NONE:
         storageInstance = new None()
         break
index 52349887e6f1c4b460802980796979aab1c604c4..149d949504678fe26f9c8b336c8866585cb10eaf 100755 (executable)
@@ -1,6 +1,5 @@
-const fs = require('node:fs')
-
 const { MongoClient } = require('mongodb')
+const fs = require('node:fs')
 
 // This script deletes charging stations
 // Filter charging stations by id pattern
index 8aa428ad1499c576cc4e517ca4c9f60331c69c18..8b9e913f39499da7f411582016877fb859fea976 100755 (executable)
@@ -1,6 +1,5 @@
-const fs = require('node:fs')
-
 const { MongoClient } = require('mongodb')
+const fs = require('node:fs')
 
 // This script sets charging stations public or private
 // Filter charging stations by id pattern
index 2ff04c09096a42fcc8e33b2a8bb937fa84fd4cc8..8c7363845c21b6c76cf388946b6b199f56d7af84 100644 (file)
@@ -1,41 +1,41 @@
 import type { JsonObject } from './JsonType.js'
 
 export enum IdTagDistribution {
+  CONNECTOR_AFFINITY = 'connector-affinity',
   RANDOM = 'random',
-  ROUND_ROBIN = 'round-robin',
-  CONNECTOR_AFFINITY = 'connector-affinity'
+  ROUND_ROBIN = 'round-robin'
 }
 
 export interface AutomaticTransactionGeneratorConfiguration extends JsonObject {
   enable: boolean
-  minDuration: number
+  idTagDistribution?: IdTagDistribution
+  maxDelayBetweenTwoTransactions: number
   maxDuration: number
   minDelayBetweenTwoTransactions: number
-  maxDelayBetweenTwoTransactions: number
+  minDuration: number
   probabilityOfStart: number
-  stopAfterHours: number
-  stopAbsoluteDuration: boolean
   requireAuthorize?: boolean
-  idTagDistribution?: IdTagDistribution
+  stopAbsoluteDuration: boolean
+  stopAfterHours: number
 }
 
 export interface Status {
-  start: boolean
-  startDate?: Date
-  lastRunDate?: Date
-  stopDate?: Date
-  stoppedDate?: Date
-  authorizeRequests: number
   acceptedAuthorizeRequests: number
-  rejectedAuthorizeRequests: number
-  startTransactionRequests: number
   acceptedStartTransactionRequests: number
-  rejectedStartTransactionRequests: number
-  stopTransactionRequests: number
   acceptedStopTransactionRequests: number
+  authorizeRequests: number
+  lastRunDate?: Date
+  rejectedAuthorizeRequests: number
+  rejectedStartTransactionRequests: number
   rejectedStopTransactionRequests: number
   skippedConsecutiveTransactions: number
   skippedTransactions: number
+  start: boolean
+  startDate?: Date
+  startTransactionRequests: number
+  stopDate?: Date
+  stoppedDate?: Date
+  stopTransactionRequests: number
 }
 
 export interface ChargingStationAutomaticTransactionGeneratorConfiguration {
index 630e1bd0cb1c55666b362d173c272f91e86af32b..1363ccbf30084860004880e6e9f8884c2790fc0a 100644 (file)
@@ -8,18 +8,18 @@ interface ConnectorsConfiguration {
   connectorsStatus?: ConnectorStatus[]
 }
 
-export type EvseStatusConfiguration = Omit<EvseStatus, 'connectors'> & {
+export type EvseStatusConfiguration = {
   connectorsStatus?: ConnectorStatus[]
-}
+} & Omit<EvseStatus, 'connectors'>
 
 interface EvsesConfiguration {
   evsesStatus?: EvseStatusConfiguration[]
 }
 
-export type ChargingStationConfiguration = ChargingStationInfoConfiguration &
+export type ChargingStationConfiguration = {
+  configurationHash?: string
+} & ChargingStationAutomaticTransactionGeneratorConfiguration &
+  ChargingStationInfoConfiguration &
   ChargingStationOcppConfiguration &
-  ChargingStationAutomaticTransactionGeneratorConfiguration &
   ConnectorsConfiguration &
-  EvsesConfiguration & {
-    configurationHash?: string
-  }
+  EvsesConfiguration
index 3fa6adeccf6310ebebac3dbb679133dfe9f10aa3..4b36805fcdf9033e06819b5ba19b68235209977c 100644 (file)
@@ -1,13 +1,13 @@
 export enum ChargingStationEvents {
+  accepted = 'accepted',
   added = 'added',
+  connected = 'connected',
+  connectorStatusChanged = 'connectorStatusChanged',
   deleted = 'deleted',
-  started = 'started',
-  stopped = 'stopped',
-  updated = 'updated',
+  disconnected = 'disconnected',
   registered = 'registered',
-  accepted = 'accepted',
   rejected = 'rejected',
-  connected = 'connected',
-  disconnected = 'disconnected',
-  connectorStatusChanged = 'connectorStatusChanged'
+  started = 'started',
+  stopped = 'stopped',
+  updated = 'updated'
 }
index 8f4966f9f4d66178fe159e9e7f70c5b033df4962..b1fb3454a802ddd218fdad37a38d955a4547b3be 100644 (file)
@@ -1,32 +1,32 @@
 import type { ChargingStationTemplate } from './ChargingStationTemplate.js'
 import type { FirmwareStatus } from './ocpp/Requests.js'
 
-export type ChargingStationInfo = Omit<
+export type ChargingStationInfo = {
+  chargeBoxSerialNumber?: string
+  chargePointSerialNumber?: string
+  chargingStationId?: string
+  firmwareStatus?: FirmwareStatus
+  hashId: string
+  /** @deprecated Use `hashId` instead. */
+  infoHash?: string
+  maximumAmperage?: number // Always in Ampere
+  maximumPower?: number // Always in Watt
+  meterSerialNumber?: string
+  templateIndex: number
+  templateName: string
+} & Omit<
   ChargingStationTemplate,
+  | 'AutomaticTransactionGenerator'
+  | 'chargeBoxSerialNumberPrefix'
+  | 'chargePointSerialNumberPrefix'
+  | 'Configuration'
   | 'Connectors'
   | 'Evses'
-  | 'Configuration'
-  | 'AutomaticTransactionGenerator'
+  | 'meterSerialNumberPrefix'
   | 'numberOfConnectors'
   | 'power'
   | 'powerUnit'
-  | 'chargeBoxSerialNumberPrefix'
-  | 'chargePointSerialNumberPrefix'
-  | 'meterSerialNumberPrefix'
-> & {
-  hashId: string
-  templateIndex: number
-  templateName: string
-  /** @deprecated Use `hashId` instead. */
-  infoHash?: string
-  chargingStationId?: string
-  chargeBoxSerialNumber?: string
-  chargePointSerialNumber?: string
-  meterSerialNumber?: string
-  maximumPower?: number // Always in Watt
-  maximumAmperage?: number // Always in Ampere
-  firmwareStatus?: FirmwareStatus
-}
+>
 
 export interface ChargingStationInfoConfiguration {
   stationInfo?: ChargingStationInfo
index fdf03a40574765aa9fd7247f13edb18305c23554..efa2f35d19ccd4d510772ede8ab4c877d607554e 100644 (file)
@@ -2,8 +2,8 @@ import type { JsonObject } from './JsonType.js'
 import type { OCPPConfigurationKey } from './ocpp/Configuration.js'
 
 export interface ConfigurationKey extends OCPPConfigurationKey {
-  visible?: boolean
   reboot?: boolean
+  visible?: boolean
 }
 
 export interface ChargingStationOcppConfiguration extends JsonObject {
index 9497e500900c8b8e3e44d4a72520e50c5c0f0bf6..46cc94790db6106013c258a923e05adcbf01a663 100644 (file)
@@ -1,5 +1,4 @@
 import type { ClientRequestArgs } from 'node:http'
-
 import type { ClientOptions } from 'ws'
 
 import type { AutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator.js'
@@ -22,15 +21,15 @@ export enum CurrentType {
 }
 
 export enum PowerUnits {
-  WATT = 'W',
-  KILO_WATT = 'kW'
+  KILO_WATT = 'kW',
+  WATT = 'W'
 }
 
 export enum AmpereUnits {
-  MILLI_AMPERE = 'mA',
+  AMPERE = 'A',
   CENTI_AMPERE = 'cA',
   DECI_AMPERE = 'dA',
-  AMPERE = 'A'
+  MILLI_AMPERE = 'mA'
 }
 
 export enum Voltage {
@@ -43,12 +42,12 @@ export enum Voltage {
 export type WsOptions = ClientOptions & ClientRequestArgs
 
 export interface FirmwareUpgrade extends JsonObject {
+  failureStatus?: FirmwareStatus
+  reset?: boolean
   versionUpgrade?: {
     patternGroup?: number
     step?: number
   }
-  reset?: boolean
-  failureStatus?: FirmwareStatus
 }
 
 interface CommandsSupport extends JsonObject {
@@ -57,79 +56,79 @@ interface CommandsSupport extends JsonObject {
 }
 
 enum x509CertificateType {
-  V2GRootCertificate = 'V2GRootCertificate',
-  MORootCertificate = 'MORootCertificate',
+  ChargingStationCertificate = 'ChargingStationCertificate',
   CSMSRootCertificate = 'CSMSRootCertificate',
   ManufacturerRootCertificate = 'ManufacturerRootCertificate',
-  ChargingStationCertificate = 'ChargingStationCertificate',
-  V2GCertificate = 'V2GCertificate'
+  MORootCertificate = 'MORootCertificate',
+  V2GCertificate = 'V2GCertificate',
+  V2GRootCertificate = 'V2GRootCertificate'
 }
 
 export interface ChargingStationTemplate {
-  templateHash?: string
-  supervisionUrls?: string | string[]
-  supervisionUrlOcppConfiguration?: boolean
-  supervisionUrlOcppKey?: string
-  supervisionUser?: string
-  supervisionPassword?: string
-  autoStart?: boolean
-  ocppVersion?: OCPPVersion
-  ocppProtocol?: OCPPProtocol
-  ocppStrictCompliance?: boolean
-  ocppPersistentConfiguration?: boolean
-  stationInfoPersistentConfiguration?: boolean
+  amperageLimitationOcppKey?: string
+  amperageLimitationUnit?: AmpereUnits
+  AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration
   automaticTransactionGeneratorPersistentConfiguration?: boolean
-  wsOptions?: WsOptions
-  idTagsFile?: string
+  autoReconnectMaxRetries?: number
+  autoRegister?: boolean
+  autoStart?: boolean
   baseName: string
-  nameSuffix?: string
-  fixedName?: boolean
+  beginEndMeterValues?: boolean
+  chargeBoxSerialNumberPrefix?: string
   chargePointModel: string
-  chargePointVendor: string
   chargePointSerialNumberPrefix?: string
-  chargeBoxSerialNumberPrefix?: string
-  firmwareVersionPattern?: string
-  firmwareVersion?: string
+  chargePointVendor: string
+  commandsSupport?: CommandsSupport
+  Configuration?: ChargingStationOcppConfiguration
+  Connectors?: Record<string, ConnectorStatus>
+  currentOutType?: CurrentType
+  customValueLimitationMeterValues?: boolean
+  enableStatistics?: boolean
+  Evses?: Record<string, EvseTemplate>
   firmwareUpgrade?: FirmwareUpgrade
+  firmwareVersion?: string
+  firmwareVersionPattern?: string
+  fixedName?: boolean
   iccid?: string
+  idTagsFile?: string
   imsi?: string
+  mainVoltageMeterValues?: boolean
+  messageTriggerSupport?: Record<MessageTrigger, boolean>
+  meteringPerTransaction?: boolean
   meterSerialNumberPrefix?: string
   meterType?: string
+  /** @deprecated Replaced by remoteAuthorization. */
+  mustAuthorizeAtRemoteStart?: boolean
+  nameSuffix?: string
+  numberOfConnectors?: number | number[]
+  numberOfPhases?: number
+  ocppPersistentConfiguration?: boolean
+  ocppProtocol?: OCPPProtocol
+  ocppStrictCompliance?: boolean
+  ocppVersion?: OCPPVersion
+  outOfOrderEndMeterValues?: boolean
+  /** @deprecated Replaced by ocppStrictCompliance. */
+  payloadSchemaValidation?: boolean
+  phaseLineToLineVoltageMeterValues?: boolean
   power?: number | number[]
-  powerUnit?: PowerUnits
   powerSharedByConnectors?: boolean
-  currentOutType?: CurrentType
-  voltageOut?: Voltage
-  numberOfPhases?: number
-  numberOfConnectors?: number | number[]
-  useConnectorId0?: boolean
+  powerUnit?: PowerUnits
   randomConnectors?: boolean
-  resetTime?: number
-  autoRegister?: boolean
-  autoReconnectMaxRetries?: number
   reconnectExponentialDelay?: boolean
   registrationMaxRetries?: number
-  enableStatistics?: boolean
   remoteAuthorization?: boolean
-  /** @deprecated Replaced by remoteAuthorization. */
-  mustAuthorizeAtRemoteStart?: boolean
-  /** @deprecated Replaced by ocppStrictCompliance. */
-  payloadSchemaValidation?: boolean
-  amperageLimitationOcppKey?: string
-  amperageLimitationUnit?: AmpereUnits
-  beginEndMeterValues?: boolean
-  outOfOrderEndMeterValues?: boolean
-  meteringPerTransaction?: boolean
-  transactionDataMeterValues?: boolean
+  resetTime?: number
+  stationInfoPersistentConfiguration?: boolean
   stopTransactionsOnStopped?: boolean
-  mainVoltageMeterValues?: boolean
-  phaseLineToLineVoltageMeterValues?: boolean
-  customValueLimitationMeterValues?: boolean
-  commandsSupport?: CommandsSupport
-  messageTriggerSupport?: Record<MessageTrigger, boolean>
-  Configuration?: ChargingStationOcppConfiguration
-  AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration
-  Evses?: Record<string, EvseTemplate>
-  Connectors?: Record<string, ConnectorStatus>
+  supervisionPassword?: string
+  supervisionUrlOcppConfiguration?: boolean
+  supervisionUrlOcppKey?: string
+  supervisionUrls?: string | string[]
+  supervisionUser?: string
+  templateHash?: string
+  transactionDataMeterValues?: boolean
+  useConnectorId0?: boolean
+  voltageOut?: Voltage
+  wsOptions?: WsOptions
   x509Certificates?: Record<x509CertificateType, string>
 }
index 89e79a375c34d9be8eff1e2c7f544c87c8d4c822..b9abdeb2e17cdb2514f0ece46904c141298d6706 100644 (file)
@@ -2,7 +2,6 @@ import type { WebSocket } from 'ws'
 
 import type { WorkerData } from '../worker/index.js'
 import type { ChargingStationAutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator.js'
-import { ChargingStationEvents } from './ChargingStationEvents.js'
 import type { ChargingStationInfo } from './ChargingStationInfo.js'
 import type { ChargingStationOcppConfiguration } from './ChargingStationOcppConfiguration.js'
 import type { ConnectorStatus } from './ConnectorStatus.js'
@@ -11,40 +10,42 @@ import type { JsonObject } from './JsonType.js'
 import type { BootNotificationResponse } from './ocpp/Responses.js'
 import type { Statistics } from './Statistics.js'
 
+import { ChargingStationEvents } from './ChargingStationEvents.js'
+
 export interface ChargingStationOptions extends JsonObject {
-  supervisionUrls?: string | string[]
-  persistentConfiguration?: boolean
-  autoStart?: boolean
   autoRegister?: boolean
+  autoStart?: boolean
   enableStatistics?: boolean
   ocppStrictCompliance?: boolean
+  persistentConfiguration?: boolean
   stopTransactionsOnStopped?: boolean
+  supervisionUrls?: string | string[]
 }
 
 export interface ChargingStationWorkerData extends WorkerData {
   index: number
-  templateFile: string
   options?: ChargingStationOptions
+  templateFile: string
 }
 
-export type EvseStatusWorkerType = Omit<EvseStatus, 'connectors'> & {
+export type EvseStatusWorkerType = {
   connectors?: ConnectorStatus[]
-}
+} & Omit<EvseStatus, 'connectors'>
 
 export interface ChargingStationData extends WorkerData {
-  started: boolean
-  stationInfo: ChargingStationInfo
+  automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
+  bootNotificationResponse?: BootNotificationResponse
   connectors: ConnectorStatus[]
   evses: EvseStatusWorkerType[]
   ocppConfiguration: ChargingStationOcppConfiguration
+  started: boolean
+  stationInfo: ChargingStationInfo
   supervisionUrl: string
   wsState?:
+    | typeof WebSocket.CLOSED
+    | typeof WebSocket.CLOSING
     | typeof WebSocket.CONNECTING
     | typeof WebSocket.OPEN
-    | typeof WebSocket.CLOSING
-    | typeof WebSocket.CLOSED
-  bootNotificationResponse?: BootNotificationResponse
-  automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
 }
 
 enum ChargingStationMessageEvents {
@@ -63,6 +64,6 @@ export type ChargingStationWorkerMessageEvents =
 export type ChargingStationWorkerMessageData = ChargingStationData | Statistics
 
 export interface ChargingStationWorkerMessage<T extends ChargingStationWorkerMessageData> {
-  event: ChargingStationWorkerMessageEvents
   data: T
+  event: ChargingStationWorkerMessageEvents
 }
index 216262f12cb82557a4e8cd930d33cce2ac52f1d3..23457f5db6aab91581c7fffc97c0b1f12b3a5102 100644 (file)
@@ -1,6 +1,5 @@
 import type { ListenOptions } from 'node:net'
 import type { ResourceLimits } from 'node:worker_threads'
-
 import type { WorkerChoiceStrategy } from 'poolifier'
 
 import type { WorkerProcessType } from '../worker/index.js'
@@ -12,14 +11,14 @@ type ServerOptions = ListenOptions
 export enum ConfigurationSection {
   log = 'log',
   performanceStorage = 'performanceStorage',
-  worker = 'worker',
-  uiServer = 'uiServer'
+  uiServer = 'uiServer',
+  worker = 'worker'
 }
 
 export enum SupervisionUrlDistribution {
-  ROUND_ROBIN = 'round-robin',
+  CHARGING_STATION_AFFINITY = 'charging-station-affinity',
   RANDOM = 'random',
-  CHARGING_STATION_AFFINITY = 'charging-station-affinity'
+  ROUND_ROBIN = 'round-robin'
 }
 
 export interface StationTemplateUrl {
@@ -29,16 +28,16 @@ export interface StationTemplateUrl {
 }
 
 export interface LogConfiguration {
+  console?: boolean
   enabled?: boolean
-  file?: string
   errorFile?: string
-  statisticsInterval?: number
-  level?: string
-  console?: boolean
+  file?: string
   format?: string
+  level?: string
+  maxFiles?: number | string
+  maxSize?: number | string
   rotate?: boolean
-  maxFiles?: string | number
-  maxSize?: string | number
+  statisticsInterval?: number
 }
 
 export enum ApplicationProtocolVersion {
@@ -47,16 +46,16 @@ export enum ApplicationProtocolVersion {
 }
 
 export interface UIServerConfiguration {
-  enabled?: boolean
-  type?: ApplicationProtocol
-  version?: ApplicationProtocolVersion
-  options?: ServerOptions
   authentication?: {
     enabled: boolean
+    password?: string
     type: AuthenticationType
     username?: string
-    password?: string
   }
+  enabled?: boolean
+  options?: ServerOptions
+  type?: ApplicationProtocol
+  version?: ApplicationProtocolVersion
 }
 
 export interface StorageConfiguration {
@@ -65,62 +64,62 @@ export interface StorageConfiguration {
   uri?: string
 }
 
-export type ElementsPerWorkerType = number | 'auto' | 'all'
+export type ElementsPerWorkerType = 'all' | 'auto' | number
 
 export interface WorkerConfiguration {
-  processType?: WorkerProcessType
-  startDelay?: number
+  elementAddDelay?: number
   elementsPerWorker?: ElementsPerWorkerType
   /** @deprecated Use `elementAddDelay` instead. */
   elementStartDelay?: number
-  elementAddDelay?: number
-  poolMinSize?: number
   poolMaxSize?: number
+  poolMinSize?: number
+  processType?: WorkerProcessType
   resourceLimits?: ResourceLimits
+  startDelay?: number
 }
 
 export interface ConfigurationData {
-  supervisionUrls?: string | string[]
-  supervisionUrlDistribution?: SupervisionUrlDistribution
-  stationTemplateUrls: StationTemplateUrl[]
-  log?: LogConfiguration
-  worker?: WorkerConfiguration
-  uiServer?: UIServerConfiguration
-  performanceStorage?: StorageConfiguration
   /** @deprecated Moved to charging station template. */
   autoReconnectMaxRetries?: number
   /** @deprecated Moved to worker configuration section. */
-  workerProcess?: WorkerProcessType
-  /** @deprecated Moved to worker configuration section. */
-  workerStartDelay?: number
+  chargingStationsPerWorker?: number
   /** @deprecated Moved to worker configuration section. */
   elementAddDelay?: number
-  /** @deprecated Moved to worker configuration section. */
-  workerPoolMinSize?: number
-  /** @deprecated Moved to worker configuration section. */
-  workerPoolMaxSize?: number
-  /** @deprecated Moved to worker configuration section. */
-  workerPoolStrategy?: WorkerChoiceStrategy
-  /** @deprecated Moved to worker configuration section. */
-  chargingStationsPerWorker?: number
+  log?: LogConfiguration
   /** @deprecated Moved to log configuration section. */
-  logStatisticsInterval?: number
+  logConsole?: boolean
   /** @deprecated Moved to log configuration section. */
   logEnabled?: boolean
   /** @deprecated Moved to log configuration section. */
-  logConsole?: boolean
+  logErrorFile?: string
+  /** @deprecated Moved to log configuration section. */
+  logFile?: string
   /** @deprecated Moved to log configuration section. */
   logFormat?: string
   /** @deprecated Moved to log configuration section. */
   logLevel?: string
   /** @deprecated Moved to log configuration section. */
-  logRotate?: boolean
-  /** @deprecated Moved to log configuration section. */
   logMaxFiles?: number | string
   /** @deprecated Moved to log configuration section. */
   logMaxSize?: number | string
   /** @deprecated Moved to log configuration section. */
-  logFile?: string
+  logRotate?: boolean
   /** @deprecated Moved to log configuration section. */
-  logErrorFile?: string
+  logStatisticsInterval?: number
+  performanceStorage?: StorageConfiguration
+  stationTemplateUrls: StationTemplateUrl[]
+  supervisionUrlDistribution?: SupervisionUrlDistribution
+  supervisionUrls?: string | string[]
+  uiServer?: UIServerConfiguration
+  worker?: WorkerConfiguration
+  /** @deprecated Moved to worker configuration section. */
+  workerPoolMaxSize?: number
+  /** @deprecated Moved to worker configuration section. */
+  workerPoolMinSize?: number
+  /** @deprecated Moved to worker configuration section. */
+  workerPoolStrategy?: WorkerChoiceStrategy
+  /** @deprecated Moved to worker configuration section. */
+  workerProcess?: WorkerProcessType
+  /** @deprecated Moved to worker configuration section. */
+  workerStartDelay?: number
 }
index 26f5433424fa5404000760720d3a7a06ea8d8022..b9fef35e0026e540c3501e7f147018490d1bf8d9 100644 (file)
@@ -6,23 +6,23 @@ import type { AvailabilityType } from './ocpp/Requests.js'
 import type { Reservation } from './ocpp/Reservation.js'
 
 export interface ConnectorStatus {
+  authorizeIdTag?: string
   availability: AvailabilityType
   bootStatus?: ConnectorStatusEnum
-  status?: ConnectorStatusEnum
-  MeterValues: SampledValueTemplate[]
-  authorizeIdTag?: string
+  chargingProfiles?: ChargingProfile[]
+  energyActiveImportRegisterValue?: number // In Wh
   idTagAuthorized?: boolean
-  localAuthorizeIdTag?: string
   idTagLocalAuthorized?: boolean
-  transactionRemoteStarted?: boolean
-  transactionStarted?: boolean
-  transactionStart?: Date
+  localAuthorizeIdTag?: string
+  MeterValues: SampledValueTemplate[]
+  reservation?: Reservation
+  status?: ConnectorStatusEnum
+  transactionBeginMeterValue?: MeterValue
+  transactionEnergyActiveImportRegisterValue?: number // In Wh
   transactionId?: number
-  transactionSetInterval?: NodeJS.Timeout
   transactionIdTag?: string
-  energyActiveImportRegisterValue?: number // In Wh
-  transactionEnergyActiveImportRegisterValue?: number // In Wh
-  transactionBeginMeterValue?: MeterValue
-  chargingProfiles?: ChargingProfile[]
-  reservation?: Reservation
+  transactionRemoteStarted?: boolean
+  transactionSetInterval?: NodeJS.Timeout
+  transactionStart?: Date
+  transactionStarted?: boolean
 }
index 34bad1ad4eabbf612fa2c27398def5fea4434f73..89d20479aa550d4abddf6c7eedc2ff1b512aece4 100644 (file)
@@ -1,7 +1,7 @@
 import type { JsonType } from './JsonType.js'
 
 export interface HandleErrorParams<T extends JsonType> {
-  throwError?: boolean
   consoleOut?: boolean
   errorResponse?: T
+  throwError?: boolean
 }
index 8fb09e48d1ef8d1dc2af12412f21656dcdb7fc8c..dcb00ad55ada651a78a4d53126bcd858a8858141 100644 (file)
@@ -6,6 +6,6 @@ export interface EvseTemplate {
 }
 
 export interface EvseStatus {
-  connectors: Map<number, ConnectorStatus>
   availability: AvailabilityType
+  connectors: Map<number, ConnectorStatus>
 }
index 2b6bc380cbe401c1c294407c489ed47f9bc331fc..8884a8991729d6e26d3535a256b81119786894b9 100644 (file)
@@ -1,8 +1,8 @@
 export enum FileType {
   Authorization = 'authorization',
-  Configuration = 'configuration',
   ChargingStationConfiguration = 'charging station configuration',
   ChargingStationTemplate = 'charging station template',
-  PerformanceRecords = 'performance records',
-  JsonSchema = 'json schema'
+  Configuration = 'configuration',
+  JsonSchema = 'json schema',
+  PerformanceRecords = 'performance records'
 }
index 17db232c50cb7c55ec21d80cb0aacd1137010415..78142b7793f3fbba6f8c2ae1817a3ae88e57987a 100644 (file)
@@ -1,7 +1,7 @@
-type JsonPrimitive = string | number | boolean | Date | null
+type JsonPrimitive = boolean | Date | null | number | string
 
 export type JsonObject = {
   [key in string]?: JsonType
 }
 
-export type JsonType = JsonPrimitive | JsonType[] | JsonObject
+export type JsonType = JsonObject | JsonPrimitive | JsonType[]
index 833efa8237537b29f83189b527d58df3fd0478f1..1c13b4fb4e8d6cfe24e6fbd1748a8857237ae8b5 100644 (file)
@@ -1,6 +1,6 @@
 export interface MeasurandValues {
+  allPhases: number
   L1: number
   L2: number
   L3: number
-  allPhases: number
 }
index 0ba5307897af6e8939bdc05f92ea186df0bf5172..a567316b8d60d2c403828f030307449df97da633 100644 (file)
@@ -2,8 +2,8 @@ import type { ConfigurationData } from './ConfigurationData.js'
 import type { TemplateStatistics } from './Statistics.js'
 
 export interface SimulatorState {
-  version: string
   configuration: ConfigurationData | undefined
   started: boolean
   templateStatistics: Map<string, TemplateStatistics>
+  version: string
 }
index 9601a90cb70e2ace501599190aba61f1d834c14a..7df512e2a58d958328c8079a63824e9bf4a0241b 100644 (file)
@@ -9,34 +9,34 @@ export interface TimestampedData {
 }
 
 export type StatisticsData = Partial<{
-  requestCount: number
-  responseCount: number
-  errorCount: number
-  timeMeasurementCount: number
-  measurementTimeSeries: CircularBuffer<TimestampedData> | TimestampedData[]
+  avgTimeMeasurement: number
   currentTimeMeasurement: number
-  minTimeMeasurement: number
+  errorCount: number
   maxTimeMeasurement: number
-  totalTimeMeasurement: number
-  avgTimeMeasurement: number
+  measurementTimeSeries: CircularBuffer<TimestampedData> | TimestampedData[]
   medTimeMeasurement: number
+  minTimeMeasurement: number
   ninetyFiveThPercentileTimeMeasurement: number
+  requestCount: number
+  responseCount: number
   stdDevTimeMeasurement: number
+  timeMeasurementCount: number
+  totalTimeMeasurement: number
 }>
 
 export interface Statistics extends WorkerData {
+  createdAt: Date
   id: string
   name: string
-  uri: string
-  createdAt: Date
+  statisticsData: Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>
   updatedAt?: Date
-  statisticsData: Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
+  uri: string
 }
 
 export interface TemplateStatistics {
+  added: number
   configured: number
+  indexes: Set<number>
   provisioned: number
-  added: number
   started: number
-  indexes: Set<number>
 }
index 232c9d41069d86b61a3f9ff661c20a1fc812f389..1128007e9d5d4827c00d9a361d3cba1b3b1058dd 100644 (file)
@@ -1,15 +1,15 @@
 export enum StorageType {
-  NONE = 'none',
   JSON_FILE = 'jsonfile',
+  MARIA_DB = 'mariadb',
   MONGO_DB = 'mongodb',
   MYSQL = 'mysql',
-  MARIA_DB = 'mariadb',
+  NONE = 'none',
   SQLITE = 'sqlite'
 }
 
 export enum DBName {
+  MARIA_DB = 'MariaDB',
   MONGO_DB = 'MongoDB',
   MYSQL = 'MySQL',
-  MARIA_DB = 'MariaDB',
   SQLITE = 'SQLite'
 }
index b5cf30fff02cbca47089e577d75a0a8a8e7f0bd2..7a3dce2d99ce2091e42d1ed8c081dc6dc78bd388 100644 (file)
@@ -33,49 +33,49 @@ export type ProtocolRequestHandler = (
   uuid?: `${string}-${string}-${string}-${string}-${string}`,
   procedureName?: ProcedureName,
   payload?: RequestPayload
-) => undefined | Promise<undefined> | ResponsePayload | Promise<ResponsePayload>
+) => Promise<ResponsePayload> | Promise<undefined> | ResponsePayload | undefined
 
 export enum ProcedureName {
-  SIMULATOR_STATE = 'simulatorState',
-  START_SIMULATOR = 'startSimulator',
-  STOP_SIMULATOR = 'stopSimulator',
-  LIST_TEMPLATES = 'listTemplates',
-  LIST_CHARGING_STATIONS = 'listChargingStations',
   ADD_CHARGING_STATIONS = 'addChargingStations',
+  AUTHORIZE = 'authorize',
+  BOOT_NOTIFICATION = 'bootNotification',
+  CLOSE_CONNECTION = 'closeConnection',
+  DATA_TRANSFER = 'dataTransfer',
   DELETE_CHARGING_STATIONS = 'deleteChargingStations',
-  PERFORMANCE_STATISTICS = 'performanceStatistics',
-  START_CHARGING_STATION = 'startChargingStation',
-  STOP_CHARGING_STATION = 'stopChargingStation',
+  DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
+  FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification',
+  HEARTBEAT = 'heartbeat',
+  LIST_CHARGING_STATIONS = 'listChargingStations',
+  LIST_TEMPLATES = 'listTemplates',
+  METER_VALUES = 'meterValues',
   OPEN_CONNECTION = 'openConnection',
-  CLOSE_CONNECTION = 'closeConnection',
-  START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
-  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+  PERFORMANCE_STATISTICS = 'performanceStatistics',
   SET_SUPERVISION_URL = 'setSupervisionUrl',
+  SIMULATOR_STATE = 'simulatorState',
+  START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
+  START_CHARGING_STATION = 'startChargingStation',
+  START_SIMULATOR = 'startSimulator',
   START_TRANSACTION = 'startTransaction',
-  STOP_TRANSACTION = 'stopTransaction',
-  AUTHORIZE = 'authorize',
-  BOOT_NOTIFICATION = 'bootNotification',
   STATUS_NOTIFICATION = 'statusNotification',
-  HEARTBEAT = 'heartbeat',
-  METER_VALUES = 'meterValues',
-  DATA_TRANSFER = 'dataTransfer',
-  DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
-  FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification'
+  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+  STOP_CHARGING_STATION = 'stopChargingStation',
+  STOP_SIMULATOR = 'stopSimulator',
+  STOP_TRANSACTION = 'stopTransaction'
 }
 
 export interface RequestPayload extends JsonObject {
-  hashIds?: string[]
   connectorIds?: number[]
+  hashIds?: string[]
 }
 
 export enum ResponseStatus {
-  SUCCESS = 'success',
-  FAILURE = 'failure'
+  FAILURE = 'failure',
+  SUCCESS = 'success'
 }
 
 export interface ResponsePayload extends JsonObject {
-  status: ResponseStatus
-  hashIdsSucceeded?: string[]
   hashIdsFailed?: string[]
+  hashIdsSucceeded?: string[]
   responsesFailed?: BroadcastChannelResponsePayload[]
+  status: ResponseStatus
 }
index 499fe09bc90eb0183b33cc010d21fd8af948332f..753c2268d2b7f4227fe9a3d748b1d64b4f041dda 100644 (file)
@@ -19,22 +19,22 @@ export const WebSocketCloseEventStatusString: Record<WebSocketCloseEventStatusCo
   })
 
 export enum WebSocketCloseEventStatusCode {
-  CLOSE_NORMAL = 1000,
-  CLOSE_GOING_AWAY = 1001,
-  CLOSE_PROTOCOL_ERROR = 1002,
-  CLOSE_UNSUPPORTED = 1003,
-  CLOSE_RESERVED = 1004,
-  CLOSE_NO_STATUS = 1005,
   CLOSE_ABNORMAL = 1006,
+  CLOSE_BAD_GATEWAY = 1014,
+  CLOSE_GOING_AWAY = 1001,
   CLOSE_INVALID_PAYLOAD = 1007,
-  CLOSE_POLICY_VIOLATION = 1008,
-  CLOSE_TOO_LARGE = 1009,
   CLOSE_MISSING_EXTENSION = 1010,
+  CLOSE_NO_STATUS = 1005,
+  CLOSE_NORMAL = 1000,
+  CLOSE_POLICY_VIOLATION = 1008,
+  CLOSE_PROTOCOL_ERROR = 1002,
+  CLOSE_RESERVED = 1004,
   CLOSE_SERVER_INTERNAL_ERROR = 1011,
   CLOSE_SERVICE_RESTART = 1012,
+  CLOSE_TLS_HANDSHAKE = 1015,
+  CLOSE_TOO_LARGE = 1009,
   CLOSE_TRY_AGAIN_LATER = 1013,
-  CLOSE_BAD_GATEWAY = 1014,
-  CLOSE_TLS_HANDSHAKE = 1015
+  CLOSE_UNSUPPORTED = 1003
 }
 
 export interface WSError extends Error {
index 077c2f3750acce4fe2e6ece971e8aff3ba5d5d09..4582f2b344421da8a9fea2aaff264bc149e9ee91 100644 (file)
@@ -11,24 +11,24 @@ export type BroadcastChannelResponse = [
 ]
 
 export enum BroadcastChannelProcedureName {
-  START_CHARGING_STATION = 'startChargingStation',
-  STOP_CHARGING_STATION = 'stopChargingStation',
+  AUTHORIZE = 'authorize',
+  BOOT_NOTIFICATION = 'bootNotification',
+  CLOSE_CONNECTION = 'closeConnection',
+  DATA_TRANSFER = 'dataTransfer',
   DELETE_CHARGING_STATIONS = 'deleteChargingStations',
+  DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
+  FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification',
+  HEARTBEAT = 'heartbeat',
+  METER_VALUES = 'meterValues',
   OPEN_CONNECTION = 'openConnection',
-  CLOSE_CONNECTION = 'closeConnection',
-  START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
-  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
   SET_SUPERVISION_URL = 'setSupervisionUrl',
+  START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
+  START_CHARGING_STATION = 'startChargingStation',
   START_TRANSACTION = 'startTransaction',
-  STOP_TRANSACTION = 'stopTransaction',
-  AUTHORIZE = 'authorize',
-  BOOT_NOTIFICATION = 'bootNotification',
   STATUS_NOTIFICATION = 'statusNotification',
-  HEARTBEAT = 'heartbeat',
-  METER_VALUES = 'meterValues',
-  DATA_TRANSFER = 'dataTransfer',
-  DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
-  FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification'
+  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+  STOP_CHARGING_STATION = 'stopChargingStation',
+  STOP_TRANSACTION = 'stopTransaction'
 }
 
 export interface BroadcastChannelRequestPayload extends RequestPayload {
@@ -37,7 +37,7 @@ export interface BroadcastChannelRequestPayload extends RequestPayload {
 }
 
 export interface BroadcastChannelResponsePayload
-  extends Omit<ResponsePayload, 'hashIdsSucceeded' | 'hashIdsFailed' | 'responsesFailed'> {
+  extends Omit<ResponsePayload, 'hashIdsFailed' | 'hashIdsSucceeded' | 'responsesFailed'> {
   hashId: string | undefined
 }
 
index ef7467bd4841d3186c32a5d3432243403f29c920..9acc5487bab5fd4f02f0b787d3c75448687a4ce4 100644 (file)
@@ -1,11 +1,11 @@
 export enum OCPP16ChargePointStatus {
   Available = 'Available',
-  Preparing = 'Preparing',
   Charging = 'Charging',
-  SuspendedEVSE = 'SuspendedEVSE',
-  SuspendedEV = 'SuspendedEV',
+  Faulted = 'Faulted',
   Finishing = 'Finishing',
+  Preparing = 'Preparing',
   Reserved = 'Reserved',
-  Unavailable = 'Unavailable',
-  Faulted = 'Faulted'
+  SuspendedEV = 'SuspendedEV',
+  SuspendedEVSE = 'SuspendedEVSE',
+  Unavailable = 'Unavailable'
 }
index 744696c586306f52bca08fd23b2d9f28e37db6c2..ecf6bebbb53d67a471adfc15b4c938b489311028 100644 (file)
@@ -2,33 +2,33 @@ import type { JsonObject } from '../../JsonType.js'
 
 export interface OCPP16ChargingProfile extends JsonObject {
   chargingProfileId: number
-  transactionId?: number
-  stackLevel: number
-  chargingProfilePurpose: OCPP16ChargingProfilePurposeType
   chargingProfileKind: OCPP16ChargingProfileKindType
+  chargingProfilePurpose: OCPP16ChargingProfilePurposeType
+  chargingSchedule: OCPP16ChargingSchedule
   recurrencyKind?: OCPP16RecurrencyKindType
+  stackLevel: number
+  transactionId?: number
   validFrom?: Date
   validTo?: Date
-  chargingSchedule: OCPP16ChargingSchedule
 }
 
 export interface OCPP16ChargingSchedule extends JsonObject {
-  startSchedule?: Date
-  duration?: number
   chargingRateUnit: OCPP16ChargingRateUnitType
   chargingSchedulePeriod: OCPP16ChargingSchedulePeriod[]
+  duration?: number
   minChargeRate?: number
+  startSchedule?: Date
 }
 
 export interface OCPP16ChargingSchedulePeriod extends JsonObject {
-  startPeriod: number
   limit: number
   numberPhases?: number
+  startPeriod: number
 }
 
 export enum OCPP16ChargingRateUnitType {
-  WATT = 'W',
-  AMPERE = 'A'
+  AMPERE = 'A',
+  WATT = 'W'
 }
 
 export enum OCPP16ChargingProfileKindType {
index a5888d93c200567cb6bd4d2cbcffda8d544bac1b..d40d8d1f06538b44008dec775934c3da47f46f57 100644 (file)
@@ -2,9 +2,9 @@ export enum OCPP16SupportedFeatureProfiles {
   Core = 'Core',
   FirmwareManagement = 'FirmwareManagement',
   LocalAuthListManagement = 'LocalAuthListManagement',
+  RemoteTrigger = 'RemoteTrigger',
   Reservation = 'Reservation',
-  SmartCharging = 'SmartCharging',
-  RemoteTrigger = 'RemoteTrigger'
+  SmartCharging = 'SmartCharging'
 }
 
 export enum OCPP16StandardParametersKey {
@@ -12,25 +12,34 @@ export enum OCPP16StandardParametersKey {
   AuthorizationCacheEnabled = 'AuthorizationCacheEnabled',
   AuthorizeRemoteTxRequests = 'AuthorizeRemoteTxRequests',
   BlinkRepeat = 'BlinkRepeat',
+  ChargeProfileMaxStackLevel = 'ChargeProfileMaxStackLevel',
+  ChargingScheduleAllowedChargingRateUnit = 'ChargingScheduleAllowedChargingRateUnit',
+  ChargingScheduleMaxPeriods = 'ChargingScheduleMaxPeriods',
   ClockAlignedDataInterval = 'ClockAlignedDataInterval',
   ConnectionTimeOut = 'ConnectionTimeOut',
+  ConnectorPhaseRotation = 'ConnectorPhaseRotation',
+  ConnectorPhaseRotationMaxLength = 'ConnectorPhaseRotationMaxLength',
+  ConnectorSwitch3to1PhaseSupported = 'ConnectorSwitch3to1PhaseSupported',
   GetConfigurationMaxKeys = 'GetConfigurationMaxKeys',
   HeartbeatInterval = 'HeartbeatInterval',
   HeartBeatInterval = 'HeartBeatInterval',
   LightIntensity = 'LightIntensity',
+  LocalAuthListEnabled = 'LocalAuthListEnabled',
+  LocalAuthListMaxLength = 'LocalAuthListMaxLength',
   LocalAuthorizeOffline = 'LocalAuthorizeOffline',
   LocalPreAuthorize = 'LocalPreAuthorize',
+  MaxChargingProfilesInstalled = 'MaxChargingProfilesInstalled',
   MaxEnergyOnInvalidId = 'MaxEnergyOnInvalidId',
   MeterValuesAlignedData = 'MeterValuesAlignedData',
   MeterValuesAlignedDataMaxLength = 'MeterValuesAlignedDataMaxLength',
+  MeterValueSampleInterval = 'MeterValueSampleInterval',
   MeterValuesSampledData = 'MeterValuesSampledData',
   MeterValuesSampledDataMaxLength = 'MeterValuesSampledDataMaxLength',
-  MeterValueSampleInterval = 'MeterValueSampleInterval',
   MinimumStatusDuration = 'MinimumStatusDuration',
   NumberOfConnectors = 'NumberOfConnectors',
+  ReserveConnectorZeroSupported = 'ReserveConnectorZeroSupported',
   ResetRetries = 'ResetRetries',
-  ConnectorPhaseRotation = 'ConnectorPhaseRotation',
-  ConnectorPhaseRotationMaxLength = 'ConnectorPhaseRotationMaxLength',
+  SendLocalListMaxLength = 'SendLocalListMaxLength',
   StopTransactionOnEVSideDisconnect = 'StopTransactionOnEVSideDisconnect',
   StopTransactionOnInvalidId = 'StopTransactionOnInvalidId',
   StopTxnAlignedData = 'StopTxnAlignedData',
@@ -42,16 +51,7 @@ export enum OCPP16StandardParametersKey {
   TransactionMessageAttempts = 'TransactionMessageAttempts',
   TransactionMessageRetryInterval = 'TransactionMessageRetryInterval',
   UnlockConnectorOnEVSideDisconnect = 'UnlockConnectorOnEVSideDisconnect',
-  WebSocketPingInterval = 'WebSocketPingInterval',
-  LocalAuthListEnabled = 'LocalAuthListEnabled',
-  LocalAuthListMaxLength = 'LocalAuthListMaxLength',
-  SendLocalListMaxLength = 'SendLocalListMaxLength',
-  ReserveConnectorZeroSupported = 'ReserveConnectorZeroSupported',
-  ChargeProfileMaxStackLevel = 'ChargeProfileMaxStackLevel',
-  ChargingScheduleAllowedChargingRateUnit = 'ChargingScheduleAllowedChargingRateUnit',
-  ChargingScheduleMaxPeriods = 'ChargingScheduleMaxPeriods',
-  ConnectorSwitch3to1PhaseSupported = 'ConnectorSwitch3to1PhaseSupported',
-  MaxChargingProfilesInstalled = 'MaxChargingProfilesInstalled'
+  WebSocketPingInterval = 'WebSocketPingInterval'
 }
 
 export enum OCPP16VendorParametersKey {
index 92cac7c3245bad2f2952e731727afa689ab4f2a6..d01cae07b7ed4f0f7c7dbaa1c9893f7d1608f53f 100644 (file)
@@ -2,22 +2,22 @@ import type { EmptyObject } from '../../EmptyObject.js'
 import type { JsonObject } from '../../JsonType.js'
 
 export enum OCPP16MeterValueUnit {
-  WATT_HOUR = 'Wh',
-  KILO_WATT_HOUR = 'kWh',
-  VAR_HOUR = 'varh',
+  AMP = 'A',
+  KILO_VAR = 'kvar',
   KILO_VAR_HOUR = 'kvarh',
-  WATT = 'W',
-  KILO_WATT = 'kW',
-  VOLT_AMP = 'VA',
   KILO_VOLT_AMP = 'kVA',
-  VAR = 'var',
-  KILO_VAR = 'kvar',
-  AMP = 'A',
-  VOLT = 'V',
+  KILO_WATT = 'kW',
+  KILO_WATT_HOUR = 'kWh',
+  PERCENT = 'Percent',
   TEMP_CELSIUS = 'Celsius',
   TEMP_FAHRENHEIT = 'Fahrenheit',
   TEMP_KELVIN = 'K',
-  PERCENT = 'Percent'
+  VAR = 'var',
+  VAR_HOUR = 'varh',
+  VOLT = 'V',
+  VOLT_AMP = 'VA',
+  WATT = 'W',
+  WATT_HOUR = 'Wh'
 }
 
 export enum OCPP16MeterValueContext {
@@ -35,14 +35,15 @@ export enum OCPP16MeterValueMeasurand {
   CURRENT_EXPORT = 'Current.Export',
   CURRENT_IMPORT = 'Current.Import',
   CURRENT_OFFERED = 'Current.Offered',
-  ENERGY_ACTIVE_EXPORT_REGISTER = 'Energy.Active.Export.Register',
-  ENERGY_ACTIVE_IMPORT_REGISTER = 'Energy.Active.Import.Register',
-  ENERGY_REACTIVE_EXPORT_REGISTER = 'Energy.Reactive.Export.Register',
-  ENERGY_REACTIVE_IMPORT_REGISTER = 'Energy.Reactive.Import.Register',
   ENERGY_ACTIVE_EXPORT_INTERVAL = 'Energy.Active.Export.Interval',
+  ENERGY_ACTIVE_EXPORT_REGISTER = 'Energy.Active.Export.Register',
   ENERGY_ACTIVE_IMPORT_INTERVAL = 'Energy.Active.Import.Interval',
+  ENERGY_ACTIVE_IMPORT_REGISTER = 'Energy.Active.Import.Register',
   ENERGY_REACTIVE_EXPORT_INTERVAL = 'Energy.Reactive.Export.Interval',
+  ENERGY_REACTIVE_EXPORT_REGISTER = 'Energy.Reactive.Export.Register',
   ENERGY_REACTIVE_IMPORT_INTERVAL = 'Energy.Reactive.Import.Interval',
+  ENERGY_REACTIVE_IMPORT_REGISTER = 'Energy.Reactive.Import.Register',
+  FAN_RPM = 'RPM',
   FREQUENCY = 'Frequency',
   POWER_ACTIVE_EXPORT = 'Power.Active.Export',
   POWER_ACTIVE_IMPORT = 'Power.Active.Import',
@@ -50,7 +51,6 @@ export enum OCPP16MeterValueMeasurand {
   POWER_OFFERED = 'Power.Offered',
   POWER_REACTIVE_EXPORT = 'Power.Reactive.Export',
   POWER_REACTIVE_IMPORT = 'Power.Reactive.Import',
-  FAN_RPM = 'RPM',
   STATE_OF_CHARGE = 'SoC',
   TEMPERATURE = 'Temperature',
   VOLTAGE = 'Voltage'
@@ -66,15 +66,15 @@ export enum OCPP16MeterValueLocation {
 
 export enum OCPP16MeterValuePhase {
   L1 = 'L1',
-  L2 = 'L2',
-  L3 = 'L3',
-  N = 'N',
+  L1_L2 = 'L1-L2',
   L1_N = 'L1-N',
+  L2 = 'L2',
+  L2_L3 = 'L2-L3',
   L2_N = 'L2-N',
+  L3 = 'L3',
+  L3_L1 = 'L3-L1',
   L3_N = 'L3-N',
-  L1_L2 = 'L1-L2',
-  L2_L3 = 'L2-L3',
-  L3_L1 = 'L3-L1'
+  N = 'N'
 }
 
 enum OCPP16MeterValueFormat {
@@ -83,24 +83,24 @@ enum OCPP16MeterValueFormat {
 }
 
 export interface OCPP16SampledValue extends JsonObject {
-  value: string
-  unit?: OCPP16MeterValueUnit
   context?: OCPP16MeterValueContext
+  format?: OCPP16MeterValueFormat
+  location?: OCPP16MeterValueLocation
   measurand?: OCPP16MeterValueMeasurand
   phase?: OCPP16MeterValuePhase
-  location?: OCPP16MeterValueLocation
-  format?: OCPP16MeterValueFormat
+  unit?: OCPP16MeterValueUnit
+  value: string
 }
 
 export interface OCPP16MeterValue extends JsonObject {
-  timestamp: Date
   sampledValue: OCPP16SampledValue[]
+  timestamp: Date
 }
 
 export interface OCPP16MeterValuesRequest extends JsonObject {
   connectorId: number
-  transactionId?: number
   meterValue: OCPP16MeterValue[]
+  transactionId?: number
 }
 
 export type OCPP16MeterValuesResponse = EmptyObject
index 014be2ed083e50d95b42902adae62e2f3cc6906d..82d90f66c04b061f886d68410c20bb6b3fd7e287 100644 (file)
@@ -11,65 +11,65 @@ import type { OCPP16StandardParametersKey, OCPP16VendorParametersKey } from './C
 import type { OCPP16DiagnosticsStatus } from './DiagnosticsStatus.js'
 
 export enum OCPP16RequestCommand {
-  BOOT_NOTIFICATION = 'BootNotification',
-  HEARTBEAT = 'Heartbeat',
-  STATUS_NOTIFICATION = 'StatusNotification',
   AUTHORIZE = 'Authorize',
-  START_TRANSACTION = 'StartTransaction',
-  STOP_TRANSACTION = 'StopTransaction',
-  METER_VALUES = 'MeterValues',
+  BOOT_NOTIFICATION = 'BootNotification',
+  DATA_TRANSFER = 'DataTransfer',
   DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification',
   FIRMWARE_STATUS_NOTIFICATION = 'FirmwareStatusNotification',
-  DATA_TRANSFER = 'DataTransfer'
+  HEARTBEAT = 'Heartbeat',
+  METER_VALUES = 'MeterValues',
+  START_TRANSACTION = 'StartTransaction',
+  STATUS_NOTIFICATION = 'StatusNotification',
+  STOP_TRANSACTION = 'StopTransaction'
 }
 
 export enum OCPP16IncomingRequestCommand {
-  RESET = 'Reset',
-  CLEAR_CACHE = 'ClearCache',
+  CANCEL_RESERVATION = 'CancelReservation',
   CHANGE_AVAILABILITY = 'ChangeAvailability',
-  UNLOCK_CONNECTOR = 'UnlockConnector',
-  GET_CONFIGURATION = 'GetConfiguration',
   CHANGE_CONFIGURATION = 'ChangeConfiguration',
-  GET_COMPOSITE_SCHEDULE = 'GetCompositeSchedule',
-  SET_CHARGING_PROFILE = 'SetChargingProfile',
+  CLEAR_CACHE = 'ClearCache',
   CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
+  DATA_TRANSFER = 'DataTransfer',
+  GET_COMPOSITE_SCHEDULE = 'GetCompositeSchedule',
+  GET_CONFIGURATION = 'GetConfiguration',
+  GET_DIAGNOSTICS = 'GetDiagnostics',
   REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
   REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction',
-  GET_DIAGNOSTICS = 'GetDiagnostics',
-  TRIGGER_MESSAGE = 'TriggerMessage',
-  DATA_TRANSFER = 'DataTransfer',
-  UPDATE_FIRMWARE = 'UpdateFirmware',
   RESERVE_NOW = 'ReserveNow',
-  CANCEL_RESERVATION = 'CancelReservation'
+  RESET = 'Reset',
+  SET_CHARGING_PROFILE = 'SetChargingProfile',
+  TRIGGER_MESSAGE = 'TriggerMessage',
+  UNLOCK_CONNECTOR = 'UnlockConnector',
+  UPDATE_FIRMWARE = 'UpdateFirmware'
 }
 
 export type OCPP16HeartbeatRequest = EmptyObject
 
 export interface OCPP16BootNotificationRequest extends JsonObject {
-  chargePointVendor: string
+  chargeBoxSerialNumber?: string
   chargePointModel: string
   chargePointSerialNumber?: string
-  chargeBoxSerialNumber?: string
+  chargePointVendor: string
   firmwareVersion?: string
   iccid?: string
   imsi?: string
-  meterType?: string
   meterSerialNumber?: string
+  meterType?: string
 }
 
 export interface OCPP16StatusNotificationRequest extends JsonObject {
   connectorId: number
   errorCode: OCPP16ChargePointErrorCode
-  status: OCPP16ChargePointStatus
   info?: string
+  status: OCPP16ChargePointStatus
   timestamp?: Date
-  vendorId?: string
   vendorErrorCode?: string
+  vendorId?: string
 }
 
 export type OCPP16ClearCacheRequest = EmptyObject
 
-type OCPP16ConfigurationKey = string | OCPP16StandardParametersKey | OCPP16VendorParametersKey
+type OCPP16ConfigurationKey = OCPP16StandardParametersKey | OCPP16VendorParametersKey | string
 
 export interface ChangeConfigurationRequest extends JsonObject {
   key: OCPP16ConfigurationKey
@@ -77,9 +77,9 @@ export interface ChangeConfigurationRequest extends JsonObject {
 }
 
 export interface RemoteStartTransactionRequest extends JsonObject {
+  chargingProfile?: OCPP16ChargingProfile
   connectorId?: number
   idTag: string
-  chargingProfile?: OCPP16ChargingProfile
 }
 
 export interface RemoteStopTransactionRequest extends JsonObject {
@@ -104,9 +104,9 @@ export interface ResetRequest extends JsonObject {
 }
 
 export interface OCPP16GetCompositeScheduleRequest extends JsonObject {
+  chargingRateUnit?: OCPP16ChargingRateUnitType
   connectorId: number
   duration: number
-  chargingRateUnit?: OCPP16ChargingRateUnitType
 }
 
 export interface SetChargingProfileRequest extends JsonObject {
@@ -125,16 +125,16 @@ export interface OCPP16ChangeAvailabilityRequest extends JsonObject {
 }
 
 export interface OCPP16ClearChargingProfileRequest extends JsonObject {
-  id?: number
-  connectorId?: number
   chargingProfilePurpose?: OCPP16ChargingProfilePurposeType
+  connectorId?: number
+  id?: number
   stackLevel?: number
 }
 
 export interface OCPP16UpdateFirmwareRequest extends JsonObject {
   location: string
-  retrieveDate: Date
   retries?: number
+  retrieveDate: Date
   retryInterval?: number
 }
 
@@ -144,8 +144,8 @@ export enum OCPP16FirmwareStatus {
   Downloading = 'Downloading',
   Idle = 'Idle',
   InstallationFailed = 'InstallationFailed',
-  Installing = 'Installing',
-  Installed = 'Installed'
+  Installed = 'Installed',
+  Installing = 'Installing'
 }
 
 export interface OCPP16FirmwareStatusNotificationRequest extends JsonObject {
@@ -174,16 +174,16 @@ export enum OCPP16MessageTrigger {
 }
 
 export interface OCPP16TriggerMessageRequest extends JsonObject {
-  requestedMessage: OCPP16MessageTrigger
   connectorId?: number
+  requestedMessage: OCPP16MessageTrigger
 }
 
 export enum OCPP16DataTransferVendorId {}
 
 export interface OCPP16DataTransferRequest extends JsonObject {
-  vendorId: string
-  messageId?: string
   data?: string
+  messageId?: string
+  vendorId: string
 }
 
 export interface OCPP16ReserveNowRequest extends JsonObject {
index 8b67a9267d5a3a9da03632a0bd7b20a381434102..2447d8c9155767361b8391d872e8f1c83dfaee21 100644 (file)
@@ -9,9 +9,9 @@ export interface OCPP16HeartbeatResponse extends JsonObject {
 }
 
 export enum OCPP16UnlockStatus {
-  UNLOCKED = 'Unlocked',
+  NOT_SUPPORTED = 'NotSupported',
   UNLOCK_FAILED = 'UnlockFailed',
-  NOT_SUPPORTED = 'NotSupported'
+  UNLOCKED = 'Unlocked'
 }
 
 export interface UnlockConnectorResponse extends JsonObject {
@@ -20,9 +20,9 @@ export interface UnlockConnectorResponse extends JsonObject {
 
 export enum OCPP16ConfigurationStatus {
   ACCEPTED = 'Accepted',
-  REJECTED = 'Rejected',
+  NOT_SUPPORTED = 'NotSupported',
   REBOOT_REQUIRED = 'RebootRequired',
-  NOT_SUPPORTED = 'NotSupported'
+  REJECTED = 'Rejected'
 }
 
 export interface ChangeConfigurationResponse extends JsonObject {
@@ -30,9 +30,9 @@ export interface ChangeConfigurationResponse extends JsonObject {
 }
 
 export interface OCPP16BootNotificationResponse extends JsonObject {
-  status: RegistrationStatusEnumType
   currentTime: Date
   interval: number
+  status: RegistrationStatusEnumType
 }
 
 export type OCPP16StatusNotificationResponse = EmptyObject
@@ -44,15 +44,15 @@ export interface GetConfigurationResponse extends JsonObject {
 
 export enum OCPP16ChargingProfileStatus {
   ACCEPTED = 'Accepted',
-  REJECTED = 'Rejected',
-  NOT_SUPPORTED = 'NotSupported'
+  NOT_SUPPORTED = 'NotSupported',
+  REJECTED = 'Rejected'
 }
 
 export interface OCPP16GetCompositeScheduleResponse extends JsonObject {
-  status: GenericStatus
+  chargingSchedule?: OCPP16ChargingSchedule
   connectorId?: number
   scheduleStart?: Date
-  chargingSchedule?: OCPP16ChargingSchedule
+  status: GenericStatus
 }
 
 export interface SetChargingProfileResponse extends JsonObject {
@@ -90,8 +90,8 @@ export type OCPP16DiagnosticsStatusNotificationResponse = EmptyObject
 
 export enum OCPP16TriggerMessageStatus {
   ACCEPTED = 'Accepted',
-  REJECTED = 'Rejected',
-  NOT_IMPLEMENTED = 'NotImplemented'
+  NOT_IMPLEMENTED = 'NotImplemented',
+  REJECTED = 'Rejected'
 }
 
 export interface OCPP16TriggerMessageResponse extends JsonObject {
@@ -106,17 +106,17 @@ export enum OCPP16DataTransferStatus {
 }
 
 export interface OCPP16DataTransferResponse extends JsonObject {
-  status: OCPP16DataTransferStatus
   data?: string
+  status: OCPP16DataTransferStatus
 }
 
 export enum OCPP16ReservationStatus {
   ACCEPTED = 'Accepted',
   FAULTED = 'Faulted',
+  NOT_SUPPORTED = 'NotSupported',
   OCCUPIED = 'Occupied',
   REJECTED = 'Rejected',
-  UNAVAILABLE = 'Unavailable',
-  NOT_SUPPORTED = 'NotSupported'
+  UNAVAILABLE = 'Unavailable'
 }
 
 export interface OCPP16ReserveNowResponse extends JsonObject {
index f9659f695398822e3932197137b8ee581626d6d2..6adb32203601a5217d07fc72945ad03e8728b2d8 100644 (file)
@@ -2,6 +2,7 @@ import type { JsonObject } from '../../JsonType.js'
 import type { OCPP16MeterValue } from './MeterValues.js'
 
 export enum OCPP16StopTransactionReason {
+  DE_AUTHORIZED = 'DeAuthorized',
   EMERGENCY_STOP = 'EmergencyStop',
   EV_DISCONNECTED = 'EVDisconnected',
   HARD_RESET = 'HardReset',
@@ -11,22 +12,21 @@ export enum OCPP16StopTransactionReason {
   REBOOT = 'Reboot',
   REMOTE = 'Remote',
   SOFT_RESET = 'SoftReset',
-  UNLOCK_COMMAND = 'UnlockCommand',
-  DE_AUTHORIZED = 'DeAuthorized'
+  UNLOCK_COMMAND = 'UnlockCommand'
 }
 
 export enum OCPP16AuthorizationStatus {
   ACCEPTED = 'Accepted',
   BLOCKED = 'Blocked',
+  CONCURRENT_TX = 'ConcurrentTx',
   EXPIRED = 'Expired',
-  INVALID = 'Invalid',
-  CONCURRENT_TX = 'ConcurrentTx'
+  INVALID = 'Invalid'
 }
 
 interface IdTagInfo extends JsonObject {
-  status: OCPP16AuthorizationStatus
-  parentIdTag?: string
   expiryDate?: Date
+  parentIdTag?: string
+  status: OCPP16AuthorizationStatus
 }
 
 export interface OCPP16AuthorizeRequest extends JsonObject {
@@ -41,8 +41,8 @@ export interface OCPP16StartTransactionRequest extends JsonObject {
   connectorId: number
   idTag: string
   meterStart: number
-  timestamp: Date
   reservationId?: number
+  timestamp: Date
 }
 
 export interface OCPP16StartTransactionResponse extends JsonObject {
@@ -53,10 +53,10 @@ export interface OCPP16StartTransactionResponse extends JsonObject {
 export interface OCPP16StopTransactionRequest extends JsonObject {
   idTag?: string
   meterStop: number
-  timestamp: Date
-  transactionId: number
   reason?: OCPP16StopTransactionReason
+  timestamp: Date
   transactionData?: OCPP16MeterValue[]
+  transactionId: number
 }
 
 export interface OCPP16StopTransactionResponse extends JsonObject {
index 785c604646654b509a803ff4ec57140a397a7a38..7f5fc65d47c309d6280b1f760dc6548af02341d8 100644 (file)
@@ -2,14 +2,14 @@ import type { JsonObject } from '../../JsonType.js'
 import type { GenericStatus } from '../Common.js'
 
 export enum DataEnumType {
-  string = 'string',
+  boolean = 'boolean',
+  dateTime = 'dateTime',
   decimal = 'decimal',
   integer = 'integer',
-  dateTime = 'dateTime',
-  boolean = 'boolean',
+  MemberList = 'MemberList',
   OptionList = 'OptionList',
   SequenceList = 'SequenceList',
-  MemberList = 'MemberList'
+  string = 'string'
 }
 
 export enum BootReasonEnumType {
@@ -25,16 +25,16 @@ export enum BootReasonEnumType {
 }
 
 export enum OperationalStatusEnumType {
-  Operative = 'Operative',
-  Inoperative = 'Inoperative'
+  Inoperative = 'Inoperative',
+  Operative = 'Operative'
 }
 
 export enum OCPP20ConnectorStatusEnumType {
   Available = 'Available',
+  Faulted = 'Faulted',
   Occupied = 'Occupied',
   Reserved = 'Reserved',
-  Unavailable = 'Unavailable',
-  Faulted = 'Faulted'
+  Unavailable = 'Unavailable'
 }
 
 export type GenericStatusEnumType = GenericStatus
@@ -46,11 +46,11 @@ export enum HashAlgorithmEnumType {
 }
 
 export enum GetCertificateIdUseEnumType {
-  V2GRootCertificate = 'V2GRootCertificate',
-  MORootCertificate = 'MORootCertificate',
   CSMSRootCertificate = 'CSMSRootCertificate',
+  ManufacturerRootCertificate = 'ManufacturerRootCertificate',
+  MORootCertificate = 'MORootCertificate',
   V2GCertificateChain = 'V2GCertificateChain',
-  ManufacturerRootCertificate = 'ManufacturerRootCertificate'
+  V2GRootCertificate = 'V2GRootCertificate'
 }
 
 export enum GetCertificateStatusEnumType {
@@ -65,15 +65,15 @@ export enum GetInstalledCertificateStatusEnumType {
 
 export enum InstallCertificateStatusEnumType {
   Accepted = 'Accepted',
-  Rejected = 'Rejected',
-  Failed = 'Failed'
+  Failed = 'Failed',
+  Rejected = 'Rejected'
 }
 
 export enum InstallCertificateUseEnumType {
-  V2GRootCertificate = 'V2GRootCertificate',
-  MORootCertificate = 'MORootCertificate',
   CSMSRootCertificate = 'CSMSRootCertificate',
-  ManufacturerRootCertificate = 'ManufacturerRootCertificate'
+  ManufacturerRootCertificate = 'ManufacturerRootCertificate',
+  MORootCertificate = 'MORootCertificate',
+  V2GRootCertificate = 'V2GRootCertificate'
 }
 
 export enum DeleteCertificateStatusEnumType {
@@ -96,31 +96,31 @@ export type CertificateSignedStatusEnumType = GenericStatusEnumType
 
 export interface CertificateHashDataType extends JsonObject {
   hashAlgorithm: HashAlgorithmEnumType
-  issuerNameHash: string
   issuerKeyHash: string
+  issuerNameHash: string
   serialNumber: string
 }
 
 export interface CertificateHashDataChainType extends JsonObject {
-  certificateType: GetCertificateIdUseEnumType
   certificateHashData: CertificateHashDataType
+  certificateType: GetCertificateIdUseEnumType
   childCertificateHashData?: CertificateHashDataType
 }
 
 export interface OCSPRequestDataType extends JsonObject {
   hashAlgorithm: HashAlgorithmEnumType
-  issuerNameHash: string
   issuerKeyHash: string
-  serialNumber: string
+  issuerNameHash: string
   responderURL: string
+  serialNumber: string
 }
 
 export interface StatusInfoType extends JsonObject {
-  reasonCode: string
   additionalInfo?: string
+  reasonCode: string
 }
 
 export interface EVSEType extends JsonObject {
-  id: number
   connectorId?: string
+  id: number
 }
index 2673cbaf9eccb0894b4b291ee976fff78eee16e8..7c0e38523c7cd8abf6c2091facfbb1e6887c1e4e 100644 (file)
@@ -25,16 +25,16 @@ interface ModemType extends JsonObject {
 }
 
 interface ChargingStationType extends JsonObject {
-  serialNumber?: string
-  model: string
-  vendorName: string
   firmwareVersion?: string
+  model: string
   modem?: ModemType
+  serialNumber?: string
+  vendorName: string
 }
 
 export interface OCPP20BootNotificationRequest extends JsonObject {
-  reason: BootReasonEnumType
   chargingStation: ChargingStationType
+  reason: BootReasonEnumType
 }
 
 export type OCPP20HeartbeatRequest = EmptyObject
@@ -42,10 +42,10 @@ export type OCPP20HeartbeatRequest = EmptyObject
 export type OCPP20ClearCacheRequest = EmptyObject
 
 export interface OCPP20StatusNotificationRequest extends JsonObject {
-  timestamp: Date
+  connectorId: number
   connectorStatus: OCPP20ConnectorStatusEnumType
   evseId: number
-  connectorId: number
+  timestamp: Date
 }
 
 export interface OCPP20SetVariablesRequest extends JsonObject {
@@ -53,6 +53,6 @@ export interface OCPP20SetVariablesRequest extends JsonObject {
 }
 
 export interface OCPP20InstallCertificateRequest extends JsonObject {
-  certificateType: InstallCertificateUseEnumType
   certificate: string
+  certificateType: InstallCertificateUseEnumType
 }
index b0f281c7cbda063580421cd646018259b4949266..816eea0bcaa7ab2bfa2247c6982eb3fc5c934efb 100644 (file)
@@ -10,8 +10,8 @@ import type { OCPP20SetVariableResultType } from './Variables.js'
 
 export interface OCPP20BootNotificationResponse extends JsonObject {
   currentTime: Date
-  status: RegistrationStatusEnumType
   interval: number
+  status: RegistrationStatusEnumType
   statusInfo?: StatusInfoType
 }
 
index 04a344564bcd202e3a6ab5dcef35798545b09620..0f35ee8039f05160269375cf24fec89e67e30f98 100644 (file)
@@ -23,34 +23,34 @@ enum OCPP20ComponentName {
 }
 
 export enum OCPP20RequiredVariableName {
-  MessageTimeout = 'MessageTimeout',
+  AuthorizeRemoteStart = 'AuthorizeRemoteStart',
+  BytesPerMessage = 'BytesPerMessage',
+  CertificateEntries = 'CertificateEntries',
+  DateTime = 'DateTime',
+  EVConnectionTimeOut = 'EVConnectionTimeOut',
   FileTransferProtocols = 'FileTransferProtocols',
+  ItemsPerMessage = 'ItemsPerMessage',
+  LocalAuthorizeOffline = 'LocalAuthorizeOffline',
+  LocalPreAuthorize = 'LocalPreAuthorize',
+  MessageAttemptInterval = 'MessageAttemptInterval',
+  MessageAttempts = 'TransactionEvent',
+  MessageTimeout = 'MessageTimeout',
   NetworkConfigurationPriority = 'NetworkConfigurationPriority',
   NetworkProfileConnectionAttempts = 'NetworkProfileConnectionAttempts',
   OfflineThreshold = 'OfflineThreshold',
-  MessageAttempts = 'TransactionEvent',
-  MessageAttemptInterval = 'MessageAttemptInterval',
-  UnlockOnEVSideDisconnect = 'UnlockOnEVSideDisconnect',
-  ResetRetries = 'ResetRetries',
-  ItemsPerMessage = 'ItemsPerMessage',
-  BytesPerMessage = 'BytesPerMessage',
-  DateTime = 'DateTime',
-  TimeSource = 'TimeSource',
   OrganizationName = 'OrganizationName',
-  CertificateEntries = 'CertificateEntries',
+  ResetRetries = 'ResetRetries',
   SecurityProfile = 'SecurityProfile',
-  AuthorizeRemoteStart = 'AuthorizeRemoteStart',
-  LocalAuthorizeOffline = 'LocalAuthorizeOffline',
-  LocalPreAuthorize = 'LocalPreAuthorize',
-  EVConnectionTimeOut = 'EVConnectionTimeOut',
   StopTxOnEVSideDisconnect = 'StopTxOnEVSideDisconnect',
-  TxStartPoint = 'TxStartPoint',
-  TxStopPoint = 'TxStopPoint',
   StopTxOnInvalidId = 'StopTxOnInvalidId',
+  TimeSource = 'TimeSource',
   TxEndedMeasurands = 'TxEndedMeasurands',
   TxStartedMeasurands = 'TxStartedMeasurands',
+  TxStartPoint = 'TxStartPoint',
+  TxStopPoint = 'TxStopPoint',
+  TxUpdatedInterval = 'TxUpdatedInterval',
   TxUpdatedMeasurands = 'TxUpdatedMeasurands',
-  TxUpdatedInterval = 'TxUpdatedInterval'
+  UnlockOnEVSideDisconnect = 'UnlockOnEVSideDisconnect'
 }
 
 export enum OCPP20OptionalVariableName {
@@ -64,26 +64,26 @@ export enum OCPP20VendorVariableName {
 
 enum AttributeEnumType {
   Actual = 'Actual',
-  Target = 'Target',
+  MaxSet = 'MaxSet',
   MinSet = 'MinSet',
-  MaxSet = 'MaxSet'
+  Target = 'Target'
 }
 
 interface ComponentType extends JsonObject {
-  name: string | OCPP20ComponentName
-  instance?: string
   evse?: EVSEType
+  instance?: string
+  name: OCPP20ComponentName | string
 }
 
 type VariableName =
-  | string
-  | OCPP20RequiredVariableName
   | OCPP20OptionalVariableName
+  | OCPP20RequiredVariableName
   | OCPP20VendorVariableName
+  | string
 
 interface VariableType extends JsonObject {
-  name: VariableName
   instance?: string
+  name: VariableName
 }
 
 export interface OCPP20SetVariableDataType extends JsonObject {
@@ -95,19 +95,19 @@ export interface OCPP20SetVariableDataType extends JsonObject {
 
 enum SetVariableStatusEnumType {
   Accepted = 'Accepted',
+  NotSupportedAttributeType = 'NotSupportedAttributeType',
+  RebootRequired = 'RebootRequired',
   Rejected = 'Rejected',
   UnknownComponent = 'UnknownComponent',
-  UnknownVariable = 'UnknownVariable',
-  NotSupportedAttributeType = 'NotSupportedAttributeType',
-  RebootRequired = 'RebootRequired'
+  UnknownVariable = 'UnknownVariable'
 }
 
 export interface OCPP20SetVariableResultType extends JsonObject {
-  attributeType?: AttributeEnumType
   attributeStatus: SetVariableStatusEnumType
+  attributeStatusInfo?: StatusInfoType
+  attributeType?: AttributeEnumType
   component: ComponentType
   variable: VariableType
-  attributeStatusInfo?: StatusInfoType
 }
 
 export interface OCPP20ComponentVariableType extends JsonObject {
index c4dbba8ad61b3c4b76aaa0fc41b934337ba6ed4f..26790402cf82235cdf73419bf27beefdf5045a1c 100644 (file)
@@ -1,4 +1,5 @@
 import type { JsonObject } from '../JsonType.js'
+
 import {
   OCPP16StandardParametersKey,
   OCPP16SupportedFeatureProfiles,
@@ -33,16 +34,16 @@ export type SupportedFeatureProfiles = OCPP16SupportedFeatureProfiles
 
 export enum ConnectorPhaseRotation {
   NotApplicable = 'NotApplicable',
-  Unknown = 'Unknown',
   RST = 'RST',
   RTS = 'RTS',
   SRT = 'SRT',
   STR = 'STR',
   TRS = 'TRS',
-  TSR = 'TSR'
+  TSR = 'TSR',
+  Unknown = 'Unknown'
 }
 
-export type ConfigurationKeyType = string | StandardParametersKey | VendorParametersKey
+export type ConfigurationKeyType = StandardParametersKey | string | VendorParametersKey
 
 export interface OCPPConfigurationKey extends JsonObject {
   key: ConfigurationKeyType
index d6d8357fc71cef712facbd4b305f913aa08937ac..d405ee4dc8d367e0070c6ab07bce78616a9f6e27 100644 (file)
@@ -1,23 +1,23 @@
 export enum ErrorType {
+  FORMAT_VIOLATION = 'FormatViolation',
+  // Payload for Action is syntactically incorrect or not conform the PDU structure for Action
+  FORMATION_VIOLATION = 'FormationViolation',
+  // Any other error not covered by the previous ones
+  GENERIC_ERROR = 'GenericError',
+  // An internal error occurred and the receiver was not able to process the requested Action successfully
+  INTERNAL_ERROR = 'InternalError',
   // Requested Action is not known by receiver
   NOT_IMPLEMENTED = 'NotImplemented',
   // Requested Action is recognized but not supported by the receiver
   NOT_SUPPORTED = 'NotSupported',
-  // An internal error occurred and the receiver was not able to process the requested Action successfully
-  INTERNAL_ERROR = 'InternalError',
+  // Payload for Action is syntactically correct but at least one of the fields violates occurrence constraints
+  OCCURRENCE_CONSTRAINT_VIOLATION = 'OccurrenceConstraintViolation',
+  // Payload is syntactically correct but at least one field contains an invalid value
+  PROPERTY_CONSTRAINT_VIOLATION = 'PropertyConstraintViolation',
   // Payload for Action is incomplete
   PROTOCOL_ERROR = 'ProtocolError',
   // During the processing of Action a security issue occurred preventing receiver from completing the Action successfully
   SECURITY_ERROR = 'SecurityError',
-  // Payload for Action is syntactically incorrect or not conform the PDU structure for Action
-  FORMATION_VIOLATION = 'FormationViolation',
-  FORMAT_VIOLATION = 'FormatViolation',
-  // Payload is syntactically correct but at least one field contains an invalid value
-  PROPERTY_CONSTRAINT_VIOLATION = 'PropertyConstraintViolation',
-  // Payload for Action is syntactically correct but at least one of the fields violates occurrence constraints
-  OCCURRENCE_CONSTRAINT_VIOLATION = 'OccurrenceConstraintViolation',
   // Payload for Action is syntactically correct but at least one of the fields violates data type constraints (e.g. "somestring" = 12)
-  TYPE_CONSTRAINT_VIOLATION = 'TypeConstraintViolation',
-  // Any other error not covered by the previous ones
-  GENERIC_ERROR = 'GenericError'
+  TYPE_CONSTRAINT_VIOLATION = 'TypeConstraintViolation'
 }
index 972ed474cc15fbdabc6b9a5049f5f44a8738091e..446f25c4533f028e8339440f61a1fb105ef1d4a1 100644 (file)
@@ -1,5 +1,5 @@
 export enum MessageType {
+  CALL_ERROR_MESSAGE = 4, // Callee to Caller
   CALL_MESSAGE = 2, // Caller to Callee
-  CALL_RESULT_MESSAGE = 3, // Callee to Caller
-  CALL_ERROR_MESSAGE = 4 // Callee to Caller
+  CALL_RESULT_MESSAGE = 3 // Callee to Caller
 }
index 688cd7ff006cea8e13de854c1205d93a5d9e0a55..a6171b2d8273772c6cc63a1049e5723af6b3d522 100644 (file)
@@ -1,8 +1,10 @@
 import type { ChargingStation } from '../../charging-station/index.js'
 import type { OCPPError } from '../../exception/index.js'
 import type { JsonType } from '../JsonType.js'
-import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus.js'
 import type { OCPP16MeterValuesRequest } from './1.6/MeterValues.js'
+import type { MessageType } from './MessageType.js'
+
+import { OCPP16DiagnosticsStatus } from './1.6/DiagnosticsStatus.js'
 import {
   OCPP16AvailabilityType,
   type OCPP16BootNotificationRequest,
@@ -25,7 +27,6 @@ import {
   OCPP20RequestCommand,
   type OCPP20StatusNotificationRequest,
 } from './2.0/Requests.js'
-import type { MessageType } from './MessageType.js'
 
 export const RequestCommand = {
   ...OCPP16RequestCommand,
@@ -38,8 +39,8 @@ export type OutgoingRequest = [MessageType.CALL_MESSAGE, string, RequestCommand,
 
 export interface RequestParams {
   skipBufferingOnError?: boolean
-  triggerMessage?: boolean
   throwError?: boolean
+  triggerMessage?: boolean
 }
 
 export const IncomingRequestCommand = {
@@ -63,7 +64,7 @@ export type ErrorCallback = (ocppError: OCPPError, requestStatistic?: boolean) =
 export type CachedRequest = [
   ResponseCallback,
   ErrorCallback,
-  RequestCommand | IncomingRequestCommand,
+  IncomingRequestCommand | RequestCommand,
   JsonType
 ]
 
index 5f15838a5d303639bbc1ebff347bd500809eb67b..03d085ea80fcd0711cb0f4276f3eb72f4630c1a2 100644 (file)
@@ -5,9 +5,9 @@ export type Reservation = OCPP16ReserveNowRequest
 export type ReservationKey = keyof Reservation
 
 export enum ReservationTerminationReason {
-  EXPIRED = 'Expired',
-  TRANSACTION_STARTED = 'TransactionStarted',
   CONNECTOR_STATE_CHANGED = 'ConnectorStateChanged',
+  EXPIRED = 'Expired',
+  REPLACE_EXISTING = 'ReplaceExisting',
   RESERVATION_CANCELED = 'ReservationCanceled',
-  REPLACE_EXISTING = 'ReplaceExisting'
+  TRANSACTION_STARTED = 'TransactionStarted'
 }
index 43b595150f8a999e19a3423bc91662e504a578b5..9676ff74ecc9d925083bc2fc46bdea284caaa237 100644 (file)
@@ -1,6 +1,10 @@
 import type { ChargingStation } from '../../charging-station/index.js'
 import type { JsonType } from '../JsonType.js'
 import type { OCPP16MeterValuesResponse } from './1.6/MeterValues.js'
+import type { OCPP20BootNotificationResponse, OCPP20ClearCacheResponse } from './2.0/Responses.js'
+import type { ErrorType } from './ErrorType.js'
+import type { MessageType } from './MessageType.js'
+
 import {
   OCPP16AvailabilityStatus,
   type OCPP16BootNotificationResponse,
@@ -17,10 +21,7 @@ import {
   OCPP16TriggerMessageStatus,
   OCPP16UnlockStatus,
 } from './1.6/Responses.js'
-import type { OCPP20BootNotificationResponse, OCPP20ClearCacheResponse } from './2.0/Responses.js'
 import { type GenericResponse, GenericStatus } from './Common.js'
-import type { ErrorType } from './ErrorType.js'
-import type { MessageType } from './MessageType.js'
 
 export type Response = [MessageType.CALL_RESULT_MESSAGE, string, JsonType]
 
@@ -30,7 +31,7 @@ export type ResponseHandler = (
   chargingStation: ChargingStation,
   payload: JsonType,
   requestPayload?: JsonType
-) => void | Promise<void>
+) => Promise<void> | void
 
 export type BootNotificationResponse =
   | OCPP16BootNotificationResponse
index 53cc5108d370be873bc57ae6e3431e99579eeed1..f0206ab01b6d68a60da7d023ea8e132367a94b73 100644 (file)
@@ -1,27 +1,30 @@
 import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
 
 interface StatisticsData {
-  name: string
-  requestCount: number
-  responseCount: number
+  avgTimeMeasurement: number
+  currentTimeMeasurement: number
   errorCount: number
-  timeMeasurementCount: number
+  maxTimeMeasurement: number
   measurementTimeSeries: {
     timestamp: number
     value: number
   }[]
-  currentTimeMeasurement: number
-  minTimeMeasurement: number
-  maxTimeMeasurement: number
-  totalTimeMeasurement: number
-  avgTimeMeasurement: number
   medTimeMeasurement: number
+  minTimeMeasurement: number
+  name: string
   ninetyFiveThPercentileTimeMeasurement: number
+  requestCount: number
+  responseCount: number
   stdDevTimeMeasurement: number
+  timeMeasurementCount: number
+  totalTimeMeasurement: number
 }
 
 @Entity()
 export class PerformanceRecord {
+  @Property()
+  createdAt!: Date
+
   @PrimaryKey()
   id!: string
 
@@ -29,14 +32,11 @@ export class PerformanceRecord {
   name!: string
 
   @Property()
-  uri!: string
-
-  @Property()
-  createdAt!: Date
+  statisticsData!: Partial<StatisticsData>[]
 
   @Property()
   updatedAt?: Date
 
   @Property()
-  statisticsData!: Partial<StatisticsData>[]
+  uri!: string
 }
index 5f505a537b07ca47d523010d051c22ebb01dc477..6c25adbd7d0a90005f6b7e3645cc0ea2ba4e1830 100644 (file)
@@ -9,7 +9,7 @@ export enum AsyncLockType {
   performance = 'performance'
 }
 
-type ResolveType = (value: void | PromiseLike<void>) => void
+type ResolveType = (value: PromiseLike<void> | void) => void
 
 export class AsyncLock {
   private static readonly asyncLocks = new Map<AsyncLockType, AsyncLock>()
@@ -21,19 +21,6 @@ export class AsyncLock {
     this.resolveQueue = new Queue<ResolveType>()
   }
 
-  public static async runExclusive<T>(type: AsyncLockType, fn: () => T | Promise<T>): Promise<T> {
-    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<void> {
     const asyncLock = AsyncLock.getAsyncLock(type)
     if (!asyncLock.acquired) {
@@ -45,6 +32,14 @@ export class AsyncLock {
     })
   }
 
+  private static getAsyncLock (type: AsyncLockType): AsyncLock {
+    if (!AsyncLock.asyncLocks.has(type)) {
+      AsyncLock.asyncLocks.set(type, new AsyncLock())
+    }
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    return AsyncLock.asyncLocks.get(type)!
+  }
+
   private static async release (type: AsyncLockType): Promise<void> {
     const asyncLock = AsyncLock.getAsyncLock(type)
     if (asyncLock.resolveQueue.size === 0 && asyncLock.acquired) {
@@ -58,11 +53,16 @@ export class AsyncLock {
     })
   }
 
-  private static getAsyncLock (type: AsyncLockType): AsyncLock {
-    if (!AsyncLock.asyncLocks.has(type)) {
-      AsyncLock.asyncLocks.set(type, new AsyncLock())
+  public static async runExclusive<T>(type: AsyncLockType, fn: () => Promise<T> | T): Promise<T> {
+    try {
+      await AsyncLock.acquire(type)
+      if (isAsyncFunction(fn)) {
+        return await fn()
+      } else {
+        return fn() as T
+      }
+    } finally {
+      await AsyncLock.release(type)
     }
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    return AsyncLock.asyncLocks.get(type)!
   }
 }
index c6717a74289554b5dec43c8d4a5da32178483b7e..ddeb012731e9c38efb0c326e560dc0089ea09aed 100644 (file)
@@ -33,7 +33,7 @@ export enum OutputFormat {
 export const buildEvsesStatus = (
   chargingStation: ChargingStation,
   outputFormat: OutputFormat = OutputFormat.configuration
-): (EvseStatusWorkerType | EvseStatusConfiguration)[] => {
+): (EvseStatusConfiguration | EvseStatusWorkerType)[] => {
   // eslint-disable-next-line array-callback-return
   return [...chargingStation.evses.values()].map(evseStatus => {
     const connectorsStatus = [...evseStatus.connectors.values()].map(
@@ -41,11 +41,6 @@ export const buildEvsesStatus = (
     )
     let status: EvseStatusConfiguration
     switch (outputFormat) {
-      case OutputFormat.worker:
-        return {
-          ...evseStatus,
-          connectors: connectorsStatus,
-        }
       case OutputFormat.configuration:
         status = {
           ...evseStatus,
@@ -53,6 +48,11 @@ export const buildEvsesStatus = (
         }
         delete (status as EvseStatusWorkerType).connectors
         return status
+      case OutputFormat.worker:
+        return {
+          ...evseStatus,
+          connectors: connectorsStatus,
+        }
     }
   })
 }
index 2e599d66a79408465555df2bb2e78becb1214c37..9ca0b9135bed82312bb5997f8586e32dd04c1180 100644 (file)
@@ -1,9 +1,8 @@
+import chalk from 'chalk'
 import { existsSync, type FSWatcher, readFileSync, watch } from 'node:fs'
 import { dirname, join } from 'node:path'
 import { env } from 'node:process'
 import { fileURLToPath } from 'node:url'
-
-import chalk from 'chalk'
 import { mergeDeepRight, once } from 'rambda'
 
 import {
@@ -41,17 +40,17 @@ import { hasOwnProp, isCFEnvironment } from './Utils.js'
 type ConfigurationSectionType =
   | LogConfiguration
   | StorageConfiguration
-  | WorkerConfiguration
   | UIServerConfiguration
+  | WorkerConfiguration
 
 const defaultUIServerConfiguration: UIServerConfiguration = {
   enabled: false,
-  type: ApplicationProtocol.WS,
-  version: ApplicationProtocolVersion.VERSION_11,
   options: {
     host: Constants.DEFAULT_UI_SERVER_HOST,
     port: Constants.DEFAULT_UI_SERVER_PORT,
   },
+  type: ApplicationProtocol.WS,
+  version: ApplicationProtocolVersion.VERSION_11,
 }
 
 const defaultStorageConfiguration: StorageConfiguration = {
@@ -61,169 +60,77 @@ const defaultStorageConfiguration: StorageConfiguration = {
 
 const defaultLogConfiguration: LogConfiguration = {
   enabled: true,
-  file: 'logs/combined.log',
   errorFile: 'logs/error.log',
-  statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
-  level: 'info',
+  file: 'logs/combined.log',
   format: 'simple',
+  level: 'info',
   rotate: true,
+  statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
 }
 
 const defaultWorkerConfiguration: WorkerConfiguration = {
-  processType: WorkerProcessType.workerSet,
-  startDelay: DEFAULT_WORKER_START_DELAY,
-  elementsPerWorker: 'auto',
   elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY,
-  poolMinSize: DEFAULT_POOL_MIN_SIZE,
+  elementsPerWorker: 'auto',
   poolMaxSize: DEFAULT_POOL_MAX_SIZE,
+  poolMinSize: DEFAULT_POOL_MIN_SIZE,
+  processType: WorkerProcessType.workerSet,
+  startDelay: DEFAULT_WORKER_START_DELAY,
 }
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class Configuration {
   public static configurationChangeCallback?: () => Promise<void>
 
+  private static configurationData?: ConfigurationData
   private static configurationFile: string | undefined
   private static configurationFileReloading = false
-  private static configurationData?: ConfigurationData
   private static configurationFileWatcher?: FSWatcher
   private static configurationSectionCache: Map<ConfigurationSection, ConfigurationSectionType>
 
-  static {
-    const configurationFile = join(dirname(fileURLToPath(import.meta.url)), 'assets', 'config.json')
-    if (existsSync(configurationFile)) {
-      Configuration.configurationFile = configurationFile
-    } else {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          "Configuration file './src/assets/config.json' not found, using default configuration"
-        )}`
-      )
-      Configuration.configurationData = {
-        stationTemplateUrls: [
-          {
-            file: 'siemens.station-template.json',
-            numberOfStations: 1,
-          },
-        ],
-        supervisionUrls: 'ws://localhost:8180/steve/websocket/CentralSystemService',
-        supervisionUrlDistribution: SupervisionUrlDistribution.ROUND_ROBIN,
-        uiServer: defaultUIServerConfiguration,
-        performanceStorage: defaultStorageConfiguration,
-        log: defaultLogConfiguration,
-        worker: defaultWorkerConfiguration,
-      }
-    }
-    Configuration.configurationSectionCache = new Map<
-      ConfigurationSection,
-      ConfigurationSectionType
-    >([
-      [ConfigurationSection.log, Configuration.buildLogSection()],
-      [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
-      [ConfigurationSection.worker, Configuration.buildWorkerSection()],
-      [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
-    ])
-  }
-
   private constructor () {
     // This is intentional
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
-  public static getConfigurationSection<T extends ConfigurationSectionType>(
-    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 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 getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
-    return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
-      ? Configuration.getConfigurationData()?.supervisionUrlDistribution
-      : SupervisionUrlDistribution.ROUND_ROBIN
-  }
-
-  public static workerPoolInUse (): boolean {
-    return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
-        .processType!
-    )
-  }
-
-  public static workerDynamicPoolInUse (): boolean {
-    return (
-      Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
-        .processType === WorkerProcessType.dynamicPool
-    )
-  }
-
-  private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
-    return Configuration.configurationSectionCache.has(sectionName)
-  }
-
-  private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
-    switch (sectionName) {
-      case ConfigurationSection.log:
-        Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
-        break
-      case ConfigurationSection.performanceStorage:
-        Configuration.configurationSectionCache.set(
-          sectionName,
-          Configuration.buildPerformanceStorageSection()
-        )
-        break
-      case ConfigurationSection.worker:
-        Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
-        break
-      case ConfigurationSection.uiServer:
-        Configuration.configurationSectionCache.set(
-          sectionName,
-          Configuration.buildUIServerSection()
-        )
-        break
-      default:
-        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-        throw new Error(`Unknown configuration section '${sectionName}'`)
-    }
-  }
-
-  private static buildUIServerSection (): UIServerConfiguration {
-    let uiServerConfiguration: UIServerConfiguration = defaultUIServerConfiguration
-    if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
-      uiServerConfiguration = mergeDeepRight(
-        uiServerConfiguration,
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        Configuration.getConfigurationData()!.uiServer!
-      )
+  private static buildLogSection (): LogConfiguration {
+    const deprecatedLogConfiguration: LogConfiguration = {
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
+        enabled: Configuration.getConfigurationData()?.logEnabled,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
+        file: Configuration.getConfigurationData()?.logFile,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
+        errorFile: Configuration.getConfigurationData()?.logErrorFile,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
+        statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
+        level: Configuration.getConfigurationData()?.logLevel,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
+        console: Configuration.getConfigurationData()?.logConsole,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
+        format: Configuration.getConfigurationData()?.logFormat,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
+        rotate: Configuration.getConfigurationData()?.logRotate,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
+        maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
+      }),
+      ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
+        maxSize: Configuration.getConfigurationData()?.logMaxSize,
+      }),
     }
-    if (isCFEnvironment()) {
-      delete uiServerConfiguration.options?.host
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      uiServerConfiguration.options!.port = Number.parseInt(env.PORT!)
+    const logConfiguration: LogConfiguration = {
+      ...defaultLogConfiguration,
+      ...deprecatedLogConfiguration,
+      ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
+        Configuration.getConfigurationData()?.log),
     }
-    return uiServerConfiguration
+    return logConfiguration
   }
 
   private static buildPerformanceStorageSection (): StorageConfiguration {
@@ -266,46 +173,21 @@ export class Configuration {
     return storageConfiguration
   }
 
-  private static buildLogSection (): LogConfiguration {
-    const deprecatedLogConfiguration: LogConfiguration = {
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
-        enabled: Configuration.getConfigurationData()?.logEnabled,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
-        file: Configuration.getConfigurationData()?.logFile,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
-        errorFile: Configuration.getConfigurationData()?.logErrorFile,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
-        statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
-        level: Configuration.getConfigurationData()?.logLevel,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
-        console: Configuration.getConfigurationData()?.logConsole,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
-        format: Configuration.getConfigurationData()?.logFormat,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
-        rotate: Configuration.getConfigurationData()?.logRotate,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
-        maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
-      }),
-      ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
-        maxSize: Configuration.getConfigurationData()?.logMaxSize,
-      }),
+  private static buildUIServerSection (): UIServerConfiguration {
+    let uiServerConfiguration: UIServerConfiguration = defaultUIServerConfiguration
+    if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
+      uiServerConfiguration = mergeDeepRight(
+        uiServerConfiguration,
+        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+        Configuration.getConfigurationData()!.uiServer!
+      )
     }
-    const logConfiguration: LogConfiguration = {
-      ...defaultLogConfiguration,
-      ...deprecatedLogConfiguration,
-      ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
-        Configuration.getConfigurationData()?.log),
+    if (isCFEnvironment()) {
+      delete uiServerConfiguration.options?.host
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      uiServerConfiguration.options!.port = Number.parseInt(env.PORT!)
     }
-    return logConfiguration
+    return uiServerConfiguration
   }
 
   private static buildWorkerSection (): WorkerConfiguration {
@@ -346,6 +228,32 @@ export class Configuration {
     return workerConfiguration
   }
 
+  private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
+    switch (sectionName) {
+      case ConfigurationSection.log:
+        Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
+        break
+      case ConfigurationSection.performanceStorage:
+        Configuration.configurationSectionCache.set(
+          sectionName,
+          Configuration.buildPerformanceStorageSection()
+        )
+        break
+      case ConfigurationSection.uiServer:
+        Configuration.configurationSectionCache.set(
+          sectionName,
+          Configuration.buildUIServerSection()
+        )
+        break
+      case ConfigurationSection.worker:
+        Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
+        break
+      default:
+        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+        throw new Error(`Unknown configuration section '${sectionName}'`)
+    }
+  }
+
   private static checkDeprecatedConfigurationKeys (): void {
     // connection timeout
     Configuration.warnDeprecatedConfigurationKey(
@@ -539,39 +447,6 @@ export class Configuration {
     }
   }
 
-  private static warnDeprecatedConfigurationKey (
-    key: string,
-    configurationSection?: ConfigurationSection,
-    logMsgToAppend = ''
-  ): void {
-    if (
-      configurationSection != null &&
-      Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
-        null &&
-      (
-        Configuration.getConfigurationData()?.[
-          configurationSection as keyof ConfigurationData
-        ] as Record<string, unknown>
-      )[key] != null
-    ) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
-            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
-          }`
-        )}`
-      )
-    } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
-      console.error(
-        `${chalk.green(logPrefix())} ${chalk.red(
-          `Deprecated configuration key '${key}' usage${
-            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
-          }`
-        )}`
-      )
-    }
-  }
-
   public static getConfigurationData (): ConfigurationData | undefined {
     if (
       Configuration.configurationData == null &&
@@ -641,4 +516,128 @@ export class Configuration {
       )
     }
   }
+
+  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+  public static getConfigurationSection<T extends ConfigurationSectionType>(
+    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 hasOwnProp(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)
+  }
+
+  private static warnDeprecatedConfigurationKey (
+    key: string,
+    configurationSection?: ConfigurationSection,
+    logMsgToAppend = ''
+  ): void {
+    if (
+      configurationSection != null &&
+      Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
+        null &&
+      (
+        Configuration.getConfigurationData()?.[
+          configurationSection as keyof ConfigurationData
+        ] as Record<string, unknown>
+      )[key] != null
+    ) {
+      console.error(
+        `${chalk.green(logPrefix())} ${chalk.red(
+          `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
+            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+          }`
+        )}`
+      )
+    } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
+      console.error(
+        `${chalk.green(logPrefix())} ${chalk.red(
+          `Deprecated configuration key '${key}' usage${
+            logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+          }`
+        )}`
+      )
+    }
+  }
+
+  public static workerDynamicPoolInUse (): boolean {
+    return (
+      Configuration.getConfigurationSection<WorkerConfiguration>(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<WorkerConfiguration>(ConfigurationSection.worker)
+        .processType!
+    )
+  }
+
+  static {
+    const configurationFile = join(dirname(fileURLToPath(import.meta.url)), 'assets', 'config.json')
+    if (existsSync(configurationFile)) {
+      Configuration.configurationFile = configurationFile
+    } else {
+      console.error(
+        `${chalk.green(logPrefix())} ${chalk.red(
+          "Configuration file './src/assets/config.json' not found, using default configuration"
+        )}`
+      )
+      Configuration.configurationData = {
+        log: defaultLogConfiguration,
+        performanceStorage: defaultStorageConfiguration,
+        stationTemplateUrls: [
+          {
+            file: 'siemens.station-template.json',
+            numberOfStations: 1,
+          },
+        ],
+        supervisionUrlDistribution: SupervisionUrlDistribution.ROUND_ROBIN,
+        supervisionUrls: 'ws://localhost:8180/steve/websocket/CentralSystemService',
+        uiServer: defaultUIServerConfiguration,
+        worker: defaultWorkerConfiguration,
+      }
+    }
+    Configuration.configurationSectionCache = new Map<
+      ConfigurationSection,
+      ConfigurationSectionType
+    >([
+      [ConfigurationSection.log, Configuration.buildLogSection()],
+      [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
+      [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
+      [ConfigurationSection.worker, Configuration.buildWorkerSection()],
+    ])
+  }
 }
index 22631d9548c00a84da78d9e9c11a2927e68c9bab..4de762bbc2ece85a84f9ff9620550b7722d53dc0 100644 (file)
@@ -1,8 +1,7 @@
+import chalk from 'chalk'
 import { dirname, join, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
 
-import chalk from 'chalk'
-
 import { type ElementsPerWorkerType, type FileType, StorageType } from '../types/index.js'
 import { WorkerProcessType } from '../worker/index.js'
 import { Constants } from './Constants.js'
index c770c5263d46ba569000d72568081538fbb7bee7..ad34f38f0885389fd29af269097720db6681aa67 100644 (file)
@@ -10,97 +10,99 @@ import {
 
 // eslint-disable-next-line @typescript-eslint/no-extraneous-class
 export class Constants {
-  // See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
-  private static readonly SEMVER_PATTERN =
-    '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$'
-
-  private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 30000 // Ms
-
-  static readonly DEFAULT_STATION_INFO: Partial<ChargingStationInfo> = Object.freeze({
-    enableStatistics: false,
-    autoStart: true,
-    remoteAuthorization: true,
-    currentOutType: CurrentType.AC,
-    mainVoltageMeterValues: true,
-    phaseLineToLineVoltageMeterValues: false,
-    customValueLimitationMeterValues: true,
-    ocppStrictCompliance: true,
-    outOfOrderEndMeterValues: false,
-    beginEndMeterValues: false,
-    meteringPerTransaction: true,
-    transactionDataMeterValues: false,
-    supervisionUrlOcppConfiguration: false,
-    supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
-    useConnectorId0: true,
-    ocppVersion: OCPPVersion.VERSION_16,
-    firmwareVersionPattern: Constants.SEMVER_PATTERN,
-    firmwareUpgrade: {
-      versionUpgrade: {
-        step: 1,
-      },
-      reset: true,
-    },
-    ocppPersistentConfiguration: true,
-    stationInfoPersistentConfiguration: true,
-    automaticTransactionGeneratorPersistentConfiguration: true,
-    resetTime: Constants.DEFAULT_CHARGING_STATION_RESET_TIME,
-    autoReconnectMaxRetries: -1,
-    registrationMaxRetries: -1,
-    reconnectExponentialDelay: false,
-    stopTransactionsOnStopped: true,
-  })
-
-  static readonly DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000 // Ms
-  static readonly DEFAULT_HEARTBEAT_INTERVAL = 60000 // Ms
-  static readonly DEFAULT_METER_VALUES_INTERVAL = 60000 // Ms
-
-  static readonly DEFAULT_ATG_WAIT_TIME = 1000 // Ms
   static readonly DEFAULT_ATG_CONFIGURATION: AutomaticTransactionGeneratorConfiguration =
     Object.freeze({
       enable: false,
-      minDuration: 60,
+      maxDelayBetweenTwoTransactions: 30,
       maxDuration: 120,
       minDelayBetweenTwoTransactions: 15,
-      maxDelayBetweenTwoTransactions: 30,
+      minDuration: 60,
       probabilityOfStart: 1,
-      stopAfterHours: 0.25,
       stopAbsoluteDuration: false,
+      stopAfterHours: 0.25,
     })
 
+  static readonly DEFAULT_ATG_WAIT_TIME = 1000 // Ms
+
+  static readonly DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000 // Ms
+
+  private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 30000 // Ms
   static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 386
+  static readonly DEFAULT_CONNECTION_TIMEOUT = 30
 
+  static readonly DEFAULT_FLUCTUATION_PERCENT = 5
   static readonly DEFAULT_HASH_ALGORITHM = 'sha384'
 
-  static readonly DEFAULT_IDTAG = '00000000'
+  static readonly DEFAULT_HEARTBEAT_INTERVAL = 60000 // Ms
 
-  static readonly DEFAULT_CONNECTION_TIMEOUT = 30
+  static readonly DEFAULT_IDTAG = '00000000'
 
   static readonly DEFAULT_LOG_STATISTICS_INTERVAL = 60 // Seconds
 
-  static readonly DEFAULT_FLUCTUATION_PERCENT = 5
+  static readonly DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL = 60000 // Ms
+
+  static readonly DEFAULT_METER_VALUES_INTERVAL = 60000 // Ms
 
   static readonly DEFAULT_PERFORMANCE_DIRECTORY = 'performance'
-  static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'
+
   static readonly DEFAULT_PERFORMANCE_RECORDS_DB_NAME = 'e-mobility-charging-stations-simulator'
-  static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records'
+  static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'
+  static readonly DEFAULT_STATION_INFO: Partial<ChargingStationInfo> = Object.freeze({
+    automaticTransactionGeneratorPersistentConfiguration: true,
+    autoReconnectMaxRetries: -1,
+    autoStart: true,
+    beginEndMeterValues: false,
+    currentOutType: CurrentType.AC,
+    customValueLimitationMeterValues: true,
+    enableStatistics: false,
+    firmwareUpgrade: {
+      reset: true,
+      versionUpgrade: {
+        step: 1,
+      },
+    },
+    firmwareVersionPattern: Constants.SEMVER_PATTERN,
+    mainVoltageMeterValues: true,
+    meteringPerTransaction: true,
+    ocppPersistentConfiguration: true,
+    ocppStrictCompliance: true,
+    ocppVersion: OCPPVersion.VERSION_16,
+    outOfOrderEndMeterValues: false,
+    phaseLineToLineVoltageMeterValues: false,
+    reconnectExponentialDelay: false,
+    registrationMaxRetries: -1,
+    remoteAuthorization: true,
+    resetTime: Constants.DEFAULT_CHARGING_STATION_RESET_TIME,
+    stationInfoPersistentConfiguration: true,
+    stopTransactionsOnStopped: true,
+    supervisionUrlOcppConfiguration: false,
+    supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
+    transactionDataMeterValues: false,
+    useConnectorId0: true,
+  })
 
   static readonly DEFAULT_UI_SERVER_HOST = 'localhost'
+
   static readonly DEFAULT_UI_SERVER_PORT = 8080
+  static readonly EMPTY_FROZEN_OBJECT = Object.freeze({})
 
-  static readonly UNKNOWN_OCPP_COMMAND = 'unknown OCPP command' as
-    | RequestCommand
-    | IncomingRequestCommand
+  static readonly EMPTY_FUNCTION = Object.freeze(() => {
+    /* This is intentional */
+  })
 
   static readonly MAX_RANDOM_INTEGER = 281474976710655
 
-  static readonly STOP_CHARGING_STATIONS_TIMEOUT = 60000 // Ms
+  static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records'
 
-  static readonly EMPTY_FROZEN_OBJECT = Object.freeze({})
-  static readonly EMPTY_FUNCTION = Object.freeze(() => {
-    /* This is intentional */
-  })
+  // See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+  private static readonly SEMVER_PATTERN =
+    '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$'
 
-  static readonly DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL = 60000 // Ms
+  static readonly STOP_CHARGING_STATIONS_TIMEOUT = 60000 // Ms
+
+  static readonly UNKNOWN_OCPP_COMMAND = 'unknown OCPP command' as
+    | IncomingRequestCommand
+    | RequestCommand
 
   private constructor () {
     // This is intentional
index c4fdfa10142c1d7fde8a1fdb685686813f20d91d..923c6849be00268ad41bf1a88480c95ffef69084 100644 (file)
@@ -13,16 +13,13 @@ export class ACElectricUtils {
     // This is intentional
   }
 
-  static powerTotal (nbOfPhases: number, V: number, Iph: number, cosPhi = 1): number {
-    return nbOfPhases * ACElectricUtils.powerPerPhase(V, Iph, cosPhi)
-  }
-
-  static powerPerPhase (V: number, Iph: number, cosPhi = 1): number {
-    const powerPerPhase = V * Iph * cosPhi
-    if (cosPhi === 1) {
-      return powerPerPhase
+  static amperagePerPhaseFromPower (nbOfPhases: number, P: number, V: number, cosPhi = 1): number {
+    const amperage = ACElectricUtils.amperageTotalFromPower(P, V, cosPhi)
+    const amperagePerPhase = amperage / nbOfPhases
+    if (amperage % nbOfPhases === 0) {
+      return amperagePerPhase
     }
-    return Math.round(powerPerPhase)
+    return Math.round(amperagePerPhase)
   }
 
   static amperageTotal (nbOfPhases: number, Iph: number): number {
@@ -37,13 +34,16 @@ export class ACElectricUtils {
     return Math.round(amperage)
   }
 
-  static amperagePerPhaseFromPower (nbOfPhases: number, P: number, V: number, cosPhi = 1): number {
-    const amperage = ACElectricUtils.amperageTotalFromPower(P, V, cosPhi)
-    const amperagePerPhase = amperage / nbOfPhases
-    if (amperage % nbOfPhases === 0) {
-      return amperagePerPhase
+  static powerPerPhase (V: number, Iph: number, cosPhi = 1): number {
+    const powerPerPhase = V * Iph * cosPhi
+    if (cosPhi === 1) {
+      return powerPerPhase
     }
-    return Math.round(amperagePerPhase)
+    return Math.round(powerPerPhase)
+  }
+
+  static powerTotal (nbOfPhases: number, V: number, Iph: number, cosPhi = 1): number {
+    return nbOfPhases * ACElectricUtils.powerPerPhase(V, Iph, cosPhi)
   }
 }
 
@@ -56,10 +56,6 @@ export class DCElectricUtils {
     // This is intentional
   }
 
-  static power (V: number, I: number): number {
-    return V * I
-  }
-
   static amperage (P: number, V: number): number {
     const amperage = P / V
     if (P % V === 0) {
@@ -67,4 +63,8 @@ export class DCElectricUtils {
     }
     return Math.round(amperage)
   }
+
+  static power (V: number, I: number): number {
+    return V * I
+  }
 }
index 5bc2cae33fcdf5cf06ee4bfedfdd19e3589db27a..019ea70cede07f5024fd7ed140d56c28b74df698 100644 (file)
@@ -1,9 +1,7 @@
-import process from 'node:process'
-
 import chalk from 'chalk'
+import process from 'node:process'
 
 import type { ChargingStation } from '../charging-station/index.js'
-import { getMessageTypeString } from '../charging-station/ocpp/OCPPServiceUtils.js'
 import type {
   EmptyObject,
   FileType,
@@ -13,6 +11,8 @@ import type {
   MessageType,
   RequestCommand,
 } from '../types/index.js'
+
+import { getMessageTypeString } from '../charging-station/ocpp/OCPPServiceUtils.js'
 import { logger } from './Logger.js'
 import { isNotEmptyString } from './Utils.js'
 
@@ -39,8 +39,8 @@ export const handleFileException = (
 ): void => {
   params = {
     ...{
-      throwError: true,
       consoleOut: false,
+      throwError: true,
     },
     ...params,
   }
@@ -83,15 +83,15 @@ export const handleFileException = (
 
 export const handleSendMessageError = (
   chargingStation: ChargingStation,
-  commandName: RequestCommand | IncomingRequestCommand,
+  commandName: IncomingRequestCommand | RequestCommand,
   messageType: MessageType,
   error: Error,
   params?: HandleErrorParams<EmptyObject>
 ): void => {
   params = {
     ...{
-      throwError: false,
       consoleOut: false,
+      throwError: false,
     },
     ...params,
   }
@@ -112,8 +112,8 @@ export const handleIncomingRequestError = <T extends JsonType>(
 ): T | undefined => {
   params = {
     ...{
-      throwError: true,
       consoleOut: false,
+      throwError: true,
     },
     ...params,
   }
index 8e57b81f91f4195b6887d462fb2aea08d7af6f13..be1e3548e7b1b819caa559f24c5ee95546c492d6 100644 (file)
@@ -1,6 +1,7 @@
 import { type FSWatcher, readFileSync, watch, type WatchListener } from 'node:fs'
 
 import type { FileType, JsonType } from '../types/index.js'
+
 import { handleFileException } from './ErrorUtils.js'
 import { logger } from './Logger.js'
 import { isNotEmptyString } from './Utils.js'
index c6d88c491b87b140d09e10fdc64d3d1945479860..3b0ad0bda212a489518698d72d3ebf6a25ae957b 100644 (file)
@@ -1,4 +1,5 @@
 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'
@@ -47,13 +48,13 @@ if (logConfiguration.rotate === true) {
 }
 
 export const logger = createLogger({
-  silent: logConfiguration.enabled === false,
-  level: logConfiguration.level,
   format: format.combine(
     format.splat(),
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     (format[logConfiguration.format! as keyof FormatWrap] as FormatWrap)()
   ),
+  level: logConfiguration.level,
+  silent: logConfiguration.enabled === false,
   transports,
 })
 
index 6b79957e1db0860bf654f393ea33b5d57ec82b73..f1ca57c1d50f0d7a482d589d2471021606f94085 100644 (file)
@@ -1,6 +1,7 @@
 import { CircularBuffer } from 'mnemonist'
 
 import type { ChargingStation } from '../charging-station/index.js'
+
 import {
   type ChargingStationData,
   type ChargingStationWorkerMessage,
@@ -19,8 +20,8 @@ export const buildAddedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
   return {
-    event: ChargingStationWorkerMessageEvents.added,
     data: buildChargingStationDataPayload(chargingStation),
+    event: ChargingStationWorkerMessageEvents.added,
   }
 }
 
@@ -28,8 +29,8 @@ export const buildDeletedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
   return {
-    event: ChargingStationWorkerMessageEvents.deleted,
     data: buildChargingStationDataPayload(chargingStation),
+    event: ChargingStationWorkerMessageEvents.deleted,
   }
 }
 
@@ -37,8 +38,8 @@ export const buildStartedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
   return {
-    event: ChargingStationWorkerMessageEvents.started,
     data: buildChargingStationDataPayload(chargingStation),
+    event: ChargingStationWorkerMessageEvents.started,
   }
 }
 
@@ -46,8 +47,8 @@ export const buildStoppedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
   return {
-    event: ChargingStationWorkerMessageEvents.stopped,
     data: buildChargingStationDataPayload(chargingStation),
+    event: ChargingStationWorkerMessageEvents.stopped,
   }
 }
 
@@ -55,8 +56,8 @@ export const buildUpdatedMessage = (
   chargingStation: ChargingStation
 ): ChargingStationWorkerMessage<ChargingStationData> => {
   return {
-    event: ChargingStationWorkerMessageEvents.updated,
     data: buildChargingStationDataPayload(chargingStation),
+    event: ChargingStationWorkerMessageEvents.updated,
   }
 }
 
@@ -70,30 +71,30 @@ export const buildPerformanceStatisticsMessage = (
     return [key, value]
   })
   return {
-    event: ChargingStationWorkerMessageEvents.performanceStatistics,
     data: {
+      createdAt: statistics.createdAt,
       id: statistics.id,
       name: statistics.name,
-      uri: statistics.uri,
-      createdAt: statistics.createdAt,
-      updatedAt: statistics.updatedAt,
       statisticsData,
+      updatedAt: statistics.updatedAt,
+      uri: statistics.uri,
     },
+    event: ChargingStationWorkerMessageEvents.performanceStatistics,
   }
 }
 
 const buildChargingStationDataPayload = (chargingStation: ChargingStation): ChargingStationData => {
   return {
-    started: chargingStation.started,
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    stationInfo: chargingStation.stationInfo!,
+    bootNotificationResponse: chargingStation.bootNotificationResponse,
     connectors: buildConnectorsStatus(chargingStation),
     evses: buildEvsesStatus(chargingStation, OutputFormat.worker),
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     ocppConfiguration: chargingStation.ocppConfiguration!,
+    started: chargingStation.started,
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    stationInfo: chargingStation.stationInfo!,
     supervisionUrl: chargingStation.wsConnectionUrl.href,
     wsState: chargingStation.wsConnection?.readyState,
-    bootNotificationResponse: chargingStation.bootNotificationResponse,
     ...(chargingStation.automaticTransactionGenerator != null && {
       automaticTransactionGenerator:
         buildChargingStationAutomaticTransactionGeneratorConfiguration(chargingStation),
index 6416c14e701ea9be1722b54e2d7663e2fa575e79..c816cf7150222b6febd2450d01e65388f066fbb5 100644 (file)
@@ -1,5 +1,4 @@
-import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
-import { env, nextTick } from 'node:process'
+import type { CircularBuffer } from 'mnemonist'
 
 import {
   formatDuration,
@@ -12,7 +11,8 @@ import {
   minutesToSeconds,
   secondsToMilliseconds,
 } from 'date-fns'
-import type { CircularBuffer } from 'mnemonist'
+import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
+import { env, nextTick } from 'node:process'
 import { is } from 'rambda'
 
 import {
@@ -84,7 +84,7 @@ export const isValidDate = (date: Date | number | undefined): date is Date | num
 }
 
 export const convertToDate = (
-  value: Date | string | number | undefined | null
+  value: Date | null | number | string | undefined
 ): Date | undefined => {
   if (value == null) {
     return undefined
@@ -265,12 +265,12 @@ export const JSONStringify = <
   // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
   T extends
     | JsonType
+    | Map<string, Record<string, unknown>>
     | Record<string, unknown>[]
     | Set<Record<string, unknown>>
-    | Map<string, Record<string, unknown>>
 >(
     object: T,
-    space?: string | number,
+    space?: number | string,
     mapFormat?: MapStringifyFormat
   ): string => {
   return JSON.stringify(
index f6f982f30f58b5c1389400d41cf0234d0a6770a4..d985209eb62a1e383cc45e72a02a0e29adef0dd1 100644 (file)
@@ -1,17 +1,17 @@
 import type { EventEmitterAsyncResource } from 'node:events'
-import { existsSync } from 'node:fs'
-
 import type { PoolInfo } from 'poolifier'
 
+import { existsSync } from 'node:fs'
+
 import type { SetInfo, WorkerData, WorkerOptions } from './WorkerTypes.js'
 
 export abstract class WorkerAbstract<D extends WorkerData, R extends WorkerData> {
-  protected readonly workerScript: string
   protected readonly workerOptions: WorkerOptions
+  protected readonly workerScript: string
+  public abstract readonly emitter: EventEmitterAsyncResource | undefined
   public abstract readonly info: PoolInfo | SetInfo
-  public abstract readonly size: number
   public abstract readonly maxElementsPerWorker: number | undefined
-  public abstract readonly emitter: EventEmitterAsyncResource | undefined
+  public abstract readonly size: number
 
   /**
    * `WorkerAbstract` constructor.
@@ -35,17 +35,17 @@ export abstract class WorkerAbstract<D extends WorkerData, R extends WorkerData>
     this.workerOptions = workerOptions
   }
 
+  /**
+   * Adds a task element to the worker pool/set.
+   * @param elementData -
+   */
+  public abstract addElement (elementData: D): Promise<R>
   /**
    * Starts the worker pool/set.
    */
-  public abstract start (): void | Promise<void>
+  public abstract start (): Promise<void> | void
   /**
    * Stops the worker pool/set.
    */
   public abstract stop (): Promise<void>
-  /**
-   * Adds a task element to the worker pool/set.
-   * @param elementData -
-   */
-  public abstract addElement (elementData: D): Promise<R>
 }
index 5a8dc13aad9d36beda820fda10a40428fc70c31e..07c55757a0c844f808562b890ccd4b6d10d8ca97 100644 (file)
@@ -1,6 +1,7 @@
 import { availableParallelism } from 'poolifier'
 
 import type { WorkerOptions } from './WorkerTypes.js'
+
 import { defaultErrorHandler, defaultExitHandler } from './WorkerUtils.js'
 
 export const EMPTY_FUNCTION = Object.freeze(() => {
@@ -16,16 +17,16 @@ 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({
-  workerStartDelay: DEFAULT_WORKER_START_DELAY,
   elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY,
-  poolMinSize: DEFAULT_POOL_MIN_SIZE,
-  poolMaxSize: DEFAULT_POOL_MAX_SIZE,
   elementsPerWorker: DEFAULT_ELEMENTS_PER_WORKER,
+  poolMaxSize: DEFAULT_POOL_MAX_SIZE,
+  poolMinSize: DEFAULT_POOL_MIN_SIZE,
   poolOptions: {
-    startWorkers: false,
     enableEvents: true,
-    restartWorkerOnError: true,
     errorHandler: defaultErrorHandler,
     exitHandler: defaultExitHandler,
+    restartWorkerOnError: true,
+    startWorkers: false,
   },
+  workerStartDelay: DEFAULT_WORKER_START_DELAY,
 })
index f412f081e5920d0f8021938764cf9dcf4d9b9143..38115c0be8aed7f3768adade8cd5fccd4cfdefd9 100644 (file)
@@ -2,8 +2,9 @@ import type { EventEmitterAsyncResource } from 'node:events'
 
 import { DynamicThreadPool, type PoolInfo } from 'poolifier'
 
-import { WorkerAbstract } from './WorkerAbstract.js'
 import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
+
+import { WorkerAbstract } from './WorkerAbstract.js'
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
 export class WorkerDynamicPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
@@ -27,40 +28,40 @@ export class WorkerDynamicPool<D extends WorkerData, R extends WorkerData> exten
     )
   }
 
-  get info (): PoolInfo {
-    return this.pool.info
+  /** @inheritDoc */
+  public async addElement (elementData: D): Promise<R> {
+    const response = await this.pool.execute(elementData)
+    // Start element sequentially to optimize memory at startup
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    this.workerOptions.elementAddDelay! > 0 &&
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+    return response
   }
 
-  get size (): number {
-    return this.pool.info.workerNodes
+  /** @inheritDoc */
+  public start (): void {
+    this.pool.start()
   }
 
-  get maxElementsPerWorker (): number | undefined {
-    return undefined
+  /** @inheritDoc */
+  public async stop (): Promise<void> {
+    await this.pool.destroy()
   }
 
   get emitter (): EventEmitterAsyncResource | undefined {
     return this.pool.emitter
   }
 
-  /** @inheritDoc */
-  public start (): void {
-    this.pool.start()
+  get info (): PoolInfo {
+    return this.pool.info
   }
 
-  /** @inheritDoc */
-  public async stop (): Promise<void> {
-    await this.pool.destroy()
+  get maxElementsPerWorker (): number | undefined {
+    return undefined
   }
 
-  /** @inheritDoc */
-  public async addElement (elementData: D): Promise<R> {
-    const response = await this.pool.execute(elementData)
-    // Start element sequentially to optimize memory at startup
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    this.workerOptions.elementAddDelay! > 0 &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
-    return response
+  get size (): number {
+    return this.pool.info.workerNodes
   }
 }
index 73bcd56dc62467c7bb942157d88554014b8240af..c2210fa122b31e0ac509856b92b9fcf8dba24534 100644 (file)
@@ -1,8 +1,8 @@
 import { isMainThread } from 'node:worker_threads'
-
 import { mergeDeepRight } from 'rambda'
 
 import type { WorkerAbstract } from './WorkerAbstract.js'
+
 import { DEFAULT_WORKER_OPTIONS } from './WorkerConstants.js'
 import { WorkerDynamicPool } from './WorkerDynamicPool.js'
 import { WorkerFixedPool } from './WorkerFixedPool.js'
@@ -25,12 +25,12 @@ export class WorkerFactory {
     }
     workerOptions = mergeDeepRight<WorkerOptions>(DEFAULT_WORKER_OPTIONS, workerOptions ?? {})
     switch (workerProcessType) {
-      case WorkerProcessType.workerSet:
-        return new WorkerSet<D, R>(workerScript, workerOptions)
-      case WorkerProcessType.fixedPool:
-        return new WorkerFixedPool<D, R>(workerScript, workerOptions)
       case WorkerProcessType.dynamicPool:
         return new WorkerDynamicPool<D, R>(workerScript, workerOptions)
+      case WorkerProcessType.fixedPool:
+        return new WorkerFixedPool<D, R>(workerScript, workerOptions)
+      case WorkerProcessType.workerSet:
+        return new WorkerSet<D, R>(workerScript, workerOptions)
       default:
         // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
         throw new Error(`Worker implementation type '${workerProcessType}' not found`)
index 29755fedddd92ba50dbfa06ad03e77742a911c75..22d6fcd1313a583d815d25a5dbfb28bc54b28851 100644 (file)
@@ -2,8 +2,9 @@ import type { EventEmitterAsyncResource } from 'node:events'
 
 import { FixedThreadPool, type PoolInfo } from 'poolifier'
 
-import { WorkerAbstract } from './WorkerAbstract.js'
 import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
+
+import { WorkerAbstract } from './WorkerAbstract.js'
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
 export class WorkerFixedPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
@@ -26,40 +27,40 @@ export class WorkerFixedPool<D extends WorkerData, R extends WorkerData> extends
     )
   }
 
-  get info (): PoolInfo {
-    return this.pool.info
+  /** @inheritDoc */
+  public async addElement (elementData: D): Promise<R> {
+    const response = await this.pool.execute(elementData)
+    // Start element sequentially to optimize memory at startup
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    this.workerOptions.elementAddDelay! > 0 &&
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+    return response
   }
 
-  get size (): number {
-    return this.pool.info.workerNodes
+  /** @inheritDoc */
+  public start (): void {
+    this.pool.start()
   }
 
-  get maxElementsPerWorker (): number | undefined {
-    return undefined
+  /** @inheritDoc */
+  public async stop (): Promise<void> {
+    await this.pool.destroy()
   }
 
   get emitter (): EventEmitterAsyncResource | undefined {
     return this.pool.emitter
   }
 
-  /** @inheritDoc */
-  public start (): void {
-    this.pool.start()
+  get info (): PoolInfo {
+    return this.pool.info
   }
 
-  /** @inheritDoc */
-  public async stop (): Promise<void> {
-    await this.pool.destroy()
+  get maxElementsPerWorker (): number | undefined {
+    return undefined
   }
 
-  /** @inheritDoc */
-  public async addElement (elementData: D): Promise<R> {
-    const response = await this.pool.execute(elementData)
-    // Start element sequentially to optimize memory at startup
-    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-    this.workerOptions.elementAddDelay! > 0 &&
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
-    return response
+  get size (): number {
+    return this.pool.info.workerNodes
   }
 }
index 2fd637149bcdbcd4d4ab33f843500cef5f335f9f..c1369e12cda3ee728d63cd5739a4f72d2bc87cfc 100644 (file)
@@ -18,21 +18,22 @@ import {
 import { randomizeDelay, sleep } from './WorkerUtils.js'
 
 interface ResponseWrapper<R extends WorkerData> {
-  resolve: (value: R | PromiseLike<R>) => void
   reject: (reason?: unknown) => void
+  resolve: (value: PromiseLike<R> | R) => void
   workerSetElement: WorkerSetElement
 }
 
 export class WorkerSet<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<D, R> {
-  public readonly emitter: EventEmitterAsyncResource | undefined
-  private readonly workerSet: Set<WorkerSetElement>
   private readonly promiseResponseMap: Map<
     `${string}-${string}-${string}-${string}`,
     ResponseWrapper<R>
   >
 
   private started: boolean
+  private readonly workerSet: Set<WorkerSetElement>
+
   private workerStartup: boolean
+  public readonly emitter: EventEmitterAsyncResource | undefined
 
   /**
    * Creates a new `WorkerSet`.
@@ -62,89 +63,6 @@ export class WorkerSet<D extends WorkerData, R extends WorkerData> extends Worke
     this.workerStartup = false
   }
 
-  get info (): SetInfo {
-    return {
-      version: workerSetVersion,
-      type: 'set',
-      worker: 'thread',
-      started: this.started,
-      size: this.size,
-      elementsExecuting: [...this.workerSet].reduce(
-        (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements,
-        0
-      ),
-      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      elementsPerWorker: this.maxElementsPerWorker!,
-    }
-  }
-
-  get size (): number {
-    return this.workerSet.size
-  }
-
-  get maxElementsPerWorker (): number | undefined {
-    return this.workerOptions.elementsPerWorker
-  }
-
-  /** @inheritDoc */
-  public async start (): Promise<void> {
-    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<void> {
-    for (const workerSetElement of this.workerSet) {
-      const worker = workerSetElement.worker
-      const waitWorkerExit = new Promise<void>(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()
-  }
-
-  /** @inheritDoc */
-  public async addElement (elementData: D): Promise<R> {
-    if (!this.started) {
-      throw new Error('Cannot add a WorkerSet element: not started')
-    }
-    const workerSetElement = await this.getWorkerSetElement()
-    const sendMessageToWorker = new Promise<R>((resolve, reject) => {
-      const message = {
-        uuid: randomUUID(),
-        event: WorkerMessageEvents.addWorkerElement,
-        data: elementData,
-      } satisfies WorkerMessage<D>
-      workerSetElement.worker.postMessage(message)
-      this.promiseResponseMap.set(message.uuid, {
-        resolve,
-        reject,
-        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
-  }
-
   /**
    * Adds a new `WorkerSetElement`.
    * @returns The new `WorkerSetElement`.
@@ -157,10 +75,10 @@ export class WorkerSet<D extends WorkerData, R extends WorkerData> extends Worke
     })
     worker.on('message', this.workerOptions.poolOptions?.messageHandler ?? EMPTY_FUNCTION)
     worker.on('message', (message: WorkerMessage<R>) => {
-      const { uuid, event, data } = message
+      const { data, event, uuid } = message
       if (this.promiseResponseMap.has(uuid)) {
         // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        const { resolve, reject, workerSetElement } = this.promiseResponseMap.get(uuid)!
+        const { reject, resolve, workerSetElement } = this.promiseResponseMap.get(uuid)!
         switch (event) {
           case WorkerMessageEvents.addedWorkerElement:
             this.emitter?.emit(WorkerSetEvents.elementAdded, this.info)
@@ -205,23 +123,16 @@ export class WorkerSet<D extends WorkerData, R extends WorkerData> extends Worke
       this.removeWorkerSetElement(this.getWorkerSetElementByWorker(worker))
     })
     const workerSetElement: WorkerSetElement = {
-      worker,
       numberOfWorkerElements: 0,
+      worker,
     }
     this.workerSet.add(workerSetElement)
     this.workerStartup = false
     return workerSetElement
   }
 
-  private removeWorkerSetElement (workerSetElement: WorkerSetElement | undefined): void {
-    if (workerSetElement == null) {
-      return
-    }
-    this.workerSet.delete(workerSetElement)
-  }
-
   private async getWorkerSetElement (): Promise<WorkerSetElement> {
-    let chosenWorkerSetElement: WorkerSetElement | undefined
+    let chosenWorkerSetElement: undefined | WorkerSetElement
     for (const workerSetElement of this.workerSet) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
       if (workerSetElement.numberOfWorkerElements < this.workerOptions.elementsPerWorker!) {
@@ -240,8 +151,8 @@ export class WorkerSet<D extends WorkerData, R extends WorkerData> extends Worke
     return chosenWorkerSetElement
   }
 
-  private getWorkerSetElementByWorker (worker: Worker): WorkerSetElement | undefined {
-    let workerSetElt: WorkerSetElement | undefined
+  private getWorkerSetElementByWorker (worker: Worker): undefined | WorkerSetElement {
+    let workerSetElt: undefined | WorkerSetElement
     for (const workerSetElement of this.workerSet) {
       if (workerSetElement.worker.threadId === worker.threadId) {
         workerSetElt = workerSetElement
@@ -250,4 +161,94 @@ export class WorkerSet<D extends WorkerData, R extends WorkerData> extends Worke
     }
     return workerSetElt
   }
+
+  private removeWorkerSetElement (workerSetElement: undefined | WorkerSetElement): void {
+    if (workerSetElement == null) {
+      return
+    }
+    this.workerSet.delete(workerSetElement)
+  }
+
+  /** @inheritDoc */
+  public async addElement (elementData: D): Promise<R> {
+    if (!this.started) {
+      throw new Error('Cannot add a WorkerSet element: not started')
+    }
+    const workerSetElement = await this.getWorkerSetElement()
+    const sendMessageToWorker = new Promise<R>((resolve, reject) => {
+      const message = {
+        data: elementData,
+        event: WorkerMessageEvents.addWorkerElement,
+        uuid: randomUUID(),
+      } satisfies WorkerMessage<D>
+      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<void> {
+    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<void> {
+    for (const workerSetElement of this.workerSet) {
+      const worker = workerSetElement.worker
+      const waitWorkerExit = new Promise<void>(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
+  }
 }
index 6ca9140b2abe65a74bd55cd784fbb56991aeba28..8794420abc39160455c62bad70aa69e4b7fd47d3 100644 (file)
@@ -3,28 +3,28 @@ import type { Worker } from 'node:worker_threads'
 import { type PoolEvent, PoolEvents, type ThreadPoolOptions } from 'poolifier'
 
 export enum WorkerProcessType {
-  workerSet = 'workerSet',
-  fixedPool = 'fixedPool',
   /** @experimental */
-  dynamicPool = 'dynamicPool'
+  dynamicPool = 'dynamicPool',
+  fixedPool = 'fixedPool',
+  workerSet = 'workerSet'
 }
 
 export interface SetInfo {
-  version: string
-  type: string
-  worker: string
-  started: boolean
-  size: number
   elementsExecuting: number
   elementsPerWorker: number
+  size: number
+  started: boolean
+  type: string
+  version: string
+  worker: string
 }
 
 export enum WorkerSetEvents {
-  started = 'started',
-  stopped = 'stopped',
-  error = 'error',
   elementAdded = 'elementAdded',
-  elementError = 'elementError'
+  elementError = 'elementError',
+  error = 'error',
+  started = 'started',
+  stopped = 'stopped'
 }
 
 export const WorkerEvents = {
@@ -35,36 +35,36 @@ export const WorkerEvents = {
 export type WorkerEvents = PoolEvent | WorkerSetEvents
 
 export interface WorkerOptions {
-  workerStartDelay?: number
   elementAddDelay?: number
+  elementsPerWorker?: number
   poolMaxSize: number
   poolMinSize: number
-  elementsPerWorker?: number
   poolOptions?: ThreadPoolOptions
+  workerStartDelay?: number
 }
 
 export type WorkerData = Record<string, unknown>
 
 export interface WorkerDataError extends WorkerData {
   event: WorkerMessageEvents
-  name: string
   message: string
+  name: string
   stack?: string
 }
 
 export interface WorkerSetElement {
-  worker: Worker
   numberOfWorkerElements: number
+  worker: Worker
 }
 
 export interface WorkerMessage<T extends WorkerData> {
-  uuid: `${string}-${string}-${string}-${string}`
-  event: WorkerMessageEvents
   data: T
+  event: WorkerMessageEvents
+  uuid: `${string}-${string}-${string}-${string}`
 }
 
 export enum WorkerMessageEvents {
-  addWorkerElement = 'addWorkerElement',
   addedWorkerElement = 'addedWorkerElement',
+  addWorkerElement = 'addWorkerElement',
   workerElementError = 'workerElementError'
 }
index 8fad34a228296dc1abcd546b7a02171081c33177..958ecccc2f9d4a0d08e20785d7f37fbf471b726e 100644 (file)
@@ -1,6 +1,5 @@
-import { getRandomValues } from 'node:crypto'
-
 import chalk from 'chalk'
+import { getRandomValues } from 'node:crypto'
 
 export const sleep = async (milliSeconds: number): Promise<NodeJS.Timeout> => {
   return await new Promise<NodeJS.Timeout>(resolve =>
index 798763ba40bc536f1778f4b346ebd622ef0c9811..9d1df21b1f9e1b21267cb5198bdedb86949390c6 100644 (file)
@@ -1,7 +1,8 @@
 /* eslint-disable @typescript-eslint/no-unsafe-member-access */
+import { expect } from 'expect'
 import { describe, it } from 'node:test'
 
-import { expect } from 'expect'
+import type { ChargingStation } from '../../src/charging-station/index.js'
 
 import {
   checkChargingStationState,
@@ -14,7 +15,6 @@ import {
   getPhaseRotationValue,
   validateStationInfo,
 } from '../../src/charging-station/Helpers.js'
-import type { ChargingStation } from '../../src/charging-station/index.js'
 import { BaseError } from '../../src/exception/index.js'
 import {
   type ChargingStationConfiguration,
@@ -33,10 +33,10 @@ await describe('Helpers test suite', async () => {
     baseName,
   } as ChargingStationTemplate
   const chargingStation = {
-    started: false,
-    logPrefix: () => `${baseName} |`,
-    evses: new Map<number, EvseStatus>(),
     connectors: new Map<number, ConnectorStatus>(),
+    evses: new Map<number, EvseStatus>(),
+    logPrefix: () => `${baseName} |`,
+    started: false,
   } as ChargingStation
 
   await it('Verify getChargingStationId()', () => {
index 947bac2bd4f0290863d364d7707d3c0d8ac2eb79..e5bdc769b64b4bd5818d9dedb586fea0c6cef05b 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { BaseError } from '../../src/exception/BaseError.js'
 
index c6e4bc43bc35e1c0f07b80683b139e70347f9256..44d458a6f433bd3e83a3c430b12abc6bc9138298 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { OCPPError } from '../../src/exception/OCPPError.js'
 import { ErrorType } from '../../src/types/index.js'
index f564a702b5eebad95f6a30feee3f7ed782aec870..2af0d14b559b3884c30c8f25b5b99dba4f00781c 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import {
   ApplicationProtocolVersion,
index f0eda00752d05f23d237633901f734693c9d6ef1..3ab1dcf5c83bd45647ad8d7926d9ebbc34f0e777 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { AsyncLock, AsyncLockType } from '../../src/utils/AsyncLock.js'
 
index b0689b888a6f3ecc452275cb00242af680ae59ff..23f6c5ac544190203fea1068a0b3110efd00c763 100644 (file)
@@ -1,7 +1,6 @@
 /* eslint-disable @typescript-eslint/no-unsafe-member-access */
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { FileType } from '../../src/types/index.js'
 import { handleFileException, logPrefix } from '../../src/utils/ConfigurationUtils.js'
index 3b025b2ee301c0baf2d8f314c7f0c66ed6b05b85..c408a7f499e4f59a19c973896b1317962c9ec614 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { ACElectricUtils, DCElectricUtils } from '../../src/utils/ElectricUtils.js'
 
index 1163211f3fac51e61ffe19a4dcc3c528b7deea1b..b05eec74684152d32cf0231f9e233a4684cef864 100644 (file)
@@ -1,9 +1,9 @@
 /* eslint-disable @typescript-eslint/no-unsafe-member-access */
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../src/charging-station/index.js'
+
 import {
   FileType,
   GenericStatus,
@@ -47,8 +47,8 @@ await describe('ErrorUtils test suite', async () => {
     }).toThrow(error)
     expect(() => {
       handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
-        throwError: false,
         consoleOut: true,
+        throwError: false,
       })
     }).not.toThrow()
     expect(console.warn.mock.calls.length).toBe(1)
@@ -97,8 +97,8 @@ await describe('ErrorUtils test suite', async () => {
     }
     expect(
       handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error, {
-        throwError: false,
         errorResponse,
+        throwError: false,
       })
     ).toStrictEqual(errorResponse)
     expect(chargingStation.logPrefix.mock.calls.length).toBe(3)
index c0bf1b6cc8bf4e7a0ee3329eb1f1e73b9eb45b3e..8e1f00a5432429fb8ec493dc3364fbf619869694 100644 (file)
@@ -1,6 +1,5 @@
-import { describe, it } from 'node:test'
-
 import { expect } from 'expect'
+import { describe, it } from 'node:test'
 
 import { max, min, nthPercentile, stdDeviation } from '../../src/utils/StatisticUtils.js'
 
index 3fe4684281f3e0a24ed34951740a309d0aabf18f..4f9e91028e9fd1b40f53fc87a6238fccc206c638 100644 (file)
@@ -1,14 +1,14 @@
-import { randomInt } from 'node:crypto'
-import { version } from 'node:process'
-import { describe, it } from 'node:test'
-
 import { hoursToMilliseconds, hoursToSeconds } from 'date-fns'
 import { expect } from 'expect'
 import { CircularBuffer } from 'mnemonist'
+import { randomInt } from 'node:crypto'
+import { version } from 'node:process'
+import { describe, it } from 'node:test'
 import { satisfies } from 'semver'
 
-import { runtime, runtimes } from '../../scripts/runtime.js'
 import type { TimestampedData } from '../../src/types/index.js'
+
+import { runtime, runtimes } from '../../scripts/runtime.js'
 import { Constants } from '../../src/utils/Constants.js'
 import {
   clone,
@@ -274,17 +274,17 @@ await describe('Utils test suite', async () => {
     expect(isAsyncFunction(async function named () {})).toBe(true)
     class TestClass {
       // eslint-disable-next-line @typescript-eslint/no-empty-function
-      public testSync (): void {}
-      // eslint-disable-next-line @typescript-eslint/no-empty-function
-      public async testAsync (): Promise<void> {}
+      public testArrowAsync = async (): Promise<void> => {}
       // eslint-disable-next-line @typescript-eslint/no-empty-function
       public testArrowSync = (): void => {}
       // eslint-disable-next-line @typescript-eslint/no-empty-function
-      public testArrowAsync = async (): Promise<void> => {}
+      public static async testStaticAsync (): Promise<void> {}
       // eslint-disable-next-line @typescript-eslint/no-empty-function
       public static testStaticSync (): void {}
       // eslint-disable-next-line @typescript-eslint/no-empty-function
-      public static async testStaticAsync (): Promise<void> {}
+      public async testAsync (): Promise<void> {}
+      // eslint-disable-next-line @typescript-eslint/no-empty-function
+      public testSync (): void {}
     }
     const testClass = new TestClass()
     // eslint-disable-next-line @typescript-eslint/unbound-method
index 691c0141105350e6e68e6683448bd86cabef0045..f2abb270e674fd43e5e4e2494b366655228ac67c 100644 (file)
@@ -36,7 +36,7 @@
   "devDependencies": {
     "@tsconfig/node22": "^22.0.0",
     "@types/jsdom": "^21.1.7",
-    "@types/node": "^22.4.1",
+    "@types/node": "^22.5.0",
     "@vitejs/plugin-vue": "^5.1.2",
     "@vitejs/plugin-vue-jsx": "^4.0.1",
     "@vitest/coverage-v8": "^2.0.5",
index 44ffcef3edacff2b85e5f476e941d145ab4b5a82..52e4cd8100cc49390860daa217f1cd6922d3d9b5 100644 (file)
   <input
     id="number-of-stations"
     v-model="state.numberOfStations"
-    type="number"
     min="1"
     name="number-of-stations"
     placeholder="number of stations"
+    type="number"
   >
   <p>Template options overrides:</p>
   <ul id="template-options">
       <input
         id="supervision-url"
         v-model.trim="state.supervisionUrl"
-        type="url"
         name="supervision-url"
         placeholder="wss://"
+        type="url"
       >
     </li>
     <li>
       Auto start:
       <input
         v-model="state.autoStart"
-        type="checkbox"
-        true-value="true"
         false-value="false"
+        true-value="true"
+        type="checkbox"
       >
     </li>
     <li>
       Persistent configuration:
       <input
         v-model="state.persistentConfiguration"
-        type="checkbox"
-        true-value="true"
         false-value="false"
+        true-value="true"
+        type="checkbox"
       >
     </li>
     <li>
       OCPP strict compliance:
       <input
         v-model="state.ocppStrictCompliance"
-        type="checkbox"
-        true-value="true"
         false-value="false"
+        true-value="true"
+        type="checkbox"
       >
     </li>
     <li>
       Performance statistics:
       <input
         v-model="state.enableStatistics"
-        type="checkbox"
-        true-value="true"
         false-value="false"
+        true-value="true"
+        type="checkbox"
       >
     </li>
   </ul>
 </template>
 
 <script setup lang="ts">
-import { getCurrentInstance, ref, watch } from 'vue'
-
 import Button from '@/components/buttons/Button.vue'
 import { convertToBoolean, randomUUID } from '@/composables'
+import { getCurrentInstance, ref, watch } from 'vue'
 
 const state = ref<{
-  renderTemplates: `${string}-${string}-${string}-${string}-${string}`
-  template: string
-  numberOfStations: number
-  supervisionUrl: string
   autoStart: boolean
-  persistentConfiguration: boolean
-  ocppStrictCompliance: boolean
   enableStatistics: boolean
+  numberOfStations: number
+  ocppStrictCompliance: boolean
+  persistentConfiguration: boolean
+  renderTemplates: `${string}-${string}-${string}-${string}-${string}`
+  supervisionUrl: string
+  template: string
 }>({
-  renderTemplates: randomUUID(),
-  template: '',
-  numberOfStations: 1,
-  supervisionUrl: '',
   autoStart: false,
-  persistentConfiguration: true,
-  ocppStrictCompliance: true,
   enableStatistics: false,
+  numberOfStations: 1,
+  ocppStrictCompliance: true,
+  persistentConfiguration: true,
+  renderTemplates: randomUUID(),
+  supervisionUrl: '',
+  template: '',
 })
 
 watch(getCurrentInstance()!.appContext.config.globalProperties!.$templates, () => {
index 76f4e940cf76efa955e59267ac2be6c4df62cac6..3fa00f1a9a44e9a607bd709faca6c62b38e77e48 100644 (file)
@@ -7,9 +7,9 @@
   <input
     id="supervision-url"
     v-model.trim="state.supervisionUrl"
-    type="url"
     name="supervision-url"
     placeholder="wss://"
+    type="url"
   >
   <br>
   <Button
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
-
 import Button from '@/components/buttons/Button.vue'
+import { ref } from 'vue'
 
 defineProps<{
-  hashId: string
   chargingStationId: string
+  hashId: string
 }>()
 
 const state = ref<{ supervisionUrl: string }>({
index b91b8707e88842c01363d947507cb6ea8afacc1f..082ffc04031ff34599ffd9506c2f3dadb61b2518 100644 (file)
@@ -8,9 +8,9 @@
   <input
     id="idtag"
     v-model.trim="state.idTag"
-    type="text"
     name="idtag"
     placeholder="RFID tag"
+    type="text"
   >
   <br>
   <Button
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
-
 import Button from '@/components/buttons/Button.vue'
 import { convertToInt } from '@/composables'
+import { ref } from 'vue'
 
 defineProps<{
-  hashId: string
   chargingStationId: string
   connectorId: string
+  hashId: string
 }>()
 
 const state = ref<{ idTag: string }>({
index a18b8064791e448af8c6d4e8cb14807220f7a898..708d6c448b2dec262c23d8ba11772069a49a3ccd 100644 (file)
@@ -1,7 +1,7 @@
 <template>
   <button
-    type="button"
     class="button"
+    type="button"
   >
     <slot />
   </button>
index 03748f2815940e933c3a1dfc40bd16c1ba56ac88..c2b09c5265228dd41efcae5d73b112f6e58a9080 100644 (file)
@@ -8,17 +8,16 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
-
 import Button from '@/components/buttons/Button.vue'
 import { getFromLocalStorage, setToLocalStorage } from '@/composables'
+import { ref } from 'vue'
 
 const props = defineProps<{
   id: string
-  status?: boolean
-  shared?: boolean
-  on?: () => void
   off?: () => void
+  on?: () => void
+  shared?: boolean
+  status?: boolean
 }>()
 
 const $emit = defineEmits(['clicked'])
index c59e2656801194c84dd54ceaf1f34be27d0f85ad..8f19083b96a67f941814f2b4d859022e68ae0dfa 100644 (file)
     <td class="connectors-table__column">
       <ToggleButton
         :id="`${hashId}-${connectorId}-start-transaction`"
-        :shared="true"
+        :off="
+          () => {
+            $router.push({ name: 'charging-stations' })
+          }
+        "
         :on="
           () => {
             $router.push({
             })
           }
         "
-        :off="
-          () => {
-            $router.push({ name: 'charging-stations' })
-          }
-        "
+        :shared="true"
         @clicked="
           () => {
             $emit('need-refresh')
 </template>
 
 <script setup lang="ts">
-import { useToast } from 'vue-toast-notification'
+import type { ConnectorStatus, Status } from '@/types'
 
 import Button from '@/components/buttons/Button.vue'
 import ToggleButton from '@/components/buttons/ToggleButton.vue'
 import { useUIClient } from '@/composables'
-import type { ConnectorStatus, Status } from '@/types'
+import { useToast } from 'vue-toast-notification'
 
 const props = defineProps<{
-  hashId: string
+  atgStatus?: Status
   chargingStationId: string
-  connectorId: number
   connector: ConnectorStatus
-  atgStatus?: Status
+  connectorId: number
+  hashId: string
 }>()
 
 const $emit = defineEmits(['need-refresh'])
index 941bf754a72eff06d92036338280001d47d8eaf7..12dc7f051c6349a3acb30558700263fc079cce3e 100644 (file)
       </Button>
       <ToggleButton
         :id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
-        :shared="true"
+        :off="
+          () => {
+            $router.push({ name: 'charging-stations' })
+          }
+        "
         :on="
           () => {
             $router.push({
             })
           }
         "
-        :off="
-          () => {
-            $router.push({ name: 'charging-stations' })
-          }
-        "
+        :shared="true"
         @clicked="
           () => {
             $emit('need-refresh')
         <thead id="connectors-table__head">
           <tr class="connectors-table__row">
             <th
-              scope="col"
               class="connectors-table__column"
+              scope="col"
             >
               Identifier
             </th>
             <th
-              scope="col"
               class="connectors-table__column"
+              scope="col"
             >
               Status
             </th>
             <th
-              scope="col"
               class="connectors-table__column"
+              scope="col"
             >
               Transaction
             </th>
             <th
-              scope="col"
               class="connectors-table__column"
+              scope="col"
             >
               ATG Started
             </th>
             <th
-              scope="col"
               class="connectors-table__column"
+              scope="col"
             >
               Actions
             </th>
           <CSConnector
             v-for="(connector, index) in getConnectorStatuses()"
             :key="index + 1"
-            :hash-id="chargingStation.stationInfo.hashId"
+            :atg-status="getATGStatus(index + 1)"
             :charging-station-id="chargingStation.stationInfo.chargingStationId"
-            :connector-id="index + 1"
             :connector="connector"
-            :atg-status="getATGStatus(index + 1)"
+            :connector-id="index + 1"
+            :hash-id="chargingStation.stationInfo.hashId"
             @need-refresh="$emit('need-refresh')"
           />
         </tbody>
 </template>
 
 <script setup lang="ts">
-import { useToast } from 'vue-toast-notification'
+import type { ChargingStationData, ConnectorStatus, Status } from '@/types'
 
 import Button from '@/components/buttons/Button.vue'
 import ToggleButton from '@/components/buttons/ToggleButton.vue'
 import CSConnector from '@/components/charging-stations/CSConnector.vue'
 import { useUIClient } from '@/composables'
-import type { ChargingStationData, ConnectorStatus, Status } from '@/types'
+import { useToast } from 'vue-toast-notification'
 
 const props = defineProps<{
   chargingStation: ChargingStationData
index d7e79856c5efd2cf11aedb79fde137fb80a0d619..0902cd7d3b23d30071013fbb949460cb2dfbb3cd 100644 (file)
@@ -6,68 +6,68 @@
     <thead id="cs-table__head">
       <tr class="cs-table__row">
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Name
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Started
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Supervision Url
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           WebSocket State
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Registration Status
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Template
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Vendor
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Model
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Firmware
         </th>
         <th
-          scope="col"
           class="cs-table__column"
+          scope="col"
         >
           Actions
         </th>
         <th
-          scope="col"
           class="cs-table__connectors-column"
+          scope="col"
         >
           Connector(s)
         </th>
 </template>
 
 <script setup lang="ts">
-import CSData from '@/components/charging-stations/CSData.vue'
 import type { ChargingStationData } from '@/types'
 
+import CSData from '@/components/charging-stations/CSData.vue'
+
 defineProps<{
   chargingStations: ChargingStationData[]
 }>()
index 0428df73a4da4af57656df0dba4c7a0efb414765..fd1a0e0c5697d1170c90883d5b9d2c80da249060 100644 (file)
@@ -1,5 +1,3 @@
-import { useToast } from 'vue-toast-notification'
-
 import {
   ApplicationProtocol,
   AuthenticationType,
@@ -11,24 +9,26 @@ import {
   ResponseStatus,
   type UIServerConfigurationSection,
 } from '@/types'
+import { useToast } from 'vue-toast-notification'
 
 import { randomUUID, validateUUID } from './Utils'
 
 interface ResponseHandler {
   procedureName: ProcedureName
-  resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
   reject: (reason?: unknown) => void
+  resolve: (value: PromiseLike<ResponsePayload> | ResponsePayload) => void
 }
 
 export class UIClient {
-  private static instance: UIClient | null = null
+  private static instance: null | UIClient = null
 
-  private ws?: WebSocket
   private responseHandlers: Map<
     `${string}-${string}-${string}-${string}-${string}`,
     ResponseHandler
   >
 
+  private ws?: WebSocket
+
   private constructor (private uiServerConfiguration: UIServerConfigurationSection) {
     this.openWS()
     this.responseHandlers = new Map<
@@ -47,142 +47,6 @@ export class UIClient {
     return UIClient.instance
   }
 
-  public setConfiguration (uiServerConfiguration: UIServerConfigurationSection): void {
-    if (this.ws?.readyState === WebSocket.OPEN) {
-      this.ws.close()
-      delete this.ws
-    }
-    this.uiServerConfiguration = uiServerConfiguration
-    this.openWS()
-  }
-
-  public registerWSEventListener<K extends keyof WebSocketEventMap>(
-    event: K,
-    listener: (event: WebSocketEventMap[K]) => void,
-    options?: boolean | AddEventListenerOptions
-  ) {
-    this.ws?.addEventListener(event, listener, options)
-  }
-
-  public unregisterWSEventListener<K extends keyof WebSocketEventMap>(
-    event: K,
-    listener: (event: WebSocketEventMap[K]) => void,
-    options?: boolean | AddEventListenerOptions
-  ) {
-    this.ws?.removeEventListener(event, listener, options)
-  }
-
-  public async simulatorState (): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.SIMULATOR_STATE, {})
-  }
-
-  public async startSimulator (): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.START_SIMULATOR, {})
-  }
-
-  public async stopSimulator (): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
-  }
-
-  public async listTemplates (): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
-  }
-
-  public async listChargingStations (): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
-  }
-
-  public async addChargingStations (
-    template: string,
-    numberOfStations: number,
-    options?: ChargingStationOptions
-  ): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, {
-      template,
-      numberOfStations,
-      options,
-    })
-  }
-
-  public async deleteChargingStation (hashId: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, {
-      hashIds: [hashId],
-    })
-  }
-
-  public async setSupervisionUrl (hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
-      hashIds: [hashId],
-      url: supervisionUrl,
-    })
-  }
-
-  public async startChargingStation (hashId: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.START_CHARGING_STATION, {
-      hashIds: [hashId],
-    })
-  }
-
-  public async stopChargingStation (hashId: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, {
-      hashIds: [hashId],
-    })
-  }
-
-  public async openConnection (hashId: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
-      hashIds: [hashId],
-    })
-  }
-
-  public async closeConnection (hashId: string): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
-      hashIds: [hashId],
-    })
-  }
-
-  public async startTransaction (
-    hashId: string,
-    connectorId: number,
-    idTag: string | undefined
-  ): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.START_TRANSACTION, {
-      hashIds: [hashId],
-      connectorId,
-      idTag,
-    })
-  }
-
-  public async stopTransaction (
-    hashId: string,
-    transactionId: number | undefined
-  ): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
-      hashIds: [hashId],
-      transactionId,
-    })
-  }
-
-  public async startAutomaticTransactionGenerator (
-    hashId: string,
-    connectorId: number
-  ): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
-      hashIds: [hashId],
-      connectorIds: [connectorId],
-    })
-  }
-
-  public async stopAutomaticTransactionGenerator (
-    hashId: string,
-    connectorId: number
-  ): Promise<ResponsePayload> {
-    return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
-      hashIds: [hashId],
-      connectorIds: [connectorId],
-    })
-  }
-
   private openWS (): void {
     const protocols =
       this.uiServerConfiguration.authentication?.enabled === true &&
@@ -222,37 +86,6 @@ export class UIClient {
     }
   }
 
-  private async sendRequest (
-    procedureName: ProcedureName,
-    payload: RequestPayload
-  ): Promise<ResponsePayload> {
-    return new Promise<ResponsePayload>((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, resolve, reject })
-        } 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`))
-      }
-    })
-  }
-
   private responseHandler (messageEvent: MessageEvent<string>): void {
     let response: ProtocolResponse
     try {
@@ -279,7 +112,7 @@ export class UIClient {
 
     if (this.responseHandlers.has(uuid)) {
       // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-      const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
+      const { procedureName, reject, resolve } = this.responseHandlers.get(uuid)!
       switch (responsePayload.status) {
         case ResponseStatus.SUCCESS:
           resolve(responsePayload)
@@ -300,4 +133,171 @@ export class UIClient {
       throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
     }
   }
+
+  private async sendRequest (
+    procedureName: ProcedureName,
+    payload: RequestPayload
+  ): Promise<ResponsePayload> {
+    return new Promise<ResponsePayload>((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,
+    options?: ChargingStationOptions
+  ): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, {
+      numberOfStations,
+      options,
+      template,
+    })
+  }
+
+  public async closeConnection (hashId: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
+      hashIds: [hashId],
+    })
+  }
+
+  public async deleteChargingStation (hashId: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, {
+      hashIds: [hashId],
+    })
+  }
+
+  public async listChargingStations (): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
+  }
+
+  public async listTemplates (): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
+  }
+
+  public async openConnection (hashId: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
+      hashIds: [hashId],
+    })
+  }
+
+  public registerWSEventListener<K extends keyof WebSocketEventMap>(
+    event: K,
+    listener: (event: WebSocketEventMap[K]) => void,
+    options?: AddEventListenerOptions | boolean
+  ) {
+    this.ws?.addEventListener(event, listener, options)
+  }
+
+  public setConfiguration (uiServerConfiguration: UIServerConfigurationSection): void {
+    if (this.ws?.readyState === WebSocket.OPEN) {
+      this.ws.close()
+      delete this.ws
+    }
+    this.uiServerConfiguration = uiServerConfiguration
+    this.openWS()
+  }
+
+  public async setSupervisionUrl (hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
+      hashIds: [hashId],
+      url: supervisionUrl,
+    })
+  }
+
+  public async simulatorState (): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.SIMULATOR_STATE, {})
+  }
+
+  public async startAutomaticTransactionGenerator (
+    hashId: string,
+    connectorId: number
+  ): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
+      connectorIds: [connectorId],
+      hashIds: [hashId],
+    })
+  }
+
+  public async startChargingStation (hashId: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.START_CHARGING_STATION, {
+      hashIds: [hashId],
+    })
+  }
+
+  public async startSimulator (): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.START_SIMULATOR, {})
+  }
+
+  public async startTransaction (
+    hashId: string,
+    connectorId: number,
+    idTag: string | undefined
+  ): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.START_TRANSACTION, {
+      connectorId,
+      hashIds: [hashId],
+      idTag,
+    })
+  }
+
+  public async stopAutomaticTransactionGenerator (
+    hashId: string,
+    connectorId: number
+  ): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
+      connectorIds: [connectorId],
+      hashIds: [hashId],
+    })
+  }
+
+  public async stopChargingStation (hashId: string): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, {
+      hashIds: [hashId],
+    })
+  }
+
+  public async stopSimulator (): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
+  }
+
+  public async stopTransaction (
+    hashId: string,
+    transactionId: number | undefined
+  ): Promise<ResponsePayload> {
+    return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
+      hashIds: [hashId],
+      transactionId,
+    })
+  }
+
+  public unregisterWSEventListener<K extends keyof WebSocketEventMap>(
+    event: K,
+    listener: (event: WebSocketEventMap[K]) => void,
+    options?: AddEventListenerOptions | boolean
+  ) {
+    this.ws?.removeEventListener(event, listener, options)
+  }
 }
index c4f232343f179fd3fbc85891d10bf32cd7978bf3..6c081aa134b353cf09da2073a63d7abf46a28f18 100644 (file)
@@ -1,12 +1,11 @@
-import 'vue-toast-notification/dist/theme-bootstrap.css'
-
-import { type App as AppType, type Component, createApp, ref } from 'vue'
-import ToastPlugin from 'vue-toast-notification'
+import type { ChargingStationData, ConfigurationData, UIServerConfigurationSection } from '@/types'
 
 import App from '@/App.vue'
 import { getFromLocalStorage, setToLocalStorage, UIClient } from '@/composables'
 import { router } from '@/router'
-import type { ChargingStationData, ConfigurationData, UIServerConfigurationSection } from '@/types'
+import { type App as AppType, type Component, createApp, ref } from 'vue'
+import ToastPlugin from 'vue-toast-notification'
+import 'vue-toast-notification/dist/theme-bootstrap.css'
 
 const app = createApp(App as Component)
 
index 747b7849a1e03f8906d6337c8931a7308f6006a2..ecc8278866c78c5111d222f1cb5a8ce65025cde0 100644 (file)
@@ -1,54 +1,53 @@
 /* eslint-disable @typescript-eslint/no-unsafe-assignment */
-import { createRouter, createWebHistory } from 'vue-router'
-
 import AddChargingStations from '@/components/actions/AddChargingStations.vue'
 import SetSupervisionUrl from '@/components/actions/SetSupervisionUrl.vue'
 import StartTransaction from '@/components/actions/StartTransaction.vue'
 import ChargingStationsView from '@/views/ChargingStationsView.vue'
 import NotFoundView from '@/views/NotFoundView.vue'
+import { createRouter, createWebHistory } from 'vue-router'
 
 export const router = createRouter({
   history: createWebHistory(),
   routes: [
     {
-      path: '/',
-      name: 'charging-stations',
       components: {
         default: ChargingStationsView,
       },
+      name: 'charging-stations',
+      path: '/',
     },
     {
-      path: '/add-charging-stations',
-      name: 'add-charging-stations',
       components: {
-        default: ChargingStationsView,
         action: AddChargingStations,
+        default: ChargingStationsView,
       },
+      name: 'add-charging-stations',
+      path: '/add-charging-stations',
     },
     {
-      path: '/set-supervision-url/:hashId/:chargingStationId',
-      name: 'set-supervision-url',
       components: {
-        default: ChargingStationsView,
         action: SetSupervisionUrl,
+        default: ChargingStationsView,
       },
-      props: { default: false, action: true },
+      name: 'set-supervision-url',
+      path: '/set-supervision-url/:hashId/:chargingStationId',
+      props: { action: true, default: false },
     },
     {
-      path: '/start-transaction/:hashId/:chargingStationId/:connectorId',
-      name: 'start-transaction',
       components: {
-        default: ChargingStationsView,
         action: StartTransaction,
+        default: ChargingStationsView,
       },
-      props: { default: false, action: true },
+      name: 'start-transaction',
+      path: '/start-transaction/:hashId/:chargingStationId/:connectorId',
+      props: { action: true, default: false },
     },
     {
-      name: 'not-found',
-      path: '/:pathMatch(.*)*',
       components: {
         default: NotFoundView,
       },
+      name: 'not-found',
+      path: '/:pathMatch(.*)*',
     },
   ],
 })
index de1c0164272747de56a78210303fdf4f26393623..13685ebb1076c6fcdd0f9e996214265d624b3b60 100644 (file)
@@ -6,9 +6,9 @@ declare module 'vue' {
     RouterView: (typeof import('vue-router'))['RouterView']
   }
   interface ComponentCustomProperties {
+    $chargingStations: import('vue').Ref<import('@/types').ChargingStationData[]> | undefined
     $configuration: import('vue').Ref<import('@/types').ConfigurationData> | undefined
     $templates: import('vue').Ref<string[]> | undefined
-    $chargingStations: import('vue').Ref<import('@/types').ChargingStationData[]> | undefined
     $uiClient: import('@/composables').UIClient | undefined
   }
 }
index 8cb311cae1a9cf479bf39ecae65c0caf670b9983..8790e78fcaa816062e2ca7bf8a8077d4c598321c 100644 (file)
@@ -1,22 +1,22 @@
 import type { JsonObject } from './JsonType'
 
 export enum IdTagDistribution {
+  CONNECTOR_AFFINITY = 'connector-affinity',
   RANDOM = 'random',
-  ROUND_ROBIN = 'round-robin',
-  CONNECTOR_AFFINITY = 'connector-affinity'
+  ROUND_ROBIN = 'round-robin'
 }
 
 export interface AutomaticTransactionGeneratorConfiguration extends JsonObject {
   enable: boolean
-  minDuration: number
+  idTagDistribution?: IdTagDistribution
+  maxDelayBetweenTwoTransactions: number
   maxDuration: number
   minDelayBetweenTwoTransactions: number
-  maxDelayBetweenTwoTransactions: number
+  minDuration: number
   probabilityOfStart: number
-  stopAfterHours: number
-  stopAbsoluteDuration: boolean
   requireAuthorize?: boolean
-  idTagDistribution?: IdTagDistribution
+  stopAbsoluteDuration: boolean
+  stopAfterHours: number
 }
 
 export interface ChargingStationAutomaticTransactionGeneratorConfiguration extends JsonObject {
@@ -25,19 +25,19 @@ export interface ChargingStationAutomaticTransactionGeneratorConfiguration exten
 }
 
 export interface ChargingStationData extends JsonObject {
-  started: boolean
-  stationInfo: ChargingStationInfo
+  automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
+  bootNotificationResponse?: BootNotificationResponse
   connectors: ConnectorStatus[]
   evses: EvseStatus[]
   ocppConfiguration: ChargingStationOcppConfiguration
+  started: boolean
+  stationInfo: ChargingStationInfo
   supervisionUrl: string
   wsState?:
+    | typeof WebSocket.CLOSED
+    | typeof WebSocket.CLOSING
     | typeof WebSocket.CONNECTING
     | typeof WebSocket.OPEN
-    | typeof WebSocket.CLOSING
-    | typeof WebSocket.CLOSED
-  bootNotificationResponse?: BootNotificationResponse
-  automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
 }
 
 export enum OCPP16FirmwareStatus {
@@ -46,17 +46,17 @@ export enum OCPP16FirmwareStatus {
   Downloading = 'Downloading',
   Idle = 'Idle',
   InstallationFailed = 'InstallationFailed',
-  Installing = 'Installing',
-  Installed = 'Installed'
+  Installed = 'Installed',
+  Installing = 'Installing'
 }
 
 export interface FirmwareUpgrade extends JsonObject {
+  failureStatus?: FirmwareStatus
+  reset?: boolean
   versionUpgrade?: {
     patternGroup?: number
     step?: number
   }
-  reset?: boolean
-  failureStatus?: FirmwareStatus
 }
 
 export const FirmwareStatus = {
@@ -66,76 +66,76 @@ export const FirmwareStatus = {
 export type FirmwareStatus = OCPP16FirmwareStatus
 
 export interface ChargingStationOptions extends JsonObject {
-  supervisionUrls?: string | string[]
-  persistentConfiguration?: boolean
-  autoStart?: boolean
   autoRegister?: boolean
+  autoStart?: boolean
   enableStatistics?: boolean
   ocppStrictCompliance?: boolean
+  persistentConfiguration?: boolean
   stopTransactionsOnStopped?: boolean
+  supervisionUrls?: string | string[]
 }
 
 export interface ChargingStationInfo extends JsonObject {
-  hashId: string
-  templateIndex: number
-  templateName: string
-  chargingStationId: string
-  chargeBoxSerialNumber?: string
-  chargePointSerialNumber?: string
-  meterSerialNumber?: string
-  maximumPower?: number // Always in Watt
-  maximumAmperage?: number // Always in Ampere
-  firmwareStatus?: FirmwareStatus
-  templateHash?: string
-  supervisionUrls?: string | string[]
-  supervisionUrlOcppConfiguration?: boolean
-  supervisionUrlOcppKey?: string
-  supervisionUser?: string
-  supervisionPassword?: string
-  autoStart?: boolean
-  ocppVersion?: OCPPVersion
-  ocppProtocol?: OCPPProtocol
-  ocppStrictCompliance?: boolean
-  ocppPersistentConfiguration?: boolean
-  stationInfoPersistentConfiguration?: boolean
+  amperageLimitationOcppKey?: string
+  amperageLimitationUnit?: AmpereUnits
   automaticTransactionGeneratorPersistentConfiguration?: boolean
-  idTagsFile?: string
+  autoReconnectMaxRetries?: number
+  autoRegister?: boolean
+  autoStart?: boolean
   baseName: string
-  nameSuffix?: string
-  fixedName?: boolean
+  beginEndMeterValues?: boolean
+  chargeBoxSerialNumber?: string
   chargePointModel: string
+  chargePointSerialNumber?: string
   chargePointVendor: string
-  firmwareVersionPattern?: string
-  firmwareVersion?: string
+  chargingStationId: string
+  commandsSupport?: CommandsSupport
+  currentOutType?: CurrentType
+  customValueLimitationMeterValues?: boolean
+  enableStatistics?: boolean
+  firmwareStatus?: FirmwareStatus
   firmwareUpgrade?: FirmwareUpgrade
+  firmwareVersion?: string
+  firmwareVersionPattern?: string
+  fixedName?: boolean
+  hashId: string
   iccid?: string
+  idTagsFile?: string
   imsi?: string
+  mainVoltageMeterValues?: boolean
+  maximumAmperage?: number // Always in Ampere
+  maximumPower?: number // Always in Watt
+  messageTriggerSupport?: Record<MessageTrigger, boolean>
+  meteringPerTransaction?: boolean
+  meterSerialNumber?: string
   meterType?: string
-  powerSharedByConnectors?: boolean
-  currentOutType?: CurrentType
-  voltageOut?: Voltage
+  nameSuffix?: string
   numberOfPhases?: number
-  useConnectorId0?: boolean
+  ocppPersistentConfiguration?: boolean
+  ocppProtocol?: OCPPProtocol
+  ocppStrictCompliance?: boolean
+  ocppVersion?: OCPPVersion
+  outOfOrderEndMeterValues?: boolean
+  phaseLineToLineVoltageMeterValues?: boolean
+  powerSharedByConnectors?: boolean
   randomConnectors?: boolean
-  resetTime?: number
-  autoRegister?: boolean
-  autoReconnectMaxRetries?: number
   reconnectExponentialDelay?: boolean
   registrationMaxRetries?: number
-  enableStatistics?: boolean
   remoteAuthorization?: boolean
-  amperageLimitationOcppKey?: string
-  amperageLimitationUnit?: AmpereUnits
-  beginEndMeterValues?: boolean
-  outOfOrderEndMeterValues?: boolean
-  meteringPerTransaction?: boolean
-  transactionDataMeterValues?: boolean
+  resetTime?: number
+  stationInfoPersistentConfiguration?: boolean
   stopTransactionsOnStopped?: boolean
-  mainVoltageMeterValues?: boolean
-  phaseLineToLineVoltageMeterValues?: boolean
-  customValueLimitationMeterValues?: boolean
-  commandsSupport?: CommandsSupport
-  messageTriggerSupport?: Record<MessageTrigger, boolean>
+  supervisionPassword?: string
+  supervisionUrlOcppConfiguration?: boolean
+  supervisionUrlOcppKey?: string
+  supervisionUrls?: string | string[]
+  supervisionUser?: string
+  templateHash?: string
+  templateIndex: number
+  templateName: string
+  transactionDataMeterValues?: boolean
+  useConnectorId0?: boolean
+  voltageOut?: Voltage
 }
 
 export interface ChargingStationOcppConfiguration extends JsonObject {
@@ -143,8 +143,8 @@ export interface ChargingStationOcppConfiguration extends JsonObject {
 }
 
 export interface ConfigurationKey extends OCPPConfigurationKey {
-  visible?: boolean
   reboot?: boolean
+  visible?: boolean
 }
 
 export interface OCPPConfigurationKey extends JsonObject {
@@ -154,18 +154,18 @@ export interface OCPPConfigurationKey extends JsonObject {
 }
 
 export enum OCPP16IncomingRequestCommand {
-  RESET = 'Reset',
-  CLEAR_CACHE = 'ClearCache',
   CHANGE_AVAILABILITY = 'ChangeAvailability',
-  UNLOCK_CONNECTOR = 'UnlockConnector',
-  GET_CONFIGURATION = 'GetConfiguration',
   CHANGE_CONFIGURATION = 'ChangeConfiguration',
-  SET_CHARGING_PROFILE = 'SetChargingProfile',
+  CLEAR_CACHE = 'ClearCache',
   CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
+  GET_CONFIGURATION = 'GetConfiguration',
+  GET_DIAGNOSTICS = 'GetDiagnostics',
   REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
   REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction',
-  GET_DIAGNOSTICS = 'GetDiagnostics',
-  TRIGGER_MESSAGE = 'TriggerMessage'
+  RESET = 'Reset',
+  SET_CHARGING_PROFILE = 'SetChargingProfile',
+  TRIGGER_MESSAGE = 'TriggerMessage',
+  UNLOCK_CONNECTOR = 'UnlockConnector'
 }
 
 export const IncomingRequestCommand = {
@@ -175,14 +175,14 @@ export const IncomingRequestCommand = {
 export type IncomingRequestCommand = OCPP16IncomingRequestCommand
 
 export enum OCPP16RequestCommand {
+  AUTHORIZE = 'Authorize',
   BOOT_NOTIFICATION = 'BootNotification',
+  DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification',
   HEARTBEAT = 'Heartbeat',
-  STATUS_NOTIFICATION = 'StatusNotification',
-  AUTHORIZE = 'Authorize',
-  START_TRANSACTION = 'StartTransaction',
-  STOP_TRANSACTION = 'StopTransaction',
   METER_VALUES = 'MeterValues',
-  DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification'
+  START_TRANSACTION = 'StartTransaction',
+  STATUS_NOTIFICATION = 'StatusNotification',
+  STOP_TRANSACTION = 'StopTransaction'
 }
 
 export const RequestCommand = {
@@ -200,9 +200,9 @@ export enum OCPP16RegistrationStatus {
 }
 
 export interface OCPP16BootNotificationResponse extends JsonObject {
-  status: OCPP16RegistrationStatus
   currentTime: Date
   interval: number
+  status: OCPP16RegistrationStatus
 }
 
 export enum OCPP16MessageTrigger {
@@ -248,26 +248,26 @@ export enum Voltage {
 }
 
 export enum AmpereUnits {
-  MILLI_AMPERE = 'mA',
+  AMPERE = 'A',
   CENTI_AMPERE = 'cA',
   DECI_AMPERE = 'dA',
-  AMPERE = 'A'
+  MILLI_AMPERE = 'mA'
 }
 
 export interface ConnectorStatus extends JsonObject {
+  authorizeIdTag?: string
   availability: AvailabilityType
   bootStatus?: ChargePointStatus
-  status?: ChargePointStatus
-  authorizeIdTag?: string
+  energyActiveImportRegisterValue?: number // In Wh
   idTagAuthorized?: boolean
-  localAuthorizeIdTag?: string
   idTagLocalAuthorized?: boolean
-  transactionRemoteStarted?: boolean
-  transactionStarted?: boolean
+  localAuthorizeIdTag?: string
+  status?: ChargePointStatus
+  transactionEnergyActiveImportRegisterValue?: number // In Wh
   transactionId?: number
   transactionIdTag?: string
-  energyActiveImportRegisterValue?: number // In Wh
-  transactionEnergyActiveImportRegisterValue?: number // In Wh
+  transactionRemoteStarted?: boolean
+  transactionStarted?: boolean
 }
 
 export interface EvseStatus extends JsonObject {
@@ -283,33 +283,33 @@ export type AvailabilityType = OCPP16AvailabilityType
 
 export enum OCPP16ChargePointStatus {
   AVAILABLE = 'Available',
-  PREPARING = 'Preparing',
   CHARGING = 'Charging',
-  OCCUPIED = 'Occupied',
-  SUSPENDED_EVSE = 'SuspendedEVSE',
-  SUSPENDED_EV = 'SuspendedEV',
+  FAULTED = 'Faulted',
   FINISHING = 'Finishing',
+  OCCUPIED = 'Occupied',
+  PREPARING = 'Preparing',
   RESERVED = 'Reserved',
-  UNAVAILABLE = 'Unavailable',
-  FAULTED = 'Faulted'
+  SUSPENDED_EV = 'SuspendedEV',
+  SUSPENDED_EVSE = 'SuspendedEVSE',
+  UNAVAILABLE = 'Unavailable'
 }
 export type ChargePointStatus = OCPP16ChargePointStatus
 
 export interface Status extends JsonObject {
-  start?: boolean
-  startDate?: Date
-  lastRunDate?: Date
-  stopDate?: Date
-  stoppedDate?: Date
-  authorizeRequests?: number
   acceptedAuthorizeRequests?: number
-  rejectedAuthorizeRequests?: number
-  startTransactionRequests?: number
   acceptedStartTransactionRequests?: number
-  rejectedStartTransactionRequests?: number
-  stopTransactionRequests?: number
   acceptedStopTransactionRequests?: number
+  authorizeRequests?: number
+  lastRunDate?: Date
+  rejectedAuthorizeRequests?: number
+  rejectedStartTransactionRequests?: number
   rejectedStopTransactionRequests?: number
   skippedConsecutiveTransactions?: number
   skippedTransactions?: number
+  start?: boolean
+  startDate?: Date
+  startTransactionRequests?: number
+  stopDate?: Date
+  stoppedDate?: Date
+  stopTransactionRequests?: number
 }
index 1c8de85bc20c623f1656104b01bebf1397f22407..40586918aeb13483e7bb002e9a9e7014aae27480 100644 (file)
@@ -5,16 +5,16 @@ export interface ConfigurationData {
 }
 
 export interface UIServerConfigurationSection {
-  name?: string
-  host: string
-  port: number
-  secure?: boolean
-  protocol: Protocol
-  version: ProtocolVersion
   authentication?: {
     enabled: boolean
+    password?: string
     type: AuthenticationType
     username?: string
-    password?: string
   }
+  host: string
+  name?: string
+  port: number
+  protocol: Protocol
+  secure?: boolean
+  version: ProtocolVersion
 }
index 8f597383a4bba8d98e727288dd987a579c1c0328..db6192a5242d5755b62b84e2cd72463b627e1d85 100644 (file)
@@ -1,3 +1,3 @@
-type JsonPrimitive = string | number | boolean | Date | null
+type JsonPrimitive = boolean | Date | null | number | string
 export type JsonObject = { [key in string]?: JsonType }
-export type JsonType = JsonPrimitive | JsonType[] | JsonObject
+export type JsonType = JsonObject | JsonPrimitive | JsonType[]
index 8522aa11dcd8c4a10ac40a7e95ec94523bad1992..39d9e0fa8d6df7162341c03f74c842adf5e4e84a 100644 (file)
@@ -29,51 +29,51 @@ export type ProtocolResponse = [
 
 export type ProtocolRequestHandler = (
   payload: RequestPayload
-) => ResponsePayload | Promise<ResponsePayload>
+) => Promise<ResponsePayload> | ResponsePayload
 
 export enum ProcedureName {
-  SIMULATOR_STATE = 'simulatorState',
-  START_SIMULATOR = 'startSimulator',
-  STOP_SIMULATOR = 'stopSimulator',
-  LIST_TEMPLATES = 'listTemplates',
-  LIST_CHARGING_STATIONS = 'listChargingStations',
   ADD_CHARGING_STATIONS = 'addChargingStations',
+  CLOSE_CONNECTION = 'closeConnection',
   DELETE_CHARGING_STATIONS = 'deleteChargingStations',
-  SET_SUPERVISION_URL = 'setSupervisionUrl',
-  START_CHARGING_STATION = 'startChargingStation',
-  STOP_CHARGING_STATION = 'stopChargingStation',
+  LIST_CHARGING_STATIONS = 'listChargingStations',
+  LIST_TEMPLATES = 'listTemplates',
   OPEN_CONNECTION = 'openConnection',
-  CLOSE_CONNECTION = 'closeConnection',
+  SET_SUPERVISION_URL = 'setSupervisionUrl',
+  SIMULATOR_STATE = 'simulatorState',
   START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
-  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+  START_CHARGING_STATION = 'startChargingStation',
+  START_SIMULATOR = 'startSimulator',
   START_TRANSACTION = 'startTransaction',
+  STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+  STOP_CHARGING_STATION = 'stopChargingStation',
+  STOP_SIMULATOR = 'stopSimulator',
   STOP_TRANSACTION = 'stopTransaction'
 }
 
 export interface RequestPayload extends JsonObject {
-  hashIds?: string[]
   connectorIds?: number[]
+  hashIds?: string[]
 }
 
 export enum ResponseStatus {
-  SUCCESS = 'success',
-  FAILURE = 'failure'
+  FAILURE = 'failure',
+  SUCCESS = 'success'
 }
 
 export interface ResponsePayload extends JsonObject {
-  status: ResponseStatus
   hashIds?: string[]
+  status: ResponseStatus
 }
 
 interface TemplateStatistics extends JsonObject {
-  configured: number
   added: number
-  started: number
+  configured: number
   indexes: number[]
+  started: number
 }
 
 export interface SimulatorState extends JsonObject {
-  version: string
   started: boolean
   templateStatistics: Record<string, TemplateStatistics>
+  version: string
 }
index 90f4f0608052a85a3b2b87457a41d7c9aecb86bc..b532ba254397275ea40f90b0a29521ad9a0d9d54 100644 (file)
       <ToggleButton
         :id="'simulator'"
         :key="state.renderSimulator"
-        :status="simulatorState?.started"
-        :on="() => startSimulator()"
-        :off="() => stopSimulator()"
         :class="simulatorButtonClass"
+        :off="() => stopSimulator()"
+        :on="() => startSimulator()"
+        :status="simulatorState?.started"
       >
         {{ simulatorButtonMessage }}
       </ToggleButton>
       <ToggleButton
         :id="'add-charging-stations'"
         :key="state.renderAddChargingStations"
-        :shared="true"
-        :on="
+        :off="
           () => {
-            $router.push({ name: 'add-charging-stations' })
+            $router.push({ name: 'charging-stations' })
           }
         "
-        :off="
+        :on="
           () => {
-            $router.push({ name: 'charging-stations' })
+            $router.push({ name: 'add-charging-stations' })
           }
         "
+        :shared="true"
         @clicked="
           () => {
             state.renderChargingStations = randomUUID()
 </template>
 
 <script setup lang="ts">
-import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useToast } from 'vue-toast-notification'
+import type {
+  ChargingStationData,
+  ResponsePayload,
+  SimulatorState,
+  UIServerConfigurationSection,
+} from '@/types'
 
 import ReloadButton from '@/components/buttons/ReloadButton.vue'
 import ToggleButton from '@/components/buttons/ToggleButton.vue'
@@ -128,12 +132,8 @@ import {
   setToLocalStorage,
   useUIClient,
 } from '@/composables'
-import type {
-  ChargingStationData,
-  ResponsePayload,
-  SimulatorState,
-  UIServerConfigurationSection,
-} from '@/types'
+import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useToast } from 'vue-toast-notification'
 
 const simulatorState = ref<SimulatorState | undefined>(undefined)
 
@@ -148,20 +148,20 @@ const simulatorButtonMessage = computed<string>(
 )
 
 const state = ref<{
-  renderSimulator: `${string}-${string}-${string}-${string}-${string}`
-  renderAddChargingStations: `${string}-${string}-${string}-${string}-${string}`
-  renderChargingStations: `${string}-${string}-${string}-${string}-${string}`
+  gettingChargingStations: boolean
   gettingSimulatorState: boolean
   gettingTemplates: boolean
-  gettingChargingStations: boolean
+  renderAddChargingStations: `${string}-${string}-${string}-${string}-${string}`
+  renderChargingStations: `${string}-${string}-${string}-${string}-${string}`
+  renderSimulator: `${string}-${string}-${string}-${string}-${string}`
   uiServerIndex: number
 }>({
-  renderSimulator: randomUUID(),
-  renderAddChargingStations: randomUUID(),
-  renderChargingStations: randomUUID(),
+  gettingChargingStations: false,
   gettingSimulatorState: false,
   gettingTemplates: false,
-  gettingChargingStations: false,
+  renderAddChargingStations: randomUUID(),
+  renderChargingStations: randomUUID(),
+  renderSimulator: randomUUID(),
   uiServerIndex: getFromLocalStorage<number>('uiServerConfigurationIndex', 0),
 })
 
@@ -292,14 +292,14 @@ onUnmounted(() => {
 })
 
 const uiServerConfigurations: {
-  index: number
   configuration: UIServerConfigurationSection
+  index: number
 }[] = (
   app!.appContext.config.globalProperties.$configuration!.value
     .uiServer as UIServerConfigurationSection[]
 ).map((configuration: UIServerConfigurationSection, index: number) => ({
-  index,
   configuration,
+  index,
 }))
 
 const startSimulator = (): void => {
index 3eb2838e8925cb1b38e872695e66162c97883607..d11b339dbfe77f7482b4b0c144d9d77974fd314c 100644 (file)
@@ -1,9 +1,8 @@
+import finalhandler from 'finalhandler'
 import { createServer } from 'node:http'
 import { dirname, join } from 'node:path'
 import { env } from 'node:process'
 import { fileURLToPath } from 'node:url'
-
-import finalhandler from 'finalhandler'
 import serveStatic from 'serve-static'
 
 const isCFEnvironment = env.VCAP_APPLICATION != null
index 34ceaa4cac5c26fec6f7a1af9b3662265151127f..f14b20f76858d9bf7e5a9b3a22b1c3b5e5a1245a 100644 (file)
@@ -1,8 +1,8 @@
-import { shallowMount } from '@vue/test-utils'
-import { expect, test } from 'vitest'
+import type { ChargingStationData } from '@/types'
 
 import CSTable from '@/components/charging-stations/CSTable.vue'
-import type { ChargingStationData } from '@/types'
+import { shallowMount } from '@vue/test-utils'
+import { expect, test } from 'vitest'
 
 test('renders CS table columns name', () => {
   const chargingStations: ChargingStationData[] = []
index f4004a8fc5cc21ce693e9b7957220c71f0b56cad..100a6db3dda2944760e47a02ef53b92e75ec9f07 100644 (file)
@@ -1,7 +1,6 @@
-import { fileURLToPath, URL } from 'node:url'
-
 import vue from '@vitejs/plugin-vue'
 import vueJsx from '@vitejs/plugin-vue-jsx'
+import { fileURLToPath, URL } from 'node:url'
 import { defineConfig } from 'vite'
 
 export default defineConfig({
index f0a2b12cc29e00ce5be8d6df622820726e81012e..891e3761ad2a39f6fb69d49df5fdabcf395e7de3 100644 (file)
@@ -1,5 +1,4 @@
 import { fileURLToPath } from 'node:url'
-
 import { mergeConfig } from 'vite'
 import { configDefaults, defineConfig } from 'vitest/config'
 
@@ -9,13 +8,13 @@ export default mergeConfig(
   viteConfig,
   defineConfig({
     test: {
-      environment: 'jsdom',
-      exclude: [...configDefaults.exclude, 'e2e/*'],
-      root: fileURLToPath(new URL('./', import.meta.url)),
       coverage: {
         provider: 'v8',
         reporter: ['text', 'lcov'],
       },
+      environment: 'jsdom',
+      exclude: [...configDefaults.exclude, 'e2e/*'],
+      root: fileURLToPath(new URL('./', import.meta.url)),
     },
   })
 )