From: Jérôme Benoit Date: Wed, 21 Aug 2024 21:29:22 +0000 (+0200) Subject: refactor: switch to eslint-plugin-perfectionist X-Git-Tag: ocpp-server@v1.5.2~103 X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=0749233f25516e4c73ee8dbcea8c4ad6b8a506bb;p=e-mobility-charging-stations-simulator.git refactor: switch to eslint-plugin-perfectionist Signed-off-by: Jérôme Benoit --- diff --git a/eslint.config.js b/eslint.config.js index 925bd2fe..36e27074 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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({ diff --git a/mikro-orm.config-template.ts b/mikro-orm.config-template.ts index af4b08dd..d2796255 100644 --- a/mikro-orm.config-template.ts +++ b/mikro-orm.config-template.ts @@ -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, }) diff --git a/package.json b/package.json index 62a6c3eb..72e01596 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@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", @@ -127,7 +127,7 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b80e48e1..11ad1cf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/scripts/build-requirements.js b/scripts/build-requirements.js index 75617fd6..4e4aafbc 100644 --- a/scripts/build-requirements.js +++ b/scripts/build-requirements.js @@ -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' diff --git a/scripts/bundle.js b/scripts/bundle.js index 6f419e18..35caeb57 100644 --- a/scripts/bundle.js +++ b/scripts/bundle.js @@ -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') diff --git a/scripts/runtime.js b/scripts/runtime.js index d32efe7f..ed1bf141 100644 --- a/scripts/runtime.js +++ b/scripts/runtime.js @@ -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 diff --git a/src/charging-station/AutomaticTransactionGenerator.ts b/src/charging-station/AutomaticTransactionGenerator.ts index e760b5f0..9b3bcaaf 100644 --- a/src/charging-station/AutomaticTransactionGenerator.ts +++ b/src/charging-station/AutomaticTransactionGenerator.ts @@ -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 - 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 + + 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( + // 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 { - 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 { - 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 { - 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( - // 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 { @@ -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 { + 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 { + 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 { + 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`) } } } diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index dfd91916..2603de82 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -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 - private readonly uiServer: AbstractUIServer - private storage?: Storage - private readonly templateStatistics: Map - 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 + private readonly uiServer: AbstractUIServer private uiServerStarted: boolean + private readonly version: string = version + + private readonly workerEventAdded = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` + ) + } + + private readonly workerEventDeleted = (data: ChargingStationData): void => { + this.uiServer.chargingStations.delete(data.stationInfo.hashId) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)! + --templateStatistics.added + templateStatistics.indexes.delete(data.stationInfo.templateIndex) + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))` + ) + } + + private readonly workerEventPerformanceStatistics = (data: Statistics): void => { + // eslint-disable-next-line @typescript-eslint/unbound-method + if (isAsyncFunction(this.storage?.storePerformanceStatistics)) { + ;( + this.storage.storePerformanceStatistics as ( + performanceStatistics: Statistics + ) => Promise + )(data).catch(Constants.EMPTY_FUNCTION) + } else { + ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)( + data + ) + } + } + + private readonly workerEventStarted = (data: ChargingStationData): void => { + this.uiServer.chargingStations.set(data.stationInfo.hashId, data) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ++this.templateStatistics.get(data.stationInfo.templateName)!.started + logger.info( + `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + data.stationInfo.chargingStationId + } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))` + ) + } + + private 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 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(), + 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(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 | 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, + ...(workerConfiguration.resourceLimits != null && { + workerOptions: { + resourceLimits: workerConfiguration.resourceLimits, + }, + }), + }, + workerStartDelay: workerConfiguration.startDelay, + } ) } - public async start (): Promise { - if (!this.started) { - if (!this.starting) { - this.starting = true - this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded) + private messageHandler ( + msg: ChargingStationWorkerMessage + ): 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 { + await this.stop() + if ( + this.uiServerStarted && + Configuration.getConfigurationSection(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(ConfigurationSection.worker) + ) + await this.start() + } + + private async waitChargingStationsStopped (): Promise { + return await new Promise((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 { + if (!this.started && !this.starting) { + throw new BaseError( + 'Cannot add charging station while the charging stations simulator is not started' + ) + } + const stationInfo = await this.workerImplementation?.addElement({ + index, + options, + templateFile: join( + dirname(fileURLToPath(import.meta.url)), + 'assets', + 'station-templates', + templateFile + ), + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))! + ++templateStatistics.added + templateStatistics.indexes.add(index) + return stationInfo + } + + public getLastIndex (templateName: string): number { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const indexes = [...this.templateStatistics.get(templateName)!.indexes] + .concat(0) + .sort((a, b) => a - b) + for (let i = 0; i < indexes.length - 1; i++) { + if (indexes[i + 1] - indexes[i] !== 1) { + return indexes[i] + } + } + return indexes[indexes.length - 1] + } + + public getPerformanceStatistics (): IterableIterator | undefined { + return this.storage?.getPerformanceStatistics() + } + + public getState (): SimulatorState { + return { + configuration: Configuration.getConfigurationData(), + started: this.started, + templateStatistics: this.templateStatistics, + version: this.version, + } + } + + public async start (): Promise { + if (!this.started) { + if (!this.starting) { + this.starting = true + this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded) this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted) this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted) this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped) @@ -292,317 +576,35 @@ export class Bootstrap extends EventEmitter { } } - private async restart (): Promise { - await this.stop() - if ( - this.uiServerStarted && - Configuration.getConfigurationSection(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(ConfigurationSection.worker) - ) - await this.start() - } - - private async waitChargingStationsStopped (): Promise { - return await new Promise((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, - ...(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 - ): 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 - )(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(), - }) - 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(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 { - 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 |') - } } diff --git a/src/charging-station/ChargingStation.ts b/src/charging-station/ChargingStation.ts index cdf69304..83e5517b 100644 --- a/src/charging-station/ChargingStation.ts +++ b/src/charging-station/ChargingStation.ts @@ -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 - public readonly evses: Map - public readonly requests: Map - 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 - 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 + public readonly evses: Map + 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 + 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(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( - 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 { - 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 { - 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 { + 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 { - 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)) + } + 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)) + ) + } + } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { + for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { + const evseStatus = clone(evseStatusConfiguration) + delete evseStatus.connectorsStatus + this.evses.set(evseId, { + ...(evseStatus as EvseStatus), + connectors: new Map( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [ + connectorId, + prepareConnectorStatus(connectorStatus), + ]) + ), + }) } + } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) { + const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}` + logger.error(`${this.logPrefix()} ${errorMsg}`) + throw new BaseError(errorMsg) } else { - 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 { - 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( - 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, - 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 { - 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 { - // 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() + this.ocppRequestService = OCPP16RequestService.getInstance( + OCPP16ResponseService.getInstance() + ) + break + case OCPPVersion.VERSION_20: + case OCPPVersion.VERSION_201: + this.ocppIncomingRequestService = + OCPP20IncomingRequestService.getInstance() + this.ocppRequestService = OCPP20RequestService.getInstance( + OCPP20ResponseService.getInstance() + ) + break + default: + this.handleUnsupportedVersion(ocppVersion) + break } - 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 { + 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 { + 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 { 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(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 { + 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() - this.ocppRequestService = OCPP16RequestService.getInstance( - OCPP16ResponseService.getInstance() - ) - break - case OCPPVersion.VERSION_20: - case OCPPVersion.VERSION_201: - this.ocppIncomingRequestService = - OCPP20IncomingRequestService.getInstance() - this.ocppRequestService = OCPP20RequestService.getInstance( - OCPP20ResponseService.getInstance() - ) - break - default: - this.handleUnsupportedVersion(ocppVersion) - break + // 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 { + 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)) - ) + private async stopRunningTransactions (reason?: StopTransactionReason): Promise { + if (this.hasEvses) { + for (const [evseId, evseStatus] of this.evses) { + if (evseId === 0) { + continue + } + for (const [connectorId, connectorStatus] of evseStatus.connectors) { + if (connectorStatus.transactionStarted === true) { + await this.stopTransactionOnConnector(connectorId, reason) + } + } } - } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) { - for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) { - const evseStatus = clone(evseStatusConfiguration) - delete evseStatus.connectorsStatus - this.evses.set(evseId, { - ...(evseStatus as EvseStatus), - connectors: new Map( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [ - connectorId, - prepareConnectorStatus(connectorStatus), - ]) - ), - }) - } - } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) { - const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) } else { - const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}` - logger.error(`${this.logPrefix()} ${errorMsg}`) - throw new BaseError(errorMsg) + 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)) - } - 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 { + 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 { + 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(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 { - 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 { - 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 { - 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 { + 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 { // 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 { + 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 { - 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 { - 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(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( + 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 { + 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 { + public async stopTransactionOnConnector ( + connectorId: number, + reason?: StopTransactionReason + ): Promise { + 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( + 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, + StopTransactionResponse + >(this, RequestCommand.STOP_TRANSACTION, { + meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true), + transactionId, + ...(reason != null && { reason }), + }) + } + + public get hasEvses (): boolean { + return this.connectors.size === 0 && this.evses.size > 0 + } + + public get wsConnectionUrl (): URL { + const wsConnectionBaseUrlStr = `${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + this.stationInfo?.supervisionUrlOcppConfiguration === true && + isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) && + isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value) + ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value + : this.configuredSupervisionUrl.href + }` + return new URL( + `${wsConnectionBaseUrlStr}${ + !wsConnectionBaseUrlStr.endsWith('/') ? '/' : '' + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }${this.stationInfo?.chargingStationId}` + ) } } diff --git a/src/charging-station/ChargingStationWorker.ts b/src/charging-station/ChargingStationWorker.ts index 581cd942..b8ec0afc 100644 --- a/src/charging-station/ChargingStationWorker.ts +++ b/src/charging-station/ChargingStationWorker.ts @@ -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 { constructor () { parentPort?.on('message', (message: WorkerMessage) => { - 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) } 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) } break diff --git a/src/charging-station/ConfigurationKeyUtils.ts b/src/charging-station/ConfigurationKeyUtils.ts index 072ec9a9..b182cc4f 100644 --- a/src/charging-station/ConfigurationKeyUtils.ts +++ b/src/charging-station/ConfigurationKeyUtils.ts @@ -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( diff --git a/src/charging-station/Helpers.ts b/src/charging-station/Helpers.ts index 27d397a3..240a54a4 100644 --- a/src/charging-station/Helpers.ts +++ b/src/charging-station/Helpers.ts @@ -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 } diff --git a/src/charging-station/IdTagsCache.ts b/src/charging-station/IdTagsCache.ts index 71e038be..b1993ab8 100644 --- a/src/charging-station/IdTagsCache.ts +++ b/src/charging-station/IdTagsCache.ts @@ -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 private readonly idTagsCachesAddressableIndexes: Map + private readonly logPrefix = (file: string): string => { + return logPrefix(` Id tags cache for id tags file '${file}' |`) + } + private constructor () { this.idTagsCaches = new Map() this.idTagsCachesAddressableIndexes = new Map() @@ -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) } } diff --git a/src/charging-station/SharedLRUCache.ts b/src/charging-station/SharedLRUCache.ts index 0da4558d..55cd6c23 100644 --- a/src/charging-station/SharedLRUCache.ts +++ b/src/charging-station/SharedLRUCache.ts @@ -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 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 ) } } diff --git a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts index 60e6d9ef..e70e128d 100644 --- a/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts @@ -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 | Promise | void export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel { - private readonly commandHandlers: Map private readonly chargingStation: ChargingStation + private readonly commandHandlers: Map constructor (chargingStation: ChargingStation) { super() @@ -69,86 +70,6 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne throwError: true, } this.commandHandlers = new Map([ - [ - 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 + }) + } } diff --git a/src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts index 958c9375..48bf1691 100644 --- a/src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/UIServiceWorkerBroadcastChannel.ts @@ -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 + private readonly uiService: AbstractUIService constructor (uiService: AbstractUIService) { super() @@ -29,38 +30,6 @@ export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel { this.responses = new Map() } - 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) + } + } } diff --git a/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts b/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts index 7dfa9265..66c4c0de 100644 --- a/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts +++ b/src/charging-station/broadcast-channel/WorkerBroadcastChannel.ts @@ -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) } } diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index a03b5be6..bcc96e6e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -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.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 >([ [ - OCPP16IncomingRequestCommand.RESET, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/Reset.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/CancelReservation.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CLEAR_CACHE, + OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ClearCache.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, + OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/UnlockConnector.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.GET_CONFIGURATION, + OCPP16IncomingRequestCommand.CLEAR_CACHE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/GetConfiguration.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearCache.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, + OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, + OCPP16IncomingRequestCommand.DATA_TRANSFER, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetConfiguration.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, + OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, + OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, + OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, + OCPP16IncomingRequestCommand.RESERVE_NOW, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNow.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, + OCPP16IncomingRequestCommand.RESET, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/TriggerMessage.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/Reset.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.DATA_TRANSFER, + OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/DataTransfer.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, + OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/TriggerMessage.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.RESERVE_NOW, + OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ReserveNow.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UnlockConnector.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/CancelReservation.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( + private async handleRequestCancelReservation ( chargingStation: ChargingStation, - messageId: string, - commandName: OCPP16IncomingRequestCommand, - commandPayload: ReqType - ): Promise { - let response: ResType + commandPayload: OCPP16CancelReservationRequest + ): Promise { 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( + 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 { - const { connectorId } = commandPayload + commandPayload: OCPP16ChangeAvailabilityRequest + ): Promise { + 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( + 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 { 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( + 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( + 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 { - 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 { @@ -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 { 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 { - 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( + 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 { 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( - 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( - 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 { + 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( - 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 { + 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 { + 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( - 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 { + 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( + chargingStation: ChargingStation, + messageId: string, + commandName: OCPP16IncomingRequestCommand, + commandPayload: ReqType + ): Promise { + 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( - 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) } } diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 83c9608b..655ca809 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -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( - 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DataTransfer.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.HEARTBEAT, + OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/Heartbeat.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.METER_VALUES, + OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/MeterValues.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.STATUS_NOTIFICATION, + OCPP16RequestCommand.HEARTBEAT, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StatusNotification.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/Heartbeat.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.START_TRANSACTION, + OCPP16RequestCommand.METER_VALUES, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StartTransaction.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/MeterValues.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.STOP_TRANSACTION, + OCPP16RequestCommand.START_TRANSACTION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StopTransaction.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/StartTransaction.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.DATA_TRANSFER, + OCPP16RequestCommand.STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/DataTransfer.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/StatusNotification.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, + OCPP16RequestCommand.STOP_TRANSACTION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - commandParams?: RequestType, - params?: RequestParams - ): Promise { - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. - if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { - // Pre request actions hook - switch (commandName) { - case OCPP16RequestCommand.START_TRANSACTION: - await OCPP16ServiceUtils.sendAndSetConnectorStatus( - chargingStation, - (commandParams as OCPP16StartTransactionRequest).connectorId, - OCPP16ChargePointStatus.Preparing - ) - break - } - return (await this.sendMessage( - chargingStation, - generateUUID(), - this.buildRequestPayload(chargingStation, commandName, commandParams), - commandName, - params - )) as ResponseType - } - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ErrorType.NOT_SUPPORTED, - `Unsupported OCPP command ${commandName}`, - commandName, - commandParams - ) - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters private buildRequestPayload( chargingStation: ChargingStation, @@ -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( + chargingStation: ChargingStation, + commandName: OCPP16RequestCommand, + commandParams?: RequestType, + params?: RequestParams + ): Promise { + // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. + if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + // Pre request actions hook + switch (commandName) { + case OCPP16RequestCommand.START_TRANSACTION: + await OCPP16ServiceUtils.sendAndSetConnectorStatus( + chargingStation, + (commandParams as OCPP16StartTransactionRequest).connectorId, + OCPP16ChargePointStatus.Preparing + ) + break + } + return (await this.sendMessage( + chargingStation, + generateUUID(), + this.buildRequestPayload(chargingStation, commandName, commandParams), + commandName, + params + )) as ResponseType + } + // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). + throw new OCPPError( + ErrorType.NOT_SUPPORTED, + `Unsupported OCPP command ${commandName}`, + commandName, + commandParams + ) + } } diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 71c15c91..0e738116 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -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> + + private readonly responseHandlers: Map public incomingRequestResponsePayloadValidateFunctions: Map< OCPP16IncomingRequestCommand, ValidateFunction > - protected payloadValidateFunctions: Map> - private readonly responseHandlers: Map - public constructor () { // if (new.target.name === moduleName) { // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) // } super(OCPPVersion.VERSION_16) this.responseHandlers = new Map([ + [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.BOOT_NOTIFICATION, + OCPP16RequestCommand.AUTHORIZE, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.HEARTBEAT, + OCPP16RequestCommand.BOOT_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.AUTHORIZE, + OCPP16RequestCommand.DATA_TRANSFER, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.START_TRANSACTION, + OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.STOP_TRANSACTION, + OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.STATUS_NOTIFICATION, + OCPP16RequestCommand.HEARTBEAT, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.DATA_TRANSFER, + OCPP16RequestCommand.STATUS_NOTIFICATION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, + OCPP16RequestCommand.STOP_TRANSACTION, this.ajv.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json', moduleName, 'constructor' ) @@ -205,60 +206,60 @@ export class OCPP16ResponseService extends OCPPResponseService { ValidateFunction >([ [ - OCPP16IncomingRequestCommand.RESET, + OCPP16IncomingRequestCommand.CANCEL_RESERVATION, this.ajvIncomingRequest.compile( OCPP16ServiceUtils.parseJsonSchemaFile( - '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( - 'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, + OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, + OCPP16IncomingRequestCommand.CLEAR_CACHE, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.GET_CONFIGURATION, + OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, + OCPP16IncomingRequestCommand.DATA_TRANSFER, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - 'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, + OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - 'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, + OCPP16IncomingRequestCommand.RESET, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/ResetResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.DATA_TRANSFER, + OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, + OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.RESERVE_NOW, + OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + 'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json', moduleName, 'constructor' ) ), ], [ - OCPP16IncomingRequestCommand.CANCEL_RESERVATION, + OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, this.ajvIncomingRequest.compile( - OCPP16ServiceUtils.parseJsonSchemaFile( - 'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json', + OCPP16ServiceUtils.parseJsonSchemaFile( + '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( - chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - payload: ResType, - requestPayload: ReqType - ): Promise { - if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) { - if ( - this.responseHandlers.has(commandName) && - OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) - ) { - try { - this.validatePayload(chargingStation, commandName, payload) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const responseHandler = this.responseHandlers.get(commandName)! - if (isAsyncFunction(responseHandler)) { - await responseHandler(chargingStation, payload, requestPayload) - } else { - ;( - responseHandler as ( - chargingStation: ChargingStation, - payload: JsonType, - requestPayload?: JsonType - ) => void - )(chargingStation, payload, requestPayload) - } - } catch (error) { - logger.error( - `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`, - error - ) - throw error - } - } else { - // Throw exception - throw new OCPPError( - ErrorType.NOT_IMPLEMENTED, - `${commandName} is not implemented to handle response PDU ${JSON.stringify( - payload, - undefined, - 2 - )}`, - commandName, - payload - ) - } - } else { - throw new OCPPError( - ErrorType.SECURITY_ERROR, - `${commandName} cannot be issued to handle response PDU ${JSON.stringify( - payload, - undefined, - 2 - )} while the charging station is not registered on the central server`, - commandName, - payload - ) - } - } - - private 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 { - 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 { + 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( + chargingStation: ChargingStation, + commandName: OCPP16RequestCommand, + payload: ResType, + requestPayload: ReqType + ): Promise { + if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) { + if ( + this.responseHandlers.has(commandName) && + OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) + ) { + try { + this.validatePayload(chargingStation, commandName, payload) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const responseHandler = this.responseHandlers.get(commandName)! + if (isAsyncFunction(responseHandler)) { + await responseHandler(chargingStation, payload, requestPayload) + } else { + ;( + responseHandler as ( + chargingStation: ChargingStation, + payload: JsonType, + requestPayload?: JsonType + ) => void + )(chargingStation, payload, requestPayload) + } + } catch (error) { + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`, + error + ) + throw error + } + } else { + // Throw exception + throw new OCPPError( + ErrorType.NOT_IMPLEMENTED, + `${commandName} is not implemented to handle response PDU ${JSON.stringify( + payload, + undefined, + 2 + )}`, + commandName, + payload + ) + } + } else { + throw new OCPPError( + ErrorType.SECURITY_ERROR, + `${commandName} cannot be issued to handle response PDU ${JSON.stringify( + payload, + undefined, + 2 + )} while the charging station is not registered on the central server`, + commandName, + payload + ) + } + } } diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 5d77bffb..c99bf8ed 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -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 => { - 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 => { + 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( 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) } } diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 52302a0d..9324fea9 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -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( 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 - } } diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 13fb6a12..4d519d42 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -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( - chargingStation: ChargingStation, - commandName: OCPP20RequestCommand, - commandParams?: RequestType, - params?: RequestParams - ): Promise { - // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. - if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { - // TODO: pre request actions hook - return (await this.sendMessage( - chargingStation, - generateUUID(), - this.buildRequestPayload(chargingStation, commandName, commandParams), - commandName, - params - )) as ResponseType - } - // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). - throw new OCPPError( - ErrorType.NOT_SUPPORTED, - `Unsupported OCPP command ${commandName}`, - commandName, - commandParams - ) - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters private buildRequestPayload( chargingStation: ChargingStation, @@ -121,4 +95,31 @@ export class OCPP20RequestService extends OCPPRequestService { ) } } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + public async requestHandler( + chargingStation: ChargingStation, + commandName: OCPP20RequestCommand, + commandParams?: RequestType, + params?: RequestParams + ): Promise { + // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc. + if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) { + // TODO: pre request actions hook + return (await this.sendMessage( + chargingStation, + generateUUID(), + this.buildRequestPayload(chargingStation, commandName, commandParams), + commandName, + params + )) as ResponseType + } + // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). + throw new OCPPError( + ErrorType.NOT_SUPPORTED, + `Unsupported OCPP command ${commandName}`, + commandName, + commandParams + ) + } } diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index f5faf2b7..dd16d998 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -26,14 +26,14 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js' const moduleName = 'OCPP20ResponseService' export class OCPP20ResponseService extends OCPPResponseService { + protected payloadValidateFunctions: Map> + + private readonly responseHandlers: Map public incomingRequestResponsePayloadValidateFunctions: Map< OCPP20IncomingRequestCommand, ValidateFunction > - protected payloadValidateFunctions: Map> - private readonly responseHandlers: Map - public constructor () { // if (new.target.name === moduleName) { // throw new TypeError(`Cannot construct ${new.target.name} instances directly`) @@ -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( 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 - ) - } - } } diff --git a/src/charging-station/ocpp/OCPPConstants.ts b/src/charging-station/ocpp/OCPPConstants.ts index 4b4d9256..6f530edf 100644 --- a/src/charging-station/ocpp/OCPPConstants.ts +++ b/src/charging-station/ocpp/OCPPConstants.ts @@ -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 } diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index a5f207f1..01eab722 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -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 > + 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( 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( chargingStation: ChargingStation, diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index 490f9dae..a8696bd5 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -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> + 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 { - 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 { - 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( - 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(payload) - convertDateToISOString(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( + 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(payload) - convertDateToISOString(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 { 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( + 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(payload) + convertDateToISOString(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( + 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(payload) + convertDateToISOString(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( chargingStation: ChargingStation, @@ -511,4 +457,59 @@ export abstract class OCPPRequestService { commandParams?: ReqType, params?: RequestParams ): Promise + + public async sendError ( + chargingStation: ChargingStation, + messageId: string, + ocppError: OCPPError, + commandName: IncomingRequestCommand | RequestCommand + ): Promise { + try { + // Send error message + return await this.internalSendMessage( + chargingStation, + messageId, + ocppError, + MessageType.CALL_ERROR_MESSAGE, + commandName + ) + } catch (error) { + handleSendMessageError( + chargingStation, + commandName, + MessageType.CALL_ERROR_MESSAGE, + error as Error + ) + return null + } + } + + public async sendResponse ( + chargingStation: ChargingStation, + messageId: string, + messagePayload: JsonType, + commandName: IncomingRequestCommand + ): Promise { + try { + // Send response message + return await this.internalSendMessage( + chargingStation, + messageId, + messagePayload, + MessageType.CALL_RESULT_MESSAGE, + commandName + ) + } catch (error) { + handleSendMessageError( + chargingStation, + commandName, + MessageType.CALL_RESULT_MESSAGE, + error as Error, + { + throwError: true, + } + ) + return null + } + } } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 71e62083..16bf140d 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -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> + private readonly version: OCPPVersion + public abstract incomingRequestResponsePayloadValidateFunctions: Map< IncomingRequestCommand, ValidateFunction @@ -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( chargingStation: ChargingStation, diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index 8737521b..4fa8bbfb 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -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).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).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( @@ -1401,16 +1414,4 @@ export class OCPPServiceUtils { return {} as JSONSchemaType } } - - private static readonly logPrefix = ( - ocppVersion: OCPPVersion, - moduleName?: string, - methodName?: string - ): string => { - const logMsg = - isNotEmptyString(moduleName) && isNotEmptyString(methodName) - ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:` - : ` OCPP ${ocppVersion} |` - return logPrefix(logMsg) - } } diff --git a/src/charging-station/ui-server/AbstractUIServer.ts b/src/charging-station/ui-server/AbstractUIServer.ts index 40be3687..e9696c95 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -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 - public readonly chargingStationTemplates: Set - 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 + public readonly chargingStations: Map + + public readonly chargingStationTemplates: Set public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) { this.chargingStations = new Map() @@ -58,64 +60,6 @@ export abstract class AbstractUIServer { this.uiServices = new Map() } - public buildProtocolRequest ( - uuid: `${string}-${string}-${string}-${string}-${string}`, - procedureName: ProcedureName, - requestPayload: RequestPayload - ): ProtocolRequest { - return [uuid, procedureName, requestPayload] - } - - public buildProtocolResponse ( - uuid: `${string}-${string}-${string}-${string}-${string}`, - responsePayload: ResponsePayload - ): ProtocolResponse { - return [uuid, responsePayload] - } - - public 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 { - const protocolVersion = ProtocolVersion['0.0.1'] - this.registerProtocolVersionUIService(protocolVersion) - return await (this.uiServices - .get(protocolVersion) - ?.requestHandler(request) as Promise) - } - - 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 { + const protocolVersion = ProtocolVersion['0.0.1'] + this.registerProtocolVersionUIService(protocolVersion) + return await (this.uiServices + .get(protocolVersion) + ?.requestHandler(request) as Promise) + } + public abstract sendRequest (request: ProtocolRequest): void public abstract sendResponse (response: ProtocolResponse): void - public abstract 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() + } } diff --git a/src/charging-station/ui-server/UIHttpServer.ts b/src/charging-station/ui-server/UIHttpServer.ts index 1682c0de..f33099b7 100644 --- a/src/charging-station/ui-server/UIHttpServer.ts +++ b/src/charging-station/ui-server/UIHttpServer.ts @@ -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() + } } diff --git a/src/charging-station/ui-server/UIServerFactory.ts b/src/charging-station/ui-server/UIServerFactory.ts index f25c3aae..830476b4 100644 --- a/src/charging-station/ui-server/UIServerFactory.ts +++ b/src/charging-station/ui-server/UIServerFactory.ts @@ -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) - } } diff --git a/src/charging-station/ui-server/UIServerUtils.ts b/src/charging-station/ui-server/UIServerUtils.ts index d26f59f0..2e3ea7da 100644 --- a/src/charging-station/ui-server/UIServerUtils.ts +++ b/src/charging-station/ui-server/UIServerUtils.ts @@ -22,7 +22,7 @@ export const getUsernameAndPasswordFromAuthorizationToken = ( export const handleProtocols = ( protocols: Set, _request: IncomingMessage -): string | false => { +): false | string => { let protocol: Protocol | undefined let version: ProtocolVersion | undefined if (protocols.size === 0) { diff --git a/src/charging-station/ui-server/UIWebSocketServer.ts b/src/charging-station/ui-server/UIWebSocketServer.ts index 4fa79724..2d80489f 100644 --- a/src/charging-station/ui-server/UIWebSocketServer.ts +++ b/src/charging-station/ui-server/UIWebSocketServer.ts @@ -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 - } } diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index baf621f1..3c3d51cb 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -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 - 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.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 { - 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 { - 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( @@ -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 { + 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() + } } diff --git a/src/charging-station/ui-server/ui-services/UIService001.ts b/src/charging-station/ui-server/ui-services/UIService001.ts index 511bfa7a..51553669 100644 --- a/src/charging-station/ui-server/ui-services/UIService001.ts +++ b/src/charging-station/ui-server/ui-services/UIService001.ts @@ -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 { diff --git a/src/charging-station/ui-server/ui-services/UIServiceFactory.ts b/src/charging-station/ui-server/ui-services/UIServiceFactory.ts index 08d81a36..de566fc7 100644 --- a/src/charging-station/ui-server/ui-services/UIServiceFactory.ts +++ b/src/charging-station/ui-server/ui-services/UIServiceFactory.ts @@ -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 diff --git a/src/exception/OCPPError.ts b/src/exception/OCPPError.ts index 1e716ae4..ece0283b 100644 --- a/src/exception/OCPPError.ts +++ b/src/exception/OCPPError.ts @@ -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) diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index de5503f4..15b33881 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -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( - 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, - }) - } - - private startLogStatisticsInterval (): void { - const logConfiguration = Configuration.getConfigurationSection( - 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, + }) + } + + private startLogStatisticsInterval (): void { + const logConfiguration = Configuration.getConfigurationSection( + 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( + ConfigurationSection.performanceStorage + ) + if (performanceStorageConfiguration.enabled === true) { + logger.info( + `${this.logPrefix()} storage enabled: type ${ + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + performanceStorageConfiguration.type + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + }, uri: ${performanceStorageConfiguration.uri}` + ) + } + } + + public stop (): void { + this.stopLogStatisticsInterval() + performance.clearMarks() + performance.clearMeasures() + this.performanceObserver.disconnect() } } diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index 97ab104c..e76bdb2f 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -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` - ) - } + }) } } diff --git a/src/performance/storage/MikroOrmStorage.ts b/src/performance/storage/MikroOrmStorage.ts index 7d7beede..970ac57c 100644 --- a/src/performance/storage/MikroOrmStorage.ts +++ b/src/performance/storage/MikroOrmStorage.ts @@ -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 { + 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 { + 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 { - this.clearPerformanceStatistics() + public async storePerformanceStatistics (performanceStatistics: Statistics): Promise { 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 + ) } } } diff --git a/src/performance/storage/MongoDBStorage.ts b/src/performance/storage/MongoDBStorage.ts index 5635e2b2..ef48f3bd 100644 --- a/src/performance/storage/MongoDBStorage.ts +++ b/src/performance/storage/MongoDBStorage.ts @@ -18,33 +18,22 @@ export class MongoDBStorage extends Storage { this.dbName = this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '') } - public async storePerformanceStatistics (performanceStatistics: Statistics): Promise { - try { - this.setPerformanceStatistics(performanceStatistics) - this.checkDBConnection() - await this.client - ?.db(this.dbName) - .collection(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 { - 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 { + 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 { + try { + this.setPerformanceStatistics(performanceStatistics) + this.checkDBConnection() + await this.client + ?.db(this.dbName) + .collection(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 ) } } diff --git a/src/performance/storage/None.ts b/src/performance/storage/None.ts index aae7547f..ceda1ab5 100644 --- a/src/performance/storage/None.ts +++ b/src/performance/storage/None.ts @@ -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) } } diff --git a/src/performance/storage/Storage.ts b/src/performance/storage/Storage.ts index 475c3f5d..0e21515c 100644 --- a/src/performance/storage/Storage.ts +++ b/src/performance/storage/Storage.ts @@ -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() + 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 = { - 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 { - 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 - public abstract open (): void | Promise - public abstract close (): void | Promise + public getPerformanceStatistics (): IterableIterator { + return Storage.performanceStatistics.values() + } + public abstract open (): Promise | void public abstract storePerformanceStatistics ( performanceStatistics: Statistics - ): void | Promise + ): Promise | void } diff --git a/src/performance/storage/StorageFactory.ts b/src/performance/storage/StorageFactory.ts index 2b6b84ff..277e6420 100644 --- a/src/performance/storage/StorageFactory.ts +++ b/src/performance/storage/StorageFactory.ts @@ -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 diff --git a/src/scripts/deleteChargingStations.cjs b/src/scripts/deleteChargingStations.cjs index 52349887..149d9495 100755 --- a/src/scripts/deleteChargingStations.cjs +++ b/src/scripts/deleteChargingStations.cjs @@ -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 diff --git a/src/scripts/setCSPublicFlag.cjs b/src/scripts/setCSPublicFlag.cjs index 8aa428ad..8b9e913f 100755 --- a/src/scripts/setCSPublicFlag.cjs +++ b/src/scripts/setCSPublicFlag.cjs @@ -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 diff --git a/src/types/AutomaticTransactionGenerator.ts b/src/types/AutomaticTransactionGenerator.ts index 2ff04c09..8c736384 100644 --- a/src/types/AutomaticTransactionGenerator.ts +++ b/src/types/AutomaticTransactionGenerator.ts @@ -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 { diff --git a/src/types/ChargingStationConfiguration.ts b/src/types/ChargingStationConfiguration.ts index 630e1bd0..1363ccbf 100644 --- a/src/types/ChargingStationConfiguration.ts +++ b/src/types/ChargingStationConfiguration.ts @@ -8,18 +8,18 @@ interface ConnectorsConfiguration { connectorsStatus?: ConnectorStatus[] } -export type EvseStatusConfiguration = Omit & { +export type EvseStatusConfiguration = { connectorsStatus?: ConnectorStatus[] -} +} & Omit interface EvsesConfiguration { evsesStatus?: EvseStatusConfiguration[] } -export type ChargingStationConfiguration = ChargingStationInfoConfiguration & +export type ChargingStationConfiguration = { + configurationHash?: string +} & ChargingStationAutomaticTransactionGeneratorConfiguration & + ChargingStationInfoConfiguration & ChargingStationOcppConfiguration & - ChargingStationAutomaticTransactionGeneratorConfiguration & ConnectorsConfiguration & - EvsesConfiguration & { - configurationHash?: string - } + EvsesConfiguration diff --git a/src/types/ChargingStationEvents.ts b/src/types/ChargingStationEvents.ts index 3fa6adec..4b36805f 100644 --- a/src/types/ChargingStationEvents.ts +++ b/src/types/ChargingStationEvents.ts @@ -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' } diff --git a/src/types/ChargingStationInfo.ts b/src/types/ChargingStationInfo.ts index 8f4966f9..b1fb3454 100644 --- a/src/types/ChargingStationInfo.ts +++ b/src/types/ChargingStationInfo.ts @@ -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 diff --git a/src/types/ChargingStationOcppConfiguration.ts b/src/types/ChargingStationOcppConfiguration.ts index fdf03a40..efa2f35d 100644 --- a/src/types/ChargingStationOcppConfiguration.ts +++ b/src/types/ChargingStationOcppConfiguration.ts @@ -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 { diff --git a/src/types/ChargingStationTemplate.ts b/src/types/ChargingStationTemplate.ts index 9497e500..46cc9479 100644 --- a/src/types/ChargingStationTemplate.ts +++ b/src/types/ChargingStationTemplate.ts @@ -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 + currentOutType?: CurrentType + customValueLimitationMeterValues?: boolean + enableStatistics?: boolean + Evses?: Record firmwareUpgrade?: FirmwareUpgrade + firmwareVersion?: string + firmwareVersionPattern?: string + fixedName?: boolean iccid?: string + idTagsFile?: string imsi?: string + mainVoltageMeterValues?: boolean + messageTriggerSupport?: Record + 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 - Configuration?: ChargingStationOcppConfiguration - AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration - Evses?: Record - Connectors?: Record + supervisionPassword?: string + supervisionUrlOcppConfiguration?: boolean + supervisionUrlOcppKey?: string + supervisionUrls?: string | string[] + supervisionUser?: string + templateHash?: string + transactionDataMeterValues?: boolean + useConnectorId0?: boolean + voltageOut?: Voltage + wsOptions?: WsOptions x509Certificates?: Record } diff --git a/src/types/ChargingStationWorker.ts b/src/types/ChargingStationWorker.ts index 89e79a37..b9abdeb2 100644 --- a/src/types/ChargingStationWorker.ts +++ b/src/types/ChargingStationWorker.ts @@ -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 & { +export type EvseStatusWorkerType = { connectors?: ConnectorStatus[] -} +} & Omit 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 { - event: ChargingStationWorkerMessageEvents data: T + event: ChargingStationWorkerMessageEvents } diff --git a/src/types/ConfigurationData.ts b/src/types/ConfigurationData.ts index 216262f1..23457f5d 100644 --- a/src/types/ConfigurationData.ts +++ b/src/types/ConfigurationData.ts @@ -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 } diff --git a/src/types/ConnectorStatus.ts b/src/types/ConnectorStatus.ts index 26f54334..b9fef35e 100644 --- a/src/types/ConnectorStatus.ts +++ b/src/types/ConnectorStatus.ts @@ -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 } diff --git a/src/types/Error.ts b/src/types/Error.ts index 34bad1ad..89d20479 100644 --- a/src/types/Error.ts +++ b/src/types/Error.ts @@ -1,7 +1,7 @@ import type { JsonType } from './JsonType.js' export interface HandleErrorParams { - throwError?: boolean consoleOut?: boolean errorResponse?: T + throwError?: boolean } diff --git a/src/types/Evse.ts b/src/types/Evse.ts index 8fb09e48..dcb00ad5 100644 --- a/src/types/Evse.ts +++ b/src/types/Evse.ts @@ -6,6 +6,6 @@ export interface EvseTemplate { } export interface EvseStatus { - connectors: Map availability: AvailabilityType + connectors: Map } diff --git a/src/types/FileType.ts b/src/types/FileType.ts index 2b6bc380..8884a899 100644 --- a/src/types/FileType.ts +++ b/src/types/FileType.ts @@ -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' } diff --git a/src/types/JsonType.ts b/src/types/JsonType.ts index 17db232c..78142b77 100644 --- a/src/types/JsonType.ts +++ b/src/types/JsonType.ts @@ -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[] diff --git a/src/types/MeasurandValues.ts b/src/types/MeasurandValues.ts index 833efa82..1c13b4fb 100644 --- a/src/types/MeasurandValues.ts +++ b/src/types/MeasurandValues.ts @@ -1,6 +1,6 @@ export interface MeasurandValues { + allPhases: number L1: number L2: number L3: number - allPhases: number } diff --git a/src/types/SimulatorState.ts b/src/types/SimulatorState.ts index 0ba53078..a567316b 100644 --- a/src/types/SimulatorState.ts +++ b/src/types/SimulatorState.ts @@ -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 + version: string } diff --git a/src/types/Statistics.ts b/src/types/Statistics.ts index 9601a90c..7df512e2 100644 --- a/src/types/Statistics.ts +++ b/src/types/Statistics.ts @@ -9,34 +9,34 @@ export interface TimestampedData { } export type StatisticsData = Partial<{ - requestCount: number - responseCount: number - errorCount: number - timeMeasurementCount: number - measurementTimeSeries: CircularBuffer | TimestampedData[] + avgTimeMeasurement: number currentTimeMeasurement: number - minTimeMeasurement: number + errorCount: number maxTimeMeasurement: number - totalTimeMeasurement: number - avgTimeMeasurement: number + measurementTimeSeries: CircularBuffer | 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 updatedAt?: Date - statisticsData: Map + uri: string } export interface TemplateStatistics { + added: number configured: number + indexes: Set provisioned: number - added: number started: number - indexes: Set } diff --git a/src/types/Storage.ts b/src/types/Storage.ts index 232c9d41..1128007e 100644 --- a/src/types/Storage.ts +++ b/src/types/Storage.ts @@ -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' } diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index b5cf30ff..7a3dce2d 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -33,49 +33,49 @@ export type ProtocolRequestHandler = ( uuid?: `${string}-${string}-${string}-${string}-${string}`, procedureName?: ProcedureName, payload?: RequestPayload -) => undefined | Promise | ResponsePayload | Promise +) => Promise | Promise | 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 } diff --git a/src/types/WebSocket.ts b/src/types/WebSocket.ts index 499fe09b..753c2268 100644 --- a/src/types/WebSocket.ts +++ b/src/types/WebSocket.ts @@ -19,22 +19,22 @@ export const WebSocketCloseEventStatusString: Record { + extends Omit { hashId: string | undefined } diff --git a/src/types/ocpp/1.6/ChargePointStatus.ts b/src/types/ocpp/1.6/ChargePointStatus.ts index ef7467bd..9acc5487 100644 --- a/src/types/ocpp/1.6/ChargePointStatus.ts +++ b/src/types/ocpp/1.6/ChargePointStatus.ts @@ -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' } diff --git a/src/types/ocpp/1.6/ChargingProfile.ts b/src/types/ocpp/1.6/ChargingProfile.ts index 744696c5..ecf6bebb 100644 --- a/src/types/ocpp/1.6/ChargingProfile.ts +++ b/src/types/ocpp/1.6/ChargingProfile.ts @@ -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 { diff --git a/src/types/ocpp/1.6/Configuration.ts b/src/types/ocpp/1.6/Configuration.ts index a5888d93..d40d8d1f 100644 --- a/src/types/ocpp/1.6/Configuration.ts +++ b/src/types/ocpp/1.6/Configuration.ts @@ -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 { diff --git a/src/types/ocpp/1.6/MeterValues.ts b/src/types/ocpp/1.6/MeterValues.ts index 92cac7c3..d01cae07 100644 --- a/src/types/ocpp/1.6/MeterValues.ts +++ b/src/types/ocpp/1.6/MeterValues.ts @@ -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 diff --git a/src/types/ocpp/1.6/Requests.ts b/src/types/ocpp/1.6/Requests.ts index 014be2ed..82d90f66 100644 --- a/src/types/ocpp/1.6/Requests.ts +++ b/src/types/ocpp/1.6/Requests.ts @@ -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 { diff --git a/src/types/ocpp/1.6/Responses.ts b/src/types/ocpp/1.6/Responses.ts index 8b67a926..2447d8c9 100644 --- a/src/types/ocpp/1.6/Responses.ts +++ b/src/types/ocpp/1.6/Responses.ts @@ -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 { diff --git a/src/types/ocpp/1.6/Transaction.ts b/src/types/ocpp/1.6/Transaction.ts index f9659f69..6adb3220 100644 --- a/src/types/ocpp/1.6/Transaction.ts +++ b/src/types/ocpp/1.6/Transaction.ts @@ -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 { diff --git a/src/types/ocpp/2.0/Common.ts b/src/types/ocpp/2.0/Common.ts index 785c6046..7f5fc65d 100644 --- a/src/types/ocpp/2.0/Common.ts +++ b/src/types/ocpp/2.0/Common.ts @@ -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 } diff --git a/src/types/ocpp/2.0/Requests.ts b/src/types/ocpp/2.0/Requests.ts index 2673cbaf..7c0e3852 100644 --- a/src/types/ocpp/2.0/Requests.ts +++ b/src/types/ocpp/2.0/Requests.ts @@ -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 } diff --git a/src/types/ocpp/2.0/Responses.ts b/src/types/ocpp/2.0/Responses.ts index b0f281c7..816eea0b 100644 --- a/src/types/ocpp/2.0/Responses.ts +++ b/src/types/ocpp/2.0/Responses.ts @@ -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 } diff --git a/src/types/ocpp/2.0/Variables.ts b/src/types/ocpp/2.0/Variables.ts index 04a34456..0f35ee80 100644 --- a/src/types/ocpp/2.0/Variables.ts +++ b/src/types/ocpp/2.0/Variables.ts @@ -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 { diff --git a/src/types/ocpp/Configuration.ts b/src/types/ocpp/Configuration.ts index c4dbba8a..26790402 100644 --- a/src/types/ocpp/Configuration.ts +++ b/src/types/ocpp/Configuration.ts @@ -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 diff --git a/src/types/ocpp/ErrorType.ts b/src/types/ocpp/ErrorType.ts index d6d8357f..d405ee4d 100644 --- a/src/types/ocpp/ErrorType.ts +++ b/src/types/ocpp/ErrorType.ts @@ -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' } diff --git a/src/types/ocpp/MessageType.ts b/src/types/ocpp/MessageType.ts index 972ed474..446f25c4 100644 --- a/src/types/ocpp/MessageType.ts +++ b/src/types/ocpp/MessageType.ts @@ -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 } diff --git a/src/types/ocpp/Requests.ts b/src/types/ocpp/Requests.ts index 688cd7ff..a6171b2d 100644 --- a/src/types/ocpp/Requests.ts +++ b/src/types/ocpp/Requests.ts @@ -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 ] diff --git a/src/types/ocpp/Reservation.ts b/src/types/ocpp/Reservation.ts index 5f15838a..03d085ea 100644 --- a/src/types/ocpp/Reservation.ts +++ b/src/types/ocpp/Reservation.ts @@ -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' } diff --git a/src/types/ocpp/Responses.ts b/src/types/ocpp/Responses.ts index 43b59515..9676ff74 100644 --- a/src/types/ocpp/Responses.ts +++ b/src/types/ocpp/Responses.ts @@ -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 +) => Promise | void export type BootNotificationResponse = | OCPP16BootNotificationResponse diff --git a/src/types/orm/entities/PerformanceRecord.ts b/src/types/orm/entities/PerformanceRecord.ts index 53cc5108..f0206ab0 100644 --- a/src/types/orm/entities/PerformanceRecord.ts +++ b/src/types/orm/entities/PerformanceRecord.ts @@ -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[] @Property() updatedAt?: Date @Property() - statisticsData!: Partial[] + uri!: string } diff --git a/src/utils/AsyncLock.ts b/src/utils/AsyncLock.ts index 5f505a53..6c25adbd 100644 --- a/src/utils/AsyncLock.ts +++ b/src/utils/AsyncLock.ts @@ -9,7 +9,7 @@ export enum AsyncLockType { performance = 'performance' } -type ResolveType = (value: void | PromiseLike) => void +type ResolveType = (value: PromiseLike | void) => void export class AsyncLock { private static readonly asyncLocks = new Map() @@ -21,19 +21,6 @@ export class AsyncLock { this.resolveQueue = new Queue() } - public static async runExclusive(type: AsyncLockType, fn: () => T | Promise): Promise { - try { - await AsyncLock.acquire(type) - if (isAsyncFunction(fn)) { - return await fn() - } else { - return fn() as T - } - } finally { - await AsyncLock.release(type) - } - } - private static async acquire (type: AsyncLockType): Promise { const asyncLock = AsyncLock.getAsyncLock(type) if (!asyncLock.acquired) { @@ -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 { 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(type: AsyncLockType, fn: () => Promise | T): Promise { + try { + await AsyncLock.acquire(type) + if (isAsyncFunction(fn)) { + return await fn() + } else { + return fn() as T + } + } finally { + await AsyncLock.release(type) } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return AsyncLock.asyncLocks.get(type)! } } diff --git a/src/utils/ChargingStationConfigurationUtils.ts b/src/utils/ChargingStationConfigurationUtils.ts index c6717a74..ddeb0127 100644 --- a/src/utils/ChargingStationConfigurationUtils.ts +++ b/src/utils/ChargingStationConfigurationUtils.ts @@ -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, + } } }) } diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 2e599d66..9ca0b913 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -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 + 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 - 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( - 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(ConfigurationSection.worker) - .processType! - ) - } - - public static workerDynamicPoolInUse (): boolean { - return ( - Configuration.getConfigurationSection(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 - )[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( + 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 + )[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(ConfigurationSection.worker) + .processType === WorkerProcessType.dynamicPool + ) + } + + public static workerPoolInUse (): boolean { + return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + Configuration.getConfigurationSection(ConfigurationSection.worker) + .processType! + ) + } + + 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()], + ]) + } } diff --git a/src/utils/ConfigurationUtils.ts b/src/utils/ConfigurationUtils.ts index 22631d95..4de762bb 100644 --- a/src/utils/ConfigurationUtils.ts +++ b/src/utils/ConfigurationUtils.ts @@ -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' diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index c770c526..ad34f38f 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -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 = 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 = 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 diff --git a/src/utils/ElectricUtils.ts b/src/utils/ElectricUtils.ts index c4fdfa10..923c6849 100644 --- a/src/utils/ElectricUtils.ts +++ b/src/utils/ElectricUtils.ts @@ -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 + } } diff --git a/src/utils/ErrorUtils.ts b/src/utils/ErrorUtils.ts index 5bc2cae3..019ea70c 100644 --- a/src/utils/ErrorUtils.ts +++ b/src/utils/ErrorUtils.ts @@ -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 ): void => { params = { ...{ - throwError: false, consoleOut: false, + throwError: false, }, ...params, } @@ -112,8 +112,8 @@ export const handleIncomingRequestError = ( ): T | undefined => { params = { ...{ - throwError: true, consoleOut: false, + throwError: true, }, ...params, } diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 8e57b81f..be1e3548 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -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' diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index c6d88c49..3b0ad0bd 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -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, }) diff --git a/src/utils/MessageChannelUtils.ts b/src/utils/MessageChannelUtils.ts index 6b79957e..f1ca57c1 100644 --- a/src/utils/MessageChannelUtils.ts +++ b/src/utils/MessageChannelUtils.ts @@ -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 => { return { - event: ChargingStationWorkerMessageEvents.added, data: buildChargingStationDataPayload(chargingStation), + event: ChargingStationWorkerMessageEvents.added, } } @@ -28,8 +29,8 @@ export const buildDeletedMessage = ( chargingStation: ChargingStation ): ChargingStationWorkerMessage => { return { - event: ChargingStationWorkerMessageEvents.deleted, data: buildChargingStationDataPayload(chargingStation), + event: ChargingStationWorkerMessageEvents.deleted, } } @@ -37,8 +38,8 @@ export const buildStartedMessage = ( chargingStation: ChargingStation ): ChargingStationWorkerMessage => { return { - event: ChargingStationWorkerMessageEvents.started, data: buildChargingStationDataPayload(chargingStation), + event: ChargingStationWorkerMessageEvents.started, } } @@ -46,8 +47,8 @@ export const buildStoppedMessage = ( chargingStation: ChargingStation ): ChargingStationWorkerMessage => { return { - event: ChargingStationWorkerMessageEvents.stopped, data: buildChargingStationDataPayload(chargingStation), + event: ChargingStationWorkerMessageEvents.stopped, } } @@ -55,8 +56,8 @@ export const buildUpdatedMessage = ( chargingStation: ChargingStation ): ChargingStationWorkerMessage => { 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), diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index 6416c14e..c816cf71 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -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> | Record[] | Set> - | Map> >( object: T, - space?: string | number, + space?: number | string, mapFormat?: MapStringifyFormat ): string => { return JSON.stringify( diff --git a/src/worker/WorkerAbstract.ts b/src/worker/WorkerAbstract.ts index f6f982f3..d985209e 100644 --- a/src/worker/WorkerAbstract.ts +++ b/src/worker/WorkerAbstract.ts @@ -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 { - 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 this.workerOptions = workerOptions } + /** + * Adds a task element to the worker pool/set. + * @param elementData - + */ + public abstract addElement (elementData: D): Promise /** * Starts the worker pool/set. */ - public abstract start (): void | Promise + public abstract start (): Promise | void /** * Stops the worker pool/set. */ public abstract stop (): Promise - /** - * Adds a task element to the worker pool/set. - * @param elementData - - */ - public abstract addElement (elementData: D): Promise } diff --git a/src/worker/WorkerConstants.ts b/src/worker/WorkerConstants.ts index 5a8dc13a..07c55757 100644 --- a/src/worker/WorkerConstants.ts +++ b/src/worker/WorkerConstants.ts @@ -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, }) diff --git a/src/worker/WorkerDynamicPool.ts b/src/worker/WorkerDynamicPool.ts index f412f081..38115c0b 100644 --- a/src/worker/WorkerDynamicPool.ts +++ b/src/worker/WorkerDynamicPool.ts @@ -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 extends WorkerAbstract< @@ -27,40 +28,40 @@ export class WorkerDynamicPool exten ) } - get info (): PoolInfo { - return this.pool.info + /** @inheritDoc */ + public async addElement (elementData: D): Promise { + 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 { + 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 { - await this.pool.destroy() + get maxElementsPerWorker (): number | undefined { + return undefined } - /** @inheritDoc */ - public async addElement (elementData: D): Promise { - 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 } } diff --git a/src/worker/WorkerFactory.ts b/src/worker/WorkerFactory.ts index 73bcd56d..c2210fa1 100644 --- a/src/worker/WorkerFactory.ts +++ b/src/worker/WorkerFactory.ts @@ -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(DEFAULT_WORKER_OPTIONS, workerOptions ?? {}) switch (workerProcessType) { - case WorkerProcessType.workerSet: - return new WorkerSet(workerScript, workerOptions) - case WorkerProcessType.fixedPool: - return new WorkerFixedPool(workerScript, workerOptions) case WorkerProcessType.dynamicPool: return new WorkerDynamicPool(workerScript, workerOptions) + case WorkerProcessType.fixedPool: + return new WorkerFixedPool(workerScript, workerOptions) + case WorkerProcessType.workerSet: + return new WorkerSet(workerScript, workerOptions) default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new Error(`Worker implementation type '${workerProcessType}' not found`) diff --git a/src/worker/WorkerFixedPool.ts b/src/worker/WorkerFixedPool.ts index 29755fed..22d6fcd1 100644 --- a/src/worker/WorkerFixedPool.ts +++ b/src/worker/WorkerFixedPool.ts @@ -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 extends WorkerAbstract< @@ -26,40 +27,40 @@ export class WorkerFixedPool extends ) } - get info (): PoolInfo { - return this.pool.info + /** @inheritDoc */ + public async addElement (elementData: D): Promise { + 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 { + 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 { - await this.pool.destroy() + get maxElementsPerWorker (): number | undefined { + return undefined } - /** @inheritDoc */ - public async addElement (elementData: D): Promise { - 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 } } diff --git a/src/worker/WorkerSet.ts b/src/worker/WorkerSet.ts index 2fd63714..c1369e12 100644 --- a/src/worker/WorkerSet.ts +++ b/src/worker/WorkerSet.ts @@ -18,21 +18,22 @@ import { import { randomizeDelay, sleep } from './WorkerUtils.js' interface ResponseWrapper { - resolve: (value: R | PromiseLike) => void reject: (reason?: unknown) => void + resolve: (value: PromiseLike | R) => void workerSetElement: WorkerSetElement } export class WorkerSet extends WorkerAbstract { - public readonly emitter: EventEmitterAsyncResource | undefined - private readonly workerSet: Set private readonly promiseResponseMap: Map< `${string}-${string}-${string}-${string}`, ResponseWrapper > private started: boolean + private readonly workerSet: Set + private workerStartup: boolean + public readonly emitter: EventEmitterAsyncResource | undefined /** * Creates a new `WorkerSet`. @@ -62,89 +63,6 @@ export class WorkerSet 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 { - this.addWorkerSetElement() - // Add worker set element sequentially to optimize memory at startup - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.workerOptions.workerStartDelay! > 0 && - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!))) - this.emitter?.emit(WorkerSetEvents.started, this.info) - this.started = true - } - - /** @inheritDoc */ - public async stop (): Promise { - for (const workerSetElement of this.workerSet) { - const worker = workerSetElement.worker - const waitWorkerExit = new Promise(resolve => { - worker.once('exit', () => { - resolve() - }) - }) - worker.unref() - await worker.terminate() - await waitWorkerExit - } - this.emitter?.emit(WorkerSetEvents.stopped, this.info) - this.started = false - this.emitter?.emitDestroy() - } - - /** @inheritDoc */ - public async addElement (elementData: D): Promise { - if (!this.started) { - throw new Error('Cannot add a WorkerSet element: not started') - } - const workerSetElement = await this.getWorkerSetElement() - const sendMessageToWorker = new Promise((resolve, reject) => { - const message = { - uuid: randomUUID(), - event: WorkerMessageEvents.addWorkerElement, - data: elementData, - } satisfies WorkerMessage - 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 extends Worke }) worker.on('message', this.workerOptions.poolOptions?.messageHandler ?? EMPTY_FUNCTION) worker.on('message', (message: WorkerMessage) => { - 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 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 { - 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 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 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 { + if (!this.started) { + throw new Error('Cannot add a WorkerSet element: not started') + } + const workerSetElement = await this.getWorkerSetElement() + const sendMessageToWorker = new Promise((resolve, reject) => { + const message = { + data: elementData, + event: WorkerMessageEvents.addWorkerElement, + uuid: randomUUID(), + } satisfies WorkerMessage + workerSetElement.worker.postMessage(message) + this.promiseResponseMap.set(message.uuid, { + reject, + resolve, + workerSetElement, + }) + }) + const response = await sendMessageToWorker + // Add element sequentially to optimize memory at startup + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (this.workerOptions.elementAddDelay! > 0) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)) + } + return response + } + + /** @inheritDoc */ + public async start (): Promise { + this.addWorkerSetElement() + // Add worker set element sequentially to optimize memory at startup + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.workerOptions.workerStartDelay! > 0 && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!))) + this.emitter?.emit(WorkerSetEvents.started, this.info) + this.started = true + } + + /** @inheritDoc */ + public async stop (): Promise { + for (const workerSetElement of this.workerSet) { + const worker = workerSetElement.worker + const waitWorkerExit = new Promise(resolve => { + worker.once('exit', () => { + resolve() + }) + }) + worker.unref() + await worker.terminate() + await waitWorkerExit + } + this.emitter?.emit(WorkerSetEvents.stopped, this.info) + this.started = false + this.emitter?.emitDestroy() + } + + get info (): SetInfo { + return { + elementsExecuting: [...this.workerSet].reduce( + (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements, + 0 + ), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + elementsPerWorker: this.maxElementsPerWorker!, + size: this.size, + started: this.started, + type: 'set', + version: workerSetVersion, + worker: 'thread', + } + } + + get maxElementsPerWorker (): number | undefined { + return this.workerOptions.elementsPerWorker + } + + get size (): number { + return this.workerSet.size + } } diff --git a/src/worker/WorkerTypes.ts b/src/worker/WorkerTypes.ts index 6ca9140b..8794420a 100644 --- a/src/worker/WorkerTypes.ts +++ b/src/worker/WorkerTypes.ts @@ -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 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 { - 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' } diff --git a/src/worker/WorkerUtils.ts b/src/worker/WorkerUtils.ts index 8fad34a2..958ecccc 100644 --- a/src/worker/WorkerUtils.ts +++ b/src/worker/WorkerUtils.ts @@ -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 => { return await new Promise(resolve => diff --git a/tests/charging-station/Helpers.test.ts b/tests/charging-station/Helpers.test.ts index 798763ba..9d1df21b 100644 --- a/tests/charging-station/Helpers.test.ts +++ b/tests/charging-station/Helpers.test.ts @@ -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(), connectors: new Map(), + evses: new Map(), + logPrefix: () => `${baseName} |`, + started: false, } as ChargingStation await it('Verify getChargingStationId()', () => { diff --git a/tests/exception/BaseError.test.ts b/tests/exception/BaseError.test.ts index 947bac2b..e5bdc769 100644 --- a/tests/exception/BaseError.test.ts +++ b/tests/exception/BaseError.test.ts @@ -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' diff --git a/tests/exception/OCPPError.test.ts b/tests/exception/OCPPError.test.ts index c6e4bc43..44d458a6 100644 --- a/tests/exception/OCPPError.test.ts +++ b/tests/exception/OCPPError.test.ts @@ -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' diff --git a/tests/types/ConfigurationData.test.ts b/tests/types/ConfigurationData.test.ts index f564a702..2af0d14b 100644 --- a/tests/types/ConfigurationData.test.ts +++ b/tests/types/ConfigurationData.test.ts @@ -1,6 +1,5 @@ -import { describe, it } from 'node:test' - import { expect } from 'expect' +import { describe, it } from 'node:test' import { ApplicationProtocolVersion, diff --git a/tests/utils/AsyncLock.test.ts b/tests/utils/AsyncLock.test.ts index f0eda007..3ab1dcf5 100644 --- a/tests/utils/AsyncLock.test.ts +++ b/tests/utils/AsyncLock.test.ts @@ -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' diff --git a/tests/utils/ConfigurationUtils.test.ts b/tests/utils/ConfigurationUtils.test.ts index b0689b88..23f6c5ac 100644 --- a/tests/utils/ConfigurationUtils.test.ts +++ b/tests/utils/ConfigurationUtils.test.ts @@ -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' diff --git a/tests/utils/ElectricUtils.test.ts b/tests/utils/ElectricUtils.test.ts index 3b025b2e..c408a7f4 100644 --- a/tests/utils/ElectricUtils.test.ts +++ b/tests/utils/ElectricUtils.test.ts @@ -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' diff --git a/tests/utils/ErrorUtils.test.ts b/tests/utils/ErrorUtils.test.ts index 1163211f..b05eec74 100644 --- a/tests/utils/ErrorUtils.test.ts +++ b/tests/utils/ErrorUtils.test.ts @@ -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) diff --git a/tests/utils/StatisticUtils.test.ts b/tests/utils/StatisticUtils.test.ts index c0bf1b6c..8e1f00a5 100644 --- a/tests/utils/StatisticUtils.test.ts +++ b/tests/utils/StatisticUtils.test.ts @@ -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' diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index 3fe46842..4f9e9102 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -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 {} + public testArrowAsync = async (): Promise => {} // eslint-disable-next-line @typescript-eslint/no-empty-function public testArrowSync = (): void => {} // eslint-disable-next-line @typescript-eslint/no-empty-function - public testArrowAsync = async (): Promise => {} + public static async testStaticAsync (): Promise {} // eslint-disable-next-line @typescript-eslint/no-empty-function public static testStaticSync (): void {} // eslint-disable-next-line @typescript-eslint/no-empty-function - public static async testStaticAsync (): Promise {} + public async testAsync (): Promise {} + // eslint-disable-next-line @typescript-eslint/no-empty-function + public testSync (): void {} } const testClass = new TestClass() // eslint-disable-next-line @typescript-eslint/unbound-method diff --git a/ui/web/package.json b/ui/web/package.json index 691c0141..f2abb270 100644 --- a/ui/web/package.json +++ b/ui/web/package.json @@ -36,7 +36,7 @@ "devDependencies": { "@tsconfig/node22": "^22.0.0", "@types/jsdom": "^21.1.7", - "@types/node": "^22.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", diff --git a/ui/web/src/components/actions/AddChargingStations.vue b/ui/web/src/components/actions/AddChargingStations.vue index 44ffcef3..52e4cd81 100644 --- a/ui/web/src/components/actions/AddChargingStations.vue +++ b/ui/web/src/components/actions/AddChargingStations.vue @@ -25,10 +25,10 @@

Template options overrides:

    @@ -37,45 +37,45 @@
  • Auto start:
  • Persistent configuration:
  • OCPP strict compliance:
  • Performance statistics:
@@ -110,29 +110,28 @@