]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
refactor(charging-station,meter-values): close #1936 (M-08 + f-2..f-5 + M-09) (#1949)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Sun, 5 Jul 2026 15:15:52 +0000 (17:15 +0200)
committerGitHub <noreply@github.com>
Sun, 5 Jul 2026 15:15:52 +0000 (17:15 +0200)
commit9c8261ea43f531467b0f9e77a8b8bb884fdf2994
tree95730e686c9c3b2309121521edc978fb39b95e19
parenta0cdcfc95958293619e3d7a4028f2e05b0cd6cca
refactor(charging-station,meter-values): close #1936 (M-08 + f-2..f-5 + M-09) (#1949)

* refactor(tests): split StationHelpers.ts into modular files (issue #1936 M-08)

Split the 962-LOC monolithic StationHelpers.ts into five focused modules:
- StationHelpers.types.ts (149 LOC): 7 interfaces + MockChargingStation type
- StationHelpers.cleanup.ts (160 LOC): cleanup + reset helpers
- StationHelpers.connector.ts (65 LOC): connector-status + EVSE-usage helpers
- StationHelpers.template.ts (20 LOC): mock template factory
- StationHelpers.factory.ts (607 LOC): createMockChargingStation factory

StationHelpers.ts is now a pure re-export barrel preserving the public API,
so all 77 consumer test files keep importing from './StationHelpers.js'
without change. Mechanical move only: no logic, signature, or behavior
change.

determineEvseUsage is now exported (previously module-private) because it
is consumed by the factory across module boundaries.

* feat(meter-values): add powerFactor + rampShape to EvProfile (issue #1936 M-09)

Two optional EvProfile refinements. Both default to current behavior so
existing profiles and golden tests are unchanged.

- powerFactor (0, 1], absent => 1: cos phi factor. Per-phase current
  becomes I = P / (V . phases . powerFactor); INV-1 extends to
  P_active = V.I.phases.powerFactor.
- rampShape 'linear'|'sigmoid', absent => 'linear': sigmoid uses a
  shifted-scaled logistic (k=10) pinned at f(0)=0 and f(1)=1 exactly
  after endpoint normalization.

* refactor(charging-station): extract charging-profile helpers to HelpersChargingProfile (#1936 f-2)

Split Helpers.ts (Phase 2 of 5). Moves 14 charging-profile symbols into
sibling HelpersChargingProfile.ts and re-exports the public ones from
Helpers.ts so the barrel import path is preserved. Zero behavior change:
same signatures, same code, same log content. The moduleName local in
the new file switches log prefixes from 'Helpers.<fn>' to
'HelpersChargingProfile.<fn>', reflecting the actual source location.

Moved (private inside new file):
- getChargingStationChargingProfiles, buildChargingProfilesLimit,
  ChargingProfilesLimit interface, getChargingProfileId,
  getChargingProfilesLimit, canProceedRecurringChargingProfile,
  prepareRecurringChargingProfile, checkRecurringChargingProfileDuration

Moved (exported from new file):
- getChargingStationChargingProfilesLimit, getConnectorChargingProfiles,
  getConnectorChargingProfilesLimit, prepareChargingProfileKind,
  canProceedChargingProfile (all re-exported from Helpers.ts)
- getSingleChargingSchedule (exported for Phase 3 direct import; not
  re-exported from Helpers.ts to keep the public API unchanged)

Follows the barrel pattern established by HelpersReservation.ts in
Phase 1 (PR #1946).

* refactor(charging-station): extract connector-status helpers to HelpersConnectorStatus (#1936 f-3)

Split Helpers.ts (Phase 3 of 5). Moves 8 connector-status symbols into
sibling HelpersConnectorStatus.ts and re-exports the public ones from
Helpers.ts so the barrel import path is preserved. Zero behavior change:
same signatures, same code, same log content. The moduleName local in
the new file switches log prefixes from 'Helpers.<fn>' to
'HelpersConnectorStatus.<fn>', reflecting the actual source location.

Moved (private inside new file):
- initializeConnectorStatus

Moved (exported from new file, re-exported from Helpers.ts):
- buildConnectorsMap, initializeConnectorsMapStatus, resetConnectorStatus,
  resetAuthorizeConnectorStatus, prepareConnectorStatus,
  checkStationInfoConnectorStatus, getBootConnectorStatus

Follows the barrel pattern established by HelpersReservation.ts in
Phase 1 (PR #1946).

* refactor(charging-station): extract serial-number/id helpers to HelpersId (#1936 f-4)

Extracts 4 identity helpers (getChargingStationId, getHashId, createSerialNumber,

propagateSerialNumber) plus the ChargingStationNameTemplate type alias into a

dedicated HelpersId.ts. The getRandomSerialNumberSuffix helper had a single

internal caller (createSerialNumber) and no external consumer; it is demoted to

module-private in the new file, matching its actual usage.

Helpers.ts continues to re-export the 4 public symbols and the type alias via

the barrel, so all 5 consumer files work unchanged.

Refs #1936

* refactor(charging-station): extract config/template helpers to HelpersConfig (#1936 f-5)

Extracts 10 configuration/template helpers (buildTemplateName, validateStationInfo,

getDefaultConnectorMaximumPower, checkConfiguration, setChargingStationOptions,

stationTemplateToStationInfo, getAmperageLimitationUnitDivider, getDefaultVoltageOut,

getIdTagsFile, getEvProfilesFile) plus the two template-topology helpers

getMaxNumberOfEvses and getMaxNumberOfConnectors into HelpersConfig.ts.

The two template-topology helpers were originally listed as core-station helpers

in the issue body, but getDefaultConnectorMaximumPower consumes both of them;

moving them into HelpersConfig avoids a cross-module dependency back into

Helpers.ts that would risk an ESM live-binding cycle. Helpers.ts imports

getMaxNumberOfConnectors from HelpersConfig for use in the retained

getConfiguredMaxNumberOfConnectors, closing the split with no cycle.

Helpers.ts is now 185 LOC (core station helpers + 5 barrel re-exports),

down from 1389 LOC before the phased split.

Refs #1936

* [autofix.ci] apply automated fixes

* fix(charging-station,meter-values): apply Round 1 review fixes to #1949 (issue #1936)

1. HelpersConnectorStatus.ts imported getMaxNumberOfConnectors via the

   Helpers.js barrel, which re-exports it from HelpersConfig.js while

   also re-exporting HelpersConnectorStatus.js. That is the exact ESM

   barrel cycle the config/template extraction was designed to avoid.

   Import getMaxNumberOfConnectors directly from HelpersConfig.js to

   keep the dependency direction one-way.

2. EvProfile powerFactor Zod schema tightened from (0, 1] to [0.5, 1].

   The previous positive() lower bound accepted tiny values that made

   the divisor V*phases*powerFactor collapse toward zero and produce

   non-physical current. Real onboard chargers sit at 0.98..1.0; the

   0.5 floor blocks configuration errors while keeping physically

   defensible room. Reflected in JSDoc, README, and the schema.

3. powerFactor is now AC-only. DC has no reactive component (P=V*I),

   so applying cos phi on DC profiles produces non-physical current.

   Gate on session.currentType === CurrentType.AC when computing the

   divisor; DC pins powerFactor to 1 regardless of the profile field.

   Test locks the DC-gate contract with an explicit regression case.

4. sigmoidRamp JSDoc reworded. Endpoint bit-exactness is pinned by

   the short-circuit paths at progress <= 0 and >= 1; the shift-scale

   normalization aligns interior open-interval values. The prior

   wording overstated what the normalization does on its own.

5. StationHelpers.ts barrel comment reworded to describe the public

   surface directly instead of framing it as backward compatibility.

Refs #1936

* fix(charging-station,meter-values): apply Round 2 review fixes to #1949 (issue #1936)

1. INV-1 DC docstring stale: CoherentSampleComputer.ts @file block said

   DC: P = V*I*powerFactor but the implementation and README both pin

   powerFactor to 1 on DC (no reactive component in DC). Corrected to

   DC: P = V*I and cross-referenced the AC-only gate.

2. resolvePhasedValue non-emission of OCPP measurands documented:

   Power.Factor and Power.Reactive.Import are defined in OCPP 1.6 and

   2.0.1 measurand enums but the coherent path does not emit them.

   EvProfile.powerFactor scales the AC current/power chain internally

   but is not surfaced as Power.Factor. Templates configuring these

   measurands under coherent mode are skipped with a warning. The

   JSDoc now lists the supported measurand set and calls out the

   unsupported ones explicitly.

3. propagateSerialNumber (public export via Helpers barrel) gained a

   JSDoc block with @throws {BaseError}, matching the coverage pattern

   established for HelpersConfig.ts public exports.

4. buildChargingProfilesLimit (module-private but non-trivial) gained

   a JSDoc block with @throws {BaseError} for parallelism.

5. Test describe block renamed from 'rampShape sigmoid' to

   'rampShape sigmoid' with the literal quoted, matching the string-

   literal-type convention used everywhere else for rampShape values.

Refs #1936

* docs(charging-station): complete R2 findings coverage on helper split (#1936)

Applies design decisions for all remaining R2 findings on PR #1949:

1. JSDoc completeness (Lane A M-R2.2 + Lane B M-R2.6): every public

   export re-exported through the Helpers.ts barrel now carries a

   symbol-level JSDoc block matching the HelpersConfig.ts style

   established during Phase 5 extraction. Coverage: 3 exports in

   HelpersId.ts (getChargingStationId, getHashId, createSerialNumber),

   5 exports in HelpersChargingProfile.ts (getSingleChargingSchedule,

   getChargingStationChargingProfilesLimit,

   getConnectorChargingProfilesLimit, prepareChargingProfileKind,

   canProceedChargingProfile), and 7 exports in HelpersConnectorStatus.ts

   (getBootConnectorStatus, checkStationInfoConnectorStatus,

   buildConnectorsMap, initializeConnectorsMapStatus,

   resetAuthorizeConnectorStatus, resetConnectorStatus,

   prepareConnectorStatus). ChargingStationNameTemplate type also

   documented.

2. Barrel style divergence (Lane A N-R2.1): the two barrels use

   different re-export shapes on purpose. Helpers.ts uses explicit

   name lists to control and document the external API surface;

   StationHelpers.ts uses 'export *' because its consumers are

   internal to the test tree. A one-line justification comment now

   lives in each barrel so the divergence is intentional-by-record.

3. HelpersId moduleName absence (Lane A N-R2.3): every helper in

   this file is pure (no logger.* calls), so declaring an unused

   moduleName constant would violate the no-dead-code convention.

   Documented as an explicit design decision in the @file block.

Refs #1936

* docs(charging-station): rewrite R2b JSDoc blocks that misdescribed the code (#1936)

R3 review caught 5 R2b JSDoc blocks whose narrative did not match the

actual code path. Every block is rewritten to reflect current behavior:

1. getSingleChargingSchedule: the code returns undefined for ANY array

   shape (OCPP 2.0.x), not just for zero-or-multi-entry arrays. Doc now

   states arrays are logged (debug) and skipped, only the OCPP 1.6

   single-schedule shape is consumed. The pre-existing OCPP 2.0.x

   array-shape handling gap is out of scope for this PR.

2. getChargingStationChargingProfilesLimit: the code filters

   CHARGE_POINT_MAX_PROFILE (OCPP 1.6 value). Doc now uses that name

   explicitly and calls out that the OCPP 2.0.1 equivalent

   ChargingStationMaxProfile is not handled by this filter. Also

   out-of-scope pre-existing behavior.

3. getBootConnectorStatus: doc claimed a fallback to Available, but

   the code returns Unavailable when the station or connector is

   unavailable. Doc now enumerates the four branches explicitly

   (Unavailable / persisted status / bootStatus / Available).

4. initializeConnectorsMapStatus: doc had two independent errors.

   Connector 0 is mutated (availability=Operative, chargingProfiles

   default to empty), not left untouched. And initializeConnectorStatus

   runs on the transactionStarted-unset branch, not on the pending

   flag branch. Doc now describes all four actual branches.

5. resetConnectorStatus: doc said energy counters (plural); only the

   transaction-scoped register is reset. Doc said connector identity

   (id, availability) is preserved; ConnectorStatus has no id field,

   only the outer Map key. Doc now says transaction-scoped energy

   counter and preserves availability only.

Also replaced {@link initializeConnectorStatus} references with prose

since the target is module-private and would emit unresolved-reference

warnings from TypeDoc.

Refs #1936

* fix(charging-station): close R3 findings — OCPP 2.0.x profile support + Readonly type (#1936)

Applies the 3 remaining R3 findings as proper code fixes (not docs-only):

1. getSingleChargingSchedule unwraps length-1 OCPP 2.0.x arrays.

   Previously any array shape returned undefined, so all 8 callers

   silently dropped OCPP 2.0.x profiles at limit-resolution and

   preparation time. The fix unwraps chargingSchedule[0] when the

   array has exactly one entry (the OCPP 2.0.x single-schedule shape),

   and keeps the debug log + skip path for zero or multi-entry arrays

   (2-3 concurrent schedules are valid per spec but the coherent path

   does not pick between them). JSDoc rewritten to describe the

   post-fix contract. 4 regression tests added: OCPP 1.6 shape,

   length-1 unwrap, length-2 skip, empty-array skip.

2. getChargingStationChargingProfilesLimit filter now accepts both

   ChargingProfilePurposeType.CHARGE_POINT_MAX_PROFILE (OCPP 1.6

   value 'ChargePointMaxProfile') and

   ChargingProfilePurposeType.ChargingStationMaxProfile (OCPP 2.0.1

   value 'ChargingStationMaxProfile'). Previously only the OCPP 1.6

   value matched, so OCPP 2.0.1 station-scope profiles were excluded

   from station-level limit resolution. JSDoc updated.

3. ChargingStationNameTemplate is now Readonly<Pick<...>>. The type

   was already used read-only by getChargingStationId; the Readonly

   wrapper makes the pure-read contract explicit at the type level.

Note: findings 1 and 2 are pre-existing behavior gaps exposed by the

R2b/R3 JSDoc accuracy audit, not regressions introduced by the

refactor. Fixing them here rather than deferring to a follow-up issue

matches the session precedent (PR-J F06.FR.06, PR-K

RegisterValuesWithoutPhases, PR-H physics refinements).

Refs #1936

* fix(charging-station): close R4 findings, add station-scope filter regression tests (#1936)

R4 review surfaced two convergent findings on the R3b station-scope

profile filter fix:

1. N-R4.1: the R3b filter accepted CHARGE_POINT_MAX_PROFILE (OCPP 1.6)

   and ChargingStationMaxProfile (OCPP 2.0.1), but OCPP 2.0.1 has a

   second station-scope purpose ChargingStationExternalConstraints

   (OCA J01 use-case: external LMS/EMS-imposed caps). The local

   validator at OCPP20IncomingRequestService.ts:4157-4164 already

   treats it identically to ChargingStationMaxProfile (must apply to

   EVSE 0). Filter now accepts all three station-scope purposes.

   JSDoc updated to describe the OCA J01 semantic distinction and the

   coherent path's decision to treat both OCPP 2.0.1 purposes as

   equivalent inputs to stack-level tie-break.

2. N-R4.3: R3b Fix #2 had no regression test locking the filter

   acceptance. 4 targeted tests added covering the 3 station-scope

   purposes (CHARGE_POINT_MAX_PROFILE, ChargingStationMaxProfile,

   ChargingStationExternalConstraints) plus a negative TX_PROFILE

   rejection. Tests seed connector 0's chargingProfiles directly via

   MockChargingStation and assert getChargingStationChargingProfilesLimit

   returns the expected watt limit or undefined.

Refs #1936

* docs(charging-station): apply Round 5 review fixes to #1949 (issue #1936)

R5 review surfaced one MAJOR spec-citation error and three test-quality

MINORs. Only actionable items are fixed here; prior R4b commit body is

immutable per session convention (squash-safe branch history).

1. M1 spec citation: OCA J01 in R4b JSDoc is factually wrong. J01 is the

   Metering functional block (Sending Meter Values not related to a

   transaction). ChargingStationExternalConstraints belongs to the K

   SmartCharging block: K04 for the internal Load Balancing use-case

   (matches ChargingStationMaxProfile semantic) and K11-K14 for the

   External Charging Limit family that the station reports to the CSMS

   (matches ChargingStationExternalConstraints semantic). JSDoc rewrites

   the citation to K04 vs K11-K14.

2. n2 test coverage: getSingleChargingSchedule regression tests missed

   the OCPP 2.0.1 spec upper bound of a length-3 chargingSchedule array

   (spec cardinality is 1..3). Added one test verifying the length-3

   case returns undefined (matching the current always-skip-non-length-1

   behavior).

3. n3 test coverage: the R4b station-scope filter tests all seeded a

   single profile at stackLevel 0, missing (a) tie-break correctness

   between two station-scope profiles at different stack levels and (b)

   filter + purpose interaction when a station-scope and a TX_PROFILE

   are both seeded on connector 0. Added two tests covering both cases.

4. n1 test setup rationale: the seed helper reassigns station.stationInfo

   after mock construction. This only reaches code paths that read

   chargingStation.stationInfo directly (the maximumPower sanity check);

   factory methods like getNumberOfPhases and getVoltageOut capture the

   constructor-time options in a closure and do not observe the mutation.

   Tests use ChargingRateUnitType.WATT to bypass AC/DC conversion, so

   currentOutType is not exercised. Added an inline comment locking the

   design decision so future contributors know why the mutation is inert

   for phase/voltage getters.

Renamed the seed helper from seedConnectorZeroWithProfile (singular) to

seedConnectorZeroWithProfiles (plural) to fit the multi-profile cases.

Refs #1936

* docs(charging-station): refine R5b OCA citation with K08.FR.04 merge behavior (#1936)

R6 review surfaced one MINOR precision gap in the R5b OCA citation and

one false-positive on issue linkage (rejected — issue #1936 IS the

correct closing target per session-verified reconciliation of 11

coordinated PRs).

R5b already fixed OCA J01 (Metering) to OCA K04 (Internal LMS) and

OCA K11-K14 (External Charging Limit). R6 Lane A + Lane B convergent

on a semantic distinction: K11-K14 describes the profile's ORIGIN

(external system sets the limit, station stores it internally), not

the coherent path's BEHAVIOR (composite-schedule merge input). The

authoritative citation for the merge-input behavior is OCA K08.FR.04

(Get Composite Schedule) and safety invariant SC.01. R5b also called

ChargingStationExternalConstraints an LMS/EMS-imposed cap the

station reports to the CSMS — accurate for the reporting flow via

NotifyChargingLimit / ReportChargingProfiles, but the code path uses

the profile as merge input rather than emit-source.

JSDoc now cites both K11-K14 (origin: external system sets limit,

station stores internally, reports upstream to CSMS) and K08.FR.04 /

SC.01 (behavior: composite-schedule merge input consumed by the

coherent path).

Refs #1936

* test(charging-station,meter-values): apply Round 7 test-strengthening fixes to #1949 (#1936)

R7 review surfaced two valid regression-proof gaps in the test suite

(other Lane B claims rejected as too-strict interpretation of boundary

and purpose-guard tests). Only actionable strengthening applied here.

1. Sigmoid ramp test at CoherentMeterValues.test.ts:1782+ only pinned

   endpoints (0, 1) via short-circuit and midpoint (0.5) via symmetry —

   all three points collapse to the same values on linear ramp. A

   silent revert of the sigmoid branch back to linear would pass the

   assertion. Added two interior-curvature assertions at rampUpDurationMs/4

   and 3*rampUpDurationMs/4 where sigmoid (k=10) emits ~0.07 and ~0.93

   respectively vs linear 0.25 and 0.75. Tolerance 0.15/0.85 sits well

   below the ~0.18 sigmoid-vs-linear gap, so the sigmoid feature is now

   contractually locked at interior points.

2. Readonly contract on ChargingStationNameTemplate had no regression

   lock. Added a compile-time assertion via @ts-expect-error on a

   mutation attempt. If the Readonly<Pick<...>> wrapper is silently

   removed, tsc --noEmit fails with 'Unused @ts-expect-error directive'

   at build time. Runtime tolerates the mutation (Readonly is

   compile-time only); the compile-time directive is the regression lock.

Refs #1936

* docs(charging-station): fix HelpersId JSDoc warnings on createSerialNumber (#1936)

pnpm format surfaced 3 lint warnings on HelpersId.ts:createSerialNumber:

- cspell/spellchecker: unknown word 'uppercased'

- jsdoc/require-param-description: missing params.randomSerialNumber description

- jsdoc/require-param-description: missing params.randomSerialNumberUpperCase description

Rewrote the JSDoc to spell 'upper case' with a space (standard English)

and added explicit descriptions for the two nested params. pnpm format

and pnpm lint are now warning-free.

Refs #1936

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
18 files changed:
README.md
src/assets/ev-profiles-template.json
src/charging-station/Helpers.ts
src/charging-station/HelpersChargingProfile.ts [new file with mode: 0644]
src/charging-station/HelpersConfig.ts [new file with mode: 0644]
src/charging-station/HelpersConnectorStatus.ts [new file with mode: 0644]
src/charging-station/HelpersId.ts [new file with mode: 0644]
src/charging-station/meter-values/CoherentMeterValueBuilder.ts
src/charging-station/meter-values/CoherentSampleComputer.ts
src/charging-station/meter-values/types.ts
tests/charging-station/Helpers.test.ts
tests/charging-station/helpers/StationHelpers.cleanup.ts [new file with mode: 0644]
tests/charging-station/helpers/StationHelpers.connector.ts [new file with mode: 0644]
tests/charging-station/helpers/StationHelpers.factory.ts [new file with mode: 0644]
tests/charging-station/helpers/StationHelpers.template.ts [new file with mode: 0644]
tests/charging-station/helpers/StationHelpers.ts
tests/charging-station/helpers/StationHelpers.types.ts [new file with mode: 0644]
tests/charging-station/meter-values/CoherentMeterValues.test.ts