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'
'jsdoc/check-tag-names': [
'warn',
{
- typed: true,
definedTags: ['defaultValue', 'experimental', 'typeParam'],
+ typed: true,
},
],
},
},
...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,
...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({
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,
})
"@commitlint/config-conventional": "^19.2.2",
"@eslint/js": "^9.9.0",
"@mikro-orm/cli": "^6.3.6",
- "@types/node": "^22.4.1",
+ "@types/node": "^22.5.0",
"@types/semver": "^7.5.8",
"@types/ws": "^8.5.12",
"c8": "^10.1.2",
"eslint": "^9.9.0",
"eslint-define-config": "^2.1.0",
"eslint-plugin-jsdoc": "^50.2.2",
- "eslint-plugin-simple-import-sort": "^12.1.1",
+ "eslint-plugin-perfectionist": "^3.2.0",
"eslint-plugin-vue": "^9.27.0",
"expect": "^29.7.0",
"glob": "^11.0.0",
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
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
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))
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
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
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:
'@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==}
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==}
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}
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}
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:
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==}
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:
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==}
'@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
'@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
'@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
'@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
'@types/conventional-commits-parser@5.0.0':
dependencies:
- '@types/node': 22.4.1
+ '@types/node': 22.5.0
'@types/eslint@9.6.0':
dependencies:
'@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
dependencies:
undici-types: 6.19.8
- '@types/node@22.4.1':
+ '@types/node@22.5.0':
dependencies:
undici-types: 6.19.8
'@types/ws@8.5.12':
dependencies:
- '@types/node': 22.4.1
+ '@types/node': 22.5.0
'@types/yargs-parser@21.0.3': {}
'@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
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
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)
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
ee-first@1.1.1: {}
- electron-to-chromium@1.5.12: {}
+ electron-to-chromium@1.5.13: {}
elliptic@6.5.7:
dependencies:
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)
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))
dependencies:
ci-info: 2.0.0
- is-core-module@2.15.0:
+ is-core-module@2.15.1:
dependencies:
hasown: 2.0.2
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
napi-build-utils@1.0.2: {}
+ natural-compare-lite@1.4.0: {}
+
natural-compare@1.4.0: {}
ndarray-blas-level1@1.1.3: {}
dependencies:
lower-case: 1.1.4
- node-abi@3.65.0:
+ node-abi@3.67.0:
dependencies:
semver: 7.6.3
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
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
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: {}
'@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
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
- 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
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
+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'
/* 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',
'winston-daily-rotate-file',
'ws',
],
- treeShaking: true,
+ format: 'esm',
minify: true,
- sourcemap,
- entryNames: '[name]',
outdir: './dist',
+ platform: 'node',
plugins: [
clean({
patterns: [
],
}),
],
+ sourcemap,
+ treeShaking: true,
})
console.timeEnd('Build time')
export const runtimes = {
+ browser: 'browser',
bun: 'bun',
deno: 'deno',
node: 'node',
workerd: 'workerd',
- browser: 'browser',
}
const isBun = !!globalThis.Bun || !!globalThis.process?.versions?.bun
// 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'
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'
AutomaticTransactionGenerator
>()
- public readonly connectorsStatus: Map<number, Status>
- public started: boolean
+ private readonly chargingStation: ChargingStation
+ private readonly logPrefix = (connectorId?: number): string => {
+ return logPrefix(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
+ connectorId != null ? ` on connector #${connectorId.toString()}` : ''
+ }:`
+ )
+ }
+
private starting: boolean
private stopping: boolean
- private readonly chargingStation: ChargingStation
+ public readonly connectorsStatus: Map<number, Status>
+
+ public started: boolean
private constructor (chargingStation: ChargingStation) {
this.started = false
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 {
return AutomaticTransactionGenerator.instances.get(chargingStation.stationInfo!.hashId)
}
- public static deleteInstance (chargingStation: ChargingStation): boolean {
+ private canStartConnector (connectorId: number): boolean {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return AutomaticTransactionGenerator.instances.delete(chargingStation.stationInfo!.hashId)
- }
-
- public start (stopAbsoluteDuration?: boolean): void {
- if (!checkChargingStationState(this.chargingStation, this.logPrefix())) {
- return
+ if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
+ logger.info(
+ `${this.logPrefix(
+ connectorId
+ )} entered in transaction loop while the ATG stop date has been reached`
+ )
+ return false
}
- if (this.started) {
- logger.warn(`${this.logPrefix()} is already started`)
- return
+ if (!this.chargingStation.inAcceptedState()) {
+ logger.error(
+ `${this.logPrefix(
+ connectorId
+ )} entered in transaction loop while the charging station is not in accepted state`
+ )
+ return false
}
- if (this.starting) {
- logger.warn(`${this.logPrefix()} is already starting`)
- return
+ if (!this.chargingStation.isChargingStationAvailable()) {
+ logger.info(
+ `${this.logPrefix(
+ connectorId
+ )} entered in transaction loop while the charging station is unavailable`
+ )
+ return false
}
- this.starting = true
- this.startConnectors(stopAbsoluteDuration)
- this.started = true
- this.starting = false
- }
-
- public stop (): void {
- if (!this.started) {
- logger.warn(`${this.logPrefix()} is already stopped`)
- return
+ if (!this.chargingStation.isConnectorAvailable(connectorId)) {
+ logger.info(
+ `${this.logPrefix(
+ connectorId
+ )} entered in transaction loop while the connector ${connectorId.toString()} is unavailable`
+ )
+ return false
}
- if (this.stopping) {
- logger.warn(`${this.logPrefix()} is already stopping`)
- return
+ const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus?.transactionStarted === true) {
+ logger.info(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${connectorStatus.transactionId?.toString()} is already started on connector ${connectorId.toString()}`
+ )
+ return false
}
- this.stopping = true
- this.stopConnectors()
- this.started = false
- this.stopping = false
+ return true
}
- public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void {
- if (!checkChargingStationState(this.chargingStation, this.logPrefix(connectorId))) {
- return
+ private getConnectorStatus (connectorId: number): Status {
+ const statusIndex = connectorId - 1
+ if (statusIndex < 0) {
+ logger.error(`${this.logPrefix(connectorId)} invalid connector id`)
+ throw new BaseError(`Invalid connector id ${connectorId.toString()}`)
}
- if (!this.connectorsStatus.has(connectorId)) {
- logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
- throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
+ let connectorStatus: Status | undefined
+ if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
+ connectorStatus = clone<Status>(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
+ )
+ } else {
+ logger.warn(
+ `${this.logPrefix(
+ connectorId
+ )} no status found for connector #${connectorId.toString()} in charging station configuration file. New status will be created`
+ )
}
- if (this.connectorsStatus.get(connectorId)?.start === false) {
- this.internalStartConnector(connectorId, stopAbsoluteDuration).catch(Constants.EMPTY_FUNCTION)
- } else if (this.connectorsStatus.get(connectorId)?.start === true) {
- logger.warn(`${this.logPrefix(connectorId)} is already started on connector`)
+ if (connectorStatus != null) {
+ connectorStatus.startDate = convertToDate(connectorStatus.startDate)
+ connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
+ connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
+ connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
+ if (
+ !this.started &&
+ (connectorStatus.start ||
+ this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
+ ) {
+ connectorStatus.start = false
+ }
}
+ return (
+ connectorStatus ?? {
+ acceptedAuthorizeRequests: 0,
+ acceptedStartTransactionRequests: 0,
+ acceptedStopTransactionRequests: 0,
+ authorizeRequests: 0,
+ rejectedAuthorizeRequests: 0,
+ rejectedStartTransactionRequests: 0,
+ rejectedStopTransactionRequests: 0,
+ skippedConsecutiveTransactions: 0,
+ skippedTransactions: 0,
+ start: false,
+ startTransactionRequests: 0,
+ stopTransactionRequests: 0,
+ }
+ )
}
- public stopConnector (connectorId: number): void {
- if (!this.connectorsStatus.has(connectorId)) {
- logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
- throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
- }
- if (this.connectorsStatus.get(connectorId)?.start === true) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.connectorsStatus.get(connectorId)!.start = false
- } else if (this.connectorsStatus.get(connectorId)?.start === false) {
- logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
- }
+ private getRequireAuthorize (): boolean {
+ return (
+ this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
+ )
}
- private startConnectors (stopAbsoluteDuration?: boolean): void {
- if (
- this.connectorsStatus.size > 0 &&
- this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
- ) {
- this.connectorsStatus.clear()
- this.initializeConnectorsStatus()
- }
- if (this.chargingStation.hasEvses) {
- for (const [evseId, evseStatus] of this.chargingStation.evses) {
- if (evseId > 0) {
- for (const connectorId of evseStatus.connectors.keys()) {
- this.startConnector(connectorId, stopAbsoluteDuration)
- }
- }
- }
+ private handleStartTransactionResponse (
+ connectorId: number,
+ startResponse: StartTransactionResponse
+ ): void {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
+ if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
} else {
- for (const connectorId of this.chargingStation.connectors.keys()) {
- if (connectorId > 0) {
- this.startConnector(connectorId, stopAbsoluteDuration)
- }
- }
+ logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests
}
}
- private stopConnectors (): void {
+ private initializeConnectorsStatus (): void {
if (this.chargingStation.hasEvses) {
for (const [evseId, evseStatus] of this.chargingStation.evses) {
if (evseId > 0) {
for (const connectorId of evseStatus.connectors.keys()) {
- this.stopConnector(connectorId)
+ this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
}
}
}
} else {
for (const connectorId of this.chargingStation.connectors.keys()) {
if (connectorId > 0) {
- this.stopConnector(connectorId)
+ this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
}
}
}
this.chargingStation.emit(ChargingStationEvents.updated)
}
- private canStartConnector (connectorId: number): boolean {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (new Date() > this.connectorsStatus.get(connectorId)!.stopDate!) {
- logger.info(
- `${this.logPrefix(
- connectorId
- )} entered in transaction loop while the ATG stop date has been reached`
- )
- return false
- }
- if (!this.chargingStation.inAcceptedState()) {
- logger.error(
- `${this.logPrefix(
- connectorId
- )} entered in transaction loop while the charging station is not in accepted state`
- )
- return false
- }
- if (!this.chargingStation.isChargingStationAvailable()) {
- logger.info(
- `${this.logPrefix(
- connectorId
- )} entered in transaction loop while the charging station is unavailable`
- )
- return false
- }
- if (!this.chargingStation.isConnectorAvailable(connectorId)) {
- logger.info(
- `${this.logPrefix(
- connectorId
- )} entered in transaction loop while the connector ${connectorId.toString()} is unavailable`
- )
- return false
- }
- const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
- if (connectorStatus?.transactionStarted === true) {
- logger.info(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix(connectorId)} entered in transaction loop while a transaction ${connectorStatus.transactionId?.toString()} is already started on connector ${connectorId.toString()}`
- )
- return false
- }
- return true
- }
-
- private async waitChargingStationAvailable (connectorId: number): Promise<void> {
- let logged = false
- while (!this.chargingStation.isChargingStationAvailable()) {
- if (!logged) {
- logger.info(
- `${this.logPrefix(
- connectorId
- )} transaction loop waiting for charging station to be available`
- )
- logged = true
- }
- await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
- }
- }
-
- private async waitConnectorAvailable (connectorId: number): Promise<void> {
- let logged = false
- while (!this.chargingStation.isConnectorAvailable(connectorId)) {
- if (!logged) {
- logger.info(
- `${this.logPrefix(
- connectorId
- )} transaction loop waiting for connector ${connectorId.toString()} to be available`
- )
- logged = true
- }
- await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
- }
- }
-
- private async waitRunningTransactionStopped (connectorId: number): Promise<void> {
- const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
- let logged = false
- while (connectorStatus?.transactionStarted === true) {
- if (!logged) {
- logger.info(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${connectorStatus.transactionId?.toString()} on connector ${connectorId.toString()} to be stopped`
- )
- logged = true
- }
- await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+ private startConnectors (stopAbsoluteDuration?: boolean): void {
+ if (
+ this.connectorsStatus.size > 0 &&
+ this.connectorsStatus.size !== this.chargingStation.getNumberOfConnectors()
+ ) {
+ this.connectorsStatus.clear()
+ this.initializeConnectorsStatus()
}
- }
-
- private initializeConnectorsStatus (): void {
if (this.chargingStation.hasEvses) {
for (const [evseId, evseStatus] of this.chargingStation.evses) {
if (evseId > 0) {
for (const connectorId of evseStatus.connectors.keys()) {
- this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
+ this.startConnector(connectorId, stopAbsoluteDuration)
}
}
}
} else {
for (const connectorId of this.chargingStation.connectors.keys()) {
if (connectorId > 0) {
- this.connectorsStatus.set(connectorId, this.getConnectorStatus(connectorId))
+ this.startConnector(connectorId, stopAbsoluteDuration)
}
}
}
}
- private getConnectorStatus (connectorId: number): Status {
- const statusIndex = connectorId - 1
- if (statusIndex < 0) {
- logger.error(`${this.logPrefix(connectorId)} invalid connector id`)
- throw new BaseError(`Invalid connector id ${connectorId.toString()}`)
- }
- let connectorStatus: Status | undefined
- if (this.chargingStation.getAutomaticTransactionGeneratorStatuses()?.[statusIndex] != null) {
- connectorStatus = clone<Status>(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.chargingStation.getAutomaticTransactionGeneratorStatuses()![statusIndex]
- )
- } else {
- logger.warn(
- `${this.logPrefix(
- connectorId
- )} no status found for connector #${connectorId.toString()} in charging station configuration file. New status will be created`
- )
- }
- if (connectorStatus != null) {
- connectorStatus.startDate = convertToDate(connectorStatus.startDate)
- connectorStatus.lastRunDate = convertToDate(connectorStatus.lastRunDate)
- connectorStatus.stopDate = convertToDate(connectorStatus.stopDate)
- connectorStatus.stoppedDate = convertToDate(connectorStatus.stoppedDate)
- if (
- !this.started &&
- (connectorStatus.start ||
- this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.enable !== true)
- ) {
- connectorStatus.start = false
- }
- }
- return (
- connectorStatus ?? {
- start: false,
- authorizeRequests: 0,
- acceptedAuthorizeRequests: 0,
- rejectedAuthorizeRequests: 0,
- startTransactionRequests: 0,
- acceptedStartTransactionRequests: 0,
- rejectedStartTransactionRequests: 0,
- stopTransactionRequests: 0,
- acceptedStopTransactionRequests: 0,
- rejectedStopTransactionRequests: 0,
- skippedConsecutiveTransactions: 0,
- skippedTransactions: 0,
- }
- )
- }
-
private async startTransaction (
connectorId: number
): Promise<StartTransactionResponse | undefined> {
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
return stopResponse
}
- private getRequireAuthorize (): boolean {
- return (
- this.chargingStation.getAutomaticTransactionGeneratorConfiguration()?.requireAuthorize ?? true
- )
+ private async waitChargingStationAvailable (connectorId: number): Promise<void> {
+ let logged = false
+ while (!this.chargingStation.isChargingStationAvailable()) {
+ if (!logged) {
+ logger.info(
+ `${this.logPrefix(
+ connectorId
+ )} transaction loop waiting for charging station to be available`
+ )
+ logged = true
+ }
+ await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+ }
}
- private readonly logPrefix = (connectorId?: number): string => {
- return logPrefix(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- ` ${this.chargingStation.stationInfo?.chargingStationId} | ATG${
- connectorId != null ? ` on connector #${connectorId.toString()}` : ''
- }:`
- )
+ private async waitConnectorAvailable (connectorId: number): Promise<void> {
+ let logged = false
+ while (!this.chargingStation.isConnectorAvailable(connectorId)) {
+ if (!logged) {
+ logger.info(
+ `${this.logPrefix(
+ connectorId
+ )} transaction loop waiting for connector ${connectorId.toString()} to be available`
+ )
+ logged = true
+ }
+ await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+ }
}
- private handleStartTransactionResponse (
- connectorId: number,
- startResponse: StartTransactionResponse
- ): void {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.connectorsStatus.get(connectorId)!.startTransactionRequests
- if (startResponse.idTagInfo.status === AuthorizationStatus.ACCEPTED) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.connectorsStatus.get(connectorId)!.acceptedStartTransactionRequests
- } else {
- logger.warn(`${this.logPrefix(connectorId)} start transaction rejected`)
+ private async waitRunningTransactionStopped (connectorId: number): Promise<void> {
+ const connectorStatus = this.chargingStation.getConnectorStatus(connectorId)
+ let logged = false
+ while (connectorStatus?.transactionStarted === true) {
+ if (!logged) {
+ logger.info(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix(connectorId)} transaction loop waiting for started transaction ${connectorStatus.transactionId?.toString()} on connector ${connectorId.toString()} to be stopped`
+ )
+ logged = true
+ }
+ await sleep(Constants.DEFAULT_ATG_WAIT_TIME)
+ }
+ }
+
+ public start (stopAbsoluteDuration?: boolean): void {
+ if (!checkChargingStationState(this.chargingStation, this.logPrefix())) {
+ return
+ }
+ if (this.started) {
+ logger.warn(`${this.logPrefix()} is already started`)
+ return
+ }
+ if (this.starting) {
+ logger.warn(`${this.logPrefix()} is already starting`)
+ return
+ }
+ this.starting = true
+ this.startConnectors(stopAbsoluteDuration)
+ this.started = true
+ this.starting = false
+ }
+
+ public startConnector (connectorId: number, stopAbsoluteDuration?: boolean): void {
+ if (!checkChargingStationState(this.chargingStation, this.logPrefix(connectorId))) {
+ return
+ }
+ if (!this.connectorsStatus.has(connectorId)) {
+ logger.error(`${this.logPrefix(connectorId)} starting on non existing connector`)
+ throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
+ }
+ if (this.connectorsStatus.get(connectorId)?.start === false) {
+ this.internalStartConnector(connectorId, stopAbsoluteDuration).catch(Constants.EMPTY_FUNCTION)
+ } else if (this.connectorsStatus.get(connectorId)?.start === true) {
+ logger.warn(`${this.logPrefix(connectorId)} is already started on connector`)
+ }
+ }
+
+ public stop (): void {
+ if (!this.started) {
+ logger.warn(`${this.logPrefix()} is already stopped`)
+ return
+ }
+ if (this.stopping) {
+ logger.warn(`${this.logPrefix()} is already stopping`)
+ return
+ }
+ this.stopping = true
+ this.stopConnectors()
+ this.started = false
+ this.stopping = false
+ }
+
+ public stopConnector (connectorId: number): void {
+ if (!this.connectorsStatus.has(connectorId)) {
+ logger.error(`${this.logPrefix(connectorId)} stopping on non existing connector`)
+ throw new BaseError(`Connector ${connectorId.toString()} does not exist`)
+ }
+ if (this.connectorsStatus.get(connectorId)?.start === true) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.connectorsStatus.get(connectorId)!.rejectedStartTransactionRequests
+ this.connectorsStatus.get(connectorId)!.start = false
+ } else if (this.connectorsStatus.get(connectorId)?.start === false) {
+ logger.warn(`${this.logPrefix(connectorId)} is already stopped on connector`)
}
}
}
// 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'
} from '../utils/index.js'
import { DEFAULT_ELEMENTS_PER_WORKER, type WorkerAbstract, WorkerFactory } from '../worker/index.js'
import { buildTemplateName, waitChargingStationEvents } from './Helpers.js'
-import type { AbstractUIServer } from './ui-server/AbstractUIServer.js'
import { UIServerFactory } from './ui-server/UIServerFactory.js'
const moduleName = 'Bootstrap'
enum exitCodes {
- succeeded = 0,
- missingChargingStationsConfiguration = 1,
duplicateChargingStationTemplateUrls = 2,
+ gracefulShutdownError = 4,
+ missingChargingStationsConfiguration = 1,
noChargingStationTemplates = 3,
- gracefulShutdownError = 4
+ succeeded = 0
}
export class Bootstrap extends EventEmitter {
private static instance: Bootstrap | null = null
- private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
- private readonly uiServer: AbstractUIServer
- private storage?: Storage
- private readonly templateStatistics: Map<string, TemplateStatistics>
- private readonly version: string = version
+ private readonly logPrefix = (): string => {
+ return logPrefix(' Bootstrap |')
+ }
+
private started: boolean
private starting: boolean
private stopping: boolean
+ private storage?: Storage
+ private readonly templateStatistics: Map<string, TemplateStatistics>
+ private readonly uiServer: AbstractUIServer
private uiServerStarted: boolean
+ private readonly version: string = version
+
+ private readonly workerEventAdded = (data: ChargingStationData): void => {
+ this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
+ logger.info(
+ `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ data.stationInfo.chargingStationId
+ } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
+ )
+ }
+
+ private readonly workerEventDeleted = (data: ChargingStationData): void => {
+ this.uiServer.chargingStations.delete(data.stationInfo.hashId)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)!
+ --templateStatistics.added
+ templateStatistics.indexes.delete(data.stationInfo.templateIndex)
+ logger.info(
+ `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ data.stationInfo.chargingStationId
+ } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
+ )
+ }
+
+ private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
+ // eslint-disable-next-line @typescript-eslint/unbound-method
+ if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
+ ;(
+ this.storage.storePerformanceStatistics as (
+ performanceStatistics: Statistics
+ ) => Promise<void>
+ )(data).catch(Constants.EMPTY_FUNCTION)
+ } else {
+ ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
+ data
+ )
+ }
+ }
+
+ private readonly workerEventStarted = (data: ChargingStationData): void => {
+ this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.templateStatistics.get(data.stationInfo.templateName)!.started
+ logger.info(
+ `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ data.stationInfo.chargingStationId
+ } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
+ )
+ }
+
+ private readonly workerEventStopped = (data: ChargingStationData): void => {
+ this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ --this.templateStatistics.get(data.stationInfo.templateName)!.started
+ logger.info(
+ `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ data.stationInfo.chargingStationId
+ } (hashId: ${data.stationInfo.hashId}) stopped (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
+ )
+ }
+
+ private readonly workerEventUpdated = (data: ChargingStationData): void => {
+ this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
+ }
+
+ private workerImplementation?: WorkerAbstract<ChargingStationWorkerData, ChargingStationInfo>
private constructor () {
super()
return Bootstrap.instance
}
- public get numberOfChargingStationTemplates (): number {
- return this.templateStatistics.size
- }
-
- public get numberOfConfiguredChargingStations (): number {
- return [...this.templateStatistics.values()].reduce(
- (accumulator, value) => accumulator + value.configured,
- 0
- )
- }
-
- public get numberOfProvisionedChargingStations (): number {
- return [...this.templateStatistics.values()].reduce(
- (accumulator, value) => accumulator + value.provisioned,
- 0
- )
- }
-
- public getState (): SimulatorState {
- return {
- version: this.version,
- configuration: Configuration.getConfigurationData(),
- started: this.started,
- templateStatistics: this.templateStatistics,
- }
+ private gracefulShutdown (): void {
+ this.stop()
+ .then(() => {
+ console.info(chalk.green('Graceful shutdown'))
+ this.uiServer.stop()
+ this.uiServerStarted = false
+ this.waitChargingStationsStopped()
+ // eslint-disable-next-line promise/no-nesting
+ .then(() => {
+ return exit(exitCodes.succeeded)
+ })
+ // eslint-disable-next-line promise/no-nesting
+ .catch(() => {
+ exit(exitCodes.gracefulShutdownError)
+ })
+ return undefined
+ })
+ .catch((error: unknown) => {
+ console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
+ exit(exitCodes.gracefulShutdownError)
+ })
}
- public getLastIndex (templateName: string): number {
+ private initializeCounters (): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const indexes = [...this.templateStatistics.get(templateName)!.indexes]
- .concat(0)
- .sort((a, b) => a - b)
- for (let i = 0; i < indexes.length - 1; i++) {
- if (indexes[i + 1] - indexes[i] !== 1) {
- return indexes[i]
+ const stationTemplateUrls = Configuration.getStationTemplateUrls()!
+ if (isNotEmptyArray(stationTemplateUrls)) {
+ for (const stationTemplateUrl of stationTemplateUrls) {
+ const templateName = buildTemplateName(stationTemplateUrl.file)
+ this.templateStatistics.set(templateName, {
+ added: 0,
+ configured: stationTemplateUrl.numberOfStations,
+ indexes: new Set<number>(),
+ provisioned: stationTemplateUrl.provisionedNumberOfStations ?? 0,
+ started: 0,
+ })
+ this.uiServer.chargingStationTemplates.add(templateName)
+ }
+ if (this.templateStatistics.size !== stationTemplateUrls.length) {
+ console.error(
+ chalk.red(
+ "'stationTemplateUrls' contains duplicate entries, please check your configuration"
+ )
+ )
+ exit(exitCodes.duplicateChargingStationTemplateUrls)
}
+ } else {
+ console.error(
+ chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
+ )
+ exit(exitCodes.missingChargingStationsConfiguration)
+ }
+ if (
+ this.numberOfConfiguredChargingStations === 0 &&
+ Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
+ .enabled !== true
+ ) {
+ console.error(
+ chalk.red(
+ "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
+ )
+ )
+ exit(exitCodes.noChargingStationTemplates)
}
- return indexes[indexes.length - 1]
- }
-
- public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
- return this.storage?.getPerformanceStatistics()
- }
-
- private get numberOfAddedChargingStations (): number {
- return [...this.templateStatistics.values()].reduce(
- (accumulator, value) => accumulator + value.added,
- 0
- )
}
- private get numberOfStartedChargingStations (): number {
- return [...this.templateStatistics.values()].reduce(
- (accumulator, value) => accumulator + value.started,
- 0
+ private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
+ if (!isMainThread) {
+ return
+ }
+ let elementsPerWorker: number
+ switch (workerConfiguration.elementsPerWorker) {
+ case 'all':
+ elementsPerWorker =
+ this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations
+ break
+ case 'auto':
+ elementsPerWorker =
+ this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations >
+ availableParallelism()
+ ? Math.round(
+ (this.numberOfConfiguredChargingStations +
+ this.numberOfProvisionedChargingStations) /
+ (availableParallelism() * 1.5)
+ )
+ : 1
+ break
+ default:
+ elementsPerWorker = workerConfiguration.elementsPerWorker ?? DEFAULT_ELEMENTS_PER_WORKER
+ }
+ this.workerImplementation = WorkerFactory.getWorkerImplementation<
+ ChargingStationWorkerData,
+ ChargingStationInfo
+ >(
+ join(
+ dirname(fileURLToPath(import.meta.url)),
+ `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
+ ),
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ workerConfiguration.processType!,
+ {
+ elementAddDelay: workerConfiguration.elementAddDelay,
+ elementsPerWorker,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ poolMaxSize: workerConfiguration.poolMaxSize!,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ poolMinSize: workerConfiguration.poolMinSize!,
+ poolOptions: {
+ messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
+ ...(workerConfiguration.resourceLimits != null && {
+ workerOptions: {
+ resourceLimits: workerConfiguration.resourceLimits,
+ },
+ }),
+ },
+ workerStartDelay: workerConfiguration.startDelay,
+ }
)
}
- public async start (): Promise<void> {
- if (!this.started) {
- if (!this.starting) {
- this.starting = true
- this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
+ private messageHandler (
+ msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
+ ): void {
+ // logger.debug(
+ // `${this.logPrefix()} ${moduleName}.messageHandler: Charging station worker message received: ${JSON.stringify(
+ // msg,
+ // undefined,
+ // 2
+ // )}`
+ // )
+ // Skip worker message events processing
+ // eslint-disable-next-line @typescript-eslint/dot-notation
+ if (msg['uuid'] != null) {
+ return
+ }
+ const { data, event } = msg
+ try {
+ switch (event) {
+ case ChargingStationWorkerMessageEvents.added:
+ this.emit(ChargingStationWorkerMessageEvents.added, data)
+ break
+ case ChargingStationWorkerMessageEvents.deleted:
+ this.emit(ChargingStationWorkerMessageEvents.deleted, data)
+ break
+ case ChargingStationWorkerMessageEvents.performanceStatistics:
+ this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, data)
+ break
+ case ChargingStationWorkerMessageEvents.started:
+ this.emit(ChargingStationWorkerMessageEvents.started, data)
+ break
+ case ChargingStationWorkerMessageEvents.stopped:
+ this.emit(ChargingStationWorkerMessageEvents.stopped, data)
+ break
+ case ChargingStationWorkerMessageEvents.updated:
+ this.emit(ChargingStationWorkerMessageEvents.updated, data)
+ break
+ default:
+ throw new BaseError(
+ `Unknown charging station worker message event: '${event}' received with data: ${JSON.stringify(
+ data,
+ undefined,
+ 2
+ )}`
+ )
+ }
+ } catch (error) {
+ logger.error(
+ `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling charging station worker message event '${event}':`,
+ error
+ )
+ }
+ }
+
+ private async restart (): Promise<void> {
+ await this.stop()
+ if (
+ this.uiServerStarted &&
+ Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
+ .enabled !== true
+ ) {
+ this.uiServer.stop()
+ this.uiServerStarted = false
+ }
+ this.initializeCounters()
+ // FIXME: initialize worker implementation only if the worker section has changed
+ this.initializeWorkerImplementation(
+ Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
+ )
+ await this.start()
+ }
+
+ private async waitChargingStationsStopped (): Promise<string> {
+ return await new Promise<string>((resolve, reject: (reason?: unknown) => void) => {
+ const waitTimeout = setTimeout(() => {
+ const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
+ Constants.STOP_CHARGING_STATIONS_TIMEOUT
+ )} reached at stopping charging stations`
+ console.warn(chalk.yellow(timeoutMessage))
+ reject(new Error(timeoutMessage))
+ }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
+ waitChargingStationEvents(
+ this,
+ ChargingStationWorkerMessageEvents.stopped,
+ this.numberOfStartedChargingStations
+ )
+ .then(events => {
+ resolve('Charging stations stopped')
+ return events
+ })
+ .finally(() => {
+ clearTimeout(waitTimeout)
+ })
+ .catch(reject)
+ })
+ }
+
+ public async addChargingStation (
+ index: number,
+ templateFile: string,
+ options?: ChargingStationOptions
+ ): Promise<ChargingStationInfo | undefined> {
+ if (!this.started && !this.starting) {
+ throw new BaseError(
+ 'Cannot add charging station while the charging stations simulator is not started'
+ )
+ }
+ const stationInfo = await this.workerImplementation?.addElement({
+ index,
+ options,
+ templateFile: join(
+ dirname(fileURLToPath(import.meta.url)),
+ 'assets',
+ 'station-templates',
+ templateFile
+ ),
+ })
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))!
+ ++templateStatistics.added
+ templateStatistics.indexes.add(index)
+ return stationInfo
+ }
+
+ public getLastIndex (templateName: string): number {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const indexes = [...this.templateStatistics.get(templateName)!.indexes]
+ .concat(0)
+ .sort((a, b) => a - b)
+ for (let i = 0; i < indexes.length - 1; i++) {
+ if (indexes[i + 1] - indexes[i] !== 1) {
+ return indexes[i]
+ }
+ }
+ return indexes[indexes.length - 1]
+ }
+
+ public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
+ return this.storage?.getPerformanceStatistics()
+ }
+
+ public getState (): SimulatorState {
+ return {
+ configuration: Configuration.getConfigurationData(),
+ started: this.started,
+ templateStatistics: this.templateStatistics,
+ version: this.version,
+ }
+ }
+
+ public async start (): Promise<void> {
+ if (!this.started) {
+ if (!this.starting) {
+ this.starting = true
+ this.on(ChargingStationWorkerMessageEvents.added, this.workerEventAdded)
this.on(ChargingStationWorkerMessageEvents.deleted, this.workerEventDeleted)
this.on(ChargingStationWorkerMessageEvents.started, this.workerEventStarted)
this.on(ChargingStationWorkerMessageEvents.stopped, this.workerEventStopped)
}
}
- private async restart (): Promise<void> {
- await this.stop()
- if (
- this.uiServerStarted &&
- Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
- .enabled !== true
- ) {
- this.uiServer.stop()
- this.uiServerStarted = false
- }
- this.initializeCounters()
- // FIXME: initialize worker implementation only if the worker section has changed
- this.initializeWorkerImplementation(
- Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
- )
- await this.start()
- }
-
- private async waitChargingStationsStopped (): Promise<string> {
- return await new Promise<string>((resolve, reject: (reason?: unknown) => void) => {
- const waitTimeout = setTimeout(() => {
- const timeoutMessage = `Timeout ${formatDurationMilliSeconds(
- Constants.STOP_CHARGING_STATIONS_TIMEOUT
- )} reached at stopping charging stations`
- console.warn(chalk.yellow(timeoutMessage))
- reject(new Error(timeoutMessage))
- }, Constants.STOP_CHARGING_STATIONS_TIMEOUT)
- waitChargingStationEvents(
- this,
- ChargingStationWorkerMessageEvents.stopped,
- this.numberOfStartedChargingStations
- )
- .then(events => {
- resolve('Charging stations stopped')
- return events
- })
- .finally(() => {
- clearTimeout(waitTimeout)
- })
- .catch(reject)
- })
- }
-
- private initializeWorkerImplementation (workerConfiguration: WorkerConfiguration): void {
- if (!isMainThread) {
- return
- }
- let elementsPerWorker: number
- switch (workerConfiguration.elementsPerWorker) {
- case 'all':
- elementsPerWorker =
- this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations
- break
- case 'auto':
- elementsPerWorker =
- this.numberOfConfiguredChargingStations + this.numberOfProvisionedChargingStations >
- availableParallelism()
- ? Math.round(
- (this.numberOfConfiguredChargingStations +
- this.numberOfProvisionedChargingStations) /
- (availableParallelism() * 1.5)
- )
- : 1
- break
- default:
- elementsPerWorker = workerConfiguration.elementsPerWorker ?? DEFAULT_ELEMENTS_PER_WORKER
- }
- this.workerImplementation = WorkerFactory.getWorkerImplementation<
- ChargingStationWorkerData,
- ChargingStationInfo
- >(
- join(
- dirname(fileURLToPath(import.meta.url)),
- `ChargingStationWorker${extname(fileURLToPath(import.meta.url))}`
- ),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- workerConfiguration.processType!,
- {
- workerStartDelay: workerConfiguration.startDelay,
- elementAddDelay: workerConfiguration.elementAddDelay,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- poolMaxSize: workerConfiguration.poolMaxSize!,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- poolMinSize: workerConfiguration.poolMinSize!,
- elementsPerWorker,
- poolOptions: {
- messageHandler: this.messageHandler.bind(this) as MessageHandler<Worker>,
- ...(workerConfiguration.resourceLimits != null && {
- workerOptions: {
- resourceLimits: workerConfiguration.resourceLimits,
- },
- }),
- },
- }
+ private get numberOfAddedChargingStations (): number {
+ return [...this.templateStatistics.values()].reduce(
+ (accumulator, value) => accumulator + value.added,
+ 0
)
}
- private messageHandler (
- msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
- ): void {
- // logger.debug(
- // `${this.logPrefix()} ${moduleName}.messageHandler: Charging station worker message received: ${JSON.stringify(
- // msg,
- // undefined,
- // 2
- // )}`
- // )
- // Skip worker message events processing
- // eslint-disable-next-line @typescript-eslint/dot-notation
- if (msg['uuid'] != null) {
- return
- }
- const { event, data } = msg
- try {
- switch (event) {
- case ChargingStationWorkerMessageEvents.added:
- this.emit(ChargingStationWorkerMessageEvents.added, data)
- break
- case ChargingStationWorkerMessageEvents.deleted:
- this.emit(ChargingStationWorkerMessageEvents.deleted, data)
- break
- case ChargingStationWorkerMessageEvents.started:
- this.emit(ChargingStationWorkerMessageEvents.started, data)
- break
- case ChargingStationWorkerMessageEvents.stopped:
- this.emit(ChargingStationWorkerMessageEvents.stopped, data)
- break
- case ChargingStationWorkerMessageEvents.updated:
- this.emit(ChargingStationWorkerMessageEvents.updated, data)
- break
- case ChargingStationWorkerMessageEvents.performanceStatistics:
- this.emit(ChargingStationWorkerMessageEvents.performanceStatistics, data)
- break
- default:
- throw new BaseError(
- `Unknown charging station worker message event: '${event}' received with data: ${JSON.stringify(
- data,
- undefined,
- 2
- )}`
- )
- }
- } catch (error) {
- logger.error(
- `${this.logPrefix()} ${moduleName}.messageHandler: Error occurred while handling charging station worker message event '${event}':`,
- error
- )
- }
- }
-
- private readonly workerEventAdded = (data: ChargingStationData): void => {
- this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
- logger.info(
- `${this.logPrefix()} ${moduleName}.workerEventAdded: Charging station ${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- data.stationInfo.chargingStationId
- } (hashId: ${data.stationInfo.hashId}) added (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
- )
+ public get numberOfChargingStationTemplates (): number {
+ return this.templateStatistics.size
}
- private readonly workerEventDeleted = (data: ChargingStationData): void => {
- this.uiServer.chargingStations.delete(data.stationInfo.hashId)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const templateStatistics = this.templateStatistics.get(data.stationInfo.templateName)!
- --templateStatistics.added
- templateStatistics.indexes.delete(data.stationInfo.templateIndex)
- logger.info(
- `${this.logPrefix()} ${moduleName}.workerEventDeleted: Charging station ${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- data.stationInfo.chargingStationId
- } (hashId: ${data.stationInfo.hashId}) deleted (${this.numberOfAddedChargingStations.toString()} added from ${this.numberOfConfiguredChargingStations.toString()} configured and ${this.numberOfProvisionedChargingStations.toString()} provisioned charging station(s))`
+ public get numberOfConfiguredChargingStations (): number {
+ return [...this.templateStatistics.values()].reduce(
+ (accumulator, value) => accumulator + value.configured,
+ 0
)
}
- private readonly workerEventStarted = (data: ChargingStationData): void => {
- this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.templateStatistics.get(data.stationInfo.templateName)!.started
- logger.info(
- `${this.logPrefix()} ${moduleName}.workerEventStarted: Charging station ${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- data.stationInfo.chargingStationId
- } (hashId: ${data.stationInfo.hashId}) started (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
+ public get numberOfProvisionedChargingStations (): number {
+ return [...this.templateStatistics.values()].reduce(
+ (accumulator, value) => accumulator + value.provisioned,
+ 0
)
}
- private readonly workerEventStopped = (data: ChargingStationData): void => {
- this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- --this.templateStatistics.get(data.stationInfo.templateName)!.started
- logger.info(
- `${this.logPrefix()} ${moduleName}.workerEventStopped: Charging station ${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- data.stationInfo.chargingStationId
- } (hashId: ${data.stationInfo.hashId}) stopped (${this.numberOfStartedChargingStations.toString()} started from ${this.numberOfAddedChargingStations.toString()} added charging station(s))`
+ private get numberOfStartedChargingStations (): number {
+ return [...this.templateStatistics.values()].reduce(
+ (accumulator, value) => accumulator + value.started,
+ 0
)
}
-
- private readonly workerEventUpdated = (data: ChargingStationData): void => {
- this.uiServer.chargingStations.set(data.stationInfo.hashId, data)
- }
-
- private readonly workerEventPerformanceStatistics = (data: Statistics): void => {
- // eslint-disable-next-line @typescript-eslint/unbound-method
- if (isAsyncFunction(this.storage?.storePerformanceStatistics)) {
- ;(
- this.storage.storePerformanceStatistics as (
- performanceStatistics: Statistics
- ) => Promise<void>
- )(data).catch(Constants.EMPTY_FUNCTION)
- } else {
- ;(this.storage?.storePerformanceStatistics as (performanceStatistics: Statistics) => void)(
- data
- )
- }
- }
-
- private initializeCounters (): void {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const stationTemplateUrls = Configuration.getStationTemplateUrls()!
- if (isNotEmptyArray(stationTemplateUrls)) {
- for (const stationTemplateUrl of stationTemplateUrls) {
- const templateName = buildTemplateName(stationTemplateUrl.file)
- this.templateStatistics.set(templateName, {
- configured: stationTemplateUrl.numberOfStations,
- provisioned: stationTemplateUrl.provisionedNumberOfStations ?? 0,
- added: 0,
- started: 0,
- indexes: new Set<number>(),
- })
- this.uiServer.chargingStationTemplates.add(templateName)
- }
- if (this.templateStatistics.size !== stationTemplateUrls.length) {
- console.error(
- chalk.red(
- "'stationTemplateUrls' contains duplicate entries, please check your configuration"
- )
- )
- exit(exitCodes.duplicateChargingStationTemplateUrls)
- }
- } else {
- console.error(
- chalk.red("'stationTemplateUrls' not defined or empty, please check your configuration")
- )
- exit(exitCodes.missingChargingStationsConfiguration)
- }
- if (
- this.numberOfConfiguredChargingStations === 0 &&
- Configuration.getConfigurationSection<UIServerConfiguration>(ConfigurationSection.uiServer)
- .enabled !== true
- ) {
- console.error(
- chalk.red(
- "'stationTemplateUrls' has no charging station enabled and UI server is disabled, please check your configuration"
- )
- )
- exit(exitCodes.noChargingStationTemplates)
- }
- }
-
- public async addChargingStation (
- index: number,
- templateFile: string,
- options?: ChargingStationOptions
- ): Promise<ChargingStationInfo | undefined> {
- if (!this.started && !this.starting) {
- throw new BaseError(
- 'Cannot add charging station while the charging stations simulator is not started'
- )
- }
- const stationInfo = await this.workerImplementation?.addElement({
- index,
- templateFile: join(
- dirname(fileURLToPath(import.meta.url)),
- 'assets',
- 'station-templates',
- templateFile
- ),
- options,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const templateStatistics = this.templateStatistics.get(buildTemplateName(templateFile))!
- ++templateStatistics.added
- templateStatistics.indexes.add(index)
- return stationInfo
- }
-
- private gracefulShutdown (): void {
- this.stop()
- .then(() => {
- console.info(chalk.green('Graceful shutdown'))
- this.uiServer.stop()
- this.uiServerStarted = false
- this.waitChargingStationsStopped()
- // eslint-disable-next-line promise/no-nesting
- .then(() => {
- return exit(exitCodes.succeeded)
- })
- // eslint-disable-next-line promise/no-nesting
- .catch(() => {
- exit(exitCodes.gracefulShutdownError)
- })
- return undefined
- })
- .catch((error: unknown) => {
- console.error(chalk.red('Error while shutdowning charging stations simulator: '), error)
- exit(exitCodes.gracefulShutdownError)
- })
- }
-
- private readonly logPrefix = (): string => {
- return logPrefix(' Bootstrap |')
- }
}
// 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'
import { SharedLRUCache } from './SharedLRUCache.js'
export class ChargingStation extends EventEmitter {
- public readonly index: number
- public readonly templateFile: string
- public stationInfo?: ChargingStationInfo
- public started: boolean
- public starting: boolean
- public idTagsCache: IdTagsCache
- public automaticTransactionGenerator?: AutomaticTransactionGenerator
- public ocppConfiguration?: ChargingStationOcppConfiguration
- public wsConnection: WebSocket | null
- public readonly connectors: Map<number, ConnectorStatus>
- public readonly evses: Map<number, EvseStatus>
- public readonly requests: Map<string, CachedRequest>
- public performanceStatistics?: PerformanceStatistics
- public heartbeatSetInterval?: NodeJS.Timeout
- public ocppRequestService!: OCPPRequestService
- public bootNotificationRequest?: BootNotificationRequest
- public bootNotificationResponse?: BootNotificationResponse
- public powerDivider?: number
- private stopping: boolean
+ private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
+ private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
private configurationFile!: string
private configurationFileHash!: string
+ private configuredSupervisionUrl!: URL
private connectorsConfigurationHash!: string
private evsesConfigurationHash!: string
- private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
- private ocppIncomingRequestService!: OCPPIncomingRequestService
+ private flushMessageBufferSetInterval?: NodeJS.Timeout
private readonly messageBuffer: Set<string>
- private configuredSupervisionUrl!: URL
- private wsConnectionRetryCount: number
- private templateFileWatcher?: FSWatcher
- private templateFileHash!: string
+ private ocppIncomingRequestService!: OCPPIncomingRequestService
private readonly sharedLRUCache: SharedLRUCache
+ private stopping: boolean
+ private templateFileHash!: string
+ private templateFileWatcher?: FSWatcher
+ private wsConnectionRetryCount: number
private wsPingSetInterval?: NodeJS.Timeout
- private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
- private flushMessageBufferSetInterval?: NodeJS.Timeout
+ public automaticTransactionGenerator?: AutomaticTransactionGenerator
+ public bootNotificationRequest?: BootNotificationRequest
+ public bootNotificationResponse?: BootNotificationResponse
+ public readonly connectors: Map<number, ConnectorStatus>
+ public readonly evses: Map<number, EvseStatus>
+ public heartbeatSetInterval?: NodeJS.Timeout
+ public idTagsCache: IdTagsCache
+ public readonly index: number
+ public logPrefix = (): string => {
+ if (
+ this instanceof ChargingStation &&
+ this.stationInfo != null &&
+ isNotEmptyString(this.stationInfo.chargingStationId)
+ ) {
+ return logPrefix(` ${this.stationInfo.chargingStationId} |`)
+ }
+ let stationTemplate: ChargingStationTemplate | undefined
+ try {
+ stationTemplate = JSON.parse(
+ readFileSync(this.templateFile, 'utf8')
+ ) as ChargingStationTemplate
+ } catch {
+ // Ignore
+ }
+ return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
+ }
+
+ public ocppConfiguration?: ChargingStationOcppConfiguration
+ public ocppRequestService!: OCPPRequestService
+ public performanceStatistics?: PerformanceStatistics
+ public powerDivider?: number
+ public readonly requests: Map<string, CachedRequest>
+ public started: boolean
+ public starting: boolean
+ public stationInfo?: ChargingStationInfo
+ public readonly templateFile: string
+
+ public wsConnection: null | WebSocket
constructor (index: number, templateFile: string, options?: ChargingStationOptions) {
super()
}
}
- public get hasEvses (): boolean {
- return this.connectors.size === 0 && this.evses.size > 0
- }
-
- public get wsConnectionUrl (): URL {
- const wsConnectionBaseUrlStr = `${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- this.stationInfo?.supervisionUrlOcppConfiguration === true &&
- isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
- isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
- ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
- : this.configuredSupervisionUrl.href
- }`
- return new URL(
- `${wsConnectionBaseUrlStr}${
- !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }${this.stationInfo?.chargingStationId}`
- )
+ private add (): void {
+ this.emit(ChargingStationEvents.added)
}
- public logPrefix = (): string => {
- if (
- this instanceof ChargingStation &&
- this.stationInfo != null &&
- isNotEmptyString(this.stationInfo.chargingStationId)
- ) {
- return logPrefix(` ${this.stationInfo.chargingStationId} |`)
- }
- let stationTemplate: ChargingStationTemplate | undefined
- try {
- stationTemplate = JSON.parse(
- readFileSync(this.templateFile, 'utf8')
- ) as ChargingStationTemplate
- } catch {
- // Ignore
+ private clearIntervalFlushMessageBuffer (): void {
+ if (this.flushMessageBufferSetInterval != null) {
+ clearInterval(this.flushMessageBufferSetInterval)
+ delete this.flushMessageBufferSetInterval
}
- return logPrefix(` ${getChargingStationId(this.index, stationTemplate)} |`)
- }
-
- public hasIdTags (): boolean {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
}
- public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const localStationInfo = stationInfo ?? this.stationInfo!
- switch (this.getCurrentOutType(stationInfo)) {
- case CurrentType.AC:
- return localStationInfo.numberOfPhases ?? 3
- case CurrentType.DC:
- return 0
+ private flushMessageBuffer (): void {
+ if (this.messageBuffer.size > 0) {
+ for (const message of this.messageBuffer.values()) {
+ let beginId: string | undefined
+ let commandName: RequestCommand | undefined
+ const [messageType] = JSON.parse(message) as ErrorResponse | OutgoingRequest | Response
+ const isRequest = messageType === MessageType.CALL_MESSAGE
+ if (isRequest) {
+ ;[, , commandName] = JSON.parse(message) as OutgoingRequest
+ beginId = PerformanceStatistics.beginMeasure(commandName)
+ }
+ this.wsConnection?.send(message, (error?: Error) => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
+ if (error == null) {
+ logger.debug(
+ `${this.logPrefix()} >> Buffered ${getMessageTypeString(
+ messageType
+ )} OCPP message sent '${JSON.stringify(message)}'`
+ )
+ this.messageBuffer.delete(message)
+ } else {
+ logger.debug(
+ `${this.logPrefix()} >> Buffered ${getMessageTypeString(
+ messageType
+ )} OCPP message '${JSON.stringify(message)}' send failed:`,
+ error
+ )
+ }
+ })
+ }
}
}
- public isWebSocketConnectionOpened (): boolean {
- return this.wsConnection?.readyState === WebSocket.OPEN
- }
-
- public inUnknownState (): boolean {
- return this.bootNotificationResponse?.status == null
- }
-
- public inPendingState (): boolean {
- return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
- }
-
- public inAcceptedState (): boolean {
- return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
- }
-
- public inRejectedState (): boolean {
- return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
- }
-
- public isRegistered (): boolean {
- return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
- }
-
- public isChargingStationAvailable (): boolean {
- return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
- }
-
- public hasConnector (connectorId: number): boolean {
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- if (evseStatus.connectors.has(connectorId)) {
- return true
- }
- }
- return false
+ private getAmperageLimitation (): number | undefined {
+ if (
+ isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
+ getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
+ ) {
+ return (
+ convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
+ getAmperageLimitationUnitDivider(this.stationInfo)
+ )
}
- return this.connectors.has(connectorId)
}
- public isConnectorAvailable (connectorId: number): boolean {
- return (
- connectorId > 0 &&
- this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
+ private getCachedRequest (
+ messageType: MessageType | undefined,
+ messageId: string
+ ): CachedRequest | undefined {
+ const cachedRequest = this.requests.get(messageId)
+ if (Array.isArray(cachedRequest)) {
+ return cachedRequest
+ }
+ throw new OCPPError(
+ ErrorType.PROTOCOL_ERROR,
+ `Cached request for message id '${messageId}' ${getMessageTypeString(
+ messageType
+ )} is not an array`,
+ undefined,
+ cachedRequest
)
}
- public getNumberOfConnectors (): number {
- if (this.hasEvses) {
- let numberOfConnectors = 0
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId > 0) {
- numberOfConnectors += evseStatus.connectors.size
+ private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
+ let configuration: ChargingStationConfiguration | undefined
+ if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
+ try {
+ if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
+ configuration = this.sharedLRUCache.getChargingStationConfiguration(
+ this.configurationFileHash
+ )
+ } else {
+ const measureId = `${FileType.ChargingStationConfiguration} read`
+ const beginId = PerformanceStatistics.beginMeasure(measureId)
+ configuration = JSON.parse(
+ readFileSync(this.configurationFile, 'utf8')
+ ) as ChargingStationConfiguration
+ PerformanceStatistics.endMeasure(measureId, beginId)
+ this.sharedLRUCache.setChargingStationConfiguration(configuration)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.configurationFileHash = configuration.configurationHash!
}
+ } catch (error) {
+ handleFileException(
+ this.configurationFile,
+ FileType.ChargingStationConfiguration,
+ error as NodeJS.ErrnoException,
+ this.logPrefix()
+ )
}
- return numberOfConnectors
}
- return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
- }
-
- public getNumberOfEvses (): number {
- return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
+ return configuration
}
- public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- if (evseStatus.connectors.has(connectorId)) {
- return evseStatus.connectors.get(connectorId)
- }
+ private getConfiguredSupervisionUrl (): URL {
+ let configuredSupervisionUrl: string
+ const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
+ if (isNotEmptyArray(supervisionUrls)) {
+ let configuredSupervisionUrlIndex: number
+ switch (Configuration.getSupervisionUrlDistribution()) {
+ case SupervisionUrlDistribution.RANDOM:
+ configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
+ break
+ case SupervisionUrlDistribution.ROUND_ROBIN:
+ case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
+ default:
+ !Object.values(SupervisionUrlDistribution).includes(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Configuration.getSupervisionUrlDistribution()!
+ ) &&
+ logger.warn(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
+ `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
+ SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
+ }'`
+ )
+ configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
+ break
}
- return undefined
+ configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ configuredSupervisionUrl = supervisionUrls!
}
- return this.connectors.get(connectorId)
+ if (isNotEmptyString(configuredSupervisionUrl)) {
+ return new URL(configuredSupervisionUrl)
+ }
+ const errorMsg = 'No supervision url(s) configured'
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
- public getConnectorMaximumAvailablePower (connectorId: number): number {
- let connectorAmperageLimitationLimit: number | undefined
- const amperageLimitation = this.getAmperageLimitation()
- if (
- amperageLimitation != null &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- amperageLimitation < this.stationInfo!.maximumAmperage!
- ) {
- connectorAmperageLimitationLimit =
- (this.stationInfo?.currentOutType === CurrentType.AC
- ? ACElectricUtils.powerTotal(
- this.getNumberOfPhases(),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.stationInfo.voltageOut!,
- amperageLimitation *
- (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
- )
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) /
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.powerDivider!
+ // 0 for disabling
+ private getConnectionTimeout (): number {
+ if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
+ return convertToInt(
+ getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
+ Constants.DEFAULT_CONNECTION_TIMEOUT
+ )
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
- const chargingStationChargingProfilesLimit =
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- getChargingStationChargingProfilesLimit(this)! / this.powerDivider!
- const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId)
- return min(
- isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
+ return Constants.DEFAULT_CONNECTION_TIMEOUT
+ }
+
+ private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
+ return (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- isNaN(connectorAmperageLimitationLimit!)
- ? Number.POSITIVE_INFINITY
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorAmperageLimitationLimit!,
- isNaN(chargingStationChargingProfilesLimit)
- ? Number.POSITIVE_INFINITY
- : chargingStationChargingProfilesLimit,
+ (stationInfo ?? this.stationInfo!).currentOutType ??
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- isNaN(connectorChargingProfilesLimit!)
- ? Number.POSITIVE_INFINITY
- : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorChargingProfilesLimit!
+ Constants.DEFAULT_STATION_INFO.currentOutType!
)
}
- public getTransactionIdTag (transactionId: number): string | undefined {
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- for (const connectorStatus of evseStatus.connectors.values()) {
- if (connectorStatus.transactionId === transactionId) {
- return connectorStatus.transactionIdTag
- }
- }
- }
- } else {
- for (const connectorId of this.connectors.keys()) {
- if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
- return this.getConnectorStatus(connectorId)?.transactionIdTag
- }
- }
+ private getEnergyActiveImportRegister (
+ connectorStatus: ConnectorStatus | undefined,
+ rounded = false
+ ): number {
+ if (this.stationInfo?.meteringPerTransaction === true) {
+ return (
+ (rounded
+ ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
+ ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
+ : undefined
+ : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
+ )
}
+ return (
+ (rounded
+ ? connectorStatus?.energyActiveImportRegisterValue != null
+ ? Math.round(connectorStatus.energyActiveImportRegisterValue)
+ : undefined
+ : connectorStatus?.energyActiveImportRegisterValue) ?? 0
+ )
}
- public getNumberOfRunningTransactions (): number {
- let numberOfRunningTransactions = 0
- if (this.hasEvses) {
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId === 0) {
- continue
- }
- for (const connectorStatus of evseStatus.connectors.values()) {
- if (connectorStatus.transactionStarted === true) {
- ++numberOfRunningTransactions
- }
- }
- }
- } else {
- for (const connectorId of this.connectors.keys()) {
- if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
- ++numberOfRunningTransactions
- }
- }
+ private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
+ switch (this.getCurrentOutType(stationInfo)) {
+ case CurrentType.AC:
+ return ACElectricUtils.amperagePerPhaseFromPower(
+ this.getNumberOfPhases(stationInfo),
+ maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
+ this.getVoltageOut(stationInfo)
+ )
+ case CurrentType.DC:
+ return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
}
- return numberOfRunningTransactions
}
- public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
- if (transactionId == null) {
- return undefined
- } else if (this.hasEvses) {
+ private getNumberOfReservableConnectors (): number {
+ let numberOfReservableConnectors = 0
+ if (this.hasEvses) {
for (const evseStatus of this.evses.values()) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (connectorStatus.transactionId === transactionId) {
- return connectorId
- }
- }
+ numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
}
} else {
- for (const connectorId of this.connectors.keys()) {
- if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
- return connectorId
- }
- }
+ numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
}
+ return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
}
- public getEnergyActiveImportRegisterByTransactionId (
- transactionId: number | undefined,
- rounded = false
- ): number {
- return this.getEnergyActiveImportRegister(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!),
- rounded
- )
+ private getNumberOfReservationsOnConnectorZero (): number {
+ if (
+ (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
+ (!this.hasEvses && this.connectors.get(0)?.reservation != null)
+ ) {
+ return 1
+ }
+ return 0
}
- public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
- return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
+ private getOcppConfiguration (
+ ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
+ ): ChargingStationOcppConfiguration | undefined {
+ let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
+ this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
+ if (ocppConfiguration == null) {
+ ocppConfiguration = this.getOcppConfigurationFromTemplate()
+ }
+ return ocppConfiguration
}
- public getAuthorizeRemoteTxRequests (): boolean {
- const authorizeRemoteTxRequests = getConfigurationKey(
- this,
- StandardParametersKey.AuthorizeRemoteTxRequests
- )
- return authorizeRemoteTxRequests != null
- ? convertToBoolean(authorizeRemoteTxRequests.value)
- : false
+ private getOcppConfigurationFromFile (
+ ocppPersistentConfiguration?: boolean
+ ): ChargingStationOcppConfiguration | undefined {
+ const configurationKey = this.getConfigurationFromFile()?.configurationKey
+ if (ocppPersistentConfiguration && Array.isArray(configurationKey)) {
+ return { configurationKey }
+ }
+ return undefined
}
- public getLocalAuthListEnabled (): boolean {
- const localAuthListEnabled = getConfigurationKey(
- this,
- StandardParametersKey.LocalAuthListEnabled
- )
- return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
+ private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
+ return this.getTemplateFromFile()?.Configuration
}
- public getHeartbeatInterval (): number {
- const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
- if (HeartbeatInterval != null) {
- return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
- }
- const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
- if (HeartBeatInterval != null) {
- return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
+ private getPowerDivider (): number {
+ let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
+ if (this.stationInfo?.powerSharedByConnectors === true) {
+ powerDivider = this.getNumberOfRunningTransactions()
}
- this.stationInfo?.autoRegister === false &&
- logger.warn(
- `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${Constants.DEFAULT_HEARTBEAT_INTERVAL.toString()}`
- )
- return Constants.DEFAULT_HEARTBEAT_INTERVAL
+ return powerDivider
}
- public setSupervisionUrl (url: string): void {
+ private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
+ const stationInfoFromTemplate = this.getStationInfoFromTemplate()
+ options?.persistentConfiguration != null &&
+ (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
+ const stationInfoFromFile = this.getStationInfoFromFile(
+ stationInfoFromTemplate.stationInfoPersistentConfiguration
+ )
+ let stationInfo: ChargingStationInfo
+ // Priority:
+ // 1. charging station info from template
+ // 2. charging station info from configuration file
if (
- this.stationInfo?.supervisionUrlOcppConfiguration === true &&
- isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
+ stationInfoFromFile != null &&
+ stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
) {
- setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
+ stationInfo = stationInfoFromFile
} else {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.stationInfo!.supervisionUrls = url
- this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
- this.saveStationInfo()
+ stationInfo = stationInfoFromTemplate
+ stationInfoFromFile != null &&
+ propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
}
+ return setChargingStationOptions(
+ mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
+ options
+ )
}
- public startHeartbeat (): void {
- const heartbeatInterval = this.getHeartbeatInterval()
- if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) {
- this.heartbeatSetInterval = setInterval(() => {
- this.ocppRequestService
- .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
- .catch((error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
- error
- )
- })
- }, heartbeatInterval)
- logger.info(
- `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
- heartbeatInterval
- )}`
- )
- } else if (this.heartbeatSetInterval != null) {
- logger.info(
- `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
- heartbeatInterval
- )}`
- )
- } else {
- logger.error(
- `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat`
- )
+ private getStationInfoFromFile (
+ stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
+ .stationInfoPersistentConfiguration
+ ): ChargingStationInfo | undefined {
+ let stationInfo: ChargingStationInfo | undefined
+ if (stationInfoPersistentConfiguration) {
+ stationInfo = this.getConfigurationFromFile()?.stationInfo
+ if (stationInfo != null) {
+ delete stationInfo.infoHash
+ delete (stationInfo as ChargingStationTemplate).numberOfConnectors
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (stationInfo.templateIndex == null) {
+ stationInfo.templateIndex = this.index
+ }
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (stationInfo.templateName == null) {
+ stationInfo.templateName = buildTemplateName(this.templateFile)
+ }
+ }
}
+ return stationInfo
}
- public restartHeartbeat (): void {
- // Stop heartbeat
- this.stopHeartbeat()
- // Start heartbeat
- this.startHeartbeat()
- }
-
- public restartWebSocketPing (): void {
- // Stop WebSocket ping
- this.stopWebSocketPing()
- // Start WebSocket ping
- this.startWebSocketPing()
- }
-
- public startMeterValues (connectorId: number, interval: number): void {
- if (connectorId === 0) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
- )
- return
+ private getStationInfoFromTemplate (): ChargingStationInfo {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const stationTemplate = this.getTemplateFromFile()!
+ checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
+ const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation)
+ warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
+ if (stationTemplate.Connectors != null) {
+ checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
}
- const connectorStatus = this.getConnectorStatus(connectorId)
- if (connectorStatus == null) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on non existing connector id
- ${connectorId.toString()}`
- )
- return
+ const stationInfo = stationTemplateToStationInfo(stationTemplate)
+ stationInfo.hashId = getHashId(this.index, stationTemplate)
+ stationInfo.templateIndex = this.index
+ stationInfo.templateName = buildTemplateName(this.templateFile)
+ stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
+ createSerialNumber(stationTemplate, stationInfo)
+ stationInfo.voltageOut = this.getVoltageOut(stationInfo)
+ if (isNotEmptyArray(stationTemplate.power)) {
+ const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
+ stationInfo.maximumPower =
+ stationTemplate.powerUnit === PowerUnits.KILO_WATT
+ ? stationTemplate.power[powerArrayRandomIndex] * 1000
+ : stationTemplate.power[powerArrayRandomIndex]
+ } else {
+ stationInfo.maximumPower =
+ stationTemplate.powerUnit === PowerUnits.KILO_WATT
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ stationTemplate.power! * 1000
+ : stationTemplate.power
}
- if (connectorStatus.transactionStarted === false) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction started`
- )
- return
- } else if (
- connectorStatus.transactionStarted === true &&
- connectorStatus.transactionId == null
+ stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
+ if (
+ isNotEmptyString(stationInfo.firmwareVersionPattern) &&
+ isNotEmptyString(stationInfo.firmwareVersion) &&
+ !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
) {
- logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id`
+ logger.warn(
+ `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
+ this.templateFile
+ } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
)
- return
}
- if (interval > 0) {
- connectorStatus.transactionSetInterval = setInterval(() => {
- const meterValue = buildMeterValue(
- this,
- connectorId,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- connectorStatus.transactionId!,
- interval
- )
- this.ocppRequestService
- .requestHandler<MeterValuesRequest, MeterValuesResponse>(
- this,
- RequestCommand.METER_VALUES,
- {
- connectorId,
- transactionId: connectorStatus.transactionId,
- meterValue: [meterValue],
- }
- )
- .catch((error: unknown) => {
- logger.error(
- `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
- error
- )
- })
- }, interval)
- } else {
- logger.error(
- `${this.logPrefix()} Charging station ${
- StandardParametersKey.MeterValueSampleInterval
- } configuration set to ${interval.toString()}, not sending MeterValues`
- )
+ if (stationTemplate.resetTime != null) {
+ stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
}
+ return stationInfo
}
- public stopMeterValues (connectorId: number): void {
- const connectorStatus = this.getConnectorStatus(connectorId)
- if (connectorStatus?.transactionSetInterval != null) {
- clearInterval(connectorStatus.transactionSetInterval)
+ private getTemplateFromFile (): ChargingStationTemplate | undefined {
+ let template: ChargingStationTemplate | undefined
+ try {
+ if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
+ template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
+ } else {
+ const measureId = `${FileType.ChargingStationTemplate} read`
+ const beginId = PerformanceStatistics.beginMeasure(measureId)
+ template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
+ PerformanceStatistics.endMeasure(measureId, beginId)
+ template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+ .update(JSON.stringify(template))
+ .digest('hex')
+ this.sharedLRUCache.setChargingStationTemplate(template)
+ this.templateFileHash = template.templateHash
+ }
+ } catch (error) {
+ handleFileException(
+ this.templateFile,
+ FileType.ChargingStationTemplate,
+ error as NodeJS.ErrnoException,
+ this.logPrefix()
+ )
}
+ return template
}
- public restartMeterValues (connectorId: number, interval: number): void {
- this.stopMeterValues(connectorId)
- this.startMeterValues(connectorId, interval)
+ private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
}
- private add (): void {
- this.emit(ChargingStationEvents.added)
+ private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
+ return (
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (stationInfo ?? this.stationInfo!).voltageOut ??
+ getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
+ )
}
- public async delete (deleteConfiguration = true): Promise<void> {
- if (this.started) {
- await this.stop()
- }
- AutomaticTransactionGenerator.deleteInstance(this)
- PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
- this.requests.clear()
- this.connectors.clear()
- this.evses.clear()
- this.templateFileWatcher?.unref()
- deleteConfiguration && rmSync(this.configurationFile, { force: true })
- this.chargingStationWorkerBroadcastChannel.unref()
- this.emit(ChargingStationEvents.deleted)
- this.removeAllListeners()
+ private getWebSocketPingInterval (): number {
+ return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
+ ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
+ : 0
}
- public start (): void {
- if (!this.started) {
- if (!this.starting) {
- this.starting = true
- if (this.stationInfo?.enableStatistics === true) {
- this.performanceStatistics?.start()
- }
- this.openWSConnection()
- // Monitor charging station template file
- this.templateFileWatcher = watchJsonFile(
- this.templateFile,
- FileType.ChargingStationTemplate,
- this.logPrefix(),
- undefined,
- (event, filename): void => {
- if (isNotEmptyString(filename) && event === 'change') {
- try {
- logger.debug(
- `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
- this.templateFile
- } file have changed, reload`
- )
- this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
- // Initialize
- this.initialize()
- // Restart the ATG
- const ATGStarted = this.automaticTransactionGenerator?.started
- if (ATGStarted === true) {
- this.stopAutomaticTransactionGenerator()
- }
- delete this.automaticTransactionGeneratorConfiguration
- if (
- this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
- ATGStarted === true
- ) {
- this.startAutomaticTransactionGenerator(undefined, true)
- }
- if (this.stationInfo?.enableStatistics === true) {
- this.performanceStatistics?.restart()
- } else {
- this.performanceStatistics?.stop()
- }
- // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
- } catch (error) {
- logger.error(
- `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
- error
- )
- }
- }
- }
- )
- this.started = true
- this.emit(ChargingStationEvents.started)
- this.starting = false
- } else {
- logger.warn(`${this.logPrefix()} Charging station is already starting...`)
- }
- } else {
- logger.warn(`${this.logPrefix()} Charging station is already started...`)
+ private handleErrorMessage (errorResponse: ErrorResponse): void {
+ const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
+ if (!this.requests.has(messageId)) {
+ // Error
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ `Error response for unknown message id '${messageId}'`,
+ undefined,
+ { errorDetails, errorMessage, errorType }
+ )
}
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
+ logger.debug(
+ `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
+ errorResponse
+ )}`
+ )
+ errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
}
- public async stop (
- reason?: StopTransactionReason,
- stopTransactions = this.stationInfo?.stopTransactionsOnStopped
- ): Promise<void> {
- if (this.started) {
- if (!this.stopping) {
- this.stopping = true
- await this.stopMessageSequence(reason, stopTransactions)
- this.closeWSConnection()
- if (this.stationInfo?.enableStatistics === true) {
- this.performanceStatistics?.stop()
- }
- this.templateFileWatcher?.close()
- delete this.bootNotificationResponse
- this.started = false
- this.saveConfiguration()
- this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
- this.emit(ChargingStationEvents.stopped)
- this.stopping = false
- } else {
- logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
- }
- } else {
- logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
+ private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
+ const [messageType, messageId, commandName, commandPayload] = request
+ if (this.requests.has(messageId)) {
+ throw new OCPPError(
+ ErrorType.SECURITY_ERROR,
+ `Received message with duplicate message id '${messageId}'`,
+ commandName,
+ commandPayload
+ )
}
+ if (this.stationInfo?.enableStatistics === true) {
+ this.performanceStatistics?.addRequestStatistic(commandName, messageType)
+ }
+ logger.debug(
+ `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
+ request
+ )}`
+ )
+ // Process the message
+ await this.ocppIncomingRequestService.incomingRequestHandler(
+ this,
+ messageId,
+ commandName,
+ commandPayload
+ )
+ this.emit(ChargingStationEvents.updated)
}
- public async reset (reason?: StopTransactionReason): Promise<void> {
- await this.stop(reason)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- await sleep(this.stationInfo!.resetTime!)
- this.initialize()
- this.start()
- }
-
- public saveOcppConfiguration (): void {
- if (this.stationInfo?.ocppPersistentConfiguration === true) {
- this.saveConfiguration()
+ private handleResponseMessage (response: Response): void {
+ const [messageType, messageId, commandPayload] = response
+ if (!this.requests.has(messageId)) {
+ // Error
+ throw new OCPPError(
+ ErrorType.INTERNAL_ERROR,
+ `Response for unknown message id '${messageId}'`,
+ undefined,
+ commandPayload
+ )
}
+ // Respond
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
+ messageType,
+ messageId
+ )!
+ logger.debug(
+ `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
+ response
+ )}`
+ )
+ responseCallback(commandPayload, requestPayload)
}
- public bufferMessage (message: string): void {
- this.messageBuffer.add(message)
- this.setIntervalFlushMessageBuffer()
+ private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
- public openWSConnection (
- options?: WsOptions,
- params?: { closeOpened?: boolean; terminateOpened?: boolean }
- ): void {
- options = {
- handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
- ...this.stationInfo?.wsOptions,
- ...options,
+ private initialize (options?: ChargingStationOptions): void {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const stationTemplate = this.getTemplateFromFile()!
+ checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
+ this.configurationFile = join(
+ dirname(this.templateFile.replace('station-templates', 'configurations')),
+ `${getHashId(this.index, stationTemplate)}.json`
+ )
+ const stationConfiguration = this.getConfigurationFromFile()
+ if (
+ stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
+ (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
+ ) {
+ checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
+ this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
+ } else {
+ this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
}
- params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
- if (!checkChargingStationState(this, this.logPrefix())) {
- return
+ this.stationInfo = this.getStationInfo(options)
+ validateStationInfo(this)
+ if (
+ this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
+ isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
+ isNotEmptyString(this.stationInfo.firmwareVersion)
+ ) {
+ const patternGroup =
+ this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
+ this.stationInfo.firmwareVersion.split('.').length
+ const match = new RegExp(this.stationInfo.firmwareVersionPattern)
+ .exec(this.stationInfo.firmwareVersion)
+ ?.slice(1, patternGroup + 1)
+ if (match != null) {
+ const patchLevelIndex = match.length - 1
+ match[patchLevelIndex] = (
+ convertToInt(match[patchLevelIndex]) +
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
+ ).toString()
+ this.stationInfo.firmwareVersion = match.join('.')
+ }
}
- if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
- options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
+ this.saveStationInfo()
+ this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+ if (this.stationInfo.enableStatistics === true) {
+ this.performanceStatistics = PerformanceStatistics.getInstance(
+ this.stationInfo.hashId,
+ this.stationInfo.chargingStationId,
+ this.configuredSupervisionUrl
+ )
}
- if (params.closeOpened) {
- this.closeWSConnection()
+ const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
+ if (bootNotificationRequest == null) {
+ const errorMsg = 'Error while creating boot notification request'
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
- if (params.terminateOpened) {
- this.terminateWSConnection()
+ this.bootNotificationRequest = bootNotificationRequest
+ this.powerDivider = this.getPowerDivider()
+ // OCPP configuration
+ this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
+ this.initializeOcppConfiguration()
+ this.initializeOcppServices()
+ if (this.stationInfo.autoRegister === true) {
+ this.bootNotificationResponse = {
+ currentTime: new Date(),
+ interval: millisecondsToSeconds(this.getHeartbeatInterval()),
+ status: RegistrationStatusEnumType.ACCEPTED,
+ }
}
+ }
- if (this.isWebSocketConnectionOpened()) {
- logger.warn(
- `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
- )
- return
+ private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
+ if (stationTemplate.Connectors == null && this.connectors.size === 0) {
+ const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
-
- logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
-
- this.wsConnection = new WebSocket(
- this.wsConnectionUrl,
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `ocpp${this.stationInfo?.ocppVersion}`,
- options
- )
-
- // Handle WebSocket message
- this.wsConnection.on('message', data => {
- this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
- })
- // Handle WebSocket error
- this.wsConnection.on('error', this.onError.bind(this))
- // Handle WebSocket close
- this.wsConnection.on('close', this.onClose.bind(this))
- // Handle WebSocket open
- this.wsConnection.on('open', () => {
- this.onOpen().catch((error: unknown) =>
- logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
+ if (stationTemplate.Connectors?.[0] == null) {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no connector id 0 configuration`
)
- })
- // Handle WebSocket ping
- this.wsConnection.on('ping', this.onPing.bind(this))
- // Handle WebSocket pong
- this.wsConnection.on('pong', this.onPong.bind(this))
- }
-
- public closeWSConnection (): void {
- if (this.isWebSocketConnectionOpened()) {
- this.wsConnection?.close()
- this.wsConnection = null
}
- }
-
- public getAutomaticTransactionGeneratorConfiguration ():
- | AutomaticTransactionGeneratorConfiguration
- | undefined {
- if (this.automaticTransactionGeneratorConfiguration == null) {
- let automaticTransactionGeneratorConfiguration:
- | AutomaticTransactionGeneratorConfiguration
- | undefined
- const stationTemplate = this.getTemplateFromFile()
- const stationConfiguration = this.getConfigurationFromFile()
- if (
- this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
- stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
- stationConfiguration?.automaticTransactionGenerator != null
- ) {
- automaticTransactionGeneratorConfiguration =
- stationConfiguration.automaticTransactionGenerator
- } else {
- automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
- }
- this.automaticTransactionGeneratorConfiguration = {
- ...Constants.DEFAULT_ATG_CONFIGURATION,
- ...automaticTransactionGeneratorConfiguration,
+ if (stationTemplate.Connectors != null) {
+ const { configuredMaxConnectors, templateMaxAvailableConnectors, templateMaxConnectors } =
+ checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
+ const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+ .update(
+ `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
+ )
+ .digest('hex')
+ const connectorsConfigChanged =
+ this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
+ if (this.connectors.size === 0 || connectorsConfigChanged) {
+ connectorsConfigChanged && this.connectors.clear()
+ this.connectorsConfigurationHash = connectorsConfigHash
+ if (templateMaxConnectors > 0) {
+ for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
+ if (
+ connectorId === 0 &&
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ (stationTemplate.Connectors[connectorId] == null ||
+ !this.getUseConnectorId0(stationTemplate))
+ ) {
+ continue
+ }
+ const templateConnectorId =
+ connectorId > 0 && stationTemplate.randomConnectors === true
+ ? randomInt(1, templateMaxAvailableConnectors)
+ : connectorId
+ const connectorStatus = stationTemplate.Connectors[templateConnectorId]
+ checkStationInfoConnectorStatus(
+ templateConnectorId,
+ connectorStatus,
+ this.logPrefix(),
+ this.templateFile
+ )
+ this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
+ }
+ initializeConnectorsMapStatus(this.connectors, this.logPrefix())
+ this.saveConnectorsStatus()
+ } else {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no connectors configuration defined, cannot create connectors`
+ )
+ }
}
+ } else {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no connectors configuration defined, using already defined connectors`
+ )
}
- return this.automaticTransactionGeneratorConfiguration
}
- public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
- return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
- }
-
- public startAutomaticTransactionGenerator (
- connectorIds?: number[],
- stopAbsoluteDuration?: boolean
- ): void {
- this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
- if (isNotEmptyArray(connectorIds)) {
- for (const connectorId of connectorIds) {
- this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
+ private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
+ if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
+ for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
+ this.connectors.set(
+ connectorId,
+ prepareConnectorStatus(clone<ConnectorStatus>(connectorStatus))
+ )
+ }
+ } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
+ for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
+ const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
+ delete evseStatus.connectorsStatus
+ this.evses.set(evseId, {
+ ...(evseStatus as EvseStatus),
+ connectors: new Map<number, ConnectorStatus>(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
+ connectorId,
+ prepareConnectorStatus(connectorStatus),
+ ])
+ ),
+ })
}
+ } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
+ const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
} else {
- this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
+ const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
- this.saveAutomaticTransactionGeneratorConfiguration()
- this.emit(ChargingStationEvents.updated)
}
- public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
- if (isNotEmptyArray(connectorIds)) {
- for (const connectorId of connectorIds) {
- this.automaticTransactionGenerator?.stopConnector(connectorId)
- }
+ private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
+ if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
+ this.initializeConnectorsFromTemplate(stationTemplate)
+ } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
+ this.initializeEvsesFromTemplate(stationTemplate)
+ } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
+ const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
} else {
- this.automaticTransactionGenerator?.stop()
+ const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
}
- this.saveAutomaticTransactionGeneratorConfiguration()
- this.emit(ChargingStationEvents.updated)
}
- public async stopTransactionOnConnector (
- connectorId: number,
- reason?: StopTransactionReason
- ): Promise<StopTransactionResponse> {
- const transactionId = this.getConnectorStatus(connectorId)?.transactionId
- if (
- this.stationInfo?.beginEndMeterValues === true &&
- this.stationInfo.ocppStrictCompliance === true &&
- this.stationInfo.outOfOrderEndMeterValues === false
- ) {
- const transactionEndMeterValue = buildTransactionEndMeterValue(
- this,
- connectorId,
- this.getEnergyActiveImportRegisterByTransactionId(transactionId)
+ private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
+ if (stationTemplate.Evses == null && this.evses.size === 0) {
+ const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new BaseError(errorMsg)
+ }
+ if (stationTemplate.Evses?.[0] == null) {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no evse id 0 configuration`
)
- await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
- this,
- RequestCommand.METER_VALUES,
- {
- connectorId,
- transactionId,
- meterValue: [transactionEndMeterValue],
- }
+ }
+ if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with evse id 0 with no connector id 0 configuration`
)
}
- return await this.ocppRequestService.requestHandler<
- Partial<StopTransactionRequest>,
- StopTransactionResponse
- >(this, RequestCommand.STOP_TRANSACTION, {
- transactionId,
- meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
- ...(reason != null && { reason }),
- })
- }
-
- public getReserveConnectorZeroSupported (): boolean {
- return convertToBoolean(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
- )
- }
-
- public async addReservation (reservation: Reservation): Promise<void> {
- const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
- if (reservationFound != null) {
- await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
+ if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
+ )
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
- await sendAndSetConnectorStatus(
- this,
- reservation.connectorId,
- ConnectorStatusEnum.Reserved,
- undefined,
- { send: reservation.connectorId !== 0 }
- )
- }
-
- public async removeReservation (
- reservation: Reservation,
- reason: ReservationTerminationReason
- ): Promise<void> {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const connector = this.getConnectorStatus(reservation.connectorId)!
- switch (reason) {
- case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
- case ReservationTerminationReason.TRANSACTION_STARTED:
- delete connector.reservation
- break
- case ReservationTerminationReason.RESERVATION_CANCELED:
- case ReservationTerminationReason.REPLACE_EXISTING:
- case ReservationTerminationReason.EXPIRED:
- await sendAndSetConnectorStatus(
- this,
- reservation.connectorId,
- ConnectorStatusEnum.Available,
- undefined,
- { send: reservation.connectorId !== 0 }
- )
- delete connector.reservation
- break
- default:
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- throw new BaseError(`Unknown reservation termination reason '${reason}'`)
- }
- }
-
- public getReservationBy (
- filterKey: ReservationKey,
- value: number | string
- ): Reservation | undefined {
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- for (const connectorStatus of evseStatus.connectors.values()) {
- if (connectorStatus.reservation?.[filterKey] === value) {
- return connectorStatus.reservation
+ if (stationTemplate.Evses != null) {
+ const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+ .update(JSON.stringify(stationTemplate.Evses))
+ .digest('hex')
+ const evsesConfigChanged =
+ this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
+ if (this.evses.size === 0 || evsesConfigChanged) {
+ evsesConfigChanged && this.evses.clear()
+ this.evsesConfigurationHash = evsesConfigHash
+ const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
+ if (templateMaxEvses > 0) {
+ for (const evseKey in stationTemplate.Evses) {
+ const evseId = convertToInt(evseKey)
+ this.evses.set(evseId, {
+ availability: AvailabilityType.Operative,
+ connectors: buildConnectorsMap(
+ stationTemplate.Evses[evseKey].Connectors,
+ this.logPrefix(),
+ this.templateFile
+ ),
+ })
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
}
+ this.saveEvsesStatus()
+ } else {
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no evses configuration defined, cannot create evses`
+ )
}
}
} else {
- for (const connectorStatus of this.connectors.values()) {
- if (connectorStatus.reservation?.[filterKey] === value) {
- return connectorStatus.reservation
- }
- }
+ logger.warn(
+ `${this.logPrefix()} Charging station information from template ${
+ this.templateFile
+ } with no evses configuration defined, using already defined evses`
+ )
}
}
- public isConnectorReservable (
- reservationId: number,
- idTag?: string,
- connectorId?: number
- ): boolean {
- const reservation = this.getReservationBy('reservationId', reservationId)
- const reservationExists = reservation != null && !hasReservationExpired(reservation)
- if (arguments.length === 1) {
- return !reservationExists
- } else if (arguments.length > 1) {
- const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
- const userReservationExists =
- userReservation != null && !hasReservationExpired(userReservation)
- const notConnectorZero = connectorId == null ? true : connectorId > 0
- const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
- return (
- !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
+ private initializeOcppConfiguration (): void {
+ if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
+ addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
+ }
+ if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
+ addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', {
+ visible: false,
+ })
+ }
+ if (
+ this.stationInfo?.supervisionUrlOcppConfiguration === true &&
+ isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
+ getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
+ ) {
+ addConfigurationKey(
+ this,
+ this.stationInfo.supervisionUrlOcppKey,
+ this.configuredSupervisionUrl.href,
+ { reboot: true }
)
+ } else if (
+ this.stationInfo?.supervisionUrlOcppConfiguration === false &&
+ isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
+ getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
+ ) {
+ deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, {
+ save: false,
+ })
}
- return false
- }
-
- private setIntervalFlushMessageBuffer (): void {
- if (this.flushMessageBufferSetInterval == null) {
- this.flushMessageBufferSetInterval = setInterval(() => {
- if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
- this.flushMessageBuffer()
+ if (
+ isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
+ getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
+ ) {
+ addConfigurationKey(
+ this,
+ this.stationInfo.amperageLimitationOcppKey,
+ // prettier-ignore
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
+ )
+ }
+ if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
+ addConfigurationKey(
+ this,
+ StandardParametersKey.SupportedFeatureProfiles,
+ `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
+ )
+ }
+ addConfigurationKey(
+ this,
+ StandardParametersKey.NumberOfConnectors,
+ this.getNumberOfConnectors().toString(),
+ { readonly: true },
+ { overwrite: true }
+ )
+ if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
+ addConfigurationKey(
+ this,
+ StandardParametersKey.MeterValuesSampledData,
+ MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
+ )
+ }
+ if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
+ const connectorsPhaseRotation: string[] = []
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ for (const connectorId of evseStatus.connectors.keys()) {
+ connectorsPhaseRotation.push(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
+ )
+ }
}
- if (this.messageBuffer.size === 0) {
- this.clearIntervalFlushMessageBuffer()
+ } else {
+ for (const connectorId of this.connectors.keys()) {
+ connectorsPhaseRotation.push(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
+ )
}
- }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
+ }
+ addConfigurationKey(
+ this,
+ StandardParametersKey.ConnectorPhaseRotation,
+ connectorsPhaseRotation.toString()
+ )
}
- }
-
- private clearIntervalFlushMessageBuffer (): void {
- if (this.flushMessageBufferSetInterval != null) {
- clearInterval(this.flushMessageBufferSetInterval)
- delete this.flushMessageBufferSetInterval
+ if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
+ addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
+ }
+ if (
+ getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
+ hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
+ ) {
+ addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
+ }
+ if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
+ addConfigurationKey(
+ this,
+ StandardParametersKey.ConnectionTimeOut,
+ Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
+ )
}
+ this.saveOcppConfiguration()
}
- private getNumberOfReservableConnectors (): number {
- let numberOfReservableConnectors = 0
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- numberOfReservableConnectors += getNumberOfReservableConnectors(evseStatus.connectors)
- }
- } else {
- numberOfReservableConnectors = getNumberOfReservableConnectors(this.connectors)
+ private initializeOcppServices (): void {
+ const ocppVersion = this.stationInfo?.ocppVersion
+ switch (ocppVersion) {
+ case OCPPVersion.VERSION_16:
+ this.ocppIncomingRequestService =
+ OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
+ this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
+ OCPP16ResponseService.getInstance<OCPP16ResponseService>()
+ )
+ break
+ case OCPPVersion.VERSION_20:
+ case OCPPVersion.VERSION_201:
+ this.ocppIncomingRequestService =
+ OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
+ this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
+ OCPP20ResponseService.getInstance<OCPP20ResponseService>()
+ )
+ break
+ default:
+ this.handleUnsupportedVersion(ocppVersion)
+ break
}
- return numberOfReservableConnectors - this.getNumberOfReservationsOnConnectorZero()
}
- private getNumberOfReservationsOnConnectorZero (): number {
- if (
- (this.hasEvses && this.evses.get(0)?.connectors.get(0)?.reservation != null) ||
- (!this.hasEvses && this.connectors.get(0)?.reservation != null)
- ) {
- return 1
+ private internalStopMessageSequence (): void {
+ // Stop WebSocket ping
+ this.stopWebSocketPing()
+ // Stop heartbeat
+ this.stopHeartbeat()
+ // Stop the ATG
+ if (this.automaticTransactionGenerator?.started === true) {
+ this.stopAutomaticTransactionGenerator()
}
- return 0
}
- private flushMessageBuffer (): void {
- if (this.messageBuffer.size > 0) {
- for (const message of this.messageBuffer.values()) {
- let beginId: string | undefined
- let commandName: RequestCommand | undefined
- const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse
- const isRequest = messageType === MessageType.CALL_MESSAGE
- if (isRequest) {
- ;[, , commandName] = JSON.parse(message) as OutgoingRequest
- beginId = PerformanceStatistics.beginMeasure(commandName)
- }
- this.wsConnection?.send(message, (error?: Error) => {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- isRequest && PerformanceStatistics.endMeasure(commandName!, beginId!)
- if (error == null) {
- logger.debug(
- `${this.logPrefix()} >> Buffered ${getMessageTypeString(
- messageType
- )} OCPP message sent '${JSON.stringify(message)}'`
- )
- this.messageBuffer.delete(message)
- } else {
- logger.debug(
- `${this.logPrefix()} >> Buffered ${getMessageTypeString(
- messageType
- )} OCPP message '${JSON.stringify(message)}' send failed:`,
- error
+ private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
+ this.emit(ChargingStationEvents.disconnected)
+ this.emit(ChargingStationEvents.updated)
+ switch (code) {
+ // Normal close
+ case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
+ case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
+ logger.info(
+ `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
+ code
+ )}' and reason '${reason.toString()}'`
+ )
+ this.wsConnectionRetryCount = 0
+ break
+ // Abnormal close
+ default:
+ logger.error(
+ `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
+ code
+ )}' and reason '${reason.toString()}'`
+ )
+ this.started &&
+ this.reconnect()
+ .then(() => {
+ this.emit(ChargingStationEvents.updated)
+ return undefined
+ })
+ .catch((error: unknown) =>
+ logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
)
- }
- })
- }
+ break
}
}
- private getTemplateFromFile (): ChargingStationTemplate | undefined {
- let template: ChargingStationTemplate | undefined
+ private onError (error: WSError): void {
+ this.closeWSConnection()
+ logger.error(`${this.logPrefix()} WebSocket error:`, error)
+ }
+
+ private async onMessage (data: RawData): Promise<void> {
+ let request: ErrorResponse | IncomingRequest | Response | undefined
+ let messageType: MessageType | undefined
+ let errorMsg: string
try {
- if (this.sharedLRUCache.hasChargingStationTemplate(this.templateFileHash)) {
- template = this.sharedLRUCache.getChargingStationTemplate(this.templateFileHash)
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ request = JSON.parse(data.toString()) as ErrorResponse | IncomingRequest | Response
+ if (Array.isArray(request)) {
+ ;[messageType] = request
+ // Check the type of message
+ switch (messageType) {
+ // Error Message
+ case MessageType.CALL_ERROR_MESSAGE:
+ this.handleErrorMessage(request as ErrorResponse)
+ break
+ // Incoming Message
+ case MessageType.CALL_MESSAGE:
+ await this.handleIncomingMessage(request as IncomingRequest)
+ break
+ // Response Message
+ case MessageType.CALL_RESULT_MESSAGE:
+ this.handleResponseMessage(request as Response)
+ break
+ // Unknown Message
+ default:
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ errorMsg = `Wrong message type ${messageType}`
+ logger.error(`${this.logPrefix()} ${errorMsg}`)
+ throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
+ }
} else {
- const measureId = `${FileType.ChargingStationTemplate} read`
- const beginId = PerformanceStatistics.beginMeasure(measureId)
- template = JSON.parse(readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate
- PerformanceStatistics.endMeasure(measureId, beginId)
- template.templateHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(JSON.stringify(template))
- .digest('hex')
- this.sharedLRUCache.setChargingStationTemplate(template)
- this.templateFileHash = template.templateHash
+ throw new OCPPError(
+ ErrorType.PROTOCOL_ERROR,
+ 'Incoming message is not an array',
+ undefined,
+ {
+ request,
+ }
+ )
}
} catch (error) {
- handleFileException(
- this.templateFile,
- FileType.ChargingStationTemplate,
- error as NodeJS.ErrnoException,
- this.logPrefix()
+ if (!Array.isArray(request)) {
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
+ return
+ }
+ let commandName: IncomingRequestCommand | undefined
+ let requestCommandName: IncomingRequestCommand | RequestCommand | undefined
+ let errorCallback: ErrorCallback
+ const [, messageId] = request
+ switch (messageType) {
+ case MessageType.CALL_ERROR_MESSAGE:
+ case MessageType.CALL_RESULT_MESSAGE:
+ if (this.requests.has(messageId)) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ;[, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
+ // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
+ errorCallback(error as OCPPError, false)
+ } else {
+ // Remove the request from the cache in case of error at response handling
+ this.requests.delete(messageId)
+ }
+ break
+ case MessageType.CALL_MESSAGE:
+ ;[, , commandName] = request as IncomingRequest
+ // Send error
+ await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
+ break
+ }
+ if (!(error instanceof OCPPError)) {
+ logger.warn(
+ `${this.logPrefix()} Error thrown at incoming OCPP command ${
+ commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ } message '${data.toString()}' handling is not an OCPPError:`,
+ error
+ )
+ }
+ logger.error(
+ `${this.logPrefix()} Incoming OCPP command '${
+ commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
+ }' message '${data.toString()}'${
+ this.requests.has(messageId)
+ ? ` matching cached request '${JSON.stringify(
+ this.getCachedRequest(messageType, messageId)
+ )}'`
+ : ''
+ } processing error:`,
+ error
)
}
- return template
}
- private getStationInfoFromTemplate (): ChargingStationInfo {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const stationTemplate = this.getTemplateFromFile()!
- checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
- const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation)
- warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile)
- if (stationTemplate.Connectors != null) {
- checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
- }
- const stationInfo = stationTemplateToStationInfo(stationTemplate)
- stationInfo.hashId = getHashId(this.index, stationTemplate)
- stationInfo.templateIndex = this.index
- stationInfo.templateName = buildTemplateName(this.templateFile)
- stationInfo.chargingStationId = getChargingStationId(this.index, stationTemplate)
- createSerialNumber(stationTemplate, stationInfo)
- stationInfo.voltageOut = this.getVoltageOut(stationInfo)
- if (isNotEmptyArray(stationTemplate.power)) {
- const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
- stationInfo.maximumPower =
- stationTemplate.powerUnit === PowerUnits.KILO_WATT
- ? stationTemplate.power[powerArrayRandomIndex] * 1000
- : stationTemplate.power[powerArrayRandomIndex]
+ private async onOpen (): Promise<void> {
+ if (this.isWebSocketConnectionOpened()) {
+ this.emit(ChargingStationEvents.connected)
+ this.emit(ChargingStationEvents.updated)
+ logger.info(
+ `${this.logPrefix()} Connection to OCPP server through ${
+ this.wsConnectionUrl.href
+ } succeeded`
+ )
+ let registrationRetryCount = 0
+ if (!this.isRegistered()) {
+ // Send BootNotification
+ do {
+ await this.ocppRequestService.requestHandler<
+ BootNotificationRequest,
+ BootNotificationResponse
+ >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
+ skipBufferingOnError: true,
+ })
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.bootNotificationResponse!.currentTime = convertToDate(
+ this.bootNotificationResponse?.currentTime
+ )!
+ if (!this.isRegistered()) {
+ this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
+ await sleep(
+ this.bootNotificationResponse?.interval != null
+ ? secondsToMilliseconds(this.bootNotificationResponse.interval)
+ : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
+ )
+ }
+ } while (
+ !this.isRegistered() &&
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
+ this.stationInfo?.registrationMaxRetries === -1)
+ )
+ }
+ if (!this.isRegistered()) {
+ logger.error(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount.toString()}) or retry disabled (${this.stationInfo?.registrationMaxRetries?.toString()})`
+ )
+ }
+ this.emit(ChargingStationEvents.updated)
} else {
- stationInfo.maximumPower =
- stationTemplate.powerUnit === PowerUnits.KILO_WATT
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- stationTemplate.power! * 1000
- : stationTemplate.power
- }
- stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo)
- if (
- isNotEmptyString(stationInfo.firmwareVersionPattern) &&
- isNotEmptyString(stationInfo.firmwareVersion) &&
- !new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion)
- ) {
logger.warn(
- `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
- this.templateFile
- } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
+ `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
)
}
- if (stationTemplate.resetTime != null) {
- stationInfo.resetTime = secondsToMilliseconds(stationTemplate.resetTime)
- }
- return stationInfo
}
- private getStationInfoFromFile (
- stationInfoPersistentConfiguration: boolean | undefined = Constants.DEFAULT_STATION_INFO
- .stationInfoPersistentConfiguration
- ): ChargingStationInfo | undefined {
- let stationInfo: ChargingStationInfo | undefined
- if (stationInfoPersistentConfiguration) {
- stationInfo = this.getConfigurationFromFile()?.stationInfo
- if (stationInfo != null) {
- delete stationInfo.infoHash
- delete (stationInfo as ChargingStationTemplate).numberOfConnectors
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (stationInfo.templateIndex == null) {
- stationInfo.templateIndex = this.index
- }
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (stationInfo.templateName == null) {
- stationInfo.templateName = buildTemplateName(this.templateFile)
- }
- }
- }
- return stationInfo
+ private onPing (): void {
+ logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
}
- private getStationInfo (options?: ChargingStationOptions): ChargingStationInfo {
- const stationInfoFromTemplate = this.getStationInfoFromTemplate()
- options?.persistentConfiguration != null &&
- (stationInfoFromTemplate.stationInfoPersistentConfiguration = options.persistentConfiguration)
- const stationInfoFromFile = this.getStationInfoFromFile(
- stationInfoFromTemplate.stationInfoPersistentConfiguration
- )
- let stationInfo: ChargingStationInfo
- // Priority:
- // 1. charging station info from template
- // 2. charging station info from configuration file
+ private onPong (): void {
+ logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
+ }
+
+ private async reconnect (): Promise<void> {
if (
- stationInfoFromFile != null &&
- stationInfoFromFile.templateHash === stationInfoFromTemplate.templateHash
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
+ this.stationInfo?.autoReconnectMaxRetries === -1
) {
- stationInfo = stationInfoFromFile
+ ++this.wsConnectionRetryCount
+ const reconnectDelay =
+ this.stationInfo?.reconnectExponentialDelay === true
+ ? exponentialDelay(this.wsConnectionRetryCount)
+ : secondsToMilliseconds(this.getConnectionTimeout())
+ const reconnectDelayWithdraw = 1000
+ const reconnectTimeout =
+ reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
+ logger.error(
+ `${this.logPrefix()} WebSocket connection retry in ${roundTo(
+ reconnectDelay,
+ 2
+ ).toString()}ms, timeout ${reconnectTimeout.toString()}ms`
+ )
+ await sleep(reconnectDelay)
+ logger.error(
+ `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
+ )
+ this.openWSConnection(
+ {
+ handshakeTimeout: reconnectTimeout,
+ },
+ { closeOpened: true }
+ )
+ } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
+ logger.error(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
+ )
+ }
+ }
+
+ private saveAutomaticTransactionGeneratorConfiguration (): void {
+ if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
+ this.saveConfiguration()
+ }
+ }
+
+ private saveConfiguration (): void {
+ if (isNotEmptyString(this.configurationFile)) {
+ try {
+ if (!existsSync(dirname(this.configurationFile))) {
+ mkdirSync(dirname(this.configurationFile), { recursive: true })
+ }
+ const configurationFromFile = this.getConfigurationFromFile()
+ let configurationData: ChargingStationConfiguration =
+ configurationFromFile != null
+ ? clone<ChargingStationConfiguration>(configurationFromFile)
+ : {}
+ if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
+ configurationData.stationInfo = this.stationInfo
+ } else {
+ delete configurationData.stationInfo
+ }
+ if (
+ this.stationInfo?.ocppPersistentConfiguration === true &&
+ Array.isArray(this.ocppConfiguration?.configurationKey)
+ ) {
+ configurationData.configurationKey = this.ocppConfiguration.configurationKey
+ } else {
+ delete configurationData.configurationKey
+ }
+ configurationData = mergeDeepRight(
+ configurationData,
+ buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
+ )
+ if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
+ delete configurationData.automaticTransactionGenerator
+ }
+ if (this.connectors.size > 0) {
+ configurationData.connectorsStatus = buildConnectorsStatus(this)
+ } else {
+ delete configurationData.connectorsStatus
+ }
+ if (this.evses.size > 0) {
+ configurationData.evsesStatus = buildEvsesStatus(this)
+ } else {
+ delete configurationData.evsesStatus
+ }
+ delete configurationData.configurationHash
+ const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
+ .update(
+ JSON.stringify({
+ automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
+ configurationKey: configurationData.configurationKey,
+ stationInfo: configurationData.stationInfo,
+ ...(this.connectors.size > 0 && {
+ connectorsStatus: configurationData.connectorsStatus,
+ }),
+ ...(this.evses.size > 0 && {
+ evsesStatus: configurationData.evsesStatus,
+ }),
+ } satisfies ChargingStationConfiguration)
+ )
+ .digest('hex')
+ if (this.configurationFileHash !== configurationHash) {
+ AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+ configurationData.configurationHash = configurationHash
+ const measureId = `${FileType.ChargingStationConfiguration} write`
+ const beginId = PerformanceStatistics.beginMeasure(measureId)
+ writeFileSync(
+ this.configurationFile,
+ JSON.stringify(configurationData, undefined, 2),
+ 'utf8'
+ )
+ PerformanceStatistics.endMeasure(measureId, beginId)
+ this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
+ this.sharedLRUCache.setChargingStationConfiguration(configurationData)
+ this.configurationFileHash = configurationHash
+ }).catch((error: unknown) => {
+ handleFileException(
+ this.configurationFile,
+ FileType.ChargingStationConfiguration,
+ error as NodeJS.ErrnoException,
+ this.logPrefix()
+ )
+ })
+ } else {
+ logger.debug(
+ `${this.logPrefix()} Not saving unchanged charging station configuration file ${
+ this.configurationFile
+ }`
+ )
+ }
+ } catch (error) {
+ handleFileException(
+ this.configurationFile,
+ FileType.ChargingStationConfiguration,
+ error as NodeJS.ErrnoException,
+ this.logPrefix()
+ )
+ }
} else {
- stationInfo = stationInfoFromTemplate
- stationInfoFromFile != null &&
- propagateSerialNumber(this.getTemplateFromFile(), stationInfoFromFile, stationInfo)
+ logger.error(
+ `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
+ )
}
- return setChargingStationOptions(
- mergeDeepRight(Constants.DEFAULT_STATION_INFO, stationInfo),
- options
- )
+ }
+
+ private saveConnectorsStatus (): void {
+ this.saveConfiguration()
+ }
+
+ private saveEvsesStatus (): void {
+ this.saveConfiguration()
}
private saveStationInfo (): void {
}
}
- private handleUnsupportedVersion (version: OCPPVersion | undefined): void {
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
+ private setIntervalFlushMessageBuffer (): void {
+ if (this.flushMessageBufferSetInterval == null) {
+ this.flushMessageBufferSetInterval = setInterval(() => {
+ if (this.isWebSocketConnectionOpened() && this.inAcceptedState()) {
+ this.flushMessageBuffer()
+ }
+ if (this.messageBuffer.size === 0) {
+ this.clearIntervalFlushMessageBuffer()
+ }
+ }, Constants.DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL)
+ }
}
- private initialize (options?: ChargingStationOptions): void {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const stationTemplate = this.getTemplateFromFile()!
- checkTemplate(stationTemplate, this.logPrefix(), this.templateFile)
- this.configurationFile = join(
- dirname(this.templateFile.replace('station-templates', 'configurations')),
- `${getHashId(this.index, stationTemplate)}.json`
- )
- const stationConfiguration = this.getConfigurationFromFile()
- if (
- stationConfiguration?.stationInfo?.templateHash === stationTemplate.templateHash &&
- (stationConfiguration?.connectorsStatus != null || stationConfiguration?.evsesStatus != null)
- ) {
- checkConfiguration(stationConfiguration, this.logPrefix(), this.configurationFile)
- this.initializeConnectorsOrEvsesFromFile(stationConfiguration)
- } else {
- this.initializeConnectorsOrEvsesFromTemplate(stationTemplate)
+ private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
+ if (this.stationInfo?.autoRegister === true) {
+ await this.ocppRequestService.requestHandler<
+ BootNotificationRequest,
+ BootNotificationResponse
+ >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
+ skipBufferingOnError: true,
+ })
}
- this.stationInfo = this.getStationInfo(options)
- validateStationInfo(this)
- if (
- this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
- isNotEmptyString(this.stationInfo.firmwareVersionPattern) &&
- isNotEmptyString(this.stationInfo.firmwareVersion)
- ) {
- const patternGroup =
- this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
- this.stationInfo.firmwareVersion.split('.').length
- const match = new RegExp(this.stationInfo.firmwareVersionPattern)
- .exec(this.stationInfo.firmwareVersion)
- ?.slice(1, patternGroup + 1)
- if (match != null) {
- const patchLevelIndex = match.length - 1
- match[patchLevelIndex] = (
- convertToInt(match[patchLevelIndex]) +
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.stationInfo.firmwareUpgrade!.versionUpgrade!.step!
- ).toString()
- this.stationInfo.firmwareVersion = match.join('.')
- }
+ // Start WebSocket ping
+ if (this.wsPingSetInterval == null) {
+ this.startWebSocketPing()
}
- this.saveStationInfo()
- this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
- if (this.stationInfo.enableStatistics === true) {
- this.performanceStatistics = PerformanceStatistics.getInstance(
- this.stationInfo.hashId,
- this.stationInfo.chargingStationId,
- this.configuredSupervisionUrl
- )
- }
- const bootNotificationRequest = createBootNotificationRequest(this.stationInfo)
- if (bootNotificationRequest == null) {
- const errorMsg = 'Error while creating boot notification request'
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
+ // Start heartbeat
+ if (this.heartbeatSetInterval == null) {
+ this.startHeartbeat()
}
- this.bootNotificationRequest = bootNotificationRequest
- this.powerDivider = this.getPowerDivider()
- // OCPP configuration
- this.ocppConfiguration = this.getOcppConfiguration(options?.persistentConfiguration)
- this.initializeOcppConfiguration()
- this.initializeOcppServices()
- if (this.stationInfo.autoRegister === true) {
- this.bootNotificationResponse = {
- currentTime: new Date(),
- interval: millisecondsToSeconds(this.getHeartbeatInterval()),
- status: RegistrationStatusEnumType.ACCEPTED,
+ // Initialize connectors status
+ if (this.hasEvses) {
+ for (const [evseId, evseStatus] of this.evses) {
+ if (evseId > 0) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ await sendAndSetConnectorStatus(
+ this,
+ connectorId,
+ getBootConnectorStatus(this, connectorId, connectorStatus),
+ evseId
+ )
+ }
+ }
+ }
+ } else {
+ for (const connectorId of this.connectors.keys()) {
+ if (connectorId > 0) {
+ await sendAndSetConnectorStatus(
+ this,
+ connectorId,
+ getBootConnectorStatus(
+ this,
+ connectorId,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.getConnectorStatus(connectorId)!
+ )
+ )
+ }
}
}
- }
+ if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
+ await this.ocppRequestService.requestHandler<
+ FirmwareStatusNotificationRequest,
+ FirmwareStatusNotificationResponse
+ >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: FirmwareStatus.Installed,
+ })
+ this.stationInfo.firmwareStatus = FirmwareStatus.Installed
+ }
- private initializeOcppServices (): void {
- const ocppVersion = this.stationInfo?.ocppVersion
- switch (ocppVersion) {
- case OCPPVersion.VERSION_16:
- this.ocppIncomingRequestService =
- OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>()
- this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
- OCPP16ResponseService.getInstance<OCPP16ResponseService>()
- )
- break
- case OCPPVersion.VERSION_20:
- case OCPPVersion.VERSION_201:
- this.ocppIncomingRequestService =
- OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>()
- this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
- OCPP20ResponseService.getInstance<OCPP20ResponseService>()
- )
- break
- default:
- this.handleUnsupportedVersion(ocppVersion)
- break
+ // Start the ATG
+ if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+ this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
}
+ this.flushMessageBuffer()
}
- private initializeOcppConfiguration (): void {
- if (getConfigurationKey(this, StandardParametersKey.HeartbeatInterval) == null) {
- addConfigurationKey(this, StandardParametersKey.HeartbeatInterval, '0')
- }
- if (getConfigurationKey(this, StandardParametersKey.HeartBeatInterval) == null) {
- addConfigurationKey(this, StandardParametersKey.HeartBeatInterval, '0', {
- visible: false,
- })
- }
- if (
- this.stationInfo?.supervisionUrlOcppConfiguration === true &&
- isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
- getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) == null
- ) {
- addConfigurationKey(
- this,
- this.stationInfo.supervisionUrlOcppKey,
- this.configuredSupervisionUrl.href,
- { reboot: true }
+ private startWebSocketPing (): void {
+ const webSocketPingInterval = this.getWebSocketPingInterval()
+ if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
+ this.wsPingSetInterval = setInterval(() => {
+ if (this.isWebSocketConnectionOpened()) {
+ this.wsConnection?.ping()
+ }
+ }, secondsToMilliseconds(webSocketPingInterval))
+ logger.info(
+ `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
+ webSocketPingInterval
+ )}`
)
- } else if (
- this.stationInfo?.supervisionUrlOcppConfiguration === false &&
- isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
- getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey) != null
- ) {
- deleteConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey, {
- save: false,
- })
- }
- if (
- isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
- getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) == null
- ) {
- addConfigurationKey(
- this,
- this.stationInfo.amperageLimitationOcppKey,
- // prettier-ignore
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (this.stationInfo.maximumAmperage! * getAmperageLimitationUnitDivider(this.stationInfo)).toString()
+ } else if (this.wsPingSetInterval != null) {
+ logger.info(
+ `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
+ webSocketPingInterval
+ )}`
)
- }
- if (getConfigurationKey(this, StandardParametersKey.SupportedFeatureProfiles) == null) {
- addConfigurationKey(
- this,
- StandardParametersKey.SupportedFeatureProfiles,
- `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
+ } else {
+ logger.error(
+ `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping`
)
}
- addConfigurationKey(
- this,
- StandardParametersKey.NumberOfConnectors,
- this.getNumberOfConnectors().toString(),
- { readonly: true },
- { overwrite: true }
- )
- if (getConfigurationKey(this, StandardParametersKey.MeterValuesSampledData) == null) {
- addConfigurationKey(
- this,
- StandardParametersKey.MeterValuesSampledData,
- MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
- )
+ }
+
+ private stopHeartbeat (): void {
+ if (this.heartbeatSetInterval != null) {
+ clearInterval(this.heartbeatSetInterval)
+ delete this.heartbeatSetInterval
}
- if (getConfigurationKey(this, StandardParametersKey.ConnectorPhaseRotation) == null) {
- const connectorsPhaseRotation: string[] = []
- if (this.hasEvses) {
- for (const evseStatus of this.evses.values()) {
- for (const connectorId of evseStatus.connectors.keys()) {
- connectorsPhaseRotation.push(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
+ }
+
+ private async stopMessageSequence (
+ reason?: StopTransactionReason,
+ stopTransactions?: boolean
+ ): Promise<void> {
+ this.internalStopMessageSequence()
+ // Stop ongoing transactions
+ stopTransactions && (await this.stopRunningTransactions(reason))
+ if (this.hasEvses) {
+ for (const [evseId, evseStatus] of this.evses) {
+ if (evseId > 0) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ await sendAndSetConnectorStatus(
+ this,
+ connectorId,
+ ConnectorStatusEnum.Unavailable,
+ evseId
)
+ delete connectorStatus.status
}
}
- } else {
- for (const connectorId of this.connectors.keys()) {
- connectorsPhaseRotation.push(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- getPhaseRotationValue(connectorId, this.getNumberOfPhases())!
- )
+ }
+ } else {
+ for (const connectorId of this.connectors.keys()) {
+ if (connectorId > 0) {
+ await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
+ delete this.getConnectorStatus(connectorId)?.status
}
}
- addConfigurationKey(
- this,
- StandardParametersKey.ConnectorPhaseRotation,
- connectorsPhaseRotation.toString()
- )
- }
- if (getConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests) == null) {
- addConfigurationKey(this, StandardParametersKey.AuthorizeRemoteTxRequests, 'true')
- }
- if (
- getConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled) == null &&
- hasFeatureProfile(this, SupportedFeatureProfiles.LocalAuthListManagement) === true
- ) {
- addConfigurationKey(this, StandardParametersKey.LocalAuthListEnabled, 'false')
- }
- if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) == null) {
- addConfigurationKey(
- this,
- StandardParametersKey.ConnectionTimeOut,
- Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
- )
}
- this.saveOcppConfiguration()
}
- private initializeConnectorsOrEvsesFromFile (configuration: ChargingStationConfiguration): void {
- if (configuration.connectorsStatus != null && configuration.evsesStatus == null) {
- for (const [connectorId, connectorStatus] of configuration.connectorsStatus.entries()) {
- this.connectors.set(
- connectorId,
- prepareConnectorStatus(clone<ConnectorStatus>(connectorStatus))
- )
+ private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
+ if (this.hasEvses) {
+ for (const [evseId, evseStatus] of this.evses) {
+ if (evseId === 0) {
+ continue
+ }
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ if (connectorStatus.transactionStarted === true) {
+ await this.stopTransactionOnConnector(connectorId, reason)
+ }
+ }
}
- } else if (configuration.evsesStatus != null && configuration.connectorsStatus == null) {
- for (const [evseId, evseStatusConfiguration] of configuration.evsesStatus.entries()) {
- const evseStatus = clone<EvseStatusConfiguration>(evseStatusConfiguration)
- delete evseStatus.connectorsStatus
- this.evses.set(evseId, {
- ...(evseStatus as EvseStatus),
- connectors: new Map<number, ConnectorStatus>(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- evseStatusConfiguration.connectorsStatus!.map((connectorStatus, connectorId) => [
- connectorId,
- prepareConnectorStatus(connectorStatus),
- ])
- ),
- })
- }
- } else if (configuration.evsesStatus != null && configuration.connectorsStatus != null) {
- const errorMsg = `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
} else {
- const errorMsg = `No connectors or evses defined in configuration file ${this.configurationFile}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
+ for (const connectorId of this.connectors.keys()) {
+ if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
+ await this.stopTransactionOnConnector(connectorId, reason)
+ }
+ }
}
}
- private initializeConnectorsOrEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
- if (stationTemplate.Connectors != null && stationTemplate.Evses == null) {
- this.initializeConnectorsFromTemplate(stationTemplate)
- } else if (stationTemplate.Evses != null && stationTemplate.Connectors == null) {
- this.initializeEvsesFromTemplate(stationTemplate)
- } else if (stationTemplate.Evses != null && stationTemplate.Connectors != null) {
- const errorMsg = `Connectors and evses defined at the same time in template file ${this.templateFile}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
- } else {
- const errorMsg = `No connectors or evses defined in template file ${this.templateFile}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
+ private stopWebSocketPing (): void {
+ if (this.wsPingSetInterval != null) {
+ clearInterval(this.wsPingSetInterval)
+ delete this.wsPingSetInterval
}
}
- private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
- if (stationTemplate.Connectors == null && this.connectors.size === 0) {
- const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
- }
- if (stationTemplate.Connectors?.[0] == null) {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no connector id 0 configuration`
- )
- }
- if (stationTemplate.Connectors != null) {
- const { configuredMaxConnectors, templateMaxConnectors, templateMaxAvailableConnectors } =
- checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile)
- const connectorsConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(
- `${JSON.stringify(stationTemplate.Connectors)}${configuredMaxConnectors.toString()}`
- )
- .digest('hex')
- const connectorsConfigChanged =
- this.connectors.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash
- if (this.connectors.size === 0 || connectorsConfigChanged) {
- connectorsConfigChanged && this.connectors.clear()
- this.connectorsConfigurationHash = connectorsConfigHash
- if (templateMaxConnectors > 0) {
- for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
- if (
- connectorId === 0 &&
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- (stationTemplate.Connectors[connectorId] == null ||
- !this.getUseConnectorId0(stationTemplate))
- ) {
- continue
- }
- const templateConnectorId =
- connectorId > 0 && stationTemplate.randomConnectors === true
- ? randomInt(1, templateMaxAvailableConnectors)
- : connectorId
- const connectorStatus = stationTemplate.Connectors[templateConnectorId]
- checkStationInfoConnectorStatus(
- templateConnectorId,
- connectorStatus,
- this.logPrefix(),
- this.templateFile
- )
- this.connectors.set(connectorId, clone<ConnectorStatus>(connectorStatus))
- }
- initializeConnectorsMapStatus(this.connectors, this.logPrefix())
- this.saveConnectorsStatus()
- } else {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no connectors configuration defined, cannot create connectors`
- )
- }
- }
- } else {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no connectors configuration defined, using already defined connectors`
- )
+ private terminateWSConnection (): void {
+ if (this.isWebSocketConnectionOpened()) {
+ this.wsConnection?.terminate()
+ this.wsConnection = null
}
}
- private initializeEvsesFromTemplate (stationTemplate: ChargingStationTemplate): void {
- if (stationTemplate.Evses == null && this.evses.size === 0) {
- const errorMsg = `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
- }
- if (stationTemplate.Evses?.[0] == null) {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no evse id 0 configuration`
- )
- }
- if (stationTemplate.Evses?.[0]?.Connectors[0] == null) {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with evse id 0 with no connector id 0 configuration`
- )
- }
- if (Object.keys(stationTemplate.Evses?.[0]?.Connectors as object).length > 1) {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`
- )
- }
- if (stationTemplate.Evses != null) {
- const evsesConfigHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(JSON.stringify(stationTemplate.Evses))
- .digest('hex')
- const evsesConfigChanged =
- this.evses.size !== 0 && this.evsesConfigurationHash !== evsesConfigHash
- if (this.evses.size === 0 || evsesConfigChanged) {
- evsesConfigChanged && this.evses.clear()
- this.evsesConfigurationHash = evsesConfigHash
- const templateMaxEvses = getMaxNumberOfEvses(stationTemplate.Evses)
- if (templateMaxEvses > 0) {
- for (const evseKey in stationTemplate.Evses) {
- const evseId = convertToInt(evseKey)
- this.evses.set(evseId, {
- connectors: buildConnectorsMap(
- stationTemplate.Evses[evseKey].Connectors,
- this.logPrefix(),
- this.templateFile
- ),
- availability: AvailabilityType.Operative,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- initializeConnectorsMapStatus(this.evses.get(evseId)!.connectors, this.logPrefix())
- }
- this.saveEvsesStatus()
- } else {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no evses configuration defined, cannot create evses`
- )
- }
- }
- } else {
- logger.warn(
- `${this.logPrefix()} Charging station information from template ${
- this.templateFile
- } with no evses configuration defined, using already defined evses`
- )
+ public async addReservation (reservation: Reservation): Promise<void> {
+ const reservationFound = this.getReservationBy('reservationId', reservation.reservationId)
+ if (reservationFound != null) {
+ await this.removeReservation(reservationFound, ReservationTerminationReason.REPLACE_EXISTING)
}
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.getConnectorStatus(reservation.connectorId)!.reservation = reservation
+ await sendAndSetConnectorStatus(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Reserved,
+ undefined,
+ { send: reservation.connectorId !== 0 }
+ )
}
- private getConfigurationFromFile (): ChargingStationConfiguration | undefined {
- let configuration: ChargingStationConfiguration | undefined
- if (isNotEmptyString(this.configurationFile) && existsSync(this.configurationFile)) {
- try {
- if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
- configuration = this.sharedLRUCache.getChargingStationConfiguration(
- this.configurationFileHash
- )
- } else {
- const measureId = `${FileType.ChargingStationConfiguration} read`
- const beginId = PerformanceStatistics.beginMeasure(measureId)
- configuration = JSON.parse(
- readFileSync(this.configurationFile, 'utf8')
- ) as ChargingStationConfiguration
- PerformanceStatistics.endMeasure(measureId, beginId)
- this.sharedLRUCache.setChargingStationConfiguration(configuration)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.configurationFileHash = configuration.configurationHash!
- }
- } catch (error) {
- handleFileException(
- this.configurationFile,
- FileType.ChargingStationConfiguration,
- error as NodeJS.ErrnoException,
- this.logPrefix()
- )
- }
- }
- return configuration
+ public bufferMessage (message: string): void {
+ this.messageBuffer.add(message)
+ this.setIntervalFlushMessageBuffer()
}
- private saveAutomaticTransactionGeneratorConfiguration (): void {
- if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true) {
- this.saveConfiguration()
+ public closeWSConnection (): void {
+ if (this.isWebSocketConnectionOpened()) {
+ this.wsConnection?.close()
+ this.wsConnection = null
}
}
- private saveConnectorsStatus (): void {
- this.saveConfiguration()
+ public async delete (deleteConfiguration = true): Promise<void> {
+ if (this.started) {
+ await this.stop()
+ }
+ AutomaticTransactionGenerator.deleteInstance(this)
+ PerformanceStatistics.deleteInstance(this.stationInfo?.hashId)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+ this.requests.clear()
+ this.connectors.clear()
+ this.evses.clear()
+ this.templateFileWatcher?.unref()
+ deleteConfiguration && rmSync(this.configurationFile, { force: true })
+ this.chargingStationWorkerBroadcastChannel.unref()
+ this.emit(ChargingStationEvents.deleted)
+ this.removeAllListeners()
}
- private saveEvsesStatus (): void {
- this.saveConfiguration()
+ public getAuthorizeRemoteTxRequests (): boolean {
+ const authorizeRemoteTxRequests = getConfigurationKey(
+ this,
+ StandardParametersKey.AuthorizeRemoteTxRequests
+ )
+ return authorizeRemoteTxRequests != null
+ ? convertToBoolean(authorizeRemoteTxRequests.value)
+ : false
}
- private saveConfiguration (): void {
- if (isNotEmptyString(this.configurationFile)) {
- try {
- if (!existsSync(dirname(this.configurationFile))) {
- mkdirSync(dirname(this.configurationFile), { recursive: true })
- }
- const configurationFromFile = this.getConfigurationFromFile()
- let configurationData: ChargingStationConfiguration =
- configurationFromFile != null
- ? clone<ChargingStationConfiguration>(configurationFromFile)
- : {}
- if (this.stationInfo?.stationInfoPersistentConfiguration === true) {
- configurationData.stationInfo = this.stationInfo
- } else {
- delete configurationData.stationInfo
- }
- if (
- this.stationInfo?.ocppPersistentConfiguration === true &&
- Array.isArray(this.ocppConfiguration?.configurationKey)
- ) {
- configurationData.configurationKey = this.ocppConfiguration.configurationKey
- } else {
- delete configurationData.configurationKey
- }
- configurationData = mergeDeepRight(
- configurationData,
- buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
- )
- if (this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration !== true) {
- delete configurationData.automaticTransactionGenerator
- }
- if (this.connectors.size > 0) {
- configurationData.connectorsStatus = buildConnectorsStatus(this)
- } else {
- delete configurationData.connectorsStatus
- }
- if (this.evses.size > 0) {
- configurationData.evsesStatus = buildEvsesStatus(this)
- } else {
- delete configurationData.evsesStatus
- }
- delete configurationData.configurationHash
- const configurationHash = createHash(Constants.DEFAULT_HASH_ALGORITHM)
- .update(
- JSON.stringify({
- stationInfo: configurationData.stationInfo,
- configurationKey: configurationData.configurationKey,
- automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
- ...(this.connectors.size > 0 && {
- connectorsStatus: configurationData.connectorsStatus,
- }),
- ...(this.evses.size > 0 && {
- evsesStatus: configurationData.evsesStatus,
- }),
- } satisfies ChargingStationConfiguration)
- )
- .digest('hex')
- if (this.configurationFileHash !== configurationHash) {
- AsyncLock.runExclusive(AsyncLockType.configuration, () => {
- configurationData.configurationHash = configurationHash
- const measureId = `${FileType.ChargingStationConfiguration} write`
- const beginId = PerformanceStatistics.beginMeasure(measureId)
- writeFileSync(
- this.configurationFile,
- JSON.stringify(configurationData, undefined, 2),
- 'utf8'
- )
- PerformanceStatistics.endMeasure(measureId, beginId)
- this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
- this.sharedLRUCache.setChargingStationConfiguration(configurationData)
- this.configurationFileHash = configurationHash
- }).catch((error: unknown) => {
- handleFileException(
- this.configurationFile,
- FileType.ChargingStationConfiguration,
- error as NodeJS.ErrnoException,
- this.logPrefix()
- )
- })
- } else {
- logger.debug(
- `${this.logPrefix()} Not saving unchanged charging station configuration file ${
- this.configurationFile
- }`
- )
- }
- } catch (error) {
- handleFileException(
- this.configurationFile,
- FileType.ChargingStationConfiguration,
- error as NodeJS.ErrnoException,
- this.logPrefix()
- )
+ public getAutomaticTransactionGeneratorConfiguration ():
+ | AutomaticTransactionGeneratorConfiguration
+ | undefined {
+ if (this.automaticTransactionGeneratorConfiguration == null) {
+ let automaticTransactionGeneratorConfiguration:
+ | AutomaticTransactionGeneratorConfiguration
+ | undefined
+ const stationTemplate = this.getTemplateFromFile()
+ const stationConfiguration = this.getConfigurationFromFile()
+ if (
+ this.stationInfo?.automaticTransactionGeneratorPersistentConfiguration === true &&
+ stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
+ stationConfiguration?.automaticTransactionGenerator != null
+ ) {
+ automaticTransactionGeneratorConfiguration =
+ stationConfiguration.automaticTransactionGenerator
+ } else {
+ automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator
+ }
+ this.automaticTransactionGeneratorConfiguration = {
+ ...Constants.DEFAULT_ATG_CONFIGURATION,
+ ...automaticTransactionGeneratorConfiguration,
}
- } else {
- logger.error(
- `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
- )
- }
- }
-
- private getOcppConfigurationFromTemplate (): ChargingStationOcppConfiguration | undefined {
- return this.getTemplateFromFile()?.Configuration
- }
-
- private getOcppConfigurationFromFile (
- ocppPersistentConfiguration?: boolean
- ): ChargingStationOcppConfiguration | undefined {
- const configurationKey = this.getConfigurationFromFile()?.configurationKey
- if (ocppPersistentConfiguration && Array.isArray(configurationKey)) {
- return { configurationKey }
}
- return undefined
+ return this.automaticTransactionGeneratorConfiguration
}
- private getOcppConfiguration (
- ocppPersistentConfiguration: boolean | undefined = this.stationInfo?.ocppPersistentConfiguration
- ): ChargingStationOcppConfiguration | undefined {
- let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
- this.getOcppConfigurationFromFile(ocppPersistentConfiguration)
- if (ocppConfiguration == null) {
- ocppConfiguration = this.getOcppConfigurationFromTemplate()
- }
- return ocppConfiguration
+ public getAutomaticTransactionGeneratorStatuses (): Status[] | undefined {
+ return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
}
- private async onOpen (): Promise<void> {
- if (this.isWebSocketConnectionOpened()) {
- this.emit(ChargingStationEvents.connected)
- this.emit(ChargingStationEvents.updated)
- logger.info(
- `${this.logPrefix()} Connection to OCPP server through ${
- this.wsConnectionUrl.href
- } succeeded`
- )
- let registrationRetryCount = 0
- if (!this.isRegistered()) {
- // Send BootNotification
- do {
- await this.ocppRequestService.requestHandler<
- BootNotificationRequest,
- BootNotificationResponse
- >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
- skipBufferingOnError: true,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.bootNotificationResponse!.currentTime = convertToDate(
- this.bootNotificationResponse?.currentTime
- )!
- if (!this.isRegistered()) {
- this.stationInfo?.registrationMaxRetries !== -1 && ++registrationRetryCount
- await sleep(
- this.bootNotificationResponse?.interval != null
- ? secondsToMilliseconds(this.bootNotificationResponse.interval)
- : Constants.DEFAULT_BOOT_NOTIFICATION_INTERVAL
- )
+ public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
+ if (transactionId == null) {
+ return undefined
+ } else if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ if (connectorStatus.transactionId === transactionId) {
+ return connectorId
}
- } while (
- !this.isRegistered() &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (registrationRetryCount <= this.stationInfo!.registrationMaxRetries! ||
- this.stationInfo?.registrationMaxRetries === -1)
- )
- }
- if (!this.isRegistered()) {
- logger.error(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix()} Registration failure: maximum retries reached (${registrationRetryCount.toString()}) or retry disabled (${this.stationInfo?.registrationMaxRetries?.toString()})`
- )
+ }
}
- this.emit(ChargingStationEvents.updated)
} else {
- logger.warn(
- `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.href} failed`
- )
- }
- }
-
- private onClose (code: WebSocketCloseEventStatusCode, reason: Buffer): void {
- this.emit(ChargingStationEvents.disconnected)
- this.emit(ChargingStationEvents.updated)
- switch (code) {
- // Normal close
- case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
- case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
- logger.info(
- `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
- code
- )}' and reason '${reason.toString()}'`
- )
- this.wsConnectionRetryCount = 0
- break
- // Abnormal close
- default:
- logger.error(
- `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
- code
- )}' and reason '${reason.toString()}'`
- )
- this.started &&
- this.reconnect()
- .then(() => {
- this.emit(ChargingStationEvents.updated)
- return undefined
- })
- .catch((error: unknown) =>
- logger.error(`${this.logPrefix()} Error while reconnecting:`, error)
- )
- break
+ for (const connectorId of this.connectors.keys()) {
+ if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
+ return connectorId
+ }
+ }
}
}
- private getCachedRequest (
- messageType: MessageType | undefined,
- messageId: string
- ): CachedRequest | undefined {
- const cachedRequest = this.requests.get(messageId)
- if (Array.isArray(cachedRequest)) {
- return cachedRequest
+ public getConnectorMaximumAvailablePower (connectorId: number): number {
+ let connectorAmperageLimitationLimit: number | undefined
+ const amperageLimitation = this.getAmperageLimitation()
+ if (
+ amperageLimitation != null &&
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ amperageLimitation < this.stationInfo!.maximumAmperage!
+ ) {
+ connectorAmperageLimitationLimit =
+ (this.stationInfo?.currentOutType === CurrentType.AC
+ ? ACElectricUtils.powerTotal(
+ this.getNumberOfPhases(),
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.stationInfo.voltageOut!,
+ amperageLimitation *
+ (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors())
+ )
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ DCElectricUtils.power(this.stationInfo!.voltageOut!, amperageLimitation)) /
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.powerDivider!
}
- throw new OCPPError(
- ErrorType.PROTOCOL_ERROR,
- `Cached request for message id '${messageId}' ${getMessageTypeString(
- messageType
- )} is not an array`,
- undefined,
- cachedRequest
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const connectorMaximumPower = this.stationInfo!.maximumPower! / this.powerDivider!
+ const chargingStationChargingProfilesLimit =
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ getChargingStationChargingProfilesLimit(this)! / this.powerDivider!
+ const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId)
+ return min(
+ isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ isNaN(connectorAmperageLimitationLimit!)
+ ? Number.POSITIVE_INFINITY
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ connectorAmperageLimitationLimit!,
+ isNaN(chargingStationChargingProfilesLimit)
+ ? Number.POSITIVE_INFINITY
+ : chargingStationChargingProfilesLimit,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ isNaN(connectorChargingProfilesLimit!)
+ ? Number.POSITIVE_INFINITY
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ connectorChargingProfilesLimit!
)
}
- private async handleIncomingMessage (request: IncomingRequest): Promise<void> {
- const [messageType, messageId, commandName, commandPayload] = request
- if (this.requests.has(messageId)) {
- throw new OCPPError(
- ErrorType.SECURITY_ERROR,
- `Received message with duplicate message id '${messageId}'`,
- commandName,
- commandPayload
- )
- }
- if (this.stationInfo?.enableStatistics === true) {
- this.performanceStatistics?.addRequestStatistic(commandName, messageType)
+ public getConnectorStatus (connectorId: number): ConnectorStatus | undefined {
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ if (evseStatus.connectors.has(connectorId)) {
+ return evseStatus.connectors.get(connectorId)
+ }
+ }
+ return undefined
}
- logger.debug(
- `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
- request
- )}`
- )
- // Process the message
- await this.ocppIncomingRequestService.incomingRequestHandler(
- this,
- messageId,
- commandName,
- commandPayload
- )
- this.emit(ChargingStationEvents.updated)
+ return this.connectors.get(connectorId)
}
- private handleResponseMessage (response: Response): void {
- const [messageType, messageId, commandPayload] = response
- if (!this.requests.has(messageId)) {
- // Error
- throw new OCPPError(
- ErrorType.INTERNAL_ERROR,
- `Response for unknown message id '${messageId}'`,
- undefined,
- commandPayload
- )
- }
- // Respond
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const [responseCallback, , requestCommandName, requestPayload] = this.getCachedRequest(
- messageType,
- messageId
- )!
- logger.debug(
- `${this.logPrefix()} << Command '${requestCommandName}' received response payload: ${JSON.stringify(
- response
- )}`
- )
- responseCallback(commandPayload, requestPayload)
+ public getEnergyActiveImportRegisterByConnectorId (connectorId: number, rounded = false): number {
+ return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded)
}
- private handleErrorMessage (errorResponse: ErrorResponse): void {
- const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
- if (!this.requests.has(messageId)) {
- // Error
- throw new OCPPError(
- ErrorType.INTERNAL_ERROR,
- `Error response for unknown message id '${messageId}'`,
- undefined,
- { errorType, errorMessage, errorDetails }
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const [, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
- logger.debug(
- `${this.logPrefix()} << Command '${requestCommandName}' received error response payload: ${JSON.stringify(
- errorResponse
- )}`
+ public getEnergyActiveImportRegisterByTransactionId (
+ transactionId: number | undefined,
+ rounded = false
+ ): number {
+ return this.getEnergyActiveImportRegister(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)!),
+ rounded
)
- errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails))
}
- private async onMessage (data: RawData): Promise<void> {
- let request: IncomingRequest | Response | ErrorResponse | undefined
- let messageType: MessageType | undefined
- let errorMsg: string
- try {
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse
- if (Array.isArray(request)) {
- ;[messageType] = request
- // Check the type of message
- switch (messageType) {
- // Incoming Message
- case MessageType.CALL_MESSAGE:
- await this.handleIncomingMessage(request as IncomingRequest)
- break
- // Response Message
- case MessageType.CALL_RESULT_MESSAGE:
- this.handleResponseMessage(request as Response)
- break
- // Error Message
- case MessageType.CALL_ERROR_MESSAGE:
- this.handleErrorMessage(request as ErrorResponse)
- break
- // Unknown Message
- default:
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- errorMsg = `Wrong message type ${messageType}`
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg)
- }
- } else {
- throw new OCPPError(
- ErrorType.PROTOCOL_ERROR,
- 'Incoming message is not an array',
- undefined,
- {
- request,
- }
- )
- }
- } catch (error) {
- if (!Array.isArray(request)) {
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- logger.error(`${this.logPrefix()} Incoming message '${request}' parsing error:`, error)
- return
- }
- let commandName: IncomingRequestCommand | undefined
- let requestCommandName: RequestCommand | IncomingRequestCommand | undefined
- let errorCallback: ErrorCallback
- const [, messageId] = request
- switch (messageType) {
- case MessageType.CALL_MESSAGE:
- ;[, , commandName] = request as IncomingRequest
- // Send error
- await this.ocppRequestService.sendError(this, messageId, error as OCPPError, commandName)
- break
- case MessageType.CALL_RESULT_MESSAGE:
- case MessageType.CALL_ERROR_MESSAGE:
- if (this.requests.has(messageId)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ;[, errorCallback, requestCommandName] = this.getCachedRequest(messageType, messageId)!
- // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
- errorCallback(error as OCPPError, false)
- } else {
- // Remove the request from the cache in case of error at response handling
- this.requests.delete(messageId)
- }
- break
- }
- if (!(error instanceof OCPPError)) {
- logger.warn(
- `${this.logPrefix()} Error thrown at incoming OCPP command ${
- commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- } message '${data.toString()}' handling is not an OCPPError:`,
- error
- )
- }
- logger.error(
- `${this.logPrefix()} Incoming OCPP command '${
- commandName ?? requestCommandName ?? Constants.UNKNOWN_OCPP_COMMAND
- // eslint-disable-next-line @typescript-eslint/no-base-to-string
- }' message '${data.toString()}'${
- this.requests.has(messageId)
- ? ` matching cached request '${JSON.stringify(
- this.getCachedRequest(messageType, messageId)
- )}'`
- : ''
- } processing error:`,
- error
- )
+ public getHeartbeatInterval (): number {
+ const HeartbeatInterval = getConfigurationKey(this, StandardParametersKey.HeartbeatInterval)
+ if (HeartbeatInterval != null) {
+ return secondsToMilliseconds(convertToInt(HeartbeatInterval.value))
+ }
+ const HeartBeatInterval = getConfigurationKey(this, StandardParametersKey.HeartBeatInterval)
+ if (HeartBeatInterval != null) {
+ return secondsToMilliseconds(convertToInt(HeartBeatInterval.value))
}
+ this.stationInfo?.autoRegister === false &&
+ logger.warn(
+ `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${Constants.DEFAULT_HEARTBEAT_INTERVAL.toString()}`
+ )
+ return Constants.DEFAULT_HEARTBEAT_INTERVAL
}
- private onPing (): void {
- logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`)
- }
-
- private onPong (): void {
- logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`)
+ public getLocalAuthListEnabled (): boolean {
+ const localAuthListEnabled = getConfigurationKey(
+ this,
+ StandardParametersKey.LocalAuthListEnabled
+ )
+ return localAuthListEnabled != null ? convertToBoolean(localAuthListEnabled.value) : false
}
- private onError (error: WSError): void {
- this.closeWSConnection()
- logger.error(`${this.logPrefix()} WebSocket error:`, error)
+ public getNumberOfConnectors (): number {
+ if (this.hasEvses) {
+ let numberOfConnectors = 0
+ for (const [evseId, evseStatus] of this.evses) {
+ if (evseId > 0) {
+ numberOfConnectors += evseStatus.connectors.size
+ }
+ }
+ return numberOfConnectors
+ }
+ return this.connectors.has(0) ? this.connectors.size - 1 : this.connectors.size
}
- private getEnergyActiveImportRegister (
- connectorStatus: ConnectorStatus | undefined,
- rounded = false
- ): number {
- if (this.stationInfo?.meteringPerTransaction === true) {
- return (
- (rounded
- ? connectorStatus?.transactionEnergyActiveImportRegisterValue != null
- ? Math.round(connectorStatus.transactionEnergyActiveImportRegisterValue)
- : undefined
- : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
- )
- }
- return (
- (rounded
- ? connectorStatus?.energyActiveImportRegisterValue != null
- ? Math.round(connectorStatus.energyActiveImportRegisterValue)
- : undefined
- : connectorStatus?.energyActiveImportRegisterValue) ?? 0
- )
+ public getNumberOfEvses (): number {
+ return this.evses.has(0) ? this.evses.size - 1 : this.evses.size
}
- private getUseConnectorId0 (stationTemplate?: ChargingStationTemplate): boolean {
+ public getNumberOfPhases (stationInfo?: ChargingStationInfo): number {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return stationTemplate?.useConnectorId0 ?? Constants.DEFAULT_STATION_INFO.useConnectorId0!
+ const localStationInfo = stationInfo ?? this.stationInfo!
+ switch (this.getCurrentOutType(stationInfo)) {
+ case CurrentType.AC:
+ return localStationInfo.numberOfPhases ?? 3
+ case CurrentType.DC:
+ return 0
+ }
}
- private async stopRunningTransactions (reason?: StopTransactionReason): Promise<void> {
+ public getNumberOfRunningTransactions (): number {
+ let numberOfRunningTransactions = 0
if (this.hasEvses) {
for (const [evseId, evseStatus] of this.evses) {
if (evseId === 0) {
continue
}
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ for (const connectorStatus of evseStatus.connectors.values()) {
if (connectorStatus.transactionStarted === true) {
- await this.stopTransactionOnConnector(connectorId, reason)
+ ++numberOfRunningTransactions
}
}
}
} else {
for (const connectorId of this.connectors.keys()) {
if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
- await this.stopTransactionOnConnector(connectorId, reason)
+ ++numberOfRunningTransactions
}
}
}
+ return numberOfRunningTransactions
}
- // 0 for disabling
- private getConnectionTimeout (): number {
- if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
- return convertToInt(
- getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
- Constants.DEFAULT_CONNECTION_TIMEOUT
+ public getReservationBy (
+ filterKey: ReservationKey,
+ value: number | string
+ ): Reservation | undefined {
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ if (connectorStatus.reservation?.[filterKey] === value) {
+ return connectorStatus.reservation
+ }
+ }
+ }
+ } else {
+ for (const connectorStatus of this.connectors.values()) {
+ if (connectorStatus.reservation?.[filterKey] === value) {
+ return connectorStatus.reservation
+ }
+ }
+ }
+ }
+
+ public getReserveConnectorZeroSupported (): boolean {
+ return convertToBoolean(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ getConfigurationKey(this, StandardParametersKey.ReserveConnectorZeroSupported)!.value
+ )
+ }
+
+ public getTransactionIdTag (transactionId: number): string | undefined {
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ for (const connectorStatus of evseStatus.connectors.values()) {
+ if (connectorStatus.transactionId === transactionId) {
+ return connectorStatus.transactionIdTag
+ }
+ }
+ }
+ } else {
+ for (const connectorId of this.connectors.keys()) {
+ if (this.getConnectorStatus(connectorId)?.transactionId === transactionId) {
+ return this.getConnectorStatus(connectorId)?.transactionIdTag
+ }
+ }
+ }
+ }
+
+ public hasConnector (connectorId: number): boolean {
+ if (this.hasEvses) {
+ for (const evseStatus of this.evses.values()) {
+ if (evseStatus.connectors.has(connectorId)) {
+ return true
+ }
+ }
+ return false
+ }
+ return this.connectors.has(connectorId)
+ }
+
+ public hasIdTags (): boolean {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return isNotEmptyArray(this.idTagsCache.getIdTags(getIdTagsFile(this.stationInfo!)!))
+ }
+
+ public inAcceptedState (): boolean {
+ return this.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED
+ }
+
+ public inPendingState (): boolean {
+ return this.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING
+ }
+
+ public inRejectedState (): boolean {
+ return this.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED
+ }
+
+ public inUnknownState (): boolean {
+ return this.bootNotificationResponse?.status == null
+ }
+
+ public isChargingStationAvailable (): boolean {
+ return this.getConnectorStatus(0)?.availability === AvailabilityType.Operative
+ }
+
+ public isConnectorAvailable (connectorId: number): boolean {
+ return (
+ connectorId > 0 &&
+ this.getConnectorStatus(connectorId)?.availability === AvailabilityType.Operative
+ )
+ }
+
+ public isConnectorReservable (
+ reservationId: number,
+ idTag?: string,
+ connectorId?: number
+ ): boolean {
+ const reservation = this.getReservationBy('reservationId', reservationId)
+ const reservationExists = reservation != null && !hasReservationExpired(reservation)
+ if (arguments.length === 1) {
+ return !reservationExists
+ } else if (arguments.length > 1) {
+ const userReservation = idTag != null ? this.getReservationBy('idTag', idTag) : undefined
+ const userReservationExists =
+ userReservation != null && !hasReservationExpired(userReservation)
+ const notConnectorZero = connectorId == null ? true : connectorId > 0
+ const freeConnectorsAvailable = this.getNumberOfReservableConnectors() > 0
+ return (
+ !reservationExists && !userReservationExists && notConnectorZero && freeConnectorsAvailable
)
}
- return Constants.DEFAULT_CONNECTION_TIMEOUT
+ return false
}
- private getPowerDivider (): number {
- let powerDivider = this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()
- if (this.stationInfo?.powerSharedByConnectors === true) {
- powerDivider = this.getNumberOfRunningTransactions()
+ public isRegistered (): boolean {
+ return !this.inUnknownState() && (this.inAcceptedState() || this.inPendingState())
+ }
+
+ public isWebSocketConnectionOpened (): boolean {
+ return this.wsConnection?.readyState === WebSocket.OPEN
+ }
+
+ public openWSConnection (
+ options?: WsOptions,
+ params?: { closeOpened?: boolean; terminateOpened?: boolean }
+ ): void {
+ options = {
+ handshakeTimeout: secondsToMilliseconds(this.getConnectionTimeout()),
+ ...this.stationInfo?.wsOptions,
+ ...options,
}
- return powerDivider
+ params = { ...{ closeOpened: false, terminateOpened: false }, ...params }
+ if (!checkChargingStationState(this, this.logPrefix())) {
+ return
+ }
+ if (this.stationInfo?.supervisionUser != null && this.stationInfo.supervisionPassword != null) {
+ options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`
+ }
+ if (params.closeOpened) {
+ this.closeWSConnection()
+ }
+ if (params.terminateOpened) {
+ this.terminateWSConnection()
+ }
+
+ if (this.isWebSocketConnectionOpened()) {
+ logger.warn(
+ `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.href} is already opened`
+ )
+ return
+ }
+
+ logger.info(`${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.href}`)
+
+ this.wsConnection = new WebSocket(
+ this.wsConnectionUrl,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `ocpp${this.stationInfo?.ocppVersion}`,
+ options
+ )
+
+ // Handle WebSocket message
+ this.wsConnection.on('message', data => {
+ this.onMessage(data).catch(Constants.EMPTY_FUNCTION)
+ })
+ // Handle WebSocket error
+ this.wsConnection.on('error', this.onError.bind(this))
+ // Handle WebSocket close
+ this.wsConnection.on('close', this.onClose.bind(this))
+ // Handle WebSocket open
+ this.wsConnection.on('open', () => {
+ this.onOpen().catch((error: unknown) =>
+ logger.error(`${this.logPrefix()} Error while opening WebSocket connection:`, error)
+ )
+ })
+ // Handle WebSocket ping
+ this.wsConnection.on('ping', this.onPing.bind(this))
+ // Handle WebSocket pong
+ this.wsConnection.on('pong', this.onPong.bind(this))
}
- private getMaximumAmperage (stationInfo?: ChargingStationInfo): number | undefined {
+ public async removeReservation (
+ reservation: Reservation,
+ reason: ReservationTerminationReason
+ ): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const maximumPower = (stationInfo ?? this.stationInfo!).maximumPower!
- switch (this.getCurrentOutType(stationInfo)) {
- case CurrentType.AC:
- return ACElectricUtils.amperagePerPhaseFromPower(
- this.getNumberOfPhases(stationInfo),
- maximumPower / (this.hasEvses ? this.getNumberOfEvses() : this.getNumberOfConnectors()),
- this.getVoltageOut(stationInfo)
+ const connector = this.getConnectorStatus(reservation.connectorId)!
+ switch (reason) {
+ case ReservationTerminationReason.CONNECTOR_STATE_CHANGED:
+ case ReservationTerminationReason.TRANSACTION_STARTED:
+ delete connector.reservation
+ break
+ case ReservationTerminationReason.EXPIRED:
+ case ReservationTerminationReason.REPLACE_EXISTING:
+ case ReservationTerminationReason.RESERVATION_CANCELED:
+ await sendAndSetConnectorStatus(
+ this,
+ reservation.connectorId,
+ ConnectorStatusEnum.Available,
+ undefined,
+ { send: reservation.connectorId !== 0 }
)
- case CurrentType.DC:
- return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo))
+ delete connector.reservation
+ break
+ default:
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ throw new BaseError(`Unknown reservation termination reason '${reason}'`)
}
}
- private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
- return (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (stationInfo ?? this.stationInfo!).currentOutType ??
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Constants.DEFAULT_STATION_INFO.currentOutType!
- )
+ public async reset (reason?: StopTransactionReason): Promise<void> {
+ await this.stop(reason)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await sleep(this.stationInfo!.resetTime!)
+ this.initialize()
+ this.start()
}
- private getVoltageOut (stationInfo?: ChargingStationInfo): Voltage {
- return (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (stationInfo ?? this.stationInfo!).voltageOut ??
- getDefaultVoltageOut(this.getCurrentOutType(stationInfo), this.logPrefix(), this.templateFile)
- )
+ public restartHeartbeat (): void {
+ // Stop heartbeat
+ this.stopHeartbeat()
+ // Start heartbeat
+ this.startHeartbeat()
}
- private getAmperageLimitation (): number | undefined {
- if (
- isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
- getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey) != null
- ) {
- return (
- convertToInt(getConfigurationKey(this, this.stationInfo.amperageLimitationOcppKey)?.value) /
- getAmperageLimitationUnitDivider(this.stationInfo)
- )
- }
+ public restartMeterValues (connectorId: number, interval: number): void {
+ this.stopMeterValues(connectorId)
+ this.startMeterValues(connectorId, interval)
}
- private async startMessageSequence (ATGStopAbsoluteDuration?: boolean): Promise<void> {
- if (this.stationInfo?.autoRegister === true) {
- await this.ocppRequestService.requestHandler<
- BootNotificationRequest,
- BootNotificationResponse
- >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
- skipBufferingOnError: true,
- })
- }
+ public restartWebSocketPing (): void {
+ // Stop WebSocket ping
+ this.stopWebSocketPing()
// Start WebSocket ping
- if (this.wsPingSetInterval == null) {
- this.startWebSocketPing()
- }
- // Start heartbeat
- if (this.heartbeatSetInterval == null) {
- this.startHeartbeat()
- }
- // Initialize connectors status
- if (this.hasEvses) {
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId > 0) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- await sendAndSetConnectorStatus(
- this,
- connectorId,
- getBootConnectorStatus(this, connectorId, connectorStatus),
- evseId
- )
- }
- }
- }
- } else {
- for (const connectorId of this.connectors.keys()) {
- if (connectorId > 0) {
- await sendAndSetConnectorStatus(
- this,
- connectorId,
- getBootConnectorStatus(
- this,
- connectorId,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.getConnectorStatus(connectorId)!
- )
- )
- }
- }
- }
- if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
- await this.ocppRequestService.requestHandler<
- FirmwareStatusNotificationRequest,
- FirmwareStatusNotificationResponse
- >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: FirmwareStatus.Installed,
- })
- this.stationInfo.firmwareStatus = FirmwareStatus.Installed
- }
+ this.startWebSocketPing()
+ }
- // Start the ATG
- if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
- this.startAutomaticTransactionGenerator(undefined, ATGStopAbsoluteDuration)
+ public saveOcppConfiguration (): void {
+ if (this.stationInfo?.ocppPersistentConfiguration === true) {
+ this.saveConfiguration()
}
- this.flushMessageBuffer()
}
- private internalStopMessageSequence (): void {
- // Stop WebSocket ping
- this.stopWebSocketPing()
- // Stop heartbeat
- this.stopHeartbeat()
- // Stop the ATG
- if (this.automaticTransactionGenerator?.started === true) {
- this.stopAutomaticTransactionGenerator()
+ public setSupervisionUrl (url: string): void {
+ if (
+ this.stationInfo?.supervisionUrlOcppConfiguration === true &&
+ isNotEmptyString(this.stationInfo.supervisionUrlOcppKey)
+ ) {
+ setConfigurationKeyValue(this, this.stationInfo.supervisionUrlOcppKey, url)
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.stationInfo!.supervisionUrls = url
+ this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
+ this.saveStationInfo()
}
}
- private async stopMessageSequence (
- reason?: StopTransactionReason,
- stopTransactions?: boolean
- ): Promise<void> {
- this.internalStopMessageSequence()
- // Stop ongoing transactions
- stopTransactions && (await this.stopRunningTransactions(reason))
- if (this.hasEvses) {
- for (const [evseId, evseStatus] of this.evses) {
- if (evseId > 0) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- await sendAndSetConnectorStatus(
- this,
- connectorId,
- ConnectorStatusEnum.Unavailable,
- evseId
- )
- delete connectorStatus.status
- }
+ public start (): void {
+ if (!this.started) {
+ if (!this.starting) {
+ this.starting = true
+ if (this.stationInfo?.enableStatistics === true) {
+ this.performanceStatistics?.start()
}
+ this.openWSConnection()
+ // Monitor charging station template file
+ this.templateFileWatcher = watchJsonFile(
+ this.templateFile,
+ FileType.ChargingStationTemplate,
+ this.logPrefix(),
+ undefined,
+ (event, filename): void => {
+ if (isNotEmptyString(filename) && event === 'change') {
+ try {
+ logger.debug(
+ `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
+ this.templateFile
+ } file have changed, reload`
+ )
+ this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo!)!)
+ // Initialize
+ this.initialize()
+ // Restart the ATG
+ const ATGStarted = this.automaticTransactionGenerator?.started
+ if (ATGStarted === true) {
+ this.stopAutomaticTransactionGenerator()
+ }
+ delete this.automaticTransactionGeneratorConfiguration
+ if (
+ this.getAutomaticTransactionGeneratorConfiguration()?.enable === true &&
+ ATGStarted === true
+ ) {
+ this.startAutomaticTransactionGenerator(undefined, true)
+ }
+ if (this.stationInfo?.enableStatistics === true) {
+ this.performanceStatistics?.restart()
+ } else {
+ this.performanceStatistics?.stop()
+ }
+ // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
+ } catch (error) {
+ logger.error(
+ `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
+ error
+ )
+ }
+ }
+ }
+ )
+ this.started = true
+ this.emit(ChargingStationEvents.started)
+ this.starting = false
+ } else {
+ logger.warn(`${this.logPrefix()} Charging station is already starting...`)
}
} else {
- for (const connectorId of this.connectors.keys()) {
- if (connectorId > 0) {
- await sendAndSetConnectorStatus(this, connectorId, ConnectorStatusEnum.Unavailable)
- delete this.getConnectorStatus(connectorId)?.status
- }
- }
+ logger.warn(`${this.logPrefix()} Charging station is already started...`)
}
}
- private getWebSocketPingInterval (): number {
- return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
- ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
- : 0
+ public startAutomaticTransactionGenerator (
+ connectorIds?: number[],
+ stopAbsoluteDuration?: boolean
+ ): void {
+ this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this)
+ if (isNotEmptyArray(connectorIds)) {
+ for (const connectorId of connectorIds) {
+ this.automaticTransactionGenerator?.startConnector(connectorId, stopAbsoluteDuration)
+ }
+ } else {
+ this.automaticTransactionGenerator?.start(stopAbsoluteDuration)
+ }
+ this.saveAutomaticTransactionGeneratorConfiguration()
+ this.emit(ChargingStationEvents.updated)
}
- private startWebSocketPing (): void {
- const webSocketPingInterval = this.getWebSocketPingInterval()
- if (webSocketPingInterval > 0 && this.wsPingSetInterval == null) {
- this.wsPingSetInterval = setInterval(() => {
- if (this.isWebSocketConnectionOpened()) {
- this.wsConnection?.ping()
- }
- }, secondsToMilliseconds(webSocketPingInterval))
+ public startHeartbeat (): void {
+ const heartbeatInterval = this.getHeartbeatInterval()
+ if (heartbeatInterval > 0 && this.heartbeatSetInterval == null) {
+ this.heartbeatSetInterval = setInterval(() => {
+ this.ocppRequestService
+ .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
+ .catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
+ error
+ )
+ })
+ }, heartbeatInterval)
logger.info(
- `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
- webSocketPingInterval
+ `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
+ heartbeatInterval
)}`
)
- } else if (this.wsPingSetInterval != null) {
+ } else if (this.heartbeatSetInterval != null) {
logger.info(
- `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
- webSocketPingInterval
+ `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
+ heartbeatInterval
)}`
)
} else {
logger.error(
- `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval.toString()}, not starting the WebSocket ping`
+ `${this.logPrefix()} Heartbeat interval set to ${heartbeatInterval.toString()}, not starting the heartbeat`
)
}
}
- private stopWebSocketPing (): void {
- if (this.wsPingSetInterval != null) {
- clearInterval(this.wsPingSetInterval)
- delete this.wsPingSetInterval
+ public startMeterValues (connectorId: number, interval: number): void {
+ if (connectorId === 0) {
+ logger.error(
+ `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
+ )
+ return
+ }
+ const connectorStatus = this.getConnectorStatus(connectorId)
+ if (connectorStatus == null) {
+ logger.error(
+ `${this.logPrefix()} Trying to start MeterValues on non existing connector id
+ ${connectorId.toString()}`
+ )
+ return
+ }
+ if (connectorStatus.transactionStarted === false) {
+ logger.error(
+ `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction started`
+ )
+ return
+ } else if (
+ connectorStatus.transactionStarted === true &&
+ connectorStatus.transactionId == null
+ ) {
+ logger.error(
+ `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()} with no transaction id`
+ )
+ return
+ }
+ if (interval > 0) {
+ connectorStatus.transactionSetInterval = setInterval(() => {
+ const meterValue = buildMeterValue(
+ this,
+ connectorId,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ connectorStatus.transactionId!,
+ interval
+ )
+ this.ocppRequestService
+ .requestHandler<MeterValuesRequest, MeterValuesResponse>(
+ this,
+ RequestCommand.METER_VALUES,
+ {
+ connectorId,
+ meterValue: [meterValue],
+ transactionId: connectorStatus.transactionId,
+ }
+ )
+ .catch((error: unknown) => {
+ logger.error(
+ `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
+ error
+ )
+ })
+ }, interval)
+ } else {
+ logger.error(
+ `${this.logPrefix()} Charging station ${
+ StandardParametersKey.MeterValueSampleInterval
+ } configuration set to ${interval.toString()}, not sending MeterValues`
+ )
}
}
- private getConfiguredSupervisionUrl (): URL {
- let configuredSupervisionUrl: string
- const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls()
- if (isNotEmptyArray(supervisionUrls)) {
- let configuredSupervisionUrlIndex: number
- switch (Configuration.getSupervisionUrlDistribution()) {
- case SupervisionUrlDistribution.RANDOM:
- configuredSupervisionUrlIndex = Math.floor(secureRandom() * supervisionUrls.length)
- break
- case SupervisionUrlDistribution.ROUND_ROBIN:
- case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
- default:
- !Object.values(SupervisionUrlDistribution).includes(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Configuration.getSupervisionUrlDistribution()!
- ) &&
- logger.warn(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-base-to-string
- `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' in configuration from values '${SupervisionUrlDistribution.toString()}', defaulting to '${
- SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
- }'`
- )
- configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length
- break
+ public async stop (
+ reason?: StopTransactionReason,
+ stopTransactions = this.stationInfo?.stopTransactionsOnStopped
+ ): Promise<void> {
+ if (this.started) {
+ if (!this.stopping) {
+ this.stopping = true
+ await this.stopMessageSequence(reason, stopTransactions)
+ this.closeWSConnection()
+ if (this.stationInfo?.enableStatistics === true) {
+ this.performanceStatistics?.stop()
+ }
+ this.templateFileWatcher?.close()
+ delete this.bootNotificationResponse
+ this.started = false
+ this.saveConfiguration()
+ this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash)
+ this.emit(ChargingStationEvents.stopped)
+ this.stopping = false
+ } else {
+ logger.warn(`${this.logPrefix()} Charging station is already stopping...`)
}
- configuredSupervisionUrl = supervisionUrls[configuredSupervisionUrlIndex]
} else {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- configuredSupervisionUrl = supervisionUrls!
- }
- if (isNotEmptyString(configuredSupervisionUrl)) {
- return new URL(configuredSupervisionUrl)
+ logger.warn(`${this.logPrefix()} Charging station is already stopped...`)
}
- const errorMsg = 'No supervision url(s) configured'
- logger.error(`${this.logPrefix()} ${errorMsg}`)
- throw new BaseError(errorMsg)
}
- private stopHeartbeat (): void {
- if (this.heartbeatSetInterval != null) {
- clearInterval(this.heartbeatSetInterval)
- delete this.heartbeatSetInterval
+ public stopAutomaticTransactionGenerator (connectorIds?: number[]): void {
+ if (isNotEmptyArray(connectorIds)) {
+ for (const connectorId of connectorIds) {
+ this.automaticTransactionGenerator?.stopConnector(connectorId)
+ }
+ } else {
+ this.automaticTransactionGenerator?.stop()
}
+ this.saveAutomaticTransactionGeneratorConfiguration()
+ this.emit(ChargingStationEvents.updated)
}
- private terminateWSConnection (): void {
- if (this.isWebSocketConnectionOpened()) {
- this.wsConnection?.terminate()
- this.wsConnection = null
+ public stopMeterValues (connectorId: number): void {
+ const connectorStatus = this.getConnectorStatus(connectorId)
+ if (connectorStatus?.transactionSetInterval != null) {
+ clearInterval(connectorStatus.transactionSetInterval)
}
}
- private async reconnect (): Promise<void> {
+ public async stopTransactionOnConnector (
+ connectorId: number,
+ reason?: StopTransactionReason
+ ): Promise<StopTransactionResponse> {
+ const transactionId = this.getConnectorStatus(connectorId)?.transactionId
if (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.wsConnectionRetryCount < this.stationInfo!.autoReconnectMaxRetries! ||
- this.stationInfo?.autoReconnectMaxRetries === -1
+ this.stationInfo?.beginEndMeterValues === true &&
+ this.stationInfo.ocppStrictCompliance === true &&
+ this.stationInfo.outOfOrderEndMeterValues === false
) {
- ++this.wsConnectionRetryCount
- const reconnectDelay =
- this.stationInfo?.reconnectExponentialDelay === true
- ? exponentialDelay(this.wsConnectionRetryCount)
- : secondsToMilliseconds(this.getConnectionTimeout())
- const reconnectDelayWithdraw = 1000
- const reconnectTimeout =
- reconnectDelay - reconnectDelayWithdraw > 0 ? reconnectDelay - reconnectDelayWithdraw : 0
- logger.error(
- `${this.logPrefix()} WebSocket connection retry in ${roundTo(
- reconnectDelay,
- 2
- ).toString()}ms, timeout ${reconnectTimeout.toString()}ms`
- )
- await sleep(reconnectDelay)
- logger.error(
- `${this.logPrefix()} WebSocket connection retry #${this.wsConnectionRetryCount.toString()}`
+ const transactionEndMeterValue = buildTransactionEndMeterValue(
+ this,
+ connectorId,
+ this.getEnergyActiveImportRegisterByTransactionId(transactionId)
)
- this.openWSConnection(
+ await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
+ this,
+ RequestCommand.METER_VALUES,
{
- handshakeTimeout: reconnectTimeout,
- },
- { closeOpened: true }
- )
- } else if (this.stationInfo?.autoReconnectMaxRetries !== -1) {
- logger.error(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${this.wsConnectionRetryCount.toString()}) or retries disabled (${this.stationInfo?.autoReconnectMaxRetries?.toString()})`
+ connectorId,
+ meterValue: [transactionEndMeterValue],
+ transactionId,
+ }
)
}
+ return await this.ocppRequestService.requestHandler<
+ Partial<StopTransactionRequest>,
+ StopTransactionResponse
+ >(this, RequestCommand.STOP_TRANSACTION, {
+ meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
+ transactionId,
+ ...(reason != null && { reason }),
+ })
+ }
+
+ public get hasEvses (): boolean {
+ return this.connectors.size === 0 && this.evses.size > 0
+ }
+
+ public get wsConnectionUrl (): URL {
+ const wsConnectionBaseUrlStr = `${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ this.stationInfo?.supervisionUrlOcppConfiguration === true &&
+ isNotEmptyString(this.stationInfo.supervisionUrlOcppKey) &&
+ isNotEmptyString(getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value)
+ ? getConfigurationKey(this, this.stationInfo.supervisionUrlOcppKey)?.value
+ : this.configuredSupervisionUrl.href
+ }`
+ return new URL(
+ `${wsConnectionBaseUrlStr}${
+ !wsConnectionBaseUrlStr.endsWith('/') ? '/' : ''
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }${this.stationInfo?.chargingStationId}`
+ )
}
}
// 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'
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 {
class ChargingStationWorker<Data extends ChargingStationWorkerData> {
constructor () {
parentPort?.on('message', (message: WorkerMessage<Data>) => {
- const { uuid, event, data } = message
+ const { data, event, uuid } = message
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (uuid != null) {
switch (event) {
data.options
)
parentPort?.postMessage({
- uuid,
- event: WorkerMessageEvents.addedWorkerElement,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
data: chargingStation.stationInfo!,
+ event: WorkerMessageEvents.addedWorkerElement,
+ uuid,
} satisfies WorkerMessage<ChargingStationInfo>)
} catch (error) {
parentPort?.postMessage({
- uuid,
- event: WorkerMessageEvents.workerElementError,
data: {
event,
- name: (error as Error).name,
message: (error as Error).message,
+ name: (error as Error).name,
stack: (error as Error).stack,
},
+ event: WorkerMessageEvents.workerElementError,
+ uuid,
} satisfies WorkerMessage<WorkerDataError>)
}
break
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
options = {
...{
readonly: false,
- visible: true,
reboot: false,
+ visible: true,
},
...options,
}
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 {
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(
-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 {
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,
logger,
secureRandom,
} from '../utils/index.js'
-import type { ChargingStation } from './ChargingStation.js'
import { getConfigurationKey } from './ConfigurationKeyUtils.js'
const moduleName = 'Helpers'
templateFile: string
): {
configuredMaxConnectors: number
- templateMaxConnectors: number
templateMaxAvailableConnectors: number
+ templateMaxConnectors: number
} => {
const configuredMaxConnectors = getConfiguredMaxNumberOfConnectors(stationTemplate)
checkConfiguredMaxConnectors(configuredMaxConnectors, logPrefix, templateFile)
}
return {
configuredMaxConnectors,
- templateMaxConnectors,
templateMaxAvailableConnectors,
+ templateMaxConnectors,
}
}
case OCPPVersion.VERSION_20:
case OCPPVersion.VERSION_201:
return {
- reason: bootReason,
chargingStation: {
model: stationInfo.chargePointModel,
vendorName: stationInfo.chargePointVendor,
},
}),
},
+ reason: bootReason,
} satisfies OCPP20BootNotificationRequest
}
}
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
): 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
}
interface ChargingProfilesLimit {
- limit: number
chargingProfile: ChargingProfile
+ limit: number
}
/**
// 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)) {
// 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
) {
// 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
) > chargingSchedule.duration)
) {
const chargingProfilesLimit: ChargingProfilesLimit = {
- limit: chargingSchedulePeriod.limit,
chargingProfile,
+ limit: chargingSchedulePeriod.limit,
}
logger.debug(debugLogMsg, chargingProfilesLimit)
return chargingProfilesLimit
export const prepareChargingProfileKind = (
connectorStatus: ConnectorStatus | undefined,
chargingProfile: ChargingProfile,
- currentDate: string | number | Date,
+ currentDate: Date | number | string,
logPrefix: string
): boolean => {
switch (chargingProfile.chargingProfileKind) {
export const canProceedChargingProfile = (
chargingProfile: ChargingProfile,
- currentDate: string | number | Date,
+ currentDate: Date | number | string,
logPrefix: string
): boolean => {
if (
*/
const prepareRecurringChargingProfile = (
chargingProfile: ChargingProfile,
- currentDate: string | number | Date,
+ currentDate: Date | number | string,
logPrefix: string
): boolean => {
const chargingSchedule = chargingProfile.chargingSchedule
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 (
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 (
differenceInWeeks(currentDate, recurringInterval.start)
)
recurringInterval = {
- start: chargingSchedule.startSchedule,
end: addWeeks(chargingSchedule.startSchedule, 1),
+ start: chargingSchedule.startSchedule,
}
recurringIntervalTranslated = true
}
import { type FSWatcher, readFileSync } from 'node:fs'
+import type { ChargingStation } from './ChargingStation.js'
+
import { FileType, IdTagDistribution } from '../types/index.js'
import {
handleFileException,
secureRandom,
watchJsonFile,
} from '../utils/index.js'
-import type { ChargingStation } from './ChargingStation.js'
import { getIdTagsFile } from './Helpers.js'
interface IdTagsCacheValueType {
private readonly idTagsCaches: Map<string, IdTagsCacheValueType>
private readonly idTagsCachesAddressableIndexes: Map<string, number>
+ private readonly logPrefix = (file: string): string => {
+ return logPrefix(` Id tags cache for id tags file '${file}' |`)
+ }
+
private constructor () {
this.idTagsCaches = new Map<string, IdTagsCacheValueType>()
this.idTagsCachesAddressableIndexes = new Map<string, number>()
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 {
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)
}
})
}
- 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)
}
}
import { isEmpty } from 'rambda'
import type { ChargingStationConfiguration, ChargingStationTemplate } from '../types/index.js'
+
import { isNotEmptyArray, isNotEmptyString } from '../utils/index.js'
import { Bootstrap } from './Bootstrap.js'
enum CacheType {
- chargingStationTemplate = 'chargingStationTemplate',
- chargingStationConfiguration = 'chargingStationConfiguration'
+ chargingStationConfiguration = 'chargingStationConfiguration',
+ chargingStationTemplate = 'chargingStationTemplate'
}
-type CacheValueType = ChargingStationTemplate | ChargingStationConfiguration
+type CacheValueType = ChargingStationConfiguration | ChargingStationTemplate
export class SharedLRUCache {
- private static instance: SharedLRUCache | null = null
+ private static instance: null | SharedLRUCache = null
private readonly lruCache: LRUCache<string, CacheValueType>
private constructor () {
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
)
}
}
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,
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'
const moduleName = 'ChargingStationWorkerBroadcastChannel'
type CommandResponse =
- | EmptyObject
- | StartTransactionResponse
- | StopTransactionResponse
| AuthorizeResponse
| BootNotificationResponse
- | HeartbeatResponse
| DataTransferResponse
+ | EmptyObject
+ | HeartbeatResponse
+ | StartTransactionResponse
+ | StopTransactionResponse
type CommandHandler = (
requestPayload?: BroadcastChannelRequestPayload
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
-) => Promise<CommandResponse | void> | CommandResponse | void
+) => CommandResponse | Promise<CommandResponse | void> | void
export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
- private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
private readonly chargingStation: ChargingStation
+ private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
constructor (chargingStation: ChargingStation) {
super()
throwError: true,
}
this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
- [
- BroadcastChannelProcedureName.START_CHARGING_STATION,
- () => {
- this.chargingStation.start()
- },
- ],
- [
- BroadcastChannelProcedureName.STOP_CHARGING_STATION,
- async () => {
- await this.chargingStation.stop()
- },
- ],
- [
- BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
- async (requestPayload?: BroadcastChannelRequestPayload) => {
- await this.chargingStation.delete(requestPayload?.deleteConfiguration as boolean)
- },
- ],
- [
- BroadcastChannelProcedureName.OPEN_CONNECTION,
- () => {
- this.chargingStation.openWSConnection()
- },
- ],
- [
- BroadcastChannelProcedureName.CLOSE_CONNECTION,
- () => {
- this.chargingStation.closeWSConnection()
- },
- ],
- [
- BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
- (requestPayload?: BroadcastChannelRequestPayload) => {
- this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
- },
- ],
- [
- BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
- (requestPayload?: BroadcastChannelRequestPayload) => {
- this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
- },
- ],
- [
- BroadcastChannelProcedureName.SET_SUPERVISION_URL,
- (requestPayload?: BroadcastChannelRequestPayload) => {
- this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
- },
- ],
- [
- BroadcastChannelProcedureName.START_TRANSACTION,
- async (requestPayload?: BroadcastChannelRequestPayload) =>
- await this.chargingStation.ocppRequestService.requestHandler<
- StartTransactionRequest,
- StartTransactionResponse
- >(
- this.chargingStation,
- RequestCommand.START_TRANSACTION,
- requestPayload as StartTransactionRequest,
- requestParams
- ),
- ],
- [
- BroadcastChannelProcedureName.STOP_TRANSACTION,
- async (requestPayload?: BroadcastChannelRequestPayload) =>
- await this.chargingStation.ocppRequestService.requestHandler<
- StopTransactionRequest,
- StartTransactionResponse
- >(
- this.chargingStation,
- RequestCommand.STOP_TRANSACTION,
- {
- meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
- requestPayload?.transactionId,
- true
- ),
- ...requestPayload,
- } as StopTransactionRequest,
- requestParams
- ),
- ],
[
BroadcastChannelProcedureName.AUTHORIZE,
async (requestPayload?: BroadcastChannelRequestPayload) =>
},
],
[
- 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
),
],
},
],
[
- 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
),
],
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 (
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,
}
}
return {
- hashId: this.chargingStation.stationInfo?.hashId,
- status: responseStatus,
command,
- requestPayload,
commandResponse,
+ hashId: this.chargingStation.stationInfo?.hashId,
+ requestPayload,
+ status: responseStatus,
}
}
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
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
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
+ })
+ }
}
+import type { AbstractUIService } from '../ui-server/ui-services/AbstractUIService.js'
+
import {
type BroadcastChannelResponse,
type BroadcastChannelResponsePayload,
ResponseStatus,
} from '../../types/index.js'
import { logger } from '../../utils/index.js'
-import type { AbstractUIService } from '../ui-server/ui-services/AbstractUIService.js'
import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
const moduleName = 'UIServiceWorkerBroadcastChannel'
interface Responses {
+ responses: BroadcastChannelResponsePayload[]
responsesExpected: number
responsesReceived: number
- responses: BroadcastChannelResponsePayload[]
}
export class UIServiceWorkerBroadcastChannel extends WorkerBroadcastChannel {
- private readonly uiService: AbstractUIService
private readonly responses: Map<string, Responses>
+ private readonly uiService: AbstractUIService
constructor (uiService: AbstractUIService) {
super()
this.responses = new Map<string, Responses>()
}
- private responseHandler (messageEvent: MessageEvent): void {
- const validatedMessageEvent = this.validateMessageEvent(messageEvent)
- if (validatedMessageEvent === false) {
- return
- }
- if (this.isRequest(validatedMessageEvent.data)) {
- return
- }
- const [uuid, responsePayload] = validatedMessageEvent.data as BroadcastChannelResponse
- if (!this.responses.has(uuid)) {
- this.responses.set(uuid, {
- responsesExpected: this.uiService.getBroadcastChannelExpectedResponses(uuid),
- responsesReceived: 1,
- responses: [responsePayload],
- })
- } else if (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.responses.get(uuid)!.responsesReceived <= this.responses.get(uuid)!.responsesExpected
- ) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.responses.get(uuid)!.responsesReceived
- this.responses.get(uuid)?.responses.push(responsePayload)
- }
- if (
- this.responses.get(uuid)?.responsesReceived === this.responses.get(uuid)?.responsesExpected
- ) {
- this.uiService.sendResponse(uuid, this.buildResponsePayload(uuid))
- this.responses.delete(uuid)
- this.uiService.deleteBroadcastChannelRequest(uuid)
- }
- }
-
private buildResponsePayload (uuid: string): ResponsePayload {
const responsesStatus =
this.responses
? 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
}
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)
+ }
+ }
}
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 {
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(
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)
}
}
// 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,
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'
super(OCPPVersion.VERSION_16)
this.incomingRequestHandlers = new Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>([
[
- OCPP16IncomingRequestCommand.RESET,
- this.handleRequestReset.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ this.handleRequestCancelReservation.bind(this) as unknown as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.CLEAR_CACHE,
- this.handleRequestClearCache.bind(this) as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+ this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
- this.handleRequestUnlockConnector.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+ this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.GET_CONFIGURATION,
- this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.CLEAR_CACHE,
+ this.handleRequestClearCache.bind(this) as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
- this.handleRequestChangeConfiguration.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+ this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
- this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.DATA_TRANSFER,
+ this.handleRequestDataTransfer.bind(this) as unknown as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
- this.handleRequestSetChargingProfile.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE,
+ this.handleRequestGetCompositeSchedule.bind(this) as unknown as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
- this.handleRequestClearChargingProfile.bind(this) as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+ this.handleRequestGetConfiguration.bind(this) as IncomingRequestHandler,
],
[
- OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
- this.handleRequestChangeAvailability.bind(this) as unknown as IncomingRequestHandler,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+ this.handleRequestGetDiagnostics.bind(this) as IncomingRequestHandler,
],
[
OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
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<
ValidateFunction<JsonType>
>([
[
- OCPP16IncomingRequestCommand.RESET,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
- 'assets/json-schemas/ocpp/1.6/Reset.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
+ 'assets/json-schemas/ocpp/1.6/CancelReservation.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CLEAR_CACHE,
+ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
- 'assets/json-schemas/ocpp/1.6/ClearCache.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityRequest>(
+ 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
- 'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
+ 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+ OCPP16IncomingRequestCommand.CLEAR_CACHE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
- 'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearCacheRequest>(
+ 'assets/json-schemas/ocpp/1.6/ClearCache.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationRequest>(
- 'assets/json-schemas/ocpp/1.6/ChangeConfiguration.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileRequest>(
+ 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+ OCPP16IncomingRequestCommand.DATA_TRANSFER,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
- 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
+ 'assets/json-schemas/ocpp/1.6/DataTransfer.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+ OCPP16IncomingRequestCommand.GET_CONFIGURATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
- 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationRequest>(
+ 'assets/json-schemas/ocpp/1.6/GetConfiguration.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileRequest>(
- 'assets/json-schemas/ocpp/1.6/ClearChargingProfile.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsRequest>(
+ 'assets/json-schemas/ocpp/1.6/GetDiagnostics.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+ OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityRequest>(
- 'assets/json-schemas/ocpp/1.6/ChangeAvailability.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
+ 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+ OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStartTransactionRequest>(
- 'assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
+ 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+ OCPP16IncomingRequestCommand.RESERVE_NOW,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<RemoteStopTransactionRequest>(
- 'assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
+ 'assets/json-schemas/ocpp/1.6/ReserveNow.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ OCPP16IncomingRequestCommand.RESET,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
- 'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<ResetRequest>(
+ 'assets/json-schemas/ocpp/1.6/Reset.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.DATA_TRANSFER,
+ OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
- 'assets/json-schemas/ocpp/1.6/DataTransfer.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileRequest>(
+ 'assets/json-schemas/ocpp/1.6/SetChargingProfile.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
- 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageRequest>(
+ 'assets/json-schemas/ocpp/1.6/TriggerMessage.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.RESERVE_NOW,
+ OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowRequest>(
- 'assets/json-schemas/ocpp/1.6/ReserveNow.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorRequest>(
+ 'assets/json-schemas/ocpp/1.6/UnlockConnector.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16CancelReservationRequest>(
- 'assets/json-schemas/ocpp/1.6/CancelReservation.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareRequest>(
+ 'assets/json-schemas/ocpp/1.6/UpdateFirmware.json',
moduleName,
'constructor'
)
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:`,
this.validatePayload = this.validatePayload.bind(this)
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
+ private async handleRequestCancelReservation (
chargingStation: ChargingStation,
- messageId: string,
- commandName: OCPP16IncomingRequestCommand,
- commandPayload: ReqType
- ): Promise<void> {
- let response: ResType
+ commandPayload: OCPP16CancelReservationRequest
+ ): Promise<GenericResponse> {
if (
- chargingStation.stationInfo?.ocppStrictCompliance === true &&
- chargingStation.inPendingState() &&
- (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
- commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
- ) {
- throw new OCPPError(
- ErrorType.SECURITY_ERROR,
- `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
- commandPayload,
- undefined,
- 2
- )} while the charging station is in pending state on the central server`,
- commandName,
- commandPayload
+ !OCPP16ServiceUtils.checkFeatureProfile(
+ chargingStation,
+ OCPP16SupportedFeatureProfiles.Reservation,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION
)
- }
- if (
- chargingStation.isRegistered() ||
- (chargingStation.stationInfo?.ocppStrictCompliance === false &&
- chargingStation.inUnknownState())
) {
- if (
- this.incomingRequestHandlers.has(commandName) &&
- OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
- ) {
- try {
- this.validatePayload(chargingStation, commandName, commandPayload)
- // Call the method to build the response
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
- if (isAsyncFunction(incomingRequestHandler)) {
- response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
- } else {
- response = incomingRequestHandler(chargingStation, commandPayload) as ResType
- }
- } catch (error) {
- // Log
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
- error
- )
- throw error
- }
- } else {
- // Throw exception
- throw new OCPPError(
- ErrorType.NOT_IMPLEMENTED,
- `${commandName} is not implemented to handle request PDU ${JSON.stringify(
- commandPayload,
- undefined,
- 2
- )}`,
- commandName,
- commandPayload
+ return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
+ }
+ try {
+ const { reservationId } = commandPayload
+ const reservation = chargingStation.getReservationBy('reservationId', reservationId)
+ if (reservation == null) {
+ logger.debug(
+ `${chargingStation.logPrefix()} Reservation with id ${reservationId.toString()} does not exist on charging station`
)
+ return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
}
- } else {
- throw new OCPPError(
- ErrorType.SECURITY_ERROR,
- `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
- commandPayload,
- undefined,
- 2
- )} while the charging station is not registered on the central server`,
- commandName,
- commandPayload
+ await chargingStation.removeReservation(
+ reservation,
+ ReservationTerminationReason.RESERVATION_CANCELED
)
+ return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED
+ } catch (error) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return handleIncomingRequestError<GenericResponse>(
+ chargingStation,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ error as Error,
+ {
+ errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED,
+ }
+ )!
}
- // Send the built response
- await chargingStation.ocppRequestService.sendResponse(
- chargingStation,
- messageId,
- response,
- commandName
- )
- // Emit command name event to allow delayed handling
- this.emit(commandName, chargingStation, commandPayload, response)
- }
-
- private validatePayload (
- chargingStation: ChargingStation,
- commandName: OCPP16IncomingRequestCommand,
- commandPayload: JsonType
- ): boolean {
- if (this.payloadValidateFunctions.has(commandName)) {
- return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
- }
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
- )
- return false
}
- // Simulate charging station restart
- private handleRequestReset (
- chargingStation: ChargingStation,
- commandPayload: ResetRequest
- ): GenericResponse {
- const { type } = commandPayload
- chargingStation
- .reset(`${type}Reset` as OCPP16StopTransactionReason)
- .catch(Constants.EMPTY_FUNCTION)
- logger.info(
- `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.stationInfo!.resetTime!
- )}`
- )
- return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
- }
-
- private async handleRequestUnlockConnector (
+ private async handleRequestChangeAvailability (
chargingStation: ChargingStation,
- commandPayload: UnlockConnectorRequest
- ): Promise<UnlockConnectorResponse> {
- const { connectorId } = commandPayload
+ commandPayload: OCPP16ChangeAvailabilityRequest
+ ): Promise<OCPP16ChangeAvailabilityResponse> {
+ const { connectorId, type } = commandPayload
if (!chargingStation.hasConnector(connectorId)) {
logger.error(
- `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}`
+ `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}`
)
- return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+ return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
}
+ const chargePointStatus: OCPP16ChargePointStatus =
+ type === OCPP16AvailabilityType.Operative
+ ? OCPP16ChargePointStatus.Available
+ : OCPP16ChargePointStatus.Unavailable
if (connectorId === 0) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId.toString()}`
- )
- return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
- }
- if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
- const stopResponse = await chargingStation.stopTransactionOnConnector(
- connectorId,
- OCPP16StopTransactionReason.UNLOCK_COMMAND
- )
- if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
- return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
- }
- return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
- }
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Available
- )
- return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
- }
-
- private handleRequestGetConfiguration (
- chargingStation: ChargingStation,
- commandPayload: GetConfigurationRequest
- ): GetConfigurationResponse {
- const { key } = commandPayload
- const configurationKey: OCPPConfigurationKey[] = []
- const unknownKey: string[] = []
- if (key == null) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) {
- if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) {
- continue
+ let response: OCPP16ChangeAvailabilityResponse | undefined
+ if (chargingStation.hasEvses) {
+ for (const evseStatus of chargingStation.evses.values()) {
+ response = await OCPP16ServiceUtils.changeAvailability(
+ chargingStation,
+ [...evseStatus.connectors.keys()],
+ chargePointStatus,
+ type
+ )
}
- configurationKey.push({
- key: configKey.key,
- readonly: configKey.readonly,
- value: configKey.value,
- })
+ } else {
+ response = await OCPP16ServiceUtils.changeAvailability(
+ chargingStation,
+ [...chargingStation.connectors.keys()],
+ chargePointStatus,
+ type
+ )
}
- } else if (isNotEmptyArray(key)) {
- for (const k of key) {
- const keyFound = getConfigurationKey(chargingStation, k, true)
- if (keyFound != null) {
- if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) {
- continue
- }
- configurationKey.push({
- key: keyFound.key,
- readonly: keyFound.readonly,
- value: keyFound.value,
- })
- } else {
- unknownKey.push(k)
- }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return response!
+ } else if (
+ connectorId > 0 &&
+ (chargingStation.isChargingStationAvailable() ||
+ (!chargingStation.isChargingStationAvailable() &&
+ type === OCPP16AvailabilityType.Inoperative))
+ ) {
+ if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.getConnectorStatus(connectorId)!.availability = type
+ return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
}
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.getConnectorStatus(connectorId)!.availability = type
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ chargePointStatus
+ )
+ return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
}
- return {
- configurationKey,
- unknownKey,
- }
+ return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
}
private handleRequestChangeConfiguration (
return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_NOT_SUPPORTED
}
- private handleRequestSetChargingProfile (
+ private handleRequestClearChargingProfile (
chargingStation: ChargingStation,
- commandPayload: SetChargingProfileRequest
- ): SetChargingProfileResponse {
+ commandPayload: OCPP16ClearChargingProfileRequest
+ ): OCPP16ClearChargingProfileResponse {
if (
!OCPP16ServiceUtils.checkFeatureProfile(
chargingStation,
OCPP16SupportedFeatureProfiles.SmartCharging,
- OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
+ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
)
) {
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED
+ return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
}
- const { connectorId, csChargingProfiles } = commandPayload
- if (!chargingStation.hasConnector(connectorId)) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId.toString()}`
- )
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+ const { connectorId } = commandPayload
+ if (connectorId != null) {
+ if (!chargingStation.hasConnector(connectorId)) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId.toString()}`
+ )
+ return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (isNotEmptyArray(connectorStatus?.chargingProfiles)) {
+ connectorStatus.chargingProfiles = []
+ logger.debug(
+ `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId.toString()}`
+ )
+ return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
+ }
+ } else {
+ let clearedCP = false
+ if (chargingStation.hasEvses) {
+ for (const evseStatus of chargingStation.evses.values()) {
+ for (const status of evseStatus.connectors.values()) {
+ const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
+ chargingStation,
+ commandPayload,
+ status.chargingProfiles
+ )
+ if (clearedConnectorCP && !clearedCP) {
+ clearedCP = true
+ }
+ }
+ }
+ } else {
+ for (const id of chargingStation.connectors.keys()) {
+ const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
+ chargingStation,
+ commandPayload,
+ chargingStation.getConnectorStatus(id)?.chargingProfiles
+ )
+ if (clearedConnectorCP && !clearedCP) {
+ clearedCP = true
+ }
+ }
+ }
+ if (clearedCP) {
+ return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
+ }
}
- if (
- csChargingProfiles.chargingProfilePurpose ===
- OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
- connectorId !== 0
- ) {
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+ return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+ }
+
+ private handleRequestDataTransfer (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP16DataTransferRequest
+ ): OCPP16DataTransferResponse {
+ const { vendorId } = commandPayload
+ try {
+ if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) {
+ return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED
+ }
+ return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID
+ } catch (error) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return handleIncomingRequestError<OCPP16DataTransferResponse>(
+ chargingStation,
+ OCPP16IncomingRequestCommand.DATA_TRANSFER,
+ error as Error,
+ { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED }
+ )!
}
- if (
- csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
- connectorId === 0
- ) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()}`
- )
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
- }
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)
- if (
- csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
- connectorId > 0 &&
- connectorStatus?.transactionStarted === false
- ) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} without a started transaction`
- )
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
- }
- if (
- csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
- connectorId > 0 &&
- connectorStatus?.transactionStarted === true &&
- csChargingProfiles.transactionId != null &&
- csChargingProfiles.transactionId !== connectorStatus.transactionId
- ) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} with a different transaction id ${
- csChargingProfiles.transactionId.toString()
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- } than the started transaction id ${connectorStatus.transactionId?.toString()}`
- )
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
- }
- OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles)
- logger.debug(
- `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId.toString()}: %j`,
- csChargingProfiles
- )
- return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED
}
private handleRequestGetCompositeSchedule (
) {
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()}`
}
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(
}
if (compositeSchedule != null) {
return {
- status: GenericStatus.Accepted,
- scheduleStart: compositeSchedule.startSchedule,
- connectorId,
chargingSchedule: compositeSchedule,
+ connectorId,
+ scheduleStart: compositeSchedule.startSchedule,
+ status: GenericStatus.Accepted,
}
}
return OCPP16Constants.OCPP_RESPONSE_REJECTED
}
- private handleRequestClearChargingProfile (
+ private handleRequestGetConfiguration (
chargingStation: ChargingStation,
- commandPayload: OCPP16ClearChargingProfileRequest
- ): OCPP16ClearChargingProfileResponse {
+ commandPayload: GetConfigurationRequest
+ ): GetConfigurationResponse {
+ const { key } = commandPayload
+ const configurationKey: OCPPConfigurationKey[] = []
+ const unknownKey: string[] = []
+ if (key == null) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ for (const configKey of chargingStation.ocppConfiguration!.configurationKey!) {
+ if (!OCPP16ServiceUtils.isConfigurationKeyVisible(configKey)) {
+ continue
+ }
+ configurationKey.push({
+ key: configKey.key,
+ readonly: configKey.readonly,
+ value: configKey.value,
+ })
+ }
+ } else if (isNotEmptyArray(key)) {
+ for (const k of key) {
+ const keyFound = getConfigurationKey(chargingStation, k, true)
+ if (keyFound != null) {
+ if (!OCPP16ServiceUtils.isConfigurationKeyVisible(keyFound)) {
+ continue
+ }
+ configurationKey.push({
+ key: keyFound.key,
+ readonly: keyFound.readonly,
+ value: keyFound.value,
+ })
+ } else {
+ unknownKey.push(k)
+ }
+ }
+ }
+ return {
+ configurationKey,
+ unknownKey,
+ }
+ }
+
+ private async handleRequestGetDiagnostics (
+ chargingStation: ChargingStation,
+ commandPayload: GetDiagnosticsRequest
+ ): Promise<GetDiagnosticsResponse> {
if (
!OCPP16ServiceUtils.checkFeatureProfile(
chargingStation,
- OCPP16SupportedFeatureProfiles.SmartCharging,
- OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE
+ OCPP16SupportedFeatureProfiles.FirmwareManagement,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
)
) {
- return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_EMPTY
}
- const { connectorId } = commandPayload
- if (connectorId != null) {
- if (!chargingStation.hasConnector(connectorId)) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to clear a charging profile(s) to a non existing connector id ${connectorId.toString()}`
+ const { location } = commandPayload
+ const uri = new URL(location)
+ if (uri.protocol.startsWith('ftp:')) {
+ let ftpClient: Client | undefined
+ try {
+ const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+ ConfigurationSection.log
)
- return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
- }
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)
- if (isNotEmptyArray(connectorStatus?.chargingProfiles)) {
- connectorStatus.chargingProfiles = []
- logger.debug(
- `${chargingStation.logPrefix()} Charging profile(s) cleared on connector id ${connectorId.toString()}`
+ const logFiles = readdirSync(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ resolve((fileURLToPath(import.meta.url), '../', dirname(logConfiguration.file!)))
)
- return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
- }
- } else {
- let clearedCP = false
- if (chargingStation.hasEvses) {
- for (const evseStatus of chargingStation.evses.values()) {
- for (const status of evseStatus.connectors.values()) {
- const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
- chargingStation,
- commandPayload,
- status.chargingProfiles
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .filter(file => file.endsWith(extname(logConfiguration.file!)))
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .map(file => join(dirname(logConfiguration.file!), file))
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
+ create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
+ ftpClient = new Client()
+ const accessResponse = await ftpClient.access({
+ host: uri.hostname,
+ ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
+ ...(isNotEmptyString(uri.username) && { user: uri.username }),
+ ...(isNotEmptyString(uri.password) && { password: uri.password }),
+ })
+ let uploadResponse: FTPResponse | undefined
+ if (accessResponse.code === 220) {
+ ftpClient.trackProgress(info => {
+ logger.info(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${(
+ info.bytes / 1024
+ ).toString()} bytes transferred from diagnostics archive ${info.name}`
)
- if (clearedConnectorCP && !clearedCP) {
- clearedCP = true
- }
- }
- }
- } else {
- for (const id of chargingStation.connectors.keys()) {
- const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
- chargingStation,
- commandPayload,
- chargingStation.getConnectorStatus(id)?.chargingProfiles
+ chargingStation.ocppRequestService
+ .requestHandler<
+ OCPP16DiagnosticsStatusNotificationRequest,
+ OCPP16DiagnosticsStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+ status: OCPP16DiagnosticsStatus.Uploading,
+ })
+ .catch((error: unknown) => {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
+ OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
+ }'`,
+ error
+ )
+ })
+ })
+ uploadResponse = await ftpClient.uploadFrom(
+ join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive),
+ `${uri.pathname}${diagnosticsArchive}`
)
- if (clearedConnectorCP && !clearedCP) {
- clearedCP = true
+ if (uploadResponse.code === 226) {
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16DiagnosticsStatusNotificationRequest,
+ OCPP16DiagnosticsStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+ status: OCPP16DiagnosticsStatus.Uploaded,
+ })
+ ftpClient.close()
+ return { fileName: diagnosticsArchive }
}
+ throw new OCPPError(
+ ErrorType.GENERIC_ERROR,
+ `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse.code.toString()}`,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+ )
}
+ throw new OCPPError(
+ ErrorType.GENERIC_ERROR,
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse?.code.toString()}`,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+ )
+ } catch (error) {
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16DiagnosticsStatusNotificationRequest,
+ OCPP16DiagnosticsStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+ status: OCPP16DiagnosticsStatus.UploadFailed,
+ })
+ ftpClient?.close()
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return handleIncomingRequestError<GetDiagnosticsResponse>(
+ chargingStation,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+ error as Error,
+ { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY }
+ )!
}
- if (clearedCP) {
- return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_ACCEPTED
- }
+ } else {
+ logger.error(
+ `${chargingStation.logPrefix()} Unsupported protocol ${
+ uri.protocol
+ } to transfer the diagnostic logs archive`
+ )
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16DiagnosticsStatusNotificationRequest,
+ OCPP16DiagnosticsStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
+ status: OCPP16DiagnosticsStatus.UploadFailed,
+ })
+ return OCPP16Constants.OCPP_RESPONSE_EMPTY
}
- return OCPP16Constants.OCPP_CLEAR_CHARGING_PROFILE_RESPONSE_UNKNOWN
}
- private async handleRequestChangeAvailability (
- chargingStation: ChargingStation,
- commandPayload: OCPP16ChangeAvailabilityRequest
- ): Promise<OCPP16ChangeAvailabilityResponse> {
- const { connectorId, type } = commandPayload
- if (!chargingStation.hasConnector(connectorId)) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to change the availability of a non existing connector id ${connectorId.toString()}`
- )
- return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
- }
- const chargePointStatus: OCPP16ChargePointStatus =
- type === OCPP16AvailabilityType.Operative
- ? OCPP16ChargePointStatus.Available
- : OCPP16ChargePointStatus.Unavailable
- if (connectorId === 0) {
- let response: OCPP16ChangeAvailabilityResponse | undefined
- if (chargingStation.hasEvses) {
- for (const evseStatus of chargingStation.evses.values()) {
- response = await OCPP16ServiceUtils.changeAvailability(
- chargingStation,
- [...evseStatus.connectors.keys()],
- chargePointStatus,
- type
- )
- }
- } else {
- response = await OCPP16ServiceUtils.changeAvailability(
- chargingStation,
- [...chargingStation.connectors.keys()],
- chargePointStatus,
- type
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return response!
- } else if (
- connectorId > 0 &&
- (chargingStation.isChargingStationAvailable() ||
- (!chargingStation.isChargingStationAvailable() &&
- type === OCPP16AvailabilityType.Inoperative))
- ) {
- if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.getConnectorStatus(connectorId)!.availability = type
- return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_SCHEDULED
- }
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.getConnectorStatus(connectorId)!.availability = type
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- chargePointStatus
- )
- return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_ACCEPTED
- }
- return OCPP16Constants.OCPP_AVAILABILITY_RESPONSE_REJECTED
- }
-
- private async handleRequestRemoteStartTransaction (
+ private async handleRequestRemoteStartTransaction (
chargingStation: ChargingStation,
commandPayload: RemoteStartTransactionRequest
): Promise<GenericResponse> {
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,
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
return OCPP16Constants.OCPP_RESPONSE_REJECTED
}
- private handleRequestUpdateFirmware (
+ private async handleRequestReserveNow (
chargingStation: ChargingStation,
- commandPayload: OCPP16UpdateFirmwareRequest
- ): OCPP16UpdateFirmwareResponse {
+ commandPayload: OCPP16ReserveNowRequest
+ ): Promise<OCPP16ReserveNowResponse> {
if (
!OCPP16ServiceUtils.checkFeatureProfile(
chargingStation,
- OCPP16SupportedFeatureProfiles.FirmwareManagement,
- OCPP16IncomingRequestCommand.UPDATE_FIRMWARE
+ OCPP16SupportedFeatureProfiles.Reservation,
+ OCPP16IncomingRequestCommand.RESERVE_NOW
)
) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
- )
- return OCPP16Constants.OCPP_RESPONSE_EMPTY
+ return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
- const { retrieveDate } = commandPayload
- if (
- chargingStation.stationInfo?.firmwareStatus != null &&
- chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
- ) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
+ commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
+ const { connectorId, idTag, reservationId } = commandPayload
+ if (!chargingStation.hasConnector(connectorId)) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to reserve a non existing connector id ${connectorId.toString()}`
)
- return OCPP16Constants.OCPP_RESPONSE_EMPTY
+ return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
}
- const now = Date.now()
- if (retrieveDate.getTime() <= now) {
- this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
- } else {
- setTimeout(() => {
- this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
- }, retrieveDate.getTime() - now)
+ if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) {
+ return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
}
- return OCPP16Constants.OCPP_RESPONSE_EMPTY
- }
-
- private async updateFirmwareSimulation (
- chargingStation: ChargingStation,
- maxDelay = 30,
- minDelay = 15
- ): Promise<void> {
- if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
- return
+ if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) {
+ return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
}
- if (chargingStation.hasEvses) {
- for (const [evseId, evseStatus] of chargingStation.evses) {
- if (evseId > 0) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (connectorStatus.transactionStarted === false) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
- }
+ if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
+ return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
+ resetAuthorizeConnectorStatus(connectorStatus)
+ let response: OCPP16ReserveNowResponse
+ try {
+ await removeExpiredReservations(chargingStation)
+ switch (connectorStatus.status) {
+ case OCPP16ChargePointStatus.Faulted:
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED
+ break
+ case OCPP16ChargePointStatus.Preparing:
+ case OCPP16ChargePointStatus.Charging:
+ case OCPP16ChargePointStatus.SuspendedEV:
+ case OCPP16ChargePointStatus.SuspendedEVSE:
+ case OCPP16ChargePointStatus.Finishing:
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+ break
+ case OCPP16ChargePointStatus.Unavailable:
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE
+ break
+ case OCPP16ChargePointStatus.Reserved:
+ if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) {
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+ break
}
- }
- }
- } else {
- for (const connectorId of chargingStation.connectors.keys()) {
- if (
- connectorId > 0 &&
- chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
- ) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
- }
+ // eslint-disable-next-line no-fallthrough
+ default:
+ if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
+ break
+ }
+ await chargingStation.addReservation({
+ id: commandPayload.reservationId,
+ ...commandPayload,
+ })
+ response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED
+ break
}
+ return response
+ } catch (error) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return handleIncomingRequestError<OCPP16ReserveNowResponse>(
+ chargingStation,
+ OCPP16IncomingRequestCommand.RESERVE_NOW,
+ error as Error,
+ { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
+ )!
}
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16FirmwareStatusNotificationRequest,
- OCPP16FirmwareStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: OCPP16FirmwareStatus.Downloading,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading
+ }
+
+ // Simulate charging station restart
+ private handleRequestReset (
+ chargingStation: ChargingStation,
+ commandPayload: ResetRequest
+ ): GenericResponse {
+ const { type } = commandPayload
+ chargingStation
+ .reset(`${type}Reset` as OCPP16StopTransactionReason)
+ .catch(Constants.EMPTY_FUNCTION)
+ logger.info(
+ `${chargingStation.logPrefix()} ${type} reset command received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo!.resetTime!
+ )}`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
+ }
+
+ private handleRequestSetChargingProfile (
+ chargingStation: ChargingStation,
+ commandPayload: SetChargingProfileRequest
+ ): SetChargingProfileResponse {
if (
- chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
- OCPP16FirmwareStatus.DownloadFailed
+ !OCPP16ServiceUtils.checkFeatureProfile(
+ chargingStation,
+ OCPP16SupportedFeatureProfiles.SmartCharging,
+ OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE
+ )
) {
- await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16FirmwareStatusNotificationRequest,
- OCPP16FirmwareStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
- })
- chargingStation.stationInfo.firmwareStatus =
- chargingStation.stationInfo.firmwareUpgrade.failureStatus
- return
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_NOT_SUPPORTED
}
- await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16FirmwareStatusNotificationRequest,
- OCPP16FirmwareStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: OCPP16FirmwareStatus.Downloaded,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded
- let wasTransactionsStarted = false
- let transactionsStarted: boolean
- do {
- const runningTransactions = chargingStation.getNumberOfRunningTransactions()
- if (runningTransactions > 0) {
- const waitTime = secondsToMilliseconds(15)
- logger.debug(
- `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions.toString()} transaction(s) in progress, waiting ${formatDurationMilliSeconds(
- waitTime
- )} before continuing firmware update simulation`
- )
- await sleep(waitTime)
- transactionsStarted = true
- wasTransactionsStarted = true
- } else {
- if (chargingStation.hasEvses) {
- for (const [evseId, evseStatus] of chargingStation.evses) {
- if (evseId > 0) {
- for (const [connectorId, connectorStatus] of evseStatus.connectors) {
- if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
- }
- }
- }
- }
- } else {
- for (const connectorId of chargingStation.connectors.keys()) {
- if (
- connectorId > 0 &&
- chargingStation.getConnectorStatus(connectorId)?.status !==
- OCPP16ChargePointStatus.Unavailable
- ) {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Unavailable
- )
- }
- }
- }
- transactionsStarted = false
- }
- } while (transactionsStarted)
- !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
- if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
- return
+ const { connectorId, csChargingProfiles } = commandPayload
+ if (!chargingStation.hasConnector(connectorId)) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to set charging profile(s) to a non existing connector id ${connectorId.toString()}`
+ )
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
}
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16FirmwareStatusNotificationRequest,
- OCPP16FirmwareStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: OCPP16FirmwareStatus.Installing,
- })
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing
if (
- chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
- OCPP16FirmwareStatus.InstallationFailed
+ csChargingProfiles.chargingProfilePurpose ===
+ OCPP16ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE &&
+ connectorId !== 0
) {
- await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16FirmwareStatusNotificationRequest,
- OCPP16FirmwareStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
- status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
- })
- chargingStation.stationInfo.firmwareStatus =
- chargingStation.stationInfo.firmwareUpgrade.failureStatus
- return
- }
- if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
- await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
- await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
}
- }
-
- private async handleRequestGetDiagnostics (
- chargingStation: ChargingStation,
- commandPayload: GetDiagnosticsRequest
- ): Promise<GetDiagnosticsResponse> {
if (
- !OCPP16ServiceUtils.checkFeatureProfile(
- chargingStation,
- OCPP16SupportedFeatureProfiles.FirmwareManagement,
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
+ csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ connectorId === 0
+ ) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()}`
)
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
+ }
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (
+ csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ connectorId > 0 &&
+ connectorStatus?.transactionStarted === false
) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Cannot get diagnostics: feature profile not supported`
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} without a started transaction`
)
- return OCPP16Constants.OCPP_RESPONSE_EMPTY
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
}
- const { location } = commandPayload
- const uri = new URL(location)
- if (uri.protocol.startsWith('ftp:')) {
- let ftpClient: Client | undefined
- try {
- const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
- ConfigurationSection.log
- )
- const logFiles = readdirSync(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- resolve((fileURLToPath(import.meta.url), '../', dirname(logConfiguration.file!)))
- )
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- .filter(file => file.endsWith(extname(logConfiguration.file!)))
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- .map(file => join(dirname(logConfiguration.file!), file))
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- const diagnosticsArchive = `${chargingStation.stationInfo?.chargingStationId}_logs.tar.gz`
- create({ gzip: true }, logFiles).pipe(createWriteStream(diagnosticsArchive))
- ftpClient = new Client()
- const accessResponse = await ftpClient.access({
- host: uri.hostname,
- ...(isNotEmptyString(uri.port) && { port: convertToInt(uri.port) }),
- ...(isNotEmptyString(uri.username) && { user: uri.username }),
- ...(isNotEmptyString(uri.password) && { password: uri.password }),
- })
- let uploadResponse: FTPResponse | undefined
- if (accessResponse.code === 220) {
- ftpClient.trackProgress(info => {
- logger.info(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: ${(
- info.bytes / 1024
- ).toString()} bytes transferred from diagnostics archive ${info.name}`
- )
- chargingStation.ocppRequestService
- .requestHandler<
- OCPP16DiagnosticsStatusNotificationRequest,
- OCPP16DiagnosticsStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
- status: OCPP16DiagnosticsStatus.Uploading,
- })
- .catch((error: unknown) => {
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetDiagnostics: Error while sending '${
- OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION
- }'`,
- error
- )
- })
- })
- uploadResponse = await ftpClient.uploadFrom(
- join(resolve(dirname(fileURLToPath(import.meta.url)), '../'), diagnosticsArchive),
- `${uri.pathname}${diagnosticsArchive}`
- )
- if (uploadResponse.code === 226) {
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16DiagnosticsStatusNotificationRequest,
- OCPP16DiagnosticsStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
- status: OCPP16DiagnosticsStatus.Uploaded,
- })
- ftpClient.close()
- return { fileName: diagnosticsArchive }
- }
- throw new OCPPError(
- ErrorType.GENERIC_ERROR,
- `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse.code.toString()}`,
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
- )
- }
- throw new OCPPError(
- ErrorType.GENERIC_ERROR,
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `Diagnostics transfer failed with error code ${accessResponse.code.toString()}|${uploadResponse?.code.toString()}`,
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS
- )
- } catch (error) {
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16DiagnosticsStatusNotificationRequest,
- OCPP16DiagnosticsStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
- status: OCPP16DiagnosticsStatus.UploadFailed,
- })
- ftpClient?.close()
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return handleIncomingRequestError<GetDiagnosticsResponse>(
- chargingStation,
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
- error as Error,
- { errorResponse: OCPP16Constants.OCPP_RESPONSE_EMPTY }
- )!
- }
- } else {
+ if (
+ csChargingProfiles.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ connectorId > 0 &&
+ connectorStatus?.transactionStarted === true &&
+ csChargingProfiles.transactionId != null &&
+ csChargingProfiles.transactionId !== connectorStatus.transactionId
+ ) {
logger.error(
- `${chargingStation.logPrefix()} Unsupported protocol ${
- uri.protocol
- } to transfer the diagnostic logs archive`
+ `${chargingStation.logPrefix()} Trying to set transaction charging profile(s) on connector ${connectorId.toString()} with a different transaction id ${
+ csChargingProfiles.transactionId.toString()
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ } than the started transaction id ${connectorStatus.transactionId?.toString()}`
)
- await chargingStation.ocppRequestService.requestHandler<
- OCPP16DiagnosticsStatusNotificationRequest,
- OCPP16DiagnosticsStatusNotificationResponse
- >(chargingStation, OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, {
- status: OCPP16DiagnosticsStatus.UploadFailed,
- })
- return OCPP16Constants.OCPP_RESPONSE_EMPTY
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_REJECTED
}
+ OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, csChargingProfiles)
+ logger.debug(
+ `${chargingStation.logPrefix()} Charging profile(s) set on connector id ${connectorId.toString()}: %j`,
+ csChargingProfiles
+ )
+ return OCPP16Constants.OCPP_SET_CHARGING_PROFILE_RESPONSE_ACCEPTED
}
private handleRequestTriggerMessage (
chargingStation: ChargingStation,
commandPayload: OCPP16TriggerMessageRequest
): OCPP16TriggerMessageResponse {
- const { requestedMessage, connectorId } = commandPayload
+ const { connectorId, requestedMessage } = commandPayload
if (
!OCPP16ServiceUtils.checkFeatureProfile(
chargingStation,
}
}
- private handleRequestDataTransfer (
+ private async handleRequestUnlockConnector (
chargingStation: ChargingStation,
- commandPayload: OCPP16DataTransferRequest
- ): OCPP16DataTransferResponse {
- const { vendorId } = commandPayload
- try {
- if (Object.values(OCPP16DataTransferVendorId).includes(vendorId)) {
- return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_ACCEPTED
+ commandPayload: UnlockConnectorRequest
+ ): Promise<UnlockConnectorResponse> {
+ const { connectorId } = commandPayload
+ if (!chargingStation.hasConnector(connectorId)) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to unlock a non existing connector id ${connectorId.toString()}`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+ }
+ if (connectorId === 0) {
+ logger.error(
+ `${chargingStation.logPrefix()} Trying to unlock connector id ${connectorId.toString()}`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_UNLOCK_NOT_SUPPORTED
+ }
+ if (chargingStation.getConnectorStatus(connectorId)?.transactionStarted === true) {
+ const stopResponse = await chargingStation.stopTransactionOnConnector(
+ connectorId,
+ OCPP16StopTransactionReason.UNLOCK_COMMAND
+ )
+ if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+ return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
}
- return OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_UNKNOWN_VENDOR_ID
- } catch (error) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return handleIncomingRequestError<OCPP16DataTransferResponse>(
- chargingStation,
- OCPP16IncomingRequestCommand.DATA_TRANSFER,
- error as Error,
- { errorResponse: OCPP16Constants.OCPP_DATA_TRANSFER_RESPONSE_REJECTED }
- )!
+ return OCPP16Constants.OCPP_RESPONSE_UNLOCK_FAILED
}
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Available
+ )
+ return OCPP16Constants.OCPP_RESPONSE_UNLOCKED
}
- private async handleRequestReserveNow (
+ private handleRequestUpdateFirmware (
chargingStation: ChargingStation,
- commandPayload: OCPP16ReserveNowRequest
- ): Promise<OCPP16ReserveNowResponse> {
+ commandPayload: OCPP16UpdateFirmwareRequest
+ ): OCPP16UpdateFirmwareResponse {
if (
!OCPP16ServiceUtils.checkFeatureProfile(
chargingStation,
- OCPP16SupportedFeatureProfiles.Reservation,
- OCPP16IncomingRequestCommand.RESERVE_NOW
+ OCPP16SupportedFeatureProfiles.FirmwareManagement,
+ OCPP16IncomingRequestCommand.UPDATE_FIRMWARE
)
) {
- return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: feature profile not supported`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_EMPTY
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- commandPayload.expiryDate = convertToDate(commandPayload.expiryDate)!
- const { reservationId, idTag, connectorId } = commandPayload
- if (!chargingStation.hasConnector(connectorId)) {
- logger.error(
- `${chargingStation.logPrefix()} Trying to reserve a non existing connector id ${connectorId.toString()}`
+ commandPayload.retrieveDate = convertToDate(commandPayload.retrieveDate)!
+ const { retrieveDate } = commandPayload
+ if (
+ chargingStation.stationInfo?.firmwareStatus != null &&
+ chargingStation.stationInfo.firmwareStatus !== OCPP16FirmwareStatus.Installed
+ ) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestUpdateFirmware: Cannot simulate firmware update: firmware update is already in progress`
)
- return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ return OCPP16Constants.OCPP_RESPONSE_EMPTY
}
- if (connectorId > 0 && !chargingStation.isConnectorAvailable(connectorId)) {
- return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ const now = Date.now()
+ if (retrieveDate.getTime() <= now) {
+ this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
+ } else {
+ setTimeout(() => {
+ this.updateFirmwareSimulation(chargingStation).catch(Constants.EMPTY_FUNCTION)
+ }, retrieveDate.getTime() - now)
}
- if (connectorId === 0 && !chargingStation.getReserveConnectorZeroSupported()) {
- return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ return OCPP16Constants.OCPP_RESPONSE_EMPTY
+ }
+
+ private notifyRemoteStartTransactionRejected (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ idTag: string
+ ): GenericResponse {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ logger.debug(
+ `${chargingStation.logPrefix()} Remote start transaction REJECTED on ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ chargingStation.stationInfo?.chargingStationId
+ }#${connectorId.toString()}, idTag '${idTag}', availability '${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ connectorStatus?.availability
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }', status '${connectorStatus?.status}'`
+ )
+ return OCPP16Constants.OCPP_RESPONSE_REJECTED
+ }
+
+ private setRemoteStartTransactionChargingProfile (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ chargingProfile: OCPP16ChargingProfile
+ ): boolean {
+ if (
+ chargingProfile.chargingProfilePurpose === OCPP16ChargingProfilePurposeType.TX_PROFILE &&
+ chargingProfile.transactionId == null
+ ) {
+ OCPP16ServiceUtils.setChargingProfile(chargingStation, connectorId, chargingProfile)
+ logger.debug(
+ `${chargingStation.logPrefix()} Charging profile(s) set at remote start transaction on ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ chargingStation.stationInfo?.chargingStationId
+ }#${connectorId.toString()}`,
+ chargingProfile
+ )
+ return true
}
- if (!(await OCPP16ServiceUtils.isIdTagAuthorized(chargingStation, connectorId, idTag))) {
- return OCPP16Constants.OCPP_RESERVATION_RESPONSE_REJECTED
+ logger.debug(
+ `${chargingStation.logPrefix()} Not allowed to set ${
+ chargingProfile.chargingProfilePurpose
+ } charging profile(s)${chargingProfile.transactionId != null ? ' with transactionId set' : ''} at remote start transaction`
+ )
+ return false
+ }
+
+ private async updateFirmwareSimulation (
+ chargingStation: ChargingStation,
+ maxDelay = 30,
+ minDelay = 15
+ ): Promise<void> {
+ if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
+ return
}
+ if (chargingStation.hasEvses) {
+ for (const [evseId, evseStatus] of chargingStation.evses) {
+ if (evseId > 0) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ if (connectorStatus.transactionStarted === false) {
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Unavailable
+ )
+ }
+ }
+ }
+ }
+ } else {
+ for (const connectorId of chargingStation.connectors.keys()) {
+ if (
+ connectorId > 0 &&
+ chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false
+ ) {
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Unavailable
+ )
+ }
+ }
+ }
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16FirmwareStatusNotificationRequest,
+ OCPP16FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: OCPP16FirmwareStatus.Downloading,
+ })
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)!
- resetAuthorizeConnectorStatus(connectorStatus)
- let response: OCPP16ReserveNowResponse
- try {
- await removeExpiredReservations(chargingStation)
- switch (connectorStatus.status) {
- case OCPP16ChargePointStatus.Faulted:
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED
- break
- case OCPP16ChargePointStatus.Preparing:
- case OCPP16ChargePointStatus.Charging:
- case OCPP16ChargePointStatus.SuspendedEV:
- case OCPP16ChargePointStatus.SuspendedEVSE:
- case OCPP16ChargePointStatus.Finishing:
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
- break
- case OCPP16ChargePointStatus.Unavailable:
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_UNAVAILABLE
- break
- case OCPP16ChargePointStatus.Reserved:
- if (!chargingStation.isConnectorReservable(reservationId, idTag, connectorId)) {
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
- break
+ chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloading
+ if (
+ chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
+ OCPP16FirmwareStatus.DownloadFailed
+ ) {
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16FirmwareStatusNotificationRequest,
+ OCPP16FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
+ })
+ chargingStation.stationInfo.firmwareStatus =
+ chargingStation.stationInfo.firmwareUpgrade.failureStatus
+ return
+ }
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16FirmwareStatusNotificationRequest,
+ OCPP16FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: OCPP16FirmwareStatus.Downloaded,
+ })
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Downloaded
+ let wasTransactionsStarted = false
+ let transactionsStarted: boolean
+ do {
+ const runningTransactions = chargingStation.getNumberOfRunningTransactions()
+ if (runningTransactions > 0) {
+ const waitTime = secondsToMilliseconds(15)
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.updateFirmwareSimulation: ${runningTransactions.toString()} transaction(s) in progress, waiting ${formatDurationMilliSeconds(
+ waitTime
+ )} before continuing firmware update simulation`
+ )
+ await sleep(waitTime)
+ transactionsStarted = true
+ wasTransactionsStarted = true
+ } else {
+ if (chargingStation.hasEvses) {
+ for (const [evseId, evseStatus] of chargingStation.evses) {
+ if (evseId > 0) {
+ for (const [connectorId, connectorStatus] of evseStatus.connectors) {
+ if (connectorStatus.status !== OCPP16ChargePointStatus.Unavailable) {
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Unavailable
+ )
+ }
+ }
+ }
}
- // eslint-disable-next-line no-fallthrough
- default:
- if (!chargingStation.isConnectorReservable(reservationId, idTag)) {
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_OCCUPIED
- break
+ } else {
+ for (const connectorId of chargingStation.connectors.keys()) {
+ if (
+ connectorId > 0 &&
+ chargingStation.getConnectorStatus(connectorId)?.status !==
+ OCPP16ChargePointStatus.Unavailable
+ ) {
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Unavailable
+ )
+ }
}
- await chargingStation.addReservation({
- id: commandPayload.reservationId,
- ...commandPayload,
- })
- response = OCPP16Constants.OCPP_RESERVATION_RESPONSE_ACCEPTED
- break
+ }
+ transactionsStarted = false
}
- return response
- } catch (error) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- chargingStation.getConnectorStatus(connectorId)!.status = OCPP16ChargePointStatus.Available
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return handleIncomingRequestError<OCPP16ReserveNowResponse>(
- chargingStation,
- OCPP16IncomingRequestCommand.RESERVE_NOW,
- error as Error,
- { errorResponse: OCPP16Constants.OCPP_RESERVATION_RESPONSE_FAULTED }
- )!
+ } while (transactionsStarted)
+ !wasTransactionsStarted && (await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay))))
+ if (!checkChargingStationState(chargingStation, chargingStation.logPrefix())) {
+ return
+ }
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16FirmwareStatusNotificationRequest,
+ OCPP16FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: OCPP16FirmwareStatus.Installing,
+ })
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ chargingStation.stationInfo!.firmwareStatus = OCPP16FirmwareStatus.Installing
+ if (
+ chargingStation.stationInfo?.firmwareUpgrade?.failureStatus ===
+ OCPP16FirmwareStatus.InstallationFailed
+ ) {
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+ await chargingStation.ocppRequestService.requestHandler<
+ OCPP16FirmwareStatusNotificationRequest,
+ OCPP16FirmwareStatusNotificationResponse
+ >(chargingStation, OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
+ status: chargingStation.stationInfo.firmwareUpgrade.failureStatus,
+ })
+ chargingStation.stationInfo.firmwareStatus =
+ chargingStation.stationInfo.firmwareUpgrade.failureStatus
+ return
+ }
+ if (chargingStation.stationInfo?.firmwareUpgrade?.reset === true) {
+ await sleep(secondsToMilliseconds(randomInt(minDelay, maxDelay)))
+ await chargingStation.reset(OCPP16StopTransactionReason.REBOOT)
}
}
- private async handleRequestCancelReservation (
+ private validatePayload (
chargingStation: ChargingStation,
- commandPayload: OCPP16CancelReservationRequest
- ): Promise<GenericResponse> {
+ commandName: OCPP16IncomingRequestCommand,
+ commandPayload: JsonType
+ ): boolean {
+ if (this.payloadValidateFunctions.has(commandName)) {
+ return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
+ }
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+ )
+ return false
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
+ chargingStation: ChargingStation,
+ messageId: string,
+ commandName: OCPP16IncomingRequestCommand,
+ commandPayload: ReqType
+ ): Promise<void> {
+ let response: ResType
if (
- !OCPP16ServiceUtils.checkFeatureProfile(
- chargingStation,
- OCPP16SupportedFeatureProfiles.Reservation,
- OCPP16IncomingRequestCommand.CANCEL_RESERVATION
- )
+ chargingStation.stationInfo?.ocppStrictCompliance === true &&
+ chargingStation.inPendingState() &&
+ (commandName === OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION ||
+ commandName === OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION)
) {
- return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
+ throw new OCPPError(
+ ErrorType.SECURITY_ERROR,
+ `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+ commandPayload,
+ undefined,
+ 2
+ )} while the charging station is in pending state on the central server`,
+ commandName,
+ commandPayload
+ )
}
- try {
- const { reservationId } = commandPayload
- const reservation = chargingStation.getReservationBy('reservationId', reservationId)
- if (reservation == null) {
- logger.debug(
- `${chargingStation.logPrefix()} Reservation with id ${reservationId.toString()} does not exist on charging station`
+ if (
+ chargingStation.isRegistered() ||
+ (chargingStation.stationInfo?.ocppStrictCompliance === false &&
+ chargingStation.inUnknownState())
+ ) {
+ if (
+ this.incomingRequestHandlers.has(commandName) &&
+ OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName)
+ ) {
+ try {
+ this.validatePayload(chargingStation, commandName, commandPayload)
+ // Call the method to build the response
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)!
+ if (isAsyncFunction(incomingRequestHandler)) {
+ response = (await incomingRequestHandler(chargingStation, commandPayload)) as ResType
+ } else {
+ response = incomingRequestHandler(chargingStation, commandPayload) as ResType
+ }
+ } catch (error) {
+ // Log
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: Handle incoming request error:`,
+ error
+ )
+ throw error
+ }
+ } else {
+ // Throw exception
+ throw new OCPPError(
+ ErrorType.NOT_IMPLEMENTED,
+ `${commandName} is not implemented to handle request PDU ${JSON.stringify(
+ commandPayload,
+ undefined,
+ 2
+ )}`,
+ commandName,
+ commandPayload
)
- return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED
}
- await chargingStation.removeReservation(
- reservation,
- ReservationTerminationReason.RESERVATION_CANCELED
+ } else {
+ throw new OCPPError(
+ ErrorType.SECURITY_ERROR,
+ `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
+ commandPayload,
+ undefined,
+ 2
+ )} while the charging station is not registered on the central server`,
+ commandName,
+ commandPayload
)
- return OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_ACCEPTED
- } catch (error) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return handleIncomingRequestError<GenericResponse>(
- chargingStation,
- OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
- error as Error,
- {
- errorResponse: OCPP16Constants.OCPP_CANCEL_RESERVATION_RESPONSE_REJECTED,
- }
- )!
}
+ // Send the built response
+ await chargingStation.ocppRequestService.sendResponse(
+ chargingStation,
+ messageId,
+ response,
+ commandName
+ )
+ // Emit command name event to allow delayed handling
+ this.emit(commandName, chargingStation, commandPayload, response)
}
}
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,
} 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'
),
],
[
- OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+ OCPP16RequestCommand.DATA_TRANSFER,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationRequest>(
- 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
+ 'assets/json-schemas/ocpp/1.6/DataTransfer.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.HEARTBEAT,
+ OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatRequest>(
- 'assets/json-schemas/ocpp/1.6/Heartbeat.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationRequest>(
+ 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotification.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.METER_VALUES,
+ OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16MeterValuesRequest>(
- 'assets/json-schemas/ocpp/1.6/MeterValues.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationRequest>(
+ 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.STATUS_NOTIFICATION,
+ OCPP16RequestCommand.HEARTBEAT,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationRequest>(
- 'assets/json-schemas/ocpp/1.6/StatusNotification.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatRequest>(
+ 'assets/json-schemas/ocpp/1.6/Heartbeat.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.START_TRANSACTION,
+ OCPP16RequestCommand.METER_VALUES,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionRequest>(
- 'assets/json-schemas/ocpp/1.6/StartTransaction.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16MeterValuesRequest>(
+ 'assets/json-schemas/ocpp/1.6/MeterValues.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.STOP_TRANSACTION,
+ OCPP16RequestCommand.START_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionRequest>(
- 'assets/json-schemas/ocpp/1.6/StopTransaction.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionRequest>(
+ 'assets/json-schemas/ocpp/1.6/StartTransaction.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.DATA_TRANSFER,
+ OCPP16RequestCommand.STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferRequest>(
- 'assets/json-schemas/ocpp/1.6/DataTransfer.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationRequest>(
+ 'assets/json-schemas/ocpp/1.6/StatusNotification.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+ OCPP16RequestCommand.STOP_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationRequest>(
- 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotification.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionRequest>(
+ 'assets/json-schemas/ocpp/1.6/StopTransaction.json',
moduleName,
'constructor'
)
this.buildRequestPayload = this.buildRequestPayload.bind(this)
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
- chargingStation: ChargingStation,
- commandName: OCPP16RequestCommand,
- commandParams?: RequestType,
- params?: RequestParams
- ): Promise<ResponseType> {
- // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc.
- if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) {
- // Pre request actions hook
- switch (commandName) {
- case OCPP16RequestCommand.START_TRANSACTION:
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- (commandParams as OCPP16StartTransactionRequest).connectorId,
- OCPP16ChargePointStatus.Preparing
- )
- break
- }
- return (await this.sendMessage(
- chargingStation,
- generateUUID(),
- this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams),
- commandName,
- params
- )) as ResponseType
- }
- // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
- throw new OCPPError(
- ErrorType.NOT_SUPPORTED,
- `Unsupported OCPP command ${commandName}`,
- commandName,
- commandParams
- )
- }
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
private buildRequestPayload<Request extends JsonType>(
chargingStation: ChargingStation,
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:
)
}
}
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
+ chargingStation: ChargingStation,
+ commandName: OCPP16RequestCommand,
+ commandParams?: RequestType,
+ params?: RequestParams
+ ): Promise<ResponseType> {
+ // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc.
+ if (OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) {
+ // Pre request actions hook
+ switch (commandName) {
+ case OCPP16RequestCommand.START_TRANSACTION:
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ (commandParams as OCPP16StartTransactionRequest).connectorId,
+ OCPP16ChargePointStatus.Preparing
+ )
+ break
+ }
+ return (await this.sendMessage(
+ chargingStation,
+ generateUUID(),
+ this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams),
+ commandName,
+ params
+ )) as ResponseType
+ }
+ // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+ throw new OCPPError(
+ ErrorType.NOT_SUPPORTED,
+ `Unsupported OCPP command ${commandName}`,
+ commandName,
+ commandParams
+ )
+ }
}
// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
import type { ValidateFunction } from 'ajv'
+
import { secondsToMilliseconds } from 'date-fns'
import {
const moduleName = 'OCPP16ResponseService'
export class OCPP16ResponseService extends OCPPResponseService {
+ protected payloadValidateFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
+
+ private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
public incomingRequestResponsePayloadValidateFunctions: Map<
OCPP16IncomingRequestCommand,
ValidateFunction<JsonType>
>
- protected payloadValidateFunctions: Map<OCPP16RequestCommand, ValidateFunction<JsonType>>
- private readonly responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>
-
public constructor () {
// if (new.target.name === moduleName) {
// throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
// }
super(OCPPVersion.VERSION_16)
this.responseHandlers = new Map<OCPP16RequestCommand, ResponseHandler>([
+ [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
[
OCPP16RequestCommand.BOOT_NOTIFICATION,
this.handleResponseBootNotification.bind(this) as ResponseHandler,
],
+ [OCPP16RequestCommand.DATA_TRANSFER, this.emptyResponseHandler],
+ [
+ OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+ this.emptyResponseHandler.bind(this) as ResponseHandler,
+ ],
+ [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.emptyResponseHandler],
[OCPP16RequestCommand.HEARTBEAT, this.emptyResponseHandler],
- [OCPP16RequestCommand.AUTHORIZE, this.handleResponseAuthorize.bind(this) as ResponseHandler],
+ [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler],
[
OCPP16RequestCommand.START_TRANSACTION,
this.handleResponseStartTransaction.bind(this) as ResponseHandler,
],
- [
- OCPP16RequestCommand.STOP_TRANSACTION,
- this.handleResponseStopTransaction.bind(this) as ResponseHandler,
- ],
[
OCPP16RequestCommand.STATUS_NOTIFICATION,
this.emptyResponseHandler.bind(this) as ResponseHandler,
],
- [OCPP16RequestCommand.METER_VALUES, this.emptyResponseHandler],
[
- OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
- this.emptyResponseHandler.bind(this) as ResponseHandler,
+ OCPP16RequestCommand.STOP_TRANSACTION,
+ this.handleResponseStopTransaction.bind(this) as ResponseHandler,
],
- [OCPP16RequestCommand.DATA_TRANSFER, this.emptyResponseHandler],
- [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, this.emptyResponseHandler],
])
this.payloadValidateFunctions = new Map<OCPP16RequestCommand, ValidateFunction<JsonType>>([
[
- OCPP16RequestCommand.BOOT_NOTIFICATION,
+ OCPP16RequestCommand.AUTHORIZE,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16BootNotificationResponse>(
- 'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16AuthorizeResponse>(
+ 'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.HEARTBEAT,
+ OCPP16RequestCommand.BOOT_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatResponse>(
- 'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16BootNotificationResponse>(
+ 'assets/json-schemas/ocpp/1.6/BootNotificationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.AUTHORIZE,
+ OCPP16RequestCommand.DATA_TRANSFER,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16AuthorizeResponse>(
- 'assets/json-schemas/ocpp/1.6/AuthorizeResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
+ 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.START_TRANSACTION,
+ OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionResponse>(
- 'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationResponse>(
+ 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.STOP_TRANSACTION,
+ OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionResponse>(
- 'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationResponse>(
+ 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.STATUS_NOTIFICATION,
+ OCPP16RequestCommand.HEARTBEAT,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationResponse>(
- 'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16HeartbeatResponse>(
+ 'assets/json-schemas/ocpp/1.6/HeartbeatResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
+ OCPP16RequestCommand.START_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DiagnosticsStatusNotificationResponse>(
- 'assets/json-schemas/ocpp/1.6/DiagnosticsStatusNotificationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StartTransactionResponse>(
+ 'assets/json-schemas/ocpp/1.6/StartTransactionResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.DATA_TRANSFER,
+ OCPP16RequestCommand.STATUS_NOTIFICATION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
- 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StatusNotificationResponse>(
+ 'assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
+ OCPP16RequestCommand.STOP_TRANSACTION,
this.ajv.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16FirmwareStatusNotificationResponse>(
- 'assets/json-schemas/ocpp/1.6/FirmwareStatusNotificationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16StopTransactionResponse>(
+ 'assets/json-schemas/ocpp/1.6/StopTransactionResponse.json',
moduleName,
'constructor'
)
ValidateFunction<JsonType>
>([
[
- OCPP16IncomingRequestCommand.RESET,
+ OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
this.ajvIncomingRequest.compile(
OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
- 'assets/json-schemas/ocpp/1.6/ResetResponse.json',
+ 'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CLEAR_CACHE,
+ OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
- 'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityResponse>(
+ 'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+ OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ChangeAvailabilityResponse>(
- 'assets/json-schemas/ocpp/1.6/ChangeAvailabilityResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationResponse>(
+ 'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+ OCPP16IncomingRequestCommand.CLEAR_CACHE,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorResponse>(
- 'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
+ 'assets/json-schemas/ocpp/1.6/ClearCacheResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+ OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationResponse>(
- 'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileResponse>(
+ 'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+ OCPP16IncomingRequestCommand.DATA_TRANSFER,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<ChangeConfigurationResponse>(
- 'assets/json-schemas/ocpp/1.6/ChangeConfigurationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
+ 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+ OCPP16IncomingRequestCommand.GET_CONFIGURATION,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
- 'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GetConfigurationResponse>(
+ 'assets/json-schemas/ocpp/1.6/GetConfigurationResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+ OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ClearChargingProfileResponse>(
- 'assets/json-schemas/ocpp/1.6/ClearChargingProfileResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsResponse>(
+ 'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+ OCPP16IncomingRequestCommand.RESERVE_NOW,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GetDiagnosticsResponse>(
- 'assets/json-schemas/ocpp/1.6/GetDiagnosticsResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
+ 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+ OCPP16IncomingRequestCommand.RESET,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageResponse>(
- 'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
+ 'assets/json-schemas/ocpp/1.6/ResetResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.DATA_TRANSFER,
+ OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16DataTransferResponse>(
- 'assets/json-schemas/ocpp/1.6/DataTransferResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<SetChargingProfileResponse>(
+ 'assets/json-schemas/ocpp/1.6/SetChargingProfileResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
+ OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareResponse>(
- 'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16TriggerMessageResponse>(
+ 'assets/json-schemas/ocpp/1.6/TriggerMessageResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.RESERVE_NOW,
+ OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16ReserveNowResponse>(
- 'assets/json-schemas/ocpp/1.6/ReserveNowResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<UnlockConnectorResponse>(
+ 'assets/json-schemas/ocpp/1.6/UnlockConnectorResponse.json',
moduleName,
'constructor'
)
),
],
[
- OCPP16IncomingRequestCommand.CANCEL_RESERVATION,
+ OCPP16IncomingRequestCommand.UPDATE_FIRMWARE,
this.ajvIncomingRequest.compile(
- OCPP16ServiceUtils.parseJsonSchemaFile<GenericResponse>(
- 'assets/json-schemas/ocpp/1.6/CancelReservationResponse.json',
+ OCPP16ServiceUtils.parseJsonSchemaFile<OCPP16UpdateFirmwareResponse>(
+ 'assets/json-schemas/ocpp/1.6/UpdateFirmwareResponse.json',
moduleName,
'constructor'
)
this.validatePayload = this.validatePayload.bind(this)
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
- chargingStation: ChargingStation,
- commandName: OCPP16RequestCommand,
- payload: ResType,
- requestPayload: ReqType
- ): Promise<void> {
- if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) {
- if (
- this.responseHandlers.has(commandName) &&
- OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)
- ) {
- try {
- this.validatePayload(chargingStation, commandName, payload)
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const responseHandler = this.responseHandlers.get(commandName)!
- if (isAsyncFunction(responseHandler)) {
- await responseHandler(chargingStation, payload, requestPayload)
- } else {
- ;(
- responseHandler as (
- chargingStation: ChargingStation,
- payload: JsonType,
- requestPayload?: JsonType
- ) => void
- )(chargingStation, payload, requestPayload)
- }
- } catch (error) {
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
- error
- )
- throw error
- }
- } else {
- // Throw exception
- throw new OCPPError(
- ErrorType.NOT_IMPLEMENTED,
- `${commandName} is not implemented to handle response PDU ${JSON.stringify(
- payload,
- undefined,
- 2
- )}`,
- commandName,
- payload
- )
- }
- } else {
- throw new OCPPError(
- ErrorType.SECURITY_ERROR,
- `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
- payload,
- undefined,
- 2
- )} while the charging station is not registered on the central server`,
- commandName,
- payload
- )
- }
- }
-
- private validatePayload (
- chargingStation: ChargingStation,
- commandName: OCPP16RequestCommand,
- payload: JsonType
- ): boolean {
- if (this.payloadValidateFunctions.has(commandName)) {
- return this.validateResponsePayload(chargingStation, commandName, payload)
- }
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
- )
- return false
- }
-
- private handleResponseBootNotification (
- chargingStation: ChargingStation,
- payload: OCPP16BootNotificationResponse
- ): void {
- if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
- chargingStation.bootNotificationResponse = payload
- if (chargingStation.isRegistered()) {
- chargingStation.emit(ChargingStationEvents.registered)
- if (chargingStation.inAcceptedState()) {
- addConfigurationKey(
- chargingStation,
- OCPP16StandardParametersKey.HeartbeatInterval,
- payload.interval.toString(),
- {},
- { overwrite: true, save: true }
- )
- addConfigurationKey(
- chargingStation,
- OCPP16StandardParametersKey.HeartBeatInterval,
- payload.interval.toString(),
- { visible: false },
- { overwrite: true, save: true }
- )
- chargingStation.emit(ChargingStationEvents.accepted)
- }
- } else if (chargingStation.inRejectedState()) {
- chargingStation.emit(ChargingStationEvents.rejected)
- }
- const logMsg = `${chargingStation.logPrefix()} Charging station in '${
- payload.status
- }' state on the central server`
- payload.status === RegistrationStatusEnumType.REJECTED
- ? logger.warn(logMsg)
- : logger.info(logMsg)
- } else {
- delete chargingStation.bootNotificationResponse
- logger.error(
- `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
- payload
- )
- }
- }
-
private handleResponseAuthorize (
chargingStation: ChargingStation,
payload: OCPP16AuthorizeResponse,
}
}
+ 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,
OCPP16MeterValuesResponse
>(chargingStation, OCPP16RequestCommand.METER_VALUES, {
connectorId,
- transactionId: payload.transactionId,
meterValue: [connectorStatus.transactionBeginMeterValue],
+ transactionId: payload.transactionId,
} satisfies OCPP16MeterValuesRequest))
await OCPP16ServiceUtils.sendAndSetConnectorStatus(
chargingStation,
}
}
- private async resetConnectorOnStartTransactionError (
- chargingStation: ChargingStation,
- connectorId: number
- ): Promise<void> {
- chargingStation.stopMeterValues(connectorId)
- const connectorStatus = chargingStation.getConnectorStatus(connectorId)
- resetConnectorStatus(connectorStatus)
- await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
- }
-
private async handleResponseStopTransaction (
chargingStation: ChargingStation,
payload: OCPP16StopTransactionResponse,
OCPP16MeterValuesResponse
>(chargingStation, OCPP16RequestCommand.METER_VALUES, {
connectorId: transactionConnectorId,
- transactionId: requestPayload.transactionId,
meterValue: [
OCPP16ServiceUtils.buildTransactionEndMeterValue(
chargingStation,
requestPayload.meterStop
),
],
+ transactionId: requestPayload.transactionId,
}))
if (
!chargingStation.isChargingStationAvailable() ||
logger.warn(logMsg)
}
}
+
+ private async resetConnectorOnStartTransactionError (
+ chargingStation: ChargingStation,
+ connectorId: number
+ ): Promise<void> {
+ chargingStation.stopMeterValues(connectorId)
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ resetConnectorStatus(connectorStatus)
+ await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus)
+ }
+
+ private validatePayload (
+ chargingStation: ChargingStation,
+ commandName: OCPP16RequestCommand,
+ payload: JsonType
+ ): boolean {
+ if (this.payloadValidateFunctions.has(commandName)) {
+ return this.validateResponsePayload(chargingStation, commandName, payload)
+ }
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+ )
+ return false
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
+ chargingStation: ChargingStation,
+ commandName: OCPP16RequestCommand,
+ payload: ResType,
+ requestPayload: ReqType
+ ): Promise<void> {
+ if (chargingStation.isRegistered() || commandName === OCPP16RequestCommand.BOOT_NOTIFICATION) {
+ if (
+ this.responseHandlers.has(commandName) &&
+ OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName)
+ ) {
+ try {
+ this.validatePayload(chargingStation, commandName, payload)
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const responseHandler = this.responseHandlers.get(commandName)!
+ if (isAsyncFunction(responseHandler)) {
+ await responseHandler(chargingStation, payload, requestPayload)
+ } else {
+ ;(
+ responseHandler as (
+ chargingStation: ChargingStation,
+ payload: JsonType,
+ requestPayload?: JsonType
+ ) => void
+ )(chargingStation, payload, requestPayload)
+ }
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handle response error:`,
+ error
+ )
+ throw error
+ }
+ } else {
+ // Throw exception
+ throw new OCPPError(
+ ErrorType.NOT_IMPLEMENTED,
+ `${commandName} is not implemented to handle response PDU ${JSON.stringify(
+ payload,
+ undefined,
+ 2
+ )}`,
+ commandName,
+ payload
+ )
+ }
+ } else {
+ throw new OCPPError(
+ ErrorType.SECURITY_ERROR,
+ `${commandName} cannot be issued to handle response PDU ${JSON.stringify(
+ payload,
+ undefined,
+ 2
+ )} while the charging station is not registered on the central server`,
+ commandName,
+ payload
+ )
+ }
+ }
}
// Partial Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
import type { JSONSchemaType } from 'ajv'
+
import {
addSeconds,
areIntervalsOverlapping,
import { OCPP16Constants } from './OCPP16Constants.js'
export class OCPP16ServiceUtils extends OCPPServiceUtils {
- public static checkFeatureProfile (
- chargingStation: ChargingStation,
- featureProfile: OCPP16SupportedFeatureProfiles,
- command: OCPP16RequestCommand | OCPP16IncomingRequestCommand
- ): boolean {
- if (hasFeatureProfile(chargingStation, featureProfile) === false) {
- logger.warn(
- `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
- OCPP16StandardParametersKey.SupportedFeatureProfiles
- } in configuration`
- )
- return false
- }
- return true
- }
-
- public static buildTransactionBeginMeterValue (
- chargingStation: ChargingStation,
- connectorId: number,
- meterStart: number | undefined
- ): OCPP16MeterValue {
- const meterValue: OCPP16MeterValue = {
- timestamp: new Date(),
- sampledValue: [],
- }
- // Energy.Active.Import.Register measurand (default)
- const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
- chargingStation,
- connectorId
- )
- const unitDivider =
- sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
- meterValue.sampledValue.push(
- OCPP16ServiceUtils.buildSampledValue(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- sampledValueTemplate!,
- roundTo((meterStart ?? 0) / unitDivider, 4),
- OCPP16MeterValueContext.TRANSACTION_BEGIN
- )
- )
- return meterValue
- }
-
- public static buildTransactionDataMeterValues (
- transactionBeginMeterValue: OCPP16MeterValue,
- transactionEndMeterValue: OCPP16MeterValue
- ): OCPP16MeterValue[] {
- const meterValues: OCPP16MeterValue[] = []
- meterValues.push(transactionBeginMeterValue)
- meterValues.push(transactionEndMeterValue)
- return meterValues
- }
-
- public static remoteStopTransaction = async (
- chargingStation: ChargingStation,
- connectorId: number
- ): Promise<GenericResponse> => {
- await OCPP16ServiceUtils.sendAndSetConnectorStatus(
- chargingStation,
- connectorId,
- OCPP16ChargePointStatus.Finishing
- )
- const stopResponse = await chargingStation.stopTransactionOnConnector(
- connectorId,
- OCPP16StopTransactionReason.REMOTE
- )
- if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
- return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
- }
- return OCPP16Constants.OCPP_RESPONSE_REJECTED
- }
-
public static changeAvailability = async (
chargingStation: ChargingStation,
connectorIds: number[],
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) => {
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,
// 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,
...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 => {
}
}),
].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 => {
schedulePeriod.startPeriod
),
{
- start: compositeChargingScheduleLowerInterval.start,
end: compositeChargingScheduleHigherInterval.end,
+ start: compositeChargingScheduleLowerInterval.start,
}
)
) {
schedulePeriod.startPeriod
),
{
- start: compositeChargingScheduleLowerInterval.start,
end: compositeChargingScheduleHigherInterval.end,
+ start: compositeChargingScheduleLowerInterval.start,
}
) &&
isWithinInterval(
compositeChargingScheduleLower!.chargingSchedulePeriod[index + 1].startPeriod
),
{
- start: compositeChargingScheduleLowerInterval.start,
end: compositeChargingScheduleHigherInterval.end,
+ start: compositeChargingScheduleLowerInterval.start,
}
)
) {
schedulePeriod.startPeriod
),
{
- start: compositeChargingScheduleHigherInterval.start,
end: compositeChargingScheduleLowerInterval.end,
+ start: compositeChargingScheduleHigherInterval.start,
}
)
) {
}
}),
].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,
return false
}
+ public static remoteStopTransaction = async (
+ chargingStation: ChargingStation,
+ connectorId: number
+ ): Promise<GenericResponse> => {
+ await OCPP16ServiceUtils.sendAndSetConnectorStatus(
+ chargingStation,
+ connectorId,
+ OCPP16ChargePointStatus.Finishing
+ )
+ const stopResponse = await chargingStation.stopTransactionOnConnector(
+ connectorId,
+ OCPP16StopTransactionReason.REMOTE
+ )
+ if (stopResponse.idTagInfo?.status === OCPP16AuthorizationStatus.ACCEPTED) {
+ return OCPP16Constants.OCPP_RESPONSE_ACCEPTED
+ }
+ return OCPP16Constants.OCPP_RESPONSE_REJECTED
+ }
+
+ public static buildTransactionBeginMeterValue (
+ chargingStation: ChargingStation,
+ connectorId: number,
+ meterStart: number | undefined
+ ): OCPP16MeterValue {
+ const meterValue: OCPP16MeterValue = {
+ sampledValue: [],
+ timestamp: new Date(),
+ }
+ // Energy.Active.Import.Register measurand (default)
+ const sampledValueTemplate = OCPP16ServiceUtils.getSampledValueTemplate(
+ chargingStation,
+ connectorId
+ )
+ const unitDivider =
+ sampledValueTemplate?.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR ? 1000 : 1
+ meterValue.sampledValue.push(
+ OCPP16ServiceUtils.buildSampledValue(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ sampledValueTemplate!,
+ roundTo((meterStart ?? 0) / unitDivider, 4),
+ OCPP16MeterValueContext.TRANSACTION_BEGIN
+ )
+ )
+ return meterValue
+ }
+
+ public static buildTransactionDataMeterValues (
+ transactionBeginMeterValue: OCPP16MeterValue,
+ transactionEndMeterValue: OCPP16MeterValue
+ ): OCPP16MeterValue[] {
+ const meterValues: OCPP16MeterValue[] = []
+ meterValues.push(transactionBeginMeterValue)
+ meterValues.push(transactionEndMeterValue)
+ return meterValues
+ }
+
+ public static checkFeatureProfile (
+ chargingStation: ChargingStation,
+ featureProfile: OCPP16SupportedFeatureProfiles,
+ command: OCPP16IncomingRequestCommand | OCPP16RequestCommand
+ ): boolean {
+ if (hasFeatureProfile(chargingStation, featureProfile) === false) {
+ logger.warn(
+ `${chargingStation.logPrefix()} Trying to '${command}' without '${featureProfile}' feature enabled in ${
+ OCPP16StandardParametersKey.SupportedFeatureProfiles
+ } in configuration`
+ )
+ return false
+ }
+ return true
+ }
+
+ public static isConfigurationKeyVisible (key: ConfigurationKey): boolean {
+ if (key.visible == null) {
+ return true
+ }
+ return key.visible
+ }
+
public static parseJsonSchemaFile<T extends JsonType>(
relativePath: string,
moduleName?: string,
)
}
- 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)
}
}
import type { ValidateFunction } from 'ajv'
import type { ChargingStation } from '../../../charging-station/index.js'
+
import { OCPPError } from '../../../exception/index.js'
import {
ErrorType,
this.validatePayload = this.validatePayload.bind(this)
}
+ private validatePayload (
+ chargingStation: ChargingStation,
+ commandName: OCPP20IncomingRequestCommand,
+ commandPayload: JsonType
+ ): boolean {
+ if (this.payloadValidateFunctions.has(commandName)) {
+ return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload)
+ }
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+ )
+ return false
+ }
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
// 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
- }
}
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,
} 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'
this.buildRequestPayload = this.buildRequestPayload.bind(this)
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
- chargingStation: ChargingStation,
- commandName: OCPP20RequestCommand,
- commandParams?: RequestType,
- params?: RequestParams
- ): Promise<ResponseType> {
- // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc.
- if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) {
- // TODO: pre request actions hook
- return (await this.sendMessage(
- chargingStation,
- generateUUID(),
- this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams),
- commandName,
- params
- )) as ResponseType
- }
- // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
- throw new OCPPError(
- ErrorType.NOT_SUPPORTED,
- `Unsupported OCPP command ${commandName}`,
- commandName,
- commandParams
- )
- }
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
private buildRequestPayload<Request extends JsonType>(
chargingStation: ChargingStation,
)
}
}
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
+ chargingStation: ChargingStation,
+ commandName: OCPP20RequestCommand,
+ commandParams?: RequestType,
+ params?: RequestParams
+ ): Promise<ResponseType> {
+ // FIXME?: add sanity checks on charging station availability, connector availability, connector status, etc.
+ if (OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName)) {
+ // TODO: pre request actions hook
+ return (await this.sendMessage(
+ chargingStation,
+ generateUUID(),
+ this.buildRequestPayload<RequestType>(chargingStation, commandName, commandParams),
+ commandName,
+ params
+ )) as ResponseType
+ }
+ // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+ throw new OCPPError(
+ ErrorType.NOT_SUPPORTED,
+ `Unsupported OCPP command ${commandName}`,
+ commandName,
+ commandParams
+ )
+ }
}
const moduleName = 'OCPP20ResponseService'
export class OCPP20ResponseService extends OCPPResponseService {
+ protected payloadValidateFunctions: Map<OCPP20RequestCommand, ValidateFunction<JsonType>>
+
+ private readonly responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>
public incomingRequestResponsePayloadValidateFunctions: Map<
OCPP20IncomingRequestCommand,
ValidateFunction<JsonType>
>
- protected payloadValidateFunctions: Map<OCPP20RequestCommand, ValidateFunction<JsonType>>
- private readonly responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>
-
public constructor () {
// if (new.target.name === moduleName) {
// throw new TypeError(`Cannot construct ${new.target.name} instances directly`)
this.validatePayload = this.validatePayload.bind(this)
}
+ private handleResponseBootNotification (
+ chargingStation: ChargingStation,
+ payload: OCPP20BootNotificationResponse
+ ): void {
+ if (Object.values(RegistrationStatusEnumType).includes(payload.status)) {
+ chargingStation.bootNotificationResponse = payload
+ if (chargingStation.isRegistered()) {
+ chargingStation.emit(ChargingStationEvents.registered)
+ if (chargingStation.inAcceptedState()) {
+ addConfigurationKey(
+ chargingStation,
+ OCPP20OptionalVariableName.HeartbeatInterval,
+ payload.interval.toString(),
+ {},
+ { overwrite: true, save: true }
+ )
+ chargingStation.emit(ChargingStationEvents.accepted)
+ }
+ } else if (chargingStation.inRejectedState()) {
+ chargingStation.emit(ChargingStationEvents.rejected)
+ }
+ const logMsg = `${chargingStation.logPrefix()} Charging station in '${
+ payload.status
+ }' state on the central server`
+ payload.status === RegistrationStatusEnumType.REJECTED
+ ? logger.warn(logMsg)
+ : logger.info(logMsg)
+ } else {
+ delete chargingStation.bootNotificationResponse
+ logger.error(
+ `${chargingStation.logPrefix()} Charging station boot notification response received: %j with undefined registration status`,
+ payload
+ )
+ }
+ }
+
+ private validatePayload (
+ chargingStation: ChargingStation,
+ commandName: OCPP20RequestCommand,
+ payload: JsonType
+ ): boolean {
+ if (this.payloadValidateFunctions.has(commandName)) {
+ return this.validateResponsePayload(chargingStation, commandName, payload)
+ }
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+ )
+ return false
+ }
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
public async responseHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
)
}
}
-
- 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
- )
- }
- }
}
// 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({
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({
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,
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
}
-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'
const moduleName = 'OCPPIncomingRequestService'
export abstract class OCPPIncomingRequestService extends EventEmitter {
- private static instance: OCPPIncomingRequestService | null = null
- private readonly version: OCPPVersion
+ private static instance: null | OCPPIncomingRequestService = null
protected readonly ajv: Ajv
protected abstract payloadValidateFunctions: Map<
IncomingRequestCommand,
ValidateFunction<JsonType>
>
+ private readonly version: OCPPVersion
+
protected constructor (version: OCPPVersion) {
super()
this.version = version
return OCPPIncomingRequestService.instance as T
}
+ protected handleRequestClearCache (chargingStation: ChargingStation): ClearCacheResponse {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ if (chargingStation.idTagsCache.deleteIdTags(getIdTagsFile(chargingStation.stationInfo!)!)) {
+ return OCPPConstants.OCPP_RESPONSE_ACCEPTED
+ }
+ return OCPPConstants.OCPP_RESPONSE_REJECTED
+ }
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
protected validateIncomingRequestPayload<T extends JsonType>(
chargingStation: ChargingStation,
)
}
- protected handleRequestClearCache (chargingStation: ChargingStation): ClearCacheResponse {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (chargingStation.idTagsCache.deleteIdTags(getIdTagsFile(chargingStation.stationInfo!)!)) {
- return OCPPConstants.OCPP_RESPONSE_ACCEPTED
- }
- return OCPPConstants.OCPP_RESPONSE_REJECTED
- }
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unnecessary-type-parameters
public abstract incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
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 {
logger,
} from '../../utils/index.js'
import { OCPPConstants } from './OCPPConstants.js'
-import type { OCPPResponseService } from './OCPPResponseService.js'
import {
ajvErrorsToErrorType,
convertDateToISOString,
const defaultRequestParams: RequestParams = {
skipBufferingOnError: false,
- triggerMessage: false,
throwError: false,
+ triggerMessage: false,
}
export abstract class OCPPRequestService {
- private static instance: OCPPRequestService | null = null
- private readonly version: OCPPVersion
- private readonly ocppResponseService: OCPPResponseService
+ private static instance: null | OCPPRequestService = null
protected readonly ajv: Ajv
protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
+ private readonly ocppResponseService: OCPPResponseService
+ private readonly version: OCPPVersion
protected constructor (version: OCPPVersion, ocppResponseService: OCPPResponseService) {
this.version = version
return OCPPRequestService.instance as T
}
- public async sendResponse (
- chargingStation: ChargingStation,
- messageId: string,
- messagePayload: JsonType,
- commandName: IncomingRequestCommand
- ): Promise<ResponseType> {
- try {
- // Send response message
- return await this.internalSendMessage(
- chargingStation,
- messageId,
- messagePayload,
- MessageType.CALL_RESULT_MESSAGE,
- commandName
- )
- } catch (error) {
- handleSendMessageError(
- chargingStation,
- commandName,
- MessageType.CALL_RESULT_MESSAGE,
- error as Error,
- {
- throwError: true,
- }
- )
- return null
- }
- }
-
- public async sendError (
- chargingStation: ChargingStation,
- messageId: string,
- ocppError: OCPPError,
- commandName: RequestCommand | IncomingRequestCommand
- ): Promise<ResponseType> {
- try {
- // Send error message
- return await this.internalSendMessage(
- chargingStation,
- messageId,
- ocppError,
- MessageType.CALL_ERROR_MESSAGE,
- commandName
- )
- } catch (error) {
- handleSendMessageError(
- chargingStation,
- commandName,
- MessageType.CALL_ERROR_MESSAGE,
- error as Error
- )
- return null
- }
- }
-
protected async sendMessage (
chargingStation: ChargingStation,
messageId: string,
}
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- private validateRequestPayload<T extends JsonType>(
- chargingStation: ChargingStation,
- commandName: RequestCommand | IncomingRequestCommand,
- payload: T
- ): boolean {
- if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
- return true
- }
- if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
- )
- return true
- }
- const validate = this.payloadValidateFunctions.get(commandName as RequestCommand)
- payload = clone<T>(payload)
- convertDateToISOString<T>(payload)
- if (validate?.(payload) === true) {
- return true
- }
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
- validate?.errors
- )
- // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
- throw new OCPPError(
- ajvErrorsToErrorType(validate?.errors),
- 'Request PDU is invalid',
- commandName,
- JSON.stringify(validate?.errors, undefined, 2)
- )
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- private validateIncomingRequestResponsePayload<T extends JsonType>(
+ private buildMessageToSend (
chargingStation: ChargingStation,
- commandName: RequestCommand | IncomingRequestCommand,
- payload: T
- ): boolean {
- if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
- return true
- }
- if (
- !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has(
- commandName as IncomingRequestCommand
- )
- ) {
- logger.warn(
- `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
- )
- return true
- }
- const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
- commandName as IncomingRequestCommand
- )
- payload = clone<T>(payload)
- convertDateToISOString<T>(payload)
- if (validate?.(payload) === true) {
- return true
+ messageId: string,
+ messagePayload: JsonType | OCPPError,
+ messageType: MessageType,
+ commandName: IncomingRequestCommand | RequestCommand
+ ): string {
+ let messageToSend: string
+ // Type of message
+ switch (messageType) {
+ // Error Message
+ case MessageType.CALL_ERROR_MESSAGE:
+ // Build Error Message
+ messageToSend = JSON.stringify([
+ messageType,
+ messageId,
+ (messagePayload as OCPPError).code,
+ (messagePayload as OCPPError).message,
+ (messagePayload as OCPPError).details ?? {
+ command: (messagePayload as OCPPError).command,
+ },
+ ] satisfies ErrorResponse)
+ break
+ // Request
+ case MessageType.CALL_MESSAGE:
+ // Build request
+ this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType)
+ messageToSend = JSON.stringify([
+ messageType,
+ messageId,
+ commandName as RequestCommand,
+ messagePayload as JsonType,
+ ] satisfies OutgoingRequest)
+ break
+ // Response
+ case MessageType.CALL_RESULT_MESSAGE:
+ // Build response
+ this.validateIncomingRequestResponsePayload(
+ chargingStation,
+ commandName,
+ messagePayload as JsonType
+ )
+ messageToSend = JSON.stringify([
+ messageType,
+ messageId,
+ messagePayload as JsonType,
+ ] satisfies Response)
+ break
}
- logger.error(
- `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
- validate?.errors
- )
- // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
- throw new OCPPError(
- ajvErrorsToErrorType(validate?.errors),
- 'Incoming request response PDU is invalid',
- commandName,
- JSON.stringify(validate?.errors, undefined, 2)
- )
+ return messageToSend
}
private async internalSendMessage (
messageId: string,
messagePayload: JsonType | OCPPError,
messageType: MessageType,
- commandName: RequestCommand | IncomingRequestCommand,
+ commandName: IncomingRequestCommand | RequestCommand,
params?: RequestParams
): Promise<ResponseType> {
params = {
}buffered message id '${messageId}' with content '${messageToSend}'`,
commandName,
{
- name: error.name,
message: error.message,
+ name: error.name,
stack: error.stack,
}
)
)
}
- 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 {
])
}
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ private validateIncomingRequestResponsePayload<T extends JsonType>(
+ chargingStation: ChargingStation,
+ commandName: IncomingRequestCommand | RequestCommand,
+ payload: T
+ ): boolean {
+ if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
+ return true
+ }
+ if (
+ !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has(
+ commandName as IncomingRequestCommand
+ )
+ ) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation`
+ )
+ return true
+ }
+ const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
+ commandName as IncomingRequestCommand
+ )
+ payload = clone<T>(payload)
+ convertDateToISOString<T>(payload)
+ if (validate?.(payload) === true) {
+ return true
+ }
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`,
+ validate?.errors
+ )
+ // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+ throw new OCPPError(
+ ajvErrorsToErrorType(validate?.errors),
+ 'Incoming request response PDU is invalid',
+ commandName,
+ JSON.stringify(validate?.errors, undefined, 2)
+ )
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ private validateRequestPayload<T extends JsonType>(
+ chargingStation: ChargingStation,
+ commandName: IncomingRequestCommand | RequestCommand,
+ payload: T
+ ): boolean {
+ if (chargingStation.stationInfo?.ocppStrictCompliance === false) {
+ return true
+ }
+ if (!this.payloadValidateFunctions.has(commandName as RequestCommand)) {
+ logger.warn(
+ `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
+ )
+ return true
+ }
+ const validate = this.payloadValidateFunctions.get(commandName as RequestCommand)
+ payload = clone<T>(payload)
+ convertDateToISOString<T>(payload)
+ if (validate?.(payload) === true) {
+ return true
+ }
+ logger.error(
+ `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
+ validate?.errors
+ )
+ // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+ throw new OCPPError(
+ ajvErrorsToErrorType(validate?.errors),
+ 'Request PDU is invalid',
+ commandName,
+ JSON.stringify(validate?.errors, undefined, 2)
+ )
+ }
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
commandParams?: ReqType,
params?: RequestParams
): Promise<ResType>
+
+ public async sendError (
+ chargingStation: ChargingStation,
+ messageId: string,
+ ocppError: OCPPError,
+ commandName: IncomingRequestCommand | RequestCommand
+ ): Promise<ResponseType> {
+ try {
+ // Send error message
+ return await this.internalSendMessage(
+ chargingStation,
+ messageId,
+ ocppError,
+ MessageType.CALL_ERROR_MESSAGE,
+ commandName
+ )
+ } catch (error) {
+ handleSendMessageError(
+ chargingStation,
+ commandName,
+ MessageType.CALL_ERROR_MESSAGE,
+ error as Error
+ )
+ return null
+ }
+ }
+
+ public async sendResponse (
+ chargingStation: ChargingStation,
+ messageId: string,
+ messagePayload: JsonType,
+ commandName: IncomingRequestCommand
+ ): Promise<ResponseType> {
+ try {
+ // Send response message
+ return await this.internalSendMessage(
+ chargingStation,
+ messageId,
+ messagePayload,
+ MessageType.CALL_RESULT_MESSAGE,
+ commandName
+ )
+ } catch (error) {
+ handleSendMessageError(
+ chargingStation,
+ commandName,
+ MessageType.CALL_RESULT_MESSAGE,
+ error as Error,
+ {
+ throwError: true,
+ }
+ )
+ return null
+ }
+ }
}
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
const moduleName = 'OCPPResponseService'
export abstract class OCPPResponseService {
- private static instance: OCPPResponseService | null = null
- private readonly version: OCPPVersion
+ private static instance: null | OCPPResponseService = null
protected readonly ajv: Ajv
protected readonly ajvIncomingRequest: Ajv
+ protected emptyResponseHandler = Constants.EMPTY_FUNCTION
protected abstract payloadValidateFunctions: Map<RequestCommand, ValidateFunction<JsonType>>
+ private readonly version: OCPPVersion
+
public abstract incomingRequestResponsePayloadValidateFunctions: Map<
IncomingRequestCommand,
ValidateFunction<JsonType>
)
}
- protected emptyResponseHandler = Constants.EMPTY_FUNCTION
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
public abstract responseHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
+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,
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'
}
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')
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) {
switch (chargingStation.stationInfo?.ocppVersion) {
case OCPPVersion.VERSION_16:
meterValue = {
- timestamp: new Date(),
sampledValue: [],
+ timestamp: new Date(),
}
// SoC measurand
socSampledValueTemplate = getSampledValueTemplate(
connectorMaximumPower / unitDivider,
connectorMinimumPower / unitDivider,
{
+ fallbackValue: connectorMinimumPower / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPower / unitDivider,
}
) / chargingStation.getNumberOfPhases(),
powerSampledValueTemplate.fluctuationPercent ??
connectorMaximumPowerPerPhase / unitDivider,
connectorMinimumPowerPerPhase / unitDivider,
{
+ fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
}
),
powerPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
connectorMaximumPowerPerPhase / unitDivider,
connectorMinimumPowerPerPhase / unitDivider,
{
+ fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
}
),
powerPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
connectorMaximumPowerPerPhase / unitDivider,
connectorMinimumPowerPerPhase / unitDivider,
{
+ fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPowerPerPhase / unitDivider,
}
),
powerPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
connectorMaximumPower / unitDivider,
connectorMinimumPower / unitDivider,
{
+ fallbackValue: connectorMinimumPower / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPower / unitDivider,
}
),
powerSampledValueTemplate.fluctuationPercent ??
connectorMaximumPower / unitDivider,
connectorMinimumPower / unitDivider,
{
+ fallbackValue: connectorMinimumPower / unitDivider,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumPower / unitDivider,
}
),
powerSampledValueTemplate.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentSampledValueTemplate.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentPerPhaseSampledValueTemplates.L1.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentPerPhaseSampledValueTemplates.L2.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentPerPhaseSampledValueTemplates.L3.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentSampledValueTemplate.fluctuationPercent ??
connectorMaximumAmperage,
connectorMinimumAmperage,
{
+ fallbackValue: connectorMinimumAmperage,
limitationEnabled:
chargingStation.stationInfo.customValueLimitationMeterValues,
- fallbackValue: connectorMinimumAmperage,
}
),
currentSampledValueTemplate.fluctuationPercent ??
connectorMaximumEnergyRounded,
connectorMinimumEnergyRounded,
{
- limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
fallbackValue: connectorMinimumEnergyRounded,
+ limitationEnabled: chargingStation.stationInfo.customValueLimitationMeterValues,
unitMultiplier: unitDivider,
}
),
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)
maxLimit: number,
minLimit: number,
options?: {
- limitationEnabled?: boolean
fallbackValue?: number
+ limitationEnabled?: boolean
unitMultiplier?: number
}
): number => {
options = {
...{
+ fallbackValue: 0,
limitationEnabled: false,
unitMultiplier: 1,
- fallbackValue: 0,
},
...options,
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class OCPPServiceUtils {
- public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus
- public static readonly restoreConnectorStatus = restoreConnectorStatus
- public static readonly isIdTagAuthorized = isIdTagAuthorized
+ protected static buildSampledValue = buildSampledValue
public static readonly buildTransactionEndMeterValue = buildTransactionEndMeterValue
protected static getSampledValueTemplate = getSampledValueTemplate
- protected static buildSampledValue = buildSampledValue
+ public static readonly isIdTagAuthorized = isIdTagAuthorized
+ private static readonly logPrefix = (
+ ocppVersion: OCPPVersion,
+ moduleName?: string,
+ methodName?: string
+ ): string => {
+ const logMsg =
+ isNotEmptyString(moduleName) && isNotEmptyString(methodName)
+ ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
+ : ` OCPP ${ocppVersion} |`
+ return logPrefix(logMsg)
+ }
+
+ public static readonly restoreConnectorStatus = restoreConnectorStatus
+
+ public static readonly sendAndSetConnectorStatus = sendAndSetConnectorStatus
protected constructor () {
// This is intentional
}
- public static isRequestCommandSupported (
+ public static isConnectorIdValid (
chargingStation: ChargingStation,
- command: RequestCommand
+ ocppCommand: IncomingRequestCommand,
+ connectorId: number
): boolean {
- const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
- if (
- isRequestCommand &&
- chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
- ) {
- return true
- } else if (
- isRequestCommand &&
- chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
- ) {
- return chargingStation.stationInfo.commandsSupport.outgoingCommands[command]
+ if (connectorId < 0) {
+ logger.error(
+ `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}`
+ )
+ return false
}
- logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
- return false
+ return true
}
public static isIncomingRequestCommandSupported (
return false
}
- public static isConnectorIdValid (
+ public static isRequestCommandSupported (
chargingStation: ChargingStation,
- ocppCommand: IncomingRequestCommand,
- connectorId: number
+ command: RequestCommand
): boolean {
- if (connectorId < 0) {
- logger.error(
- `${chargingStation.logPrefix()} ${ocppCommand} incoming request received with invalid connector id ${connectorId.toString()}`
- )
- return false
+ const isRequestCommand = Object.values<RequestCommand>(RequestCommand).includes(command)
+ if (
+ isRequestCommand &&
+ chargingStation.stationInfo?.commandsSupport?.outgoingCommands == null
+ ) {
+ return true
+ } else if (
+ isRequestCommand &&
+ chargingStation.stationInfo?.commandsSupport?.outgoingCommands?.[command] != null
+ ) {
+ return chargingStation.stationInfo.commandsSupport.outgoingCommands[command]
}
- return true
+ logger.error(`${chargingStation.logPrefix()} Unknown outgoing OCPP command '${command}'`)
+ return false
}
protected static parseJsonSchemaFile<T extends JsonType>(
return {} as JSONSchemaType<T>
}
}
-
- private static readonly logPrefix = (
- ocppVersion: OCPPVersion,
- moduleName?: string,
- methodName?: string
- ): string => {
- const logMsg =
- isNotEmptyString(moduleName) && isNotEmptyString(methodName)
- ? ` OCPP ${ocppVersion} | ${moduleName}.${methodName}:`
- : ` OCPP ${ocppVersion} |`
- return logPrefix(logMsg)
- }
}
+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 {
type UIServerConfiguration,
} from '../../types/index.js'
import { logger } from '../../utils/index.js'
-import type { AbstractUIService } from './ui-services/AbstractUIService.js'
import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
const moduleName = 'AbstractUIServer'
export abstract class AbstractUIServer {
- public readonly chargingStations: Map<string, ChargingStationData>
- public readonly chargingStationTemplates: Set<string>
- protected readonly httpServer: Server | Http2Server
+ protected readonly httpServer: Http2Server | Server
protected readonly responseHandlers: Map<
`${string}-${string}-${string}-${string}-${string}`,
ServerResponse | WebSocket
>
protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
+ public readonly chargingStations: Map<string, ChargingStationData>
+
+ public readonly chargingStationTemplates: Set<string>
public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
this.chargingStations = new Map<string, ChargingStationData>()
this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
}
- public buildProtocolRequest (
- uuid: `${string}-${string}-${string}-${string}-${string}`,
- procedureName: ProcedureName,
- requestPayload: RequestPayload
- ): ProtocolRequest {
- return [uuid, procedureName, requestPayload]
- }
-
- public buildProtocolResponse (
- uuid: `${string}-${string}-${string}-${string}-${string}`,
- responsePayload: ResponsePayload
- ): ProtocolResponse {
- return [uuid, responsePayload]
- }
-
- public stop (): void {
- this.stopHttpServer()
- for (const uiService of this.uiServices.values()) {
- uiService.stop()
- }
- this.clearCaches()
- }
-
- public clearCaches (): void {
- this.chargingStations.clear()
- this.chargingStationTemplates.clear()
- }
-
- public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
- const protocolVersion = ProtocolVersion['0.0.1']
- this.registerProtocolVersionUIService(protocolVersion)
- return await (this.uiServices
- .get(protocolVersion)
- ?.requestHandler(request) as Promise<ProtocolResponse>)
- }
-
- public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
- return this.responseHandlers.has(uuid)
- }
-
- protected startHttpServer (): void {
- this.httpServer.on('error', error => {
- logger.error(
- `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
- error
- )
- })
- if (!this.httpServer.listening) {
- this.httpServer.listen(this.uiServerConfiguration.options)
- }
- }
-
- protected registerProtocolVersionUIService (version: ProtocolVersion): void {
- if (!this.uiServices.has(version)) {
- this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
- }
- }
-
protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
const authorizationError = new BaseError('Unauthorized')
if (this.isBasicAuthEnabled()) {
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)
}
}
)
}
- public abstract start (): void
+ private stopHttpServer (): void {
+ if (this.httpServer.listening) {
+ this.httpServer.close()
+ this.httpServer.removeAllListeners()
+ }
+ }
+
+ public buildProtocolRequest (
+ uuid: `${string}-${string}-${string}-${string}-${string}`,
+ procedureName: ProcedureName,
+ requestPayload: RequestPayload
+ ): ProtocolRequest {
+ return [uuid, procedureName, requestPayload]
+ }
+
+ public buildProtocolResponse (
+ uuid: `${string}-${string}-${string}-${string}-${string}`,
+ responsePayload: ResponsePayload
+ ): ProtocolResponse {
+ return [uuid, responsePayload]
+ }
+
+ public clearCaches (): void {
+ this.chargingStations.clear()
+ this.chargingStationTemplates.clear()
+ }
+
+ public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
+ return this.responseHandlers.has(uuid)
+ }
+
+ public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
+
+ public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
+ const protocolVersion = ProtocolVersion['0.0.1']
+ this.registerProtocolVersionUIService(protocolVersion)
+ return await (this.uiServices
+ .get(protocolVersion)
+ ?.requestHandler(request) as Promise<ProtocolResponse>)
+ }
+
public abstract sendRequest (request: ProtocolRequest): void
public abstract sendResponse (response: ProtocolResponse): void
- public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
+ public abstract start (): void
+ public stop (): void {
+ this.stopHttpServer()
+ for (const uiService of this.uiServices.values()) {
+ uiService.stop()
+ }
+ this.clearCaches()
+ }
}
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 =
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) {
} catch (error) {
this.sendResponse(
this.buildProtocolResponse(uuid, {
- status: ResponseStatus.FAILURE,
errorMessage: (error as Error).message,
errorStack: (error as Error).stack,
+ status: ResponseStatus.FAILURE,
})
)
return
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()
+ }
}
import chalk from 'chalk'
+import type { AbstractUIServer } from './AbstractUIServer.js'
+
import { BaseError } from '../../exception/index.js'
import {
ApplicationProtocol,
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
}
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)
- }
}
export const handleProtocols = (
protocols: Set<string>,
_request: IncomingMessage
-): string | false => {
+): false | string => {
let protocol: Protocol | undefined
let version: ProtocolVersion | undefined
if (protocols.size === 0) {
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({
})
}
+ 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)) {
})
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
- }
}
+import type { AbstractUIServer } from '../AbstractUIServer.js'
+
import { BaseError, type OCPPError } from '../../../exception/index.js'
import {
BroadcastChannelProcedureName,
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 {
ProcedureName,
BroadcastChannelProcedureName
>([
- [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
- [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
+ [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
+ [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
+ [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
+ [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
[
ProcedureName.DELETE_CHARGING_STATIONS,
BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS,
],
- [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
- [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
[
- ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
- BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+ ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+ BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
],
[
- ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
- BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+ ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+ BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
],
- [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
- [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
- [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
- [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
- [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
- [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
[ProcedureName.HEARTBEAT, BroadcastChannelProcedureName.HEARTBEAT],
[ProcedureName.METER_VALUES, BroadcastChannelProcedureName.METER_VALUES],
- [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
+ [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
+ [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
[
- ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
- BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
+ ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
+ BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
],
+ [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
+ [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
+ [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
[
- ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
- BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
+ ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
+ BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
],
+ [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
+ [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
])
protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>
- private readonly version: ProtocolVersion
- private readonly uiServer: AbstractUIServer
- private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel
private readonly broadcastChannelRequests: Map<
`${string}-${string}-${string}-${string}-${string}`,
number
>
+ private readonly uiServer: AbstractUIServer
+ private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel
+ private readonly version: ProtocolVersion
+
+ public logPrefix = (modName: string, methodName: string): string => {
+ return this.uiServer.logPrefix(modName, methodName, this.version)
+ }
+
constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
this.uiServer = uiServer
this.version = version
this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
- [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
- [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
[ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
+ [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
+ [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
[ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
[ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
[ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
>()
}
- public stop (): void {
- this.broadcastChannelRequests.clear()
- this.uiServiceWorkerBroadcastChannel.close()
- }
-
- public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
- let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined
- let command: ProcedureName | undefined
- let requestPayload: RequestPayload | undefined
- let responsePayload: ResponsePayload | undefined
- try {
- ;[uuid, command, requestPayload] = request
-
- if (!this.requestHandlers.has(command)) {
- throw new BaseError(
- `'${command}' is not implemented to handle message payload ${JSON.stringify(
- requestPayload,
- undefined,
- 2
- )}`
- )
- }
-
- // Call the request handler to build the response payload
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const requestHandler = this.requestHandlers.get(command)!
- if (isAsyncFunction(requestHandler)) {
- responsePayload = await requestHandler(uuid, command, requestPayload)
- } else {
- responsePayload = (
- requestHandler as (
- uuid?: string,
- procedureName?: ProcedureName,
- payload?: RequestPayload
- ) => undefined | ResponsePayload
- )(uuid, command, requestPayload)
- }
- } catch (error) {
- // Log
- logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
- responsePayload = {
- hashIds: requestPayload?.hashIds,
- status: ResponseStatus.FAILURE,
- command,
- requestPayload,
- responsePayload,
- errorMessage: (error as OCPPError).message,
- errorStack: (error as OCPPError).stack,
- errorDetails: (error as OCPPError).details,
- } satisfies ResponsePayload
- }
- if (responsePayload != null) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return this.uiServer.buildProtocolResponse(uuid!, responsePayload)
- }
- }
-
- // public sendRequest (
- // uuid: `${string}-${string}-${string}-${string}-${string}`,
- // procedureName: ProcedureName,
- // requestPayload: RequestPayload
- // ): void {
- // this.uiServer.sendRequest(
- // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
- // )
- // }
-
- public sendResponse (
- uuid: `${string}-${string}-${string}-${string}-${string}`,
- responsePayload: ResponsePayload
- ): void {
- if (this.uiServer.hasResponseHandler(uuid)) {
- this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(uuid, responsePayload))
- }
- }
-
- public logPrefix = (modName: string, methodName: string): string => {
- return this.uiServer.logPrefix(modName, methodName, this.version)
- }
-
- public deleteBroadcastChannelRequest (
- uuid: `${string}-${string}-${string}-${string}-${string}`
- ): void {
- this.broadcastChannelRequests.delete(uuid)
- }
-
- public getBroadcastChannelExpectedResponses (
- uuid: `${string}-${string}-${string}-${string}-${string}`
- ): number {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return this.broadcastChannelRequests.get(uuid)!
- }
-
protected handleProtocolRequest (
uuid: `${string}-${string}-${string}-${string}-${string}`,
procedureName: ProcedureName,
)
}
- private sendBroadcastChannelRequest (
- uuid: `${string}-${string}-${string}-${string}-${string}`,
- procedureName: BroadcastChannelProcedureName,
- payload: BroadcastChannelRequestPayload
- ): void {
- if (isNotEmptyArray(payload.hashIds)) {
- payload.hashIds = payload.hashIds
- .map(hashId => {
- if (this.uiServer.chargingStations.has(hashId)) {
- return hashId
- }
- logger.warn(
- `${this.logPrefix(
- moduleName,
- 'sendBroadcastChannelRequest'
- )} Charging station with hashId '${hashId}' not found`
- )
- return undefined
- })
- .filter(hashId => hashId != null)
- } else {
- delete payload.hashIds
- }
- const expectedNumberOfResponses = Array.isArray(payload.hashIds)
- ? payload.hashIds.length
- : this.uiServer.chargingStations.size
- if (expectedNumberOfResponses === 0) {
- throw new BaseError(
- 'hashIds array in the request payload does not contain any valid charging station hashId'
- )
- }
- this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
- this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
- }
-
- private handleListTemplates (): ResponsePayload {
- return {
- status: ResponseStatus.SUCCESS,
- templates: [...this.uiServer.chargingStationTemplates.values()],
- } satisfies ResponsePayload
- }
-
- private handleListChargingStations (): ResponsePayload {
- return {
- status: ResponseStatus.SUCCESS,
- chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[],
- } satisfies ResponsePayload
- }
+ // public sendRequest (
+ // uuid: `${string}-${string}-${string}-${string}-${string}`,
+ // procedureName: ProcedureName,
+ // requestPayload: RequestPayload
+ // ): void {
+ // this.uiServer.sendRequest(
+ // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
+ // )
+ // }
private async handleAddChargingStations (
_uuid?: `${string}-${string}-${string}-${string}-${string}`,
_procedureName?: ProcedureName,
requestPayload?: RequestPayload
): Promise<ResponsePayload> {
- const { template, numberOfStations, options } =
+ const { numberOfStations, options, template } =
requestPayload as AddChargingStationsRequestPayload
if (!Bootstrap.getInstance().getState().started) {
return {
- status: ResponseStatus.FAILURE,
errorMessage:
'Cannot add charging station(s) while the charging stations simulator is not started',
+ status: ResponseStatus.FAILURE,
} satisfies ResponsePayload
}
if (typeof template !== 'string' || typeof numberOfStations !== 'number') {
return {
- status: ResponseStatus.FAILURE,
errorMessage: 'Invalid request payload',
+ status: ResponseStatus.FAILURE,
} satisfies ResponsePayload
}
if (!this.uiServer.chargingStationTemplates.has(template)) {
return {
- status: ResponseStatus.FAILURE,
errorMessage: `Template '${template}' not found`,
+ status: ResponseStatus.FAILURE,
} satisfies ResponsePayload
}
const succeededStationInfos: ChargingStationInfo[] = []
} satisfies ResponsePayload
}
+ private handleListChargingStations (): ResponsePayload {
+ return {
+ chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[],
+ status: ResponseStatus.SUCCESS,
+ } satisfies ResponsePayload
+ }
+
+ private handleListTemplates (): ResponsePayload {
+ return {
+ status: ResponseStatus.SUCCESS,
+ templates: [...this.uiServer.chargingStationTemplates.values()],
+ } satisfies ResponsePayload
+ }
+
private handlePerformanceStatistics (): ResponsePayload {
if (
Configuration.getConfigurationSection<StorageConfiguration>(
).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
}
}
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
}
}
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
}
}
return { status: ResponseStatus.SUCCESS }
} catch (error) {
return {
- status: ResponseStatus.FAILURE,
errorMessage: (error as Error).message,
errorStack: (error as Error).stack,
+ status: ResponseStatus.FAILURE,
} satisfies ResponsePayload
}
}
+
+ private sendBroadcastChannelRequest (
+ uuid: `${string}-${string}-${string}-${string}-${string}`,
+ procedureName: BroadcastChannelProcedureName,
+ payload: BroadcastChannelRequestPayload
+ ): void {
+ if (isNotEmptyArray(payload.hashIds)) {
+ payload.hashIds = payload.hashIds
+ .map(hashId => {
+ if (this.uiServer.chargingStations.has(hashId)) {
+ return hashId
+ }
+ logger.warn(
+ `${this.logPrefix(
+ moduleName,
+ 'sendBroadcastChannelRequest'
+ )} Charging station with hashId '${hashId}' not found`
+ )
+ return undefined
+ })
+ .filter(hashId => hashId != null)
+ } else {
+ delete payload.hashIds
+ }
+ const expectedNumberOfResponses = Array.isArray(payload.hashIds)
+ ? payload.hashIds.length
+ : this.uiServer.chargingStations.size
+ if (expectedNumberOfResponses === 0) {
+ throw new BaseError(
+ 'hashIds array in the request payload does not contain any valid charging station hashId'
+ )
+ }
+ this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
+ this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
+ }
+
+ public deleteBroadcastChannelRequest (
+ uuid: `${string}-${string}-${string}-${string}-${string}`
+ ): void {
+ this.broadcastChannelRequests.delete(uuid)
+ }
+
+ public getBroadcastChannelExpectedResponses (
+ uuid: `${string}-${string}-${string}-${string}-${string}`
+ ): number {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return this.broadcastChannelRequests.get(uuid)!
+ }
+
+ public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
+ let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined
+ let command: ProcedureName | undefined
+ let requestPayload: RequestPayload | undefined
+ let responsePayload: ResponsePayload | undefined
+ try {
+ ;[uuid, command, requestPayload] = request
+
+ if (!this.requestHandlers.has(command)) {
+ throw new BaseError(
+ `'${command}' is not implemented to handle message payload ${JSON.stringify(
+ requestPayload,
+ undefined,
+ 2
+ )}`
+ )
+ }
+
+ // Call the request handler to build the response payload
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const requestHandler = this.requestHandlers.get(command)!
+ if (isAsyncFunction(requestHandler)) {
+ responsePayload = await requestHandler(uuid, command, requestPayload)
+ } else {
+ responsePayload = (
+ requestHandler as (
+ uuid?: string,
+ procedureName?: ProcedureName,
+ payload?: RequestPayload
+ ) => ResponsePayload | undefined
+ )(uuid, command, requestPayload)
+ }
+ } catch (error) {
+ // Log
+ logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
+ responsePayload = {
+ command,
+ errorDetails: (error as OCPPError).details,
+ errorMessage: (error as OCPPError).message,
+ errorStack: (error as OCPPError).stack,
+ hashIds: requestPayload?.hashIds,
+ requestPayload,
+ responsePayload,
+ status: ResponseStatus.FAILURE,
+ } satisfies ResponsePayload
+ }
+ if (responsePayload != null) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return this.uiServer.buildProtocolResponse(uuid!, responsePayload)
+ }
+ }
+
+ public sendResponse (
+ uuid: `${string}-${string}-${string}-${string}-${string}`,
+ responsePayload: ResponsePayload
+ ): void {
+ if (this.uiServer.hasResponseHandler(uuid)) {
+ this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(uuid, responsePayload))
+ }
+ }
+
+ public stop (): void {
+ this.broadcastChannelRequests.clear()
+ this.uiServiceWorkerBroadcastChannel.close()
+ }
}
-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 {
-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
// 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)
// 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'
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 {
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)
performance.clearMeasures(name)
}
- public addRequestStatistic (
- command: RequestCommand | IncomingRequestCommand,
- messageType: MessageType
- ): void {
- switch (messageType) {
- case MessageType.CALL_MESSAGE:
- if (
- this.statistics.statisticsData.has(command) &&
- this.statistics.statisticsData.get(command)?.requestCount != null
- ) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.statistics.statisticsData.get(command)!.requestCount!
- } else {
- this.statistics.statisticsData.set(command, {
- ...this.statistics.statisticsData.get(command),
- requestCount: 1,
- })
- }
- break
- case MessageType.CALL_RESULT_MESSAGE:
- if (
- this.statistics.statisticsData.has(command) &&
- this.statistics.statisticsData.get(command)?.responseCount != null
- ) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.statistics.statisticsData.get(command)!.responseCount!
- } else {
- this.statistics.statisticsData.set(command, {
- ...this.statistics.statisticsData.get(command),
- responseCount: 1,
- })
- }
- break
- case MessageType.CALL_ERROR_MESSAGE:
- if (
- this.statistics.statisticsData.has(command) &&
- this.statistics.statisticsData.get(command)?.errorCount != null
- ) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- ++this.statistics.statisticsData.get(command)!.errorCount!
- } else {
- this.statistics.statisticsData.set(command, {
- ...this.statistics.statisticsData.get(command),
- errorCount: 1,
- })
- }
- break
- default:
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- logger.error(`${this.logPrefix()} wrong message type ${messageType}`)
- break
+ public static getInstance (
+ objId: string | undefined,
+ objName: string | undefined,
+ uri: undefined | URL
+ ): PerformanceStatistics | undefined {
+ if (objId == null) {
+ const errMsg = 'Cannot get performance statistics instance without specifying object id'
+ logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+ throw new BaseError(errMsg)
}
- }
-
- public start (): void {
- this.startLogStatisticsInterval()
- const performanceStorageConfiguration =
- Configuration.getConfigurationSection<StorageConfiguration>(
- ConfigurationSection.performanceStorage
- )
- if (performanceStorageConfiguration.enabled === true) {
- logger.info(
- `${this.logPrefix()} storage enabled: type ${
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- performanceStorageConfiguration.type
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- }, uri: ${performanceStorageConfiguration.uri}`
- )
+ if (objName == null) {
+ const errMsg = 'Cannot get performance statistics instance without specifying object name'
+ logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+ throw new BaseError(errMsg)
}
- }
-
- public stop (): void {
- this.stopLogStatisticsInterval()
- performance.clearMarks()
- performance.clearMeasures()
- this.performanceObserver.disconnect()
- }
-
- public restart (): void {
- this.stop()
- this.start()
- }
-
- private initializePerformanceObserver (): void {
- this.performanceObserver = new PerformanceObserver(performanceObserverList => {
- const lastPerformanceEntry = performanceObserverList.getEntries()[0]
- // logger.debug(
- // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
- // lastPerformanceEntry
- // )
- this.addPerformanceEntryToStatistics(lastPerformanceEntry)
- })
- this.performanceObserver.observe({ entryTypes: ['measure'] })
- }
-
- private logStatistics (): void {
- logger.info(this.logPrefix(), {
- ...this.statistics,
- statisticsData: JSON.parse(
- JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
- ) as Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>,
- })
- }
-
- private startLogStatisticsInterval (): void {
- const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
- ConfigurationSection.log
- )
- const logStatisticsInterval =
- logConfiguration.enabled === true
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- logConfiguration.statisticsInterval!
- : 0
- if (logStatisticsInterval > 0 && this.displayInterval == null) {
- this.displayInterval = setInterval(() => {
- this.logStatistics()
- }, secondsToMilliseconds(logStatisticsInterval))
- logger.info(
- `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
- )
- } else if (this.displayInterval != null) {
- logger.info(
- `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
- )
- } else if (logConfiguration.enabled === true) {
- logger.info(
- `${this.logPrefix()} log interval is set to ${logStatisticsInterval.toString()}. Not logging statistics`
- )
+ if (uri == null) {
+ const errMsg = 'Cannot get performance statistics instance without specifying object uri'
+ logger.error(`${PerformanceStatistics.logPrefix()} ${errMsg}`)
+ throw new BaseError(errMsg)
}
- }
-
- private stopLogStatisticsInterval (): void {
- if (this.displayInterval != null) {
- clearInterval(this.displayInterval)
- delete this.displayInterval
+ if (!PerformanceStatistics.instances.has(objId)) {
+ PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri))
}
+ return PerformanceStatistics.instances.get(objId)
}
private addPerformanceEntryToStatistics (entry: PerformanceEntry): void {
}
}
- private static readonly logPrefix = (): string => {
- return logPrefix(' Performance statistics')
+ private initializePerformanceObserver (): void {
+ this.performanceObserver = new PerformanceObserver(performanceObserverList => {
+ const lastPerformanceEntry = performanceObserverList.getEntries()[0]
+ // logger.debug(
+ // `${this.logPrefix()} '${lastPerformanceEntry.name}' performance entry: %j`,
+ // lastPerformanceEntry
+ // )
+ this.addPerformanceEntryToStatistics(lastPerformanceEntry)
+ })
+ this.performanceObserver.observe({ entryTypes: ['measure'] })
}
- private readonly logPrefix = (): string => {
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- return logPrefix(` ${this.objName} | Performance statistics`)
+ private logStatistics (): void {
+ logger.info(this.logPrefix(), {
+ ...this.statistics,
+ statisticsData: JSON.parse(
+ JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
+ ) as Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>,
+ })
+ }
+
+ private startLogStatisticsInterval (): void {
+ const logConfiguration = Configuration.getConfigurationSection<LogConfiguration>(
+ ConfigurationSection.log
+ )
+ const logStatisticsInterval =
+ logConfiguration.enabled === true
+ ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ logConfiguration.statisticsInterval!
+ : 0
+ if (logStatisticsInterval > 0 && this.displayInterval == null) {
+ this.displayInterval = setInterval(() => {
+ this.logStatistics()
+ }, secondsToMilliseconds(logStatisticsInterval))
+ logger.info(
+ `${this.logPrefix()} logged every ${formatDurationSeconds(logStatisticsInterval)}`
+ )
+ } else if (this.displayInterval != null) {
+ logger.info(
+ `${this.logPrefix()} already logged every ${formatDurationSeconds(logStatisticsInterval)}`
+ )
+ } else if (logConfiguration.enabled === true) {
+ logger.info(
+ `${this.logPrefix()} log interval is set to ${logStatisticsInterval.toString()}. Not logging statistics`
+ )
+ }
+ }
+
+ private stopLogStatisticsInterval (): void {
+ if (this.displayInterval != null) {
+ clearInterval(this.displayInterval)
+ delete this.displayInterval
+ }
+ }
+
+ public addRequestStatistic (
+ command: IncomingRequestCommand | RequestCommand,
+ messageType: MessageType
+ ): void {
+ switch (messageType) {
+ case MessageType.CALL_ERROR_MESSAGE:
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.errorCount != null
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.statistics.statisticsData.get(command)!.errorCount!
+ } else {
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ errorCount: 1,
+ })
+ }
+ break
+ case MessageType.CALL_MESSAGE:
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.requestCount != null
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.statistics.statisticsData.get(command)!.requestCount!
+ } else {
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ requestCount: 1,
+ })
+ }
+ break
+ case MessageType.CALL_RESULT_MESSAGE:
+ if (
+ this.statistics.statisticsData.has(command) &&
+ this.statistics.statisticsData.get(command)?.responseCount != null
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ ++this.statistics.statisticsData.get(command)!.responseCount!
+ } else {
+ this.statistics.statisticsData.set(command, {
+ ...this.statistics.statisticsData.get(command),
+ responseCount: 1,
+ })
+ }
+ break
+ default:
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ logger.error(`${this.logPrefix()} wrong message type ${messageType}`)
+ break
+ }
+ }
+
+ public restart (): void {
+ this.stop()
+ this.start()
+ }
+
+ public start (): void {
+ this.startLogStatisticsInterval()
+ const performanceStorageConfiguration =
+ Configuration.getConfigurationSection<StorageConfiguration>(
+ ConfigurationSection.performanceStorage
+ )
+ if (performanceStorageConfiguration.enabled === true) {
+ logger.info(
+ `${this.logPrefix()} storage enabled: type ${
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ performanceStorageConfiguration.type
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ }, uri: ${performanceStorageConfiguration.uri}`
+ )
+ }
+ }
+
+ public stop (): void {
+ this.stopLogStatisticsInterval()
+ performance.clearMarks()
+ performance.clearMeasures()
+ this.performanceObserver.disconnect()
}
}
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 {
}
}
- 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`
- )
- }
+ })
}
}
// 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)
this.dbName = this.getDBName()
}
- public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
+ private getClientUrl (): string | undefined {
+ switch (this.storageType) {
+ case StorageType.SQLITE:
+ case StorageType.MARIA_DB:
+ case StorageType.MYSQL:
+ return this.storageUri.toString()
+ }
+ }
+
+ private getDBName (): string {
+ if (this.storageType === StorageType.SQLITE) {
+ return `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
+ }
+ return this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '')
+ }
+
+ private getOptions (): MariaDbOptions | SqliteOptions {
+ return {
+ clientUrl: this.getClientUrl(),
+ dbName: this.dbName,
+ entities: ['./dist/types/orm/entities/*.js'],
+ entitiesTs: ['./src/types/orm/entities/*.ts'],
+ }
+ }
+
+ public async close (): Promise<void> {
+ this.clearPerformanceStatistics()
try {
- this.setPerformanceStatistics(performanceStatistics)
- await this.orm?.em.upsert({
- ...performanceStatistics,
- statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({
- name,
- ...value,
- })),
- } satisfies PerformanceRecord)
+ if (this.orm != null) {
+ await this.orm.close()
+ delete this.orm
+ }
} catch (error) {
- this.handleDBStorageError(
- this.storageType,
- error as Error,
- Constants.PERFORMANCE_RECORDS_TABLE
- )
+ this.handleDBStorageError(this.storageType, error as Error)
}
}
}
}
- public async close (): Promise<void> {
- this.clearPerformanceStatistics()
+ public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
try {
- if (this.orm != null) {
- await this.orm.close()
- delete this.orm
- }
+ this.setPerformanceStatistics(performanceStatistics)
+ await this.orm?.em.upsert({
+ ...performanceStatistics,
+ statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({
+ name,
+ ...value,
+ })),
+ } satisfies PerformanceRecord)
} catch (error) {
- this.handleDBStorageError(this.storageType, error as Error)
- }
- }
-
- private getDBName (): string {
- if (this.storageType === StorageType.SQLITE) {
- return `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
- }
- return this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '')
- }
-
- private getOptions (): SqliteOptions | MariaDbOptions {
- return {
- dbName: this.dbName,
- entities: ['./dist/types/orm/entities/*.js'],
- entitiesTs: ['./src/types/orm/entities/*.ts'],
- clientUrl: this.getClientUrl(),
- }
- }
-
- private getClientUrl (): string | undefined {
- switch (this.storageType) {
- case StorageType.SQLITE:
- case StorageType.MARIA_DB:
- case StorageType.MYSQL:
- return this.storageUri.toString()
+ this.handleDBStorageError(
+ this.storageType,
+ error as Error,
+ Constants.PERFORMANCE_RECORDS_TABLE
+ )
}
}
}
this.dbName = this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '')
}
- public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
- try {
- this.setPerformanceStatistics(performanceStatistics)
- this.checkDBConnection()
- await this.client
- ?.db(this.dbName)
- .collection<Statistics>(Constants.PERFORMANCE_RECORDS_TABLE)
- .replaceOne({ id: performanceStatistics.id }, performanceStatistics, {
- upsert: true,
- })
- } catch (error) {
- this.handleDBStorageError(
- StorageType.MONGO_DB,
- error as Error,
- Constants.PERFORMANCE_RECORDS_TABLE
+ private checkDBConnection (): void {
+ if (this.client == null) {
+ throw new BaseError(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix} ${this.getDBNameFromStorageType(
+ StorageType.MONGO_DB
+ )} client initialization failed while trying to issue a request`
)
}
- }
-
- public async open (): Promise<void> {
- try {
- if (!this.connected && this.client != null) {
- await this.client.connect()
- this.connected = true
- }
- } catch (error) {
- this.handleDBStorageError(StorageType.MONGO_DB, error as Error)
+ if (!this.connected) {
+ throw new BaseError(
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
+ `${this.logPrefix} ${this.getDBNameFromStorageType(
+ StorageType.MONGO_DB
+ )} connection not opened while trying to issue a request`
+ )
}
}
}
}
- private checkDBConnection (): void {
- if (this.client == null) {
- throw new BaseError(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix} ${this.getDBNameFromStorageType(
- StorageType.MONGO_DB
- )} client initialization failed while trying to issue a request`
- )
+ public async open (): Promise<void> {
+ try {
+ if (!this.connected && this.client != null) {
+ await this.client.connect()
+ this.connected = true
+ }
+ } catch (error) {
+ this.handleDBStorageError(StorageType.MONGO_DB, error as Error)
}
- if (!this.connected) {
- throw new BaseError(
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- `${this.logPrefix} ${this.getDBNameFromStorageType(
- StorageType.MONGO_DB
- )} connection not opened while trying to issue a request`
+ }
+
+ public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
+ try {
+ this.setPerformanceStatistics(performanceStatistics)
+ this.checkDBConnection()
+ await this.client
+ ?.db(this.dbName)
+ .collection<Statistics>(Constants.PERFORMANCE_RECORDS_TABLE)
+ .replaceOne({ id: performanceStatistics.id }, performanceStatistics, {
+ upsert: true,
+ })
+ } catch (error) {
+ this.handleDBStorageError(
+ StorageType.MONGO_DB,
+ error as Error,
+ Constants.PERFORMANCE_RECORDS_TABLE
)
}
}
// 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 {
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)
}
}
import { logger } from '../../utils/index.js'
export abstract class Storage {
- protected readonly storageUri: URL
- protected readonly logPrefix: string
- protected dbName!: string
private static readonly performanceStatistics = new Map<string, Statistics>()
+ protected dbName!: string
+ protected readonly logPrefix: string
+ protected readonly storageUri: URL
constructor (storageUri: string, logPrefix: string) {
this.storageUri = new URL(storageUri)
this.logPrefix = logPrefix
}
+ protected clearPerformanceStatistics (): void {
+ Storage.performanceStatistics.clear()
+ }
+
+ protected getDBNameFromStorageType (type: StorageType): DBName | undefined {
+ switch (type) {
+ case StorageType.MARIA_DB:
+ return DBName.MARIA_DB
+ case StorageType.MONGO_DB:
+ return DBName.MONGO_DB
+ case StorageType.MYSQL:
+ return DBName.MYSQL
+ case StorageType.SQLITE:
+ return DBName.SQLITE
+ }
+ }
+
protected handleDBStorageError (
type: StorageType,
error: Error,
table?: string,
params: HandleErrorParams<EmptyObject> = {
- throwError: false,
consoleOut: false,
+ throwError: false,
}
): void {
params = {
...{
- throwError: false,
consoleOut: false,
+ throwError: false,
},
...params,
}
}
}
- protected getDBNameFromStorageType (type: StorageType): DBName | undefined {
- switch (type) {
- case StorageType.SQLITE:
- return DBName.SQLITE
- case StorageType.MARIA_DB:
- return DBName.MARIA_DB
- case StorageType.MYSQL:
- return DBName.MYSQL
- case StorageType.MONGO_DB:
- return DBName.MONGO_DB
- }
- }
-
- public getPerformanceStatistics (): IterableIterator<Statistics> {
- return Storage.performanceStatistics.values()
- }
-
protected setPerformanceStatistics (performanceStatistics: Statistics): void {
Storage.performanceStatistics.set(performanceStatistics.id, performanceStatistics)
}
- protected clearPerformanceStatistics (): void {
- Storage.performanceStatistics.clear()
- }
+ public abstract close (): Promise<void> | void
- public abstract open (): void | Promise<void>
- public abstract close (): void | Promise<void>
+ public getPerformanceStatistics (): IterableIterator<Statistics> {
+ return Storage.performanceStatistics.values()
+ }
+ public abstract open (): Promise<void> | void
public abstract storePerformanceStatistics (
performanceStatistics: Statistics
- ): void | Promise<void>
+ ): Promise<void> | void
}
// 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 {
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
-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
-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
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 {
connectorsStatus?: ConnectorStatus[]
}
-export type EvseStatusConfiguration = Omit<EvseStatus, 'connectors'> & {
+export type EvseStatusConfiguration = {
connectorsStatus?: ConnectorStatus[]
-}
+} & Omit<EvseStatus, 'connectors'>
interface EvsesConfiguration {
evsesStatus?: EvseStatusConfiguration[]
}
-export type ChargingStationConfiguration = ChargingStationInfoConfiguration &
+export type ChargingStationConfiguration = {
+ configurationHash?: string
+} & ChargingStationAutomaticTransactionGeneratorConfiguration &
+ ChargingStationInfoConfiguration &
ChargingStationOcppConfiguration &
- ChargingStationAutomaticTransactionGeneratorConfiguration &
ConnectorsConfiguration &
- EvsesConfiguration & {
- configurationHash?: string
- }
+ EvsesConfiguration
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'
}
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
import type { OCPPConfigurationKey } from './ocpp/Configuration.js'
export interface ConfigurationKey extends OCPPConfigurationKey {
- visible?: boolean
reboot?: boolean
+ visible?: boolean
}
export interface ChargingStationOcppConfiguration extends JsonObject {
import type { ClientRequestArgs } from 'node:http'
-
import type { ClientOptions } from 'ws'
import type { AutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator.js'
}
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 {
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 {
}
enum x509CertificateType {
- V2GRootCertificate = 'V2GRootCertificate',
- MORootCertificate = 'MORootCertificate',
+ ChargingStationCertificate = 'ChargingStationCertificate',
CSMSRootCertificate = 'CSMSRootCertificate',
ManufacturerRootCertificate = 'ManufacturerRootCertificate',
- ChargingStationCertificate = 'ChargingStationCertificate',
- V2GCertificate = 'V2GCertificate'
+ MORootCertificate = 'MORootCertificate',
+ V2GCertificate = 'V2GCertificate',
+ V2GRootCertificate = 'V2GRootCertificate'
}
export interface ChargingStationTemplate {
- templateHash?: string
- supervisionUrls?: string | string[]
- supervisionUrlOcppConfiguration?: boolean
- supervisionUrlOcppKey?: string
- supervisionUser?: string
- supervisionPassword?: string
- autoStart?: boolean
- ocppVersion?: OCPPVersion
- ocppProtocol?: OCPPProtocol
- ocppStrictCompliance?: boolean
- ocppPersistentConfiguration?: boolean
- stationInfoPersistentConfiguration?: boolean
+ amperageLimitationOcppKey?: string
+ amperageLimitationUnit?: AmpereUnits
+ AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration
automaticTransactionGeneratorPersistentConfiguration?: boolean
- wsOptions?: WsOptions
- idTagsFile?: string
+ autoReconnectMaxRetries?: number
+ autoRegister?: boolean
+ autoStart?: boolean
baseName: string
- nameSuffix?: string
- fixedName?: boolean
+ beginEndMeterValues?: boolean
+ chargeBoxSerialNumberPrefix?: string
chargePointModel: string
- chargePointVendor: string
chargePointSerialNumberPrefix?: string
- chargeBoxSerialNumberPrefix?: string
- firmwareVersionPattern?: string
- firmwareVersion?: string
+ chargePointVendor: string
+ commandsSupport?: CommandsSupport
+ Configuration?: ChargingStationOcppConfiguration
+ Connectors?: Record<string, ConnectorStatus>
+ currentOutType?: CurrentType
+ customValueLimitationMeterValues?: boolean
+ enableStatistics?: boolean
+ Evses?: Record<string, EvseTemplate>
firmwareUpgrade?: FirmwareUpgrade
+ firmwareVersion?: string
+ firmwareVersionPattern?: string
+ fixedName?: boolean
iccid?: string
+ idTagsFile?: string
imsi?: string
+ mainVoltageMeterValues?: boolean
+ messageTriggerSupport?: Record<MessageTrigger, boolean>
+ meteringPerTransaction?: boolean
meterSerialNumberPrefix?: string
meterType?: string
+ /** @deprecated Replaced by remoteAuthorization. */
+ mustAuthorizeAtRemoteStart?: boolean
+ nameSuffix?: string
+ numberOfConnectors?: number | number[]
+ numberOfPhases?: number
+ ocppPersistentConfiguration?: boolean
+ ocppProtocol?: OCPPProtocol
+ ocppStrictCompliance?: boolean
+ ocppVersion?: OCPPVersion
+ outOfOrderEndMeterValues?: boolean
+ /** @deprecated Replaced by ocppStrictCompliance. */
+ payloadSchemaValidation?: boolean
+ phaseLineToLineVoltageMeterValues?: boolean
power?: number | number[]
- powerUnit?: PowerUnits
powerSharedByConnectors?: boolean
- currentOutType?: CurrentType
- voltageOut?: Voltage
- numberOfPhases?: number
- numberOfConnectors?: number | number[]
- useConnectorId0?: boolean
+ powerUnit?: PowerUnits
randomConnectors?: boolean
- resetTime?: number
- autoRegister?: boolean
- autoReconnectMaxRetries?: number
reconnectExponentialDelay?: boolean
registrationMaxRetries?: number
- enableStatistics?: boolean
remoteAuthorization?: boolean
- /** @deprecated Replaced by remoteAuthorization. */
- mustAuthorizeAtRemoteStart?: boolean
- /** @deprecated Replaced by ocppStrictCompliance. */
- payloadSchemaValidation?: boolean
- amperageLimitationOcppKey?: string
- amperageLimitationUnit?: AmpereUnits
- beginEndMeterValues?: boolean
- outOfOrderEndMeterValues?: boolean
- meteringPerTransaction?: boolean
- transactionDataMeterValues?: boolean
+ resetTime?: number
+ stationInfoPersistentConfiguration?: boolean
stopTransactionsOnStopped?: boolean
- mainVoltageMeterValues?: boolean
- phaseLineToLineVoltageMeterValues?: boolean
- customValueLimitationMeterValues?: boolean
- commandsSupport?: CommandsSupport
- messageTriggerSupport?: Record<MessageTrigger, boolean>
- Configuration?: ChargingStationOcppConfiguration
- AutomaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration
- Evses?: Record<string, EvseTemplate>
- Connectors?: Record<string, ConnectorStatus>
+ supervisionPassword?: string
+ supervisionUrlOcppConfiguration?: boolean
+ supervisionUrlOcppKey?: string
+ supervisionUrls?: string | string[]
+ supervisionUser?: string
+ templateHash?: string
+ transactionDataMeterValues?: boolean
+ useConnectorId0?: boolean
+ voltageOut?: Voltage
+ wsOptions?: WsOptions
x509Certificates?: Record<x509CertificateType, string>
}
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'
import type { BootNotificationResponse } from './ocpp/Responses.js'
import type { Statistics } from './Statistics.js'
+import { ChargingStationEvents } from './ChargingStationEvents.js'
+
export interface ChargingStationOptions extends JsonObject {
- supervisionUrls?: string | string[]
- persistentConfiguration?: boolean
- autoStart?: boolean
autoRegister?: boolean
+ autoStart?: boolean
enableStatistics?: boolean
ocppStrictCompliance?: boolean
+ persistentConfiguration?: boolean
stopTransactionsOnStopped?: boolean
+ supervisionUrls?: string | string[]
}
export interface ChargingStationWorkerData extends WorkerData {
index: number
- templateFile: string
options?: ChargingStationOptions
+ templateFile: string
}
-export type EvseStatusWorkerType = Omit<EvseStatus, 'connectors'> & {
+export type EvseStatusWorkerType = {
connectors?: ConnectorStatus[]
-}
+} & Omit<EvseStatus, 'connectors'>
export interface ChargingStationData extends WorkerData {
- started: boolean
- stationInfo: ChargingStationInfo
+ automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
+ bootNotificationResponse?: BootNotificationResponse
connectors: ConnectorStatus[]
evses: EvseStatusWorkerType[]
ocppConfiguration: ChargingStationOcppConfiguration
+ started: boolean
+ stationInfo: ChargingStationInfo
supervisionUrl: string
wsState?:
+ | typeof WebSocket.CLOSED
+ | typeof WebSocket.CLOSING
| typeof WebSocket.CONNECTING
| typeof WebSocket.OPEN
- | typeof WebSocket.CLOSING
- | typeof WebSocket.CLOSED
- bootNotificationResponse?: BootNotificationResponse
- automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
}
enum ChargingStationMessageEvents {
export type ChargingStationWorkerMessageData = ChargingStationData | Statistics
export interface ChargingStationWorkerMessage<T extends ChargingStationWorkerMessageData> {
- event: ChargingStationWorkerMessageEvents
data: T
+ event: ChargingStationWorkerMessageEvents
}
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'
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 {
}
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 {
}
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 {
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
}
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
}
import type { JsonType } from './JsonType.js'
export interface HandleErrorParams<T extends JsonType> {
- throwError?: boolean
consoleOut?: boolean
errorResponse?: T
+ throwError?: boolean
}
}
export interface EvseStatus {
- connectors: Map<number, ConnectorStatus>
availability: AvailabilityType
+ connectors: Map<number, ConnectorStatus>
}
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'
}
-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[]
export interface MeasurandValues {
+ allPhases: number
L1: number
L2: number
L3: number
- allPhases: number
}
import type { TemplateStatistics } from './Statistics.js'
export interface SimulatorState {
- version: string
configuration: ConfigurationData | undefined
started: boolean
templateStatistics: Map<string, TemplateStatistics>
+ version: string
}
}
export type StatisticsData = Partial<{
- requestCount: number
- responseCount: number
- errorCount: number
- timeMeasurementCount: number
- measurementTimeSeries: CircularBuffer<TimestampedData> | TimestampedData[]
+ avgTimeMeasurement: number
currentTimeMeasurement: number
- minTimeMeasurement: number
+ errorCount: number
maxTimeMeasurement: number
- totalTimeMeasurement: number
- avgTimeMeasurement: number
+ measurementTimeSeries: CircularBuffer<TimestampedData> | TimestampedData[]
medTimeMeasurement: number
+ minTimeMeasurement: number
ninetyFiveThPercentileTimeMeasurement: number
+ requestCount: number
+ responseCount: number
stdDevTimeMeasurement: number
+ timeMeasurementCount: number
+ totalTimeMeasurement: number
}>
export interface Statistics extends WorkerData {
+ createdAt: Date
id: string
name: string
- uri: string
- createdAt: Date
+ statisticsData: Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>
updatedAt?: Date
- statisticsData: Map<string | RequestCommand | IncomingRequestCommand, StatisticsData>
+ uri: string
}
export interface TemplateStatistics {
+ added: number
configured: number
+ indexes: Set<number>
provisioned: number
- added: number
started: number
- indexes: Set<number>
}
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'
}
uuid?: `${string}-${string}-${string}-${string}-${string}`,
procedureName?: ProcedureName,
payload?: RequestPayload
-) => undefined | Promise<undefined> | ResponsePayload | Promise<ResponsePayload>
+) => Promise<ResponsePayload> | Promise<undefined> | ResponsePayload | undefined
export enum ProcedureName {
- SIMULATOR_STATE = 'simulatorState',
- START_SIMULATOR = 'startSimulator',
- STOP_SIMULATOR = 'stopSimulator',
- LIST_TEMPLATES = 'listTemplates',
- LIST_CHARGING_STATIONS = 'listChargingStations',
ADD_CHARGING_STATIONS = 'addChargingStations',
+ AUTHORIZE = 'authorize',
+ BOOT_NOTIFICATION = 'bootNotification',
+ CLOSE_CONNECTION = 'closeConnection',
+ DATA_TRANSFER = 'dataTransfer',
DELETE_CHARGING_STATIONS = 'deleteChargingStations',
- PERFORMANCE_STATISTICS = 'performanceStatistics',
- START_CHARGING_STATION = 'startChargingStation',
- STOP_CHARGING_STATION = 'stopChargingStation',
+ DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
+ FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification',
+ HEARTBEAT = 'heartbeat',
+ LIST_CHARGING_STATIONS = 'listChargingStations',
+ LIST_TEMPLATES = 'listTemplates',
+ METER_VALUES = 'meterValues',
OPEN_CONNECTION = 'openConnection',
- CLOSE_CONNECTION = 'closeConnection',
- START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
- STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+ PERFORMANCE_STATISTICS = 'performanceStatistics',
SET_SUPERVISION_URL = 'setSupervisionUrl',
+ SIMULATOR_STATE = 'simulatorState',
+ START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
+ START_CHARGING_STATION = 'startChargingStation',
+ START_SIMULATOR = 'startSimulator',
START_TRANSACTION = 'startTransaction',
- STOP_TRANSACTION = 'stopTransaction',
- AUTHORIZE = 'authorize',
- BOOT_NOTIFICATION = 'bootNotification',
STATUS_NOTIFICATION = 'statusNotification',
- HEARTBEAT = 'heartbeat',
- METER_VALUES = 'meterValues',
- DATA_TRANSFER = 'dataTransfer',
- DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
- FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification'
+ STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+ STOP_CHARGING_STATION = 'stopChargingStation',
+ STOP_SIMULATOR = 'stopSimulator',
+ STOP_TRANSACTION = 'stopTransaction'
}
export interface RequestPayload extends JsonObject {
- hashIds?: string[]
connectorIds?: number[]
+ hashIds?: string[]
}
export enum ResponseStatus {
- SUCCESS = 'success',
- FAILURE = 'failure'
+ FAILURE = 'failure',
+ SUCCESS = 'success'
}
export interface ResponsePayload extends JsonObject {
- status: ResponseStatus
- hashIdsSucceeded?: string[]
hashIdsFailed?: string[]
+ hashIdsSucceeded?: string[]
responsesFailed?: BroadcastChannelResponsePayload[]
+ status: ResponseStatus
}
})
export enum WebSocketCloseEventStatusCode {
- CLOSE_NORMAL = 1000,
- CLOSE_GOING_AWAY = 1001,
- CLOSE_PROTOCOL_ERROR = 1002,
- CLOSE_UNSUPPORTED = 1003,
- CLOSE_RESERVED = 1004,
- CLOSE_NO_STATUS = 1005,
CLOSE_ABNORMAL = 1006,
+ CLOSE_BAD_GATEWAY = 1014,
+ CLOSE_GOING_AWAY = 1001,
CLOSE_INVALID_PAYLOAD = 1007,
- CLOSE_POLICY_VIOLATION = 1008,
- CLOSE_TOO_LARGE = 1009,
CLOSE_MISSING_EXTENSION = 1010,
+ CLOSE_NO_STATUS = 1005,
+ CLOSE_NORMAL = 1000,
+ CLOSE_POLICY_VIOLATION = 1008,
+ CLOSE_PROTOCOL_ERROR = 1002,
+ CLOSE_RESERVED = 1004,
CLOSE_SERVER_INTERNAL_ERROR = 1011,
CLOSE_SERVICE_RESTART = 1012,
+ CLOSE_TLS_HANDSHAKE = 1015,
+ CLOSE_TOO_LARGE = 1009,
CLOSE_TRY_AGAIN_LATER = 1013,
- CLOSE_BAD_GATEWAY = 1014,
- CLOSE_TLS_HANDSHAKE = 1015
+ CLOSE_UNSUPPORTED = 1003
}
export interface WSError extends Error {
]
export enum BroadcastChannelProcedureName {
- START_CHARGING_STATION = 'startChargingStation',
- STOP_CHARGING_STATION = 'stopChargingStation',
+ AUTHORIZE = 'authorize',
+ BOOT_NOTIFICATION = 'bootNotification',
+ CLOSE_CONNECTION = 'closeConnection',
+ DATA_TRANSFER = 'dataTransfer',
DELETE_CHARGING_STATIONS = 'deleteChargingStations',
+ DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
+ FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification',
+ HEARTBEAT = 'heartbeat',
+ METER_VALUES = 'meterValues',
OPEN_CONNECTION = 'openConnection',
- CLOSE_CONNECTION = 'closeConnection',
- START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
- STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
SET_SUPERVISION_URL = 'setSupervisionUrl',
+ START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
+ START_CHARGING_STATION = 'startChargingStation',
START_TRANSACTION = 'startTransaction',
- STOP_TRANSACTION = 'stopTransaction',
- AUTHORIZE = 'authorize',
- BOOT_NOTIFICATION = 'bootNotification',
STATUS_NOTIFICATION = 'statusNotification',
- HEARTBEAT = 'heartbeat',
- METER_VALUES = 'meterValues',
- DATA_TRANSFER = 'dataTransfer',
- DIAGNOSTICS_STATUS_NOTIFICATION = 'diagnosticsStatusNotification',
- FIRMWARE_STATUS_NOTIFICATION = 'firmwareStatusNotification'
+ STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+ STOP_CHARGING_STATION = 'stopChargingStation',
+ STOP_TRANSACTION = 'stopTransaction'
}
export interface BroadcastChannelRequestPayload extends RequestPayload {
}
export interface BroadcastChannelResponsePayload
- extends Omit<ResponsePayload, 'hashIdsSucceeded' | 'hashIdsFailed' | 'responsesFailed'> {
+ extends Omit<ResponsePayload, 'hashIdsFailed' | 'hashIdsSucceeded' | 'responsesFailed'> {
hashId: string | undefined
}
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'
}
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 {
Core = 'Core',
FirmwareManagement = 'FirmwareManagement',
LocalAuthListManagement = 'LocalAuthListManagement',
+ RemoteTrigger = 'RemoteTrigger',
Reservation = 'Reservation',
- SmartCharging = 'SmartCharging',
- RemoteTrigger = 'RemoteTrigger'
+ SmartCharging = 'SmartCharging'
}
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',
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 {
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 {
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',
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'
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 {
}
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
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
}
export interface RemoteStartTransactionRequest extends JsonObject {
+ chargingProfile?: OCPP16ChargingProfile
connectorId?: number
idTag: string
- chargingProfile?: OCPP16ChargingProfile
}
export interface RemoteStopTransactionRequest extends JsonObject {
}
export interface OCPP16GetCompositeScheduleRequest extends JsonObject {
+ chargingRateUnit?: OCPP16ChargingRateUnitType
connectorId: number
duration: number
- chargingRateUnit?: OCPP16ChargingRateUnitType
}
export interface SetChargingProfileRequest 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
}
Downloading = 'Downloading',
Idle = 'Idle',
InstallationFailed = 'InstallationFailed',
- Installing = 'Installing',
- Installed = 'Installed'
+ Installed = 'Installed',
+ Installing = 'Installing'
}
export interface OCPP16FirmwareStatusNotificationRequest extends JsonObject {
}
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 {
}
export enum OCPP16UnlockStatus {
- UNLOCKED = 'Unlocked',
+ NOT_SUPPORTED = 'NotSupported',
UNLOCK_FAILED = 'UnlockFailed',
- NOT_SUPPORTED = 'NotSupported'
+ UNLOCKED = 'Unlocked'
}
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 {
}
export interface OCPP16BootNotificationResponse extends JsonObject {
- status: RegistrationStatusEnumType
currentTime: Date
interval: number
+ status: RegistrationStatusEnumType
}
export type OCPP16StatusNotificationResponse = EmptyObject
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 {
export enum OCPP16TriggerMessageStatus {
ACCEPTED = 'Accepted',
- REJECTED = 'Rejected',
- NOT_IMPLEMENTED = 'NotImplemented'
+ NOT_IMPLEMENTED = 'NotImplemented',
+ REJECTED = 'Rejected'
}
export interface OCPP16TriggerMessageResponse extends JsonObject {
}
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 {
import type { OCPP16MeterValue } from './MeterValues.js'
export enum OCPP16StopTransactionReason {
+ DE_AUTHORIZED = 'DeAuthorized',
EMERGENCY_STOP = 'EmergencyStop',
EV_DISCONNECTED = 'EVDisconnected',
HARD_RESET = 'HardReset',
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 {
connectorId: number
idTag: string
meterStart: number
- timestamp: Date
reservationId?: number
+ timestamp: Date
}
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 {
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 {
}
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
}
export enum GetCertificateIdUseEnumType {
- V2GRootCertificate = 'V2GRootCertificate',
- MORootCertificate = 'MORootCertificate',
CSMSRootCertificate = 'CSMSRootCertificate',
+ ManufacturerRootCertificate = 'ManufacturerRootCertificate',
+ MORootCertificate = 'MORootCertificate',
V2GCertificateChain = 'V2GCertificateChain',
- ManufacturerRootCertificate = 'ManufacturerRootCertificate'
+ V2GRootCertificate = 'V2GRootCertificate'
}
export enum GetCertificateStatusEnumType {
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 {
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
}
}
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
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 {
}
export interface OCPP20InstallCertificateRequest extends JsonObject {
- certificateType: InstallCertificateUseEnumType
certificate: string
+ certificateType: InstallCertificateUseEnumType
}
export interface OCPP20BootNotificationResponse extends JsonObject {
currentTime: Date
- status: RegistrationStatusEnumType
interval: number
+ status: RegistrationStatusEnumType
statusInfo?: StatusInfoType
}
}
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 {
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 {
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 {
import type { JsonObject } from '../JsonType.js'
+
import {
OCPP16StandardParametersKey,
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
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'
}
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
}
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,
OCPP20RequestCommand,
type OCPP20StatusNotificationRequest,
} from './2.0/Requests.js'
-import type { MessageType } from './MessageType.js'
export const RequestCommand = {
...OCPP16RequestCommand,
export interface RequestParams {
skipBufferingOnError?: boolean
- triggerMessage?: boolean
throwError?: boolean
+ triggerMessage?: boolean
}
export const IncomingRequestCommand = {
export type CachedRequest = [
ResponseCallback,
ErrorCallback,
- RequestCommand | IncomingRequestCommand,
+ IncomingRequestCommand | RequestCommand,
JsonType
]
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'
}
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,
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]
chargingStation: ChargingStation,
payload: JsonType,
requestPayload?: JsonType
-) => void | Promise<void>
+) => Promise<void> | void
export type BootNotificationResponse =
| OCPP16BootNotificationResponse
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
name!: string
@Property()
- uri!: string
-
- @Property()
- createdAt!: Date
+ statisticsData!: Partial<StatisticsData>[]
@Property()
updatedAt?: Date
@Property()
- statisticsData!: Partial<StatisticsData>[]
+ uri!: string
}
performance = 'performance'
}
-type ResolveType = (value: void | PromiseLike<void>) => void
+type ResolveType = (value: PromiseLike<void> | void) => void
export class AsyncLock {
private static readonly asyncLocks = new Map<AsyncLockType, AsyncLock>()
this.resolveQueue = new Queue<ResolveType>()
}
- public static async runExclusive<T>(type: AsyncLockType, fn: () => T | Promise<T>): Promise<T> {
- try {
- await AsyncLock.acquire(type)
- if (isAsyncFunction(fn)) {
- return await fn()
- } else {
- return fn() as T
- }
- } finally {
- await AsyncLock.release(type)
- }
- }
-
private static async acquire (type: AsyncLockType): Promise<void> {
const asyncLock = AsyncLock.getAsyncLock(type)
if (!asyncLock.acquired) {
})
}
+ private static getAsyncLock (type: AsyncLockType): AsyncLock {
+ if (!AsyncLock.asyncLocks.has(type)) {
+ AsyncLock.asyncLocks.set(type, new AsyncLock())
+ }
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return AsyncLock.asyncLocks.get(type)!
+ }
+
private static async release (type: AsyncLockType): Promise<void> {
const asyncLock = AsyncLock.getAsyncLock(type)
if (asyncLock.resolveQueue.size === 0 && asyncLock.acquired) {
})
}
- private static getAsyncLock (type: AsyncLockType): AsyncLock {
- if (!AsyncLock.asyncLocks.has(type)) {
- AsyncLock.asyncLocks.set(type, new AsyncLock())
+ public static async runExclusive<T>(type: AsyncLockType, fn: () => Promise<T> | T): Promise<T> {
+ try {
+ await AsyncLock.acquire(type)
+ if (isAsyncFunction(fn)) {
+ return await fn()
+ } else {
+ return fn() as T
+ }
+ } finally {
+ await AsyncLock.release(type)
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return AsyncLock.asyncLocks.get(type)!
}
}
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(
)
let status: EvseStatusConfiguration
switch (outputFormat) {
- case OutputFormat.worker:
- return {
- ...evseStatus,
- connectors: connectorsStatus,
- }
case OutputFormat.configuration:
status = {
...evseStatus,
}
delete (status as EvseStatusWorkerType).connectors
return status
+ case OutputFormat.worker:
+ return {
+ ...evseStatus,
+ connectors: connectorsStatus,
+ }
}
})
}
+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 {
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 = {
const defaultLogConfiguration: LogConfiguration = {
enabled: true,
- file: 'logs/combined.log',
errorFile: 'logs/error.log',
- statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
- level: 'info',
+ file: 'logs/combined.log',
format: 'simple',
+ level: 'info',
rotate: true,
+ statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
}
const defaultWorkerConfiguration: WorkerConfiguration = {
- processType: WorkerProcessType.workerSet,
- startDelay: DEFAULT_WORKER_START_DELAY,
- elementsPerWorker: 'auto',
elementAddDelay: DEFAULT_ELEMENT_ADD_DELAY,
- poolMinSize: DEFAULT_POOL_MIN_SIZE,
+ elementsPerWorker: 'auto',
poolMaxSize: DEFAULT_POOL_MAX_SIZE,
+ poolMinSize: DEFAULT_POOL_MIN_SIZE,
+ processType: WorkerProcessType.workerSet,
+ startDelay: DEFAULT_WORKER_START_DELAY,
}
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Configuration {
public static configurationChangeCallback?: () => Promise<void>
+ private static configurationData?: ConfigurationData
private static configurationFile: string | undefined
private static configurationFileReloading = false
- private static configurationData?: ConfigurationData
private static configurationFileWatcher?: FSWatcher
private static configurationSectionCache: Map<ConfigurationSection, ConfigurationSectionType>
- static {
- const configurationFile = join(dirname(fileURLToPath(import.meta.url)), 'assets', 'config.json')
- if (existsSync(configurationFile)) {
- Configuration.configurationFile = configurationFile
- } else {
- console.error(
- `${chalk.green(logPrefix())} ${chalk.red(
- "Configuration file './src/assets/config.json' not found, using default configuration"
- )}`
- )
- Configuration.configurationData = {
- stationTemplateUrls: [
- {
- file: 'siemens.station-template.json',
- numberOfStations: 1,
- },
- ],
- supervisionUrls: 'ws://localhost:8180/steve/websocket/CentralSystemService',
- supervisionUrlDistribution: SupervisionUrlDistribution.ROUND_ROBIN,
- uiServer: defaultUIServerConfiguration,
- performanceStorage: defaultStorageConfiguration,
- log: defaultLogConfiguration,
- worker: defaultWorkerConfiguration,
- }
- }
- Configuration.configurationSectionCache = new Map<
- ConfigurationSection,
- ConfigurationSectionType
- >([
- [ConfigurationSection.log, Configuration.buildLogSection()],
- [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
- [ConfigurationSection.worker, Configuration.buildWorkerSection()],
- [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
- ])
- }
-
private constructor () {
// This is intentional
}
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
- public static getConfigurationSection<T extends ConfigurationSectionType>(
- sectionName: ConfigurationSection
- ): T {
- if (!Configuration.isConfigurationSectionCached(sectionName)) {
- Configuration.cacheConfigurationSection(sectionName)
- }
- return Configuration.configurationSectionCache.get(sectionName) as T
- }
-
- public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
- const checkDeprecatedConfigurationKeysOnce = once(
- Configuration.checkDeprecatedConfigurationKeys.bind(Configuration)
- )
- checkDeprecatedConfigurationKeysOnce()
- return Configuration.getConfigurationData()?.stationTemplateUrls
- }
-
- public static getSupervisionUrls (): string | string[] | undefined {
- if (
- Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null
- ) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
- 'supervisionURLs' as keyof ConfigurationData
- ] as string | string[]
- }
- return Configuration.getConfigurationData()?.supervisionUrls
- }
-
- public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
- return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
- ? Configuration.getConfigurationData()?.supervisionUrlDistribution
- : SupervisionUrlDistribution.ROUND_ROBIN
- }
-
- public static workerPoolInUse (): boolean {
- return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
- .processType!
- )
- }
-
- public static workerDynamicPoolInUse (): boolean {
- return (
- Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
- .processType === WorkerProcessType.dynamicPool
- )
- }
-
- private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
- return Configuration.configurationSectionCache.has(sectionName)
- }
-
- private static cacheConfigurationSection (sectionName: ConfigurationSection): void {
- switch (sectionName) {
- case ConfigurationSection.log:
- Configuration.configurationSectionCache.set(sectionName, Configuration.buildLogSection())
- break
- case ConfigurationSection.performanceStorage:
- Configuration.configurationSectionCache.set(
- sectionName,
- Configuration.buildPerformanceStorageSection()
- )
- break
- case ConfigurationSection.worker:
- Configuration.configurationSectionCache.set(sectionName, Configuration.buildWorkerSection())
- break
- case ConfigurationSection.uiServer:
- Configuration.configurationSectionCache.set(
- sectionName,
- Configuration.buildUIServerSection()
- )
- break
- default:
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
- throw new Error(`Unknown configuration section '${sectionName}'`)
- }
- }
-
- private static buildUIServerSection (): UIServerConfiguration {
- let uiServerConfiguration: UIServerConfiguration = defaultUIServerConfiguration
- if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.uiServer)) {
- uiServerConfiguration = mergeDeepRight(
- uiServerConfiguration,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- Configuration.getConfigurationData()!.uiServer!
- )
+ private static buildLogSection (): LogConfiguration {
+ const deprecatedLogConfiguration: LogConfiguration = {
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logEnabled') && {
+ enabled: Configuration.getConfigurationData()?.logEnabled,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logFile') && {
+ file: Configuration.getConfigurationData()?.logFile,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logErrorFile') && {
+ errorFile: Configuration.getConfigurationData()?.logErrorFile,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logStatisticsInterval') && {
+ statisticsInterval: Configuration.getConfigurationData()?.logStatisticsInterval,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logLevel') && {
+ level: Configuration.getConfigurationData()?.logLevel,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logConsole') && {
+ console: Configuration.getConfigurationData()?.logConsole,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logFormat') && {
+ format: Configuration.getConfigurationData()?.logFormat,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logRotate') && {
+ rotate: Configuration.getConfigurationData()?.logRotate,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxFiles') && {
+ maxFiles: Configuration.getConfigurationData()?.logMaxFiles,
+ }),
+ ...(hasOwnProp(Configuration.getConfigurationData(), 'logMaxSize') && {
+ maxSize: Configuration.getConfigurationData()?.logMaxSize,
+ }),
}
- if (isCFEnvironment()) {
- delete uiServerConfiguration.options?.host
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- uiServerConfiguration.options!.port = Number.parseInt(env.PORT!)
+ const logConfiguration: LogConfiguration = {
+ ...defaultLogConfiguration,
+ ...deprecatedLogConfiguration,
+ ...(hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.log) &&
+ Configuration.getConfigurationData()?.log),
}
- return uiServerConfiguration
+ return logConfiguration
}
private static buildPerformanceStorageSection (): StorageConfiguration {
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 {
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(
}
}
- private static warnDeprecatedConfigurationKey (
- key: string,
- configurationSection?: ConfigurationSection,
- logMsgToAppend = ''
- ): void {
- if (
- configurationSection != null &&
- Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
- null &&
- (
- Configuration.getConfigurationData()?.[
- configurationSection as keyof ConfigurationData
- ] as Record<string, unknown>
- )[key] != null
- ) {
- console.error(
- `${chalk.green(logPrefix())} ${chalk.red(
- `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
- logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
- }`
- )}`
- )
- } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
- console.error(
- `${chalk.green(logPrefix())} ${chalk.red(
- `Deprecated configuration key '${key}' usage${
- logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
- }`
- )}`
- )
- }
- }
-
public static getConfigurationData (): ConfigurationData | undefined {
if (
Configuration.configurationData == null &&
)
}
}
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
+ public static getConfigurationSection<T extends ConfigurationSectionType>(
+ sectionName: ConfigurationSection
+ ): T {
+ if (!Configuration.isConfigurationSectionCached(sectionName)) {
+ Configuration.cacheConfigurationSection(sectionName)
+ }
+ return Configuration.configurationSectionCache.get(sectionName) as T
+ }
+
+ public static getStationTemplateUrls (): StationTemplateUrl[] | undefined {
+ const checkDeprecatedConfigurationKeysOnce = once(
+ Configuration.checkDeprecatedConfigurationKeys.bind(Configuration)
+ )
+ checkDeprecatedConfigurationKeysOnce()
+ return Configuration.getConfigurationData()?.stationTemplateUrls
+ }
+
+ public static getSupervisionUrlDistribution (): SupervisionUrlDistribution | undefined {
+ return hasOwnProp(Configuration.getConfigurationData(), 'supervisionUrlDistribution')
+ ? Configuration.getConfigurationData()?.supervisionUrlDistribution
+ : SupervisionUrlDistribution.ROUND_ROBIN
+ }
+
+ public static getSupervisionUrls (): string | string[] | undefined {
+ if (
+ Configuration.getConfigurationData()?.['supervisionURLs' as keyof ConfigurationData] != null
+ ) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Configuration.getConfigurationData()!.supervisionUrls = Configuration.getConfigurationData()![
+ 'supervisionURLs' as keyof ConfigurationData
+ ] as string | string[]
+ }
+ return Configuration.getConfigurationData()?.supervisionUrls
+ }
+
+ private static isConfigurationSectionCached (sectionName: ConfigurationSection): boolean {
+ return Configuration.configurationSectionCache.has(sectionName)
+ }
+
+ private static warnDeprecatedConfigurationKey (
+ key: string,
+ configurationSection?: ConfigurationSection,
+ logMsgToAppend = ''
+ ): void {
+ if (
+ configurationSection != null &&
+ Configuration.getConfigurationData()?.[configurationSection as keyof ConfigurationData] !=
+ null &&
+ (
+ Configuration.getConfigurationData()?.[
+ configurationSection as keyof ConfigurationData
+ ] as Record<string, unknown>
+ )[key] != null
+ ) {
+ console.error(
+ `${chalk.green(logPrefix())} ${chalk.red(
+ `Deprecated configuration key '${key}' usage in section '${configurationSection}'${
+ logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+ }`
+ )}`
+ )
+ } else if (Configuration.getConfigurationData()?.[key as keyof ConfigurationData] != null) {
+ console.error(
+ `${chalk.green(logPrefix())} ${chalk.red(
+ `Deprecated configuration key '${key}' usage${
+ logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
+ }`
+ )}`
+ )
+ }
+ }
+
+ public static workerDynamicPoolInUse (): boolean {
+ return (
+ Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
+ .processType === WorkerProcessType.dynamicPool
+ )
+ }
+
+ public static workerPoolInUse (): boolean {
+ return [WorkerProcessType.dynamicPool, WorkerProcessType.fixedPool].includes(
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ Configuration.getConfigurationSection<WorkerConfiguration>(ConfigurationSection.worker)
+ .processType!
+ )
+ }
+
+ static {
+ const configurationFile = join(dirname(fileURLToPath(import.meta.url)), 'assets', 'config.json')
+ if (existsSync(configurationFile)) {
+ Configuration.configurationFile = configurationFile
+ } else {
+ console.error(
+ `${chalk.green(logPrefix())} ${chalk.red(
+ "Configuration file './src/assets/config.json' not found, using default configuration"
+ )}`
+ )
+ Configuration.configurationData = {
+ log: defaultLogConfiguration,
+ performanceStorage: defaultStorageConfiguration,
+ stationTemplateUrls: [
+ {
+ file: 'siemens.station-template.json',
+ numberOfStations: 1,
+ },
+ ],
+ supervisionUrlDistribution: SupervisionUrlDistribution.ROUND_ROBIN,
+ supervisionUrls: 'ws://localhost:8180/steve/websocket/CentralSystemService',
+ uiServer: defaultUIServerConfiguration,
+ worker: defaultWorkerConfiguration,
+ }
+ }
+ Configuration.configurationSectionCache = new Map<
+ ConfigurationSection,
+ ConfigurationSectionType
+ >([
+ [ConfigurationSection.log, Configuration.buildLogSection()],
+ [ConfigurationSection.performanceStorage, Configuration.buildPerformanceStorageSection()],
+ [ConfigurationSection.uiServer, Configuration.buildUIServerSection()],
+ [ConfigurationSection.worker, Configuration.buildWorkerSection()],
+ ])
+ }
}
+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'
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class Constants {
- // See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
- private static readonly SEMVER_PATTERN =
- '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$'
-
- private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 30000 // Ms
-
- static readonly DEFAULT_STATION_INFO: Partial<ChargingStationInfo> = Object.freeze({
- enableStatistics: false,
- autoStart: true,
- remoteAuthorization: true,
- currentOutType: CurrentType.AC,
- mainVoltageMeterValues: true,
- phaseLineToLineVoltageMeterValues: false,
- customValueLimitationMeterValues: true,
- ocppStrictCompliance: true,
- outOfOrderEndMeterValues: false,
- beginEndMeterValues: false,
- meteringPerTransaction: true,
- transactionDataMeterValues: false,
- supervisionUrlOcppConfiguration: false,
- supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
- useConnectorId0: true,
- ocppVersion: OCPPVersion.VERSION_16,
- firmwareVersionPattern: Constants.SEMVER_PATTERN,
- firmwareUpgrade: {
- versionUpgrade: {
- step: 1,
- },
- reset: true,
- },
- ocppPersistentConfiguration: true,
- stationInfoPersistentConfiguration: true,
- automaticTransactionGeneratorPersistentConfiguration: true,
- resetTime: Constants.DEFAULT_CHARGING_STATION_RESET_TIME,
- autoReconnectMaxRetries: -1,
- registrationMaxRetries: -1,
- reconnectExponentialDelay: false,
- stopTransactionsOnStopped: true,
- })
-
- static readonly DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000 // Ms
- static readonly DEFAULT_HEARTBEAT_INTERVAL = 60000 // Ms
- static readonly DEFAULT_METER_VALUES_INTERVAL = 60000 // Ms
-
- static readonly DEFAULT_ATG_WAIT_TIME = 1000 // Ms
static readonly DEFAULT_ATG_CONFIGURATION: AutomaticTransactionGeneratorConfiguration =
Object.freeze({
enable: false,
- minDuration: 60,
+ maxDelayBetweenTwoTransactions: 30,
maxDuration: 120,
minDelayBetweenTwoTransactions: 15,
- maxDelayBetweenTwoTransactions: 30,
+ minDuration: 60,
probabilityOfStart: 1,
- stopAfterHours: 0.25,
stopAbsoluteDuration: false,
+ stopAfterHours: 0.25,
})
+ static readonly DEFAULT_ATG_WAIT_TIME = 1000 // Ms
+
+ static readonly DEFAULT_BOOT_NOTIFICATION_INTERVAL = 60000 // Ms
+
+ private static readonly DEFAULT_CHARGING_STATION_RESET_TIME = 30000 // Ms
static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 386
+ static readonly DEFAULT_CONNECTION_TIMEOUT = 30
+ static readonly DEFAULT_FLUCTUATION_PERCENT = 5
static readonly DEFAULT_HASH_ALGORITHM = 'sha384'
- static readonly DEFAULT_IDTAG = '00000000'
+ static readonly DEFAULT_HEARTBEAT_INTERVAL = 60000 // Ms
- static readonly DEFAULT_CONNECTION_TIMEOUT = 30
+ static readonly DEFAULT_IDTAG = '00000000'
static readonly DEFAULT_LOG_STATISTICS_INTERVAL = 60 // Seconds
- static readonly DEFAULT_FLUCTUATION_PERCENT = 5
+ static readonly DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL = 60000 // Ms
+
+ static readonly DEFAULT_METER_VALUES_INTERVAL = 60000 // Ms
static readonly DEFAULT_PERFORMANCE_DIRECTORY = 'performance'
- static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'
+
static readonly DEFAULT_PERFORMANCE_RECORDS_DB_NAME = 'e-mobility-charging-stations-simulator'
- static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records'
+ static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'
+ static readonly DEFAULT_STATION_INFO: Partial<ChargingStationInfo> = Object.freeze({
+ automaticTransactionGeneratorPersistentConfiguration: true,
+ autoReconnectMaxRetries: -1,
+ autoStart: true,
+ beginEndMeterValues: false,
+ currentOutType: CurrentType.AC,
+ customValueLimitationMeterValues: true,
+ enableStatistics: false,
+ firmwareUpgrade: {
+ reset: true,
+ versionUpgrade: {
+ step: 1,
+ },
+ },
+ firmwareVersionPattern: Constants.SEMVER_PATTERN,
+ mainVoltageMeterValues: true,
+ meteringPerTransaction: true,
+ ocppPersistentConfiguration: true,
+ ocppStrictCompliance: true,
+ ocppVersion: OCPPVersion.VERSION_16,
+ outOfOrderEndMeterValues: false,
+ phaseLineToLineVoltageMeterValues: false,
+ reconnectExponentialDelay: false,
+ registrationMaxRetries: -1,
+ remoteAuthorization: true,
+ resetTime: Constants.DEFAULT_CHARGING_STATION_RESET_TIME,
+ stationInfoPersistentConfiguration: true,
+ stopTransactionsOnStopped: true,
+ supervisionUrlOcppConfiguration: false,
+ supervisionUrlOcppKey: VendorParametersKey.ConnectionUrl,
+ transactionDataMeterValues: false,
+ useConnectorId0: true,
+ })
static readonly DEFAULT_UI_SERVER_HOST = 'localhost'
+
static readonly DEFAULT_UI_SERVER_PORT = 8080
+ static readonly EMPTY_FROZEN_OBJECT = Object.freeze({})
- static readonly UNKNOWN_OCPP_COMMAND = 'unknown OCPP command' as
- | RequestCommand
- | IncomingRequestCommand
+ static readonly EMPTY_FUNCTION = Object.freeze(() => {
+ /* This is intentional */
+ })
static readonly MAX_RANDOM_INTEGER = 281474976710655
- static readonly STOP_CHARGING_STATIONS_TIMEOUT = 60000 // Ms
+ static readonly PERFORMANCE_RECORDS_TABLE = 'performance_records'
- static readonly EMPTY_FROZEN_OBJECT = Object.freeze({})
- static readonly EMPTY_FUNCTION = Object.freeze(() => {
- /* This is intentional */
- })
+ // See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+ private static readonly SEMVER_PATTERN =
+ '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$'
- static readonly DEFAULT_MESSAGE_BUFFER_FLUSH_INTERVAL = 60000 // Ms
+ static readonly STOP_CHARGING_STATIONS_TIMEOUT = 60000 // Ms
+
+ static readonly UNKNOWN_OCPP_COMMAND = 'unknown OCPP command' as
+ | IncomingRequestCommand
+ | RequestCommand
private constructor () {
// This is intentional
// 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 {
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)
}
}
// 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) {
}
return Math.round(amperage)
}
+
+ static power (V: number, I: number): number {
+ return V * I
+ }
}
-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,
MessageType,
RequestCommand,
} from '../types/index.js'
+
+import { getMessageTypeString } from '../charging-station/ocpp/OCPPServiceUtils.js'
import { logger } from './Logger.js'
import { isNotEmptyString } from './Utils.js'
): void => {
params = {
...{
- throwError: true,
consoleOut: false,
+ throwError: true,
},
...params,
}
export const handleSendMessageError = (
chargingStation: ChargingStation,
- commandName: RequestCommand | IncomingRequestCommand,
+ commandName: IncomingRequestCommand | RequestCommand,
messageType: MessageType,
error: Error,
params?: HandleErrorParams<EmptyObject>
): void => {
params = {
...{
- throwError: false,
consoleOut: false,
+ throwError: false,
},
...params,
}
): T | undefined => {
params = {
...{
- throwError: true,
consoleOut: false,
+ throwError: true,
},
...params,
}
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'
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'
}
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,
})
import { CircularBuffer } from 'mnemonist'
import type { ChargingStation } from '../charging-station/index.js'
+
import {
type ChargingStationData,
type ChargingStationWorkerMessage,
chargingStation: ChargingStation
): ChargingStationWorkerMessage<ChargingStationData> => {
return {
- event: ChargingStationWorkerMessageEvents.added,
data: buildChargingStationDataPayload(chargingStation),
+ event: ChargingStationWorkerMessageEvents.added,
}
}
chargingStation: ChargingStation
): ChargingStationWorkerMessage<ChargingStationData> => {
return {
- event: ChargingStationWorkerMessageEvents.deleted,
data: buildChargingStationDataPayload(chargingStation),
+ event: ChargingStationWorkerMessageEvents.deleted,
}
}
chargingStation: ChargingStation
): ChargingStationWorkerMessage<ChargingStationData> => {
return {
- event: ChargingStationWorkerMessageEvents.started,
data: buildChargingStationDataPayload(chargingStation),
+ event: ChargingStationWorkerMessageEvents.started,
}
}
chargingStation: ChargingStation
): ChargingStationWorkerMessage<ChargingStationData> => {
return {
- event: ChargingStationWorkerMessageEvents.stopped,
data: buildChargingStationDataPayload(chargingStation),
+ event: ChargingStationWorkerMessageEvents.stopped,
}
}
chargingStation: ChargingStation
): ChargingStationWorkerMessage<ChargingStationData> => {
return {
- event: ChargingStationWorkerMessageEvents.updated,
data: buildChargingStationDataPayload(chargingStation),
+ event: ChargingStationWorkerMessageEvents.updated,
}
}
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),
-import { getRandomValues, randomBytes, randomUUID } from 'node:crypto'
-import { env, nextTick } from 'node:process'
+import type { CircularBuffer } from 'mnemonist'
import {
formatDuration,
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 {
}
export const convertToDate = (
- value: Date | string | number | undefined | null
+ value: Date | null | number | string | undefined
): Date | undefined => {
if (value == null) {
return undefined
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
T extends
| JsonType
+ | Map<string, Record<string, unknown>>
| Record<string, unknown>[]
| Set<Record<string, unknown>>
- | Map<string, Record<string, unknown>>
>(
object: T,
- space?: string | number,
+ space?: number | string,
mapFormat?: MapStringifyFormat
): string => {
return JSON.stringify(
import type { EventEmitterAsyncResource } from 'node:events'
-import { existsSync } from 'node:fs'
-
import type { PoolInfo } from 'poolifier'
+import { existsSync } from 'node:fs'
+
import type { SetInfo, WorkerData, WorkerOptions } from './WorkerTypes.js'
export abstract class WorkerAbstract<D extends WorkerData, R extends WorkerData> {
- protected readonly workerScript: string
protected readonly workerOptions: WorkerOptions
+ protected readonly workerScript: string
+ public abstract readonly emitter: EventEmitterAsyncResource | undefined
public abstract readonly info: PoolInfo | SetInfo
- public abstract readonly size: number
public abstract readonly maxElementsPerWorker: number | undefined
- public abstract readonly emitter: EventEmitterAsyncResource | undefined
+ public abstract readonly size: number
/**
* `WorkerAbstract` constructor.
this.workerOptions = workerOptions
}
+ /**
+ * Adds a task element to the worker pool/set.
+ * @param elementData -
+ */
+ public abstract addElement (elementData: D): Promise<R>
/**
* Starts the worker pool/set.
*/
- public abstract start (): void | Promise<void>
+ public abstract start (): Promise<void> | void
/**
* Stops the worker pool/set.
*/
public abstract stop (): Promise<void>
- /**
- * Adds a task element to the worker pool/set.
- * @param elementData -
- */
- public abstract addElement (elementData: D): Promise<R>
}
import { availableParallelism } from 'poolifier'
import type { WorkerOptions } from './WorkerTypes.js'
+
import { defaultErrorHandler, defaultExitHandler } from './WorkerUtils.js'
export const EMPTY_FUNCTION = Object.freeze(() => {
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,
})
import { DynamicThreadPool, type PoolInfo } from 'poolifier'
-import { WorkerAbstract } from './WorkerAbstract.js'
import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
+
+import { WorkerAbstract } from './WorkerAbstract.js'
import { randomizeDelay, sleep } from './WorkerUtils.js'
export class WorkerDynamicPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
)
}
- get info (): PoolInfo {
- return this.pool.info
+ /** @inheritDoc */
+ public async addElement (elementData: D): Promise<R> {
+ const response = await this.pool.execute(elementData)
+ // Start element sequentially to optimize memory at startup
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.workerOptions.elementAddDelay! > 0 &&
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+ return response
}
- get size (): number {
- return this.pool.info.workerNodes
+ /** @inheritDoc */
+ public start (): void {
+ this.pool.start()
}
- get maxElementsPerWorker (): number | undefined {
- return undefined
+ /** @inheritDoc */
+ public async stop (): Promise<void> {
+ await this.pool.destroy()
}
get emitter (): EventEmitterAsyncResource | undefined {
return this.pool.emitter
}
- /** @inheritDoc */
- public start (): void {
- this.pool.start()
+ get info (): PoolInfo {
+ return this.pool.info
}
- /** @inheritDoc */
- public async stop (): Promise<void> {
- await this.pool.destroy()
+ get maxElementsPerWorker (): number | undefined {
+ return undefined
}
- /** @inheritDoc */
- public async addElement (elementData: D): Promise<R> {
- const response = await this.pool.execute(elementData)
- // Start element sequentially to optimize memory at startup
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.workerOptions.elementAddDelay! > 0 &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
- return response
+ get size (): number {
+ return this.pool.info.workerNodes
}
}
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'
}
workerOptions = mergeDeepRight<WorkerOptions>(DEFAULT_WORKER_OPTIONS, workerOptions ?? {})
switch (workerProcessType) {
- case WorkerProcessType.workerSet:
- return new WorkerSet<D, R>(workerScript, workerOptions)
- case WorkerProcessType.fixedPool:
- return new WorkerFixedPool<D, R>(workerScript, workerOptions)
case WorkerProcessType.dynamicPool:
return new WorkerDynamicPool<D, R>(workerScript, workerOptions)
+ case WorkerProcessType.fixedPool:
+ return new WorkerFixedPool<D, R>(workerScript, workerOptions)
+ case WorkerProcessType.workerSet:
+ return new WorkerSet<D, R>(workerScript, workerOptions)
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Worker implementation type '${workerProcessType}' not found`)
import { FixedThreadPool, type PoolInfo } from 'poolifier'
-import { WorkerAbstract } from './WorkerAbstract.js'
import type { WorkerData, WorkerOptions } from './WorkerTypes.js'
+
+import { WorkerAbstract } from './WorkerAbstract.js'
import { randomizeDelay, sleep } from './WorkerUtils.js'
export class WorkerFixedPool<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<
)
}
- get info (): PoolInfo {
- return this.pool.info
+ /** @inheritDoc */
+ public async addElement (elementData: D): Promise<R> {
+ const response = await this.pool.execute(elementData)
+ // Start element sequentially to optimize memory at startup
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.workerOptions.elementAddDelay! > 0 &&
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
+ return response
}
- get size (): number {
- return this.pool.info.workerNodes
+ /** @inheritDoc */
+ public start (): void {
+ this.pool.start()
}
- get maxElementsPerWorker (): number | undefined {
- return undefined
+ /** @inheritDoc */
+ public async stop (): Promise<void> {
+ await this.pool.destroy()
}
get emitter (): EventEmitterAsyncResource | undefined {
return this.pool.emitter
}
- /** @inheritDoc */
- public start (): void {
- this.pool.start()
+ get info (): PoolInfo {
+ return this.pool.info
}
- /** @inheritDoc */
- public async stop (): Promise<void> {
- await this.pool.destroy()
+ get maxElementsPerWorker (): number | undefined {
+ return undefined
}
- /** @inheritDoc */
- public async addElement (elementData: D): Promise<R> {
- const response = await this.pool.execute(elementData)
- // Start element sequentially to optimize memory at startup
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.workerOptions.elementAddDelay! > 0 &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (await sleep(randomizeDelay(this.workerOptions.elementAddDelay!)))
- return response
+ get size (): number {
+ return this.pool.info.workerNodes
}
}
import { randomizeDelay, sleep } from './WorkerUtils.js'
interface ResponseWrapper<R extends WorkerData> {
- resolve: (value: R | PromiseLike<R>) => void
reject: (reason?: unknown) => void
+ resolve: (value: PromiseLike<R> | R) => void
workerSetElement: WorkerSetElement
}
export class WorkerSet<D extends WorkerData, R extends WorkerData> extends WorkerAbstract<D, R> {
- public readonly emitter: EventEmitterAsyncResource | undefined
- private readonly workerSet: Set<WorkerSetElement>
private readonly promiseResponseMap: Map<
`${string}-${string}-${string}-${string}`,
ResponseWrapper<R>
>
private started: boolean
+ private readonly workerSet: Set<WorkerSetElement>
+
private workerStartup: boolean
+ public readonly emitter: EventEmitterAsyncResource | undefined
/**
* Creates a new `WorkerSet`.
this.workerStartup = false
}
- get info (): SetInfo {
- return {
- version: workerSetVersion,
- type: 'set',
- worker: 'thread',
- started: this.started,
- size: this.size,
- elementsExecuting: [...this.workerSet].reduce(
- (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements,
- 0
- ),
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- elementsPerWorker: this.maxElementsPerWorker!,
- }
- }
-
- get size (): number {
- return this.workerSet.size
- }
-
- get maxElementsPerWorker (): number | undefined {
- return this.workerOptions.elementsPerWorker
- }
-
- /** @inheritDoc */
- public async start (): Promise<void> {
- this.addWorkerSetElement()
- // Add worker set element sequentially to optimize memory at startup
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- this.workerOptions.workerStartDelay! > 0 &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!)))
- this.emitter?.emit(WorkerSetEvents.started, this.info)
- this.started = true
- }
-
- /** @inheritDoc */
- public async stop (): Promise<void> {
- for (const workerSetElement of this.workerSet) {
- const worker = workerSetElement.worker
- const waitWorkerExit = new Promise<void>(resolve => {
- worker.once('exit', () => {
- resolve()
- })
- })
- worker.unref()
- await worker.terminate()
- await waitWorkerExit
- }
- this.emitter?.emit(WorkerSetEvents.stopped, this.info)
- this.started = false
- this.emitter?.emitDestroy()
- }
-
- /** @inheritDoc */
- public async addElement (elementData: D): Promise<R> {
- if (!this.started) {
- throw new Error('Cannot add a WorkerSet element: not started')
- }
- const workerSetElement = await this.getWorkerSetElement()
- const sendMessageToWorker = new Promise<R>((resolve, reject) => {
- const message = {
- uuid: randomUUID(),
- event: WorkerMessageEvents.addWorkerElement,
- data: elementData,
- } satisfies WorkerMessage<D>
- workerSetElement.worker.postMessage(message)
- this.promiseResponseMap.set(message.uuid, {
- resolve,
- reject,
- workerSetElement,
- })
- })
- const response = await sendMessageToWorker
- // Add element sequentially to optimize memory at startup
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (this.workerOptions.elementAddDelay! > 0) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- await sleep(randomizeDelay(this.workerOptions.elementAddDelay!))
- }
- return response
- }
-
/**
* Adds a new `WorkerSetElement`.
* @returns The new `WorkerSetElement`.
})
worker.on('message', this.workerOptions.poolOptions?.messageHandler ?? EMPTY_FUNCTION)
worker.on('message', (message: WorkerMessage<R>) => {
- const { uuid, event, data } = message
+ const { data, event, uuid } = message
if (this.promiseResponseMap.has(uuid)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const { resolve, reject, workerSetElement } = this.promiseResponseMap.get(uuid)!
+ const { reject, resolve, workerSetElement } = this.promiseResponseMap.get(uuid)!
switch (event) {
case WorkerMessageEvents.addedWorkerElement:
this.emitter?.emit(WorkerSetEvents.elementAdded, this.info)
this.removeWorkerSetElement(this.getWorkerSetElementByWorker(worker))
})
const workerSetElement: WorkerSetElement = {
- worker,
numberOfWorkerElements: 0,
+ worker,
}
this.workerSet.add(workerSetElement)
this.workerStartup = false
return workerSetElement
}
- private removeWorkerSetElement (workerSetElement: WorkerSetElement | undefined): void {
- if (workerSetElement == null) {
- return
- }
- this.workerSet.delete(workerSetElement)
- }
-
private async getWorkerSetElement (): Promise<WorkerSetElement> {
- let chosenWorkerSetElement: WorkerSetElement | undefined
+ let chosenWorkerSetElement: undefined | WorkerSetElement
for (const workerSetElement of this.workerSet) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (workerSetElement.numberOfWorkerElements < this.workerOptions.elementsPerWorker!) {
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
}
return workerSetElt
}
+
+ private removeWorkerSetElement (workerSetElement: undefined | WorkerSetElement): void {
+ if (workerSetElement == null) {
+ return
+ }
+ this.workerSet.delete(workerSetElement)
+ }
+
+ /** @inheritDoc */
+ public async addElement (elementData: D): Promise<R> {
+ if (!this.started) {
+ throw new Error('Cannot add a WorkerSet element: not started')
+ }
+ const workerSetElement = await this.getWorkerSetElement()
+ const sendMessageToWorker = new Promise<R>((resolve, reject) => {
+ const message = {
+ data: elementData,
+ event: WorkerMessageEvents.addWorkerElement,
+ uuid: randomUUID(),
+ } satisfies WorkerMessage<D>
+ workerSetElement.worker.postMessage(message)
+ this.promiseResponseMap.set(message.uuid, {
+ reject,
+ resolve,
+ workerSetElement,
+ })
+ })
+ const response = await sendMessageToWorker
+ // Add element sequentially to optimize memory at startup
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ if (this.workerOptions.elementAddDelay! > 0) {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ await sleep(randomizeDelay(this.workerOptions.elementAddDelay!))
+ }
+ return response
+ }
+
+ /** @inheritDoc */
+ public async start (): Promise<void> {
+ this.addWorkerSetElement()
+ // Add worker set element sequentially to optimize memory at startup
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ this.workerOptions.workerStartDelay! > 0 &&
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ (await sleep(randomizeDelay(this.workerOptions.workerStartDelay!)))
+ this.emitter?.emit(WorkerSetEvents.started, this.info)
+ this.started = true
+ }
+
+ /** @inheritDoc */
+ public async stop (): Promise<void> {
+ for (const workerSetElement of this.workerSet) {
+ const worker = workerSetElement.worker
+ const waitWorkerExit = new Promise<void>(resolve => {
+ worker.once('exit', () => {
+ resolve()
+ })
+ })
+ worker.unref()
+ await worker.terminate()
+ await waitWorkerExit
+ }
+ this.emitter?.emit(WorkerSetEvents.stopped, this.info)
+ this.started = false
+ this.emitter?.emitDestroy()
+ }
+
+ get info (): SetInfo {
+ return {
+ elementsExecuting: [...this.workerSet].reduce(
+ (accumulator, workerSetElement) => accumulator + workerSetElement.numberOfWorkerElements,
+ 0
+ ),
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ elementsPerWorker: this.maxElementsPerWorker!,
+ size: this.size,
+ started: this.started,
+ type: 'set',
+ version: workerSetVersion,
+ worker: 'thread',
+ }
+ }
+
+ get maxElementsPerWorker (): number | undefined {
+ return this.workerOptions.elementsPerWorker
+ }
+
+ get size (): number {
+ return this.workerSet.size
+ }
}
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 = {
export type WorkerEvents = PoolEvent | WorkerSetEvents
export interface WorkerOptions {
- workerStartDelay?: number
elementAddDelay?: number
+ elementsPerWorker?: number
poolMaxSize: number
poolMinSize: number
- elementsPerWorker?: number
poolOptions?: ThreadPoolOptions
+ workerStartDelay?: number
}
export type WorkerData = Record<string, unknown>
export interface WorkerDataError extends WorkerData {
event: WorkerMessageEvents
- name: string
message: string
+ name: string
stack?: string
}
export interface WorkerSetElement {
- worker: Worker
numberOfWorkerElements: number
+ worker: Worker
}
export interface WorkerMessage<T extends WorkerData> {
- uuid: `${string}-${string}-${string}-${string}`
- event: WorkerMessageEvents
data: T
+ event: WorkerMessageEvents
+ uuid: `${string}-${string}-${string}-${string}`
}
export enum WorkerMessageEvents {
- addWorkerElement = 'addWorkerElement',
addedWorkerElement = 'addedWorkerElement',
+ addWorkerElement = 'addWorkerElement',
workerElementError = 'workerElementError'
}
-import { getRandomValues } from 'node:crypto'
-
import chalk from 'chalk'
+import { getRandomValues } from 'node:crypto'
export const sleep = async (milliSeconds: number): Promise<NodeJS.Timeout> => {
return await new Promise<NodeJS.Timeout>(resolve =>
/* 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,
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,
baseName,
} as ChargingStationTemplate
const chargingStation = {
- started: false,
- logPrefix: () => `${baseName} |`,
- evses: new Map<number, EvseStatus>(),
connectors: new Map<number, ConnectorStatus>(),
+ evses: new Map<number, EvseStatus>(),
+ logPrefix: () => `${baseName} |`,
+ started: false,
} as ChargingStation
await it('Verify getChargingStationId()', () => {
-import { describe, it } from 'node:test'
-
import { expect } from 'expect'
+import { describe, it } from 'node:test'
import { BaseError } from '../../src/exception/BaseError.js'
-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'
-import { describe, it } from 'node:test'
-
import { expect } from 'expect'
+import { describe, it } from 'node:test'
import {
ApplicationProtocolVersion,
-import { describe, it } from 'node:test'
-
import { expect } from 'expect'
+import { describe, it } from 'node:test'
import { AsyncLock, AsyncLockType } from '../../src/utils/AsyncLock.js'
/* 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'
-import { describe, it } from 'node:test'
-
import { expect } from 'expect'
+import { describe, it } from 'node:test'
import { ACElectricUtils, DCElectricUtils } from '../../src/utils/ElectricUtils.js'
/* 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,
}).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)
}
expect(
handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error, {
- throwError: false,
errorResponse,
+ throwError: false,
})
).toStrictEqual(errorResponse)
expect(chargingStation.logPrefix.mock.calls.length).toBe(3)
-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'
-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,
expect(isAsyncFunction(async function named () {})).toBe(true)
class TestClass {
// eslint-disable-next-line @typescript-eslint/no-empty-function
- public testSync (): void {}
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- public async testAsync (): Promise<void> {}
+ public testArrowAsync = async (): Promise<void> => {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public testArrowSync = (): void => {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
- public testArrowAsync = async (): Promise<void> => {}
+ public static async testStaticAsync (): Promise<void> {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public static testStaticSync (): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
- public static async testStaticAsync (): Promise<void> {}
+ public async testAsync (): Promise<void> {}
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ public testSync (): void {}
}
const testClass = new TestClass()
// eslint-disable-next-line @typescript-eslint/unbound-method
"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",
<input
id="number-of-stations"
v-model="state.numberOfStations"
- type="number"
min="1"
name="number-of-stations"
placeholder="number of stations"
+ type="number"
>
<p>Template options overrides:</p>
<ul id="template-options">
<input
id="supervision-url"
v-model.trim="state.supervisionUrl"
- type="url"
name="supervision-url"
placeholder="wss://"
+ type="url"
>
</li>
<li>
Auto start:
<input
v-model="state.autoStart"
- type="checkbox"
- true-value="true"
false-value="false"
+ true-value="true"
+ type="checkbox"
>
</li>
<li>
Persistent configuration:
<input
v-model="state.persistentConfiguration"
- type="checkbox"
- true-value="true"
false-value="false"
+ true-value="true"
+ type="checkbox"
>
</li>
<li>
OCPP strict compliance:
<input
v-model="state.ocppStrictCompliance"
- type="checkbox"
- true-value="true"
false-value="false"
+ true-value="true"
+ type="checkbox"
>
</li>
<li>
Performance statistics:
<input
v-model="state.enableStatistics"
- type="checkbox"
- true-value="true"
false-value="false"
+ true-value="true"
+ type="checkbox"
>
</li>
</ul>
</template>
<script setup lang="ts">
-import { getCurrentInstance, ref, watch } from 'vue'
-
import Button from '@/components/buttons/Button.vue'
import { convertToBoolean, randomUUID } from '@/composables'
+import { getCurrentInstance, ref, watch } from 'vue'
const state = ref<{
- renderTemplates: `${string}-${string}-${string}-${string}-${string}`
- template: string
- numberOfStations: number
- supervisionUrl: string
autoStart: boolean
- persistentConfiguration: boolean
- ocppStrictCompliance: boolean
enableStatistics: boolean
+ numberOfStations: number
+ ocppStrictCompliance: boolean
+ persistentConfiguration: boolean
+ renderTemplates: `${string}-${string}-${string}-${string}-${string}`
+ supervisionUrl: string
+ template: string
}>({
- renderTemplates: randomUUID(),
- template: '',
- numberOfStations: 1,
- supervisionUrl: '',
autoStart: false,
- persistentConfiguration: true,
- ocppStrictCompliance: true,
enableStatistics: false,
+ numberOfStations: 1,
+ ocppStrictCompliance: true,
+ persistentConfiguration: true,
+ renderTemplates: randomUUID(),
+ supervisionUrl: '',
+ template: '',
})
watch(getCurrentInstance()!.appContext.config.globalProperties!.$templates, () => {
<input
id="supervision-url"
v-model.trim="state.supervisionUrl"
- type="url"
name="supervision-url"
placeholder="wss://"
+ type="url"
>
<br>
<Button
</template>
<script setup lang="ts">
-import { ref } from 'vue'
-
import Button from '@/components/buttons/Button.vue'
+import { ref } from 'vue'
defineProps<{
- hashId: string
chargingStationId: string
+ hashId: string
}>()
const state = ref<{ supervisionUrl: string }>({
<input
id="idtag"
v-model.trim="state.idTag"
- type="text"
name="idtag"
placeholder="RFID tag"
+ type="text"
>
<br>
<Button
</template>
<script setup lang="ts">
-import { ref } from 'vue'
-
import Button from '@/components/buttons/Button.vue'
import { convertToInt } from '@/composables'
+import { ref } from 'vue'
defineProps<{
- hashId: string
chargingStationId: string
connectorId: string
+ hashId: string
}>()
const state = ref<{ idTag: string }>({
<template>
<button
- type="button"
class="button"
+ type="button"
>
<slot />
</button>
</template>
<script setup lang="ts">
-import { ref } from 'vue'
-
import Button from '@/components/buttons/Button.vue'
import { getFromLocalStorage, setToLocalStorage } from '@/composables'
+import { ref } from 'vue'
const props = defineProps<{
id: string
- status?: boolean
- shared?: boolean
- on?: () => void
off?: () => void
+ on?: () => void
+ shared?: boolean
+ status?: boolean
}>()
const $emit = defineEmits(['clicked'])
<td class="connectors-table__column">
<ToggleButton
:id="`${hashId}-${connectorId}-start-transaction`"
- :shared="true"
+ :off="
+ () => {
+ $router.push({ name: 'charging-stations' })
+ }
+ "
:on="
() => {
$router.push({
})
}
"
- :off="
- () => {
- $router.push({ name: 'charging-stations' })
- }
- "
+ :shared="true"
@clicked="
() => {
$emit('need-refresh')
</template>
<script setup lang="ts">
-import { useToast } from 'vue-toast-notification'
+import type { ConnectorStatus, Status } from '@/types'
import Button from '@/components/buttons/Button.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
import { useUIClient } from '@/composables'
-import type { ConnectorStatus, Status } from '@/types'
+import { useToast } from 'vue-toast-notification'
const props = defineProps<{
- hashId: string
+ atgStatus?: Status
chargingStationId: string
- connectorId: number
connector: ConnectorStatus
- atgStatus?: Status
+ connectorId: number
+ hashId: string
}>()
const $emit = defineEmits(['need-refresh'])
</Button>
<ToggleButton
:id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
- :shared="true"
+ :off="
+ () => {
+ $router.push({ name: 'charging-stations' })
+ }
+ "
:on="
() => {
$router.push({
})
}
"
- :off="
- () => {
- $router.push({ name: 'charging-stations' })
- }
- "
+ :shared="true"
@clicked="
() => {
$emit('need-refresh')
<thead id="connectors-table__head">
<tr class="connectors-table__row">
<th
- scope="col"
class="connectors-table__column"
+ scope="col"
>
Identifier
</th>
<th
- scope="col"
class="connectors-table__column"
+ scope="col"
>
Status
</th>
<th
- scope="col"
class="connectors-table__column"
+ scope="col"
>
Transaction
</th>
<th
- scope="col"
class="connectors-table__column"
+ scope="col"
>
ATG Started
</th>
<th
- scope="col"
class="connectors-table__column"
+ scope="col"
>
Actions
</th>
<CSConnector
v-for="(connector, index) in getConnectorStatuses()"
:key="index + 1"
- :hash-id="chargingStation.stationInfo.hashId"
+ :atg-status="getATGStatus(index + 1)"
:charging-station-id="chargingStation.stationInfo.chargingStationId"
- :connector-id="index + 1"
:connector="connector"
- :atg-status="getATGStatus(index + 1)"
+ :connector-id="index + 1"
+ :hash-id="chargingStation.stationInfo.hashId"
@need-refresh="$emit('need-refresh')"
/>
</tbody>
</template>
<script setup lang="ts">
-import { useToast } from 'vue-toast-notification'
+import type { ChargingStationData, ConnectorStatus, Status } from '@/types'
import Button from '@/components/buttons/Button.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
import CSConnector from '@/components/charging-stations/CSConnector.vue'
import { useUIClient } from '@/composables'
-import type { ChargingStationData, ConnectorStatus, Status } from '@/types'
+import { useToast } from 'vue-toast-notification'
const props = defineProps<{
chargingStation: ChargingStationData
<thead id="cs-table__head">
<tr class="cs-table__row">
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Name
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Started
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Supervision Url
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
WebSocket State
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Registration Status
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Template
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Vendor
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Model
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Firmware
</th>
<th
- scope="col"
class="cs-table__column"
+ scope="col"
>
Actions
</th>
<th
- scope="col"
class="cs-table__connectors-column"
+ scope="col"
>
Connector(s)
</th>
</template>
<script setup lang="ts">
-import CSData from '@/components/charging-stations/CSData.vue'
import type { ChargingStationData } from '@/types'
+import CSData from '@/components/charging-stations/CSData.vue'
+
defineProps<{
chargingStations: ChargingStationData[]
}>()
-import { useToast } from 'vue-toast-notification'
-
import {
ApplicationProtocol,
AuthenticationType,
ResponseStatus,
type UIServerConfigurationSection,
} from '@/types'
+import { useToast } from 'vue-toast-notification'
import { randomUUID, validateUUID } from './Utils'
interface ResponseHandler {
procedureName: ProcedureName
- resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
reject: (reason?: unknown) => void
+ resolve: (value: PromiseLike<ResponsePayload> | ResponsePayload) => void
}
export class UIClient {
- private static instance: UIClient | null = null
+ private static instance: null | UIClient = null
- private ws?: WebSocket
private responseHandlers: Map<
`${string}-${string}-${string}-${string}-${string}`,
ResponseHandler
>
+ private ws?: WebSocket
+
private constructor (private uiServerConfiguration: UIServerConfigurationSection) {
this.openWS()
this.responseHandlers = new Map<
return UIClient.instance
}
- public setConfiguration (uiServerConfiguration: UIServerConfigurationSection): void {
- if (this.ws?.readyState === WebSocket.OPEN) {
- this.ws.close()
- delete this.ws
- }
- this.uiServerConfiguration = uiServerConfiguration
- this.openWS()
- }
-
- public registerWSEventListener<K extends keyof WebSocketEventMap>(
- event: K,
- listener: (event: WebSocketEventMap[K]) => void,
- options?: boolean | AddEventListenerOptions
- ) {
- this.ws?.addEventListener(event, listener, options)
- }
-
- public unregisterWSEventListener<K extends keyof WebSocketEventMap>(
- event: K,
- listener: (event: WebSocketEventMap[K]) => void,
- options?: boolean | AddEventListenerOptions
- ) {
- this.ws?.removeEventListener(event, listener, options)
- }
-
- public async simulatorState (): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.SIMULATOR_STATE, {})
- }
-
- public async startSimulator (): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.START_SIMULATOR, {})
- }
-
- public async stopSimulator (): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
- }
-
- public async listTemplates (): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
- }
-
- public async listChargingStations (): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
- }
-
- public async addChargingStations (
- template: string,
- numberOfStations: number,
- options?: ChargingStationOptions
- ): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, {
- template,
- numberOfStations,
- options,
- })
- }
-
- public async deleteChargingStation (hashId: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, {
- hashIds: [hashId],
- })
- }
-
- public async setSupervisionUrl (hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
- hashIds: [hashId],
- url: supervisionUrl,
- })
- }
-
- public async startChargingStation (hashId: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.START_CHARGING_STATION, {
- hashIds: [hashId],
- })
- }
-
- public async stopChargingStation (hashId: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, {
- hashIds: [hashId],
- })
- }
-
- public async openConnection (hashId: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
- hashIds: [hashId],
- })
- }
-
- public async closeConnection (hashId: string): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
- hashIds: [hashId],
- })
- }
-
- public async startTransaction (
- hashId: string,
- connectorId: number,
- idTag: string | undefined
- ): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.START_TRANSACTION, {
- hashIds: [hashId],
- connectorId,
- idTag,
- })
- }
-
- public async stopTransaction (
- hashId: string,
- transactionId: number | undefined
- ): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
- hashIds: [hashId],
- transactionId,
- })
- }
-
- public async startAutomaticTransactionGenerator (
- hashId: string,
- connectorId: number
- ): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
- hashIds: [hashId],
- connectorIds: [connectorId],
- })
- }
-
- public async stopAutomaticTransactionGenerator (
- hashId: string,
- connectorId: number
- ): Promise<ResponsePayload> {
- return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
- hashIds: [hashId],
- connectorIds: [connectorId],
- })
- }
-
private openWS (): void {
const protocols =
this.uiServerConfiguration.authentication?.enabled === true &&
}
}
- private async sendRequest (
- procedureName: ProcedureName,
- payload: RequestPayload
- ): Promise<ResponsePayload> {
- return new Promise<ResponsePayload>((resolve, reject) => {
- if (this.ws?.readyState === WebSocket.OPEN) {
- const uuid = randomUUID()
- const msg = JSON.stringify([uuid, procedureName, payload])
- const sendTimeout = setTimeout(() => {
- this.responseHandlers.delete(uuid)
- reject(new Error(`Send request '${procedureName}' message: connection timeout`))
- }, 60000)
- try {
- this.ws.send(msg)
- this.responseHandlers.set(uuid, { procedureName, resolve, reject })
- } catch (error) {
- this.responseHandlers.delete(uuid)
- reject(
- new Error(
- `Send request '${procedureName}' message: error ${(error as Error).toString()}`
- )
- )
- } finally {
- clearTimeout(sendTimeout)
- }
- } else {
- reject(new Error(`Send request '${procedureName}' message: connection closed`))
- }
- })
- }
-
private responseHandler (messageEvent: MessageEvent<string>): void {
let response: ProtocolResponse
try {
if (this.responseHandlers.has(uuid)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
+ const { procedureName, reject, resolve } = this.responseHandlers.get(uuid)!
switch (responsePayload.status) {
case ResponseStatus.SUCCESS:
resolve(responsePayload)
throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
}
}
+
+ private async sendRequest (
+ procedureName: ProcedureName,
+ payload: RequestPayload
+ ): Promise<ResponsePayload> {
+ return new Promise<ResponsePayload>((resolve, reject) => {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ const uuid = randomUUID()
+ const msg = JSON.stringify([uuid, procedureName, payload])
+ const sendTimeout = setTimeout(() => {
+ this.responseHandlers.delete(uuid)
+ reject(new Error(`Send request '${procedureName}' message: connection timeout`))
+ }, 60000)
+ try {
+ this.ws.send(msg)
+ this.responseHandlers.set(uuid, { procedureName, reject, resolve })
+ } catch (error) {
+ this.responseHandlers.delete(uuid)
+ reject(
+ new Error(
+ `Send request '${procedureName}' message: error ${(error as Error).toString()}`
+ )
+ )
+ } finally {
+ clearTimeout(sendTimeout)
+ }
+ } else {
+ reject(new Error(`Send request '${procedureName}' message: connection closed`))
+ }
+ })
+ }
+
+ public async addChargingStations (
+ template: string,
+ numberOfStations: number,
+ options?: ChargingStationOptions
+ ): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, {
+ numberOfStations,
+ options,
+ template,
+ })
+ }
+
+ public async closeConnection (hashId: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
+ hashIds: [hashId],
+ })
+ }
+
+ public async deleteChargingStation (hashId: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, {
+ hashIds: [hashId],
+ })
+ }
+
+ public async listChargingStations (): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
+ }
+
+ public async listTemplates (): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
+ }
+
+ public async openConnection (hashId: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
+ hashIds: [hashId],
+ })
+ }
+
+ public registerWSEventListener<K extends keyof WebSocketEventMap>(
+ event: K,
+ listener: (event: WebSocketEventMap[K]) => void,
+ options?: AddEventListenerOptions | boolean
+ ) {
+ this.ws?.addEventListener(event, listener, options)
+ }
+
+ public setConfiguration (uiServerConfiguration: UIServerConfigurationSection): void {
+ if (this.ws?.readyState === WebSocket.OPEN) {
+ this.ws.close()
+ delete this.ws
+ }
+ this.uiServerConfiguration = uiServerConfiguration
+ this.openWS()
+ }
+
+ public async setSupervisionUrl (hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
+ hashIds: [hashId],
+ url: supervisionUrl,
+ })
+ }
+
+ public async simulatorState (): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.SIMULATOR_STATE, {})
+ }
+
+ public async startAutomaticTransactionGenerator (
+ hashId: string,
+ connectorId: number
+ ): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
+ connectorIds: [connectorId],
+ hashIds: [hashId],
+ })
+ }
+
+ public async startChargingStation (hashId: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.START_CHARGING_STATION, {
+ hashIds: [hashId],
+ })
+ }
+
+ public async startSimulator (): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.START_SIMULATOR, {})
+ }
+
+ public async startTransaction (
+ hashId: string,
+ connectorId: number,
+ idTag: string | undefined
+ ): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.START_TRANSACTION, {
+ connectorId,
+ hashIds: [hashId],
+ idTag,
+ })
+ }
+
+ public async stopAutomaticTransactionGenerator (
+ hashId: string,
+ connectorId: number
+ ): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
+ connectorIds: [connectorId],
+ hashIds: [hashId],
+ })
+ }
+
+ public async stopChargingStation (hashId: string): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, {
+ hashIds: [hashId],
+ })
+ }
+
+ public async stopSimulator (): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
+ }
+
+ public async stopTransaction (
+ hashId: string,
+ transactionId: number | undefined
+ ): Promise<ResponsePayload> {
+ return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
+ hashIds: [hashId],
+ transactionId,
+ })
+ }
+
+ public unregisterWSEventListener<K extends keyof WebSocketEventMap>(
+ event: K,
+ listener: (event: WebSocketEventMap[K]) => void,
+ options?: AddEventListenerOptions | boolean
+ ) {
+ this.ws?.removeEventListener(event, listener, options)
+ }
}
-import 'vue-toast-notification/dist/theme-bootstrap.css'
-
-import { type App as AppType, type Component, createApp, ref } from 'vue'
-import ToastPlugin from 'vue-toast-notification'
+import type { ChargingStationData, ConfigurationData, UIServerConfigurationSection } from '@/types'
import App from '@/App.vue'
import { getFromLocalStorage, setToLocalStorage, UIClient } from '@/composables'
import { router } from '@/router'
-import type { ChargingStationData, ConfigurationData, UIServerConfigurationSection } from '@/types'
+import { type App as AppType, type Component, createApp, ref } from 'vue'
+import ToastPlugin from 'vue-toast-notification'
+import 'vue-toast-notification/dist/theme-bootstrap.css'
const app = createApp(App as Component)
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
-import { createRouter, createWebHistory } from 'vue-router'
-
import AddChargingStations from '@/components/actions/AddChargingStations.vue'
import SetSupervisionUrl from '@/components/actions/SetSupervisionUrl.vue'
import StartTransaction from '@/components/actions/StartTransaction.vue'
import ChargingStationsView from '@/views/ChargingStationsView.vue'
import NotFoundView from '@/views/NotFoundView.vue'
+import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(),
routes: [
{
- path: '/',
- name: 'charging-stations',
components: {
default: ChargingStationsView,
},
+ name: 'charging-stations',
+ path: '/',
},
{
- path: '/add-charging-stations',
- name: 'add-charging-stations',
components: {
- default: ChargingStationsView,
action: AddChargingStations,
+ default: ChargingStationsView,
},
+ name: 'add-charging-stations',
+ path: '/add-charging-stations',
},
{
- path: '/set-supervision-url/:hashId/:chargingStationId',
- name: 'set-supervision-url',
components: {
- default: ChargingStationsView,
action: SetSupervisionUrl,
+ default: ChargingStationsView,
},
- props: { default: false, action: true },
+ name: 'set-supervision-url',
+ path: '/set-supervision-url/:hashId/:chargingStationId',
+ props: { action: true, default: false },
},
{
- path: '/start-transaction/:hashId/:chargingStationId/:connectorId',
- name: 'start-transaction',
components: {
- default: ChargingStationsView,
action: StartTransaction,
+ default: ChargingStationsView,
},
- props: { default: false, action: true },
+ name: 'start-transaction',
+ path: '/start-transaction/:hashId/:chargingStationId/:connectorId',
+ props: { action: true, default: false },
},
{
- name: 'not-found',
- path: '/:pathMatch(.*)*',
components: {
default: NotFoundView,
},
+ name: 'not-found',
+ path: '/:pathMatch(.*)*',
},
],
})
RouterView: (typeof import('vue-router'))['RouterView']
}
interface ComponentCustomProperties {
+ $chargingStations: import('vue').Ref<import('@/types').ChargingStationData[]> | undefined
$configuration: import('vue').Ref<import('@/types').ConfigurationData> | undefined
$templates: import('vue').Ref<string[]> | undefined
- $chargingStations: import('vue').Ref<import('@/types').ChargingStationData[]> | undefined
$uiClient: import('@/composables').UIClient | undefined
}
}
import type { JsonObject } from './JsonType'
export enum IdTagDistribution {
+ CONNECTOR_AFFINITY = 'connector-affinity',
RANDOM = 'random',
- ROUND_ROBIN = 'round-robin',
- CONNECTOR_AFFINITY = 'connector-affinity'
+ ROUND_ROBIN = 'round-robin'
}
export interface AutomaticTransactionGeneratorConfiguration extends JsonObject {
enable: boolean
- minDuration: number
+ idTagDistribution?: IdTagDistribution
+ maxDelayBetweenTwoTransactions: number
maxDuration: number
minDelayBetweenTwoTransactions: number
- maxDelayBetweenTwoTransactions: number
+ minDuration: number
probabilityOfStart: number
- stopAfterHours: number
- stopAbsoluteDuration: boolean
requireAuthorize?: boolean
- idTagDistribution?: IdTagDistribution
+ stopAbsoluteDuration: boolean
+ stopAfterHours: number
}
export interface ChargingStationAutomaticTransactionGeneratorConfiguration extends JsonObject {
}
export interface ChargingStationData extends JsonObject {
- started: boolean
- stationInfo: ChargingStationInfo
+ automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
+ bootNotificationResponse?: BootNotificationResponse
connectors: ConnectorStatus[]
evses: EvseStatus[]
ocppConfiguration: ChargingStationOcppConfiguration
+ started: boolean
+ stationInfo: ChargingStationInfo
supervisionUrl: string
wsState?:
+ | typeof WebSocket.CLOSED
+ | typeof WebSocket.CLOSING
| typeof WebSocket.CONNECTING
| typeof WebSocket.OPEN
- | typeof WebSocket.CLOSING
- | typeof WebSocket.CLOSED
- bootNotificationResponse?: BootNotificationResponse
- automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration
}
export enum OCPP16FirmwareStatus {
Downloading = 'Downloading',
Idle = 'Idle',
InstallationFailed = 'InstallationFailed',
- Installing = 'Installing',
- Installed = 'Installed'
+ Installed = 'Installed',
+ Installing = 'Installing'
}
export interface FirmwareUpgrade extends JsonObject {
+ failureStatus?: FirmwareStatus
+ reset?: boolean
versionUpgrade?: {
patternGroup?: number
step?: number
}
- reset?: boolean
- failureStatus?: FirmwareStatus
}
export const FirmwareStatus = {
export type FirmwareStatus = OCPP16FirmwareStatus
export interface ChargingStationOptions extends JsonObject {
- supervisionUrls?: string | string[]
- persistentConfiguration?: boolean
- autoStart?: boolean
autoRegister?: boolean
+ autoStart?: boolean
enableStatistics?: boolean
ocppStrictCompliance?: boolean
+ persistentConfiguration?: boolean
stopTransactionsOnStopped?: boolean
+ supervisionUrls?: string | string[]
}
export interface ChargingStationInfo extends JsonObject {
- hashId: string
- templateIndex: number
- templateName: string
- chargingStationId: string
- chargeBoxSerialNumber?: string
- chargePointSerialNumber?: string
- meterSerialNumber?: string
- maximumPower?: number // Always in Watt
- maximumAmperage?: number // Always in Ampere
- firmwareStatus?: FirmwareStatus
- templateHash?: string
- supervisionUrls?: string | string[]
- supervisionUrlOcppConfiguration?: boolean
- supervisionUrlOcppKey?: string
- supervisionUser?: string
- supervisionPassword?: string
- autoStart?: boolean
- ocppVersion?: OCPPVersion
- ocppProtocol?: OCPPProtocol
- ocppStrictCompliance?: boolean
- ocppPersistentConfiguration?: boolean
- stationInfoPersistentConfiguration?: boolean
+ amperageLimitationOcppKey?: string
+ amperageLimitationUnit?: AmpereUnits
automaticTransactionGeneratorPersistentConfiguration?: boolean
- idTagsFile?: string
+ autoReconnectMaxRetries?: number
+ autoRegister?: boolean
+ autoStart?: boolean
baseName: string
- nameSuffix?: string
- fixedName?: boolean
+ beginEndMeterValues?: boolean
+ chargeBoxSerialNumber?: string
chargePointModel: string
+ chargePointSerialNumber?: string
chargePointVendor: string
- firmwareVersionPattern?: string
- firmwareVersion?: string
+ chargingStationId: string
+ commandsSupport?: CommandsSupport
+ currentOutType?: CurrentType
+ customValueLimitationMeterValues?: boolean
+ enableStatistics?: boolean
+ firmwareStatus?: FirmwareStatus
firmwareUpgrade?: FirmwareUpgrade
+ firmwareVersion?: string
+ firmwareVersionPattern?: string
+ fixedName?: boolean
+ hashId: string
iccid?: string
+ idTagsFile?: string
imsi?: string
+ mainVoltageMeterValues?: boolean
+ maximumAmperage?: number // Always in Ampere
+ maximumPower?: number // Always in Watt
+ messageTriggerSupport?: Record<MessageTrigger, boolean>
+ meteringPerTransaction?: boolean
+ meterSerialNumber?: string
meterType?: string
- powerSharedByConnectors?: boolean
- currentOutType?: CurrentType
- voltageOut?: Voltage
+ nameSuffix?: string
numberOfPhases?: number
- useConnectorId0?: boolean
+ ocppPersistentConfiguration?: boolean
+ ocppProtocol?: OCPPProtocol
+ ocppStrictCompliance?: boolean
+ ocppVersion?: OCPPVersion
+ outOfOrderEndMeterValues?: boolean
+ phaseLineToLineVoltageMeterValues?: boolean
+ powerSharedByConnectors?: boolean
randomConnectors?: boolean
- resetTime?: number
- autoRegister?: boolean
- autoReconnectMaxRetries?: number
reconnectExponentialDelay?: boolean
registrationMaxRetries?: number
- enableStatistics?: boolean
remoteAuthorization?: boolean
- amperageLimitationOcppKey?: string
- amperageLimitationUnit?: AmpereUnits
- beginEndMeterValues?: boolean
- outOfOrderEndMeterValues?: boolean
- meteringPerTransaction?: boolean
- transactionDataMeterValues?: boolean
+ resetTime?: number
+ stationInfoPersistentConfiguration?: boolean
stopTransactionsOnStopped?: boolean
- mainVoltageMeterValues?: boolean
- phaseLineToLineVoltageMeterValues?: boolean
- customValueLimitationMeterValues?: boolean
- commandsSupport?: CommandsSupport
- messageTriggerSupport?: Record<MessageTrigger, boolean>
+ supervisionPassword?: string
+ supervisionUrlOcppConfiguration?: boolean
+ supervisionUrlOcppKey?: string
+ supervisionUrls?: string | string[]
+ supervisionUser?: string
+ templateHash?: string
+ templateIndex: number
+ templateName: string
+ transactionDataMeterValues?: boolean
+ useConnectorId0?: boolean
+ voltageOut?: Voltage
}
export interface ChargingStationOcppConfiguration extends JsonObject {
}
export interface ConfigurationKey extends OCPPConfigurationKey {
- visible?: boolean
reboot?: boolean
+ visible?: boolean
}
export interface OCPPConfigurationKey extends JsonObject {
}
export enum OCPP16IncomingRequestCommand {
- RESET = 'Reset',
- CLEAR_CACHE = 'ClearCache',
CHANGE_AVAILABILITY = 'ChangeAvailability',
- UNLOCK_CONNECTOR = 'UnlockConnector',
- GET_CONFIGURATION = 'GetConfiguration',
CHANGE_CONFIGURATION = 'ChangeConfiguration',
- SET_CHARGING_PROFILE = 'SetChargingProfile',
+ CLEAR_CACHE = 'ClearCache',
CLEAR_CHARGING_PROFILE = 'ClearChargingProfile',
+ GET_CONFIGURATION = 'GetConfiguration',
+ GET_DIAGNOSTICS = 'GetDiagnostics',
REMOTE_START_TRANSACTION = 'RemoteStartTransaction',
REMOTE_STOP_TRANSACTION = 'RemoteStopTransaction',
- GET_DIAGNOSTICS = 'GetDiagnostics',
- TRIGGER_MESSAGE = 'TriggerMessage'
+ RESET = 'Reset',
+ SET_CHARGING_PROFILE = 'SetChargingProfile',
+ TRIGGER_MESSAGE = 'TriggerMessage',
+ UNLOCK_CONNECTOR = 'UnlockConnector'
}
export const IncomingRequestCommand = {
export type IncomingRequestCommand = OCPP16IncomingRequestCommand
export enum OCPP16RequestCommand {
+ AUTHORIZE = 'Authorize',
BOOT_NOTIFICATION = 'BootNotification',
+ DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification',
HEARTBEAT = 'Heartbeat',
- STATUS_NOTIFICATION = 'StatusNotification',
- AUTHORIZE = 'Authorize',
- START_TRANSACTION = 'StartTransaction',
- STOP_TRANSACTION = 'StopTransaction',
METER_VALUES = 'MeterValues',
- DIAGNOSTICS_STATUS_NOTIFICATION = 'DiagnosticsStatusNotification'
+ START_TRANSACTION = 'StartTransaction',
+ STATUS_NOTIFICATION = 'StatusNotification',
+ STOP_TRANSACTION = 'StopTransaction'
}
export const RequestCommand = {
}
export interface OCPP16BootNotificationResponse extends JsonObject {
- status: OCPP16RegistrationStatus
currentTime: Date
interval: number
+ status: OCPP16RegistrationStatus
}
export enum OCPP16MessageTrigger {
}
export enum AmpereUnits {
- MILLI_AMPERE = 'mA',
+ AMPERE = 'A',
CENTI_AMPERE = 'cA',
DECI_AMPERE = 'dA',
- AMPERE = 'A'
+ MILLI_AMPERE = 'mA'
}
export interface ConnectorStatus extends JsonObject {
+ authorizeIdTag?: string
availability: AvailabilityType
bootStatus?: ChargePointStatus
- status?: ChargePointStatus
- authorizeIdTag?: string
+ energyActiveImportRegisterValue?: number // In Wh
idTagAuthorized?: boolean
- localAuthorizeIdTag?: string
idTagLocalAuthorized?: boolean
- transactionRemoteStarted?: boolean
- transactionStarted?: boolean
+ localAuthorizeIdTag?: string
+ status?: ChargePointStatus
+ transactionEnergyActiveImportRegisterValue?: number // In Wh
transactionId?: number
transactionIdTag?: string
- energyActiveImportRegisterValue?: number // In Wh
- transactionEnergyActiveImportRegisterValue?: number // In Wh
+ transactionRemoteStarted?: boolean
+ transactionStarted?: boolean
}
export interface EvseStatus extends JsonObject {
export enum OCPP16ChargePointStatus {
AVAILABLE = 'Available',
- PREPARING = 'Preparing',
CHARGING = 'Charging',
- OCCUPIED = 'Occupied',
- SUSPENDED_EVSE = 'SuspendedEVSE',
- SUSPENDED_EV = 'SuspendedEV',
+ FAULTED = 'Faulted',
FINISHING = 'Finishing',
+ OCCUPIED = 'Occupied',
+ PREPARING = 'Preparing',
RESERVED = 'Reserved',
- UNAVAILABLE = 'Unavailable',
- FAULTED = 'Faulted'
+ SUSPENDED_EV = 'SuspendedEV',
+ SUSPENDED_EVSE = 'SuspendedEVSE',
+ UNAVAILABLE = 'Unavailable'
}
export type ChargePointStatus = OCPP16ChargePointStatus
export interface Status extends JsonObject {
- start?: boolean
- startDate?: Date
- lastRunDate?: Date
- stopDate?: Date
- stoppedDate?: Date
- authorizeRequests?: number
acceptedAuthorizeRequests?: number
- rejectedAuthorizeRequests?: number
- startTransactionRequests?: number
acceptedStartTransactionRequests?: number
- rejectedStartTransactionRequests?: number
- stopTransactionRequests?: number
acceptedStopTransactionRequests?: number
+ authorizeRequests?: number
+ lastRunDate?: Date
+ rejectedAuthorizeRequests?: number
+ rejectedStartTransactionRequests?: number
rejectedStopTransactionRequests?: number
skippedConsecutiveTransactions?: number
skippedTransactions?: number
+ start?: boolean
+ startDate?: Date
+ startTransactionRequests?: number
+ stopDate?: Date
+ stoppedDate?: Date
+ stopTransactionRequests?: number
}
}
export interface UIServerConfigurationSection {
- name?: string
- host: string
- port: number
- secure?: boolean
- protocol: Protocol
- version: ProtocolVersion
authentication?: {
enabled: boolean
+ password?: string
type: AuthenticationType
username?: string
- password?: string
}
+ host: string
+ name?: string
+ port: number
+ protocol: Protocol
+ secure?: boolean
+ version: ProtocolVersion
}
-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[]
export type ProtocolRequestHandler = (
payload: RequestPayload
-) => ResponsePayload | Promise<ResponsePayload>
+) => Promise<ResponsePayload> | ResponsePayload
export enum ProcedureName {
- SIMULATOR_STATE = 'simulatorState',
- START_SIMULATOR = 'startSimulator',
- STOP_SIMULATOR = 'stopSimulator',
- LIST_TEMPLATES = 'listTemplates',
- LIST_CHARGING_STATIONS = 'listChargingStations',
ADD_CHARGING_STATIONS = 'addChargingStations',
+ CLOSE_CONNECTION = 'closeConnection',
DELETE_CHARGING_STATIONS = 'deleteChargingStations',
- SET_SUPERVISION_URL = 'setSupervisionUrl',
- START_CHARGING_STATION = 'startChargingStation',
- STOP_CHARGING_STATION = 'stopChargingStation',
+ LIST_CHARGING_STATIONS = 'listChargingStations',
+ LIST_TEMPLATES = 'listTemplates',
OPEN_CONNECTION = 'openConnection',
- CLOSE_CONNECTION = 'closeConnection',
+ SET_SUPERVISION_URL = 'setSupervisionUrl',
+ SIMULATOR_STATE = 'simulatorState',
START_AUTOMATIC_TRANSACTION_GENERATOR = 'startAutomaticTransactionGenerator',
- STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+ START_CHARGING_STATION = 'startChargingStation',
+ START_SIMULATOR = 'startSimulator',
START_TRANSACTION = 'startTransaction',
+ STOP_AUTOMATIC_TRANSACTION_GENERATOR = 'stopAutomaticTransactionGenerator',
+ STOP_CHARGING_STATION = 'stopChargingStation',
+ STOP_SIMULATOR = 'stopSimulator',
STOP_TRANSACTION = 'stopTransaction'
}
export interface RequestPayload extends JsonObject {
- hashIds?: string[]
connectorIds?: number[]
+ hashIds?: string[]
}
export enum ResponseStatus {
- SUCCESS = 'success',
- FAILURE = 'failure'
+ FAILURE = 'failure',
+ SUCCESS = 'success'
}
export interface ResponsePayload extends JsonObject {
- status: ResponseStatus
hashIds?: string[]
+ status: ResponseStatus
}
interface TemplateStatistics extends JsonObject {
- configured: number
added: number
- started: number
+ configured: number
indexes: number[]
+ started: number
}
export interface SimulatorState extends JsonObject {
- version: string
started: boolean
templateStatistics: Record<string, TemplateStatistics>
+ version: string
}
<ToggleButton
:id="'simulator'"
:key="state.renderSimulator"
- :status="simulatorState?.started"
- :on="() => startSimulator()"
- :off="() => stopSimulator()"
:class="simulatorButtonClass"
+ :off="() => stopSimulator()"
+ :on="() => startSimulator()"
+ :status="simulatorState?.started"
>
{{ simulatorButtonMessage }}
</ToggleButton>
<ToggleButton
:id="'add-charging-stations'"
:key="state.renderAddChargingStations"
- :shared="true"
- :on="
+ :off="
() => {
- $router.push({ name: 'add-charging-stations' })
+ $router.push({ name: 'charging-stations' })
}
"
- :off="
+ :on="
() => {
- $router.push({ name: 'charging-stations' })
+ $router.push({ name: 'add-charging-stations' })
}
"
+ :shared="true"
@clicked="
() => {
state.renderChargingStations = randomUUID()
</template>
<script setup lang="ts">
-import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
-import { useToast } from 'vue-toast-notification'
+import type {
+ ChargingStationData,
+ ResponsePayload,
+ SimulatorState,
+ UIServerConfigurationSection,
+} from '@/types'
import ReloadButton from '@/components/buttons/ReloadButton.vue'
import ToggleButton from '@/components/buttons/ToggleButton.vue'
setToLocalStorage,
useUIClient,
} from '@/composables'
-import type {
- ChargingStationData,
- ResponsePayload,
- SimulatorState,
- UIServerConfigurationSection,
-} from '@/types'
+import { computed, getCurrentInstance, onMounted, onUnmounted, ref, watch } from 'vue'
+import { useToast } from 'vue-toast-notification'
const simulatorState = ref<SimulatorState | undefined>(undefined)
)
const state = ref<{
- renderSimulator: `${string}-${string}-${string}-${string}-${string}`
- renderAddChargingStations: `${string}-${string}-${string}-${string}-${string}`
- renderChargingStations: `${string}-${string}-${string}-${string}-${string}`
+ gettingChargingStations: boolean
gettingSimulatorState: boolean
gettingTemplates: boolean
- gettingChargingStations: boolean
+ renderAddChargingStations: `${string}-${string}-${string}-${string}-${string}`
+ renderChargingStations: `${string}-${string}-${string}-${string}-${string}`
+ renderSimulator: `${string}-${string}-${string}-${string}-${string}`
uiServerIndex: number
}>({
- renderSimulator: randomUUID(),
- renderAddChargingStations: randomUUID(),
- renderChargingStations: randomUUID(),
+ gettingChargingStations: false,
gettingSimulatorState: false,
gettingTemplates: false,
- gettingChargingStations: false,
+ renderAddChargingStations: randomUUID(),
+ renderChargingStations: randomUUID(),
+ renderSimulator: randomUUID(),
uiServerIndex: getFromLocalStorage<number>('uiServerConfigurationIndex', 0),
})
})
const uiServerConfigurations: {
- index: number
configuration: UIServerConfigurationSection
+ index: number
}[] = (
app!.appContext.config.globalProperties.$configuration!.value
.uiServer as UIServerConfigurationSection[]
).map((configuration: UIServerConfigurationSection, index: number) => ({
- index,
configuration,
+ index,
}))
const startSimulator = (): void => {
+import finalhandler from 'finalhandler'
import { createServer } from 'node:http'
import { dirname, join } from 'node:path'
import { env } from 'node:process'
import { fileURLToPath } from 'node:url'
-
-import finalhandler from 'finalhandler'
import serveStatic from 'serve-static'
const isCFEnvironment = env.VCAP_APPLICATION != null
-import { shallowMount } from '@vue/test-utils'
-import { expect, test } from 'vitest'
+import type { ChargingStationData } from '@/types'
import CSTable from '@/components/charging-stations/CSTable.vue'
-import type { ChargingStationData } from '@/types'
+import { shallowMount } from '@vue/test-utils'
+import { expect, test } from 'vitest'
test('renders CS table columns name', () => {
const chargingStations: ChargingStationData[] = []
-import { fileURLToPath, URL } from 'node:url'
-
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
+import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
export default defineConfig({
import { fileURLToPath } from 'node:url'
-
import { mergeConfig } from 'vite'
import { configDefaults, defineConfig } from 'vitest/config'
viteConfig,
defineConfig({
test: {
- environment: 'jsdom',
- exclude: [...configDefaults.exclude, 'e2e/*'],
- root: fileURLToPath(new URL('./', import.meta.url)),
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
},
+ environment: 'jsdom',
+ exclude: [...configDefaults.exclude, 'e2e/*'],
+ root: fileURLToPath(new URL('./', import.meta.url)),
},
})
)