]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
fix: validate protocol inputs after dependency refresh (#2128)
authorrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Mon, 5 Oct 2026 22:38:52 +0000 (00:38 +0200)
committerGitHub <noreply@github.com>
Mon, 5 Oct 2026 22:38:52 +0000 (00:38 +0200)
* chore(deps): lock file maintenance

* fix(deps): preserve strict lint compatibility

* [autofix.ci] apply automated fixes

* fix(deps): leave changelog to release automation

* fix(deps): support typescript-eslint 8.71

* fix(ui-common): validate websocket ready state

* fix(ui): validate protocol enum inputs

* fix: validate protocol enum inputs

* [autofix.ci] apply automated fixes

* fix(types): harden dependency upgrade compatibility

* [autofix.ci] apply automated fixes

* fix: harden meter values and UI errors

* [autofix.ci] apply automated fixes

* refactor(ocpp): use meter value unit enums

* refactor(ocpp): reuse canonical enum values

* fix(ocpp): address dependency refresh regressions

* fix(ocpp16): reject unsupported unit descriptors

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jérôme Benoit <jerome.benoit@sap.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
77 files changed:
cspell.config.yaml
pnpm-lock.yaml
src/charging-station/ChargingStation.ts
src/charging-station/HelpersConfig.ts
src/charging-station/broadcast-channel/ChargingStationWorkerBroadcastChannel.ts
src/charging-station/meter-values/CoherentMeterValueBuilder.ts
src/charging-station/meter-values/MeterValueUtils.ts
src/charging-station/meter-values/TransactionIntervalUtils.ts
src/charging-station/meter-values/index.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestBuilders.ts
src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20CertificateManager.ts
src/charging-station/ocpp/2.0/OCPP20RequestBuilders.ts
src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts
src/charging-station/ocpp/2.0/OCPP20VariableManager.ts
src/charging-station/ocpp/OCPPConstants.ts
src/charging-station/ocpp/OCPPRequestService.ts
src/charging-station/ocpp/OCPPServiceUtils.ts
src/charging-station/ocpp/OCPPSignedMeterValueUtils.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIServerUtils.ts
src/exception/OCPPError.ts
src/performance/PerformanceStatistics.ts
src/types/MeasurandPerPhaseSampledValueTemplates.ts
src/types/Statistics.ts
src/types/index.ts
src/types/ocpp/1.6/MeterValues.ts
src/types/ocpp/ChargingProfile.ts
src/types/ocpp/MeterValues.ts
src/types/ocpp/Requests.ts
src/utils/EnumUtils.ts [new file with mode: 0644]
src/utils/ErrorUtils.ts
src/utils/index.ts
tests/charging-station/ChargingStation-Configuration.test.ts
tests/charging-station/ChargingStation-Lifecycle.test.ts
tests/charging-station/ChargingStation-Resilience.test.ts
tests/charging-station/Helpers.test.ts
tests/charging-station/TransactionIntervalUtils.test.ts
tests/charging-station/meter-values/CoherentMeterValues.test.ts
tests/charging-station/meter-values/StrategyDispatch.test.ts
tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-Reset.test.ts
tests/charging-station/ocpp/1.6/OCPP16IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/1.6/OCPP16Integration-CoherentMeterValues.test.ts
tests/charging-station/ocpp/1.6/OCPP16RequestBuilders.test.ts [new file with mode: 0644]
tests/charging-station/ocpp/1.6/OCPP16ServiceUtils-SignedMeterValues.test.ts
tests/charging-station/ocpp/1.6/OCPP16ServiceUtils.test.ts
tests/charging-station/ocpp/2.0/OCPP20IncomingRequestService-TriggerMessage.test.ts
tests/charging-station/ocpp/2.0/OCPP20Integration.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestBuilders-SignedMeterValues.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestBuilders.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-AlignedMeterValues.test.ts
tests/charging-station/ocpp/2.0/OCPP20ServiceUtils-TransactionEvent.test.ts
tests/charging-station/ocpp/OCPPIncomingRequestService-StationState.test.ts
tests/charging-station/ocpp/OCPPServiceUtils-MeterValues.test.ts
tests/charging-station/ocpp/OCPPSignedMeterValueUtils.test.ts
tests/charging-station/ocpp/auth/factories/AuthComponentFactory.test.ts
tests/charging-station/ocpp/auth/utils/AuthValidators.test.ts
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIWebSocketServer.test.ts
tests/charging-station/ui-server/ui-services/AbstractUIService.test.ts
tests/charging-station/ui-server/ui-services/UIServiceRequestPayloadSchemas.test.ts
tests/ocpp-server/poetry.lock
tests/types/ocpp/ChargingProfile.test.ts [new file with mode: 0644]
tests/types/ocpp/MeterValues.test.ts [new file with mode: 0644]
tests/utils/Configuration.test.ts
tests/utils/ConfigurationUtils.test.ts
ui/cli/src/commands/ocpp.ts
ui/common/src/client/adapter.ts
ui/common/src/index.ts
ui/common/src/utils/enum.ts [new file with mode: 0644]
ui/web/package.json
ui/web/src/skins/classic/ClassicLayout.vue
ui/web/src/skins/classic/components/actions/StartTransaction.vue
ui/web/src/skins/classic/components/charging-stations/CSConnector.vue
ui/web/src/skins/classic/components/charging-stations/CSData.vue
ui/web/tests/unit/skins/classic/Actions.test.ts

index 706d623620335c76d294ccbfbb46cd21ce88cbbc..124140f9f3263c4c6dc8f599215fc15ada429bce 100644 (file)
@@ -2,6 +2,7 @@ version: '0.2'
 words:
   - DECI
   - CENTI
+  - Celcius
   - MILLI
   - MILLIWATT
   - Benoit
index 3af3c879ae6367d5ea0bc58caedec27f76fcdfe0..30855231b106696a01f97b144658a4d0e2a3d483 100644 (file)
@@ -168,16 +168,16 @@ importers:
     dependencies:
       '@mikro-orm/better-sqlite':
         specifier: ^6.6.14
-        version: 6.6.14(@mikro-orm/core@6.6.16)(mariadb@3.4.5)(supports-color@7.2.0)
+        version: 6.6.14(@mikro-orm/core@6.6.17)(mariadb@3.4.5)(supports-color@7.2.0)
       '@mikro-orm/core':
         specifier: ^6.6.16
-        version: 6.6.16
+        version: 6.6.17
       '@mikro-orm/mariadb':
         specifier: ^6.6.16
-        version: 6.6.16(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(supports-color@7.2.0)
+        version: 6.6.17(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(supports-color@7.2.0)
       '@mikro-orm/reflection':
         specifier: ^6.6.16
-        version: 6.6.16(@mikro-orm/core@6.6.16)
+        version: 6.6.17(@mikro-orm/core@6.6.17)
       '@modelcontextprotocol/sdk':
         specifier: ~1.31.0
         version: 1.31.0(supports-color@7.2.0)(zod@4.6.5)
@@ -232,22 +232,22 @@ importers:
     devDependencies:
       '@commitlint/cli':
         specifier: ^21.2.3
-        version: 21.2.3(@types/node@24.19.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)
+        version: 21.2.3(@types/node@24.19.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3)
       '@commitlint/config-conventional':
         specifier: ^21.2.3
         version: 21.2.3
       '@cspell/eslint-plugin':
         specifier: ^10.3.6
-        version: 10.3.6(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+        version: 10.3.6(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       '@eslint/js':
         specifier: ^10.0.1
-        version: 10.0.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+        version: 10.0.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       '@mikro-orm/cli':
         specifier: ^6.6.16
-        version: 6.6.16(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
+        version: 6.6.17(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
       '@types/node':
         specifier: ^24.19.0
-        version: 24.19.0
+        version: 24.19.1
       '@types/semver':
         specifier: ^7.8.0
         version: 7.8.0
@@ -268,16 +268,16 @@ importers:
         version: 2.1.1(esbuild@0.28.2)
       eslint:
         specifier: ^10.11.0
-        version: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+        version: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       eslint-plugin-jsdoc:
         specifier: ^65.0.1
-        version: 65.0.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+        version: 65.0.2(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       eslint-plugin-perfectionist:
         specifier: ^5.12.1
-        version: 5.12.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+        version: 5.12.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       eslint-plugin-vue:
         specifier: ^10.11.1
-        version: 10.11.1(@stylistic/eslint-plugin@5.10.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0)))(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0))
+        version: 10.11.1(@stylistic/eslint-plugin@5.10.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0)))(@typescript-eslint/parser@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0))
       husky:
         specifier: ^9.1.7
         version: 9.1.7
@@ -286,7 +286,7 @@ importers:
         version: 17.6.0
       neostandard:
         specifier: ^0.14.0-next.1
-        version: 0.14.0-next.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+        version: 0.14.0-next.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       prettier:
         specifier: ^3.9.9
         version: 3.9.9
@@ -301,7 +301,7 @@ importers:
         version: 0.35.12(supports-color@7.2.0)
       ts-node:
         specifier: ^10.9.2
-        version: 10.9.2(@types/node@24.19.0)(typescript@6.0.3)
+        version: 10.9.2(@types/node@24.19.1)(typescript@6.0.3)
       tsx:
         specifier: ^4.23.15
         version: 4.23.15
@@ -313,7 +313,7 @@ importers:
         version: 6.0.1
       vue-eslint-parser:
         specifier: ^10.4.1
-        version: 10.4.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
+        version: 10.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
     optionalDependencies:
       bufferutil:
         specifier: ^4.1.0
@@ -375,7 +375,7 @@ importers:
         version: 3.5.43(typescript@6.0.3)
       vue-router:
         specifier: ^5.3.1
-        version: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.10)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+        version: 5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
       vue-toast-notification:
         specifier: ^3.1.3
         version: 3.1.3(vue@3.5.43(typescript@6.0.3))
@@ -388,10 +388,10 @@ importers:
         version: 30.0.0
       '@vitejs/plugin-vue':
         specifier: ^6.0.9
-        version: 6.0.9(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+        version: 6.0.9(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
       '@vitejs/plugin-vue-jsx':
         specifier: ^5.1.6
-        version: 5.1.6(supports-color@7.2.0)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
+        version: 5.1.6(supports-color@7.2.0)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))
       '@vitest/coverage-v8':
         specifier: ^5.0.3
         version: 5.0.3(vitest@5.0.3)
@@ -406,13 +406,13 @@ importers:
         version: 30.1.1
       vite:
         specifier: ^8.3.1
-        version: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+        version: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
       vitest:
         specifier: ^5.0.3
-        version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.0)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
+        version: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
       vue-tsc:
-        specifier: ^3.3.11
-        version: 3.3.11(typescript@6.0.3)
+        specifier: ^3.3.12
+        version: 3.3.12(typescript@6.0.3)
 
 packages:
 
@@ -420,12 +420,12 @@ packages:
     resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==}
     engines: {node: '>=4'}
 
-  '@asamuzakjp/css-color@7.0.1':
-    resolution: {integrity: sha512-C9duntabagkBZ1LebM7FKmphR4Q1pBclLxVbZETQV0akkFjV0ooFxo8FlvAQyKj9F6l8rEnmidgcTlpyxFYizg==}
+  '@asamuzakjp/css-color@7.1.3':
+    resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==}
     engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
 
-  '@asamuzakjp/dom-selector@9.2.1':
-    resolution: {integrity: sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==}
+  '@asamuzakjp/dom-selector@9.2.4':
+    resolution: {integrity: sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==}
     engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
 
   '@babel/code-frame@7.29.7':
@@ -695,8 +695,8 @@ packages:
   '@cspell/dict-companies@3.2.13':
     resolution: {integrity: sha512-mBJeYzCkiiWgNQxKg7zyQOpFPwcXO4IAzXGbOEFfcQ4Q0G5Fa7Kqd8z+gvnvSK7NPWKJaFmiApC6I0cka02qmQ==}
 
-  '@cspell/dict-cpp@7.1.2':
-    resolution: {integrity: sha512-bkR33oE+ToyD1hwSV4XeEUB9oDkizHRX03CrQfj23tX4VUVbie5vkEHzlKVhcw/Q+fO2dtV/Vz4f6O5M4a2iZQ==}
+  '@cspell/dict-cpp@7.1.3':
+    resolution: {integrity: sha512-2d2MSh+C+xzzdqjLwtt40TdXWDR/pK0q1Q9L5zC+1EPSCIZxlN7CHbhlXWpl0JqUSdy2xaWaWucwnoGznMTneA==}
 
   '@cspell/dict-cryptocurrencies@5.0.5':
     resolution: {integrity: sha512-R68hYYF/rtlE6T/dsObStzN5QZw+0aQBinAXuWCVqwdS7YZo0X33vGMfChkHaiCo3Z2+bkegqHlqxZF4TD3rUA==}
@@ -725,8 +725,8 @@ packages:
   '@cspell/dict-elixir@4.0.8':
     resolution: {integrity: sha512-CyfphrbMyl4Ms55Vzuj+mNmd693HjBFr9hvU+B2YbFEZprE5AG+EXLYTMRWrXbpds4AuZcvN3deM2XVB80BN/Q==}
 
-  '@cspell/dict-en-common-misspellings@2.2.2':
-    resolution: {integrity: sha512-gu0zS7nm0jgVA2EswgWOR8od8wbNYVgcGzo0pGaDqcSkKS4WhABehLOwbyI0NIEVB+ME6SmckLVGk7g+VD1C6A==}
+  '@cspell/dict-en-common-misspellings@2.2.3':
+    resolution: {integrity: sha512-rwskWjznsDSZe3ck03BGUNT3UY9MjO1e4E9LJWACFzdDjkdM7uzro2/WfKvZTBVWVnRlzdRXPrtjkUZFI2A66A==}
 
   '@cspell/dict-en-gb-mit@3.1.29':
     resolution: {integrity: sha512-mInExsCFZui0f9Vg/MToWfQZzwMfPgHX8BCYdZNdXWbYz+jITXcnvssbdUkui/DVbriZVJ74Zu9bv2jOEqTCNA==}
@@ -749,8 +749,8 @@ packages:
   '@cspell/dict-fullstack@3.2.9':
     resolution: {integrity: sha512-diZX+usW5aZ4/b2T0QM/H/Wl9aNMbdODa1Jq0ReBr/jazmNeWjd+PyqeVgzd1joEaHY+SAnjrf/i9CwKd2ZtWQ==}
 
-  '@cspell/dict-gaming-terms@1.1.2':
-    resolution: {integrity: sha512-9XnOvaoTBscq0xuD6KTEIkk9hhdfBkkvJAIsvw3JMcnp1214OCGW8+kako5RqQ2vTZR3Tnf3pc57o7VgkM0q1Q==}
+  '@cspell/dict-gaming-terms@1.1.3':
+    resolution: {integrity: sha512-hLGzd/f+KX5B/5YxAXuQi4DYEfu6IKudtNFO/Nima3JLXD+8Xn+d0q6VdaBxkWVhGYlzow9o+dce+S9mNtgpMg==}
 
   '@cspell/dict-git@3.1.1':
     resolution: {integrity: sha512-XvuGVYj9YEu4EzntqyhH8XlTRyy57C6j8oxkIbUQfR2UYzxZGAP77XHl7qY7eC/40r/haDwnBaccyFR6BRmUCg==}
@@ -808,8 +808,8 @@ packages:
   '@cspell/dict-node@5.0.9':
     resolution: {integrity: sha512-hO+ga+uYZ/WA4OtiMEyKt5rDUlUyu3nXMf8KVEeqq2msYvAPdldKBGH7lGONg6R/rPhv53Rb+0Y1SLdoK1+7wQ==}
 
-  '@cspell/dict-npm@5.2.50':
-    resolution: {integrity: sha512-JZhJb0wrWpqnzDBM1VG8f6zZPRCOo6YMxct0hFhgBnbOTIHQiqZf9S1BSXCRMmmQlAWqFXjKNDp+rf7ZiJcO9w==}
+  '@cspell/dict-npm@5.2.52':
+    resolution: {integrity: sha512-JttzSladJ4KVhfg9QiBM4os+wAKbet2F58wpbM5VJLnL8fAj3xwvBvSxdY64yrW6Y5wdSPHoTCLqXOKoyBJFnA==}
 
   '@cspell/dict-php@4.1.3':
     resolution: {integrity: sha512-41oKzl7rLVrrVZA/Gs0ESnP6KliEX7c+IwaaNmOL3PAoHasFDVEmIFeENIzuDdM12UcHT+MlLkAm3Qw/lS/ERA==}
@@ -817,8 +817,8 @@ packages:
   '@cspell/dict-powershell@5.0.17':
     resolution: {integrity: sha512-khOoIdBgKRk2scnr7888pf70yJ1Tr3RGdnYcAN0MJ2vtaNRKoOTCFc13gXUkNDHwEyhMI1h9oz3KsT180F2amw==}
 
-  '@cspell/dict-public-licenses@2.0.16':
-    resolution: {integrity: sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==}
+  '@cspell/dict-public-licenses@2.0.17':
+    resolution: {integrity: sha512-Asdumgg3oZ4ki//b2a+beiVIsNZA5thim4t4v3lTDLY/6a8VAPmTjzC3Y2t0x7ywC/bzAhi1KW/fg8JlwHDgig==}
 
   '@cspell/dict-python@4.5.2':
     resolution: {integrity: sha512-CFcTDGN/ycn45WXC5ZZ06pG09WbGKMpPm1rkMuqMP98/jwjw3iiK8c0izWRd8x027n1rOZ9kJ8f9Sd+bQ22HJg==}
@@ -838,8 +838,8 @@ packages:
   '@cspell/dict-shell@1.2.0':
     resolution: {integrity: sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==}
 
-  '@cspell/dict-software-terms@5.4.5':
-    resolution: {integrity: sha512-5PgLJNdaFKeA6iklB0XXjmgNaoLJJqf54oRTD1raG/mgwojVlRnWE1G7x4QH9Gz07h3B3+lMDv1F57Xxs4jang==}
+  '@cspell/dict-software-terms@5.4.6':
+    resolution: {integrity: sha512-r4Xw367hTosVCAQ884nPWhwcy3Jc6YWQFkW1XBdxFGdzdCltzuNf9qdJL1AwiTWMbiRnifIqKaXN0A38WCVEbQ==}
 
   '@cspell/dict-sql@2.2.1':
     resolution: {integrity: sha512-qDHF8MpAYCf4pWU8NKbnVGzkoxMNrFqBHyG/dgrlic5EQiKANCLELYtGlX5auIMDLmTf1inA0eNtv74tyRJ/vg==}
@@ -892,40 +892,40 @@ packages:
     resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
     engines: {node: '>=12'}
 
-  '@csstools/color-helpers@6.1.1':
-    resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==}
+  '@csstools/color-helpers@6.1.2':
+    resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==}
     engines: {node: '>=20.19.0'}
 
-  '@csstools/css-calc@3.4.0':
-    resolution: {integrity: sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==}
+  '@csstools/css-calc@3.4.3':
+    resolution: {integrity: sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==}
     engines: {node: '>=20.19.0'}
     peerDependencies:
-      '@csstools/css-parser-algorithms': ^4.0.0
-      '@csstools/css-tokenizer': ^4.0.0
+      '@csstools/css-parser-algorithms': ^4.0.2
+      '@csstools/css-tokenizer': ^4.0.2
 
-  '@csstools/css-color-parser@4.2.3':
-    resolution: {integrity: sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==}
+  '@csstools/css-color-parser@4.2.6':
+    resolution: {integrity: sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==}
     engines: {node: '>=20.19.0'}
     peerDependencies:
-      '@csstools/css-parser-algorithms': ^4.0.0
-      '@csstools/css-tokenizer': ^4.0.0
+      '@csstools/css-parser-algorithms': ^4.0.2
+      '@csstools/css-tokenizer': ^4.0.2
 
-  '@csstools/css-parser-algorithms@4.0.0':
-    resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
+  '@csstools/css-parser-algorithms@4.0.2':
+    resolution: {integrity: sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==}
     engines: {node: '>=20.19.0'}
     peerDependencies:
-      '@csstools/css-tokenizer': ^4.0.0
+      '@csstools/css-tokenizer': ^4.0.2
 
-  '@csstools/css-syntax-patches-for-csstree@1.1.14':
-    resolution: {integrity: sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==}
+  '@csstools/css-syntax-patches-for-csstree@1.1.15':
+    resolution: {integrity: sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==}
     peerDependencies:
       css-tree: ^3.2.1
     peerDependenciesMeta:
       css-tree:
         optional: true
 
-  '@csstools/css-tokenizer@4.0.1':
-    resolution: {integrity: sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==}
+  '@csstools/css-tokenizer@4.0.2':
+    resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==}
     engines: {node: '>=20.19.0'}
 
   '@dabh/diagnostics@2.0.9':
@@ -1146,8 +1146,8 @@ packages:
       '@noble/hashes':
         optional: true
 
-  '@hono/node-server@2.1.1':
-    resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==}
+  '@hono/node-server@2.1.3':
+    resolution: {integrity: sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==}
     engines: {node: '>=20'}
     peerDependencies:
       hono: ^4
@@ -1216,13 +1216,13 @@ packages:
     peerDependencies:
       '@mikro-orm/core': ^6.0.0
 
-  '@mikro-orm/cli@6.6.16':
-    resolution: {integrity: sha512-EsAr7A4wbmBib3LwUcnH1cJOYgGC8irBptt+0BiC7IaP+p2UsnQBvTBbM895rAk1rXiInFhqWKjBgSKZcDf4yQ==}
+  '@mikro-orm/cli@6.6.17':
+    resolution: {integrity: sha512-cQ9HXCVe1L+MXvlg3jNPVGkIgA4ngsH48GoGj3SfxJdrdB7W5JOLuu8dGQrc6HDUvuff78lkBpIy/XROm8t/fg==}
     engines: {node: '>= 18.12.0'}
     hasBin: true
 
-  '@mikro-orm/core@6.6.16':
-    resolution: {integrity: sha512-ym26fbhTti7Zh6oN1y74byoUoMEq1ahckiV7BZX5pHU/0OaUESiQA+Zar2Tv2ZFuZA9946XQ805F7/4NH6oDUQ==}
+  '@mikro-orm/core@6.6.17':
+    resolution: {integrity: sha512-LFl6s0U6evvhB2iqifGEAzY0Nl1DC4Ge2N6GcPoNEvtg3OC/0GjvIftyzXnpaANjo/IxEaY/ge5BgMLt1B729g==}
     engines: {node: '>= 18.12.0'}
 
   '@mikro-orm/knex@6.6.14':
@@ -1241,8 +1241,8 @@ packages:
       mariadb:
         optional: true
 
-  '@mikro-orm/knex@6.6.16':
-    resolution: {integrity: sha512-H0fYK4vSe2mtmfBDDNsOw5Z56JDYEzbaoIo24JhkzmoGna85ddwyYzYB289DMS8Ukn7qZKhfR2pn2kQDRSJiyw==}
+  '@mikro-orm/knex@6.6.17':
+    resolution: {integrity: sha512-v65plaUKRuPsmMBXl/0h6ejqhVvS0Np4bGJSxHGOuWPONNEQHvSlt1aIpddhSYtdnzCXKV2hQNVBEaj8/wkA2Q==}
     engines: {node: '>= 18.12.0'}
     peerDependencies:
       '@mikro-orm/core': ^6.0.0
@@ -1257,14 +1257,14 @@ packages:
       mariadb:
         optional: true
 
-  '@mikro-orm/mariadb@6.6.16':
-    resolution: {integrity: sha512-4BPpR6KYzMvByp9sJg5MlXrKxoDPrFDtnYku+swzXDgcyCNZwBJisRf0hkSXd3xL7JSV5yrsg+vZY+KjKO0ECQ==}
+  '@mikro-orm/mariadb@6.6.17':
+    resolution: {integrity: sha512-JcdyFFXyMIzHgv/bTtWWGFQph4X5AdeO9f7pLNn6yDkuF4cuzgUstmCHGbs7ZLzGY5tSgxQ2bB/GbgrJwPr3zA==}
     engines: {node: '>= 18.12.0'}
     peerDependencies:
       '@mikro-orm/core': ^6.0.0
 
-  '@mikro-orm/reflection@6.6.16':
-    resolution: {integrity: sha512-OXMhyWOag3Fyz+RyvptHKmwIPXE++5k7aFEt8JVK7YfEjqTvuB9Y7N3jQH08HQXe8VPsYs4RjAKz7DEk5E6JAA==}
+  '@mikro-orm/reflection@6.6.17':
+    resolution: {integrity: sha512-ep79EneWXx7tctmI5amZYgUT4eQv+S2/+ZGlgHU2KBP5lARNAkE1D4EaTD/hzwbt1kNDQud0TcGEgUDoaiawoA==}
     engines: {node: '>= 18.12.0'}
     peerDependencies:
       '@mikro-orm/core': ^6.0.0
@@ -1279,8 +1279,8 @@ packages:
       '@cfworker/json-schema':
         optional: true
 
-  '@mongodb-js/saslprep@1.5.4':
-    resolution: {integrity: sha512-05UC0jQsjKAOuXQ0H9Ud9vUTJpZIg+n/FinpR30tI5I8pY2inTfPOZ5OF/cg3Ce/N9MoD1xhRCeOsJtuTbFYlw==}
+  '@mongodb-js/saslprep@1.5.5':
+    resolution: {integrity: sha512-0PV4mvjGd2Q2/viDvXuoSNzTjQngHvtuWUry1vqvcK4Q/CTeO1zEKMf9set01DMocJnogl/pyNAdvyk+Pe0kQA==}
 
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -1301,8 +1301,8 @@ packages:
     resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
     engines: {node: '>=8.0.0'}
 
-  '@oxc-project/types@0.151.0':
-    resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==}
+  '@oxc-project/types@0.152.0':
+    resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
 
   '@parcel/watcher-android-arm64@2.6.0':
     resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==}
@@ -1396,98 +1396,98 @@ packages:
   '@polka/url@1.0.0-next.29':
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
 
-  '@rolldown/binding-android-arm-eabi@1.2.10':
-    resolution: {integrity: sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==}
+  '@rolldown/binding-android-arm-eabi@1.2.12':
+    resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm]
     os: [android]
 
-  '@rolldown/binding-android-arm64@1.2.10':
-    resolution: {integrity: sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==}
+  '@rolldown/binding-android-arm64@1.2.12':
+    resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [android]
 
-  '@rolldown/binding-darwin-arm64@1.2.10':
-    resolution: {integrity: sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==}
+  '@rolldown/binding-darwin-arm64@1.2.12':
+    resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [darwin]
 
-  '@rolldown/binding-darwin-x64@1.2.10':
-    resolution: {integrity: sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==}
+  '@rolldown/binding-darwin-x64@1.2.12':
+    resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [x64]
     os: [darwin]
 
-  '@rolldown/binding-freebsd-x64@1.2.10':
-    resolution: {integrity: sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==}
+  '@rolldown/binding-freebsd-x64@1.2.12':
+    resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [x64]
     os: [freebsd]
 
-  '@rolldown/binding-linux-arm-gnueabihf@1.2.10':
-    resolution: {integrity: sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==}
+  '@rolldown/binding-linux-arm-gnueabihf@1.2.12':
+    resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm]
     os: [linux]
 
-  '@rolldown/binding-linux-arm64-gnu@1.2.10':
-    resolution: {integrity: sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==}
+  '@rolldown/binding-linux-arm64-gnu@1.2.12':
+    resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@rolldown/binding-linux-arm64-musl@1.2.10':
-    resolution: {integrity: sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==}
+  '@rolldown/binding-linux-arm64-musl@1.2.12':
+    resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@rolldown/binding-linux-ppc64-gnu@1.2.10':
-    resolution: {integrity: sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==}
+  '@rolldown/binding-linux-ppc64-gnu@1.2.12':
+    resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [ppc64]
     os: [linux]
     libc: [glibc]
 
-  '@rolldown/binding-linux-s390x-gnu@1.2.10':
-    resolution: {integrity: sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==}
+  '@rolldown/binding-linux-s390x-gnu@1.2.12':
+    resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [s390x]
     os: [linux]
     libc: [glibc]
 
-  '@rolldown/binding-linux-x64-gnu@1.2.10':
-    resolution: {integrity: sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==}
+  '@rolldown/binding-linux-x64-gnu@1.2.12':
+    resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@rolldown/binding-linux-x64-musl@1.2.10':
-    resolution: {integrity: sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==}
+  '@rolldown/binding-linux-x64-musl@1.2.12':
+    resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@rolldown/binding-openharmony-arm64@1.2.10':
-    resolution: {integrity: sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==}
+  '@rolldown/binding-openharmony-arm64@1.2.12':
+    resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [openharmony]
 
-  '@rolldown/binding-win32-arm64-msvc@1.2.10':
-    resolution: {integrity: sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==}
+  '@rolldown/binding-win32-arm64-msvc@1.2.12':
+    resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [arm64]
     os: [win32]
 
-  '@rolldown/binding-win32-x64-msvc@1.2.10':
-    resolution: {integrity: sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==}
+  '@rolldown/binding-win32-x64-msvc@1.2.12':
+    resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
     cpu: [x64]
     os: [win32]
@@ -1561,14 +1561,11 @@ packages:
   '@types/minimatch@3.0.5':
     resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
 
-  '@types/node@24.13.6':
-    resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==}
+  '@types/node@24.19.1':
+    resolution: {integrity: sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==}
 
-  '@types/node@24.19.0':
-    resolution: {integrity: sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew==}
-
-  '@types/node@26.6.2':
-    resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==}
+  '@types/node@26.6.4':
+    resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==}
 
   '@types/parse-json@4.0.2':
     resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1591,63 +1588,63 @@ packages:
   '@types/ws@8.18.2':
     resolution: {integrity: sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==}
 
-  '@typescript-eslint/eslint-plugin@8.70.1':
-    resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
+  '@typescript-eslint/eslint-plugin@8.71.0':
+    resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
-      '@typescript-eslint/parser': ^8.70.1
+      '@typescript-eslint/parser': ^8.71.0
       eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/parser@8.70.1':
-    resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==}
+  '@typescript-eslint/parser@8.71.0':
+    resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/project-service@8.70.1':
-    resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==}
+  '@typescript-eslint/project-service@8.71.0':
+    resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/scope-manager@8.70.1':
-    resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==}
+  '@typescript-eslint/scope-manager@8.71.0':
+    resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/tsconfig-utils@8.70.1':
-    resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==}
+  '@typescript-eslint/tsconfig-utils@8.71.0':
+    resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/type-utils@8.70.1':
-    resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==}
+  '@typescript-eslint/type-utils@8.71.0':
+    resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/types@8.70.1':
-    resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==}
+  '@typescript-eslint/types@8.71.0':
+    resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript-eslint/typescript-estree@8.70.1':
-    resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==}
+  '@typescript-eslint/typescript-estree@8.71.0':
+    resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/utils@8.70.1':
-    resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==}
+  '@typescript-eslint/utils@8.71.0':
+    resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
       typescript: '>=4.8.4 <6.1.0'
 
-  '@typescript-eslint/visitor-keys@8.70.1':
-    resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==}
+  '@typescript-eslint/visitor-keys@8.71.0':
+    resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@vitejs/plugin-vue-jsx@5.1.6':
@@ -1673,12 +1670,12 @@ packages:
       '@vitest/browser':
         optional: true
 
-  '@vitest/istanbul-lib-coverage@1.0.1':
-    resolution: {integrity: sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==}
+  '@vitest/istanbul-lib-coverage@1.0.2':
+    resolution: {integrity: sha512-9J/JMwOf9AoJhAywhrn7ScKTL38hsWQP/qPG60OtaAFcQ5OXPwKsxZFlbnuCKmZ61m8/lGgHYnFpdyQZUvG/iA==}
     engines: {node: '>=22'}
 
-  '@vitest/istanbul-lib-report@1.0.1':
-    resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==}
+  '@vitest/istanbul-lib-report@1.0.2':
+    resolution: {integrity: sha512-gUsfXZJbzPamoIY5TvHFiMMoXESBrUMo+xqaj+rYrWI69+EnvRlYBlP96ZnHPY4vX8kyUpgAnFUCg5wZG/HkDQ==}
     engines: {node: '>=22'}
 
   '@vitest/mocker@5.0.3':
@@ -1755,8 +1752,8 @@ packages:
   '@vue/devtools-shared@8.2.1':
     resolution: {integrity: sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==}
 
-  '@vue/language-core@3.3.11':
-    resolution: {integrity: sha512-QJmpliwAVpC/OxubIByPAhNzsQPRc8/gxlN2qnVzVfIMjMDz/9RnXRFoetjz5yEgXVXyp4LqhXq3V53PjmNzFw==}
+  '@vue/language-core@3.3.12':
+    resolution: {integrity: sha512-k7uIKR0n4CBidk233y7cac9osnDQ5WRidTl6nsrNvpZ3nf8cx9iJxRVsRzVsoPRHbowT/iKgGMTS9RGov3jeIA==}
 
   '@vue/reactivity@3.5.43':
     resolution: {integrity: sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==}
@@ -1841,8 +1838,8 @@ packages:
     resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
     engines: {node: '>=8'}
 
-  ansi-regex@6.3.0:
-    resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==}
+  ansi-regex@6.4.0:
+    resolution: {integrity: sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==}
     engines: {node: '>=12'}
 
   ansi-styles@4.3.0:
@@ -1917,8 +1914,8 @@ packages:
   base64-js@1.5.1:
     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
 
-  baseline-browser-mapping@2.11.25:
-    resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==}
+  baseline-browser-mapping@2.11.27:
+    resolution: {integrity: sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==}
     engines: {node: '>=6.0.0'}
     hasBin: true
 
@@ -1969,8 +1966,8 @@ packages:
     resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
     engines: {node: '>=8'}
 
-  browserslist@4.29.0:
-    resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==}
+  browserslist@4.29.3:
+    resolution: {integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
@@ -2011,11 +2008,11 @@ packages:
     resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
     engines: {node: '>=10'}
 
-  caniuse-lite@1.0.30001810:
-    resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
+  caniuse-lite@1.0.30001814:
+    resolution: {integrity: sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==}
 
-  chai@6.2.2:
-    resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+  chai@6.3.0:
+    resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==}
     engines: {node: '>=18'}
 
   chalk@4.1.2:
@@ -2377,8 +2374,8 @@ packages:
   effect@3.22.2:
     resolution: {integrity: sha512-fm1CQXCs7GTMwpadZxwU5rnAb2bN6mbQpknq/pXBGCzSKkosNYb9k0wsoL0TfB10rSa5xOEZJjo77u/KLXSANg==}
 
-  electron-to-chromium@1.5.438:
-    resolution: {integrity: sha512-AN9xMU1hJiT65LkCUPL1DZm5TCbOPb2Qsm5pYwFLEXp6/qj9SdT4yMiqs7Qqstwvkn1zF7l36SRGt+s6XcJ0FA==}
+  electron-to-chromium@1.5.444:
+    resolution: {integrity: sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==}
 
   emoji-regex@10.6.0:
     resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@@ -2396,8 +2393,8 @@ packages:
   end-of-stream@1.4.5:
     resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
 
-  enhanced-resolve@5.25.1:
-    resolution: {integrity: sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==}
+  enhanced-resolve@5.26.0:
+    resolution: {integrity: sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==}
     engines: {node: '>=10.13.0'}
 
   entities@7.0.1:
@@ -2479,8 +2476,8 @@ packages:
     peerDependencies:
       eslint: '>=8'
 
-  eslint-plugin-jsdoc@65.0.1:
-    resolution: {integrity: sha512-7IvB+ZS71WCw5tGKh6f6D5k55qtXKdrGTnIpQTBKWdpyZ5IfhGHfh0i6ArWlCJ4JEtz22YRk5Bqu1MvbOlpMKQ==}
+  eslint-plugin-jsdoc@65.0.2:
+    resolution: {integrity: sha512-zy1VuN7NNUmfXk15B5ZDAGTXBiye+JskSJZ22Qd5E3VlUluX/RHf8TXYTj8yradaKKJt+69aylhJS22TX/aeRA==}
     engines: {node: ^22.22.2 || >=24.15.0}
     peerDependencies:
       eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0
@@ -2489,8 +2486,8 @@ packages:
       typescript:
         optional: true
 
-  eslint-plugin-n@18.3.0:
-    resolution: {integrity: sha512-cPVguuDe6DrIPb/qUXHf8P89MaVTUmiYWwpt5gX5AILsvRIiZAxMFXcFR6QHYBksqKJpjfUBlL/RleCJUWcD7w==}
+  eslint-plugin-n@18.4.1:
+    resolution: {integrity: sha512-2sBReWOvCbzgkPri9M5SFmD5EbIwZLa1xz6B5RZsrI19WSFKpyx1sOMr+pdk8puCiXuEgCbErEHy1PuaUrc/4w==}
     engines: {node: ^20.19.0 || ^22.13.0 || >=24}
     peerDependencies:
       eslint: '>=8.57.1'
@@ -2547,8 +2544,8 @@ packages:
     resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
     engines: {node: ^20.19.0 || ^22.13.0 || >=24}
 
-  eslint@10.11.0:
-    resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==}
+  eslint@10.12.0:
+    resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==}
     engines: {node: ^20.19.0 || ^22.13.0 || >=24}
     hasBin: true
     peerDependencies:
@@ -2630,8 +2627,8 @@ packages:
     resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
     engines: {node: '>= 18'}
 
-  exsolve@1.1.1:
-    resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==}
+  exsolve@1.1.3:
+    resolution: {integrity: sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA==}
 
   fast-check@3.23.2:
     resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
@@ -2640,8 +2637,8 @@ packages:
   fast-deep-equal@3.1.3:
     resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
 
-  fast-equals@6.0.4:
-    resolution: {integrity: sha512-JiBeR0k76FIahi9zAtQR7X1jgZDKLE+fs5S9ryjOGYZWZCOcm8QiwgeahkFvaboS3i6PkGBopRznHKuKhm9zpg==}
+  fast-equals@6.1.0:
+    resolution: {integrity: sha512-MDA0N5wXvmx0yR0xeOf20qVMbyaEzS2kuxhBrDrIT5w3Gap7bZ/24jtvOvbthbZFmf+7PCC8XCJKGpmhVtvbIg==}
     engines: {node: '>=6.0.0'}
 
   fast-glob@3.3.3:
@@ -2672,8 +2669,8 @@ packages:
   fecha@4.2.3:
     resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
 
-  figlet@1.11.4:
-    resolution: {integrity: sha512-ZU41480OncL+NVLQrT0GVnbO0COL24sLSrzksioDgunmdJNYEUxhJJZ4Y3x1csN8XXqN5OcCZTLG8lKdquNyfQ==}
+  figlet@1.12.0:
+    resolution: {integrity: sha512-Ith3WsUi0N/EqGt+0/F6XnWhT04sohhH2tT0Dq7GdUmWbMTIoCbg2Jq9qKggmTO62ST5kRCfB311EmIMbizGyA==}
     engines: {node: '>= 17.0.0'}
     hasBin: true
 
@@ -2820,8 +2817,8 @@ packages:
     resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==}
     engines: {node: '>=18'}
 
-  globals@17.12.0:
-    resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==}
+  globals@17.13.0:
+    resolution: {integrity: sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==}
     engines: {node: '>=18'}
 
   globby@11.1.0:
@@ -2858,8 +2855,8 @@ packages:
     resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
     engines: {node: '>=0.10.0'}
 
-  hono@4.13.8:
-    resolution: {integrity: sha512-/Gng7NfoykZl2pjukW5Z6+8Yxm3BPRf86GTbQnt0SbySkvax4fyL4H3HhY1cCpBGmiW9XDRFzRV+CXK2W8QudQ==}
+  hono@4.13.12:
+    resolution: {integrity: sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==}
     engines: {node: '>=16.9.0'}
 
   hookable@5.5.3:
@@ -2909,16 +2906,16 @@ packages:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
 
-  ignore@7.0.10:
-    resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==}
+  ignore@7.0.12:
+    resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==}
     engines: {node: '>= 4'}
 
   import-fresh@3.3.1:
     resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
     engines: {node: '>=6'}
 
-  import-fresh@4.0.0:
-    resolution: {integrity: sha512-Fpi660c7VPDM3fPKYovStd9IP1CPOikf6v/dGxJJMmHPcwYQIMJ4W7kO1avBYEpMqkCh+Dx3Ln6H7VYqgztLjw==}
+  import-fresh@4.0.1:
+    resolution: {integrity: sha512-/XFeXYgkuWgbtqPC7qPKvciXTvhtz7WMNQKQsAM1Kz+j0I5OWJDrybfiya6QtHF9oEvPkLwTHmC9+4OR/MmfIA==}
     engines: {node: '>=22.15'}
 
   import-meta-resolve@4.2.0:
@@ -2955,8 +2952,8 @@ packages:
     peerDependencies:
       fp-ts: ^2.5.0
 
-  ip-address@10.7.2:
-    resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==}
+  ip-address@10.7.3:
+    resolution: {integrity: sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==}
     engines: {node: '>= 12'}
 
   ipaddr.js@1.9.1:
@@ -3074,6 +3071,10 @@ packages:
     resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
     hasBin: true
 
+  js-yaml@5.4.2:
+    resolution: {integrity: sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==}
+    hasBin: true
+
   jsdoc-type-pratt-parser@9.2.2:
     resolution: {integrity: sha512-eFY7Rlscvv/BpY6UsVsjKwMhu1tJO/F4U/vC9g3yeRy/JqszuO1UP6Vdq+85G+3FP6fEw/sWR8a5ftPFY86csw==}
     engines: {node: ^22.22.2 || >=24.15.0}
@@ -3335,8 +3336,8 @@ packages:
     resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
     engines: {node: '>=8.6'}
 
-  mikro-orm@6.6.16:
-    resolution: {integrity: sha512-42bvve+XSzvBpOuZeZxJsM5aW4DQnDn23E5to4cvEjUdaPX7jg1bRh+l8f/8/WJJNzG1T05V5vHSe+SHzqN87A==}
+  mikro-orm@6.6.17:
+    resolution: {integrity: sha512-AL7V8ytnDRpwUMcYwOiGNIOW7WuE8ca2hf+pw9jcpSVWwwJNu/j9K574lf0PALaJnikbzidsxmjCUrhQ0ea+sA==}
     engines: {node: '>= 18.12.0'}
 
   mime-db@1.54.0:
@@ -3827,8 +3828,8 @@ packages:
     engines: {node: 20 || >=22}
     hasBin: true
 
-  rolldown@1.2.10:
-    resolution: {integrity: sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==}
+  rolldown@1.2.12:
+    resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==}
     engines: {node: ^20.19.0 || >=22.12.0}
     hasBin: true
 
@@ -3932,8 +3933,8 @@ packages:
     resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==}
     engines: {node: '>= 18'}
 
-  source-map-js@1.2.1:
-    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+  source-map-js@1.2.2:
+    resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
     engines: {node: '>=0.10.0'}
 
   sparse-bitfield@3.0.3:
@@ -3966,8 +3967,8 @@ packages:
     resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
     engines: {node: '>= 0.8'}
 
-  std-env@4.2.0:
-    resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
+  std-env@4.3.0:
+    resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
 
   stdin-discarder@0.3.2:
     resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
@@ -3985,8 +3986,8 @@ packages:
     resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
     engines: {node: '>=18'}
 
-  string-width@8.2.2:
-    resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
+  string-width@8.3.0:
+    resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==}
     engines: {node: '>=20'}
 
   string_decoder@1.3.0:
@@ -4049,8 +4050,8 @@ packages:
     resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
     engines: {node: '>=8'}
 
-  tinybench@6.1.4:
-    resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==}
+  tinybench@6.2.0:
+    resolution: {integrity: sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==}
     engines: {node: '>=20.0.0'}
 
   tinyexec@1.3.1:
@@ -4061,15 +4062,15 @@ packages:
     resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
     engines: {node: '>=12.0.0'}
 
-  tinyrainbow@3.1.1:
-    resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
+  tinyrainbow@3.2.0:
+    resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==}
     engines: {node: '>=14.0.0'}
 
-  tldts-core@7.4.15:
-    resolution: {integrity: sha512-ERuv0p98XgSzmlSLJr8vDNxX+uATGInlN97V3R+JpYWaSqn5IG3ewWgCwczk4bkOpgth/o8Nt5FOi4B4w0n/1A==}
+  tldts-core@7.4.16:
+    resolution: {integrity: sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==}
 
-  tldts@7.4.15:
-    resolution: {integrity: sha512-SJVBeHOxDbNoq14CvpAoA2mLEWdbldGK8nR+yumpjY5nrlZxOflcA7p1Qj1gbTGmbu9DY9qLj/bENSWhBzjxRQ==}
+  tldts@7.4.16:
+    resolution: {integrity: sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==}
     hasBin: true
 
   to-regex-range@5.0.1:
@@ -4151,8 +4152,8 @@ packages:
     resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
     engines: {node: '>= 18'}
 
-  typescript-eslint@8.70.1:
-    resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==}
+  typescript-eslint@8.71.0:
+    resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     peerDependencies:
       eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
@@ -4176,20 +4177,17 @@ packages:
   ufo@1.6.4:
     resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
 
-  undici-types@7.18.2:
-    resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
-
   undici-types@7.24.6:
     resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
 
-  undici-types@8.11.0:
-    resolution: {integrity: sha512-Ophcodxps+mlNdcccUrHXy+Pp8X7Q0ycBk3QelSNl+jBAPMh6cEB/5xs9xDL3PJVnVs2Cp//nnsJySuD9D2WFA==}
+  undici-types@8.11.2:
+    resolution: {integrity: sha512-iMVNmWZ0leK/goS6eXMizSzmm9CDWtyphwbaCms3DNLqRxDL+mMoNVcZMTyyVgXP0N+Z8neAMzDoUOUJL8veKg==}
 
   undici-types@8.9.0:
     resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==}
 
-  undici@8.11.0:
-    resolution: {integrity: sha512-AdzuGcAkzhbha3GgCSoUBx2P0quwym6qdFNCQz/fDGZc8w9XJ741wHUbt2kTmfPz+ZIOMkySFkiY7RXmiDDC8w==}
+  undici@8.11.2:
+    resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==}
     engines: {node: '>=22.19.0'}
 
   unicorn-magic@0.3.0:
@@ -4267,8 +4265,8 @@ packages:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
 
-  vite@8.3.1:
-    resolution: {integrity: sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==}
+  vite@8.3.2:
+    resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
     engines: {node: ^20.19.0 || >=22.12.0}
     hasBin: true
     peerDependencies:
@@ -4355,24 +4353,24 @@ packages:
     resolution: {integrity: sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==}
     engines: {node: '>=8.0.0 || >=10.0.0'}
 
-  vscode-jsonrpc@9.0.2:
-    resolution: {integrity: sha512-SbQSV9yRemARxeXw6LU5sS6Zq0e9/DgCCX5yelH263ZQWukbTk8EF8fjTrr1dziasf4GwlJbvTwFnTrnQFWZXQ==}
+  vscode-jsonrpc@9.0.3:
+    resolution: {integrity: sha512-qETYf9PCl1lVfwR+oBmMfc4vP2nqLNA35ABxorw3j06+xlKRgDVxbLgKm4Y/xPL0YWdeDjLoI4ImICIT3nB0zQ==}
     engines: {node: '>=14.0.0'}
 
-  vscode-languageserver-protocol@3.18.3:
-    resolution: {integrity: sha512-DF49+WeV5py4zO5hhobp60jjsDSK0lAqA0OuKBLBvp423HPWQcCbhZz3JgyfIewsEz2f8U+X75xNIFHdiXZm2w==}
+  vscode-languageserver-protocol@3.18.4:
+    resolution: {integrity: sha512-CCXbyr99dafRJzJH/0Ryk7ly5T8XPmSR0//OuF2w4m+9JiOin7zcAT5iET8GRE7Z9taFBejb3GbM0a0mbjdbrg==}
 
-  vscode-languageserver-textdocument@1.0.14:
-    resolution: {integrity: sha512-EQyqJMi552E4ZTf46izQ4Fj6XquqxCySR3J5ZSD1SisMf6RfpeOWHxGBE8Gr6V0/3GHIGdAzDn8F8+1nTGCnoQ==}
+  vscode-languageserver-textdocument@1.0.15:
+    resolution: {integrity: sha512-Gsfr9S8LU3S6885cS+NaleasJ0n3V5y/SOTqHCifQiXWFw3G9w9/YItDfwHrlCz+dSZWWZ8ilYVMcJe7Sz7Sxw==}
 
-  vscode-languageserver-types@3.18.3:
-    resolution: {integrity: sha512-XIlzJ7Qp/jzSI1ds7/FwPAWrPeTZA7pAtlW4hdJ1J6xXWJL6dR9QYnDhJOdLzdKhUQ5Mm6mvUMw+3DcOQQasPw==}
+  vscode-languageserver-types@3.18.4:
+    resolution: {integrity: sha512-nQsacoiijvl0fRf2q28uFDrAVwRhdCa3QveiF7/CX0r18mLR5FU/1WskSsCKOjmEbtteKUnY8x8Yzxop+k0zwg==}
 
   vscode-uri@3.2.0:
     resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==}
 
-  vue-component-type-helpers@3.3.11:
-    resolution: {integrity: sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==}
+  vue-component-type-helpers@3.3.12:
+    resolution: {integrity: sha512-HwCqmgTfLFskQc4P5TRnbduCMyPat2Ezu0elLWOGn0dD/6BKHP15B5bxce6aDi+E4kblPtru8amIBQzu0Di3qQ==}
 
   vue-eslint-parser@10.4.1:
     resolution: {integrity: sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==}
@@ -4404,8 +4402,8 @@ packages:
     peerDependencies:
       vue: ^3.0
 
-  vue-tsc@3.3.11:
-    resolution: {integrity: sha512-gOb0B9rtU2+f1dszwPqSH5kAieIF9ReeLhD3kSRNHv5WZZUQz/JdVXW0RTdqhNTMlQkqKzrTTviqKr/4FYZraQ==}
+  vue-tsc@3.3.12:
+    resolution: {integrity: sha512-kbpaz18O9jPVkXVHD1BEjpUXM6WhXSu+62L1sGGr6Wbzd8GbvkBQWjoj3KZ/C0aq2MZllce7UdtWzwCIYsBMGA==}
     hasBin: true
     peerDependencies:
       typescript: '>=5.0.0'
@@ -4587,15 +4585,15 @@ snapshots:
 
   '@arr/every@1.0.1': {}
 
-  '@asamuzakjp/css-color@7.0.1':
+  '@asamuzakjp/css-color@7.1.3':
     dependencies:
-      '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-color-parser': 4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-tokenizer': 4.0.1
+      '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-color-parser': 4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-tokenizer': 4.0.2
       lru-cache: 11.5.3
 
-  '@asamuzakjp/dom-selector@9.2.1':
+  '@asamuzakjp/dom-selector@9.2.4':
     dependencies:
       bidi-js: 1.1.0
       css-tree: 3.2.1
@@ -4646,7 +4644,7 @@ snapshots:
     dependencies:
       '@babel/compat-data': 7.29.7
       '@babel/helper-validator-option': 7.29.7
-      browserslist: 4.29.0
+      browserslist: 4.29.3
       lru-cache: 5.1.1
       semver: 6.3.1
 
@@ -4794,12 +4792,12 @@ snapshots:
 
   '@colors/colors@1.6.1': {}
 
-  '@commitlint/cli@21.2.3(@types/node@24.19.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)':
+  '@commitlint/cli@21.2.3(@types/node@24.19.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3)':
     dependencies:
       '@commitlint/config-conventional': 21.2.3
       '@commitlint/format': 21.2.3
       '@commitlint/lint': 21.2.3
-      '@commitlint/load': 21.2.3(@types/node@24.19.0)(typescript@6.0.3)
+      '@commitlint/load': 21.2.3(@types/node@24.19.1)(typescript@6.0.3)
       '@commitlint/read': 21.2.3(conventional-commits-parser@7.1.2)
       '@commitlint/types': 21.2.3
       tinyexec: 1.3.1
@@ -4844,14 +4842,14 @@ snapshots:
       '@commitlint/rules': 21.2.3
       '@commitlint/types': 21.2.3
 
-  '@commitlint/load@21.2.3(@types/node@24.19.0)(typescript@6.0.3)':
+  '@commitlint/load@21.2.3(@types/node@24.19.1)(typescript@6.0.3)':
     dependencies:
       '@commitlint/config-validator': 21.2.3
       '@commitlint/execute-rule': 21.0.1
       '@commitlint/resolve-extends': 21.2.3
       '@commitlint/types': 21.2.3
       cosmiconfig: 9.0.2(typescript@6.0.3)
-      cosmiconfig-typescript-loader: 6.3.0(@types/node@24.19.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3)
+      cosmiconfig-typescript-loader: 6.3.0(@types/node@24.19.1)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3)
       es-toolkit: 1.52.0
       is-plain-obj: 4.1.0
       picocolors: 1.1.1
@@ -4920,7 +4918,7 @@ snapshots:
       '@cspell/dict-aws': 4.0.17
       '@cspell/dict-bash': 4.2.3
       '@cspell/dict-companies': 3.2.13
-      '@cspell/dict-cpp': 7.1.2
+      '@cspell/dict-cpp': 7.1.3
       '@cspell/dict-cryptocurrencies': 5.0.5
       '@cspell/dict-csharp': 4.0.8
       '@cspell/dict-css': 4.1.2
@@ -4930,7 +4928,7 @@ snapshots:
       '@cspell/dict-docker': 1.1.17
       '@cspell/dict-dotnet': 5.0.15
       '@cspell/dict-elixir': 4.0.8
-      '@cspell/dict-en-common-misspellings': 2.2.2
+      '@cspell/dict-en-common-misspellings': 2.2.3
       '@cspell/dict-en-gb-mit': 3.1.29
       '@cspell/dict-en_us': 4.4.40
       '@cspell/dict-filetypes': 3.0.18
@@ -4938,7 +4936,7 @@ snapshots:
       '@cspell/dict-fonts': 4.0.6
       '@cspell/dict-fsharp': 1.1.1
       '@cspell/dict-fullstack': 3.2.9
-      '@cspell/dict-gaming-terms': 1.1.2
+      '@cspell/dict-gaming-terms': 1.1.3
       '@cspell/dict-git': 3.1.1
       '@cspell/dict-golang': 6.0.29
       '@cspell/dict-google': 1.0.9
@@ -4956,17 +4954,17 @@ snapshots:
       '@cspell/dict-markdown': 2.0.18(@cspell/dict-css@4.1.2)(@cspell/dict-html-symbol-entities@4.0.5)(@cspell/dict-html@4.0.16)(@cspell/dict-typescript@3.2.3)
       '@cspell/dict-monkeyc': 1.1.2
       '@cspell/dict-node': 5.0.9
-      '@cspell/dict-npm': 5.2.50
+      '@cspell/dict-npm': 5.2.52
       '@cspell/dict-php': 4.1.3
       '@cspell/dict-powershell': 5.0.17
-      '@cspell/dict-public-licenses': 2.0.16
+      '@cspell/dict-public-licenses': 2.0.17
       '@cspell/dict-python': 4.5.2
       '@cspell/dict-r': 2.1.1
       '@cspell/dict-ruby': 5.1.4
       '@cspell/dict-rust': 4.1.2
       '@cspell/dict-scala': 5.0.11
       '@cspell/dict-shell': 1.2.0
-      '@cspell/dict-software-terms': 5.4.5
+      '@cspell/dict-software-terms': 5.4.6
       '@cspell/dict-sql': 2.2.1
       '@cspell/dict-svelte': 1.0.7
       '@cspell/dict-swift': 2.0.6
@@ -4999,7 +4997,7 @@ snapshots:
 
   '@cspell/dict-companies@3.2.13': {}
 
-  '@cspell/dict-cpp@7.1.2': {}
+  '@cspell/dict-cpp@7.1.3': {}
 
   '@cspell/dict-cryptocurrencies@5.0.5': {}
 
@@ -5019,7 +5017,7 @@ snapshots:
 
   '@cspell/dict-elixir@4.0.8': {}
 
-  '@cspell/dict-en-common-misspellings@2.2.2': {}
+  '@cspell/dict-en-common-misspellings@2.2.3': {}
 
   '@cspell/dict-en-gb-mit@3.1.29': {}
 
@@ -5035,7 +5033,7 @@ snapshots:
 
   '@cspell/dict-fullstack@3.2.9': {}
 
-  '@cspell/dict-gaming-terms@1.1.2': {}
+  '@cspell/dict-gaming-terms@1.1.3': {}
 
   '@cspell/dict-git@3.1.1': {}
 
@@ -5076,13 +5074,13 @@ snapshots:
 
   '@cspell/dict-node@5.0.9': {}
 
-  '@cspell/dict-npm@5.2.50': {}
+  '@cspell/dict-npm@5.2.52': {}
 
   '@cspell/dict-php@4.1.3': {}
 
   '@cspell/dict-powershell@5.0.17': {}
 
-  '@cspell/dict-public-licenses@2.0.16': {}
+  '@cspell/dict-public-licenses@2.0.17': {}
 
   '@cspell/dict-python@4.5.2':
     dependencies:
@@ -5098,7 +5096,7 @@ snapshots:
 
   '@cspell/dict-shell@1.2.0': {}
 
-  '@cspell/dict-software-terms@5.4.5': {}
+  '@cspell/dict-software-terms@5.4.6': {}
 
   '@cspell/dict-sql@2.2.1': {}
 
@@ -5119,12 +5117,12 @@ snapshots:
       '@cspell/url': 10.3.6
       import-meta-resolve: 4.2.0
 
-  '@cspell/eslint-plugin@10.3.6(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))':
+  '@cspell/eslint-plugin@10.3.6(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))':
     dependencies:
       '@cspell/cspell-types': 10.3.6
       '@cspell/url': 10.3.6
       cspell-lib: 10.3.6
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       synckit: 0.11.13
 
   '@cspell/filetypes@10.3.6': {}
@@ -5139,29 +5137,29 @@ snapshots:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.9
 
-  '@csstools/color-helpers@6.1.1': {}
+  '@csstools/color-helpers@6.1.2': {}
 
-  '@csstools/css-calc@3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)':
+  '@csstools/css-calc@3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)':
     dependencies:
-      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-tokenizer': 4.0.1
+      '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-tokenizer': 4.0.2
 
-  '@csstools/css-color-parser@4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)':
+  '@csstools/css-color-parser@4.2.6(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)':
     dependencies:
-      '@csstools/color-helpers': 6.1.1
-      '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1)
-      '@csstools/css-tokenizer': 4.0.1
+      '@csstools/color-helpers': 6.1.2
+      '@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-parser-algorithms': 4.0.2(@csstools/css-tokenizer@4.0.2)
+      '@csstools/css-tokenizer': 4.0.2
 
-  '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1)':
+  '@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2)':
     dependencies:
-      '@csstools/css-tokenizer': 4.0.1
+      '@csstools/css-tokenizer': 4.0.2
 
-  '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)':
+  '@csstools/css-syntax-patches-for-csstree@1.1.15(css-tree@3.2.1)':
     optionalDependencies:
       css-tree: 3.2.1
 
-  '@csstools/css-tokenizer@4.0.1': {}
+  '@csstools/css-tokenizer@4.0.2': {}
 
   '@dabh/diagnostics@2.0.9':
     dependencies:
@@ -5174,7 +5172,7 @@ snapshots:
   '@es-joy/jsdoccomment@0.98.0':
     dependencies:
       '@types/estree': 1.0.9
-      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/types': 8.71.0
       comment-parser: 1.4.9
       esquery: 1.7.0
       jsdoc-type-pratt-parser: 9.2.2
@@ -5259,9 +5257,9 @@ snapshots:
   '@esbuild/win32-x64@0.28.2':
     optional: true
 
-  '@eslint-community/eslint-utils@4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))':
+  '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))':
     dependencies:
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.2': {}
@@ -5282,9 +5280,9 @@ snapshots:
     dependencies:
       '@types/json-schema': 7.0.15
 
-  '@eslint/js@10.0.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))':
+  '@eslint/js@10.0.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))':
     optionalDependencies:
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
 
   '@eslint/object-schema@3.0.5': {}
 
@@ -5295,9 +5293,9 @@ snapshots:
 
   '@exodus/bytes@1.16.0': {}
 
-  '@hono/node-server@2.1.1(hono@4.13.8)':
+  '@hono/node-server@2.1.3(hono@4.13.12)':
     dependencies:
-      hono: 4.13.8
+      hono: 4.13.12
 
   '@humanfs/core@0.19.2':
     dependencies:
@@ -5324,7 +5322,7 @@ snapshots:
   '@jercle/yargonaut@1.1.5':
     dependencies:
       chalk: 4.1.2
-      figlet: 1.11.4
+      figlet: 1.12.0
       parent-require: 1.0.0
 
   '@jridgewell/gen-mapping@0.3.13':
@@ -5359,10 +5357,10 @@ snapshots:
 
   '@keyv/serialize@1.1.1': {}
 
-  '@mikro-orm/better-sqlite@6.6.14(@mikro-orm/core@6.6.16)(mariadb@3.4.5)(supports-color@7.2.0)':
+  '@mikro-orm/better-sqlite@6.6.14(@mikro-orm/core@6.6.17)(mariadb@3.4.5)(supports-color@7.2.0)':
     dependencies:
-      '@mikro-orm/core': 6.6.16
-      '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
+      '@mikro-orm/core': 6.6.17
+      '@mikro-orm/knex': 6.6.14(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
       better-sqlite3: 11.10.0
       fs-extra: 11.3.3
       sqlstring-sqlite: 0.1.1
@@ -5378,11 +5376,11 @@ snapshots:
       - supports-color
       - tedious
 
-  '@mikro-orm/cli@6.6.16(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
+  '@mikro-orm/cli@6.6.17(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
     dependencies:
       '@jercle/yargonaut': 1.1.5
-      '@mikro-orm/core': 6.6.16
-      '@mikro-orm/knex': 6.6.16(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
+      '@mikro-orm/core': 6.6.17
+      '@mikro-orm/knex': 6.6.17(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
       fs-extra: 11.3.3
       tsconfig-paths: 4.2.0
       yargs: 17.7.2
@@ -5399,19 +5397,19 @@ snapshots:
       - supports-color
       - tedious
 
-  '@mikro-orm/core@6.6.16':
+  '@mikro-orm/core@6.6.17':
     dependencies:
       dataloader: 2.2.3
       dotenv: 17.3.1
       esprima: 4.0.1
       fs-extra: 11.3.3
       globby: 11.1.0
-      mikro-orm: 6.6.16
+      mikro-orm: 6.6.17
       reflect-metadata: 0.2.2
 
-  '@mikro-orm/knex@6.6.14(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
+  '@mikro-orm/knex@6.6.14(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
     dependencies:
-      '@mikro-orm/core': 6.6.16
+      '@mikro-orm/core': 6.6.17
       fs-extra: 11.3.3
       knex: 3.2.10(better-sqlite3@11.10.0)(supports-color@7.2.0)
       sqlstring: 2.3.3
@@ -5428,9 +5426,9 @@ snapshots:
       - supports-color
       - tedious
 
-  '@mikro-orm/knex@6.6.16(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
+  '@mikro-orm/knex@6.6.17(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)':
     dependencies:
-      '@mikro-orm/core': 6.6.16
+      '@mikro-orm/core': 6.6.17
       fs-extra: 11.3.3
       knex: 3.2.10(better-sqlite3@11.10.0)(supports-color@7.2.0)
       sqlstring: 2.3.3
@@ -5447,10 +5445,10 @@ snapshots:
       - supports-color
       - tedious
 
-  '@mikro-orm/mariadb@6.6.16(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(supports-color@7.2.0)':
+  '@mikro-orm/mariadb@6.6.17(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(supports-color@7.2.0)':
     dependencies:
-      '@mikro-orm/core': 6.6.16
-      '@mikro-orm/knex': 6.6.16(@mikro-orm/core@6.6.16)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
+      '@mikro-orm/core': 6.6.17
+      '@mikro-orm/knex': 6.6.17(@mikro-orm/core@6.6.17)(better-sqlite3@11.10.0)(mariadb@3.4.5)(supports-color@7.2.0)
       mariadb: 3.4.5
     transitivePeerDependencies:
       - better-sqlite3
@@ -5464,15 +5462,15 @@ snapshots:
       - supports-color
       - tedious
 
-  '@mikro-orm/reflection@6.6.16(@mikro-orm/core@6.6.16)':
+  '@mikro-orm/reflection@6.6.17(@mikro-orm/core@6.6.17)':
     dependencies:
-      '@mikro-orm/core': 6.6.16
+      '@mikro-orm/core': 6.6.17
       globby: 11.1.0
       ts-morph: 27.0.2
 
   '@modelcontextprotocol/sdk@1.31.0(supports-color@7.2.0)(zod@4.6.5)':
     dependencies:
-      '@hono/node-server': 2.1.1(hono@4.13.8)
+      '@hono/node-server': 2.1.3(hono@4.13.12)
       ajv: 8.20.0
       ajv-formats: 3.0.1(ajv@8.20.0)
       content-type: 1.0.5
@@ -5482,7 +5480,7 @@ snapshots:
       eventsource-parser: 3.1.1
       express: 5.2.1(supports-color@7.2.0)
       express-rate-limit: 8.7.0(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0)
-      hono: 4.13.8
+      hono: 4.13.12
       jose: 6.2.12
       json-schema-typed: 8.0.2
       pkce-challenge: 5.0.1
@@ -5492,7 +5490,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@mongodb-js/saslprep@1.5.4':
+  '@mongodb-js/saslprep@1.5.5':
     dependencies:
       sparse-bitfield: 3.0.3
 
@@ -5512,7 +5510,7 @@ snapshots:
 
   '@opentelemetry/api@1.9.1': {}
 
-  '@oxc-project/types@0.151.0': {}
+  '@oxc-project/types@0.152.0': {}
 
   '@parcel/watcher-android-arm64@2.6.0':
     optional: true
@@ -5576,49 +5574,49 @@ snapshots:
 
   '@polka/url@1.0.0-next.29': {}
 
-  '@rolldown/binding-android-arm-eabi@1.2.10':
+  '@rolldown/binding-android-arm-eabi@1.2.12':
     optional: true
 
-  '@rolldown/binding-android-arm64@1.2.10':
+  '@rolldown/binding-android-arm64@1.2.12':
     optional: true
 
-  '@rolldown/binding-darwin-arm64@1.2.10':
+  '@rolldown/binding-darwin-arm64@1.2.12':
     optional: true
 
-  '@rolldown/binding-darwin-x64@1.2.10':
+  '@rolldown/binding-darwin-x64@1.2.12':
     optional: true
 
-  '@rolldown/binding-freebsd-x64@1.2.10':
+  '@rolldown/binding-freebsd-x64@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-arm-gnueabihf@1.2.10':
+  '@rolldown/binding-linux-arm-gnueabihf@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-arm64-gnu@1.2.10':
+  '@rolldown/binding-linux-arm64-gnu@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-arm64-musl@1.2.10':
+  '@rolldown/binding-linux-arm64-musl@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-ppc64-gnu@1.2.10':
+  '@rolldown/binding-linux-ppc64-gnu@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-s390x-gnu@1.2.10':
+  '@rolldown/binding-linux-s390x-gnu@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-x64-gnu@1.2.10':
+  '@rolldown/binding-linux-x64-gnu@1.2.12':
     optional: true
 
-  '@rolldown/binding-linux-x64-musl@1.2.10':
+  '@rolldown/binding-linux-x64-musl@1.2.12':
     optional: true
 
-  '@rolldown/binding-openharmony-arm64@1.2.10':
+  '@rolldown/binding-openharmony-arm64@1.2.12':
     optional: true
 
-  '@rolldown/binding-win32-arm64-msvc@1.2.10':
+  '@rolldown/binding-win32-arm64-msvc@1.2.12':
     optional: true
 
-  '@rolldown/binding-win32-x64-msvc@1.2.10':
+  '@rolldown/binding-win32-x64-msvc@1.2.12':
     optional: true
 
   '@rolldown/pluginutils@1.0.1': {}
@@ -5638,11 +5636,11 @@ snapshots:
 
   '@standard-schema/spec@1.1.0': {}
 
-  '@stylistic/eslint-plugin@5.10.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))':
+  '@stylistic/eslint-plugin@5.10.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))':
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      '@typescript-eslint/types': 8.70.1
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@typescript-eslint/types': 8.71.0
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       eslint-visitor-keys: 4.2.1
       espree: 10.4.0
       estraverse: 5.3.0
@@ -5679,24 +5677,20 @@ snapshots:
 
   '@types/jsdom@30.0.0':
     dependencies:
-      '@types/node': 24.13.6
+      '@types/node': 24.19.1
       '@types/tough-cookie': 4.0.5
       parse5: 8.0.1
-      undici-types: 8.11.0
+      undici-types: 8.11.2
 
   '@types/json-schema@7.0.15': {}
 
   '@types/minimatch@3.0.5': {}
 
-  '@types/node@24.13.6':
-    dependencies:
-      undici-types: 7.18.2
-
-  '@types/node@24.19.0':
+  '@types/node@24.19.1':
     dependencies:
       undici-types: 7.24.6
 
-  '@types/node@26.6.2':
+  '@types/node@26.6.4':
     dependencies:
       undici-types: 8.9.0
 
@@ -5716,87 +5710,87 @@ snapshots:
 
   '@types/ws@8.18.2':
     dependencies:
-      '@types/node': 24.19.0
+      '@types/node': 24.19.1
 
-  '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
-      '@typescript-eslint/parser': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/scope-manager': 8.70.1
-      '@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/visitor-keys': 8.70.1
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
-      ignore: 7.0.10
+      '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/scope-manager': 8.71.0
+      '@typescript-eslint/type-utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/visitor-keys': 8.71.0
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
+      ignore: 7.0.12
       natural-compare: 1.4.0
       ts-api-utils: 2.5.0(typescript@6.0.3)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/parser@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
-      '@typescript-eslint/scope-manager': 8.70.1
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/visitor-keys': 8.70.1
+      '@typescript-eslint/scope-manager': 8.71.0
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/visitor-keys': 8.71.0
       debug: 4.4.3(supports-color@7.2.0)
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/project-service@8.70.1(supports-color@7.2.0)(typescript@5.9.3)':
+  '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
-      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3)
+      '@typescript-eslint/types': 8.71.0
       debug: 4.4.3(supports-color@7.2.0)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/project-service@8.70.1(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/project-service@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
-      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
-      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
+      '@typescript-eslint/types': 8.71.0
       debug: 4.4.3(supports-color@7.2.0)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/scope-manager@8.70.1':
+  '@typescript-eslint/scope-manager@8.71.0':
     dependencies:
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/visitor-keys': 8.70.1
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/visitor-keys': 8.71.0
 
-  '@typescript-eslint/tsconfig-utils@8.70.1(typescript@5.9.3)':
+  '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)':
     dependencies:
       typescript: 5.9.3
 
-  '@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)':
+  '@typescript-eslint/tsconfig-utils@8.71.0(typescript@6.0.3)':
     dependencies:
       typescript: 6.0.3
 
-  '@typescript-eslint/type-utils@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/type-utils@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       debug: 4.4.3(supports-color@7.2.0)
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       ts-api-utils: 2.5.0(typescript@6.0.3)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/types@8.70.1': {}
+  '@typescript-eslint/types@8.71.0': {}
 
-  '@typescript-eslint/typescript-estree@8.70.1(supports-color@7.2.0)(typescript@5.9.3)':
+  '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/project-service': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
-      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@5.9.3)
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/visitor-keys': 8.70.1
+      '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3)
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/visitor-keys': 8.71.0
       debug: 4.4.3(supports-color@7.2.0)
       minimatch: 10.2.6
       semver: 7.8.5
@@ -5806,12 +5800,12 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/typescript-estree@8.70.1(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/typescript-estree@8.71.0(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
-      '@typescript-eslint/project-service': 8.70.1(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3)
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/visitor-keys': 8.70.1
+      '@typescript-eslint/project-service': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@6.0.3)
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/visitor-keys': 8.71.0
       debug: 4.4.3(supports-color@7.2.0)
       minimatch: 10.2.6
       semver: 7.8.5
@@ -5821,66 +5815,66 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
+  '@typescript-eslint/utils@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      '@typescript-eslint/scope-manager': 8.70.1
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@6.0.3)
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@typescript-eslint/scope-manager': 8.71.0
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/visitor-keys@8.70.1':
+  '@typescript-eslint/visitor-keys@8.71.0':
     dependencies:
-      '@typescript-eslint/types': 8.70.1
+      '@typescript-eslint/types': 8.71.0
       eslint-visitor-keys: 5.0.1
 
-  '@vitejs/plugin-vue-jsx@5.1.6(supports-color@7.2.0)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))':
+  '@vitejs/plugin-vue-jsx@5.1.6(supports-color@7.2.0)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))':
     dependencies:
       '@babel/core': 7.29.7(supports-color@7.2.0)
       '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))
       '@babel/plugin-transform-typescript': 7.29.9(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
       '@rolldown/pluginutils': 1.0.1
       '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
       vue: 3.5.43(typescript@6.0.3)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@6.0.9(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))':
+  '@vitejs/plugin-vue@6.0.9(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3))':
     dependencies:
       '@rolldown/pluginutils': 1.0.1
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
       vue: 3.5.43(typescript@6.0.3)
 
   '@vitest/coverage-v8@5.0.3(vitest@5.0.3)':
     dependencies:
       '@bcoe/v8-coverage': 1.0.2
-      '@vitest/istanbul-lib-coverage': 1.0.1
-      '@vitest/istanbul-lib-report': 1.0.1
+      '@vitest/istanbul-lib-coverage': 1.0.2
+      '@vitest/istanbul-lib-report': 1.0.2
       ast-v8-to-istanbul: 1.0.7
       magicast: 0.5.5
       obug: 2.2.1
-      std-env: 4.2.0
-      tinyrainbow: 3.1.1
-      vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.0)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
+      std-env: 4.3.0
+      tinyrainbow: 3.2.0
+      vitest: 5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
 
-  '@vitest/istanbul-lib-coverage@1.0.1': {}
+  '@vitest/istanbul-lib-coverage@1.0.2': {}
 
-  '@vitest/istanbul-lib-report@1.0.1':
+  '@vitest/istanbul-lib-report@1.0.2':
     dependencies:
-      '@vitest/istanbul-lib-coverage': 1.0.1
+      '@vitest/istanbul-lib-coverage': 1.0.2
 
-  '@vitest/mocker@5.0.3(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))':
+  '@vitest/mocker@5.0.3(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))':
     dependencies:
       '@jridgewell/trace-mapping': 0.3.31
       '@vitest/spy': 5.0.3
       estree-walker: 3.0.3
       magic-string: 1.4.2
     optionalDependencies:
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
 
   '@vitest/spy@5.0.3': {}
 
@@ -5943,7 +5937,7 @@ snapshots:
       '@vue/shared': 3.5.43
       entities: 7.0.1
       estree-walker: 2.0.2
-      source-map-js: 1.2.1
+      source-map-js: 1.2.2
 
   '@vue/compiler-dom@3.5.43':
     dependencies:
@@ -5960,7 +5954,7 @@ snapshots:
       estree-walker: 2.0.2
       magic-string: 0.30.21
       postcss: 8.5.28
-      source-map-js: 1.2.1
+      source-map-js: 1.2.2
 
   '@vue/compiler-ssr@3.5.43':
     dependencies:
@@ -5980,7 +5974,7 @@ snapshots:
 
   '@vue/devtools-shared@8.2.1': {}
 
-  '@vue/language-core@3.3.11':
+  '@vue/language-core@3.3.12':
     dependencies:
       '@volar/language-core': 2.4.28
       '@vue/compiler-dom': 3.5.43
@@ -6019,7 +6013,7 @@ snapshots:
       '@vue/compiler-dom': 3.5.43
       js-beautify: 2.0.3
       vue: 3.5.43(typescript@6.0.3)
-      vue-component-type-helpers: 3.3.11
+      vue-component-type-helpers: 3.3.12
     optionalDependencies:
       '@vue/server-renderer': 3.5.43
 
@@ -6072,7 +6066,7 @@ snapshots:
 
   ansi-regex@5.0.1: {}
 
-  ansi-regex@6.3.0: {}
+  ansi-regex@6.4.0: {}
 
   ansi-styles@4.3.0:
     dependencies:
@@ -6132,7 +6126,7 @@ snapshots:
 
   base64-js@1.5.1: {}
 
-  baseline-browser-mapping@2.11.25: {}
+  baseline-browser-mapping@2.11.27: {}
 
   basic-ftp@6.2.1: {}
 
@@ -6194,13 +6188,13 @@ snapshots:
     dependencies:
       fill-range: 7.1.1
 
-  browserslist@4.29.0:
+  browserslist@4.29.3:
     dependencies:
-      baseline-browser-mapping: 2.11.25
-      caniuse-lite: 1.0.30001810
-      electron-to-chromium: 1.5.438
+      baseline-browser-mapping: 2.11.27
+      caniuse-lite: 1.0.30001814
+      electron-to-chromium: 1.5.444
       node-releases: 2.0.57
-      update-browserslist-db: 1.3.3(browserslist@4.29.0)
+      update-browserslist-db: 1.3.3(browserslist@4.29.3)
 
   bson@7.3.3: {}
 
@@ -6240,9 +6234,9 @@ snapshots:
 
   camelcase@6.3.0: {}
 
-  caniuse-lite@1.0.30001810: {}
+  caniuse-lite@1.0.30001814: {}
 
-  chai@6.2.2: {}
+  chai@6.3.0: {}
 
   chalk@4.1.2:
     dependencies:
@@ -6405,9 +6399,9 @@ snapshots:
       object-assign: 4.1.1
       vary: 1.1.2
 
-  cosmiconfig-typescript-loader@6.3.0(@types/node@24.19.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3):
+  cosmiconfig-typescript-loader@6.3.0(@types/node@24.19.1)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3):
     dependencies:
-      '@types/node': 24.19.0
+      '@types/node': 24.19.1
       cosmiconfig: 9.0.2(typescript@6.0.3)
       jiti: 2.6.1
       typescript: 6.0.3
@@ -6455,7 +6449,7 @@ snapshots:
       '@cspell/cspell-pipe': 10.3.6
       '@cspell/cspell-types': 10.3.6
       cspell-trie-lib: 10.3.6(@cspell/cspell-types@10.3.6)
-      fast-equals: 6.0.4
+      fast-equals: 6.1.0
 
   cspell-glob@10.3.6:
     dependencies:
@@ -6492,9 +6486,9 @@ snapshots:
       cspell-trie-lib: 10.3.6(@cspell/cspell-types@10.3.6)
       env-paths: 4.0.0
       gensequence: 8.0.8
-      import-fresh: 4.0.0
+      import-fresh: 4.0.1
       resolve-from: 5.0.0
-      vscode-languageserver-textdocument: 1.0.14
+      vscode-languageserver-textdocument: 1.0.15
       vscode-uri: 3.2.0
       xdg-basedir: 5.1.0
 
@@ -6505,7 +6499,7 @@ snapshots:
   css-tree@3.2.1:
     dependencies:
       mdn-data: 2.27.1
-      source-map-js: 1.2.1
+      source-map-js: 1.2.2
 
   cssesc@3.0.0: {}
 
@@ -6633,7 +6627,7 @@ snapshots:
       '@standard-schema/spec': 1.1.0
       fast-check: 3.23.2
 
-  electron-to-chromium@1.5.438: {}
+  electron-to-chromium@1.5.444: {}
 
   emoji-regex@10.6.0: {}
 
@@ -6647,7 +6641,7 @@ snapshots:
     dependencies:
       once: 1.4.0
 
-  enhanced-resolve@5.25.1:
+  enhanced-resolve@5.26.0:
     dependencies:
       graceful-fs: 4.2.11
       tapable: 2.3.3
@@ -6729,28 +6723,28 @@ snapshots:
 
   escape-string-regexp@5.0.0: {}
 
-  eslint-compat-utils@0.5.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0)):
+  eslint-compat-utils@0.5.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0)):
     dependencies:
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       semver: 7.8.5
 
-  eslint-plugin-es-x@7.8.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0)):
+  eslint-plugin-es-x@7.8.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       '@eslint-community/regexpp': 4.12.2
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
-      eslint-compat-utils: 0.5.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint-compat-utils: 0.5.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
 
-  eslint-plugin-jsdoc@65.0.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+  eslint-plugin-jsdoc@65.0.2(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
     dependencies:
       '@es-joy/jsdoccomment': 0.98.0
       '@es-joy/resolve.exports': 1.2.0
-      '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       are-docs-informative: 0.1.1
       comment-parser: 1.4.9
       debug: 4.4.3(supports-color@7.2.0)
       escape-string-regexp: 5.0.0
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       espree: 11.2.0
       esquery: 1.7.0
       html-entities: 2.6.0
@@ -6764,49 +6758,50 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-n@18.3.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(typescript@6.0.3):
+  eslint-plugin-n@18.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(typescript@6.0.3):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      enhanced-resolve: 5.25.1
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
-      eslint-plugin-es-x: 7.8.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      enhanced-resolve: 5.26.0
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint-plugin-es-x: 7.8.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       get-tsconfig: 4.14.3
       globals: 15.15.0
       globrex: 0.1.2
       ignore: 5.3.2
+      js-yaml: 5.4.2
       semver: 7.8.5
     optionalDependencies:
       typescript: 6.0.3
 
-  eslint-plugin-perfectionist@5.12.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+  eslint-plugin-perfectionist@5.12.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
     dependencies:
-      '@typescript-eslint/types': 8.70.1
-      '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/types': 8.71.0
+      '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
       natural-orderby: 5.0.0
     optionalDependencies:
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
     transitivePeerDependencies:
       - supports-color
       - typescript
 
-  eslint-plugin-promise@7.3.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0)):
+  eslint-plugin-promise@7.3.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
 
-  eslint-plugin-vue@10.11.1(@stylistic/eslint-plugin@5.10.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0)))(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)):
+  eslint-plugin-vue@10.11.1(@stylistic/eslint-plugin@5.10.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0)))(@typescript-eslint/parser@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(vue-eslint-parser@10.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 7.1.6
       semver: 7.8.5
-      vue-eslint-parser: 10.4.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
+      vue-eslint-parser: 10.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
       xml-name-validator: 5.0.0
     optionalDependencies:
-      '@stylistic/eslint-plugin': 5.10.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      '@typescript-eslint/parser': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@stylistic/eslint-plugin': 5.10.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
 
   eslint-scope@9.1.2:
     dependencies:
@@ -6821,9 +6816,9 @@ snapshots:
 
   eslint-visitor-keys@5.0.1: {}
 
-  eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0):
+  eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0):
     dependencies:
-      '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       '@eslint-community/regexpp': 4.12.2
       '@eslint/config-array': 0.23.5(supports-color@7.2.0)
       '@eslint/config-helpers': 0.7.0
@@ -6912,7 +6907,7 @@ snapshots:
     dependencies:
       debug: 4.4.3(supports-color@7.2.0)
       express: 5.2.1(supports-color@7.2.0)
-      ip-address: 10.7.2
+      ip-address: 10.7.3
     transitivePeerDependencies:
       - supports-color
 
@@ -6949,7 +6944,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  exsolve@1.1.1: {}
+  exsolve@1.1.3: {}
 
   fast-check@3.23.2:
     dependencies:
@@ -6957,7 +6952,7 @@ snapshots:
 
   fast-deep-equal@3.1.3: {}
 
-  fast-equals@6.0.4: {}
+  fast-equals@6.1.0: {}
 
   fast-glob@3.3.3:
     dependencies:
@@ -6983,7 +6978,7 @@ snapshots:
 
   fecha@4.2.3: {}
 
-  figlet@1.11.4:
+  figlet@1.12.0:
     dependencies:
       commander: 14.0.3
 
@@ -7149,7 +7144,7 @@ snapshots:
 
   globals@15.15.0: {}
 
-  globals@17.12.0: {}
+  globals@17.13.0: {}
 
   globby@11.1.0:
     dependencies:
@@ -7182,7 +7177,7 @@ snapshots:
     dependencies:
       parse-passwd: 1.0.0
 
-  hono@4.13.8: {}
+  hono@4.13.12: {}
 
   hookable@5.5.3: {}
 
@@ -7226,14 +7221,14 @@ snapshots:
 
   ignore@5.3.2: {}
 
-  ignore@7.0.10: {}
+  ignore@7.0.12: {}
 
   import-fresh@3.3.1:
     dependencies:
       parent-module: 1.0.1
       resolve-from: 4.0.0
 
-  import-fresh@4.0.0: {}
+  import-fresh@4.0.1: {}
 
   import-meta-resolve@4.2.0: {}
 
@@ -7258,7 +7253,7 @@ snapshots:
     dependencies:
       fp-ts: 2.16.11
 
-  ip-address@10.7.2: {}
+  ip-address@10.7.3: {}
 
   ipaddr.js@1.9.1: {}
 
@@ -7341,17 +7336,21 @@ snapshots:
     dependencies:
       argparse: 2.0.1
 
+  js-yaml@5.4.2:
+    dependencies:
+      argparse: 2.0.1
+
   jsdoc-type-pratt-parser@9.2.2:
     dependencies:
       '@types/estree': 1.0.9
-      '@types/node': 26.6.2
+      '@types/node': 26.6.4
 
   jsdom@30.1.1:
     dependencies:
-      '@asamuzakjp/css-color': 7.0.1
-      '@asamuzakjp/dom-selector': 9.2.1
+      '@asamuzakjp/css-color': 7.1.3
+      '@asamuzakjp/dom-selector': 9.2.4
       '@bramus/specificity': 2.4.2
-      '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1)
+      '@csstools/css-syntax-patches-for-csstree': 1.1.15(css-tree@3.2.1)
       '@exodus/bytes': 1.16.0
       css-tree: 3.2.1
       data-urls: 7.0.0
@@ -7362,7 +7361,7 @@ snapshots:
       parse5: 8.0.1
       saxes: 6.0.0
       tough-cookie: 6.0.2
-      undici: 8.11.0
+      undici: 8.11.2
       w3c-xmlserializer: 6.0.0
       webidl-conversions: 8.0.1
       whatwg-mimetype: 5.0.0
@@ -7542,14 +7541,14 @@ snapshots:
     dependencies:
       '@babel/parser': 7.29.9
       '@babel/types': 7.29.8
-      source-map-js: 1.2.1
+      source-map-js: 1.2.2
 
   make-error@1.3.6: {}
 
   mariadb@3.4.5:
     dependencies:
       '@types/geojson': 7946.0.16
-      '@types/node': 24.13.6
+      '@types/node': 24.19.1
       denque: 2.1.0
       iconv-lite: 0.6.3
       lru-cache: 10.4.3
@@ -7577,7 +7576,7 @@ snapshots:
       braces: 3.0.3
       picomatch: 2.3.2
 
-  mikro-orm@6.6.16: {}
+  mikro-orm@6.6.17: {}
 
   mime-db@1.54.0: {}
 
@@ -7635,7 +7634,7 @@ snapshots:
 
   mongodb@7.7.0:
     dependencies:
-      '@mongodb-js/saslprep': 1.5.4
+      '@mongodb-js/saslprep': 1.5.5
       bson: 7.3.3
       mongodb-connection-string-url: 7.0.2
 
@@ -7675,17 +7674,17 @@ snapshots:
     dependencies:
       content-type: 2.1.0
 
-  neostandard@0.14.0-next.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+  neostandard@0.14.0-next.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
     dependencies:
       '@humanwhocodes/gitignore-to-minimatch': 1.0.2
-      '@stylistic/eslint-plugin': 5.10.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
-      eslint-plugin-n: 18.3.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(typescript@6.0.3)
-      eslint-plugin-promise: 7.3.0(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))
+      '@stylistic/eslint-plugin': 5.10.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint-plugin-n: 18.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(typescript@6.0.3)
+      eslint-plugin-promise: 7.3.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))
       find-up: 8.0.0
-      globals: 17.12.0
+      globals: 17.13.0
       peowly: 2.0.1
-      typescript-eslint: 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      typescript-eslint: 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
     transitivePeerDependencies:
       - supports-color
       - ts-declaration-location
@@ -7762,7 +7761,7 @@ snapshots:
       is-unicode-supported: 2.1.0
       log-symbols: 7.0.1
       stdin-discarder: 0.3.2
-      string-width: 8.2.2
+      string-width: 8.3.0
 
   p-limit@3.1.0:
     dependencies:
@@ -7859,7 +7858,7 @@ snapshots:
   pkg-types@2.3.3:
     dependencies:
       confbox: 0.3.1
-      exsolve: 1.1.1
+      exsolve: 1.1.3
       pathe: 2.0.3
 
   please-upgrade-node@3.2.0:
@@ -7882,7 +7881,7 @@ snapshots:
     dependencies:
       nanoid: 3.3.19
       picocolors: 1.1.1
-      source-map-js: 1.2.1
+      source-map-js: 1.2.2
 
   prebuild-install@7.1.3:
     dependencies:
@@ -8013,26 +8012,26 @@ snapshots:
       glob: 13.0.6
       package-json-from-dist: 1.0.1
 
-  rolldown@1.2.10:
+  rolldown@1.2.12:
     dependencies:
-      '@oxc-project/types': 0.151.0
+      '@oxc-project/types': 0.152.0
       '@rolldown/pluginutils': 1.0.1
     optionalDependencies:
-      '@rolldown/binding-android-arm-eabi': 1.2.10
-      '@rolldown/binding-android-arm64': 1.2.10
-      '@rolldown/binding-darwin-arm64': 1.2.10
-      '@rolldown/binding-darwin-x64': 1.2.10
-      '@rolldown/binding-freebsd-x64': 1.2.10
-      '@rolldown/binding-linux-arm-gnueabihf': 1.2.10
-      '@rolldown/binding-linux-arm64-gnu': 1.2.10
-      '@rolldown/binding-linux-arm64-musl': 1.2.10
-      '@rolldown/binding-linux-ppc64-gnu': 1.2.10
-      '@rolldown/binding-linux-s390x-gnu': 1.2.10
-      '@rolldown/binding-linux-x64-gnu': 1.2.10
-      '@rolldown/binding-linux-x64-musl': 1.2.10
-      '@rolldown/binding-openharmony-arm64': 1.2.10
-      '@rolldown/binding-win32-arm64-msvc': 1.2.10
-      '@rolldown/binding-win32-x64-msvc': 1.2.10
+      '@rolldown/binding-android-arm-eabi': 1.2.12
+      '@rolldown/binding-android-arm64': 1.2.12
+      '@rolldown/binding-darwin-arm64': 1.2.12
+      '@rolldown/binding-darwin-x64': 1.2.12
+      '@rolldown/binding-freebsd-x64': 1.2.12
+      '@rolldown/binding-linux-arm-gnueabihf': 1.2.12
+      '@rolldown/binding-linux-arm64-gnu': 1.2.12
+      '@rolldown/binding-linux-arm64-musl': 1.2.12
+      '@rolldown/binding-linux-ppc64-gnu': 1.2.12
+      '@rolldown/binding-linux-s390x-gnu': 1.2.12
+      '@rolldown/binding-linux-x64-gnu': 1.2.12
+      '@rolldown/binding-linux-x64-musl': 1.2.12
+      '@rolldown/binding-openharmony-arm64': 1.2.12
+      '@rolldown/binding-win32-arm64-msvc': 1.2.12
+      '@rolldown/binding-win32-x64-msvc': 1.2.12
 
   router@2.2.0(supports-color@7.2.0):
     dependencies:
@@ -8150,7 +8149,7 @@ snapshots:
   skott@0.35.12(supports-color@7.2.0):
     dependencies:
       '@parcel/watcher': 2.6.0
-      '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@5.9.3)
       commander: 11.1.0
       compression: 1.8.2(supports-color@7.2.0)
       depcheck: 1.4.7(supports-color@7.2.0)
@@ -8180,7 +8179,7 @@ snapshots:
 
   smol-toml@1.9.0: {}
 
-  source-map-js@1.2.1: {}
+  source-map-js@1.2.2: {}
 
   sparse-bitfield@3.0.3:
     dependencies:
@@ -8205,7 +8204,7 @@ snapshots:
 
   statuses@2.0.2: {}
 
-  std-env@4.2.0: {}
+  std-env@4.3.0: {}
 
   stdin-discarder@0.3.2: {}
 
@@ -8223,7 +8222,7 @@ snapshots:
       get-east-asian-width: 1.7.0
       strip-ansi: 7.2.0
 
-  string-width@8.2.2:
+  string-width@8.3.0:
     dependencies:
       get-east-asian-width: 1.7.0
       strip-ansi: 7.2.0
@@ -8238,7 +8237,7 @@ snapshots:
 
   strip-ansi@7.2.0:
     dependencies:
-      ansi-regex: 6.3.0
+      ansi-regex: 6.4.0
 
   strip-bom@3.0.0: {}
 
@@ -8289,7 +8288,7 @@ snapshots:
 
   tildify@2.0.0: {}
 
-  tinybench@6.1.4: {}
+  tinybench@6.2.0: {}
 
   tinyexec@1.3.1: {}
 
@@ -8298,13 +8297,13 @@ snapshots:
       fdir: 6.5.0(picomatch@4.0.7)
       picomatch: 4.0.7
 
-  tinyrainbow@3.1.1: {}
+  tinyrainbow@3.2.0: {}
 
-  tldts-core@7.4.15: {}
+  tldts-core@7.4.16: {}
 
-  tldts@7.4.15:
+  tldts@7.4.16:
     dependencies:
-      tldts-core: 7.4.15
+      tldts-core: 7.4.16
 
   to-regex-range@5.0.1:
     dependencies:
@@ -8321,7 +8320,7 @@ snapshots:
 
   tough-cookie@6.0.2:
     dependencies:
-      tldts: 7.4.15
+      tldts: 7.4.16
 
   tr46@5.1.1:
     dependencies:
@@ -8350,14 +8349,14 @@ snapshots:
       '@ts-morph/common': 0.28.1
       code-block-writer: 13.0.3
 
-  ts-node@10.9.2(@types/node@24.19.0)(typescript@6.0.3):
+  ts-node@10.9.2(@types/node@24.19.1)(typescript@6.0.3):
     dependencies:
       '@cspotcode/source-map-support': 0.8.1
       '@tsconfig/node10': 1.0.13
       '@tsconfig/node12': 1.0.11
       '@tsconfig/node14': 1.0.3
       '@tsconfig/node16': 1.0.4
-      '@types/node': 24.19.0
+      '@types/node': 24.19.1
       acorn: 8.18.0
       acorn-walk: 8.3.5
       arg: 4.1.3
@@ -8394,13 +8393,13 @@ snapshots:
       media-typer: 1.1.1
       mime-types: 3.0.2
 
-  typescript-eslint@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+  typescript-eslint@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/parser': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/typescript-estree': 8.70.1(supports-color@7.2.0)(typescript@6.0.3)
-      '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/parser': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/typescript-estree': 8.71.0(supports-color@7.2.0)(typescript@6.0.3)
+      '@typescript-eslint/utils': 8.71.0(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       typescript: 6.0.3
     transitivePeerDependencies:
       - supports-color
@@ -8408,7 +8407,7 @@ snapshots:
   typescript-language-server@6.0.1:
     dependencies:
       vscode-jsonrpc: 5.0.1
-      vscode-languageserver-protocol: 3.18.3
+      vscode-languageserver-protocol: 3.18.4
 
   typescript@5.9.3: {}
 
@@ -8416,15 +8415,13 @@ snapshots:
 
   ufo@1.6.4: {}
 
-  undici-types@7.18.2: {}
-
   undici-types@7.24.6: {}
 
-  undici-types@8.11.0: {}
+  undici-types@8.11.2: {}
 
   undici-types@8.9.0: {}
 
-  undici@8.11.0: {}
+  undici@8.11.2: {}
 
   unicorn-magic@0.3.0: {}
 
@@ -8437,19 +8434,19 @@ snapshots:
       pathe: 2.0.3
       picomatch: 4.0.7
 
-  unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.10)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)):
+  unplugin@3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)):
     dependencies:
       '@jridgewell/remapping': 2.3.5
       picomatch: 4.0.7
       webpack-virtual-modules: 0.6.2
     optionalDependencies:
       esbuild: 0.28.2
-      rolldown: 1.2.10
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      rolldown: 1.2.12
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
 
-  update-browserslist-db@1.3.3(browserslist@4.29.0):
+  update-browserslist-db@1.3.3(browserslist@4.29.3):
     dependencies:
-      browserslist: 4.29.0
+      browserslist: 4.29.3
       escalade: 3.2.0
       picocolors: 1.1.1
 
@@ -8468,40 +8465,40 @@ snapshots:
 
   vary@1.1.2: {}
 
-  vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1):
+  vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1):
     dependencies:
       lightningcss: 1.33.0
       picomatch: 4.0.7
       postcss: 8.5.28
-      rolldown: 1.2.10
+      rolldown: 1.2.12
       tinyglobby: 0.2.17
     optionalDependencies:
-      '@types/node': 24.19.0
+      '@types/node': 24.19.1
       esbuild: 0.28.2
       fsevents: 2.3.3
       jiti: 2.6.1
       tsx: 4.23.15
       yaml: 2.9.1
 
-  vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.0)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)):
+  vitest@5.0.3(@opentelemetry/api@1.9.1)(@types/node@24.19.1)(@vitest/coverage-v8@5.0.3)(jsdom@30.1.1)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)):
     dependencies:
       '@types/chai': 5.2.3
-      '@vitest/mocker': 5.0.3(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
-      chai: 6.2.2
+      '@vitest/mocker': 5.0.3(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
+      chai: 6.3.0
       es-module-lexer: 2.3.2
       expect-type: 1.4.0
       magic-string: 1.4.2
       obug: 2.2.1
       picomatch: 4.0.7
-      std-env: 4.2.0
-      tinybench: 6.1.4
+      std-env: 4.3.0
+      tinybench: 6.2.0
       tinyexec: 1.3.1
       tinyglobby: 0.2.17
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
       why-is-node-running: 3.2.1
     optionalDependencies:
       '@opentelemetry/api': 1.9.1
-      '@types/node': 24.19.0
+      '@types/node': 24.19.1
       '@vitest/coverage-v8': 5.0.3(vitest@5.0.3)
       jsdom: 30.1.1
     transitivePeerDependencies:
@@ -8509,25 +8506,25 @@ snapshots:
 
   vscode-jsonrpc@5.0.1: {}
 
-  vscode-jsonrpc@9.0.2: {}
+  vscode-jsonrpc@9.0.3: {}
 
-  vscode-languageserver-protocol@3.18.3:
+  vscode-languageserver-protocol@3.18.4:
     dependencies:
-      vscode-jsonrpc: 9.0.2
-      vscode-languageserver-types: 3.18.3
+      vscode-jsonrpc: 9.0.3
+      vscode-languageserver-types: 3.18.4
 
-  vscode-languageserver-textdocument@1.0.14: {}
+  vscode-languageserver-textdocument@1.0.15: {}
 
-  vscode-languageserver-types@3.18.3: {}
+  vscode-languageserver-types@3.18.4: {}
 
   vscode-uri@3.2.0: {}
 
-  vue-component-type-helpers@3.3.11: {}
+  vue-component-type-helpers@3.3.12: {}
 
-  vue-eslint-parser@10.4.1(eslint@10.11.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
+  vue-eslint-parser@10.4.1(eslint@10.12.0(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
     dependencies:
       debug: 4.4.3(supports-color@7.2.0)
-      eslint: 10.11.0(jiti@2.6.1)(supports-color@7.2.0)
+      eslint: 10.12.0(jiti@2.6.1)(supports-color@7.2.0)
       eslint-scope: 9.1.2
       eslint-visitor-keys: 5.0.1
       espree: 11.2.0
@@ -8536,7 +8533,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  vue-router@5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.10)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3)):
+  vue-router@5.3.1(@vue/compiler-sfc@3.5.43)(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))(vue@3.5.43(typescript@6.0.3)):
     dependencies:
       '@vue-macros/common': 3.1.4(vue@3.5.43(typescript@6.0.3))
       '@vue/devtools-api': 8.2.1
@@ -8552,12 +8549,12 @@ snapshots:
       picomatch: 4.0.7
       scule: 1.3.0
       tinyglobby: 0.2.17
-      unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.10)(vite@8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
+      unplugin: 3.4.0(esbuild@0.28.2)(rolldown@1.2.12)(vite@8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1))
       unplugin-utils: 0.3.2
       vue: 3.5.43(typescript@6.0.3)
     optionalDependencies:
       '@vue/compiler-sfc': 3.5.43
-      vite: 8.3.1(@types/node@24.19.0)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
+      vite: 8.3.2(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.6.1)(tsx@4.23.15)(yaml@2.9.1)
     transitivePeerDependencies:
       - '@farmfe/core'
       - '@rsbuild/core'
@@ -8573,10 +8570,10 @@ snapshots:
     dependencies:
       vue: 3.5.43(typescript@6.0.3)
 
-  vue-tsc@3.3.11(typescript@6.0.3):
+  vue-tsc@3.3.12(typescript@6.0.3):
     dependencies:
       '@volar/typescript': 2.4.28(typescript@6.0.3)
-      '@vue/language-core': 3.3.11
+      '@vue/language-core': 3.3.12
       typescript: 6.0.3
 
   vue@3.5.43(typescript@6.0.3):
@@ -8728,7 +8725,7 @@ snapshots:
       cliui: 9.0.1
       escalade: 3.2.0
       get-caller-file: 2.0.5
-      string-width: 8.2.2
+      string-width: 8.3.0
       y18n: 5.0.8
       yargs-parser: 22.0.0
 
index 92b5fb66389f5d7d649e40850927225c927ddfee..a43335631eb1fc6f97ff13d309fa42a9e3edacce 100644 (file)
@@ -43,7 +43,7 @@ import {
   type HeartbeatRequest,
   type HeartbeatResponse,
   type IncomingRequest,
-  type IncomingRequestCommand,
+  IncomingRequestCommand,
   type JsonType,
   MapStringifyFormat,
   MessageType,
@@ -52,6 +52,7 @@ import {
   OCPP20ReadingContextEnumType,
   OCPP20RequiredVariableName,
   type OCPP20TransactionEventRequest,
+  type OCPPCommandName,
   OCPPVersion,
   type OutgoingRequest,
   PowerUnits,
@@ -98,6 +99,7 @@ import {
   ensureError,
   formatDurationMilliSeconds,
   formatDurationSeconds,
+  getEnumStringValue,
   getErrorMessage,
   getMessageTypeString,
   getWebSocketCloseEventStatusString,
@@ -218,6 +220,12 @@ class TransactionEventQueuePersistenceDiscardedError extends BaseError {}
 class TransactionEventQueuePersistenceSupersededError extends BaseError {}
 
 export class ChargingStation extends EventEmitter {
+  private static readonly normalWebSocketCloseEventStatusCodes: ReadonlySet<number> =
+    new Set<number>([
+      WebSocketCloseEventStatusCode.CLOSE_NO_STATUS,
+      WebSocketCloseEventStatusCode.CLOSE_NORMAL,
+    ])
+
   public automaticTransactionGenerator?: AutomaticTransactionGenerator
   public bootNotificationRequest?: BootNotificationRequest
   public bootNotificationResponse?: BootNotificationResponse
@@ -1437,10 +1445,7 @@ export class ChargingStation extends EventEmitter {
    * @param command - OCPP command the statistic is recorded against.
    * @param messageType - Message type of the recorded exchange.
    */
-  public recordRequestStatistic (
-    command: IncomingRequestCommand | RequestCommand,
-    messageType: MessageType
-  ): void {
+  public recordRequestStatistic (command: OCPPCommandName, messageType: MessageType): void {
     if (this.stationInfo?.enableStatistics === true) {
       this.performanceStatistics?.addRequestStatistic(command, messageType)
     }
@@ -3439,11 +3444,7 @@ export class ChargingStation extends EventEmitter {
     }
   }
 
-  private onClose (
-    wsConnection: WebSocket,
-    code: WebSocketCloseEventStatusCode,
-    reason: Buffer
-  ): void {
+  private onClose (wsConnection: WebSocket, code: number, reason: Buffer): void {
     const closedByRequest = this.wsConnectionsClosedByRequest.delete(wsConnection)
     if (wsConnection !== this.wsConnection) {
       return
@@ -3458,24 +3459,14 @@ export class ChargingStation extends EventEmitter {
     )
     this.emitChargingStationEvent(ChargingStationEvents.disconnected)
     this.emitChargingStationEvent(ChargingStationEvents.updated)
-    switch (code) {
-      // Normal close
-      case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
-      case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
-        logger.info(
-          `${this.logPrefix()} ${moduleName}.onClose: WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
-            code
-          )}' and reason '${reason.toString()}'`
-        )
-        break
-      // Abnormal close
-      default:
-        logger.error(
-          `${this.logPrefix()} ${moduleName}.onClose: WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
-            code
-          )}' and reason '${reason.toString()}'`
-        )
-        break
+    if (ChargingStation.normalWebSocketCloseEventStatusCodes.has(code)) {
+      logger.info(
+        `${this.logPrefix()} ${moduleName}.onClose: WebSocket normally closed with status '${getWebSocketCloseEventStatusString(code)}' and reason '${reason.toString()}'`
+      )
+    } else {
+      logger.error(
+        `${this.logPrefix()} ${moduleName}.onClose: WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(code)}' and reason '${reason.toString()}'`
+      )
     }
     // Reconnect on any close we did not request while still started: a
     // server-initiated drop, or an internal close wanting a fresh connection
@@ -3520,37 +3511,49 @@ export class ChargingStation extends EventEmitter {
       // 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
+        const [parsedMessageType] = request
         if (
           this.isStopping() &&
-          messageType !== MessageType.CALL_RESULT_MESSAGE &&
-          messageType !== MessageType.CALL_ERROR_MESSAGE
+          parsedMessageType !== MessageType.CALL_RESULT_MESSAGE &&
+          parsedMessageType !== MessageType.CALL_ERROR_MESSAGE
         ) {
           return
         }
-        switch (messageType) {
+        switch (parsedMessageType) {
           // Error Message
           case MessageType.CALL_ERROR_MESSAGE:
-            this.handleErrorMessage(request as ErrorResponse)
+            messageType = MessageType.CALL_ERROR_MESSAGE
+            this.handleErrorMessage(request)
             break
           // Incoming Message
-          case MessageType.CALL_MESSAGE:
+          case MessageType.CALL_MESSAGE: {
+            messageType = MessageType.CALL_MESSAGE
             if (!messageLifecycleIsCurrent()) return
+            const commandName = getEnumStringValue(IncomingRequestCommand, request[2])
+            if (commandName == null) {
+              throw new OCPPError(
+                ErrorType.NOT_IMPLEMENTED,
+                `Unknown OCPP command '${request[2]}'`,
+                OCPPConstants.UNKNOWN_OCPP_COMMAND,
+                { command: request[2] }
+              )
+            }
             await this.handleIncomingMessage(
-              request as IncomingRequest,
+              [parsedMessageType, request[1], commandName, request[3]],
               messageLifecycleSignal,
               messageLifecycleIsCurrent,
               messageSourceIsCurrent
             )
             break
+          }
           // Response Message
           case MessageType.CALL_RESULT_MESSAGE:
-            this.handleResponseMessage(request as Response)
+            messageType = MessageType.CALL_RESULT_MESSAGE
+            this.handleResponseMessage(request)
             break
           // Unknown Message
           default:
-            // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
-            errorMsg = `Wrong message type ${messageType}`
+            errorMsg = `Wrong message type ${String(parsedMessageType)}`
             logger.error(`${this.logPrefix()} ${moduleName}.onMessage: ${errorMsg}`)
             throw new OCPPError(
               this.stationInfo?.ocppVersion !== OCPPVersion.VERSION_16
@@ -3603,8 +3606,8 @@ export class ChargingStation extends EventEmitter {
         }
         return
       }
-      let commandName: IncomingRequestCommand | undefined
-      let requestCommandName: IncomingRequestCommand | RequestCommand | undefined
+      let commandName: string | undefined
+      let requestCommandName: string | undefined
       let errorCallback: ErrorCallback
       const [, messageId] = request
       const ocppError =
@@ -3625,11 +3628,13 @@ export class ChargingStation extends EventEmitter {
             this.requests.delete(messageId)
           }
           break
-        case MessageType.CALL_MESSAGE:
+        case MessageType.CALL_MESSAGE: {
           if (!messageLifecycleIsCurrent() || !messageSourceIsCurrent()) return
-          ;[, , commandName] = request as IncomingRequest
-          await this.ocppRequestService.sendError(this, messageId, ocppError, commandName)
+          const [, , rawCommandName] = request
+          commandName = typeof rawCommandName === 'string' ? rawCommandName : undefined
+          await this.ocppRequestService.sendError(this, messageId, ocppError, ocppError.command)
           break
+        }
       }
       if (!(error instanceof OCPPError)) {
         logger.warn(
@@ -4322,8 +4327,8 @@ export class ChargingStation extends EventEmitter {
         onCompleteCallback()
         return
       }
-      if (isRequest) {
-        ;[, messageId, commandName] = parsedMessage as OutgoingRequest
+      if (parsedMessage[0] === MessageType.CALL_MESSAGE) {
+        ;[, messageId, commandName] = parsedMessage
       }
       const bufferedMessageInFlight: {
         entry: BufferedMessageEntry
index cc43f9cc2ef8979cd603220485aeaf8d5bccd7c7..31a6858770f1171b597b6ad04ae16a1c1293231a 100644 (file)
@@ -334,7 +334,7 @@ export const getDefaultVoltageOut = (
   templateFile: string
 ): Voltage => {
   const errorMsg = `Unknown ${currentType} currentOutType in template file ${templateFile}, cannot define default voltage out`
-  let defaultVoltageOut: number
+  let defaultVoltageOut: Voltage
   switch (currentType) {
     case CurrentType.AC:
       defaultVoltageOut = Voltage.VOLTAGE_230
index 421c6dd82168512f586d6e6b47b99b83340e3066..a1dd166883ab0978cc4a43f0503f85fbfc9ab574 100644 (file)
@@ -20,6 +20,8 @@ import {
   GetCertificateStatusEnumType,
   type HeartbeatResponse,
   Iso15118EVCertificateStatusEnumType,
+  isOCPP16MeterValue,
+  isOCPP20MeterValue,
   type MessageEvent,
   type MeterValuesRequest,
   type MeterValuesResponse,
@@ -49,6 +51,7 @@ import {
 import {
   Constants,
   convertToInt,
+  getEnumStringValue,
   getErrorMessage,
   isAsyncFunction,
   isEmpty,
@@ -116,17 +119,18 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
     [
       BroadcastChannelProcedureName.GET_15118_EV_CERTIFICATE,
       r =>
-        (r as OCPP20Get15118EVCertificateResponse).status ===
+        getEnumStringValue(Iso15118EVCertificateStatusEnumType, r.status) ===
         Iso15118EVCertificateStatusEnumType.Accepted,
     ],
     [
       BroadcastChannelProcedureName.GET_CERTIFICATE_STATUS,
       r =>
-        (r as OCPP20GetCertificateStatusResponse).status === GetCertificateStatusEnumType.Accepted,
+        getEnumStringValue(GetCertificateStatusEnumType, r.status) ===
+        GetCertificateStatusEnumType.Accepted,
     ],
     [
       BroadcastChannelProcedureName.SIGN_CERTIFICATE,
-      r => (r as OCPP20SignCertificateResponse).status === GenericStatus.Accepted,
+      r => getEnumStringValue(GenericStatus, r.status) === GenericStatus.Accepted,
     ],
   ])
 
@@ -525,15 +529,21 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
       )
     }
     const requestedPublicKeyIncluded =
-      requestedMeterValues?.some(meterValue =>
-        isOcpp2
-          ? (meterValue as OCPP20MeterValue).sampledValue.some(sampledValue =>
-              isNotEmptyString(getRawSignedMeterValuePublicKey(sampledValue))
-            )
-          : (meterValue as OCPP16MeterValue).sampledValue.some(sampledValue =>
-              isNotEmptyString(getOCPP16SignedMeterValuePublicKey(sampledValue))
-            )
-      ) === true
+      requestedMeterValues?.some(meterValue => {
+        if (isOcpp2 && isOCPP20MeterValue(meterValue)) {
+          return meterValue.sampledValue.some(sampledValue =>
+            isNotEmptyString(getRawSignedMeterValuePublicKey(sampledValue))
+          )
+        }
+        if (!isOcpp2 && isOCPP16MeterValue(meterValue)) {
+          return meterValue.sampledValue.some(sampledValue =>
+            isNotEmptyString(getOCPP16SignedMeterValuePublicKey(sampledValue))
+          )
+        }
+        throw new BaseError(
+          `${this.chargingStation.logPrefix()} ${moduleName}.handleMeterValues: meterValue does not match the station OCPP version`
+        )
+      }) === true
     if (requestedMeterValues == null && connectorStatus?.transactionEnding === true) {
       throw new BaseError(
         `${this.chargingStation.logPrefix()} ${moduleName}.handleMeterValues: Transaction is ending`
@@ -615,14 +625,20 @@ export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChanne
         // SignedData SampledValue when signing is enabled for the connector.
         // OCPP 2.0.x signing is applied inline by the versioned dispatcher.
         if (!isOcpp2 && transactionId != null) {
+          if (!isOCPP16MeterValue(meterValue)) {
+            throw new BaseError('Generated MeterValue does not match OCPP 1.6')
+          }
           publicKeyIncluded = OCPP16ServiceUtils.appendSignedUpdatedReadings(
             this.chargingStation,
             connectorId,
             convertToInt(transactionId),
-            meterValue as OCPP16MeterValue
+            meterValue
           )
         } else if (isOcpp2) {
-          publicKeyIncluded = (meterValue as OCPP20MeterValue).sampledValue.some(sampledValue =>
+          if (!isOCPP20MeterValue(meterValue)) {
+            throw new BaseError('Generated MeterValue does not match OCPP 2.0.x')
+          }
+          publicKeyIncluded = meterValue.sampledValue.some(sampledValue =>
             isNotEmptyString(getRawSignedMeterValuePublicKey(sampledValue))
           )
         }
index efcb3305b79a3ec1fc8dd45d890382cc77c82aff..330bc0a0ef142d94c3b1f0c05e2f811cabc577fa 100644 (file)
@@ -34,6 +34,8 @@ import {
   MeterValuePhase,
   MeterValueUnit,
   OCPP16MeterValueFormat,
+  OCPP16MeterValueMeasurand,
+  OCPP20MeasurandEnumType,
   OCPPVersion,
 } from '../../types/index.js'
 import {
@@ -185,11 +187,17 @@ const energyIntervalFallbackIdentity = (
   JSON.stringify([
     template.phase,
     template.location ?? MeterValueLocation.OUTLET,
-    (template.unit as MeterValueUnit | undefined) ?? MeterValueUnit.WATT_HOUR,
+    ocppVersion === OCPPVersion.VERSION_16
+      ? (template.unit ?? MeterValueUnit.WATT_HOUR)
+      : (template.unitOfMeasure?.unit ?? template.unit ?? MeterValueUnit.WATT_HOUR),
+    ocppVersion === OCPPVersion.VERSION_16 ? 0 : (template.unitOfMeasure?.multiplier ?? 0),
     context ?? template.context ?? MeterValueContext.SAMPLE_PERIODIC,
     ocppVersion === OCPPVersion.VERSION_16
       ? (template.format ?? OCPP16MeterValueFormat.RAW)
-      : canonicalizeCustomData(template.customData),
+      : [
+          canonicalizeCustomData(template.customData),
+          canonicalizeCustomData(template.unitOfMeasure?.customData),
+        ],
   ])
 
 const templateFamilyKey = (
@@ -201,7 +209,9 @@ const templateFamilyKey = (
     customData: template.customData,
     location: template.location ?? MeterValueLocation.OUTLET,
     measurand: template.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
-    unit: (template.unit as MeterValueUnit | undefined) ?? MeterValueUnit.WATT_HOUR,
+    unit: template.unitOfMeasure?.unit ?? template.unit ?? MeterValueUnit.WATT_HOUR,
+    unitCustomData: template.unitOfMeasure?.customData,
+    unitMultiplier: template.unitOfMeasure?.multiplier,
   })
 
 /**
@@ -492,6 +502,10 @@ const serializeCoherentMeterValue = (
   const templates = resolveTemplates(context, connectorId, connectorStatus, evseIdOverride)
   const groups = groupTemplatesByMeasurand(templates)
   if (enabledMeasurands?.has(MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL) === true) {
+    const intervalMeasurand =
+      context.stationInfo?.ocppVersion === OCPPVersion.VERSION_16
+        ? OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL
+        : OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL
     const intervalTemplates = groups.get(MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL) ?? []
     const intervalTemplateIdentities = new Set(
       intervalTemplates.map(template =>
@@ -502,8 +516,8 @@ const serializeCoherentMeterValue = (
       .filter(template => {
         const intervalTemplate = {
           ...template,
-          measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
-        } as SampledValueTemplate
+          measurand: intervalMeasurand,
+        }
         return !intervalTemplateIdentities.has(
           energyIntervalFallbackIdentity(
             intervalTemplate,
@@ -512,13 +526,10 @@ const serializeCoherentMeterValue = (
           )
         )
       })
-      .map(
-        template =>
-          ({
-            ...template,
-            measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
-          }) as SampledValueTemplate
-      )
+      .map(template => ({
+        ...template,
+        measurand: intervalMeasurand,
+      }))
     if (isNotEmptyArray(intervalFallbacks)) {
       groups.set(MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL, [
         ...intervalTemplates,
@@ -580,10 +591,7 @@ const serializeCoherentMeterValue = (
         measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL
           ? projectDcOutputValue(context, currentType, effectiveEvseId, template, raw)
           : raw
-      const unitDivider = resolveMeterValueUnitDivider(
-        measurand,
-        template.unit as MeterValueUnit | undefined
-      )
+      const unitDivider = resolveMeterValueUnitDivider(measurand, template.unit)
       const scaled =
         measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL
           ? truncateTransactionIntervalValue(physicalValue / unitDivider)
index 829bc03368e216d314ea2568053b5c008e5d6fa6..bcf98a0cee688a0248d6d00afbdcb01421114988 100644 (file)
@@ -97,7 +97,7 @@ export const areMeterValueUnitsCompatible = (
  * Resolves the base-to-configured-unit divider for a meter-value measurand.
  * @param measurand - OCPP meter-value measurand.
  * @param unit - Configured output unit.
- * @returns `Constants.UNIT_DIVIDER_KILO` for the family's kilo unit, otherwise `1`.
+ * @returns The named unit's divider from the family's base unit.
  */
 export const resolveMeterValueUnitDivider = (
   measurand: MeterValueMeasurand,
@@ -107,6 +107,26 @@ export const resolveMeterValueUnitDivider = (
   return kiloUnit != null && kiloUnit === unit ? Constants.UNIT_DIVIDER_KILO : 1
 }
 
+/**
+ * Resolves the scale from an emitted meter-value number to its physical base quantity.
+ * The named-unit scale and OCPP 2.0 decimal multiplier are cumulative.
+ * @param measurand - Meter-value measurand used to resolve base/kilo unit families.
+ * @param unit - Emitted unit name.
+ * @param multiplier - OCPP 2.0 decimal multiplier.
+ * @returns Scale from the emitted number to the physical base quantity.
+ */
+export const resolveMeterValueUnitScale = (
+  measurand: MeterValueMeasurand,
+  unit: string | undefined,
+  multiplier = 0
+): number => {
+  const namedUnitScale =
+    unit === MeterValueUnit.KILO_PASCAL
+      ? Constants.UNIT_DIVIDER_KILO
+      : resolveMeterValueUnitDivider(measurand, unit)
+  return namedUnitScale * 10 ** multiplier
+}
+
 /**
  * Resolves an OCPP line or line-to-neutral phase to its one-based line index.
  * @param phase - OCPP meter-value phase.
@@ -137,6 +157,8 @@ export const resolveLinePhaseIndex = (phase: string | undefined): number | undef
  * @param identity.location - Effective measurement location
  * @param identity.measurand - Effective measurand
  * @param identity.unit - Effective unit
+ * @param identity.unitCustomData - Unit descriptor custom data
+ * @param identity.unitMultiplier - Effective decimal multiplier
  * @returns Stable serialized family identity
  */
 export const buildSampledValueFamilyKey = (identity: {
@@ -144,12 +166,16 @@ export const buildSampledValueFamilyKey = (identity: {
   customData: unknown
   location: MeterValueLocation | undefined
   measurand: MeterValueMeasurand
-  unit: MeterValueUnit | undefined
+  unit: string | undefined
+  unitCustomData?: unknown
+  unitMultiplier?: number
 }): string =>
   JSON.stringify([
     identity.context,
     identity.location,
     identity.unit,
+    identity.unitMultiplier ?? 0,
     identity.measurand,
     canonicalizeCustomData(identity.customData),
+    canonicalizeCustomData(identity.unitCustomData),
   ])
index 1280b6faac604d0e56f9143951b4c415ee573cd3..c434355e26f8e4d035d27cce04c3e5216c764478 100644 (file)
@@ -1,5 +1,6 @@
 import { type ConnectorStatus, CurrentType, MeterValueMeasurand } from '../../types/index.js'
-import { Constants, isEmpty } from '../../utils/index.js'
+import { isEmpty } from '../../utils/index.js'
+import { resolveMeterValueUnitScale } from './MeterValueUtils.js'
 
 export interface TransactionIntervalState {
   consumed: number
@@ -112,15 +113,17 @@ export const getRepresentedTransactionIntervalEnergyWh = (
         : Number.parseFloat(sampledValue.value)
     if (!Number.isFinite(value)) continue
     const unit = sampledValue.unitOfMeasure?.unit ?? sampledValue.unit
-    const unitMultiplier =
-      unit === 'kWh' ? Constants.UNIT_DIVIDER_KILO : unit === 'MWh' ? 1_000_000 : 1
-    const decimalMultiplier = 10 ** (sampledValue.unitOfMeasure?.multiplier ?? 0)
+    const unitScale = resolveMeterValueUnitScale(
+      MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+      unit,
+      sampledValue.unitOfMeasure?.multiplier
+    )
     const phaseMultiplier = /^L[123](?:-N)?$/.test(sampledValue.phase ?? '') ? numberOfPhases : 1
     const locationMultiplier =
       sampledValue.location === 'Inlet' && inletToOutputEfficiency > 0 ? inletToOutputEfficiency : 1
     representedEnergyWh = Math.max(
       representedEnergyWh,
-      value * unitMultiplier * decimalMultiplier * phaseMultiplier * locationMultiplier
+      value * unitScale * phaseMultiplier * locationMultiplier
     )
   }
   return representedEnergyWh
index 45904dd5a981a86031861b5609b961b0e97d8f35..9b6d6c58067efcfd222e5f320b276e36dcbe7aa2 100644 (file)
@@ -58,6 +58,7 @@ export {
   type MeterValueUnitFamily,
   resolveLinePhaseIndex,
   resolveMeterValueUnitDivider,
+  resolveMeterValueUnitScale,
 } from './MeterValueUtils.js'
 export {
   captureTransactionIntervalState,
index 9911148460665dd4059585c72343aa18c7289517..53e4d330ef247091c898ee4aa3aed59ad4efbb40 100644 (file)
@@ -36,7 +36,7 @@ import {
   type TransactionMeterValueDelivery,
   TransactionMeterValueDeliveryBarrier,
 } from '../../../charging-station/meter-values/index.js'
-import { OCPPError } from '../../../exception/index.js'
+import { BaseError, OCPPError } from '../../../exception/index.js'
 import {
   type ChangeConfigurationRequest,
   type ChangeConfigurationResponse,
@@ -53,6 +53,7 @@ import {
   type GetDiagnosticsResponse,
   type IncomingRequestCommand,
   type IncomingRequestHandler,
+  isOCPP16ChargingProfile,
   type JsonType,
   type LogConfiguration,
   OCPP16AuthorizationStatus,
@@ -83,7 +84,6 @@ import {
   type OCPP16HeartbeatResponse,
   OCPP16IncomingRequestCommand,
   OCPP16MessageTrigger,
-  type OCPP16MeterValue,
   OCPP16MeterValueContext,
   type OCPP16MeterValuesRequest,
   type OCPP16MeterValuesResponse,
@@ -108,6 +108,7 @@ import {
   type RemoteStopTransactionRequest,
   ReservationTerminationReason,
   type ResetRequest,
+  ResetType,
   type SetChargingProfileRequest,
   type SetChargingProfileResponse,
   type StatusNotificationOptions,
@@ -123,6 +124,7 @@ import {
   convertToIntOrNaN,
   ensureError,
   formatDurationMilliSeconds,
+  getEnumStringValue,
   handleIncomingRequestError,
   isEmpty,
   isNotEmptyArray,
@@ -138,6 +140,7 @@ import { OCPPConstants } from '../OCPPConstants.js'
 import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
 import { isIdTagAuthorized } from '../OCPPServiceOperations.js'
 import {
+  assertMeterValueBuilderResult,
   buildMeterValue,
   createPayloadValidatorMap,
   isConnectorIdValid,
@@ -154,6 +157,38 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils.js'
 
 const moduleName = 'OCPP16IncomingRequestService'
 
+const requireOCPP16ChargePointStatus = (status: string | undefined): OCPP16ChargePointStatus => {
+  const ocpp16Status = getEnumStringValue(OCPP16ChargePointStatus, status)
+  if (ocpp16Status == null) {
+    throw new BaseError(`Invalid OCPP 1.6 charge point status '${String(status)}'`)
+  }
+  return ocpp16Status
+}
+
+const resolveTriggeredStatusNotificationStatus = (
+  chargingStation: ChargingStation,
+  connectorId: number,
+  status: string | undefined
+): OCPP16ChargePointStatus =>
+  requireOCPP16ChargePointStatus(
+    status ??
+      (connectorId === 0
+        ? chargingStation.getConnectorStatus(0)?.availability === OCPP16AvailabilityType.Operative
+          ? OCPP16ChargePointStatus.Available
+          : OCPP16ChargePointStatus.Unavailable
+        : undefined)
+  )
+
+const requireOCPP16ChargingProfiles = (
+  chargingProfiles: ConnectorStatus['chargingProfiles']
+): OCPP16ChargingProfile[] => {
+  const profiles = chargingProfiles ?? []
+  if (!profiles.every(isOCPP16ChargingProfile)) {
+    throw new BaseError('Charging profiles do not match OCPP 1.6')
+  }
+  return profiles
+}
+
 /**
  * Per-station lifecycle state carried on {@link OCPP16IncomingRequestService}.
  *
@@ -752,8 +787,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
                   OCPP16RequestCommand.STATUS_NOTIFICATION,
                   {
                     connectorId,
-                    status: chargingStation.getConnectorStatus(connectorId)
-                      ?.status as OCPP16ChargePointStatus,
+                    status: resolveTriggeredStatusNotificationStatus(
+                      chargingStation,
+                      connectorId,
+                      chargingStation.getConnectorStatus(connectorId)?.status
+                    ),
                   },
                   {
                     triggerMessage: true,
@@ -768,7 +806,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
                     OCPP16RequestCommand.STATUS_NOTIFICATION,
                     {
                       connectorId,
-                      status: connectorStatus.status as OCPP16ChargePointStatus,
+                      status: resolveTriggeredStatusNotificationStatus(
+                        chargingStation,
+                        connectorId,
+                        connectorStatus.status
+                      ),
                     },
                     {
                       triggerMessage: true,
@@ -1119,6 +1161,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
   ): ChangeConfigurationResponse {
     const { key, value } = commandPayload
     const keyToChange = getConfigurationKey(chargingStation, key, true)
+    const standardKey = getEnumStringValue(OCPP16StandardParametersKey, keyToChange?.key)
     if (keyToChange?.readonly === true) {
       return OCPP16Constants.OCPP_CONFIGURATION_RESPONSE_REJECTED
     } else if (keyToChange?.readonly === false) {
@@ -1131,7 +1174,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
         OCPP16StandardParametersKey.TransactionMessageRetryInterval,
         OCPP16StandardParametersKey.WebSocketPingInterval,
       ])
-      if (integerKeys.has(keyToChange.key as OCPP16StandardParametersKey)) {
+      if (standardKey != null && integerKeys.has(standardKey)) {
         // convertToInt would truncate '1.5' → 1 and throw on ''/'abc'; Number() instead yields a non-integer
         // float or NaN that !Number.isInteger rejects, and isNotEmptyString rejects '' (Number('') === 0 would otherwise pass).
         const numValue = Number(value)
@@ -1145,11 +1188,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
         valueChanged = true
       }
       let triggerHeartbeatRestart = false
-      if (
-        (keyToChange.key as OCPP16StandardParametersKey) ===
-          OCPP16StandardParametersKey.HeartBeatInterval &&
-        valueChanged
-      ) {
+      if (standardKey === OCPP16StandardParametersKey.HeartBeatInterval && valueChanged) {
         setConfigurationKeyValue(
           chargingStation,
           OCPP16StandardParametersKey.HeartbeatInterval,
@@ -1157,11 +1196,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
         )
         triggerHeartbeatRestart = true
       }
-      if (
-        (keyToChange.key as OCPP16StandardParametersKey) ===
-          OCPP16StandardParametersKey.HeartbeatInterval &&
-        valueChanged
-      ) {
+      if (standardKey === OCPP16StandardParametersKey.HeartbeatInterval && valueChanged) {
         setConfigurationKeyValue(
           chargingStation,
           OCPP16StandardParametersKey.HeartBeatInterval,
@@ -1172,16 +1207,11 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
       if (triggerHeartbeatRestart) {
         chargingStation.restartHeartbeat()
       }
-      if (
-        (keyToChange.key as OCPP16StandardParametersKey) ===
-          OCPP16StandardParametersKey.WebSocketPingInterval &&
-        valueChanged
-      ) {
+      if (standardKey === OCPP16StandardParametersKey.WebSocketPingInterval && valueChanged) {
         chargingStation.restartWebSocketPing()
       }
       if (
-        (keyToChange.key as OCPP16StandardParametersKey) ===
-          OCPP16StandardParametersKey.MeterValueSampleInterval &&
+        standardKey === OCPP16StandardParametersKey.MeterValueSampleInterval &&
         chargingStation.getNumberOfRunningTransactions() > 0 &&
         valueChanged
       ) {
@@ -1247,7 +1277,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
         const clearedCP = OCPP16ServiceUtils.clearChargingProfiles(
           chargingStation,
           commandPayload,
-          connectorStatus.chargingProfiles as OCPP16ChargingProfile[]
+          requireOCPP16ChargingProfiles(connectorStatus.chargingProfiles)
         )
         if (clearedCP) {
           logger.debug(
@@ -1262,7 +1292,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
         const clearedConnectorCP = OCPP16ServiceUtils.clearChargingProfiles(
           chargingStation,
           commandPayload,
-          connectorStatus.chargingProfiles as OCPP16ChargingProfile[]
+          requireOCPP16ChargingProfiles(connectorStatus.chargingProfiles)
         )
         if (clearedConnectorCP && !clearedCP) {
           clearedCP = true
@@ -1333,10 +1363,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
       const allChargingProfiles: OCPP16ChargingProfile[] = []
       for (const { connectorId: aggregatedConnectorId } of chargingStation.iterateConnectors()) {
         allChargingProfiles.push(
-          ...(getConnectorChargingProfiles(
-            chargingStation,
-            aggregatedConnectorId
-          ) as OCPP16ChargingProfile[])
+          ...getConnectorChargingProfiles(chargingStation, aggregatedConnectorId).filter(
+            isOCPP16ChargingProfile
+          )
         )
       }
       if (isEmpty(allChargingProfiles)) {
@@ -1370,10 +1399,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
     ) {
       return OCPP16Constants.OCPP_RESPONSE_REJECTED
     }
-    const chargingProfiles = getConnectorChargingProfiles(
-      chargingStation,
-      connectorId
-    ) as OCPP16ChargingProfile[]
+    const chargingProfiles = getConnectorChargingProfiles(chargingStation, connectorId).filter(
+      isOCPP16ChargingProfile
+    )
     const compositeSchedule = this.composeCompositeSchedule(
       chargingStation,
       chargingProfiles,
@@ -1888,8 +1916,18 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
     chargingStation: ChargingStation,
     commandPayload: ResetRequest
   ): GenericResponse {
-    const { type } = commandPayload
-    const reason = `${type}Reset` as OCPP16StopTransactionReason
+    const resetType = getEnumStringValue(ResetType, commandPayload.type)
+    if (resetType == null) {
+      throw new OCPPError(
+        ErrorType.FORMAT_VIOLATION,
+        `Invalid Reset type '${commandPayload.type}'`,
+        OCPP16IncomingRequestCommand.RESET
+      )
+    }
+    const reason =
+      resetType === ResetType.HARD
+        ? OCPP16StopTransactionReason.HARD_RESET
+        : OCPP16StopTransactionReason.SOFT_RESET
     const graceful = reason === OCPP16StopTransactionReason.SOFT_RESET
     this.pendingResetActions.set(commandPayload, async lifecycleSignal => {
       if (chargingStation.lifecycleAbortSignal !== lifecycleSignal || lifecycleSignal.aborted) {
@@ -1898,7 +1936,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
       await chargingStation.reset(reason, graceful)
     })
     logger.info(
-      `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: ${type} reset request received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
+      `${chargingStation.logPrefix()} ${moduleName}.handleRequestReset: ${resetType} reset request received, simulating it. The station will be back online in ${formatDurationMilliSeconds(
         chargingStation.stationInfo?.resetTime ?? 0
       )}`
     )
@@ -2153,7 +2191,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
               target.transactionId != null
                 ? captureTransactionIntervalState(target.connectorStatus)
                 : undefined
-            const meterValue = buildMeterValue(
+            const builtMeterValue = buildMeterValue(
               chargingStation,
               target.transactionId,
               0,
@@ -2161,7 +2199,9 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService<OCP
               OCPP16MeterValueContext.TRIGGER,
               false,
               { advanceEnergy: true, connectorId: target.connectorId, snapshot: true }
-            ) as OCPP16MeterValue
+            )
+            assertMeterValueBuilderResult(builtMeterValue, OCPPVersion.VERSION_16)
+            const meterValue = builtMeterValue
             if (!isNotEmptyArray(meterValue.sampledValue)) {
               delivery?.settle(true)
               for (const reservedTarget of targets) reservedTarget.delivery?.settle(true)
index 37b3ef520706a9e3abd426372e3384fc88101764..797ae01f0c4ca11b15aea2a6773bbd12bea55dbe 100644 (file)
@@ -1,18 +1,35 @@
+import { BaseError } from '../../../exception/index.js'
 import {
   type ChargingStationInfo,
   type MeterValueContext,
   type MeterValuePhase,
   type OCPP16BootNotificationRequest,
-  type OCPP16MeterValueContext,
+  OCPP16MeterValueContext,
   OCPP16MeterValueFormat,
   OCPP16MeterValueLocation,
   OCPP16MeterValueMeasurand,
+  OCPP16MeterValuePhase,
+  OCPP16MeterValueUnit,
   type OCPP16SampledValue,
   type OCPP16SignedMeterValue,
   type SampledValueTemplate,
 } from '../../../types/index.js'
+import { getEnumStringValue } from '../../../utils/index.js'
 import { resolveSampledValueFields } from '../OCPPServiceUtils.js'
 
+const resolveOCPP16EnumValue = <T extends string>(
+  enumObject: Readonly<Record<string, T>>,
+  value: string | undefined,
+  fieldName: string,
+  providedValue: string | undefined
+): T | undefined => {
+  const resolvedValue = getEnumStringValue(enumObject, value)
+  if (providedValue != null && resolvedValue == null) {
+    throw new BaseError(`Invalid OCPP 1.6 sampled value ${fieldName} '${providedValue}'`)
+  }
+  return resolvedValue
+}
+
 export const buildOCPP16BootNotificationRequest = (
   stationInfo: ChargingStationInfo
 ): OCPP16BootNotificationRequest => ({
@@ -51,15 +68,48 @@ export function buildOCPP16SampledValue (
   context?: MeterValueContext,
   phase?: MeterValuePhase
 ): OCPP16SampledValue {
+  if (sampledValueTemplate.unitOfMeasure != null) {
+    throw new BaseError('Invalid OCPP 1.6 sampled value unitOfMeasure')
+  }
   const fields = resolveSampledValueFields(sampledValueTemplate, value, context, phase)
+  const resolvedContext = resolveOCPP16EnumValue(
+    OCPP16MeterValueContext,
+    fields.context,
+    'context',
+    context ?? sampledValueTemplate.context
+  )
+  const resolvedLocation = resolveOCPP16EnumValue(
+    OCPP16MeterValueLocation,
+    fields.location,
+    'location',
+    sampledValueTemplate.location
+  )
+  const resolvedMeasurand = resolveOCPP16EnumValue(
+    OCPP16MeterValueMeasurand,
+    fields.measurand,
+    'measurand',
+    sampledValueTemplate.measurand
+  )
+  const resolvedPhase = resolveOCPP16EnumValue(
+    OCPP16MeterValuePhase,
+    fields.phase,
+    'phase',
+    phase ?? sampledValueTemplate.phase
+  )
+  const resolvedUnit = resolveOCPP16EnumValue(
+    OCPP16MeterValueUnit,
+    fields.unit,
+    'unit',
+    sampledValueTemplate.unit
+  )
   return {
-    context: fields.context,
-    location: fields.location,
-    measurand: fields.measurand,
-    ...(fields.unit != null && { unit: fields.unit }),
+    ...(resolvedContext != null && { context: resolvedContext }),
+    ...(resolvedLocation != null && { location: resolvedLocation }),
+    ...(resolvedMeasurand != null && { measurand: resolvedMeasurand }),
+    ...(resolvedUnit != null && { unit: resolvedUnit }),
     value: fields.value.toString(),
-    ...(fields.phase != null && { phase: fields.phase }),
-  } as OCPP16SampledValue
+    ...(resolvedPhase != null && { phase: resolvedPhase }),
+  }
 }
 
 /**
index fc5a49f85b1beb117f71555e9190b377379fd420..5f7025ed9f08e15731d859ddd63a383f563e437d 100644 (file)
@@ -8,8 +8,6 @@ import {
   isWithinInterval,
 } from 'date-fns'
 
-import type { SigningMethodEnumType } from '../../../types/index.js'
-
 import {
   type ChargingStation,
   getConfigurationKey,
@@ -32,6 +30,7 @@ import {
   type ConnectorStatus,
   ErrorType,
   type GenericResponse,
+  type MeterValue,
   type MeterValuesRequest,
   type MeterValuesResponse,
   OCPP16AuthorizationStatus,
@@ -75,6 +74,7 @@ import {
   convertToDate,
   convertToInt,
   ensureError,
+  getEnumStringValue,
   isNotEmptyArray,
   isNotEmptyString,
   logger,
@@ -84,6 +84,7 @@ import {
 import { mapOCPP16Status, OCPPAuthServiceFactory } from '../auth/index.js'
 import { sendAndSetConnectorStatus } from '../OCPPConnectorStatusOperations.js'
 import {
+  assertMeterValueBuilderResult,
   buildEmptyMeterValue,
   buildMeterValue,
   createPayloadConfigs,
@@ -108,6 +109,12 @@ import { OCPP16Constants } from './OCPP16Constants.js'
 import { buildOCPP16SampledValue, buildSignedOCPP16SampledValue } from './OCPP16RequestBuilders.js'
 
 const moduleName = 'OCPP16ServiceUtils'
+
+const requireOCPP16MeterValue = (meterValue: MeterValue): OCPP16MeterValue => {
+  assertMeterValueBuilderResult(meterValue, OCPPVersion.VERSION_16)
+  return meterValue
+}
+
 const RFC3339_TIMESTAMP_PATTERN =
   /^(\d{4})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?([Zz]|([+-])(\d{2}):(\d{2}))$/
 const DAYS_PER_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const
@@ -352,7 +359,7 @@ export class OCPP16ServiceUtils {
         transactionId
       )
     }
-    const meterValue = buildEmptyMeterValue() as OCPP16MeterValue
+    const meterValue = requireOCPP16MeterValue(buildEmptyMeterValue())
     const meterStartOutputWh = meterStart ?? 0
     // Energy.Active.Import.Register measurand (default)
     const sampledValueTemplate = getSampledValueTemplate(chargingStation, connectorId)
@@ -456,7 +463,7 @@ export class OCPP16ServiceUtils {
       sampledValueTemplate.unit === OCPP16MeterValueUnit.KILO_WATT_HOUR
         ? Constants.UNIT_DIVIDER_KILO
         : 1
-    const meterValue = { sampledValue: [], timestamp } as OCPP16MeterValue
+    const meterValue = requireOCPP16MeterValue(buildEmptyMeterValue(timestamp))
     meterValue.sampledValue.push(
       buildOCPP16SampledValue(
         sampledValueTemplate,
@@ -1137,11 +1144,9 @@ export class OCPP16ServiceUtils {
         const sampleAt = Date.now()
         const elapsedInterval = Math.max(elapsedIntervalCarry, sampleAt - lastSampleAt)
         const intervalState = captureTransactionIntervalState(connectorStatus)
-        const meterValue = buildMeterValue(
-          chargingStation,
-          transactionId,
-          elapsedInterval
-        ) as OCPP16MeterValue
+        const meterValue = requireOCPP16MeterValue(
+          buildMeterValue(chargingStation, transactionId, elapsedInterval)
+        )
         lastSampleAt = sampleAt
         elapsedIntervalCarry = 0
         completeTransactionIntervalState(intervalState, 'default', [meterValue])
@@ -1331,6 +1336,18 @@ export class OCPP16ServiceUtils {
       )
     }
 
+    const rawStopReason = reason ?? requestOverrides.reason
+    const validatedStopReason = getEnumStringValue(OCPP16StopTransactionReason, rawStopReason)
+    if (rawStopReason !== undefined && validatedStopReason == null) {
+      return Promise.reject(
+        new OCPPError(
+          ErrorType.FORMAT_VIOLATION,
+          `${moduleName}.stopTransactionOnConnector: Invalid StopTransaction reason '${rawStopReason}'`,
+          RequestCommand.STOP_TRANSACTION
+        )
+      )
+    }
+
     let transactionEndingOwned = true
     const periodicMeterValuesWasRunning =
       connectorStatus.transactionUpdatedMeterValuesSetInterval != null
@@ -1506,7 +1523,7 @@ export class OCPP16ServiceUtils {
             timestamp,
             transactionData,
             transactionId,
-            ...(reason != null && { reason: reason as StopTransactionRequest['reason'] }),
+            ...(validatedStopReason != null && { reason: validatedStopReason }),
           })
           if (
             strictEndMeterValueIsSolePublicKeyCarrier &&
@@ -1998,13 +2015,15 @@ export class OCPP16ServiceUtils {
       getConfigurationKey(chargingStation, startTxnSampledDataKey)?.value != null
         ? startTxnSampledDataKey
         : undefined
-    return buildMeterValue(
-      chargingStation,
-      transactionId,
-      0,
-      measurandsKey,
-      OCPP16MeterValueContext.TRANSACTION_BEGIN
-    ) as OCPP16MeterValue
+    return requireOCPP16MeterValue(
+      buildMeterValue(
+        chargingStation,
+        transactionId,
+        0,
+        measurandsKey,
+        OCPP16MeterValueContext.TRANSACTION_BEGIN
+      )
+    )
   }
 
   private static buildSignedSampledValue (
@@ -2058,23 +2077,25 @@ export class OCPP16ServiceUtils {
       chargingStation.getConnectorStatus(connectorId)?.transactionEnergyActiveImportIntervalCarry
         ?.default ?? 0
     if (intervalEnergyWh <= 0) return
-    const intervalMeterValue = buildMeterValue(
-      chargingStation,
-      transactionId,
-      0,
-      OCPP16StandardParametersKey.StopTxnSampledData,
-      OCPP16MeterValueContext.TRANSACTION_END,
-      false,
-      {
-        advanceEnergy: false,
-        connectorId,
-        energyIntervalWhOverride: intervalEnergyWh,
-        energyRegisterWhOverride: meterStop,
-        projectDcForLocation: true,
-        snapshot: true,
-        timestamp: meterValue.timestamp,
-      }
-    ) as OCPP16MeterValue
+    const intervalMeterValue = requireOCPP16MeterValue(
+      buildMeterValue(
+        chargingStation,
+        transactionId,
+        0,
+        OCPP16StandardParametersKey.StopTxnSampledData,
+        OCPP16MeterValueContext.TRANSACTION_END,
+        false,
+        {
+          advanceEnergy: false,
+          connectorId,
+          energyIntervalWhOverride: intervalEnergyWh,
+          energyRegisterWhOverride: meterStop,
+          projectDcForLocation: true,
+          snapshot: true,
+          timestamp: meterValue.timestamp,
+        }
+      )
+    )
     const intervalSamples = intervalMeterValue.sampledValue.filter(
       sampledValue =>
         sampledValue.measurand === OCPP16MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL
@@ -2237,7 +2258,7 @@ export class OCPP16ServiceUtils {
     const configuredSigningMethod = getConfigurationKey(
       chargingStation,
       OCPP16VendorParametersKey.SigningMethod
-    )?.value as SigningMethodEnumType | undefined
+    )?.value
 
     const prerequisiteResult = validateSigningPrerequisites(publicKeyHex, configuredSigningMethod)
     if (!prerequisiteResult.enabled) {
index 4b0e3310bd0609a7cd2d7be4afebd4ea3d66baad..94f141f5614af425faadacd6d507d1179dbdffd2 100644 (file)
@@ -20,6 +20,7 @@ import {
 import {
   atomicWriteFile,
   convertToDate,
+  getEnumStringValue,
   getErrorMessage,
   isEmpty,
   isNotEmptyArray,
@@ -288,10 +289,14 @@ export class OCPP20CertificateManager {
         .map(dirent => dirent.name)
 
       for (const certType of certTypes) {
-        if (filterTypes != null && isNotEmptyArray(filterTypes)) {
-          if (!filterTypes.includes(certType as InstallCertificateUseEnumType)) {
-            continue
-          }
+        const certificateType = getEnumStringValue(InstallCertificateUseEnumType, certType)
+        if (certificateType == null) continue
+        if (
+          filterTypes != null &&
+          isNotEmptyArray(filterTypes) &&
+          !filterTypes.includes(certificateType)
+        ) {
+          continue
         }
 
         const certTypeDir = join(basePath, certType)
@@ -307,9 +312,7 @@ export class OCPP20CertificateManager {
 
             certificateHashDataChain.push({
               certificateHashData: hashData,
-              certificateType: this.mapInstallTypeToGetType(
-                certType as InstallCertificateUseEnumType
-              ),
+              certificateType: this.mapInstallTypeToGetType(certificateType),
             })
           } catch {
             // Skip unreadable or unparsable certificate file
index b6fde07f9c08d1548b01ef2f3ddf654eae7d8ca1..b5c77d4289097abbbcea244ca168130c9caa6004 100644 (file)
@@ -1,18 +1,29 @@
 import type { StopTransactionReason } from '../../../types/index.js'
 
+import { BaseError } from '../../../exception/index.js'
 import {
   BootReasonEnumType,
   type ChargingStationInfo,
   type MeterValueContext,
   type MeterValuePhase,
+  MeterValueUnit,
   OCPP16StopTransactionReason,
   type OCPP20BootNotificationRequest,
+  OCPP20LocationEnumType,
   OCPP20MeasurandEnumType,
+  OCPP20PhaseEnumType,
+  OCPP20ReadingContextEnumType,
   OCPP20ReasonEnumType,
   type OCPP20SampledValue,
   OCPP20TriggerReasonEnumType,
   type SampledValueTemplate,
 } from '../../../types/index.js'
+import { convertToFloat, getEnumStringValue } from '../../../utils/index.js'
+import {
+  areMeterValueUnitsCompatible,
+  getMeterValueUnitFamily,
+  resolveMeterValueUnitScale,
+} from '../../meter-values/index.js'
 import { resolveSampledValueFields } from '../OCPPServiceUtils.js'
 import {
   generateSignedMeterData,
@@ -24,6 +35,19 @@ import {
   type SignedSampledValueResult,
 } from '../OCPPSignedMeterValueUtils.js'
 
+const requireOCPP20EnumValue = <T extends string>(
+  enumObject: Readonly<Record<string, T>>,
+  value: string | undefined,
+  fieldName: string
+): T | undefined => {
+  if (value == null) return undefined
+  const resolvedValue = getEnumStringValue(enumObject, value)
+  if (resolvedValue == null) {
+    throw new BaseError(`Invalid OCPP 2.0.x sampled value ${fieldName} '${value}'`)
+  }
+  return resolvedValue
+}
+
 export const buildOCPP20BootNotificationRequest = (
   stationInfo: ChargingStationInfo,
   bootReason: BootReasonEnumType = BootReasonEnumType.PowerUp
@@ -50,7 +74,7 @@ export const buildOCPP20BootNotificationRequest = (
 /**
  * Builds an OCPP 2.0.1 sampled value from a template and measurement data.
  * @param sampledValueTemplate - The sampled value template to use.
- * @param value - The measured value.
+ * @param value - The measured value expressed in the template's flat/default unit.
  * @param context - The reading context.
  * @param phase - The phase of the measurement.
  * @param signingConfig - Optional signing configuration for generating signedMeterValue.
@@ -64,17 +88,61 @@ export function buildOCPP20SampledValue (
   signingConfig?: SampledValueSigningConfig
 ): SignedSampledValueResult<OCPP20SampledValue> {
   const fields = resolveSampledValueFields(sampledValueTemplate, value, context, phase)
+  const resolvedContext = requireOCPP20EnumValue(
+    OCPP20ReadingContextEnumType,
+    fields.context,
+    'context'
+  )
+  const resolvedLocation = requireOCPP20EnumValue(
+    OCPP20LocationEnumType,
+    fields.location,
+    'location'
+  )
+  const resolvedMeasurand = requireOCPP20EnumValue(
+    OCPP20MeasurandEnumType,
+    fields.measurand,
+    'measurand'
+  )
+  const resolvedPhase = requireOCPP20EnumValue(OCPP20PhaseEnumType, fields.phase, 'phase')
+  const sourceUnit = fields.unit
+  const emittedUnit = sampledValueTemplate.unitOfMeasure?.unit ?? sourceUnit
+  if (
+    sourceUnit != null &&
+    emittedUnit != null &&
+    sourceUnit !== emittedUnit &&
+    !areMeterValueUnitsCompatible(fields.measurand, sourceUnit, emittedUnit)
+  ) {
+    throw new BaseError(
+      `Cannot convert OCPP 2.0.x sampled value unit '${sourceUnit}' to '${emittedUnit}'`
+    )
+  }
+  const unitOfMeasure =
+    sampledValueTemplate.unitOfMeasure != null || sourceUnit != null
+      ? {
+          ...sampledValueTemplate.unitOfMeasure,
+          ...(sampledValueTemplate.unitOfMeasure?.unit == null &&
+            sourceUnit != null && { unit: sourceUnit }),
+        }
+      : undefined
+  const emittedScale = resolveMeterValueUnitScale(
+    fields.measurand,
+    emittedUnit,
+    unitOfMeasure?.multiplier
+  )
+  const emittedValue =
+    (convertToFloat(fields.value) * resolveMeterValueUnitScale(fields.measurand, sourceUnit)) /
+    emittedScale
   const sampledValue: OCPP20SampledValue = {
     ...(sampledValueTemplate.customData != null && {
       customData: sampledValueTemplate.customData,
     }),
-    context: fields.context,
-    location: fields.location,
-    measurand: fields.measurand,
-    ...(fields.unit != null && { unitOfMeasure: { unit: fields.unit } }),
-    value: fields.value,
-    ...(fields.phase != null && { phase: fields.phase }),
-  } as OCPP20SampledValue
+    ...(resolvedContext != null && { context: resolvedContext }),
+    ...(resolvedLocation != null && { location: resolvedLocation }),
+    ...(resolvedMeasurand != null && { measurand: resolvedMeasurand }),
+    ...(unitOfMeasure != null && { unitOfMeasure }),
+    value: emittedValue,
+    ...(resolvedPhase != null && { phase: resolvedPhase }),
+  }
 
   let publicKeyIncluded = false
 
@@ -86,11 +154,12 @@ export function buildOCPP20SampledValue (
       signingConfig.publicKeyWithSignedMeterValue,
       signingConfig.publicKeySentInTransaction
     )
+    const unitFamily = getMeterValueUnitFamily(fields.measurand, emittedUnit)
     const signedMeterDataParams: SignedMeterDataParams = {
       context: fields.context,
       meterSerialNumber: signingConfig.meterSerialNumber,
-      meterValue: fields.value,
-      meterValueUnit: fields.unit,
+      meterValue: emittedValue * emittedScale,
+      meterValueUnit: getEnumStringValue(MeterValueUnit, unitFamily?.baseUnit ?? emittedUnit),
       timestamp: signingConfig.timestamp ?? new Date(),
       transactionId: signingConfig.transactionId,
     }
index e2bdb63d9619a9bda93e06f00af37a8186cee669..08c1afbbfc6d2f968a493a1a1a96074cd59e8457 100644 (file)
@@ -38,6 +38,7 @@ import {
   recordPendingSharedEnergy,
   resolveInletToOutputEfficiency,
   resolveLinePhaseIndex,
+  resolveMeterValueUnitScale,
   resolveRootSeed,
   type TransactionMeterValueDelivery,
   TransactionMeterValueDeliveryBarrier,
@@ -52,6 +53,7 @@ import {
   type ConnectorStatusEnum,
   CurrentType,
   ErrorType,
+  isOCPP20MeterValue,
   type MeterValue,
   MeterValueLocation,
   OCPP20AuthorizationStatusEnumType,
@@ -124,6 +126,7 @@ import {
 } from '../auth/index.js'
 import { sendPostTransactionStatus } from '../OCPPConnectorStatusOperations.js'
 import {
+  assertMeterValueBuilderResult,
   buildClockAlignedConnectorMeterValue,
   buildMeterValue,
   createPayloadConfigs,
@@ -740,9 +743,11 @@ const normalizeClockAlignedAdditiveSample = (
   unitFamily: MeterValueUnitFamily
 ): OCPP20SampledValue => {
   const configuredUnit = sampledValue.unitOfMeasure?.unit
-  const namedUnitMultiplier =
-    configuredUnit === unitFamily.kiloUnit ? Constants.UNIT_DIVIDER_KILO : 1
-  const decimalMultiplier = 10 ** (sampledValue.unitOfMeasure?.multiplier ?? 0)
+  const unitScale = resolveMeterValueUnitScale(
+    sampledValue.measurand ?? OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
+    configuredUnit,
+    sampledValue.unitOfMeasure?.multiplier
+  )
   return {
     ...sampledValue,
     unitOfMeasure: {
@@ -750,7 +755,7 @@ const normalizeClockAlignedAdditiveSample = (
       multiplier: 0,
       unit: unitFamily.baseUnit,
     },
-    value: sampledValue.value * namedUnitMultiplier * decimalMultiplier,
+    value: sampledValue.value * unitScale,
   }
 }
 
@@ -1545,7 +1550,8 @@ export class OCPP20ServiceUtils {
           energyRegisterWhOverride: sharedEnergyRegisterWh,
         }),
       }
-    ) as OCPP20MeterValue
+    )
+    assertMeterValueBuilderResult(meterValue, OCPPVersion.VERSION_201)
     const sharedObservationEnergyWh =
       usesSharedEvseRegister &&
       context !== OCPP20ReadingContextEnumType.TRANSACTION_BEGIN &&
@@ -4615,10 +4621,14 @@ export class OCPP20ServiceUtils {
     evseId?: number
   ): OCPP20MeterValue[] {
     const connectorStatus = chargingStation.getConnectorStatus(connectorId, evseId)
-    const endedMeterValues = (connectorStatus?.transactionEndedMeterValues ??
-      []) as OCPP20MeterValue[]
-    const beginMeterValue = connectorStatus?.transactionBeginMeterValue as
-      OCPP20MeterValue | undefined
+    const endedMeterValues = connectorStatus?.transactionEndedMeterValues ?? []
+    if (!endedMeterValues.every(isOCPP20MeterValue)) {
+      throw new BaseError('Stored transaction-ended MeterValues do not match OCPP 2.0.x')
+    }
+    const beginMeterValue = connectorStatus?.transactionBeginMeterValue
+    if (beginMeterValue != null && !isOCPP20MeterValue(beginMeterValue)) {
+      throw new BaseError('Stored transaction-begin MeterValue does not match OCPP 2.0.x')
+    }
     const measurandsKey = buildConfigKey(
       OCPP20ComponentName.SampledDataCtrlr,
       OCPP20RequiredVariableName.TxEndedMeasurands
index f9d2a85ac2f4d5113f1cd222583003efd696ae0d..012da23bc8c333ec97891ff649a07c78f8b73ea8 100644 (file)
@@ -17,7 +17,13 @@ import {
   SetVariableStatusEnumType,
   type VariableType,
 } from '../../../types/index.js'
-import { Constants, convertToIntOrNaN, isEmpty, logger } from '../../../utils/index.js'
+import {
+  Constants,
+  convertToIntOrNaN,
+  getEnumStringValue,
+  isEmpty,
+  logger,
+} from '../../../utils/index.js'
 import {
   addConfigurationKey,
   buildConfigKey,
@@ -42,10 +48,10 @@ import {
 } from './OCPP20VariableRegistry.js'
 
 const isOCPP20ComponentName = (name: string): name is OCPP20ComponentName => {
-  return Object.values(OCPP20ComponentName).includes(name as OCPP20ComponentName)
+  return getEnumStringValue(OCPP20ComponentName, name) != null
 }
 const isOCPP20RequiredVariableName = (name: string): name is OCPP20RequiredVariableName => {
-  return Object.values(OCPP20RequiredVariableName).includes(name as OCPP20RequiredVariableName)
+  return getEnumStringValue(OCPP20RequiredVariableName, name) != null
 }
 
 const computeConfigurationKeyName = (variableMetadata: VariableMetadata): string =>
index 8d8568e074f68caa717ab1c5a3e7515399565b90..4e5c8d4625c933bbc35503fcb12392af94b26dcb 100644 (file)
@@ -5,11 +5,11 @@ import {
   ConfigurationStatus,
   DataTransferStatus,
   GenericStatus,
-  type IncomingRequestCommand,
   MeterValueMeasurand,
-  type RequestCommand,
+  type OCPPCommandName,
   ReservationStatus,
   TriggerMessageStatus,
+  UNKNOWN_OCPP_COMMAND,
   UnlockStatus,
 } from '../../types/index.js'
 import { Constants } from '../../utils/index.js'
@@ -161,8 +161,7 @@ export class OCPPConstants {
   })
 
   static readonly OCPP_WEBSOCKET_TIMEOUT_MS = 60_000
-  static readonly UNKNOWN_OCPP_COMMAND = 'unknown OCPP command' as
-    IncomingRequestCommand | RequestCommand
+  static readonly UNKNOWN_OCPP_COMMAND: OCPPCommandName = UNKNOWN_OCPP_COMMAND
 
   protected constructor () {
     // This is intentional
index 5a098b47adb1599ebd1bb3d247586121b916ec0e..923bffc27657fcd1e15a747caff176203419f4c1 100644 (file)
@@ -10,9 +10,10 @@ import {
   type ErrorCallback,
   type ErrorResponse,
   ErrorType,
-  type IncomingRequestCommand,
+  IncomingRequestCommand,
   type JsonType,
   MessageType,
+  type OCPPCommandName,
   type OCPPVersion,
   type OutgoingRequest,
   type PendingRequestCancellationCallback,
@@ -28,6 +29,7 @@ import {
   ensureError,
   formatDurationMilliSeconds,
   generateUUID,
+  getEnumStringValue,
   getErrorMessage,
   getMessageTypeString,
   handleSendMessageError,
@@ -42,6 +44,17 @@ import {
   validatePayload,
 } from './OCPPServiceUtils.js'
 
+const requireOCPPCommand = <T extends IncomingRequestCommand | RequestCommand>(
+  commands: Readonly<Record<string, T>>,
+  commandName: string
+): T => {
+  const command = getEnumStringValue(commands, commandName)
+  if (command == null) {
+    throw new OCPPError(ErrorType.NOT_SUPPORTED, `Unknown OCPP command '${commandName}'`)
+  }
+  return command
+}
+
 interface OutgoingCallCancellationState {
   readonly destructiveError: OCPPError | undefined
   readonly destructiveGeneration: number
@@ -410,7 +423,7 @@ export abstract class OCPPRequestService {
     chargingStation: ChargingStation,
     messageId: string,
     ocppError: OCPPError,
-    commandName: IncomingRequestCommand | RequestCommand
+    commandName: OCPPCommandName
   ): Promise<ResponseType> {
     try {
       return await this.internalSendMessage(
@@ -477,11 +490,12 @@ export abstract class OCPPRequestService {
     payload: T,
     { forceValidation = false }: { forceValidation?: boolean } = {}
   ): boolean {
+    const requestCommand = getEnumStringValue(RequestCommand, commandName)
     return validatePayload(
       chargingStation,
       commandName,
       payload,
-      this.payloadValidatorFunctions.get(commandName as RequestCommand),
+      requestCommand == null ? undefined : this.payloadValidatorFunctions.get(requestCommand),
       'request',
       true,
       forceValidation
@@ -565,13 +579,16 @@ export abstract class OCPPRequestService {
     commandName: IncomingRequestCommand | RequestCommand,
     payload: T
   ): boolean {
+    const incomingRequestCommand = getEnumStringValue(IncomingRequestCommand, commandName)
     return validatePayload(
       chargingStation,
       commandName,
       payload,
-      this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
-        commandName as IncomingRequestCommand
-      ),
+      incomingRequestCommand == null
+        ? undefined
+        : this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get(
+          incomingRequestCommand
+        ),
       'incoming request response',
       true
     )
@@ -582,7 +599,7 @@ export abstract class OCPPRequestService {
     messageId: string,
     messagePayload: JsonType | OCPPError,
     messageType: MessageType,
-    commandName: IncomingRequestCommand | RequestCommand
+    commandName: OCPPCommandName
   ): string {
     let messageToSend: string
     // Type of message
@@ -606,20 +623,22 @@ export abstract class OCPPRequestService {
         break
       }
       // Request
-      case MessageType.CALL_MESSAGE:
-        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType)
+      case MessageType.CALL_MESSAGE: {
+        const requestCommand = requireOCPPCommand(RequestCommand, commandName)
+        this.validateRequestPayload(chargingStation, requestCommand, messagePayload as JsonType)
         messageToSend = JSON.stringify([
           messageType,
           messageId,
-          commandName as RequestCommand,
+          requestCommand,
           messagePayload as JsonType,
         ] satisfies OutgoingRequest)
         break
+      }
       // Response
       case MessageType.CALL_RESULT_MESSAGE:
         this.validateIncomingRequestResponsePayload(
           chargingStation,
-          commandName,
+          requireOCPPCommand(IncomingRequestCommand, commandName),
           messagePayload as JsonType
         )
         messageToSend = JSON.stringify([
@@ -664,7 +683,7 @@ export abstract class OCPPRequestService {
     messageId: string,
     messagePayload: JsonType | OCPPError,
     messageType: MessageType,
-    commandName: IncomingRequestCommand | RequestCommand,
+    commandName: OCPPCommandName,
     params: RequestParams,
     responseTimeoutMs: number
   ): PendingSendOperation {
@@ -752,7 +771,12 @@ export abstract class OCPPRequestService {
         // reject an already-answered request while its handler is still running.
         chargingStation.requests.delete(messageId)
         this.ocppResponseService
-          .responseHandler(chargingStation, commandName as RequestCommand, payload, requestPayload)
+          .responseHandler(
+            chargingStation,
+            requireOCPPCommand(RequestCommand, commandName),
+            payload,
+            requestPayload
+          )
           .then(() => {
             resolve(payload)
             return undefined
@@ -911,7 +935,7 @@ export abstract class OCPPRequestService {
               chargingStation,
               messageId,
               messagePayload as JsonType,
-              commandName,
+              requireOCPPCommand(RequestCommand, commandName),
               responseCallback,
               errorCallback,
               cancelPendingSend,
@@ -1046,7 +1070,7 @@ export abstract class OCPPRequestService {
             chargingStation,
             messageId,
             messagePayload as JsonType,
-            commandName,
+            requireOCPPCommand(RequestCommand, commandName),
             responseCallback,
             errorCallback,
             cancelPendingSend,
@@ -1105,7 +1129,7 @@ export abstract class OCPPRequestService {
                     chargingStation,
                     messageId,
                     messagePayload as JsonType,
-                    commandName,
+                    requireOCPPCommand(RequestCommand, commandName),
                     responseCallback,
                     errorCallback,
                     cancelPendingSend,
@@ -1203,7 +1227,7 @@ export abstract class OCPPRequestService {
     messageId: string,
     messagePayload: JsonType | OCPPError,
     messageType: MessageType,
-    commandName: IncomingRequestCommand | RequestCommand,
+    commandName: OCPPCommandName,
     params?: RequestParams,
     cancellationGenerationAtRequestStart?: number
   ): Promise<ResponseType> {
@@ -1219,7 +1243,7 @@ export abstract class OCPPRequestService {
       ((chargingStation.inUnknownState() ||
         chargingStation.inPendingState() ||
         chargingStation.inRejectedState()) &&
-        commandName === RequestCommand.BOOT_NOTIFICATION) ||
+        getEnumStringValue(RequestCommand, commandName) === RequestCommand.BOOT_NOTIFICATION) ||
       (chargingStation.stationInfo?.ocppStrictCompliance === false &&
         chargingStation.inUnknownState()) ||
       chargingStation.inAcceptedState() ||
index b7e3977cc21ea71449fbace0aca6a925b2448680..d20cdd3d190632c6a7324f0f9d369e03737031d4 100644 (file)
@@ -10,7 +10,6 @@ import type {
   BootReasonEnumType,
   OCPP20RequiredVariableName,
   OCPP20VendorVariableName,
-  SigningMethodEnumType,
 } from '../../types/index.js'
 
 import {
@@ -31,6 +30,7 @@ import {
   resolveInletToOutputEfficiency,
   resolveLinePhaseIndex,
   resolveMeterValueUnitDivider,
+  resolveMeterValueUnitScale,
   resolveRootSeed,
   truncateTransactionIntervalValue,
 } from '../../charging-station/meter-values/index.js'
@@ -41,9 +41,12 @@ import {
   type ConfigurationKeyType,
   type ConnectorStatus,
   CurrentType,
+  type CustomDataType,
   ErrorType,
   FileType,
   IncomingRequestCommand,
+  isOCPP16SampledValue,
+  isOCPP20SampledValue,
   type JsonType,
   type MeasurandPerPhaseSampledValueTemplates,
   type MeasurandValues,
@@ -54,13 +57,19 @@ import {
   MeterValueMeasurand,
   MeterValuePhase,
   MeterValueUnit,
+  type OCPP16MeterValue,
   OCPP16MeterValueFormat,
+  type OCPP16MeterValueUnit,
   OCPP20ComponentName,
+  OCPP20LocationEnumType,
+  OCPP20MeasurandEnumType,
   type OCPP20MeterValue,
   OCPP20OptionalVariableName,
   OCPP20PhaseEnumType,
   OCPP20ReadingContextEnumType,
   type OCPP20SampledValue,
+  OCPP20UnitEnumType,
+  type OCPP20UnitOfMeasure,
   OCPPVersion,
   RequestCommand,
   type SampledValue,
@@ -76,6 +85,7 @@ import {
   convertToFloat,
   convertToInt,
   DCElectricUtils,
+  getEnumStringValue,
   getRandomFloatFluctuatedRounded,
   getRandomFloatRounded,
   handleFileException,
@@ -108,6 +118,63 @@ import {
 
 const moduleName = 'OCPPServiceUtils'
 
+const LINE_PHASES = [MeterValuePhase.L1, MeterValuePhase.L2, MeterValuePhase.L3] as const
+const LINE_TO_LINE_PHASES = [
+  MeterValuePhase.L1_L2,
+  MeterValuePhase.L2_L3,
+  MeterValuePhase.L3_L1,
+] as const
+const LINE_TO_NEUTRAL_PHASES = [
+  MeterValuePhase.L1_N,
+  MeterValuePhase.L2_N,
+  MeterValuePhase.L3_N,
+] as const
+
+const toSnapshotBaselineSample = (sampledValue: SampledValue): OCPP20SampledValue => {
+  if (isOCPP20SampledValue(sampledValue)) return sampledValue
+  const context = getEnumStringValue(OCPP20ReadingContextEnumType, sampledValue.context)
+  const location = getEnumStringValue(OCPP20LocationEnumType, sampledValue.location)
+  const measurand = getEnumStringValue(OCPP20MeasurandEnumType, sampledValue.measurand)
+  const phase = getEnumStringValue(OCPP20PhaseEnumType, sampledValue.phase)
+  const unit = getEnumStringValue(OCPP20UnitEnumType, sampledValue.unit)
+  return {
+    ...(context != null && { context }),
+    ...(location != null && { location }),
+    ...(measurand != null && { measurand }),
+    ...(phase != null && { phase }),
+    ...(unit != null && { unitOfMeasure: { unit } }),
+    value: convertToFloat(sampledValue.value),
+  }
+}
+
+export function assertMeterValueBuilderResult (
+  meterValue: MeterValue,
+  version: OCPPVersion.VERSION_16
+): asserts meterValue is OCPP16MeterValue
+export function assertMeterValueBuilderResult (
+  meterValue: MeterValue,
+  version: OCPPVersion.VERSION_20 | OCPPVersion.VERSION_201
+): asserts meterValue is OCPP20MeterValue
+/**
+ * Validates the protocol representation of an internal MeterValue builder result.
+ * Empty sampled-value arrays are valid omission signals inside builders.
+ * @param meterValue - Internal builder result to validate.
+ * @param version - OCPP version expected by the caller.
+ * @throws {BaseError} When a non-empty result contains a sample from another OCPP version.
+ */
+export function assertMeterValueBuilderResult (meterValue: MeterValue, version: OCPPVersion): void {
+  const matchesVersion =
+    meterValue.sampledValue.length === 0 ||
+    (version === OCPPVersion.VERSION_16
+      ? meterValue.sampledValue.every(isOCPP16SampledValue)
+      : meterValue.sampledValue.every(isOCPP20SampledValue))
+  if (!matchesVersion) {
+    throw new BaseError(
+      `MeterValue builder returned sampled values that do not match OCPP ${version}`
+    )
+  }
+}
+
 const isOCPP20FlagEnabled = (
   chargingStation: ChargingStation,
   component: OCPP20ComponentName,
@@ -1006,12 +1073,13 @@ const buildCurrentMeasurandValue = (
 }
 
 /**
- * Builds an empty MeterValue with no sampled values and the current timestamp.
+ * Builds an empty MeterValue with no sampled values.
+ * @param timestamp - Meter value timestamp; defaults to the current time
  * @returns Empty MeterValue object
  */
-export const buildEmptyMeterValue = (): MeterValue => ({
+export const buildEmptyMeterValue = (timestamp = new Date()): MeterValue => ({
   sampledValue: [],
-  timestamp: new Date(),
+  timestamp,
 })
 
 /**
@@ -1162,7 +1230,7 @@ const createVersionedSampledValueDispatcher = (
             const configuredSigningMethod = getConfigurationKey(
               chargingStation,
               buildConfigKey(OCPP20ComponentName.FiscalMetering, VendorParametersKey.SigningMethod)
-            )?.value as SigningMethodEnumType | undefined
+            )?.value
 
             const prerequisiteResult = validateSigningPrerequisites(
               publicKeyHex,
@@ -1246,8 +1314,6 @@ const createVersionedSampledValueDispatcher = (
 // abort semantics, no `resetStationState` hook — a diagnostic side-effect
 // cache freed only by GC.
 const warnedInvalidMeasurands = new WeakMap<ChargingStation, Set<string>>()
-const KNOWN_MEASURANDS: ReadonlySet<string> = new Set<string>(Object.values(MeterValueMeasurand))
-
 const getOrCreateWarnedMeasurands = (chargingStation: ChargingStation): Set<string> => {
   let warned = warnedInvalidMeasurands.get(chargingStation)
   if (warned == null) {
@@ -1302,8 +1368,9 @@ const resolveEnabledMeasurands = (
     if (isEmpty(trimmed)) {
       continue
     }
-    if (KNOWN_MEASURANDS.has(trimmed)) {
-      enabled.add(trimmed as MeterValueMeasurand)
+    const measurand = getEnumStringValue(MeterValueMeasurand, trimmed)
+    if (measurand != null) {
+      enabled.add(measurand)
       continue
     }
     const warned = getOrCreateWarnedMeasurands(chargingStation)
@@ -1338,11 +1405,17 @@ const getEnergyTemplateIdentity = (
   return JSON.stringify([
     resolved.phase,
     resolved.location,
-    resolved.unit,
+    ocppVersion === OCPPVersion.VERSION_16
+      ? resolved.unit
+      : (template.unitOfMeasure?.unit ?? resolved.unit),
+    ocppVersion === OCPPVersion.VERSION_16 ? 0 : (template.unitOfMeasure?.multiplier ?? 0),
     resolved.context,
     ocppVersion === OCPPVersion.VERSION_16
       ? (template.format ?? OCPP16MeterValueFormat.RAW)
-      : canonicalizeCustomData(template.customData),
+      : [
+          canonicalizeCustomData(template.customData),
+          canonicalizeCustomData(template.unitOfMeasure?.customData),
+        ],
   ])
 }
 
@@ -1369,7 +1442,7 @@ const addEnergyIntervalTemplateFallbacks = (
       {
         ...template,
         measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
-      } as SampledValueTemplate,
+      },
     ]
   })
 }
@@ -1492,7 +1565,7 @@ const buildEnergyIntervalSampledValues = (
           physicalValue /
             resolveMeterValueUnitDivider(
               MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
-              template.unit as string | undefined
+              template.unit
             )
         ),
         context,
@@ -1526,7 +1599,9 @@ const applySnapshotRegisterValuesWithoutPhases = (
       customData: template.customData,
       location: identity.location,
       measurand: identity.measurand,
-      unit: identity.unit,
+      unit: template.unitOfMeasure?.unit ?? identity.unit,
+      unitCustomData: template.unitOfMeasure?.customData,
+      unitMultiplier: template.unitOfMeasure?.multiplier,
     })
     const family = families.get(key) ?? []
     family.push(template)
@@ -1592,7 +1667,7 @@ const expandClockAlignedSnapshotSamples = (
   ).map((template): SampledValueTemplate => {
     const measurand = template.measurand ?? MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
     return evseId === 0 && template.location == null && isElectricalMeasurand(measurand)
-      ? ({ ...template, location: MeterValueLocation.INLET } as SampledValueTemplate)
+      ? { ...template, location: MeterValueLocation.INLET }
       : template
   })
   const templatesWithIntervalFallback =
@@ -1608,14 +1683,18 @@ const expandClockAlignedSnapshotSamples = (
     const templatesByIdentity = new Map<string, SampledValueTemplate>()
     for (const template of templatesWithIntervalFallback) {
       const identity = resolveSampledValueFields(template, 0, context, template.phase)
+      const isOcpp16 = chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_16
       const key = JSON.stringify([
-        identity.measurand,
         identity.phase,
-        identity.location,
-        identity.unit,
-        chargingStation.stationInfo?.ocppVersion === OCPPVersion.VERSION_16
-          ? template.format
-          : canonicalizeCustomData(template.customData),
+        buildSampledValueFamilyKey({
+          context: identity.context,
+          customData: isOcpp16 ? template.format : template.customData,
+          location: identity.location,
+          measurand: identity.measurand,
+          unit: isOcpp16 ? identity.unit : (template.unitOfMeasure?.unit ?? identity.unit),
+          unitCustomData: isOcpp16 ? undefined : template.unitOfMeasure?.customData,
+          unitMultiplier: isOcpp16 ? undefined : template.unitOfMeasure?.multiplier,
+        }),
       ])
       if (!templatesByIdentity.has(key)) templatesByIdentity.set(key, template)
     }
@@ -1711,7 +1790,12 @@ const expandClockAlignedSnapshotSamples = (
         ? [...phasedPowerByLine.values()].reduce(
             (total, sample) =>
               total +
-              sample.value * resolveMeterValueUnitDivider(measurand, sample.unitOfMeasure?.unit),
+              sample.value *
+                resolveMeterValueUnitScale(
+                  measurand,
+                  sample.unitOfMeasure?.unit,
+                  sample.unitOfMeasure?.multiplier
+                ),
             0
           )
         : undefined
@@ -1721,7 +1805,12 @@ const expandClockAlignedSnapshotSamples = (
     } else if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
       if (evseId !== 0 && source != null && preferBaseline) {
         rawValue =
-          source.value * resolveMeterValueUnitDivider(measurand, source.unitOfMeasure?.unit)
+          source.value *
+          resolveMeterValueUnitScale(
+            measurand,
+            source.unitOfMeasure?.unit,
+            source.unitOfMeasure?.multiplier
+          )
       } else if (phaseFamily === 'Aggregate') {
         rawValue = energyRegister
       } else if (phaseFamily === 'Line') {
@@ -1749,7 +1838,13 @@ const expandClockAlignedSnapshotSamples = (
     ) {
       continue
     } else if (source != null && preferBaseline) {
-      rawValue = source.value * resolveMeterValueUnitDivider(measurand, source.unitOfMeasure?.unit)
+      rawValue =
+        source.value *
+        resolveMeterValueUnitScale(
+          measurand,
+          source.unitOfMeasure?.unit,
+          source.unitOfMeasure?.multiplier
+        )
       if (
         (measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT ||
           measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL) &&
@@ -1782,7 +1877,7 @@ const expandClockAlignedSnapshotSamples = (
       if (idle) {
         rawValue = 0
       } else {
-        const divider = resolveMeterValueUnitDivider(measurand, template.unit as string | undefined)
+        const divider = resolveMeterValueUnitDivider(measurand, template.unit)
         const maximumValue =
           measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT
             ? chargingStation.getConnectorMaximumAvailablePower(connectorId, evseId) /
@@ -1822,7 +1917,13 @@ const expandClockAlignedSnapshotSamples = (
     ) {
       rawValue = randomInt(template.minimumValue ?? 0, Constants.SOC_MAXIMUM_PERCENT + 1)
     } else if (source != null) {
-      rawValue = source.value * resolveMeterValueUnitDivider(measurand, source.unitOfMeasure?.unit)
+      rawValue =
+        source.value *
+        resolveMeterValueUnitScale(
+          measurand,
+          source.unitOfMeasure?.unit,
+          source.unitOfMeasure?.multiplier
+        )
       if (
         (measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT ||
           measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL) &&
@@ -1836,7 +1937,7 @@ const expandClockAlignedSnapshotSamples = (
         getRandomFloatFluctuatedRounded(
           convertToFloat(template.value),
           template.fluctuationPercent ?? Constants.DEFAULT_FLUCTUATION_PERCENT
-        ) * resolveMeterValueUnitDivider(measurand, template.unit as string | undefined)
+        ) * resolveMeterValueUnitDivider(measurand, template.unit)
     } else if (measurand === MeterValueMeasurand.VOLTAGE) {
       const nominal =
         phaseFamily === 'LineToLine'
@@ -1857,8 +1958,7 @@ const expandClockAlignedSnapshotSamples = (
       rawValue,
       preferBaseline
     )
-    const unitValue =
-      physicalValue / resolveMeterValueUnitDivider(measurand, template.unit as string | undefined)
+    const unitValue = physicalValue / resolveMeterValueUnitDivider(measurand, template.unit)
     const value =
       measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL
         ? truncateTransactionIntervalValue(unitValue)
@@ -1867,18 +1967,29 @@ const expandClockAlignedSnapshotSamples = (
   }
   return applyClockAlignedVoltageControls(
     chargingStation,
-    expanded as OCPP20SampledValue[],
+    expanded,
     buildVersionedSampledValue,
     context
   )
 }
 
+const getSampledValueUnit = (
+  sampledValue: SampledValue
+): NonNullable<OCPP20UnitOfMeasure['unit']> | OCPP16MeterValueUnit | undefined =>
+  isOCPP20SampledValue(sampledValue) ? sampledValue.unitOfMeasure?.unit : sampledValue.unit
+
+const getSampledValueCustomData = (sampledValue: SampledValue): CustomDataType | undefined =>
+  isOCPP20SampledValue(sampledValue) ? sampledValue.customData : undefined
+
+const getSampledValueFormat = (sampledValue: SampledValue): OCPP16MeterValueFormat | undefined =>
+  isOCPP20SampledValue(sampledValue) ? undefined : sampledValue.format
+
 const applyClockAlignedVoltageControls = (
   chargingStation: ChargingStation,
-  sampledValues: OCPP20SampledValue[],
+  sampledValues: SampledValue[],
   buildVersionedSampledValue: BuildVersionedSampledValue,
   context: MeterValueContext | undefined
-): OCPP20SampledValue[] => {
+): SampledValue[] => {
   if (
     chargingStation.getNumberOfPhases() !== 3 ||
     chargingStation.stationInfo?.currentOutType !== CurrentType.AC
@@ -1892,45 +2003,60 @@ const applyClockAlignedVoltageControls = (
   const configuredPhaseSamples = sampledValues.filter(
     sample => sample.measurand === MeterValueMeasurand.VOLTAGE && sample.phase != null
   )
-  const automaticSamples: OCPP20SampledValue[] = []
+  const automaticSamples: SampledValue[] = []
   for (const aggregateVoltage of aggregateVoltages) {
     const configuredPhases = new Set(
       configuredPhaseSamples
         .filter(
           sample =>
             sample.context === aggregateVoltage.context &&
-            canonicalizeCustomData(sample.customData) ===
-              canonicalizeCustomData(aggregateVoltage.customData) &&
-            sample.format === aggregateVoltage.format &&
+            canonicalizeCustomData(getSampledValueCustomData(sample)) ===
+              canonicalizeCustomData(getSampledValueCustomData(aggregateVoltage)) &&
+            getSampledValueFormat(sample) === getSampledValueFormat(aggregateVoltage) &&
             sample.location === aggregateVoltage.location &&
-            sample.unitOfMeasure?.unit === aggregateVoltage.unitOfMeasure?.unit
+            getSampledValueUnit(sample) === getSampledValueUnit(aggregateVoltage)
         )
         .map(sample => sample.phase)
     )
-    const template = {
-      ...aggregateVoltage,
-      unit: aggregateVoltage.unitOfMeasure?.unit,
-    } as SampledValueTemplate
-    const addPhase = (phase: OCPP20PhaseEnumType, value: number): void => {
+    const template: SampledValueTemplate = {
+      ...(isOCPP20SampledValue(aggregateVoltage)
+        ? {
+            customData: aggregateVoltage.customData,
+            unit: getSampledValueUnit(aggregateVoltage),
+            unitOfMeasure: aggregateVoltage.unitOfMeasure,
+          }
+        : { format: aggregateVoltage.format, unit: aggregateVoltage.unit }),
+      context: aggregateVoltage.context,
+      location: aggregateVoltage.location,
+      measurand: aggregateVoltage.measurand,
+      phase: aggregateVoltage.phase,
+    }
+    const addPhase = (phase: MeterValuePhase, value: number): void => {
       if (!configuredPhases.has(phase)) {
         automaticSamples.push(
-          buildVersionedSampledValue(
-            template,
-            roundTo(value, 2),
-            context,
-            phase
-          ) as OCPP20SampledValue
+          buildVersionedSampledValue(template, roundTo(value, 2), context, phase)
         )
       }
     }
-    addPhase(OCPP20PhaseEnumType.L1_N, aggregateVoltage.value)
-    addPhase(OCPP20PhaseEnumType.L2_N, aggregateVoltage.value)
-    addPhase(OCPP20PhaseEnumType.L3_N, aggregateVoltage.value)
+    const aggregateValue =
+      convertToFloat(aggregateVoltage.value) *
+      resolveMeterValueUnitScale(
+        MeterValueMeasurand.VOLTAGE,
+        isOCPP20SampledValue(aggregateVoltage)
+          ? aggregateVoltage.unitOfMeasure?.unit
+          : aggregateVoltage.unit,
+        isOCPP20SampledValue(aggregateVoltage)
+          ? aggregateVoltage.unitOfMeasure?.multiplier
+          : undefined
+      )
+    addPhase(MeterValuePhase.L1_N, aggregateValue)
+    addPhase(MeterValuePhase.L2_N, aggregateValue)
+    addPhase(MeterValuePhase.L3_N, aggregateValue)
     if (chargingStation.stationInfo.phaseLineToLineVoltageMeterValues === true) {
-      const lineToLineVoltage = aggregateVoltage.value * Math.sqrt(3)
-      addPhase(OCPP20PhaseEnumType.L1_L2, lineToLineVoltage)
-      addPhase(OCPP20PhaseEnumType.L2_L3, lineToLineVoltage)
-      addPhase(OCPP20PhaseEnumType.L3_L1, lineToLineVoltage)
+      const lineToLineVoltage = aggregateValue * Math.sqrt(3)
+      addPhase(MeterValuePhase.L1_L2, lineToLineVoltage)
+      addPhase(MeterValuePhase.L2_L3, lineToLineVoltage)
+      addPhase(MeterValuePhase.L3_L1, lineToLineVoltage)
     }
   }
   const retainedSamples =
@@ -2050,7 +2176,7 @@ export const buildClockAlignedConnectorMeterValue = (
       RequestCommand.METER_VALUES
     )
   }
-  return buildIdentifiedMeterValue(
+  const meterValue = buildIdentifiedMeterValue(
     chargingStation,
     { ...identity, commitState: identity.commitState ?? true, snapshot: true },
     interval,
@@ -2058,7 +2184,9 @@ export const buildClockAlignedConnectorMeterValue = (
     context,
     false,
     OCPP20ComponentName.AlignedDataCtrlr
-  ) as OCPP20MeterValue
+  )
+  assertMeterValueBuilderResult(meterValue, OCPPVersion.VERSION_201)
+  return meterValue
 }
 
 const buildIdentifiedMeterValue = (
@@ -2106,24 +2234,31 @@ const buildIdentifiedMeterValue = (
   if (snapshot && identity.sampledValueBaseline != null) {
     const timestamp = identity.timestamp ?? new Date()
     if (signingConfig != null) signingConfig.timestamp = timestamp
-    return {
-      sampledValue: expandClockAlignedSnapshotSamples(
-        chargingStation,
-        connectorId,
-        evseId,
-        identity.sampledValueBaseline,
-        buildSignedVersionedSampledValue,
-        measurandsKey,
-        context,
-        registerValuesWithoutPhases,
-        identity.idle ?? true,
-        identity.transactionId != null,
-        identity.energyRegisterWhOverride,
-        true,
-        identity.sampledValueTemplates
-      ),
-      timestamp,
-    } as OCPP20MeterValue
+    const sampledValue = expandClockAlignedSnapshotSamples(
+      chargingStation,
+      connectorId,
+      evseId,
+      identity.sampledValueBaseline,
+      buildSignedVersionedSampledValue,
+      measurandsKey,
+      context,
+      registerValuesWithoutPhases,
+      identity.idle ?? true,
+      identity.transactionId != null,
+      identity.energyRegisterWhOverride,
+      true,
+      identity.sampledValueTemplates
+    )
+    if (isOCPP20x(chargingStation.stationInfo?.ocppVersion)) {
+      if (!sampledValue.every(isOCPP20SampledValue)) {
+        throw new BaseError('Clock-aligned snapshot contains OCPP 1.6 SampledValues for OCPP 2.0.x')
+      }
+      return { sampledValue, timestamp }
+    }
+    if (!sampledValue.every(isOCPP16SampledValue)) {
+      throw new BaseError('Clock-aligned snapshot contains OCPP 2.0.x SampledValues for OCPP 1.6')
+    }
+    return { sampledValue, timestamp }
   }
   const connectorStatus = chargingStation.getConnectorStatus(connectorId, evseId)
   const transactionBegin = context === MeterValueContext.TRANSACTION_BEGIN
@@ -2177,13 +2312,23 @@ const buildIdentifiedMeterValue = (
         coherentMeterValue.timestamp
     }
     if (snapshot) {
-      const coherentOcpp20MeterValue = coherentMeterValue as OCPP20MeterValue
-      coherentOcpp20MeterValue.sampledValue = applyClockAlignedVoltageControls(
+      const version = chargingStation.stationInfo?.ocppVersion
+      const controlledSampledValues = applyClockAlignedVoltageControls(
         chargingStation,
-        coherentOcpp20MeterValue.sampledValue,
+        coherentMeterValue.sampledValue,
         buildSignedVersionedSampledValue,
         context
       )
+      const controlledMeterValue = {
+        ...coherentMeterValue,
+        sampledValue: controlledSampledValues,
+      } as MeterValue
+      if (version === OCPPVersion.VERSION_16) {
+        assertMeterValueBuilderResult(controlledMeterValue, OCPPVersion.VERSION_16)
+      } else {
+        assertMeterValueBuilderResult(controlledMeterValue, OCPPVersion.VERSION_201)
+      }
+      coherentMeterValue.sampledValue = controlledMeterValue.sampledValue
     }
     // Only transactional builds may flip the one-time public-key flag. A
     // coherent session always carries a transactionId, but guard explicitly to
@@ -2262,7 +2407,7 @@ const buildIdentifiedMeterValue = (
         connectorId,
         meterValue,
         voltageMeasurand,
-        `L${phase.toString()}-N` as MeterValuePhase,
+        LINE_TO_NEUTRAL_PHASES[phase - 1],
         chargingStation.getVoltageOut(),
         buildVersionedSampledValue,
         measurandsKey,
@@ -2272,11 +2417,7 @@ const buildIdentifiedMeterValue = (
         snapshot
       )
       if (chargingStation.stationInfo?.phaseLineToLineVoltageMeterValues === true) {
-        const nextPhase =
-          (phase + 1) % chargingStation.getNumberOfPhases() !== 0
-            ? ((phase + 1) % chargingStation.getNumberOfPhases()).toString()
-            : chargingStation.getNumberOfPhases().toString()
-        const lineToLineLabel = `L${phase.toString()}-L${nextPhase}` as MeterValuePhase
+        const lineToLineLabel = LINE_TO_LINE_PHASES[phase - 1]
         // `V_LL = sqrt(3) * V_LN` in a balanced 3-phase Y system; the
         // sqrt(3) factor comes from the 30-degree phase separation, not
         // from the phase count itself. Emitting L-L values makes physical
@@ -2347,7 +2488,7 @@ const buildIdentifiedMeterValue = (
             `L${phase.toString()}` as keyof MeasurandPerPhaseSampledValueTemplates
           ]
         if (phaseTemplate != null) {
-          const phaseValue = `L${phase.toString()}-N` as MeterValuePhase
+          const phaseValue = LINE_TO_NEUTRAL_PHASES[phase - 1]
           const phasePowerValue =
             powerMeasurand.values[`L${phase.toString()}` as keyof MeasurandValues]
           meterValue.sampledValue.push(
@@ -2415,7 +2556,7 @@ const buildIdentifiedMeterValue = (
       chargingStation.getNumberOfPhases() === 3 && phase <= chargingStation.getNumberOfPhases();
       phase++
     ) {
-      const phaseValue = `L${phase.toString()}` as MeterValuePhase
+      const phaseValue = LINE_PHASES[phase - 1]
       meterValue.sampledValue.push(
         buildVersionedSampledValue(
           currentMeasurand.perPhaseTemplates[
@@ -2658,7 +2799,7 @@ const buildIdentifiedMeterValue = (
       chargingStation,
       connectorId,
       evseId,
-      meterValue.sampledValue as OCPP20SampledValue[],
+      meterValue.sampledValue.map(toSnapshotBaselineSample),
       buildSignedVersionedSampledValue,
       measurandsKey,
       context,
@@ -2892,7 +3033,7 @@ export const resolveSampledValueFields = (
   location: MeterValueLocation | undefined
   measurand: MeterValueMeasurand
   phase: MeterValuePhase | undefined
-  unit: MeterValueUnit | undefined
+  unit: string | undefined
   value: number
 } => {
   const sampledValueMeasurand =
@@ -2902,9 +3043,7 @@ export const resolveSampledValueFields = (
     location: sampledValueTemplate.location ?? getMeasurandDefaultLocation(sampledValueMeasurand),
     measurand: sampledValueMeasurand,
     phase: phase ?? sampledValueTemplate.phase,
-    unit:
-      (sampledValueTemplate.unit as MeterValueUnit | undefined) ??
-      getMeasurandDefaultUnit(sampledValueMeasurand),
+    unit: sampledValueTemplate.unit ?? getMeasurandDefaultUnit(sampledValueMeasurand),
     value,
   }
 }
index 318537dcc1d6d82a11b18577d91d821f971e1b07..17815104fecd98edb19c9df0a1fa3d72dd5bf071 100644 (file)
@@ -9,6 +9,7 @@ import {
   type SampledValue,
   SigningMethodEnumType,
 } from '../../types/index.js'
+import { getEnumStringValue } from '../../utils/index.js'
 import { getErrorMessage, isJsonObject, isNotEmptyString, logger } from '../../utils/index.js'
 
 export const getOCPP16SignedMeterValuePublicKey = (
@@ -146,8 +147,16 @@ export interface SigningPrerequisiteSuccess {
 
 export const validateSigningPrerequisites = (
   publicKeyHex: string | undefined,
-  configuredSigningMethod: SigningMethodEnumType | undefined
+  configuredSigningMethod: string | undefined
 ): SigningPrerequisiteResult | SigningPrerequisiteSuccess => {
+  const signingMethod = getEnumStringValue(SigningMethodEnumType, configuredSigningMethod)
+  if (configuredSigningMethod !== undefined && signingMethod == null) {
+    return {
+      enabled: false,
+      reason: `SigningMethod '${configuredSigningMethod}' is invalid`,
+    }
+  }
+
   if (!isNotEmptyString(publicKeyHex)) {
     return { enabled: false, reason: 'Public key is not configured' }
   }
@@ -161,28 +170,23 @@ export const validateSigningPrerequisites = (
     }
   }
 
-  if (configuredSigningMethod != null && configuredSigningMethod !== derivedMethod) {
+  if (signingMethod != null && signingMethod !== derivedMethod) {
     return {
       enabled: false,
       reason:
-        `SigningMethod mismatch: configured '${configuredSigningMethod}' ` +
+        `SigningMethod mismatch: configured '${signingMethod}' ` +
         `but public key uses '${derivedMethod}'`,
     }
   }
 
-  return { enabled: true, signingMethod: configuredSigningMethod ?? derivedMethod }
+  return { enabled: true, signingMethod: signingMethod ?? derivedMethod }
 }
 
-const PUBLIC_KEY_WITH_SIGNED_METER_VALUE_VALUES = new Set<string>(
-  Object.values(PublicKeyWithSignedMeterValueEnumType)
-)
-
 export const parsePublicKeyWithSignedMeterValue = (
   value: string | undefined
 ): PublicKeyWithSignedMeterValueEnumType =>
-  value != null && PUBLIC_KEY_WITH_SIGNED_METER_VALUE_VALUES.has(value)
-    ? (value as PublicKeyWithSignedMeterValueEnumType)
-    : PublicKeyWithSignedMeterValueEnumType.Never
+  getEnumStringValue(PublicKeyWithSignedMeterValueEnumType, value) ??
+  PublicKeyWithSignedMeterValueEnumType.Never
 
 export const shouldIncludePublicKey = (
   config: PublicKeyWithSignedMeterValueEnumType,
index 3ec2ea9e235c7bb83022f243e74710614530d9da..d232bc29c933142e9c233294afa2560ce959dc71 100644 (file)
@@ -9,17 +9,23 @@ import { BaseError } from '../../exception/index.js'
 import {
   ApplicationProtocolVersion,
   MapStringifyFormat,
-  type ProcedureName,
-  type Protocol,
+  ProcedureName,
+  Protocol,
   type ProtocolRequest,
   type ProtocolResponse,
-  type ProtocolVersion,
+  ProtocolVersion,
   type RequestPayload,
   ResponseStatus,
   type UIServerConfiguration,
   type UUIDv4,
 } from '../../types/index.js'
-import { generateUUID, getErrorMessage, JSONStringify, logger } from '../../utils/index.js'
+import {
+  generateUUID,
+  getEnumStringValue,
+  getErrorMessage,
+  JSONStringify,
+  logger,
+} from '../../utils/index.js'
 import { AbstractUIServer } from './AbstractUIServer.js'
 import {
   DEFAULT_COMPRESSION_THRESHOLD_BYTES,
@@ -208,11 +214,17 @@ export class UIHttpServer extends AbstractUIServer {
           `Malformed URL path: '${pathname}' (expected /ui/:version/:procedureName)`
         )
       }
-      const [protocol, version, procedureName] = parts as [Protocol, ProtocolVersion, ProcedureName]
-      const fullProtocol = `${protocol}${version}`
-      if (!isProtocolAndVersionSupported(fullProtocol)) {
+      const [rawProtocol, rawVersion, rawProcedureName] = parts
+      const protocol = getEnumStringValue(Protocol, rawProtocol)
+      const version = getEnumStringValue(ProtocolVersion, rawVersion)
+      const procedureName = getEnumStringValue(ProcedureName, rawProcedureName)
+      const fullProtocol = `${rawProtocol}${rawVersion}`
+      if (protocol == null || version == null || !isProtocolAndVersionSupported(fullProtocol)) {
         throw new BaseError(`Unsupported UI protocol version: '${fullProtocol}'`)
       }
+      if (procedureName == null) {
+        throw new BaseError(`Unsupported UI procedure: '${rawProcedureName}'`)
+      }
       this.registerProtocolVersionUIService(version)
 
       req.on('error', error => {
@@ -251,7 +263,12 @@ export class UIHttpServer extends AbstractUIServer {
         `${this.logPrefix(moduleName, 'requestListener')} Handle HTTP request error:`,
         error
       )
-      this.sendResponse(this.buildProtocolResponse(uuid, { status: ResponseStatus.FAILURE }))
+      this.sendResponse(
+        this.buildProtocolResponse(uuid, {
+          errorMessage: getErrorMessage(error),
+          status: ResponseStatus.FAILURE,
+        })
+      )
     }
   }
 
index 52ec53a144e4673e79d588e3efe543eae3f7fe8a..d237b6afd6448dd5a31c30847b86e74b9bc1ebfb 100644 (file)
@@ -1,7 +1,7 @@
 import type { IncomingMessage } from 'node:http'
 
 import { Protocol, ProtocolVersion } from '../../types/index.js'
-import { isEmpty, logger, logPrefix } from '../../utils/index.js'
+import { getEnumStringValue, isEmpty, logger, logPrefix } from '../../utils/index.js'
 
 export enum HttpMethod {
   DELETE = 'DELETE',
@@ -57,10 +57,7 @@ export const isProtocolAndVersionSupported = (protocolStr: string): boolean => {
   if (protocolAndVersion == null) {
     return false
   }
-  const [protocol, version] = protocolAndVersion
-  return (
-    Object.values(Protocol).includes(protocol) && Object.values(ProtocolVersion).includes(version)
-  )
+  return true
 }
 
 export const getProtocolAndVersion = (
@@ -73,10 +70,16 @@ export const getProtocolAndVersion = (
     return undefined
   }
   const protocolIndex = protocolStr.indexOf(Protocol.UI)
-  const protocol = protocolStr.substring(protocolIndex, protocolIndex + Protocol.UI.length)
-  const version = protocolStr.substring(protocolIndex + Protocol.UI.length)
-  if (isEmpty(protocol) || isEmpty(version)) {
+  const protocol = getEnumStringValue(
+    Protocol,
+    protocolStr.substring(protocolIndex, protocolIndex + Protocol.UI.length)
+  )
+  const version = getEnumStringValue(
+    ProtocolVersion,
+    protocolStr.substring(protocolIndex + Protocol.UI.length)
+  )
+  if (protocol == null || version == null) {
     return undefined
   }
-  return [protocol, version] as [Protocol, ProtocolVersion]
+  return [protocol, version]
 }
index 4f5477aa37ae60f283d3e7a93f9a55cecb9fdc0b..75f92b06091a214d6ca3bd60b06bcad71ef49b0b 100644 (file)
@@ -1,6 +1,6 @@
 // Partial Copyright Jerome Benoit. 2021-2025. All Rights Reserved.
 
-import type { ErrorType, IncomingRequestCommand, JsonType, RequestCommand } from '../types/index.js'
+import type { ErrorType, JsonType, OCPPCommandName } from '../types/index.js'
 
 // Direct path: the `ocpp/index.js` barrel pulls OCPP20ServiceUtils which transitively imports OCPPError, causing a TDZ cycle.
 import { OCPPConstants } from '../charging-station/ocpp/OCPPConstants.js'
@@ -8,16 +8,11 @@ import { BaseError } from './BaseError.js'
 
 export class OCPPError extends BaseError {
   code: ErrorType
-  command: IncomingRequestCommand | RequestCommand
+  command: OCPPCommandName
   details?: JsonType
   public override readonly name = 'OCPPError' as const
 
-  constructor (
-    code: ErrorType,
-    message: string,
-    command?: IncomingRequestCommand | RequestCommand,
-    details?: JsonType
-  ) {
+  constructor (code: ErrorType, message: string, command?: OCPPCommandName, details?: JsonType) {
     super(message)
 
     this.code = code
index d19aebb3c9222dbfd3e04d9634bd11ca811cee3e..325f9384f5996209c4322da19da26b3bf9d171b5 100644 (file)
@@ -10,11 +10,9 @@ import { parentPort } from 'node:worker_threads'
 import { BaseError } from '../exception/index.js'
 import {
   ConfigurationSection,
-  type IncomingRequestCommand,
   type LogConfiguration,
   MapStringifyFormat,
   MessageType,
-  type RequestCommand,
   type Statistics,
   type StatisticsData,
   type StorageConfiguration,
@@ -127,10 +125,7 @@ export class PerformanceStatistics {
     return logPrefix(' Performance statistics')
   }
 
-  public addRequestStatistic (
-    command: IncomingRequestCommand | RequestCommand,
-    messageType: MessageType
-  ): void {
+  public addRequestStatistic (command: string, messageType: MessageType): void {
     switch (messageType) {
       case MessageType.CALL_ERROR_MESSAGE: {
         const commandStatisticsData = this.statistics.statisticsData.get(command)
@@ -282,7 +277,7 @@ export class PerformanceStatistics {
       ...this.statistics,
       statisticsData: JSON.parse(
         JSONStringify(this.statistics.statisticsData, undefined, MapStringifyFormat.object)
-      ) as Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>,
+      ) as Map<string, StatisticsData>,
     })
   }
 
index 7c262192315de9e1f4e7fbe4b7050cc27f1141cd..5b4c627fe716a2b78e68551e2fd2ab5a04d39449 100644 (file)
@@ -1,4 +1,17 @@
-import type { SampledValue } from './ocpp/MeterValues.js'
+import type { JsonObject } from './JsonType.js'
+import type { OCPP16MeterValueFormat, OCPP16SampledValue } from './ocpp/1.6/MeterValues.js'
+import type { CustomDataType } from './ocpp/2.0/Common.js'
+import type {
+  OCPP20SampledValue,
+  OCPP20SignedMeterValue,
+  OCPP20UnitOfMeasure,
+} from './ocpp/2.0/MeterValues.js'
+import type {
+  MeterValueContext,
+  MeterValueLocation,
+  MeterValueMeasurand,
+  MeterValuePhase,
+} from './ocpp/MeterValues.js'
 
 export interface MeasurandPerPhaseSampledValueTemplates {
   L1?: SampledValueTemplate
@@ -6,7 +19,17 @@ export interface MeasurandPerPhaseSampledValueTemplates {
   L3?: SampledValueTemplate
 }
 
-export type SampledValueTemplate = SampledValue & {
+export interface SampledValueTemplate extends JsonObject {
+  context?: MeterValueContext
+  customData?: CustomDataType
   fluctuationPercent?: number
+  format?: OCPP16MeterValueFormat
+  location?: MeterValueLocation
+  measurand?: MeterValueMeasurand
   minimumValue?: number
+  phase?: MeterValuePhase
+  signedMeterValue?: OCPP20SignedMeterValue
+  unit?: NonNullable<OCPP16SampledValue['unit'] | OCPP20UnitOfMeasure['unit']>
+  unitOfMeasure?: OCPP20UnitOfMeasure
+  value?: OCPP16SampledValue['value'] | OCPP20SampledValue['value']
 }
index fc979871c244e3382af9eebb6edc6071d5ef0d70..f4dffa055434d2d0c32122fd2bbe0eb0d8b7064e 100644 (file)
@@ -1,13 +1,12 @@
 import type { CircularBuffer } from 'mnemonist'
 
 import type { WorkerData } from '../worker/index.js'
-import type { IncomingRequestCommand, RequestCommand } from './ocpp/Requests.js'
 
 export interface Statistics extends WorkerData {
   createdAt: Date
   id: string
   name: string
-  statisticsData: Map<IncomingRequestCommand | RequestCommand | string, StatisticsData>
+  statisticsData: Map<string, StatisticsData>
   updatedAt?: Date
   uri: string
 }
index 1f94039151f4ea0becacc60eede3ba9ef62db216..273d8927ec43e744290ac0be9e120394d343c645 100644 (file)
@@ -347,6 +347,7 @@ export {
   ChargingRateUnitType,
   type ChargingSchedule,
   type ChargingSchedulePeriod,
+  isOCPP16ChargingProfile,
   RecurrencyKindType,
 } from './ocpp/ChargingProfile.js'
 export { type GenericResponse, GenericStatus, RegistrationStatusEnumType } from './ocpp/Common.js'
@@ -366,6 +367,10 @@ export { ConnectorStatusEnum, type ConnectorStatusTransition } from './ocpp/Conn
 export { ErrorType } from './ocpp/ErrorType.js'
 export { MessageType } from './ocpp/MessageType.js'
 export {
+  isOCPP16MeterValue,
+  isOCPP16SampledValue,
+  isOCPP20MeterValue,
+  isOCPP20SampledValue,
   type MeterValue,
   MeterValueContext,
   MeterValueLocation,
@@ -391,6 +396,8 @@ export {
   type IncomingRequestHandler,
   MessageTrigger,
   type MeterValuesRequest,
+  type OCPPCommand,
+  type OCPPCommandName,
   type OutgoingRequest,
   type PendingRequestCancellationCallback,
   type PendingRequestCancellationOptions,
@@ -400,6 +407,7 @@ export {
   type ResponseType,
   type StatusNotificationOptions,
   type StatusNotificationRequest,
+  UNKNOWN_OCPP_COMMAND,
 } from './ocpp/Requests.js'
 export {
   type Reservation,
index 6bcd4b7fe14e6c69fb395d31e67e3e6af8d6ca85..b14d2c661184455449f5c6ce1b75c07b907c5b9b 100644 (file)
@@ -73,6 +73,7 @@ export enum OCPP16MeterValueUnit {
   KILO_WATT = 'kW',
   KILO_WATT_HOUR = 'kWh',
   PERCENT = 'Percent',
+  TEMP_CELCIUS = 'Celcius',
   TEMP_CELSIUS = 'Celsius',
   TEMP_FAHRENHEIT = 'Fahrenheit',
   TEMP_KELVIN = 'K',
index 11336f16a0c126e9240cae74191dda395288e094..80dbf25417c207575946db054ce57ad92937f16d 100644 (file)
@@ -19,6 +19,12 @@ import {
 
 export type ChargingProfile = OCPP16ChargingProfile | OCPP20ChargingProfileType
 
+export const isOCPP16ChargingProfile = (
+  chargingProfile: ChargingProfile
+): chargingProfile is OCPP16ChargingProfile =>
+  typeof chargingProfile.chargingProfileId === 'number' &&
+  !Array.isArray(chargingProfile.chargingSchedule)
+
 export type ChargingSchedule = OCPP16ChargingSchedule | OCPP20ChargingScheduleType
 
 export type ChargingSchedulePeriod = OCPP16ChargingSchedulePeriod | OCPP20ChargingSchedulePeriodType
index 988b73271167c2c1078827554663c3911219a8b0..1796abfc234d26d2be507a63cbb0a9db4e2bdedc 100644 (file)
@@ -19,6 +19,20 @@ import {
 
 export type MeterValue = OCPP16MeterValue | OCPP20MeterValue
 
+export const isOCPP16SampledValue = (
+  sampledValue: OCPP16SampledValue | OCPP20SampledValue
+): sampledValue is OCPP16SampledValue => typeof sampledValue.value === 'string'
+
+export const isOCPP20SampledValue = (
+  sampledValue: OCPP16SampledValue | OCPP20SampledValue
+): sampledValue is OCPP20SampledValue => typeof sampledValue.value === 'number'
+
+export const isOCPP16MeterValue = (meterValue: MeterValue): meterValue is OCPP16MeterValue =>
+  meterValue.sampledValue.length === 0 || meterValue.sampledValue.every(isOCPP16SampledValue)
+
+export const isOCPP20MeterValue = (meterValue: MeterValue): meterValue is OCPP20MeterValue =>
+  meterValue.sampledValue.length > 0 && meterValue.sampledValue.every(isOCPP20SampledValue)
+
 export const MeterValueUnit = {
   ...OCPP16MeterValueUnit,
   ...OCPP20UnitEnumType,
index c2df63d0133da72117859116cb9d9ffa24f4bf2e..ac5cbc0eb58bee25010a94bb39f0ac195a9b75ec 100644 (file)
@@ -100,6 +100,11 @@ export const RequestCommand = {
   ...OCPP16RequestCommand,
   ...OCPP20RequestCommand,
 } as const
+export type OCPPCommand = IncomingRequestCommand | RequestCommand
+
+export const UNKNOWN_OCPP_COMMAND = 'unknown OCPP command'
+export type OCPPCommandName = `${OCPPCommand}` | typeof UNKNOWN_OCPP_COMMAND
+
 // eslint-disable-next-line @typescript-eslint/no-redeclare
 export type RequestCommand = OCPP16RequestCommand | OCPP20RequestCommand
 
diff --git a/src/utils/EnumUtils.ts b/src/utils/EnumUtils.ts
new file mode 100644 (file)
index 0000000..f2f48ef
--- /dev/null
@@ -0,0 +1,10 @@
+export const getEnumStringValue = <T extends string>(
+  enumObject: Readonly<Record<string, T>>,
+  value: unknown
+): T | undefined => {
+  for (const key in enumObject) {
+    if (Object.hasOwn(enumObject, key) && enumObject[key] === value) {
+      return enumObject[key]
+    }
+  }
+}
index dbcfb6d5530eee431380a77cc7a3d8d27c3bc58d..77b51379a88d3a5d96d8ebfc8f5ff548f0583e40 100644 (file)
@@ -9,7 +9,7 @@ import type {
   IncomingRequestCommand,
   JsonType,
   MessageType,
-  RequestCommand,
+  OCPPCommandName,
 } from '../types/index.js'
 
 import { logger } from './Logger.js'
@@ -104,7 +104,7 @@ export const handleFileException = (
 
 export const handleSendMessageError = (
   chargingStation: ChargingStation,
-  commandName: IncomingRequestCommand | RequestCommand,
+  commandName: OCPPCommandName,
   messageType: MessageType,
   error: Error,
   params?: HandleErrorParams<EmptyObject>
index 300a68d9f3f0fccb28996349ca2e1a2f11b5dcc6..2cb7b8003b2453a0dd5f17650f2f84eba91ff5c0 100644 (file)
@@ -33,6 +33,7 @@ export {
 export { ConfigurationValidationError, validateConfiguration } from './ConfigurationValidation.js'
 export { Constants } from './Constants.js'
 export { ACElectricUtils, DCElectricUtils } from './ElectricUtils.js'
+export { getEnumStringValue } from './EnumUtils.js'
 export {
   ensureError,
   getErrorMessage,
index a3c1b24d3d220178fa7c24295ca23254e8e917b3..03a45241037a0e8034594735893eee7824b5ab0e 100644 (file)
@@ -13,6 +13,7 @@ import { Constants } from '../../src/utils/index.js'
 import { standardCleanup, withMockTimers } from '../helpers/TestLifecycleHelpers.js'
 import { TEST_HEARTBEAT_INTERVAL_MS, TEST_ONE_HOUR_MS } from './ChargingStationTestConstants.js'
 import { cleanupChargingStation, createMockChargingStation } from './helpers/StationHelpers.js'
+import { WebSocketReadyState } from './mocks/MockWebSocket.js'
 
 await describe('ChargingStation Configuration Management', async () => {
   // ===== B02/B03 BOOT NOTIFICATION BEHAVIOR TESTS =====
@@ -460,7 +461,7 @@ await describe('ChargingStation Configuration Management', async () => {
       assert.strictEqual(station.isWebSocketConnectionOpened(), true)
 
       // Act - change ready state to CLOSED
-      mocks.webSocket.readyState = 3 // WebSocketReadyState.CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Assert
       assert.strictEqual(station.isWebSocketConnectionOpened(), false)
@@ -473,7 +474,7 @@ await describe('ChargingStation Configuration Management', async () => {
       const mocks = result.mocks
 
       // Act
-      mocks.webSocket.readyState = 0 // WebSocketReadyState.CONNECTING
+      mocks.webSocket.readyState = WebSocketReadyState.CONNECTING
 
       // Assert
       assert.strictEqual(station.isWebSocketConnectionOpened(), false)
@@ -486,7 +487,7 @@ await describe('ChargingStation Configuration Management', async () => {
       const mocks = result.mocks
 
       // Act
-      mocks.webSocket.readyState = 2 // WebSocketReadyState.CLOSING
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSING
 
       // Assert
       assert.strictEqual(station.isWebSocketConnectionOpened(), false)
@@ -632,7 +633,7 @@ await describe('ChargingStation Configuration Management', async () => {
       let openEventFired = false
 
       // First close the connection to test opening
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Set up listener
       mocks.webSocket.on('open', () => {
@@ -644,7 +645,7 @@ await describe('ChargingStation Configuration Management', async () => {
 
       // Assert
       assert.strictEqual(openEventFired, true)
-      assert.strictEqual(mocks.webSocket.readyState, 1) // WebSocketReadyState.OPEN
+      assert.strictEqual(mocks.webSocket.readyState, WebSocketReadyState.OPEN)
     })
 
     await it('should emit close event and set readyState via simulateClose()', () => {
@@ -664,7 +665,7 @@ await describe('ChargingStation Configuration Management', async () => {
 
       // Assert
       assert.strictEqual(closeCode, 1001)
-      assert.strictEqual(mocks.webSocket.readyState, 3) // WebSocketReadyState.CLOSED
+      assert.strictEqual(mocks.webSocket.readyState, WebSocketReadyState.CLOSED)
     })
 
     await it('should emit error event via simulateError()', () => {
@@ -741,7 +742,7 @@ await describe('ChargingStation Configuration Management', async () => {
       const mocks = result.mocks
 
       // Close the WebSocket
-      mocks.webSocket.readyState = 3 // WebSocketReadyState.CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act & Assert
       assert.throws(
index 8ce43b449b565e01034254af1764bb3f5fccdf82..750b4b8bf21a9c115f80288b9909235dd7fdd0ac 100644 (file)
@@ -19,8 +19,10 @@ import { OCPP20RequestService } from '../../src/charging-station/ocpp/2.0/OCPP20
 import { OCPP20ResponseService } from '../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import { OCPP20ServiceUtils } from '../../src/charging-station/ocpp/2.0/OCPP20ServiceUtils.js'
 import { stopRunningTransactions } from '../../src/charging-station/ocpp/OCPPServiceOperations.js'
+import { OCPPError } from '../../src/exception/index.js'
 import {
   AvailabilityType,
+  ErrorType,
   MessageType,
   OCPP16AuthorizationStatus,
   OCPP16ChargePointStatus,
@@ -38,7 +40,7 @@ import {
   OCPP20TriggerReasonEnumType,
   OCPPVersion,
 } from '../../src/types/index.js'
-import { AsyncLock, AsyncLockType, Constants } from '../../src/utils/index.js'
+import { AsyncLock, AsyncLockType, Constants, logger } from '../../src/utils/index.js'
 import {
   flushMicrotasks,
   setupConnectorWithTransaction,
@@ -2119,6 +2121,78 @@ await describe('ChargingStation Lifecycle', async () => {
       assert.strictEqual(closeConnection.mock.callCount(), 1)
     })
 
+    await it('should reject an unknown OCPP command before typed dispatch', async () => {
+      const sendError = mock.fn((..._args: unknown[]): Promise<void> => Promise.resolve())
+      const { station: activeStation } = createMockChargingStation({
+        ocppRequestService: { sendError },
+        ocppVersion: OCPPVersion.VERSION_20,
+        started: true,
+      })
+      station = activeStation
+      const stationLifecycle = activeStation as unknown as {
+        onMessage: (
+          data: string,
+          sourceConnection?: NonNullable<typeof activeStation.wsConnection>
+        ) => Promise<void>
+      }
+      stationLifecycle.onMessage = (
+        ChargingStation.prototype as unknown as {
+          onMessage: typeof stationLifecycle.onMessage
+        }
+      ).onMessage
+      const sourceConnection = activeStation.wsConnection
+      assert.ok(sourceConnection != null)
+
+      await stationLifecycle.onMessage.call(
+        activeStation,
+        JSON.stringify([MessageType.CALL_MESSAGE, 'unknown-command', 'UnknownCommand', {}]),
+        sourceConnection
+      )
+
+      assert.strictEqual(sendError.mock.callCount(), 1)
+      const error: unknown = sendError.mock.calls[0]?.arguments[2]
+      assert.ok(error instanceof OCPPError)
+      assert.strictEqual(error.code, ErrorType.NOT_IMPLEMENTED)
+      assert.deepStrictEqual(error.details, { command: 'UnknownCommand' })
+    })
+
+    await it('should report the parsed unknown OCPP message type', async t => {
+      const { station: activeStation } = createMockChargingStation({
+        ocppVersion: OCPPVersion.VERSION_20,
+        started: true,
+      })
+      station = activeStation
+      const errorMock = t.mock.method(logger, 'error', () => undefined)
+      const stationLifecycle = activeStation as unknown as {
+        onMessage: (
+          data: string,
+          sourceConnection?: NonNullable<typeof activeStation.wsConnection>
+        ) => Promise<void>
+      }
+      stationLifecycle.onMessage = (
+        ChargingStation.prototype as unknown as {
+          onMessage: typeof stationLifecycle.onMessage
+        }
+      ).onMessage
+      const sourceConnection = activeStation.wsConnection
+      assert.ok(sourceConnection != null)
+
+      await stationLifecycle.onMessage.call(
+        activeStation,
+        JSON.stringify([99, 'unknown-message-type', {}]),
+        sourceConnection
+      )
+
+      assert.ok(
+        errorMock.mock.calls.some(call => {
+          const loggedMessage: unknown = call.arguments[0]
+          return (
+            typeof loggedMessage === 'string' && loggedMessage.includes('Wrong message type 99')
+          )
+        })
+      )
+    })
+
     await it('should drop a late CALL at the websocket boundary without buffering and accepts it after restart', async () => {
       const transactionId = '00000000-0000-4000-8000-000000000027'
       const requestHandler = mock.fn((...args: unknown[]): Promise<Record<string, never>> => {
index 6a3cb5bed59d3a3a12fc294ca8d45489c4e85566..9ac467ca703035e1892e838ad2f9bb55630690e3 100644 (file)
@@ -11,6 +11,7 @@ import { RegistrationStatusEnumType, RequestCommand } from '../../src/types/inde
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
 import { TEST_HEARTBEAT_INTERVAL_MS } from './ChargingStationTestConstants.js'
 import { cleanupChargingStation, createMockChargingStation } from './helpers/StationHelpers.js'
+import { WebSocketReadyState } from './mocks/MockWebSocket.js'
 
 await describe('ChargingStation Resilience', async () => {
   await describe('Error Recovery and Resilience', async () => {
@@ -45,7 +46,7 @@ await describe('ChargingStation Resilience', async () => {
       mocks.webSocket.simulateClose(1006, 'Connection lost')
 
       // Assert - WebSocket should be in CLOSED state
-      assert.strictEqual(mocks.webSocket.readyState, 3) // CLOSED
+      assert.strictEqual(mocks.webSocket.readyState, WebSocketReadyState.CLOSED)
     })
 
     await it('should track connection retry count', () => {
@@ -183,7 +184,7 @@ await describe('ChargingStation Resilience', async () => {
 
       // Assert - Station should remain in valid state
       assert.strictEqual(station.getNumberOfConnectors() > 0, true)
-      assert.strictEqual(mocks.webSocket.readyState, 3) // CLOSED
+      assert.strictEqual(mocks.webSocket.readyState, WebSocketReadyState.CLOSED)
     })
 
     await it('should handle boot notification rejected state', () => {
@@ -319,7 +320,7 @@ await describe('ChargingStation Resilience', async () => {
       const testMessage = '[2,"test-msg-1","BootNotification",{}]'
 
       // Ensure WebSocket is closed
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer a message
       station.bufferMessage(testMessage)
@@ -339,7 +340,7 @@ await describe('ChargingStation Resilience', async () => {
       const testMessage = '[2,"test-msg-2","Heartbeat",{}]'
 
       // Ensure WebSocket is open
-      mocks.webSocket.readyState = 1 // OPEN
+      mocks.webSocket.readyState = WebSocketReadyState.OPEN
       mocks.webSocket.simulateOpen()
 
       // Act - Send message
@@ -361,7 +362,7 @@ await describe('ChargingStation Resilience', async () => {
       const msg3 = '[2,"msg-3","StatusNotification",{}]'
 
       // Simulate offline: close the connection
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer multiple messages
       station.bufferMessage(msg1)
@@ -390,7 +391,7 @@ await describe('ChargingStation Resilience', async () => {
         '[2,"m5","Cmd5",{}]',
       ]
 
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer all messages
       for (const msg of messages) {
@@ -412,7 +413,7 @@ await describe('ChargingStation Resilience', async () => {
       const mocks = result.mocks
       const messageCount = 100
 
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer many messages
       for (let i = 0; i < messageCount; i++) {
@@ -445,7 +446,7 @@ await describe('ChargingStation Resilience', async () => {
       const mocks = result.mocks
       const testMessage = '[2,"offline-msg","Test",{}]'
 
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer message
       station.bufferMessage(testMessage)
@@ -466,7 +467,7 @@ await describe('ChargingStation Resilience', async () => {
       const mocks = result.mocks
       const testMessage = '[2,"clear-test","Command",{}]'
 
-      mocks.webSocket.readyState = 3 // CLOSED
+      mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
       // Act - Buffer message
       station.bufferMessage(testMessage)
@@ -497,7 +498,7 @@ await describe('ChargingStation Resilience', async () => {
       // Act - Perform multiple buffer/disconnect cycles
       for (let cycle = 0; cycle < cycleCount; cycle++) {
         // Simulate disconnection
-        mocks.webSocket.readyState = 3 // CLOSED
+        mocks.webSocket.readyState = WebSocketReadyState.CLOSED
 
         // Buffer messages in this cycle
         for (let i = 0; i < messagesPerCycle; i++) {
index 5f44a71aa05c74f85746978fb36606989abfb2bd..a5763b70383936dceb08793caad8eeccd6b9bdde 100644 (file)
@@ -41,7 +41,6 @@ import {
   CurrentType,
   OCPPVersion,
   type Reservation,
-  type SampledValueTemplate,
 } from '../../src/types/index.js'
 import { logger } from '../../src/utils/index.js'
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
@@ -977,7 +976,7 @@ await describe('Helpers', async () => {
       const connectorStatus: ConnectorStatus = {
         availability: AvailabilityType.Operative,
         energyActiveImportIntervalBaselines: { 'station:aligned': 42 },
-        MeterValues: [{} as unknown as SampledValueTemplate],
+        MeterValues: [{}],
         status: ConnectorStatusEnum.Available,
         transactionEnergyActiveImportIntervalBaselines: { transaction: 21 },
         transactionStarted: true,
index b4aa964b2c9af07b315f2d28e3ea82fb62f5bb9a..ed0a7bc9bf3559888c7e31284ca286b0372f7fbd 100644 (file)
@@ -5,8 +5,6 @@
 import assert from 'node:assert/strict'
 import { afterEach, describe, it } from 'node:test'
 
-import type { ConnectorStatus } from '../../src/types/index.js'
-
 import {
   captureTransactionIntervalState,
   completeTransactionIntervalState,
@@ -15,6 +13,13 @@ import {
   restoreTransactionIntervalState,
   truncateTransactionIntervalValue,
 } from '../../src/charging-station/meter-values/index.js'
+import {
+  type ConnectorStatus,
+  MeterValueLocation,
+  MeterValueMeasurand,
+  MeterValuePhase,
+  MeterValueUnit,
+} from '../../src/types/index.js'
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
 
 await describe('TransactionIntervalUtils', async () => {
@@ -51,8 +56,8 @@ await describe('TransactionIntervalUtils', async () => {
         {
           sampledValue: [
             {
-              measurand: 'Energy.Active.Import.Interval',
-              unit: 'kWh',
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+              unit: MeterValueUnit.KILO_WATT_HOUR,
               value: '0.01',
             },
           ],
@@ -66,9 +71,9 @@ await describe('TransactionIntervalUtils', async () => {
         {
           sampledValue: [
             {
-              measurand: 'Energy.Active.Import.Interval',
-              phase: 'L1',
-              unitOfMeasure: { unit: 'kWh' },
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+              phase: MeterValuePhase.L1,
+              unitOfMeasure: { unit: MeterValueUnit.KILO_WATT_HOUR },
               value: 0.003,
             },
           ],
@@ -82,9 +87,24 @@ await describe('TransactionIntervalUtils', async () => {
         {
           sampledValue: [
             {
-              location: 'Inlet',
-              measurand: 'Energy.Active.Import.Interval',
-              unit: 'Wh',
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+              unitOfMeasure: { multiplier: -1, unit: MeterValueUnit.KILO_WATT_HOUR },
+              value: 0.2,
+            },
+          ],
+        },
+        1
+      ),
+      20
+    )
+    assert.strictEqual(
+      getRepresentedTransactionIntervalEnergyWh(
+        {
+          sampledValue: [
+            {
+              location: MeterValueLocation.INLET,
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+              unit: MeterValueUnit.WATT_HOUR,
               value: 100,
             },
           ],
@@ -99,10 +119,10 @@ await describe('TransactionIntervalUtils', async () => {
         {
           sampledValue: [
             {
-              location: 'Inlet',
-              measurand: 'Energy.Active.Import.Interval',
-              phase: 'L1',
-              unit: 'Wh',
+              location: MeterValueLocation.INLET,
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_INTERVAL,
+              phase: MeterValuePhase.L1,
+              unit: MeterValueUnit.WATT_HOUR,
               value: 100 / 3,
             },
           ],
index 1887902f0181b6c646dbd38f9d4c5fa3ee1545fa..14f9bb678d4379a64e4879911f3f71e673128b16 100644 (file)
@@ -21,7 +21,6 @@ import type {
   ChargingStationInfo,
   ConnectorStatus,
   OCPP20SampledValue,
-  SampledValue,
   SampledValueTemplate,
 } from '../../../src/types/index.js'
 
@@ -36,16 +35,21 @@ import {
   resolveRootSeed,
 } from '../../../src/charging-station/meter-values/index.js'
 import { hashLabel } from '../../../src/charging-station/meter-values/PRNG.js'
+import { buildOCPP16SampledValue } from '../../../src/charging-station/ocpp/1.6/OCPP16RequestBuilders.js'
 import { buildOCPP20SampledValue } from '../../../src/charging-station/ocpp/2.0/OCPP20RequestBuilders.js'
 import {
   AvailabilityType,
   CurrentType,
+  isOCPP20SampledValue,
   MeterValueContext,
   MeterValueLocation,
   MeterValueMeasurand,
   MeterValuePhase,
   MeterValueUnit,
+  OCPP16MeterValueFormat,
+  OCPP20MeasurandEnumType,
   OCPPVersion,
+  Voltage,
 } from '../../../src/types/index.js'
 import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
 import { TEST_METER_VALUES_INTERVAL_MS } from '../ChargingStationTestConstants.js'
@@ -96,7 +100,7 @@ const buildContext = (
     groupUnderEvse?: boolean
     numberOfPhases?: number
     siblingConnectorMeterValues?: SampledValueTemplate[]
-    voltageOut?: number
+    voltageOut?: Voltage
   } = {}
 ): {
   connectorStatus: ConnectorStatus
@@ -106,7 +110,7 @@ const buildContext = (
   stationInfo: ChargingStationInfo
 } => {
   const numberOfPhases = overrides.numberOfPhases ?? 1
-  const voltageOut = overrides.voltageOut ?? 230
+  const voltageOut = overrides.voltageOut ?? Voltage.VOLTAGE_230
   const evseMax = overrides.evseMaxPowerW ?? 22000
 
   const stationInfo: ChargingStationInfo = {
@@ -203,7 +207,7 @@ const templatesFor = (
         : measurand === MeterValueMeasurand.POWER_ACTIVE_IMPORT
           ? MeterValueUnit.WATT
           : undefined
-    return (unit != null ? { measurand, unit } : { measurand }) as SampledValueTemplate
+    return unit != null ? { measurand, unit } : { measurand }
   })
 }
 
@@ -214,13 +218,7 @@ const templatesFor = (
  * @param value - Numeric value to be serialized.
  * @returns Minimal SampledValue with stringified value.
  */
-const passThroughBuilder: BuildVersionedSampledValue = (template, value): SampledValue => {
-  return {
-    ...(template.measurand != null && { measurand: template.measurand }),
-    ...(template.unit != null && { unit: template.unit as never }),
-    value: value.toString(),
-  } as SampledValue
-}
+const passThroughBuilder: BuildVersionedSampledValue = buildOCPP16SampledValue
 
 /**
  * Creates a coherent session and asserts it is defined. Encapsulates the
@@ -266,7 +264,7 @@ const advanceDcEnergyAtLocations = (
     measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
     unit: MeterValueUnit.WATT_HOUR,
     value: 0,
-  })) as SampledValueTemplate[]
+  }))
 
   const meterValue = buildCoherentMeterValue(
     context,
@@ -323,7 +321,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 7400,
         numberOfPhases: 1,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -355,7 +353,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -385,7 +383,7 @@ await describe('CoherentMeterValues', async () => {
       const { connectorStatus, context, sessions } = buildContext({
         currentType: CurrentType.DC,
         evseMaxPowerW: 50000,
-        voltageOut: 400,
+        voltageOut: Voltage.VOLTAGE_400,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -549,7 +547,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.DC,
         evseMaxPowerW: 1000,
         groupUnderEvse: true,
-        voltageOut: 400,
+        voltageOut: Voltage.VOLTAGE_400,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -573,7 +571,7 @@ await describe('CoherentMeterValues', async () => {
             unit: MeterValueUnit.WATT_HOUR,
           },
         ]
-      ) as unknown as SampledValueTemplate[]
+      )
 
       const meterValue = buildCoherentMeterValue(
         context,
@@ -613,7 +611,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.DC,
         evseMaxPowerW: 1000,
         groupUnderEvse: true,
-        voltageOut: 400,
+        voltageOut: Voltage.VOLTAGE_400,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -634,7 +632,7 @@ await describe('CoherentMeterValues', async () => {
           unit:
             measurand === MeterValueMeasurand.VOLTAGE ? MeterValueUnit.VOLT : MeterValueUnit.AMP,
         }))
-      ) as unknown as SampledValueTemplate[]
+      )
 
       const meterValue = buildCoherentMeterValue(
         context,
@@ -861,7 +859,6 @@ await describe('CoherentMeterValues', async () => {
         {
           fluctuationPercent: 0,
           measurand: MeterValueMeasurand.FREQUENCY,
-          unit: MeterValueUnit.HERTZ,
           value: '50',
         },
         {
@@ -875,7 +872,7 @@ await describe('CoherentMeterValues', async () => {
           value: '50Hz',
         },
         { measurand: MeterValueMeasurand.CURRENT_EXPORT },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const enabledMeasurands = new Set([
         MeterValueMeasurand.CURRENT_EXPORT,
         MeterValueMeasurand.FREQUENCY,
@@ -981,7 +978,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 1,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1033,7 +1030,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 1,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1113,7 +1110,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1154,7 +1151,7 @@ await describe('CoherentMeterValues', async () => {
       const { connectorStatus, context, sessions } = buildContext({
         currentType: CurrentType.DC,
         evseMaxPowerW: 50000,
-        voltageOut: 400,
+        voltageOut: Voltage.VOLTAGE_400,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1361,7 +1358,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1374,13 +1371,7 @@ await describe('CoherentMeterValues', async () => {
       sessions.set(1, session)
       // Phase-preserving builder: include template.phase and template.measurand
       // so per-phase emissions can be counted and cross-checked.
-      const phaseBuilder: BuildVersionedSampledValue = (template, value): SampledValue =>
-        ({
-          ...(template.measurand != null && { measurand: template.measurand }),
-          ...(template.phase != null && { phase: template.phase as never }),
-          ...(template.unit != null && { unit: template.unit as never }),
-          value: value.toString(),
-        }) as SampledValue
+      const phaseBuilder: BuildVersionedSampledValue = buildOCPP16SampledValue
       connectorStatus.MeterValues = [
         { measurand: MeterValueMeasurand.VOLTAGE, phase: MeterValuePhase.L1_N },
         { measurand: MeterValueMeasurand.VOLTAGE, phase: MeterValuePhase.L2_N },
@@ -1404,7 +1395,7 @@ await describe('CoherentMeterValues', async () => {
         },
         { measurand: MeterValueMeasurand.CURRENT_IMPORT, phase: MeterValuePhase.L1 },
         { measurand: MeterValueMeasurand.CURRENT_IMPORT, phase: MeterValuePhase.N },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(context, session, phaseBuilder, {
         intervalMs: TEST_METER_VALUES_INTERVAL_MS,
         nowMs: TEST_METER_VALUES_INTERVAL_MS,
@@ -1449,7 +1440,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 7400,
         numberOfPhases: 1,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1463,7 +1454,7 @@ await describe('CoherentMeterValues', async () => {
       connectorStatus.MeterValues = [
         { measurand: MeterValueMeasurand.VOLTAGE, phase: MeterValuePhase.L1_L2 },
         { measurand: MeterValueMeasurand.VOLTAGE, phase: MeterValuePhase.L1_N },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(context, session, passThroughBuilder, {
         intervalMs: TEST_METER_VALUES_INTERVAL_MS,
         nowMs: TEST_METER_VALUES_INTERVAL_MS,
@@ -1481,7 +1472,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1500,7 +1491,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L1_N,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(context, session, passThroughBuilder, {
         intervalMs: 3_600_000,
         nowMs: 3_600_000,
@@ -1522,7 +1513,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1555,7 +1546,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L3_N,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(
         context,
         session,
@@ -1587,7 +1578,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1616,7 +1607,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L3_N,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(context, session, passThroughBuilder, {
         intervalMs: 3_600_000,
         nowMs: 3_600_000,
@@ -1640,7 +1631,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1667,7 +1658,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L3_N,
           unit: MeterValueUnit.WATT,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(
         context,
         session,
@@ -1699,7 +1690,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1728,7 +1719,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L3_N,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(
         context,
         session,
@@ -1776,7 +1767,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1820,7 +1811,7 @@ await describe('CoherentMeterValues', async () => {
           measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       const mv = buildCoherentMeterValue(
         context,
         session,
@@ -1859,7 +1850,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -1888,7 +1879,7 @@ await describe('CoherentMeterValues', async () => {
           phase: MeterValuePhase.L2_N,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
 
       const meterValue = buildCoherentMeterValue(
         context,
@@ -1926,7 +1917,7 @@ await describe('CoherentMeterValues', async () => {
       connectorStatus.MeterValues = [
         { phase: MeterValuePhase.L1_N },
         { context: MeterValueContext.TRANSACTION_BEGIN, phase: MeterValuePhase.L2_N },
-        { format: 'SignedData', phase: MeterValuePhase.L3_N },
+        { format: OCPP16MeterValueFormat.SIGNED_DATA, phase: MeterValuePhase.L3_N },
         {
           location: MeterValueLocation.OUTLET,
           phase: MeterValuePhase.L1_N,
@@ -1936,7 +1927,7 @@ await describe('CoherentMeterValues', async () => {
           customData: { vendorId: 'sensor-b' },
           phase: MeterValuePhase.L2_N,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
 
       const meterValue = buildCoherentMeterValue(
         context,
@@ -1949,8 +1940,10 @@ await describe('CoherentMeterValues', async () => {
         true
       )
       const energySamples = meterValue.sampledValue.filter(
-        sample => sample.measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
-      ) as OCPP20SampledValue[]
+        (sample): sample is OCPP20SampledValue =>
+          isOCPP20SampledValue(sample) &&
+          sample.measurand === OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER
+      )
 
       assert.deepStrictEqual(
         energySamples.map(sample => [
@@ -1985,13 +1978,13 @@ await describe('CoherentMeterValues', async () => {
       const evseTemplate: SampledValueTemplate = {
         measurand: MeterValueMeasurand.STATE_OF_CHARGE,
         unit: MeterValueUnit.PERCENT,
-      } as unknown as SampledValueTemplate
+      }
       const { connectorStatus, context, sessions } = buildContext({
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         evseMeterValues: [evseTemplate],
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2007,7 +2000,7 @@ await describe('CoherentMeterValues', async () => {
         {
           measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: MeterValueUnit.WATT_HOUR,
-        } as unknown as SampledValueTemplate,
+        },
       ]
       const mv = buildCoherentMeterValue(context, session, passThroughBuilder, {
         intervalMs: TEST_METER_VALUES_INTERVAL_MS,
@@ -2029,13 +2022,13 @@ await describe('CoherentMeterValues', async () => {
       const connectorTemplate: SampledValueTemplate = {
         measurand: MeterValueMeasurand.STATE_OF_CHARGE,
         unit: MeterValueUnit.PERCENT,
-      } as unknown as SampledValueTemplate
+      }
       // No evseMeterValues override => getEvseIdByConnectorId returns undefined.
       const { connectorStatus, context, sessions } = buildContext({
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2067,7 +2060,7 @@ await describe('CoherentMeterValues', async () => {
       const connectorTemplate: SampledValueTemplate = {
         measurand: MeterValueMeasurand.STATE_OF_CHARGE,
         unit: MeterValueUnit.PERCENT,
-      } as unknown as SampledValueTemplate
+      }
       // evseMeterValues=[] => getEvseIdByConnectorId returns 1, getEvseStatus(1).MeterValues = [].
       // resolveTemplates must skip the empty EVSE array and fall back to connector-level.
       const { connectorStatus, context, sessions } = buildContext({
@@ -2075,7 +2068,7 @@ await describe('CoherentMeterValues', async () => {
         evseMaxPowerW: 22000,
         evseMeterValues: [],
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2107,7 +2100,7 @@ await describe('CoherentMeterValues', async () => {
       const connectorTemplate: SampledValueTemplate = {
         measurand: MeterValueMeasurand.STATE_OF_CHARGE,
         unit: MeterValueUnit.PERCENT,
-      } as unknown as SampledValueTemplate
+      }
       // groupUnderEvse=true + no evseMeterValues => getEvseIdByConnectorId returns 1,
       // getEvseStatus(1).MeterValues = undefined. resolveTemplates must fall back
       // to connector-level (guard: isNotEmptyArray(evseTemplates) narrows undefined
@@ -2117,7 +2110,7 @@ await describe('CoherentMeterValues', async () => {
         evseMaxPowerW: 22000,
         groupUnderEvse: true,
         numberOfPhases: 3,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2154,7 +2147,7 @@ await describe('CoherentMeterValues', async () => {
       const siblingTemplate: SampledValueTemplate = {
         measurand: MeterValueMeasurand.POWER_ACTIVE_IMPORT,
         unit: MeterValueUnit.WATT,
-      } as unknown as SampledValueTemplate
+      }
       const { connectorStatus, context, sessions } = buildContext({
         currentType: CurrentType.AC,
         evseMaxPowerW: 22000,
@@ -2162,7 +2155,7 @@ await describe('CoherentMeterValues', async () => {
         groupUnderEvse: true,
         numberOfPhases: 3,
         siblingConnectorMeterValues: [siblingTemplate],
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2203,7 +2196,7 @@ await describe('CoherentMeterValues', async () => {
           currentType: CurrentType.AC,
           evseMaxPowerW: 7400,
           numberOfPhases: 1,
-          voltageOut: 230,
+          voltageOut: Voltage.VOLTAGE_230,
         })
         const session = createSessionOrFail(context, {
           connectorId: 1,
@@ -2245,7 +2238,7 @@ await describe('CoherentMeterValues', async () => {
           currentType: CurrentType.AC,
           evseMaxPowerW: 22000,
           numberOfPhases: 3,
-          voltageOut: 230,
+          voltageOut: Voltage.VOLTAGE_230,
         })
         const session = createSessionOrFail(context, {
           connectorId: 1,
@@ -2292,7 +2285,7 @@ await describe('CoherentMeterValues', async () => {
           currentType: CurrentType.DC,
           evseMaxPowerW: 50000,
           numberOfPhases: 0,
-          voltageOut: 400,
+          voltageOut: Voltage.VOLTAGE_400,
         })
         const session = createSessionOrFail(context, {
           connectorId: 1,
@@ -2341,7 +2334,7 @@ await describe('CoherentMeterValues', async () => {
         currentType: CurrentType.AC,
         evseMaxPowerW: 7400,
         numberOfPhases: 1,
-        voltageOut: 230,
+        voltageOut: Voltage.VOLTAGE_230,
       })
       const session = createSessionOrFail(context, {
         connectorId: 1,
@@ -2408,7 +2401,7 @@ await describe('CoherentMeterValues', async () => {
           currentType: CurrentType.AC,
           evseMaxPowerW: 7400,
           numberOfPhases: 1,
-          voltageOut: 230,
+          voltageOut: Voltage.VOLTAGE_230,
         })
         const session = createSessionOrFail(context, {
           connectorId: 1,
index 64b89e51409732f43b342c09747185343000aee9..ba9da0e614f4b633393fbf49f94eb32ccb1e2570 100644 (file)
@@ -44,7 +44,7 @@ const energyTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
   unit: 'Wh',
   value: '0',
-} as unknown as SampledValueTemplate
+}
 
 await describe('StrategyDispatch', async () => {
   let station: ChargingStation
@@ -148,7 +148,7 @@ await describe('StrategyDispatch', async () => {
           {
             measurand: MeterValueMeasurand.STATE_OF_CHARGE,
             unit: 'Percent',
-          } as unknown as SampledValueTemplate,
+          },
         ]
         connectorStatus.transactionId = TEST_TRANSACTION_ID
         connectorStatus.transactionEnergyActiveImportRegisterValue = 0
@@ -230,7 +230,7 @@ await describe('StrategyDispatch', async () => {
             phase: MeterValuePhase.L3_N,
             unit: MeterValueUnit.WATT_HOUR,
           },
-        ] as unknown as SampledValueTemplate[]
+        ]
         connectorStatus.transactionId = TEST_TRANSACTION_ID
         connectorStatus.transactionEnergyActiveImportRegisterValue = 6000
         connectorStatus.energyActiveImportRegisterValue = 6000
@@ -271,10 +271,14 @@ await describe('StrategyDispatch', async () => {
       const connectorStatus = station.getConnectorStatus(1)
       assert.ok(connectorStatus != null)
       const templates = connectorStatus.MeterValues
-      connectorStatus.MeterValues = ['sensor-a', 'sensor-b'].flatMap(vendorId =>
+      connectorStatus.MeterValues = ['sensor-a', 'sensor-b'].flatMap((vendorId, index) =>
         templates.map(template => ({
           ...template,
-          customData: { vendorId },
+          unitOfMeasure: {
+            customData: { vendorId },
+            multiplier: index,
+            unit: MeterValueUnit.WATT_HOUR,
+          },
         }))
       )
       const meterValue = buildMeterValue(
@@ -290,16 +294,21 @@ await describe('StrategyDispatch', async () => {
       assert.strictEqual(
         energySamples.length,
         2,
-        'phase suppression must preserve distinct customData register families'
+        'phase suppression must preserve distinct native unit descriptor families'
       )
       assert.deepStrictEqual(
         energySamples.map(sample => [
-          (sample as { customData?: { vendorId?: string } }).customData?.vendorId,
+          (
+            sample as {
+              unitOfMeasure?: { customData?: { vendorId?: string }; multiplier?: number }
+            }
+          ).unitOfMeasure?.customData?.vendorId,
+          (sample as { unitOfMeasure?: { multiplier?: number } }).unitOfMeasure?.multiplier,
           sample.phase,
         ]),
         [
-          ['sensor-a', undefined],
-          ['sensor-b', undefined],
+          ['sensor-a', 0, undefined],
+          ['sensor-b', 1, undefined],
         ]
       )
     })
index 1eeab0b4946ad52d64eea6f25315c3efe3d73683..766d2663d64a6ceb59eda84c8c2dee9f871c008c 100644 (file)
@@ -8,7 +8,9 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { ResetRequest } from '../../../../src/types/index.js'
 
+import { OCPPError } from '../../../../src/exception/index.js'
 import {
+  ErrorType,
   GenericStatus,
   OCPP16IncomingRequestCommand,
   ResetType,
@@ -72,6 +74,17 @@ await describe('OCPP16IncomingRequestService — Reset', async () => {
     assert.strictEqual(response.status, GenericStatus.Accepted)
   })
 
+  await it('should reject an invalid reset type before scheduling a reset', () => {
+    const { testableService } = testContext
+    const station = ResetFixtures.createStandardStation(0)
+    const resetRequest = { type: 'Invalid' } as unknown as ResetRequest
+
+    assert.throws(
+      () => testableService.handleRequestReset(station, resetRequest),
+      (error: unknown) => error instanceof OCPPError && error.code === ErrorType.FORMAT_VIOLATION
+    )
+  })
+
   // @spec §5.13 — TC_024_CS: Hard reset with active transaction
   await it('should handle hard reset request with active transaction', () => {
     // Arrange
index 40813f0a796f28feb2fbf20558ad89ea11d31f13..47c30c0c4a14bdb41d8cc8564d531f4ae8447a05 100644 (file)
@@ -20,7 +20,10 @@ import type {
   RequestParams,
 } from '../../../../src/types/index.js'
 
-import { TransactionMeterValueDeliveryBarrier } from '../../../../src/charging-station/meter-values/index.js'
+import {
+  type CoherentSession,
+  TransactionMeterValueDeliveryBarrier,
+} from '../../../../src/charging-station/meter-values/index.js'
 import { createTestableIncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/__testable__/index.js'
 import { OCPP16IncomingRequestService } from '../../../../src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.js'
 import { OCPP16ServiceUtils } from '../../../../src/charging-station/ocpp/1.6/OCPP16ServiceUtils.js'
@@ -29,10 +32,13 @@ import {
   CurrentType,
   ErrorType,
   OCPP16AuthorizationStatus,
+  OCPP16AvailabilityType,
+  OCPP16ChargePointStatus,
   OCPP16DiagnosticsStatus,
   OCPP16FirmwareStatus,
   OCPP16IncomingRequestCommand,
   OCPP16MessageTrigger,
+  OCPP16MeterValueContext,
   OCPP16MeterValueFormat,
   OCPP16MeterValueLocation,
   OCPP16MeterValueMeasurand,
@@ -44,6 +50,7 @@ import {
   OCPP16VendorParametersKey,
   OCPPVersion,
   PublicKeyWithSignedMeterValueEnumType,
+  Voltage,
 } from '../../../../src/types/index.js'
 import { Constants, logger } from '../../../../src/utils/index.js'
 import {
@@ -189,6 +196,81 @@ await describe('OCPP16IncomingRequestService — TriggerMessage', async () => {
       // Assert
       assert.strictEqual(response.status, OCPP16TriggerMessageStatus.ACCEPTED)
     })
+
+    await it('should send an OCPP 1.6 coherent snapshot for a MeterValues trigger', async () => {
+      // Arrange
+      const { incomingRequestService, station, testableService } = context
+      const transactionId = 100
+      setupConnectorWithTransaction(station, 1, { transactionId })
+      assert.ok(station.stationInfo != null)
+      station.stationInfo.coherentMeterValues = true
+      enableConnectorMeterValues(station, 1)
+      const session: CoherentSession = {
+        connectorId: 1,
+        currentType: CurrentType.AC,
+        numberOfPhases: 3,
+        profile: {
+          batteryCapacityWh: 40000,
+          chargingCurve: [
+            { powerFraction: 1, socPercent: 0 },
+            { powerFraction: 1, socPercent: 100 },
+          ],
+          id: 'trigger-test',
+          initialSocPercentMax: 30,
+          initialSocPercentMin: 30,
+          maxPowerW: 11000,
+          weight: 1,
+        },
+        rampUpDurationMs: 0,
+        sessionStartMs: Date.now(),
+        socPercent: 30,
+        transactionId,
+        voltageOutNominal: Voltage.VOLTAGE_230,
+      }
+      station.__injectCoherentSession(transactionId, session)
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      const energyBefore = connectorStatus.transactionEnergyActiveImportRegisterValue
+      const socBefore = session.socPercent
+      let sentRequest: OCPP16MeterValuesRequest | undefined
+      ;(
+        station.ocppRequestService as unknown as {
+          requestHandler: (...args: unknown[]) => Promise<unknown>
+        }
+      ).requestHandler = (...args: unknown[]) => {
+        if (args[1] === OCPP16RequestCommand.METER_VALUES) {
+          sentRequest = args[2] as OCPP16MeterValuesRequest
+        }
+        return Promise.resolve({})
+      }
+      const request: OCPP16TriggerMessageRequest = {
+        connectorId: 1,
+        requestedMessage: OCPP16MessageTrigger.MeterValues,
+      }
+
+      // Act
+      const response = testableService.handleRequestTriggerMessage(station, request)
+      incomingRequestService.emit(
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        station,
+        request,
+        response
+      )
+      await flushMicrotasks()
+
+      // Assert
+      assert.strictEqual(response.status, OCPP16TriggerMessageStatus.ACCEPTED)
+      assert.ok(sentRequest != null)
+      assert.strictEqual(sentRequest.connectorId, 1)
+      assert.ok((sentRequest.meterValue[0]?.sampledValue.length ?? 0) > 0)
+      assert.strictEqual(
+        sentRequest.meterValue[0]?.sampledValue[0]?.context,
+        OCPP16MeterValueContext.TRIGGER
+      )
+      assert.strictEqual(typeof sentRequest.meterValue[0]?.sampledValue[0]?.value, 'string')
+      assert.strictEqual(connectorStatus.transactionEnergyActiveImportRegisterValue, energyBefore)
+      assert.strictEqual(session.socPercent, socBefore)
+    })
   })
 
   await it('should keep active transaction interval state unchanged during MeterValues admission', () => {
@@ -1114,7 +1196,7 @@ await describe('OCPP16IncomingRequestService — TriggerMessage', async () => {
 
       // Act
       const response = testableService.handleRequestTriggerMessage(station, {
-        requestedMessage: 'UnknownMessage' as OCPP16MessageTrigger,
+        requestedMessage: 'UnknownMessage' as unknown as OCPP16MessageTrigger,
       })
 
       // Assert
@@ -1236,6 +1318,53 @@ await describe('OCPP16IncomingRequestService — TriggerMessage', async () => {
       })
     }
 
+    await it('should include connector zero with a derived valid status in a broadcast trigger', () => {
+      // Arrange
+      const stationStatus = station.getConnectorStatus(0)
+      const connectorOneStatus = station.getConnectorStatus(1)
+      const connectorTwoStatus = station.getConnectorStatus(2)
+      assert.ok(stationStatus != null)
+      assert.ok(connectorOneStatus != null)
+      assert.ok(connectorTwoStatus != null)
+      delete stationStatus.status
+      stationStatus.availability = OCPP16AvailabilityType.Operative
+      connectorOneStatus.status = OCPP16ChargePointStatus.Available
+      connectorTwoStatus.status = OCPP16ChargePointStatus.Unavailable
+      const request: OCPP16TriggerMessageRequest = {
+        requestedMessage: OCPP16MessageTrigger.StatusNotification,
+      }
+
+      // Act
+      incomingRequestServiceForListener.emit(
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        station,
+        request,
+        { status: OCPP16TriggerMessageStatus.ACCEPTED }
+      )
+
+      // Assert
+      assert.deepStrictEqual(
+        requestHandlerMock.mock.calls.map(call => call.arguments.slice(1)),
+        [
+          [
+            OCPP16RequestCommand.STATUS_NOTIFICATION,
+            { connectorId: 0, status: OCPP16ChargePointStatus.Available },
+            { triggerMessage: true },
+          ],
+          [
+            OCPP16RequestCommand.STATUS_NOTIFICATION,
+            { connectorId: 1, status: OCPP16ChargePointStatus.Available },
+            { triggerMessage: true },
+          ],
+          [
+            OCPP16RequestCommand.STATUS_NOTIFICATION,
+            { connectorId: 2, status: OCPP16ChargePointStatus.Unavailable },
+            { triggerMessage: true },
+          ],
+        ]
+      )
+    })
+
     await it('should handle requestHandler rejection gracefully', async () => {
       const rejectingMock = mock.fn(async () => Promise.reject(new Error('test error')))
       const { station: rejectStation } = createMockChargingStation({
index fb1a676232691e3fe8b6c2099ac4eafba21423cb..86f993a671158b08542b0064aec123ebe087ee6e 100644 (file)
@@ -81,18 +81,18 @@ const configureMeterValueTemplates = (station: ChargingStation): void => {
   const templates: SampledValueTemplate[] = [
     {
       measurand: MeterValueMeasurand.STATE_OF_CHARGE,
-      unit: 'Percent',
-    } as unknown as SampledValueTemplate,
-    { measurand: MeterValueMeasurand.VOLTAGE, unit: 'V' } as unknown as SampledValueTemplate,
+      unit: OCPP16MeterValueUnit.PERCENT,
+    },
+    { measurand: MeterValueMeasurand.VOLTAGE, unit: OCPP16MeterValueUnit.VOLT },
     {
       measurand: MeterValueMeasurand.POWER_ACTIVE_IMPORT,
-      unit: 'W',
-    } as unknown as SampledValueTemplate,
-    { measurand: MeterValueMeasurand.CURRENT_IMPORT, unit: 'A' } as unknown as SampledValueTemplate,
+      unit: OCPP16MeterValueUnit.WATT,
+    },
+    { measurand: MeterValueMeasurand.CURRENT_IMPORT, unit: OCPP16MeterValueUnit.AMP },
     {
       measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
       unit: OCPP16MeterValueUnit.WATT_HOUR,
-    } as unknown as SampledValueTemplate,
+    },
   ]
   const connectorStatus = station.getConnectorStatus(CONNECTOR_ID)
   if (connectorStatus != null) {
diff --git a/tests/charging-station/ocpp/1.6/OCPP16RequestBuilders.test.ts b/tests/charging-station/ocpp/1.6/OCPP16RequestBuilders.test.ts
new file mode 100644 (file)
index 0000000..5c18562
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * @file Tests for OCPP16RequestBuilders
+ * @description Verifies OCPP 1.6 version-specific pure builders
+ */
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+
+import { buildOCPP16SampledValue } from '../../../../src/charging-station/ocpp/1.6/OCPP16RequestBuilders.js'
+import { BaseError } from '../../../../src/exception/index.js'
+import {
+  OCPP16MeterValueMeasurand,
+  OCPP16MeterValueUnit,
+  type SampledValueTemplate,
+} from '../../../../src/types/index.js'
+
+await describe('OCPP16RequestBuilders', async () => {
+  await describe('buildOCPP16SampledValue', async () => {
+    for (const unit of [OCPP16MeterValueUnit.TEMP_CELCIUS, OCPP16MeterValueUnit.TEMP_CELSIUS]) {
+      await it(`should preserve the schema-supported ${unit} temperature unit`, () => {
+        const sampledValue = buildOCPP16SampledValue({ unit }, 21)
+
+        assert.strictEqual(sampledValue.unit, unit)
+      })
+    }
+
+    await it('should omit the OCPP 2.0-only default unit for OCPP 1.6 Frequency', () => {
+      const sampledValue = buildOCPP16SampledValue(
+        { measurand: OCPP16MeterValueMeasurand.FREQUENCY },
+        50
+      )
+
+      assert.strictEqual(sampledValue.unit, undefined)
+    })
+
+    await it('should reject an OCPP 2.0-only unit descriptor', () => {
+      assert.throws(
+        () =>
+          buildOCPP16SampledValue(
+            {
+              unitOfMeasure: { multiplier: 3, unit: OCPP16MeterValueUnit.WATT_HOUR },
+            },
+            1
+          ),
+        (error: unknown) =>
+          error instanceof BaseError && error.message.includes('sampled value unitOfMeasure')
+      )
+    })
+
+    for (const fieldName of ['context', 'location', 'measurand', 'phase', 'unit'] as const) {
+      await it(`should reject an invalid supplied ${fieldName}`, () => {
+        const template = { [fieldName]: 'Invalid' } as unknown as SampledValueTemplate
+
+        assert.throws(
+          () => buildOCPP16SampledValue(template, 1),
+          (error: unknown) =>
+            error instanceof BaseError && error.message.includes(`sampled value ${fieldName}`)
+        )
+      })
+    }
+  })
+})
index cac239b72b9e0d28baed1ab00fae4c37dc3cf7c1..13b6223ef1f21a6bc6268890cfcaa1205dd096f4 100644 (file)
@@ -198,6 +198,33 @@ await describe('OCPP 1.6 — Signed MeterValues', async () => {
       assert.strictEqual(signedSamples[0].context, OCPP16MeterValueContext.TRANSACTION_BEGIN)
     })
 
+    await it('should not sign when the configured signing method is invalid', () => {
+      const connectorStatus = station.getConnectorStatus(1)
+      assert.ok(connectorStatus != null)
+      connectorStatus.transactionId = 42
+      upsertConfigurationKey(station, OCPP16VendorParametersKey.SampledDataSignReadings, 'true')
+      upsertConfigurationKey(
+        station,
+        OCPP16VendorParametersKey.SampledDataSignStartedReadings,
+        'true'
+      )
+      upsertConfigurationKey(
+        station,
+        `${OCPP16VendorParametersKey.MeterPublicKey}1`,
+        TEST_PUBLIC_KEY_HEX
+      )
+      upsertConfigurationKey(station, OCPP16VendorParametersKey.SigningMethod, 'invalid')
+
+      const meterValue = OCPP16ServiceUtils.buildTransactionBeginMeterValue(station, 1, 5000)
+
+      assert.strictEqual(
+        meterValue.sampledValue.some(
+          sampledValue => sampledValue.format === OCPP16MeterValueFormat.SIGNED_DATA
+        ),
+        false
+      )
+    })
+
     await it('should project only the ordinary DC Inlet begin sample while signing the raw Outlet reading', () => {
       assert.ok(station.stationInfo != null)
       station.stationInfo.conversionEfficiency = 0.8
index 9a7a148726172c3fe2f3ac989be8ec807d2722e8..df8e34c1de81379ba25cc18f9d507f737e8ae978 100644 (file)
@@ -48,6 +48,7 @@ import {
   type OCPP16StopTransactionResponse,
   OCPP16SupportedFeatureProfiles,
   OCPP16VendorParametersKey,
+  OCPP20ReasonEnumType,
   OCPPVersion,
   type RequestParams,
 } from '../../../../src/types/index.js'
@@ -729,6 +730,37 @@ await describe('OCPP16ServiceUtils — pure functions', async () => {
       )
     }
 
+    for (const invalidReason of [
+      'InvalidReason',
+      OCPP20ReasonEnumType.EnergyLimitReached,
+    ] as const) {
+      await it(`should reject unsupported reason '${invalidReason}' before mutating transaction state`, async () => {
+        const requestHandler = mock.fn(() => Promise.resolve({}))
+        const { station } = createMockChargingStation({
+          ocppRequestService: { requestHandler },
+          ocppVersion: OCPPVersion.VERSION_16,
+        })
+        setupConnectorWithTransaction(station, 1, { transactionId: 100 })
+        const connectorStatus = station.getConnectorStatus(1)
+        assert.ok(connectorStatus != null)
+
+        const requestOverrides = {
+          reason: invalidReason,
+        } as unknown as Partial<OCPP16StopTransactionRequest>
+
+        await assert.rejects(
+          OCPP16ServiceUtils.stopTransactionOnConnector(station, 1, undefined, requestOverrides),
+          (error: unknown) =>
+            error instanceof OCPPError &&
+            error.code === ErrorType.FORMAT_VIOLATION &&
+            error.message.includes('Invalid StopTransaction reason')
+        )
+
+        assert.notStrictEqual(connectorStatus.transactionEnding, true)
+        assert.strictEqual(requestHandler.mock.callCount(), 0)
+      })
+    }
+
     await it('should suppress periodic MeterValues while a transaction is ending', t => {
       t.mock.timers.enable({ apis: ['setInterval'] })
       const requestHandler = mock.fn(() => Promise.resolve({}))
index 4db42dad7741d153c0ac5154ecc9b8432225ba25..92d9ace62201683fb0f91d7bc076937a35911d80 100644 (file)
@@ -8,7 +8,6 @@ import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type {
   ConnectorStatus,
-  EvseStatus,
   OCPP20FirmwareStatusNotificationRequest,
   OCPP20MeterValuesRequest,
   OCPP20TransactionEventRequest,
@@ -43,6 +42,7 @@ import {
   OCPP20RequiredVariableName,
   OCPP20TransactionEventEnumType,
   OCPP20TriggerReasonEnumType,
+  OCPP20UnitEnumType,
   OCPPVersion,
   PublicKeyWithSignedMeterValueEnumType,
   ReasonCodeEnumType,
@@ -83,9 +83,9 @@ function addTriggerMessageConnector (
   connectorStatus.MeterValues = [
     {
       measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-      unit: 'Wh',
+      unit: OCPP20UnitEnumType.WATT_HOUR,
     },
-  ] as unknown as NonNullable<EvseStatus['MeterValues']>
+  ]
   connectorStatus.energyActiveImportRegisterValue = energyValue
   evseStatus.connectors.set(connectorId, connectorStatus)
   return connectorStatus
@@ -162,9 +162,9 @@ function createTriggerMessageStation (): {
     connectorStatus.MeterValues = [
       {
         measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-        unit: 'Wh',
+        unit: OCPP20UnitEnumType.WATT_HOUR,
       },
-    ] as unknown as NonNullable<EvseStatus['MeterValues']>
+    ]
     connectorStatus.energyActiveImportRegisterValue = (evseId ?? 0) * 10
   }
   return { mockStation, requestHandlerMock }
@@ -608,9 +608,9 @@ await describe('F06 - TriggerMessage', async () => {
         {
           location: OCPP20LocationEnumType.Inlet,
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
-          unit: 'Wh',
+          unit: OCPP20UnitEnumType.WATT_HOUR,
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connectorStatus.transactionEnergyActiveImportRegisterValue = 150
       connectorStatus.transactionEnergyActiveImportIntervalBaselines = { [baselineKey]: 60 }
       connectorStatus.transactionEnergyActiveImportIntervalCarry = { [baselineKey]: 0 }
@@ -749,9 +749,9 @@ await describe('F06 - TriggerMessage', async () => {
         connectorStatus.MeterValues = [
           {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-            unit: 'Wh',
+            unit: OCPP20UnitEnumType.WATT_HOUR,
           },
-        ] as unknown as NonNullable<EvseStatus['MeterValues']>
+        ]
         connectorStatus.transactionId = `duplicate-response-${evseId.toString()}`
         connectorStatus.transactionStarted = true
       }
@@ -1458,9 +1458,9 @@ await describe('F06 - TriggerMessage', async () => {
       connectorStatus.MeterValues = [
         {
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
-          unit: 'Wh',
+          unit: OCPP20UnitEnumType.WATT_HOUR,
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connectorStatus.transactionEnergyActiveImportRegisterValue = 0
       connectorStatus.transactionEnergyActiveImportIntervalBaselines = { [baselineKey]: 0 }
       connectorStatus.transactionEnergyActiveImportIntervalCarry = { [baselineKey]: 0 }
@@ -1556,14 +1556,14 @@ await describe('F06 - TriggerMessage', async () => {
         {
           fluctuationPercent: 0,
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
-          unit: 'Wh',
+          unit: OCPP20UnitEnumType.WATT_HOUR,
         },
         {
           fluctuationPercent: 0,
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
-          unit: 'Wh',
+          unit: OCPP20UnitEnumType.WATT_HOUR,
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connectorStatus.transactionEnergyActiveImportRegisterValue = 150
       // Pin the register read ahead of the sample window so the coalesced interval energy
       // reflects only the register delta, never wall-clock proration (deterministic on any host).
@@ -1974,7 +1974,7 @@ await describe('F06 - TriggerMessage', async () => {
     function seedActiveTransaction (evseId: number, transactionId: string): void {
       const evseStatus = mockStation.getEvseStatus(evseId)
       if (evseStatus != null) {
-        evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as EvseStatus['MeterValues']
+        evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       }
       setupConnectorWithTransaction(mockStation, evseId, {
         energyImport: 1234,
@@ -2420,7 +2420,7 @@ await describe('F06 - TriggerMessage', async () => {
       assert.ok(evseStatus != null && connectorStatus != null)
       evseStatus.connectors.delete(2)
       evseStatus.connectors.set(1, connectorStatus)
-      evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as EvseStatus['MeterValues']
+      evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       connectorStatus.transactionStarted = true
       connectorStatus.transactionId = 'txn-evse-2-local-connector'
       connectorStatus.transactionEnergyActiveImportRegisterValue = 1234
@@ -2522,7 +2522,7 @@ await describe('F06 - TriggerMessage', async () => {
       })
       const rejectEvseStatus = rejectStation.getEvseStatus(1)
       if (rejectEvseStatus != null) {
-        rejectEvseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as EvseStatus['MeterValues']
+        rejectEvseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       }
       setupConnectorWithTransaction(rejectStation, 1, {
         energyImport: 1234,
index 06423a276fb696a2c00f94f228196be52cc6902d..12fadaaa6ffcf4d05559729795732b6f38fcd4b6 100644 (file)
@@ -167,7 +167,7 @@ await describe('OCPP 2.0 Integration — SetVariables → GetVariables consisten
   })
 
   await it('should reject SetVariables on an unknown component and confirm GetVariables returns UnknownComponent', () => {
-    const unknownComponent = { name: 'NonExistentComponent' as OCPP20ComponentName }
+    const unknownComponent = { name: 'NonExistentComponent' as unknown as OCPP20ComponentName }
     const variableName = 'SomeVariable'
 
     // Attempt to set a variable on a component that does not exist in the registry
index 79c078ac2031ab3da547e87e00c54c3a17a3543b..d81a094dcce1e9b495f8f8c0c13ae4296d275651 100644 (file)
@@ -36,13 +36,13 @@ const energyTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
   unit: 'Wh',
   value: '0',
-} as unknown as SampledValueTemplate
+}
 
 const voltageTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.VOLTAGE,
   unit: 'V',
   value: '230',
-} as unknown as SampledValueTemplate
+}
 
 await describe('OCPP 2.0 Signed Meter Values', async () => {
   await describe('buildOCPP20SampledValue with signing config', async () => {
index ae61f00c5d579493542d6bf090d3601458ce12f8..8f9a4d474c31e34c81ac3e171174962fe237d8c2 100644 (file)
@@ -9,9 +9,19 @@
 import assert from 'node:assert/strict'
 import { afterEach, describe, it } from 'node:test'
 
-import type { StopTransactionReason } from '../../../../src/types/index.js'
-
-import { mapStopReasonToOCPP20 } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestBuilders.js'
+import {
+  buildOCPP20SampledValue,
+  mapStopReasonToOCPP20,
+} from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestBuilders.js'
+import { BaseError } from '../../../../src/exception/index.js'
+import {
+  OCPP16MeterValueMeasurand,
+  OCPP16StopTransactionReason,
+  OCPP20MeasurandEnumType,
+  OCPP20UnitEnumType,
+  PublicKeyWithSignedMeterValueEnumType,
+  type SampledValueTemplate,
+} from '../../../../src/types/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 
 await describe('OCPP20RequestBuilders', async () => {
@@ -19,9 +29,68 @@ await describe('OCPP20RequestBuilders', async () => {
     standardCleanup()
   })
 
+  await describe('buildOCPP20SampledValue', async () => {
+    await it('should reject an OCPP 1.6-only measurand', () => {
+      assert.throws(
+        () => buildOCPP20SampledValue({ measurand: OCPP16MeterValueMeasurand.FAN_RPM }, 1),
+        (error: unknown) =>
+          error instanceof BaseError && error.message.includes('sampled value measurand')
+      )
+    })
+
+    await it('should reject an invalid measurand', () => {
+      const template = { measurand: 'Invalid' } as unknown as SampledValueTemplate
+
+      assert.throws(
+        () => buildOCPP20SampledValue(template, 1),
+        (error: unknown) =>
+          error instanceof BaseError && error.message.includes('sampled value measurand')
+      )
+    })
+
+    await it('should preserve a custom unit string', () => {
+      const { sampledValue } = buildOCPP20SampledValue({ unit: 'custom-unit' }, 1)
+
+      assert.deepStrictEqual(sampledValue.unitOfMeasure, { unit: 'custom-unit' })
+    })
+
+    await it('should preserve physical quantity across standardized unit multipliers', () => {
+      const { sampledValue } = buildOCPP20SampledValue(
+        {
+          measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
+          unit: OCPP20UnitEnumType.WATT_HOUR,
+          unitOfMeasure: { multiplier: 3, unit: OCPP20UnitEnumType.KILO_WATT_HOUR },
+        },
+        2_500_000,
+        undefined,
+        undefined,
+        {
+          enabled: true,
+          meterSerialNumber: 'SIM-METER-001',
+          publicKeySentInTransaction: false,
+          publicKeyWithSignedMeterValue: PublicKeyWithSignedMeterValueEnumType.Never,
+          transactionId: 'tx-1',
+        }
+      )
+
+      assert.strictEqual(sampledValue.value, 2.5)
+      assert.deepStrictEqual(sampledValue.unitOfMeasure, {
+        multiplier: 3,
+        unit: OCPP20UnitEnumType.KILO_WATT_HOUR,
+      })
+      assert.strictEqual(sampledValue.value * 1_000 * 10 ** 3, 2_500_000)
+      assert.match(
+        Buffer.from(sampledValue.signedMeterValue?.signedMeterData ?? '', 'base64').toString(
+          'utf8'
+        ),
+        /"RV":2500/u
+      )
+    })
+  })
+
   await describe('mapStopReasonToOCPP20', async () => {
     await it('should map Other to Other/AbnormalCondition', () => {
-      const result = mapStopReasonToOCPP20('Other' as StopTransactionReason)
+      const result = mapStopReasonToOCPP20(OCPP16StopTransactionReason.OTHER)
 
       assert.strictEqual(result.stoppedReason, 'Other')
       assert.strictEqual(result.triggerReason, 'AbnormalCondition')
@@ -35,7 +104,7 @@ await describe('OCPP20RequestBuilders', async () => {
     })
 
     await it('should map Remote to Remote/RemoteStop', () => {
-      const result = mapStopReasonToOCPP20('Remote' as StopTransactionReason)
+      const result = mapStopReasonToOCPP20(OCPP16StopTransactionReason.REMOTE)
 
       assert.strictEqual(result.stoppedReason, 'Remote')
       assert.strictEqual(result.triggerReason, 'RemoteStop')
index 8c01efcc597b5c8686d5f8e5b8606055f4230c6c..59602f372d87e45b12f635cfa3a5e7d8611c647b 100644 (file)
@@ -214,7 +214,7 @@ function createAlignedStation (
   for (const evseId of evseIds) {
     const evseStatus = mockStation.getEvseStatus(evseId)
     if (evseStatus != null) {
-      evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as EvseStatus['MeterValues']
+      evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
     }
   }
   const seedRegister = (connectorId: number, value: number): void => {
@@ -359,7 +359,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       evseStatus.MeterValues = []
       connector1.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
         {
@@ -368,7 +368,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '230',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -409,14 +409,14 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           customData: { vendorId: 'sensor-b' },
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -460,7 +460,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '210',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -468,7 +468,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '240',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, ALIGNED_MEASURANDS_KEY, OCPP20MeasurandEnumType.VOLTAGE)
@@ -516,7 +516,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(connector2 != null)
       stationEvse.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_REACTIVE_IMPORT_REGISTER, unit: 'varh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       evseStatus.MeterValues = []
       connector1.MeterValues = [
         {
@@ -525,7 +525,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'varh',
           value: '1000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -533,7 +533,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'varh',
           value: '2000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -582,7 +582,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit,
             value,
           },
-        ] as unknown as NonNullable<EvseStatus['MeterValues']>
+        ]
       }
       for (const evseId of [1, 2]) {
         const evseStatus = mockStation.getEvseStatus(evseId)
@@ -628,7 +628,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
           unit: 'W',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       evse1.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -644,7 +644,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '9000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       evse2.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -653,7 +653,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '2000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -727,12 +727,12 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             value: outletValue.toString(),
           },
         ]
-      ) as unknown as NonNullable<EvseStatus['MeterValues']>
+      )
       stationEvse.MeterValues = sourcePairs.map(([measurand, unit]) => ({
         location: OCPP20LocationEnumType.Outlet,
         measurand,
         unit,
-      })) as unknown as NonNullable<EvseStatus['MeterValues']>
+      }))
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -804,12 +804,12 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '400',
         },
       ]
-      physicalEvse.MeterValues = templates as unknown as NonNullable<EvseStatus['MeterValues']>
+      physicalEvse.MeterValues = templates
       stationEvse.MeterValues = templates.map(template => ({
         ...template,
         location: OCPP20LocationEnumType.Inlet,
         value: undefined,
-      })) as unknown as NonNullable<EvseStatus['MeterValues']>
+      }))
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -865,7 +865,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'kW',
           value: '1',
         },
-      ] as unknown as ConnectorStatus['MeterValues']
+      ]
       const stationMeter = mockStation.getEvseStatus(0)
       assert.ok(stationMeter != null)
       stationMeter.MeterValues = [
@@ -875,7 +875,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '0',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -913,7 +913,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Percent',
           value: '50',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       stationMeter.MeterValues = []
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-soc' })
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
@@ -956,7 +956,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit,
             value,
           },
-        ] as unknown as NonNullable<EvseStatus['MeterValues']>
+        ]
       }
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
@@ -999,7 +999,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit,
             value: value.toString(),
           },
-        ] as unknown as NonNullable<EvseStatus['MeterValues']>
+        ]
       }
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
@@ -1107,7 +1107,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
               unit: 'varh',
               value: '1000',
             },
-          ] as unknown as NonNullable<ConnectorStatus['MeterValues']>
+          ]
         }
       }
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-1' })
@@ -1199,7 +1199,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -1213,7 +1213,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-suppressed' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -1280,7 +1280,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-independent-cadences' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -1371,7 +1371,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
             unit: 'Wh',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         evseStatus.MeterValues = [
           {
             fluctuationPercent: 0,
@@ -1379,7 +1379,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit: 'Wh',
             value: '60',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         const transactionId = `tx-station-cadence-${String(meteringPerTransaction)}`
         const baselineTimestamp = Date.now()
         setupConnectorWithTransaction(mockStation, 1, { transactionId })
@@ -1456,7 +1456,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '230',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       connectorStatus.energyActiveImportRegisterValue = 115
       connectorStatus.energyActiveImportIntervalBaselines = {
         [`station:${ALIGNED_MEASURANDS_KEY}`]: 100,
@@ -1496,7 +1496,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       const activeAt = new Date(60_000)
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-active-idle-baseline' })
       connectorStatus.energyActiveImportRegisterValue = 40
@@ -1553,7 +1553,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.energyActiveImportIntervalBaseline = 80
       connector1.energyActiveImportRegisterValue = 40
       connector2.energyActiveImportRegisterValue = 60
@@ -1609,7 +1609,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       connector1.energyActiveImportRegisterValue = 10
       connector2.energyActiveImportRegisterValue = 30
       connector1.energyActiveImportIntervalBaselines = {
@@ -1663,7 +1663,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: OCPP20UnitEnumType.WATT_HOUR,
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       const stationBaselineKey = `station:${ALIGNED_MEASURANDS_KEY}`
       connectorStatus.energyActiveImportRegisterValue = 10
       connectorStatus.energyActiveImportIntervalBaselines = {
@@ -1754,7 +1754,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-shared-evse-restore' })
       const connector1 = mockStation.getConnectorStatus(1, 1)
       const connector2 = mockStation.getConnectorStatus(2, 1)
@@ -1991,7 +1991,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       connectorStatus.energyActiveImportRegisterValue = 40
       connectorStatus.transactionEnergyActiveImportRegisterValue = 0
       connectorStatus.energyActiveImportIntervalBaselines = {
@@ -2104,7 +2104,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(stationEvseStatus != null)
       assert.ok(evseStatus != null)
       assert.ok(connectorStatus != null)
-      stationEvseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as EvseStatus['MeterValues']
+      stationEvseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -2112,7 +2112,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '0',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       connectorStatus.energyActiveImportRegisterValue = 40
       connectorStatus.transactionEnergyActiveImportRegisterValue = 0
       connectorStatus.energyActiveImportIntervalBaselines = {
@@ -2192,7 +2192,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-between-slots' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -2738,7 +2738,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_REACTIVE_IMPORT_INTERVAL,
           unit: 'varh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -2766,7 +2766,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'varh',
           value: '5',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-coalesced' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -2857,7 +2857,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -2865,7 +2865,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.energyActiveImportRegisterLastUpdatedAt = new Date(0)
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-meter-values-retry' })
       let failed = false
@@ -3167,6 +3167,26 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.strictEqual(energySample.value, 1234)
     })
 
+    await it('should not sign active aligned samples when the configured signing method is invalid', async () => {
+      const { mockStation, requestHandlerMock } = alignedStation
+      upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
+      upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
+      upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
+      upsertConfigurationKey(mockStation, SIGN_READINGS_KEY, 'true')
+      upsertConfigurationKey(mockStation, SIGN_UPDATED_READINGS_KEY, 'true')
+      upsertConfigurationKey(mockStation, FISCAL_PUBLIC_KEY, TEST_PUBLIC_KEY_HEX)
+      upsertConfigurationKey(mockStation, FISCAL_SIGNING_METHOD, 'invalid')
+      setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-invalid-method' })
+
+      await OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
+
+      const samples = sentTransactionEvents(requestHandlerMock).flatMap(transactionEvent =>
+        (transactionEvent.meterValue ?? []).flatMap(meterValue => meterValue.sampledValue)
+      )
+      assert.ok(samples.length > 0)
+      assert.ok(samples.every(sample => sample.signedMeterValue == null))
+    })
+
     await it('should not sign active aligned samples when SignUpdatedReadings is disabled', async () => {
       const { mockStation, requestHandlerMock } = alignedStation
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
@@ -3257,7 +3277,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         })
-      ) as unknown as NonNullable<EvseStatus['MeterValues']>
+      )
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-coherent' })
       if (mockStation.stationInfo != null) mockStation.stationInfo.meteringPerTransaction = true
       const coherentConnectorStatus = mockStation.getConnectorStatus(1)
@@ -3626,7 +3646,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '90')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -3701,7 +3721,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '1000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -3732,7 +3752,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       mock.method(mockStation, 'getConnectorMaximumAvailablePower', () => 3600)
       evseStatus.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -3790,7 +3810,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '230',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -3825,7 +3845,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '230',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(mockStation, SEND_DURING_IDLE_KEY, 'false')
@@ -3887,7 +3907,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '1000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       setupConnectorWithTransaction(mockStation, 1, {
@@ -3925,7 +3945,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '1000',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -4709,7 +4729,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       stationConnectorStatus.energyActiveImportRegisterValue = 7
 
       void OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
@@ -4761,7 +4781,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
             unit: 'W',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         sourceEvse.MeterValues = [
           {
             fluctuationPercent: 0,
@@ -4770,7 +4790,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit: 'W',
             value: '800',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         setupConnectorWithTransaction(mockStation, 1, {
           transactionId: `tx-station-power-${location}`,
         })
@@ -4814,7 +4834,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
           unit: 'W',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       sourceEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -4823,7 +4843,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '800',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, {
         transactionId: 'tx-station-outlet-power',
       })
@@ -4880,7 +4900,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       sourceEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -4889,7 +4909,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '800',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-station-energy' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -4950,7 +4970,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         fluctuationPercent: 0,
         location: OCPP20LocationEnumType.Inlet,
         measurand,
-      })) as NonNullable<EvseStatus['MeterValues']>
+      }))
       sourceEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -5031,7 +5051,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         location: OCPP20LocationEnumType.Outlet,
         measurand,
         phase: MeterValuePhase.L1_N,
-      })) as NonNullable<EvseStatus['MeterValues']>
+      }))
       sourceEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -5075,7 +5095,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(sourceEvse != null)
       stationEvse.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT, unit: 'W' },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       sourceEvse.MeterValues = [
         MeterValuePhase.L1_N,
         MeterValuePhase.L2_N,
@@ -5086,7 +5106,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'W',
         value: '1000',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-phased-power' })
 
       void OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
@@ -5118,7 +5138,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '9000',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       sourceEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -5127,7 +5147,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '1000',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-partial-phase' })
 
       void OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
@@ -5160,7 +5180,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'varh',
           value: '1000',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
 
       void OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
 
@@ -5187,8 +5207,8 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(stationEvse != null)
       assert.ok(sourceEvse != null)
       stationEvse.MeterValues = [
-        { measurand: OCPP20MeasurandEnumType.CURRENT_IMPORT, unit: 'A' },
-      ] as unknown as EvseStatus['MeterValues']
+        { measurand: OCPP20MeasurandEnumType.CURRENT_IMPORT, unit: OCPP20UnitEnumType.AMP },
+      ]
       sourceEvse.MeterValues = [
         MeterValuePhase.L1_N,
         MeterValuePhase.L2_N,
@@ -5199,7 +5219,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'A',
         value: '5',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-current' })
 
       void OCPP20ServiceUtils.emitClockAlignedMeterValues(mockStation)
@@ -5222,8 +5242,8 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       const stationEvse = mockStation.getEvseStatus(0)
       assert.ok(stationEvse != null)
       stationEvse.MeterValues = [
-        { measurand: OCPP20MeasurandEnumType.CURRENT_IMPORT, unit: 'A' },
-      ] as unknown as EvseStatus['MeterValues']
+        { measurand: OCPP20MeasurandEnumType.CURRENT_IMPORT, unit: OCPP20UnitEnumType.AMP },
+      ]
       const phaseValues = [
         [MeterValuePhase.L1_N, 10],
         [MeterValuePhase.L2_N, 20],
@@ -5241,7 +5261,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit: 'A',
             value: value.toString(),
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         setupConnectorWithTransaction(mockStation, evseId, {
           transactionId: `tx-cross-evse-current-${evseId.toString()}`,
         })
@@ -5284,7 +5304,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       ] as const
       stationEvse.MeterValues = measurands.map(measurand => ({
         measurand,
-      })) as EvseStatus['MeterValues']
+      }))
       const phaseValues = [
         [MeterValuePhase.L1, 10],
         [MeterValuePhase.L1_N, 20],
@@ -5299,7 +5319,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: measurand === OCPP20MeasurandEnumType.CURRENT_IMPORT ? 'A' : 'W',
           value: value.toString(),
         }))
-      ) as unknown as EvseStatus['MeterValues']
+      )
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-phase-aliases' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -5347,7 +5367,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(phasedEvse != null)
       stationEvse.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT, unit: 'W' },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       aggregateEvse.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -5355,7 +5375,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '3000',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       phasedEvse.MeterValues = [
         MeterValuePhase.L1_N,
         MeterValuePhase.L2_N,
@@ -5366,7 +5386,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'W',
         value: '1000',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-aggregate-power' })
       setupConnectorWithTransaction(mockStation, 2, { transactionId: 'tx-phased-power' })
 
@@ -5406,7 +5426,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             unit: 'W',
             value: '2000',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
       }
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-custom-data' })
 
@@ -5451,7 +5471,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
           unit: 'W',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       sourceEvse.MeterValues = []
       connector1.MeterValues = [
         {
@@ -5461,7 +5481,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'W',
           value: '1000',
         },
-      ] as unknown as ConnectorStatus['MeterValues']
+      ]
       connector2.MeterValues = [
         {
           customData: (() => {
@@ -5545,7 +5565,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         })
-      ) as unknown as EvseStatus['MeterValues']
+      )
       connectorStatus.energyActiveImportRegisterValue = 800
       upsertConfigurationKey(
         mockStation,
@@ -5597,7 +5617,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '800',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-main-energy' })
       upsertConfigurationKey(
         mockStation,
@@ -5664,7 +5684,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '230',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         buildConfigKey(OCPP20ComponentName.AlignedDataCtrlr, OCPP20RequiredVariableName.Measurands),
@@ -5768,7 +5788,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       assert.ok(evseStatus != null)
       evseStatus.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, {
         transactionId: '00000000-0000-4000-8000-000000000123',
       })
@@ -5815,7 +5835,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-shared-energy-1' })
       setupConnectorWithTransaction(mockStation, 2, { transactionId: 'tx-shared-energy-2' })
       const connector1 = mockStation.getConnectorStatus(1, 1)
@@ -5883,7 +5903,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -5937,7 +5957,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -5981,7 +6001,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6086,7 +6106,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6175,7 +6195,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '60',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6356,7 +6376,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
             unit: 'Wh',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         upsertConfigurationKey(
           mockStation,
           TX_UPDATED_MEASURANDS_KEY,
@@ -6422,7 +6442,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6510,7 +6530,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6602,7 +6622,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'Wh',
         value: '90',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6652,7 +6672,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6699,7 +6719,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -6712,7 +6732,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6840,7 +6860,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -6929,7 +6949,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -7026,8 +7046,8 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
       }
       stationEvse.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL, unit: 'Wh' },
-      ] as unknown as EvseStatus['MeterValues']
-      evseStatus.MeterValues = [intervalTemplate] as unknown as EvseStatus['MeterValues']
+      ]
+      evseStatus.MeterValues = [intervalTemplate]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(
         mockStation,
@@ -7088,7 +7108,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -7157,7 +7177,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         ALIGNED_MEASURANDS_KEY,
@@ -7214,7 +7234,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         location: OCPP20LocationEnumType.Inlet,
         measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
         unit: 'Wh',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -7230,7 +7250,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         })),
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -7297,7 +7317,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -7372,7 +7392,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '10',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -7555,7 +7575,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
           unit: 'W',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       evseStatus.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -7574,7 +7594,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.POWER_ACTIVE_IMPORT,
           unit: 'W',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(mockStation, ALIGNED_DATA_INTERVAL_KEY, '60')
       upsertConfigurationKey(mockStation, ALIGNED_ENABLED_KEY, 'true')
       upsertConfigurationKey(
@@ -7661,7 +7681,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       upsertConfigurationKey(
         mockStation,
         TX_UPDATED_MEASURANDS_KEY,
@@ -7804,10 +7824,10 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'varh',
           value: '1000',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       stationEvse.MeterValues = [
         { measurand: OCPP20MeasurandEnumType.ENERGY_REACTIVE_IMPORT_REGISTER, unit: 'varh' },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-shared-register-1' })
       setupConnectorWithTransaction(mockStation, 2, { transactionId: 'tx-shared-register-2' })
 
@@ -7847,7 +7867,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -7887,7 +7907,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '80',
         },
-      ] as unknown as EvseStatus['MeterValues']
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-dc-inlet-interval' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -7948,7 +7968,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         },
         { measurand: OCPP20MeasurandEnumType.VOLTAGE },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -7988,7 +8008,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Hz',
           value: '50',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -8077,7 +8097,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
             phase,
             unit: 'Wh',
-          })) as unknown as EvseStatus['MeterValues'],
+          })),
           timestamp: new Date(60_000),
         },
         60_000,
@@ -8112,7 +8132,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'Wh',
         value: '90',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -8158,7 +8178,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         phase,
         unit: 'Wh',
         value: '90',
-      })) as unknown as EvseStatus['MeterValues']
+      }))
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-aligned-phase-interval' })
       const connectorStatus = mockStation.getConnectorStatus(1, 1)
       assert.ok(connectorStatus != null)
@@ -8231,7 +8251,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: 'Wh',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       setupConnectorWithTransaction(mockStation, 1, { transactionId: 'tx-phase-only' })
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
@@ -8367,7 +8387,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '1000',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -8376,7 +8396,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '2000',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       setupConnectorWithTransaction(mockStation, 2, { transactionId: 'tx-2' })
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
@@ -8417,7 +8437,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '100',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           customData: { vendorId: 'connector-2' },
@@ -8426,7 +8446,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'Wh',
           value: '200',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       setupConnectorWithTransaction(mockStation, 2, { transactionId: 'tx-2' })
       upsertConfigurationKey(
         mockStation,
@@ -8521,7 +8541,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '211',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       connector2.MeterValues = [
         {
           fluctuationPercent: 0,
@@ -8543,7 +8563,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '240',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -8902,7 +8922,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           phase: MeterValuePhase.L2_N,
           unit: 'V',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const invalidSinglePhaseMeterValue = buildClockAlignedConnectorMeterValue(
         mockStation,
         { connectorId: 1, evseId: 1 },
@@ -8944,7 +8964,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           phase: MeterValuePhase.N,
           unit: 'V',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const dcMeterValue = buildClockAlignedConnectorMeterValue(
         mockStation,
         { connectorId: 1, evseId: 1 },
@@ -8975,7 +8995,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           value: '230',
         },
         { measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER, unit: 'Wh' },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
@@ -9066,6 +9086,63 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
         false
       )
     })
+
+    await it('should preserve custom unit metadata and physical values on automatic voltage phases', () => {
+      // Arrange
+      const { mockStation } = createAlignedStation({ connectorsCount: 1, evsesCount: 1 })
+      mock.method(mockStation, 'getNumberOfPhases', () => 3)
+      assert.ok(mockStation.stationInfo != null)
+      mockStation.stationInfo.currentOutType = CurrentType.AC
+      mockStation.stationInfo.mainVoltageMeterValues = false
+      const evseStatus = mockStation.getEvseStatus(1)
+      assert.ok(evseStatus != null)
+      evseStatus.MeterValues = [
+        {
+          fluctuationPercent: 0,
+          measurand: OCPP20MeasurandEnumType.VOLTAGE,
+          unit: 'custom-voltage',
+          unitOfMeasure: {
+            customData: { vendorId: 'custom-meter' },
+            multiplier: 1,
+            unit: 'custom-voltage',
+          },
+          value: '23',
+        },
+      ]
+      const measurandsKey = buildConfigKey(
+        OCPP20ComponentName.AlignedDataCtrlr,
+        OCPP20RequiredVariableName.Measurands
+      )
+      upsertConfigurationKey(mockStation, measurandsKey, OCPP20MeasurandEnumType.VOLTAGE)
+
+      // Act
+      const meterValue = buildClockAlignedConnectorMeterValue(
+        mockStation,
+        { connectorId: 1, evseId: 1 },
+        60_000,
+        measurandsKey,
+        OCPP20ReadingContextEnumType.SAMPLE_CLOCK
+      )
+
+      // Assert
+      assert.deepStrictEqual(
+        meterValue.sampledValue.map(sample => ({
+          phase: sample.phase,
+          physicalValue: sample.value * 10,
+          unitOfMeasure: sample.unitOfMeasure,
+        })),
+        [MeterValuePhase.L1_N, MeterValuePhase.L2_N, MeterValuePhase.L3_N].map(phase => ({
+          phase,
+          physicalValue: 23,
+          unitOfMeasure: {
+            customData: { vendorId: 'custom-meter' },
+            multiplier: 1,
+            unit: 'custom-voltage',
+          },
+        }))
+      )
+    })
+
     await it('should not suppress automatic voltage phases across customData identities', () => {
       const { mockStation } = createAlignedStation({ connectorsCount: 1, evsesCount: 1 })
       mock.method(mockStation, 'getNumberOfPhases', () => 3)
@@ -9091,7 +9168,7 @@ await describe('J01 - Autonomous clock-aligned MeterValues (#2011 Category 2F)',
           unit: 'V',
           value: '231',
         },
-      ] as unknown as NonNullable<EvseStatus['MeterValues']>
+      ]
       const measurandsKey = buildConfigKey(
         OCPP20ComponentName.AlignedDataCtrlr,
         OCPP20RequiredVariableName.Measurands
index 24acb93a31e69e75009860a279574e1cad8ffb64..217eba5fea04e988c15d72876beb86f2e76e6152 100644 (file)
@@ -14,7 +14,7 @@ import assert from 'node:assert/strict'
 import { afterEach, beforeEach, describe, it, mock } from 'node:test'
 
 import type { CoherentSession } from '../../../../src/charging-station/meter-values/index.js'
-import type { ConnectorStatus, EmptyObject, EvseStatus } from '../../../../src/types/index.js'
+import type { ConnectorStatus, EmptyObject } from '../../../../src/types/index.js'
 
 import { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
 import {
@@ -50,6 +50,7 @@ import {
   ConnectorStatusEnum,
   CurrentType,
   ErrorType,
+  isOCPP20MeterValue,
   OCPP20ChargingStateEnumType,
   OCPP20ComponentName,
   OCPP20IdTokenEnumType,
@@ -70,7 +71,6 @@ import {
   OCPPVersion,
   PublicKeyWithSignedMeterValueEnumType,
   type RequestParams,
-  type SampledValueTemplate,
   SigningMethodEnumType,
   Voltage,
 } from '../../../../src/types/index.js'
@@ -747,7 +747,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
             unit: 'Wh',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         addConfigurationKey(
           station,
           intervalBaselineKey,
@@ -3091,7 +3091,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
         const transactionId = generateUUID()
         let online = false
         const attemptedSequenceNumbers: number[] = []
-        const malformedTriggerReason = 'Bogus' as OCPP20TriggerReasonEnumType
+        const malformedTriggerReason = 'Bogus' as unknown as OCPP20TriggerReasonEnumType
         let successorPayload: OCPP20TransactionEventRequest | undefined
         const malformedError = new OCPPError(
           ErrorType.FORMAT_VIOLATION,
@@ -5622,7 +5622,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_INTERVAL,
             unit: 'Wh',
           },
-        ] as unknown as EvseStatus['MeterValues']
+        ]
         addConfigurationKey(
           station,
           `${OCPP20ComponentName.OCPPCommCtrlr}.${OCPP20RequiredVariableName.MessageAttempts}.TransactionEvent`,
@@ -5689,7 +5689,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
             unit: 'Wh',
             value: '0',
-          } as unknown as SampledValueTemplate,
+          },
           20,
           undefined,
           undefined,
@@ -6479,7 +6479,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
             unit: 'Wh',
             value: '0',
-          } as unknown as SampledValueTemplate,
+          },
           20,
           undefined,
           undefined,
@@ -6753,7 +6753,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             measurand: OCPP20MeasurandEnumType.ENERGY_ACTIVE_IMPORT_REGISTER,
             unit: 'Wh',
             value: '0',
-          } as unknown as SampledValueTemplate,
+          },
           2,
           undefined,
           undefined,
@@ -7245,6 +7245,74 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
         assert.deepStrictEqual(observedIntervals, [1000, 10_000])
       })
 
+      await it('should send a periodic event without a MeterValue for an empty selection', async t => {
+        // Arrange
+        t.mock.timers.enable({ apis: ['setInterval'] })
+        const transactionId = generateUUID()
+        setupConnectorWithTransaction(mockStation, 1, { transactionId })
+        assert.ok(mockStation.stationInfo != null)
+        mockStation.stationInfo.coherentMeterValues = true
+        const session: CoherentSession = {
+          connectorId: 1,
+          currentType: CurrentType.AC,
+          numberOfPhases: 3,
+          profile: {
+            batteryCapacityWh: 40000,
+            chargingCurve: [
+              { powerFraction: 1, socPercent: 0 },
+              { powerFraction: 1, socPercent: 100 },
+            ],
+            id: 'empty-selection-test',
+            initialSocPercentMax: 30,
+            initialSocPercentMin: 30,
+            maxPowerW: 11000,
+            weight: 1,
+          },
+          rampUpDurationMs: 0,
+          sessionStartMs: Date.now(),
+          socPercent: 30,
+          transactionId,
+          voltageOutNominal: Voltage.VOLTAGE_230,
+        }
+        mockStation.__injectCoherentSession(transactionId, session)
+        addConfigurationKey(
+          mockStation,
+          buildConfigKey(
+            OCPP20ComponentName.SampledDataCtrlr,
+            OCPP20RequiredVariableName.TxUpdatedMeasurands
+          ),
+          '',
+          undefined,
+          { overwrite: true }
+        )
+
+        // Act
+        const meterValue = OCPP20ServiceUtils.buildTransactionMeterValue(
+          mockStation,
+          1,
+          1,
+          transactionId,
+          1000,
+          buildConfigKey(
+            OCPP20ComponentName.SampledDataCtrlr,
+            OCPP20RequiredVariableName.TxUpdatedMeasurands
+          )
+        )
+        assert.deepStrictEqual(meterValue.sampledValue, [])
+
+        OCPP20ServiceUtils.startUpdatedMeterValues(mockStation, 1, 1000, 1)
+        t.mock.timers.tick(1000)
+        for (let index = 0; index < 10; index++) await flushMicrotasks()
+
+        // Assert
+        assert.strictEqual(sentRequests.length, 1)
+        assert.strictEqual(
+          sentRequests[0].payload.triggerReason,
+          OCPP20TriggerReasonEnumType.MeterValuePeriodic
+        )
+        assert.strictEqual(sentRequests[0].payload.meterValue, undefined)
+      })
+
       await it('should not start timer when interval is zero', () => {
         const connectorId = 1
 
@@ -7681,7 +7749,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
         mockTracking.station.getEvseIdByConnectorId(connectorId) ?? 1
       )
       if (evseStatus != null) {
-        evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as ConnectorStatus['MeterValues']
+        evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       }
 
       addConfigurationKey(
@@ -11126,7 +11194,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
       // Arrange
       const evseStatus = station.getEvseStatus(1)
       if (evseStatus != null) {
-        evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as ConnectorStatus['MeterValues']
+        evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       }
 
       const transactionId = generateUUID()
@@ -11181,7 +11249,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             unit: 'Wh',
             value: '1000',
           },
-        ] as unknown as ConnectorStatus['MeterValues']
+        ]
         addConfigurationKey(
           station,
           buildConfigKey(
@@ -11237,7 +11305,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
             unit: 'Wh',
             value: '1000000',
           },
-        ] as unknown as ConnectorStatus['MeterValues']
+        ]
         addConfigurationKey(
           station,
           buildConfigKey(
@@ -11284,7 +11352,8 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           60_000,
           txUpdatedMeasurandsKey,
           OCPP20ReadingContextEnumType.SAMPLE_PERIODIC
-        ) as OCPP20MeterValue
+        )
+        assert.ok(isOCPP20MeterValue(meterValue))
         const expectedEnergyDelta = Number(
           (
             (station.getConnectorMaximumAvailablePower(connectorId) * sampleInterval) /
@@ -11372,7 +11441,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
       // Arrange
       const evseStatus = mockTracking.station.getEvseStatus(1)
       if (evseStatus != null) {
-        evseStatus.MeterValues = [{ unit: 'Wh' }] as unknown as ConnectorStatus['MeterValues']
+        evseStatus.MeterValues = [{ unit: OCPP20UnitEnumType.WATT_HOUR }]
       }
 
       const transactionId = generateUUID()
@@ -11427,7 +11496,7 @@ await describe('OCPP20 TransactionEvent ServiceUtils', async () => {
           unit: 'Wh',
           value: '1000',
         },
-      ] as unknown as ConnectorStatus['MeterValues']
+      ]
       const transactionId = generateUUID()
       setupConnectorWithTransaction(station, 1, { transactionId })
       connectorStatus.transactionEnergyActiveImportRegisterValue = 1000
index 486e3ed68daf7c9305bd6e022d0f75f995f6d554..b83617d2cd2047e5a308b1bde9e0abb048ef46c5 100644 (file)
@@ -126,10 +126,10 @@ await describe('OCPPIncomingRequestService — per-station state plumbing', asyn
 
   await it('should bind a nested post-restart CALL to the fresh station state', async () => {
     const oldState = plumbing.getOrCreateStationState(stationA)
-    const outerCommand = 'OuterInheritedCall' as Parameters<
+    const outerCommand = 'OuterInheritedCall' as unknown as Parameters<
       OCPP16IncomingRequestService['incomingRequestHandler']
     >[2]
-    const nestedCommand = 'NestedRestartedCall' as Parameters<
+    const nestedCommand = 'NestedRestartedCall' as unknown as Parameters<
       OCPP16IncomingRequestService['incomingRequestHandler']
     >[2]
     let freshState: OCPP16StationStateShape | undefined
index 05c258ce91c8587fe13a89680d2e6150d6d71ae8..30a8964d87870f1a8dc144fc28494f171d0a0e16 100644 (file)
@@ -19,17 +19,20 @@ import type { ChargingStation } from '../../../src/charging-station/index.js'
 
 import { addConfigurationKey, buildConfigKey } from '../../../src/charging-station/index.js'
 import {
+  assertMeterValueBuilderResult,
   buildClockAlignedConnectorMeterValue,
   buildMeterValue,
 } from '../../../src/charging-station/ocpp/OCPPServiceUtils.js'
 import {
   CurrentType,
+  type MeterValue,
   MeterValueContext,
   MeterValueLocation,
   MeterValueMeasurand,
   MeterValuePhase,
   MeterValueUnit,
   OCPP20ComponentName,
+  OCPP20LocationEnumType,
   OCPP20OptionalVariableName,
   OCPP20RequiredVariableName,
   OCPPVersion,
@@ -49,31 +52,31 @@ const energyTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
   unit: 'Wh',
   value: '0',
-} as unknown as SampledValueTemplate
+}
 
 const socTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.STATE_OF_CHARGE,
   unit: 'Percent',
   value: '75',
-} as unknown as SampledValueTemplate
+}
 
 const voltageTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.VOLTAGE,
   unit: 'V',
   value: '230',
-} as unknown as SampledValueTemplate
+}
 
 const powerTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.POWER_ACTIVE_IMPORT,
   unit: 'W',
   value: '11000',
-} as unknown as SampledValueTemplate
+}
 
 const currentTemplate: SampledValueTemplate = {
   measurand: MeterValueMeasurand.CURRENT_IMPORT,
   unit: 'A',
   value: '16',
-} as unknown as SampledValueTemplate
+}
 
 await describe('buildMeterValue', async () => {
   let station: ChargingStation
@@ -82,6 +85,21 @@ await describe('buildMeterValue', async () => {
     standardCleanup()
   })
 
+  await it('should allow empty internal results but reject mixed version samples', () => {
+    const emptyMeterValue: MeterValue = { sampledValue: [], timestamp: new Date() }
+    assert.doesNotThrow(() => {
+      assertMeterValueBuilderResult(emptyMeterValue, OCPPVersion.VERSION_201)
+    })
+
+    const mixedMeterValue = {
+      sampledValue: [{ value: '1' }, { value: 1 }],
+      timestamp: new Date(),
+    } as unknown as MeterValue
+    assert.throws(() => {
+      assertMeterValueBuilderResult(mixedMeterValue, OCPPVersion.VERSION_201)
+    }, /do not match OCPP 2\.0\.1/u)
+  })
+
   await describe('OCPP 1.6', async () => {
     beforeEach(() => {
       const { station: s } = createMockChargingStation({
@@ -120,7 +138,7 @@ await describe('buildMeterValue', async () => {
           measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
           unit: MeterValueUnit.WATT_HOUR,
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
 
       const meterValue = buildMeterValue(station, TEST_TRANSACTION_ID, 0)
 
@@ -233,7 +251,7 @@ await describe('buildMeterValue', async () => {
             unit: 'Wh',
             value: '1000',
           },
-        ] as unknown as SampledValueTemplate[]
+        ]
 
         buildMeterValue(testStation, TEST_TRANSACTION_ID_STRING, 3_600_000)
 
@@ -261,7 +279,7 @@ await describe('buildMeterValue', async () => {
           unit: MeterValueUnit.WATT_HOUR,
           value: '0',
         },
-      ] as unknown as SampledValueTemplate[]
+      ]
       addConfigurationKey(
         station,
         buildConfigKey(OCPP20ComponentName.AlignedDataCtrlr, OCPP20RequiredVariableName.Measurands),
@@ -290,6 +308,92 @@ await describe('buildMeterValue', async () => {
       assert.deepStrictEqual(values, [[125], [0]])
     })
 
+    await it('should preserve physical energy when consuming a multiplied snapshot baseline', () => {
+      const template: SampledValueTemplate = {
+        measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+        unit: MeterValueUnit.WATT_HOUR,
+        unitOfMeasure: { multiplier: 1, unit: MeterValueUnit.WATT_HOUR },
+      }
+
+      const meterValue = buildClockAlignedConnectorMeterValue(
+        station,
+        {
+          connectorId: 1,
+          evseId: 1,
+          sampledValueBaseline: [
+            {
+              location: OCPP20LocationEnumType.Outlet,
+              measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+              unitOfMeasure: { multiplier: -1, unit: MeterValueUnit.KILO_WATT_HOUR },
+              value: 2,
+            },
+          ],
+          sampledValueTemplates: [template],
+          transactionId: TEST_TRANSACTION_ID_STRING,
+        },
+        60_000,
+        undefined,
+        MeterValueContext.SAMPLE_CLOCK
+      )
+
+      assert.strictEqual(meterValue.sampledValue[0]?.value, 20)
+      assert.deepStrictEqual(meterValue.sampledValue[0]?.unitOfMeasure, {
+        multiplier: 1,
+        unit: MeterValueUnit.WATT_HOUR,
+      })
+    })
+
+    await it('should preserve native unit descriptor families during snapshot phase suppression', () => {
+      addConfigurationKey(
+        station,
+        buildConfigKey(
+          OCPP20ComponentName.SampledDataCtrlr,
+          OCPP20OptionalVariableName.RegisterValuesWithoutPhases
+        ),
+        'true',
+        undefined,
+        { overwrite: true }
+      )
+      const templates = ['sensor-a', 'sensor-b'].map((vendorId, index) => ({
+        measurand: MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
+        phase: index === 0 ? MeterValuePhase.L1_N : MeterValuePhase.L2_N,
+        unit: MeterValueUnit.WATT_HOUR,
+        unitOfMeasure: {
+          customData: { vendorId },
+          multiplier: index,
+          unit: MeterValueUnit.WATT_HOUR,
+        },
+      })) as SampledValueTemplate[]
+
+      const meterValue = buildClockAlignedConnectorMeterValue(
+        station,
+        {
+          connectorId: 1,
+          energyRegisterWhOverride: 100,
+          evseId: 1,
+          sampledValueBaseline: [],
+          sampledValueTemplates: templates,
+          transactionId: TEST_TRANSACTION_ID_STRING,
+        },
+        60_000,
+        undefined,
+        MeterValueContext.SAMPLE_CLOCK
+      )
+
+      assert.deepStrictEqual(
+        meterValue.sampledValue.map(sample => [
+          sample.unitOfMeasure?.customData?.vendorId,
+          sample.unitOfMeasure?.multiplier,
+          sample.phase,
+          sample.value,
+        ]),
+        [
+          ['sensor-a', 0, undefined, 100],
+          ['sensor-b', 1, undefined, 10],
+        ]
+      )
+    })
+
     await it('should suppress register phases by effective OCPP 2.0 output identity', () => {
       addConfigurationKey(
         station,
index 412c8b73421b248655c5f4033e5375da578c8078..5dd86413cff9b14a8b391d03645b750f2f527f44 100644 (file)
@@ -175,6 +175,20 @@ await describe('SignedMeterValueUtils', async () => {
       assert.strictEqual(result.enabled, true)
     })
 
+    await it('should return disabled when configured method is invalid', () => {
+      const result = validateSigningPrerequisites(TEST_PUBLIC_KEY_HEX, 'invalid-signing-method')
+
+      assert.strictEqual(result.enabled, false)
+      assert.ok((result as { reason: string }).reason.includes('invalid'))
+    })
+
+    await it('should return disabled when configured method is empty', () => {
+      const result = validateSigningPrerequisites(TEST_PUBLIC_KEY_HEX, '')
+
+      assert.strictEqual(result.enabled, false)
+      assert.ok((result as { reason: string }).reason.includes('invalid'))
+    })
+
     await it('should return disabled when public key is undefined', () => {
       const result = validateSigningPrerequisites(undefined, undefined)
       assert.strictEqual(result.enabled, false)
index 099b3b536b8e028c4095291bd960988f7c14c353..7e4c561b81958e508da7a948593b0e0379973710 100644 (file)
@@ -52,7 +52,7 @@ await describe('AuthComponentFactory', async () => {
 
     await it('should throw error for unsupported version', () => {
       const { station: chargingStation } = createMockChargingStation({
-        stationInfo: { ocppVersion: 'VERSION_15' as OCPPVersion },
+        stationInfo: { ocppVersion: 'VERSION_15' as unknown as OCPPVersion },
       })
 
       assert.throws(() => AuthComponentFactory.createAdapter(chargingStation), {
index 80745b20d78ddee9c76e889ad2bf67882d1bf8a3..1a6d94a60b59ed47aeb88e275e08672b55e53938 100644 (file)
@@ -373,8 +373,7 @@ await describe('AuthValidators', async () => {
 
     await it('should return false for unsupported type', () => {
       const identifier: Identifier = {
-        // @ts-expect-error: Testing invalid type
-        type: 'UNSUPPORTED_TYPE',
+        type: 'UNSUPPORTED_TYPE' as unknown as IdentifierType,
         value: 'VALUE',
       }
 
index 0874a3f83e74ee5e84fc9b76434f06c4a972fa76..1a0fa05b33671659d39726c918bd4d5ae58cd730 100644 (file)
@@ -560,6 +560,23 @@ await describe('UIHttpServer', async () => {
       })
     })
 
+    await it('should reject an unknown procedure with an actionable error', async () => {
+      const broadcastServer = createBroadcastServer()
+      try {
+        const res = new MockServerResponse()
+
+        broadcastServer.emitRequest(buildProcedureRequest('unknownProcedure', {}), res)
+        await awaitFinish(res)
+
+        const payload = parseHttpResponsePayload(res)
+        assert.strictEqual(payload.status, ResponseStatus.FAILURE)
+        assert.strictEqual(payload.errorMessage, "Unsupported UI procedure: 'unknownProcedure'")
+        assert.strictEqual(payload.errorStack, undefined)
+      } finally {
+        broadcastServer.stop()
+      }
+    })
+
     await it('should respond immediately to a synchronous non-broadcast procedure', async () => {
       const broadcastServer = createBroadcastServer()
       try {
index ac9b9951c175230df1ba8d5a7752b6e8c80286c0..27f5f0dbb2ec3076ad66dccf49e3eaf6e249e57f 100644 (file)
@@ -35,6 +35,7 @@ import {
   standardCleanup,
   withMockTimers,
 } from '../../helpers/TestLifecycleHelpers.js'
+import { WebSocketReadyState } from '../mocks/MockWebSocket.js'
 import { TEST_HASH_ID, TEST_UUID, TEST_UUID_2 } from './UIServerTestConstants.js'
 import {
   awaitFinish,
@@ -193,7 +194,7 @@ await describe('UIWebSocketServer', async () => {
     const config = createMockUIServerConfiguration()
     const server = new TestableUIWebSocketServer(config)
     const ws = createMockUIWebSocket()
-    ws.readyState = 0
+    ws.readyState = WebSocketReadyState.CONNECTING
 
     server.addResponseHandler(TEST_UUID, ws)
     server.sendResponse([TEST_UUID, { status: ResponseStatus.SUCCESS }])
@@ -206,7 +207,7 @@ await describe('UIWebSocketServer', async () => {
     const config = createMockUIServerConfiguration()
     const server = new TestableUIWebSocketServer(config)
     const ws = createMockUIWebSocket()
-    ws.readyState = 1
+    ws.readyState = WebSocketReadyState.OPEN
     ws.send = (): void => {
       throw new Error('WebSocket send error')
     }
index f328ea43da47e6122380d871e40bfaf1bce58bc3..f567afa23a5a5182d2fa6fa0766da4aaa8567720 100644 (file)
@@ -175,7 +175,11 @@ await describe('AbstractUIService', async () => {
 
     const service = server.getUIService(ProtocolVersion['0.0.1'])
 
-    const request = createProtocolRequest(TEST_UUID, 'UnknownProcedure' as ProcedureName, {})
+    const request = createProtocolRequest(
+      TEST_UUID,
+      'UnknownProcedure' as unknown as ProcedureName,
+      {}
+    )
 
     assert.notStrictEqual(service, undefined)
     if (service != null) {
index 65a1876f1e7968a425a700d19586333c1442a1e3..70568179616e17071e5b27fd3b98e3007794edb8 100644 (file)
@@ -296,7 +296,7 @@ await describe('UIServiceRequestPayloadSchemas', async () => {
       // procedure has no schema to check against.
       const response = await dispatchUntrustedPayload(
         service,
-        'UnknownProcedure' as ProcedureNameType,
+        'UnknownProcedure' as unknown as ProcedureNameType,
         { hashIds: 'not-an-array' }
       )
 
index d58fd87a20a93750e7faa1f10876c94ad86e36d3..f346d4f990e2e842590b0bcea0a8aa2d7f6b179f 100644 (file)
@@ -2,69 +2,69 @@
 
 [[package]]
 name = "ast-serialize"
-version = "0.11.2"
+version = "0.12.1"
 description = "Python bindings for mypy AST serialization"
 optional = false
 python-versions = ">=3.7"
 groups = ["dev"]
 files = [
-    {file = "ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b"},
-    {file = "ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f"},
-    {file = "ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d"},
-    {file = "ast_serialize-0.11.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89499a439955931281986e97ca4dd3c064bf0d2e0027c0017344eb86667733a1"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484"},
-    {file = "ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27"},
-    {file = "ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450"},
+    {file = "ast_serialize-0.12.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e73255c9227fd74eac8a9b55c4049e8ad7b66d1f690bf827c98a86b2e594def7"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:4655ef993e69e01bb47d2d99647de9bbb74af03938438656832cd010d95de348"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bac5a99a2c91dd823be9b8c44645694fccbb0750773cc5b27889a9f6f22fce89"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6485e681625ed7a094221f16a7ff2ef154946112266a05cf83bde50c959ef345"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a513bc6f60980d01767f7cbe39b17ce0373e722824a74ce28d6cea49ee3c8460"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:82866f3523d53ffca8d2a69a750bec52908b69f728012e40959bebce2620453c"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:243054a05a5190f5d087b5c8b16423e7f1cefa4991bac26e9c8ace18074b75b6"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9fbe5a3e8acddfc2fddff3dbbc7ea0e9798b3df3428f851b8abc52a3806f31"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d3d516da3463071d27e64caf54d88cba25cf4ad4afcc807e0bcf67743719f03e"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d6711adf11136c77e3a35517de9488a5081d1012874fae99c2876b64f4daace"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cbe239bee4bd609186daf60b95b7b0f47146c7f7f55f6da83807d747d6fe753f"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5bbf582286c9dc6b4c544ef645dc99e4b3aa09db28892bc60344141f6926641f"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99e33c93efb5254a70c525b46038212371dfe5693d48eb2d0d5f17d936a263d7"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:aa6c17a2b7f07e81fa8cfcc4aa7c832b3e57733853aebea113ab502f9b0963db"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f896fa38e0af38821e1ab1425c5dee89e359623e165765bdeae7d0eb6909e76d"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af699e81fd7ce80b8b03945826d8ea23dd36d072f10d4613402da597ba4ee9c6"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-win32.whl", hash = "sha256:10b59afc108eb285146acb23d1b5ec0fc58bb3c09cb2ab8876402df06c373c3b"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:72e871f6995a066c1b19104f8a6b5832b1163adb9a8267c2aa4711fbb0f4d1f3"},
+    {file = "ast_serialize-0.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3398e458047d21c9bc1b323fe5aab77c608dc9ddb65b2d44deebaff503a1f1eb"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:410233de149ab8414cb27c6fc73e9d2baa35d6f971672d540d752060d980ffb4"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9a2310845302f1a6bd45ae8a67d5760211103a8d66410b854bfa440d107e093"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff65f40f49d5e1a1a043ba366081d59a4e26a9f5c1b07eb1170e172115da7ca"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:536d783c4d91331f094e0a892221e619be5ffbe6fb6640885f14d7f226ec90ca"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c42d2d65f388d1960c5796231eb9bf5a988c46228633eb489605c4549ad16c52"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5628a12acc875fe7a167910f18d101dd101c2a7b1e6c2b6f7289ffaff25805c"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e855adfa5bb982b2e6fe09056b2d584f6dd4fce085d91a07d1155683751b6b5"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:fafe1471e8aca6c87b4913b7b54ff97197adf702fbe28692284b929dfa62ff96"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a5cac246474d2a703147d1605a6ac5ba0fa9e0a443cf1cf42513adf4df02686f"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6e25cd319fb0d7b39fcac666784ec86708ccbc78d07a698b1400cf5ed40c045b"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:657a7354ea16ed4d29f8127ed477c6fee3915c111d135f020ca835a991438e90"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:9eb9de7e59621acdb3e66984f374272b33d56d15a04763e2fd0604211e1c8303"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:09cc4d3103c1fc97f6845ba307af1db9cde5226bef47f8843220dde83f2276ba"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:c9e2a592706fd791c2271ce9c8f4e38c98d3ea0b4a86b511e09a4fe3ac44ab37"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ac042e95a575432c1730ca4cb9183066a2074886a599f46c1f1b0955fb8198"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:b3cd105995942cc6163a229674a86161ba1305f646493efd59bd6723a357ee14"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:a9cd24a26126088693ca054547ea0a391398a29cf1a3a2bec1009b4b6acc8b82"},
+    {file = "ast_serialize-0.12.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9649cd903db0dc047906c6dd740784a2ba665d54f7e43ba31457edbce76c9493"},
+    {file = "ast_serialize-0.12.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5ef62601db3ce5c23445132262a193075e211fb2fc87b46b7550dd351fac0976"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98d91cd3a6cb76a39512ee090a539d1e3206b732ad8150eb38918cffa1ddf515"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8a32f184ce3e4b1d0b06d642a1243281cf55b99e0323680b1b8f904029fd7700"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e62126ac2be2d9340ac1b3ee7a0466a883ecbed634cff0929c88ca0b671483b7"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f93a70fa9826c04ea9f2c3a880f87f4cca09a828144ed5084682abd28110980"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1858887be56a64a2aea899423dfe43787c34c75c18b0d7497de8e618d54b2790"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89a2bc39a820bc7785b60c53a5742b4e8dd4c1a599294e2dd68fae545883d44a"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50a9eaedf1db4857dad7cc47dd757ed70bfcc40b89d44d516c4a2f0d5033bd76"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:c30b609e8fea426b310543126de876592236a25aa8ebd59f1e2b323dd52a4085"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b1ad06513022cfa1337744959255af0ef16119d2beb1e547b67f37ad9433d4a"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6add54b495e37ae3cf3a1f0d5eaba364814eb72e93026adc41b7791e4b0d45d3"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:5fc136cd08001b817ad0b3e7426f50a7d2b8982dc7c6491f0af78af4c3dd8672"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:45a9e6b700bbd973d49942668a5cdbeffa693f2e250b8a5409abd1fa9d351854"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:a1d8267f83c613ea0a31f2518df074bd62e98a4b3a4892f6a529d74e08e02dba"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a0cbab9796e6ce841197feeba4008faa96b4cc7741129542fd81c882d7a4f01"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b4282695f1d51a3c6ef76560351bad5af880eff7d755aefea325bffb9bf68c25"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-win32.whl", hash = "sha256:119d1b0cadaba4a6e475f9bbe79eecc79351e373142eabe7c353f0250d70aebb"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-win_amd64.whl", hash = "sha256:3d6ed63d4fc1ec867b8cb522d58c36df0e8f05e487bea0ffd102043a37636d72"},
+    {file = "ast_serialize-0.12.1-cp39-abi3-win_arm64.whl", hash = "sha256:610a41351de68199de9a1434499083b4256c0df7658ec1cfc0a0a7b20b08d317"},
+    {file = "ast_serialize-0.12.1.tar.gz", hash = "sha256:5285a390caf1c44368ae270f037f797b91427d138b7d43cad0f1fda4c83518d9"},
 ]
 
 [[package]]
@@ -93,133 +93,133 @@ files = [
 
 [[package]]
 name = "coverage"
-version = "7.16.1"
+version = "7.16.2"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.10"
 groups = ["dev"]
 files = [
-    {file = "coverage-7.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f12a9e27ca7b65e40a8475d27899b2d45064d9020e6a89148939e01987b5853"},
-    {file = "coverage-7.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f590d46c30d9e4c1fda3efefe5443f4c2f6a4192c5ca2ba403653e9ebadf097"},
-    {file = "coverage-7.16.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3df82f0a3cef4e1bcfc799436056f0b978dda319d0bfd4460c6e479b2802d98a"},
-    {file = "coverage-7.16.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb462d59146656e278d1e8ed913ce374d0ba68a4e081acde1867f6d3377fc881"},
-    {file = "coverage-7.16.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7db888dd0a1df1a653cae7f99d4047430d2187a3626eda51bc847b0fd6b9b43"},
-    {file = "coverage-7.16.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:38a7e16f061504ac2b45370bf5bf97e8250d8d3f25e37385bae884978166554b"},
-    {file = "coverage-7.16.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b26b55b18e1a53e1a159dd743728c4ddbbef28ba19000a91aaff5ce023197ec"},
-    {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb2f528b50953e29d22033b3256c396a700193c6e45b2490222ef9c333cbbf9"},
-    {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3284754371dc78592aa3ae4d661d30ee20e02b2b6b0de3590a181a936d0d3b38"},
-    {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:802d1246c540e07486d4ee1adfa19a797e4b33529ffc371dc140644e8f27da0a"},
-    {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6fc735d6fe6d57f803e7ba021be4dde48e43e6aff94e6954350555d5332b0594"},
-    {file = "coverage-7.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed5ade1bb18f62edace1bd198c66f9d4c75a8385d5fd24e87d917ea1a5958773"},
-    {file = "coverage-7.16.1-cp310-cp310-win32.whl", hash = "sha256:0c309096926b119543dc16438a11ef4c80783d2f4e59ff94f7f462651a944cdc"},
-    {file = "coverage-7.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:7562f8067ed9360e8b9739e5703403a7686dd1b36bf0f89fc538047c54cdea90"},
-    {file = "coverage-7.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72e013665e25cf9d44779f01f340af26319756f9a76822b7c94ce6b1d93813da"},
-    {file = "coverage-7.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb05c0ff98b56ba6969adf35556bc43bcb8d094df8bc9cb403acff53460c4e07"},
-    {file = "coverage-7.16.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d0ececb32090e3fbb03e0d352b973a0485879b4de6c58daf47227b9988b99e5"},
-    {file = "coverage-7.16.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f0ba3892d81aacf36996c52f16bca04e39af31a6c5de930b7688ab617f4a6475"},
-    {file = "coverage-7.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6410b75fe07d5271eaa95fc24bd0a9177ed588d9d1c10c0cf67829adb8f0567"},
-    {file = "coverage-7.16.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d1039cb2de093225d597109342ce1675626bd127565e80b3044f4eca07c15b2e"},
-    {file = "coverage-7.16.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf047bc39fde5425be2628666d0f435ed8817859111c3aacc84b32d858069f5d"},
-    {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c05913d0d5badf7ac83200f35dcf9514cce5df16a7cc89e7d1d7fff0461813b"},
-    {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4027bf6d7bc0a16df058ce913b69f10c5687f8e1ca668f08caa659ce101744bf"},
-    {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4f48b345f831eaf4402ab6333c2dc3e2e2b5bc7b9c1b8fe12680dee3f0538f01"},
-    {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8643baeb590726c558b2faed6cd59b0917480f9367fcc692026f1a86d824fd08"},
-    {file = "coverage-7.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d06dcc420b570bf683cdb647cc8fe62b672d9e429ef711c3cbbb7a6880ca1572"},
-    {file = "coverage-7.16.1-cp311-cp311-win32.whl", hash = "sha256:946f58aa59b08bcd6afcc6a7bd0ff54ed5eee844f32ad69fe6814836d15856a2"},
-    {file = "coverage-7.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:684c7ee9b4c04358fe6ac8b517ab51ec35fcd79d08ff0f105dd8bcd96885bbb7"},
-    {file = "coverage-7.16.1-cp311-cp311-win_arm64.whl", hash = "sha256:1b24f79e25bcf6c73931aeca7a3dfc7595c0cb5e9364aba3fdf387a3de4b1c22"},
-    {file = "coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af"},
-    {file = "coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c"},
-    {file = "coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809"},
-    {file = "coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf"},
-    {file = "coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f"},
-    {file = "coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360"},
-    {file = "coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08"},
-    {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941"},
-    {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489"},
-    {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776"},
-    {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6"},
-    {file = "coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0"},
-    {file = "coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d"},
-    {file = "coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e"},
-    {file = "coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8"},
-    {file = "coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9"},
-    {file = "coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245"},
-    {file = "coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5"},
-    {file = "coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5"},
-    {file = "coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f"},
-    {file = "coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41"},
-    {file = "coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878"},
-    {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2"},
-    {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080"},
-    {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037"},
-    {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b"},
-    {file = "coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0"},
-    {file = "coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a"},
-    {file = "coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa"},
-    {file = "coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d"},
-    {file = "coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15"},
-    {file = "coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57"},
-    {file = "coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3"},
-    {file = "coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc"},
-    {file = "coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751"},
-    {file = "coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab"},
-    {file = "coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9"},
-    {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412"},
-    {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a"},
-    {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540"},
-    {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9"},
-    {file = "coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361"},
-    {file = "coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b"},
-    {file = "coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b"},
-    {file = "coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f"},
-    {file = "coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152"},
-    {file = "coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb"},
-    {file = "coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741"},
-    {file = "coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1"},
-    {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72"},
-    {file = "coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8"},
-    {file = "coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c"},
-    {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a"},
-    {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da"},
-    {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2"},
-    {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e"},
-    {file = "coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae"},
-    {file = "coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2"},
-    {file = "coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d"},
-    {file = "coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6"},
-    {file = "coverage-7.16.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a337dc2d54c74430cd2febb8ee04f7c508ba8b3b412bf0463f077a66cfc73743"},
-    {file = "coverage-7.16.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3acd1d78397dead78dd1b011b5fc19cc823c190349acd549e63856dff649c80e"},
-    {file = "coverage-7.16.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73a32694603a34ad01d7e51a481a4023410d8099e1d0757e067945695c10f0ae"},
-    {file = "coverage-7.16.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fbbe8265736659a6be2e6042b6a35be13545d14b243cc1d7ecf65f90d788a370"},
-    {file = "coverage-7.16.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b0359eb4c62f9993e176bc8f50450fc736a6b90dbcc05bb8584e948812699ae"},
-    {file = "coverage-7.16.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:da506e669a8a851b59e122b4b219ea70996a6296f44f3a9348a852526ff961de"},
-    {file = "coverage-7.16.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:64a2a5985d81810ed605ff0dc4ccd6555efcb5700353825532a9a0aea65826e1"},
-    {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:66d70132b69b861805dc1ca46cdd733e54c416890e8f1371d2fd103f70b59c9c"},
-    {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:db651a9cf325a542bc2b7b8cc8f1b2bdc6492739bae3103731b2f1c85b96cff6"},
-    {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:4184e78a4465dcda359fb403172b8951dd220929cb0984c02fabca1742fff06f"},
-    {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bc53c3f3adaa939b7a063533ffe0ae1259e7073393c618043a99a6970a87e3df"},
-    {file = "coverage-7.16.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b44308854ef210b9b78df9cdfd4e159513382a859f5ef8464306d14a54c2a040"},
-    {file = "coverage-7.16.1-cp315-cp315-win32.whl", hash = "sha256:dccc142614d3419ed71857deb43f1d757829a4c7fce9994464b71e7e38309827"},
-    {file = "coverage-7.16.1-cp315-cp315-win_amd64.whl", hash = "sha256:961fc424e9d5229a99f8f1189942d8e7f4e1519147c3af64842f944aca03914d"},
-    {file = "coverage-7.16.1-cp315-cp315-win_arm64.whl", hash = "sha256:681a9488c5a234397c4f013da065aa9e53eb7af4c78f1f80c6f15e7208acb855"},
-    {file = "coverage-7.16.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:8bb09a2d19b04db1fa0e087a7ca4f12458f7e0e7364cfcd838441d86fb1c61f6"},
-    {file = "coverage-7.16.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2270a794600b635ca9452ce4c32e2fe81a35f9caa17ffac0eba99f14f275bd4d"},
-    {file = "coverage-7.16.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4eff405b545dfcf79cf0d9d3ff750e5c5a887aa066114175193a81d249c5ee6"},
-    {file = "coverage-7.16.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ee1d5fc9e3bd6a217906929cc97880239a91d20dae7746f538eb0eefee705ab1"},
-    {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4ec944947de098ad5a1738413f9364689a57067ecbc328e9de37218aa1e5cc1"},
-    {file = "coverage-7.16.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f73ee3956fde2d461c9e2955dd48166e4821fc8587d135e8780fb84da2a098b"},
-    {file = "coverage-7.16.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ec9a4ee989c0d06ad95add0dbfdbb72b00ef53f43431ca0b612384e7878e5de"},
-    {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7e5727b2508f817f3126d6c33327dda32fe69d15514badfcd61db8bc4209ecef"},
-    {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:19a3ea2f364012ef06678118fffdc92442a16bef4a5c8ad4f4019dd8f9ac8876"},
-    {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:996c2b891b441ec2b39725ee3e8386e2f11b4894b92be225fdfd54a3eeada2c8"},
-    {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:a125fac1f6b1e88488d208a86b578e1790e3c4937f2e1568d23356141d236220"},
-    {file = "coverage-7.16.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b10095528b866d322d33d6bf1709b7f8cbf959f12e8cb2ba22fc59c8717866b0"},
-    {file = "coverage-7.16.1-cp315-cp315t-win32.whl", hash = "sha256:531d9be377fdcc05593b974656872eb82e808ebeb42a72515e3aaeb8bb7166f5"},
-    {file = "coverage-7.16.1-cp315-cp315t-win_amd64.whl", hash = "sha256:46a88f51770df7c9bc376bd57d3f86cdc7624b8e16ac4b585a655c22b7a1b4db"},
-    {file = "coverage-7.16.1-cp315-cp315t-win_arm64.whl", hash = "sha256:7580432cbe1e8b762660ae5806f04f869e1c02e519836a43f8094437e561e9f0"},
-    {file = "coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4"},
-    {file = "coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210"},
+    {file = "coverage-7.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23219888477edd736b6fcaec1272d47d93b926e999641ffea7e53a1738e70b2b"},
+    {file = "coverage-7.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40c0f00899fe6181ae7f434ceb200e51f5ee4b8ed10e3b5f0b605f0cae15da87"},
+    {file = "coverage-7.16.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4624f80732f6b427ac58f1f59c577a0994a12e8174b5af6a027b4b58795d4c3"},
+    {file = "coverage-7.16.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:191803c4996b499fcd78c2ad5e5f767dcc53cb4dc6de6d6a741b443a1821ef02"},
+    {file = "coverage-7.16.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fd670ac43b709c575aefc25bf52d8a598a3bc5017bddfd0a179152ab06a2deb"},
+    {file = "coverage-7.16.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:705e5af11d34647efdc170c7840b6857c81cf74be96419a553f237e68e62cb72"},
+    {file = "coverage-7.16.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8afd9bf35cc6a1f22eb3634808fa8e0b91902459c5721ef2e4461dfe771d7f08"},
+    {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f43bac1856ba269b905302778d4df433d6006489a192174ad77ac528e395032"},
+    {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f8475460aa33ee28ac896ab1156d0bb3b6c639f7f8383c2677d3359eb35f8205"},
+    {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d6276d78f6fca7d0ac066d5da4165c5acd07829e8305c2cb900b738fb3a75a72"},
+    {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:736fde09ea39646d11f8e3b76bd3425c075aa4dd45f24891970bb77c14ff20f5"},
+    {file = "coverage-7.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c85d54e7e8a2ca932fe8399301af9b8d5907ea2a455ffaff6e7d1208db83b943"},
+    {file = "coverage-7.16.2-cp310-cp310-win32.whl", hash = "sha256:5139009b5efd2194fc168ee9362f0e191ba612ef5d29242f9269c22f9b8f80c7"},
+    {file = "coverage-7.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:c3305c38a2fa21a4254f2ace7dd9ef5fc569c9a558b66e7017650b3d637fb95e"},
+    {file = "coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d"},
+    {file = "coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5"},
+    {file = "coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b"},
+    {file = "coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510"},
+    {file = "coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25"},
+    {file = "coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a"},
+    {file = "coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5"},
+    {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6"},
+    {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17"},
+    {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9"},
+    {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035"},
+    {file = "coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878"},
+    {file = "coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516"},
+    {file = "coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d"},
+    {file = "coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a"},
+    {file = "coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48"},
+    {file = "coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99"},
+    {file = "coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808"},
+    {file = "coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d"},
+    {file = "coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c"},
+    {file = "coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800"},
+    {file = "coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148"},
+    {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7"},
+    {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad"},
+    {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6"},
+    {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd"},
+    {file = "coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206"},
+    {file = "coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a"},
+    {file = "coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162"},
+    {file = "coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02"},
+    {file = "coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9"},
+    {file = "coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b"},
+    {file = "coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204"},
+    {file = "coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5"},
+    {file = "coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7"},
+    {file = "coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541"},
+    {file = "coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc"},
+    {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967"},
+    {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7"},
+    {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23"},
+    {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8"},
+    {file = "coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4"},
+    {file = "coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865"},
+    {file = "coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6"},
+    {file = "coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79"},
+    {file = "coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a"},
+    {file = "coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee"},
+    {file = "coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b"},
+    {file = "coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03"},
+    {file = "coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda"},
+    {file = "coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c"},
+    {file = "coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9"},
+    {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4"},
+    {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90"},
+    {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95"},
+    {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8"},
+    {file = "coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6"},
+    {file = "coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011"},
+    {file = "coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b"},
+    {file = "coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb"},
+    {file = "coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf"},
+    {file = "coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036"},
+    {file = "coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3"},
+    {file = "coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831"},
+    {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf"},
+    {file = "coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681"},
+    {file = "coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0"},
+    {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb"},
+    {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7"},
+    {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64"},
+    {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20"},
+    {file = "coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce"},
+    {file = "coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46"},
+    {file = "coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c"},
+    {file = "coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91"},
+    {file = "coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8"},
+    {file = "coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24"},
+    {file = "coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09"},
+    {file = "coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843"},
+    {file = "coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1"},
+    {file = "coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982"},
+    {file = "coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34"},
+    {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6"},
+    {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4"},
+    {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0"},
+    {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a"},
+    {file = "coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63"},
+    {file = "coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e"},
+    {file = "coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae"},
+    {file = "coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495"},
+    {file = "coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75"},
+    {file = "coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55"},
+    {file = "coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2"},
+    {file = "coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2"},
+    {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1"},
+    {file = "coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f"},
+    {file = "coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d"},
+    {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0"},
+    {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283"},
+    {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec"},
+    {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5"},
+    {file = "coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c"},
+    {file = "coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35"},
+    {file = "coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5"},
+    {file = "coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8"},
+    {file = "coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f"},
+    {file = "coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa"},
 ]
 
 [package.extras]
@@ -713,128 +713,140 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""}
 
 [[package]]
 name = "rpds-py"
-version = "2026.6.3"
+version = "2026.9.1"
 description = "Python bindings to Rust's persistent data structures (rpds)"
 optional = false
 python-versions = ">=3.11"
 groups = ["main"]
 files = [
-    {file = "rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127"},
-    {file = "rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f"},
-    {file = "rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80"},
-    {file = "rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5"},
-    {file = "rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e"},
-    {file = "rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146"},
-    {file = "rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577"},
-    {file = "rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76"},
-    {file = "rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826"},
-    {file = "rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f"},
+    {file = "rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f"},
+    {file = "rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9"},
+    {file = "rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c"},
+    {file = "rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569"},
+    {file = "rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15"},
+    {file = "rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b"},
+    {file = "rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32"},
+    {file = "rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0"},
+    {file = "rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37"},
+    {file = "rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12"},
 ]
 
 [[package]]
diff --git a/tests/types/ocpp/ChargingProfile.test.ts b/tests/types/ocpp/ChargingProfile.test.ts
new file mode 100644 (file)
index 0000000..b40e99d
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * @file Tests for cross-version charging-profile type guards
+ * @description Verifies that extension properties cannot change a profile's protocol classification
+ */
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+
+import {
+  isOCPP16ChargingProfile,
+  OCPP20ChargingProfileKindEnumType,
+  OCPP20ChargingProfilePurposeEnumType,
+  type OCPP20ChargingProfileType,
+  OCPP20ChargingRateUnitEnumType,
+} from '../../../src/types/index.js'
+
+await describe('ChargingProfile type guards', async () => {
+  await it('should not classify an OCPP 2.0 profile with an extension chargingProfileId as OCPP 1.6', () => {
+    const chargingProfile: OCPP20ChargingProfileType = {
+      chargingProfileId: 42,
+      chargingProfileKind: OCPP20ChargingProfileKindEnumType.Absolute,
+      chargingProfilePurpose: OCPP20ChargingProfilePurposeEnumType.TxProfile,
+      chargingSchedule: [
+        {
+          chargingRateUnit: OCPP20ChargingRateUnitEnumType.W,
+          chargingSchedulePeriod: [{ limit: 11_000, startPeriod: 0 }],
+          id: 1,
+        },
+      ],
+      id: 1,
+      stackLevel: 0,
+    }
+
+    assert.strictEqual(isOCPP16ChargingProfile(chargingProfile), false)
+  })
+})
diff --git a/tests/types/ocpp/MeterValues.test.ts b/tests/types/ocpp/MeterValues.test.ts
new file mode 100644 (file)
index 0000000..fe0d273
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * @file Tests for cross-version MeterValue type guards
+ * @description Verifies that protocol-specific guards remain disjoint at runtime
+ */
+import assert from 'node:assert/strict'
+import { describe, it } from 'node:test'
+
+import {
+  isOCPP16MeterValue,
+  isOCPP20MeterValue,
+  type MeterValue,
+} from '../../../src/types/index.js'
+
+await describe('MeterValue type guards', async () => {
+  await it('should classify an empty sampled-value list only as OCPP 1.6', () => {
+    const meterValue: MeterValue = {
+      sampledValue: [],
+      timestamp: new Date(),
+    }
+
+    assert.strictEqual(isOCPP16MeterValue(meterValue), true)
+    assert.strictEqual(isOCPP20MeterValue(meterValue), false)
+  })
+})
index ef2455147cc735ff43388265ba1a0b7a9ca9a679..5226fad0c47921ad6f2d6fb142eacfff1582d39e 100644 (file)
@@ -186,7 +186,7 @@ await describe('Configuration', async () => {
 
   await it('should throw for unknown configuration section', () => {
     assert.throws(() => {
-      Configuration.getConfigurationSection('unknown' as ConfigurationSection)
+      Configuration.getConfigurationSection('unknown' as unknown as ConfigurationSection)
     }, Error)
   })
 })
index ce6d38c67befeaaf3e6b553c1fc7527bface662f..a28b42c7e56a13b441171afba4692060fc7d8b0a 100644 (file)
@@ -41,7 +41,7 @@ await describe('ConfigurationUtils', async () => {
 
     // Test unsupported storage type
     assert.throws(() => {
-      getDefaultPerformanceStorageUri('unsupported' as StorageType)
+      getDefaultPerformanceStorageUri('unsupported' as unknown as StorageType)
     }, Error)
   })
 })
index 6ab5a4069af76e02af238d157be4f4ba6b292b35..9f808606d3379b9901540cf2bd6f7d8b631771dd 100644 (file)
@@ -2,8 +2,10 @@ import { Command, Option } from 'commander'
 import {
   buildAuthorizePayload,
   buildStatusNotificationPayload,
-  type ChargePointStatus,
-  type OCPP16ChargePointErrorCode,
+  getEnumStringValue,
+  OCPP16ChargePointErrorCode,
+  OCPP16ChargePointStatus,
+  OCPP20ConnectorStatusEnumType,
   OCPPVersion,
   ProcedureName,
   type RequestPayload,
@@ -191,16 +193,25 @@ export const createOcppCommands = (program: Command): Command => {
             if (ocppVersion === OCPPVersion.VERSION_16 && options.errorCode == null) {
               throw new Error('--error-code is required for OCPP 1.6 stations')
             }
+            const status =
+              ocppVersion === OCPPVersion.VERSION_16
+                ? getEnumStringValue(OCPP16ChargePointStatus, options.status)
+                : getEnumStringValue(OCPP20ConnectorStatusEnumType, options.status)
+            if (status == null) {
+              throw new Error(`Unsupported connector status '${options.status}'`)
+            }
+            const errorCode =
+              options.errorCode == null
+                ? undefined
+                : getEnumStringValue(OCPP16ChargePointErrorCode, options.errorCode)
+            if (options.errorCode != null && errorCode == null) {
+              throw new Error(`Unsupported OCPP 1.6 error code '${options.errorCode}'`)
+            }
             payload = {
-              ...buildStatusNotificationPayload(
-                options.connectorId,
-                options.status as ChargePointStatus,
-                ocppVersion,
-                {
-                  errorCode: options.errorCode as OCPP16ChargePointErrorCode | undefined,
-                  evseId: options.evseId,
-                }
-              ),
+              ...buildStatusNotificationPayload(options.connectorId, status, ocppVersion, {
+                errorCode,
+                evseId: options.evseId,
+              }),
               ...buildHashIdsPayload(resolvedHashIds),
             }
             await runAction(program, ProcedureName.STATUS_NOTIFICATION, payload, undefined, config)
index 737c2679ba129142f3c0468f07b2eb6aa40a9646..0f5d84c28312d935ebc3bc02d495b5477d04b430 100644 (file)
@@ -1,6 +1,6 @@
-import type { DataConverter, WebSocketReadyState } from './types.js'
+import type { DataConverter } from './types.js'
 
-import { type WebSocketLike } from './types.js'
+import { type WebSocketLike, WebSocketReadyState } from './types.js'
 
 interface RawWebSocket {
   close(code?: number, reason?: string): void
@@ -86,7 +86,18 @@ export const createWsAdapter = (ws: RawWebSocket, options: WsAdapterOptions): We
     },
 
     get readyState (): WebSocketReadyState {
-      return ws.readyState
+      switch (ws.readyState) {
+        case 0:
+          return WebSocketReadyState.CONNECTING
+        case 1:
+          return WebSocketReadyState.OPEN
+        case 2:
+          return WebSocketReadyState.CLOSING
+        case 3:
+          return WebSocketReadyState.CLOSED
+        default:
+          throw new RangeError(`Unknown WebSocket ready state: ${ws.readyState.toString()}`)
+      }
     },
 
     send (data: string): void {
index 944db50afb4d9959953e7fb81ca1eed228a51c6b..c2e35fb75f8436e8b9a535c1660181182a093127 100644 (file)
@@ -11,6 +11,7 @@ export * from './types/JsonType.js'
 export * from './types/UIProtocol.js'
 export * from './types/UUID.js'
 export * from './utils/converters.js'
+export * from './utils/enum.js'
 export * from './utils/payloadBuilders.js'
 export * from './utils/UUID.js'
 export * from './utils/websocket.js'
diff --git a/ui/common/src/utils/enum.ts b/ui/common/src/utils/enum.ts
new file mode 100644 (file)
index 0000000..f2f48ef
--- /dev/null
@@ -0,0 +1,10 @@
+export const getEnumStringValue = <T extends string>(
+  enumObject: Readonly<Record<string, T>>,
+  value: unknown
+): T | undefined => {
+  for (const key in enumObject) {
+    if (Object.hasOwn(enumObject, key) && enumObject[key] === value) {
+      return enumObject[key]
+    }
+  }
+}
index 687d42f653b7c32a5ca8d7b5390a0e71fb4f6237..3cecdd241dcc4c0e243661b5249ffec460d36be5 100644 (file)
@@ -46,6 +46,6 @@
     "jsdom": "^30.1.1",
     "vite": "^8.3.1",
     "vitest": "^5.0.3",
-    "vue-tsc": "^3.3.11"
+    "vue-tsc": "^3.3.12"
   }
 }
index b9140d6c392200413bfaacc24660769d0179cdd6..60fadc0c3d66472ced9f9b765ba19d3ff49738f8 100644 (file)
         <ToggleButton
           :id="'add-charging-stations'"
           :key="state.renderAddChargingStations"
-          :off="
-            () => {
-              $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
-            }
-          "
-          :on="
-            () => {
-              $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS }).catch(() => undefined)
-            }
-          "
+          :off="showChargingStations"
+          :on="showAddChargingStations"
           :shared="true"
         >
           Add Charging Stations
@@ -154,6 +146,14 @@ const $chargingStations = useChargingStations()
 const $route = useRoute()
 const $router = useRouter()
 
+const showChargingStations = (): void => {
+  $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
+}
+
+const showAddChargingStations = (): void => {
+  $router.push({ name: ROUTE_NAMES.ADD_CHARGING_STATIONS }).catch(() => undefined)
+}
+
 const { activeSkinId, availableSkins: skins, switchSkin } = useSkin()
 const { activeThemeId, availableThemes, switchTheme } = useTheme()
 
index 6b5720a9127a9fb94e287595fceceb4b9af49c11..f7c08683099ee712bcce7c48264e7507c19e18bb 100644 (file)
@@ -37,8 +37,7 @@
 </template>
 
 <script setup lang="ts">
-import type { OCPPVersion } from 'ui-common'
-
+import { getEnumStringValue, OCPPVersion } from 'ui-common'
 import { computed } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 
@@ -61,7 +60,15 @@ const evseId = computed(() =>
 )
 const ocppVersion = computed(() => {
   const raw = $route.query.ocppVersion
-  return typeof raw === 'string' ? (raw as OCPPVersion) : undefined
+  if (raw == null) return undefined
+  if (typeof raw !== 'string') {
+    throw new Error('Invalid OCPP version query parameter')
+  }
+  const parsedVersion = getEnumStringValue(OCPPVersion, raw)
+  if (parsedVersion == null) {
+    throw new Error(`Invalid OCPP version query parameter '${raw}'`)
+  }
+  return parsedVersion
 })
 
 const toggleButtonId = computed(
index cc156377df87d9a73f97404946a7b1243e79d7f1..d53281394b38776931efc6c06b061cbd299d7c3f 100644 (file)
       <ToggleButton
         v-if="connector.transactionStarted !== true"
         :id="`${hashId}-${evseId ?? 0}-${connectorId}-start-transaction`"
-        :off="
-          () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
-          }
-        "
-        :on="
-          () => {
-            $router
-              .push({
-                name: ROUTE_NAMES.START_TRANSACTION,
-                params: { hashId, chargingStationId, connectorId },
-                query: {
-                  ...(evseId != null ? { evseId: String(evseId) } : {}),
-                  ...(ocppVersion != null ? { ocppVersion } : {}),
-                },
-              })
-              .catch(() => undefined)
-          }
-        "
+        :off="showChargingStations"
+        :on="showStartTransaction"
         :shared="true"
         @clicked="$emit('need-refresh')"
       >
@@ -97,6 +80,7 @@
 import type { ChargePointStatus, ConnectorStatus, OCPPVersion, Status } from 'ui-common'
 
 import {
+  getEnumStringValue,
   isOCPP20x,
   OCPP16ChargePointErrorCode,
   OCPP16ChargePointStatus,
@@ -126,6 +110,27 @@ defineEmits<{ 'need-refresh': [] }>()
 
 const $router = useRouter()
 
+const showChargingStations = (): void => {
+  $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
+}
+
+const showStartTransaction = (): void => {
+  $router
+    .push({
+      name: ROUTE_NAMES.START_TRANSACTION,
+      params: {
+        chargingStationId: props.chargingStationId,
+        connectorId: props.connectorId,
+        hashId: props.hashId,
+      },
+      query: {
+        ...(props.evseId != null ? { evseId: String(props.evseId) } : {}),
+        ...(props.ocppVersion != null ? { ocppVersion: props.ocppVersion } : {}),
+      },
+    })
+    .catch(() => undefined)
+}
+
 const {
   lockConnector,
   setConnectorStatus,
@@ -148,9 +153,9 @@ const statusOptions = computed(() =>
 const errorCodeOptions = Object.values(OCPP16ChargePointErrorCode)
 const selectedStatus = ref<ChargePointStatus>(
   isOCPP20x(props.ocppVersion)
-    ? ((props.connector.status as OCPP20ConnectorStatusEnumType | undefined) ??
+    ? (getEnumStringValue(OCPP20ConnectorStatusEnumType, props.connector.status) ??
         OCPP20ConnectorStatusEnumType.AVAILABLE)
-    : ((props.connector.status as OCPP16ChargePointStatus | undefined) ??
+    : (getEnumStringValue(OCPP16ChargePointStatus, props.connector.status) ??
         OCPP16ChargePointStatus.AVAILABLE)
 )
 const selectedErrorCode = ref<OCPP16ChargePointErrorCode>(OCPP16ChargePointErrorCode.NO_ERROR)
index a8e8c2604f09c04d64fe4cc757e99ee87be976d6..f8cbda850be541d8c22abf46172ad12026108278 100644 (file)
       />
       <ToggleButton
         :id="`${chargingStation.stationInfo.hashId}-set-supervision-url`"
-        :off="
-          () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
-          }
-        "
-        :on="
-          () => {
-            $router
-              .push({
-                name: ROUTE_NAMES.SET_SUPERVISION_URL,
-                params: {
-                  hashId: chargingStation.stationInfo.hashId,
-                  chargingStationId: chargingStation.stationInfo.chargingStationId,
-                },
-              })
-              .catch(() => undefined)
-          }
-        "
+        :off="showChargingStations"
+        :on="showSupervisionUrl"
         :shared="true"
         @clicked="$emit('need-refresh')"
       >
       </ToggleButton>
       <ToggleButton
         :id="`${chargingStation.stationInfo.hashId}-change-configuration`"
-        :off="
-          () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
-          }
-        "
-        :on="
-          () => {
-            $router
-              .push({
-                name: ROUTE_NAMES.CHANGE_CONFIGURATION,
-                params: {
-                  hashId: chargingStation.stationInfo.hashId,
-                  chargingStationId: chargingStation.stationInfo.chargingStationId,
-                },
-              })
-              .catch(() => undefined)
-          }
-        "
+        :off="showChargingStations"
+        :on="showChangeConfiguration"
         :shared="true"
         @clicked="$emit('need-refresh')"
       >
       </ToggleButton>
       <ToggleButton
         :id="`${chargingStation.stationInfo.hashId}-show-details`"
-        :off="
-          () => {
-            $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
-          }
-        "
-        :on="
-          () => {
-            $router
-              .push({
-                name: ROUTE_NAMES.SHOW_DETAILS,
-                params: {
-                  hashId: chargingStation.stationInfo.hashId,
-                  chargingStationId: chargingStation.stationInfo.chargingStationId,
-                },
-              })
-              .catch(() => undefined)
-          }
-        "
+        :off="showChargingStations"
+        :on="showDetails"
         :shared="true"
         @clicked="$emit('need-refresh')"
       >
@@ -173,6 +125,7 @@ import {
   WebSocketReadyState,
 } from 'ui-common'
 import { computed } from 'vue'
+import { useRouter } from 'vue-router'
 
 import {
   deleteLocalStorageByKeyPattern,
@@ -193,6 +146,41 @@ const props = defineProps<{
 
 const emit = defineEmits<{ 'need-refresh': [] }>()
 
+const $router = useRouter()
+
+const showChargingStations = (): void => {
+  $router.push({ name: ROUTE_NAMES.CHARGING_STATIONS }).catch(() => undefined)
+}
+
+const showStationAction = (
+  name:
+    | typeof ROUTE_NAMES.CHANGE_CONFIGURATION
+    | typeof ROUTE_NAMES.SET_SUPERVISION_URL
+    | typeof ROUTE_NAMES.SHOW_DETAILS
+): void => {
+  $router
+    .push({
+      name,
+      params: {
+        chargingStationId: props.chargingStation.stationInfo.chargingStationId,
+        hashId: props.chargingStation.stationInfo.hashId,
+      },
+    })
+    .catch(() => undefined)
+}
+
+const showSupervisionUrl = (): void => {
+  showStationAction(ROUTE_NAMES.SET_SUPERVISION_URL)
+}
+
+const showChangeConfiguration = (): void => {
+  showStationAction(ROUTE_NAMES.CHANGE_CONFIGURATION)
+}
+
+const showDetails = (): void => {
+  showStationAction(ROUTE_NAMES.SHOW_DETAILS)
+}
+
 const isWebSocketOpen = computed(() => props.chargingStation.wsState === WebSocketReadyState.OPEN)
 
 const connectorEntries = computed((): ConnectorEntry[] =>
index c90622263cf7de5946f73387dfa98dd32b0d4982..b20b63ada1e0973f17a5892251f4de2635419fbb 100644 (file)
@@ -36,7 +36,7 @@ const mockPush = vi.fn().mockResolvedValue(undefined)
 const mockRoute = ref<{
   name: string
   params: Record<string, string>
-  query: Record<string, string>
+  query: Record<string, string | string[]>
 }>({
   name: 'start-transaction',
   params: { chargingStationId: TEST_STATION_ID, connectorId: '1', hashId: TEST_HASH_ID },
@@ -364,6 +364,37 @@ describe('Actions', () => {
       )
     })
 
+    for (const invalidVersion of ['invalid', [OCPPVersion.VERSION_16, OCPPVersion.VERSION_20]]) {
+      it(`should reject the present invalid OCPP version ${JSON.stringify(invalidVersion)}`, async () => {
+        mockRoute.value.query = { evseId: '1', ocppVersion: invalidVersion }
+        const wrapper = mountStartTx()
+        await wrapper.find('input[name="idtag"]').setValue('RFID-001')
+
+        await wrapper.findComponent(ButtonStub).trigger('click')
+        await flushPromises()
+
+        expect(toastMock.error).toHaveBeenCalledWith('Error at authorizing RFID tag')
+        expect(mockClient.authorize).not.toHaveBeenCalled()
+        expect(mockClient.startTransaction).not.toHaveBeenCalled()
+      })
+    }
+
+    it('should preserve the legacy OCPP 1.6 fallback when version is absent', async () => {
+      mockRoute.value.query = { evseId: '1' }
+      const wrapper = mountStartTx()
+      await wrapper.find('input[type="checkbox"]').setValue(false)
+      await wrapper.find('input[name="idtag"]').setValue('RFID-001')
+
+      await wrapper.findComponent(ButtonStub).trigger('click')
+      await flushPromises()
+
+      expect(mockClient.authorize).not.toHaveBeenCalled()
+      expect(mockClient.startTransaction).toHaveBeenCalledWith(
+        TEST_HASH_ID,
+        expect.objectContaining({ ocppVersion: undefined })
+      )
+    })
+
     it('should toast success on successful transaction start', async () => {
       const wrapper = mountStartTx()
       const idTagInput = wrapper.find('input[name="idtag"]')