]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
feat(simulator): add AC/DC conversion efficiency template support (#2063)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 4 Aug 2026 20:04:07 +0000 (22:04 +0200)
committerGitHub <noreply@github.com>
Tue, 4 Aug 2026 20:04:07 +0000 (22:04 +0200)
Add optional template tunable conversionEfficiency (float, (0, 1], default 1) reducing available charging power on DC stations only (currentOutType === DC). Applied at runtime in getConnectorMaximumAvailablePower to both power-derived bounds; amperage and charging-profile limits unchanged; no reduced value persisted. Absent field keeps existing behavior.

Closes #443

README.md
src/charging-station/ChargingStation.ts
src/charging-station/TemplateSchema.ts
src/types/ChargingStationTemplate.ts
tests/charging-station/ChargingStation-ConversionEfficiency.test.ts [new file with mode: 0644]
tests/charging-station/TemplateSchema.test.ts

index 15abb2f6aacf7a5754cea8527e986485c2548b74..66ffaf3958239c6bdd832b1870d8a3ec613d1eb0 100644 (file)
--- a/README.md
+++ b/README.md
@@ -233,6 +233,7 @@ But the modifications to test have to be done to the files in the build target d
 | powerSharedByConnectors                              | true/false    | false                                                                                                                              | boolean                                                                                                                                                                       | charging stations power shared by its connectors. When true, any single connector can draw up to the full station power; when false, each connector is allocated an equal share                                                                                                                                                                                                                                                                                                                                                                               |
 | powerUnit                                            | W/kW          | W                                                                                                                                  | string                                                                                                                                                                        | charging stations power unit                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
 | currentOutType                                       | AC/DC         | AC                                                                                                                                 | string                                                                                                                                                                        | charging stations current out type                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
+| conversionEfficiency                                 | (0, 1]        | 1                                                                                                                                  | float                                                                                                                                                                         | charging stations AC input to DC output power conversion efficiency, applied to the available charging power on DC stations only (currentOutType DC); leaves maximumPower and maximumAmperage unchanged                                                                                                                                                                                                                                                                                                                                                       |
 | voltageOut                                           |               | AC:230/DC:400                                                                                                                      | integer                                                                                                                                                                       | charging stations voltage out                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
 | numberOfPhases                                       | 0/1/3         | AC:3/DC:0                                                                                                                          | integer                                                                                                                                                                       | charging stations number of phase(s)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
 | numberOfConnectors                                   |               |                                                                                                                                    | integer \| integer[]                                                                                                                                                          | charging stations number of connector(s)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
index 41fa2cf5981c60d5daa2c99e9f437e505fb9561f..f225b29d23a3786d7714a20d9de12bf9d13b17a9 100644 (file)
@@ -618,7 +618,7 @@ export class ChargingStation extends EventEmitter {
     ) {
       const voltageOut = this.getVoltageOut()
       connectorAmperageLimitationLimit =
-        (this.stationInfo?.currentOutType === CurrentType.AC
+        (this.getCurrentOutType() === CurrentType.AC
           ? ACElectricUtils.powerTotal(
             this.getNumberOfPhases(),
             voltageOut,
@@ -636,8 +636,23 @@ export class ChargingStation extends EventEmitter {
       )
       return Number.POSITIVE_INFINITY
     }
-    const connectorMaximumPower = maximumPower / (this.powerDivider ?? 1)
-    const connectorHardwareMaximumPower = this.getConnectorStatus(connectorId)?.maximumPower
+    // On DC, the template maximumPower is the AC input-side power; the
+    // power actually available for charging is input * conversion efficiency.
+    // AC => 1 (not applied). Absent => 1 (backward compatible). Applied to both
+    // power-derived bounds: the connector hardware bound is an input-side power
+    // rating too (its default derives from the AC template power; an explicit
+    // Connectors[n].maximumPower is configured directly), so it must be reduced
+    // as well and cannot short-circuit the factor through min().
+    const conversionEfficiency =
+      this.getCurrentOutType() === CurrentType.DC
+        ? (this.stationInfo?.conversionEfficiency ?? 1)
+        : 1
+    const connectorMaximumPower = (maximumPower / (this.powerDivider ?? 1)) * conversionEfficiency
+    const connectorHardwareMaximumPowerInput = this.getConnectorStatus(connectorId)?.maximumPower
+    const connectorHardwareMaximumPower =
+      connectorHardwareMaximumPowerInput == null
+        ? undefined
+        : connectorHardwareMaximumPowerInput * conversionEfficiency
     const chargingStationChargingProfilesLimit =
       (getChargingStationChargingProfilesLimit(this) ?? Number.POSITIVE_INFINITY) /
       (this.powerDivider ?? 1)
index 5f22dd66fc7b170e2d1620ea7d4ffe05f53bada0..6a87eaca9d5b45f7b09510104f81a131767690d3 100644 (file)
@@ -185,6 +185,7 @@ const BaseTemplateSchema = z.looseObject({
   commandsSupport: CommandsSupportSchema.optional(),
   Configuration: OcppConfigurationSchema.optional(),
   Connectors: z.record(z.string().regex(/^\d+$/), ConnectorStatusSchema).optional(),
+  conversionEfficiency: z.number().positive().max(1).optional(),
   currentOutType: z.string().optional(),
   customValueLimitationMeterValues: z.boolean().optional(),
   enableStatistics: z.boolean().optional(),
index b9a2cd097771fce087bb63a8371a4fecbce06c10..03b4126d240f731e2b0b25e25c04b8410f8971b2 100644 (file)
@@ -73,6 +73,18 @@ export interface ChargingStationTemplate {
   commandsSupport?: CommandsSupport
   Configuration?: ChargingStationOcppConfiguration
   Connectors?: Record<string, ConnectorStatus>
+  /**
+   * AC-input to DC-output power conversion efficiency, in (0, 1]. DC stations
+   * only (`currentOutType === CurrentType.DC`). Absent ⇒ 1 (no reduction,
+   * backward compatible). Reduces only the available charging power returned by
+   * `getConnectorMaximumAvailablePower`, and the DC output-side MeterValues
+   * derived from it (Power, Current, Energy), which are assumed to be measured
+   * at the connector outlet (the OCPP default `location` for these measurands).
+   * `stationInfo.maximumPower` (the AC input-side power) and
+   * `stationInfo.maximumAmperage` (derived from it as `maximumPower / voltageOut`
+   * on DC) are left unchanged and are not reduced by this factor.
+   */
+  conversionEfficiency?: number
   currentOutType?: CurrentType
   customValueLimitationMeterValues?: boolean
   enableStatistics?: boolean
diff --git a/tests/charging-station/ChargingStation-ConversionEfficiency.test.ts b/tests/charging-station/ChargingStation-ConversionEfficiency.test.ts
new file mode 100644 (file)
index 0000000..46a2882
--- /dev/null
@@ -0,0 +1,126 @@
+/**
+ * @file Tests for template AC/DC conversion efficiency.
+ * @description On DC stations, template `maximumPower` is the AC input-side
+ * power; the power available for charging is `input * conversionEfficiency`.
+ * The factor is applied at runtime in `getConnectorMaximumAvailablePower` only
+ * (DC-only, absent => 1). `stationInfo.maximumPower` (the AC input-side power)
+ * and `stationInfo.maximumAmperage` (derived from it as `maximumPower /
+ * voltageOut` on DC) are left unchanged and are not reduced by the factor.
+ */
+import assert from 'node:assert/strict'
+import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, it } from 'node:test'
+
+import { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+const POWER_W = 50000
+
+const tmpRoots: string[] = []
+
+interface TemplateOverrides {
+  connectorMaximumPower?: number
+  conversionEfficiency?: number
+  currentOutType?: string
+}
+
+// Fresh DC template with powerSharedByConnectors:false (deterministic
+// powerDivider = number of connectors). By default the connectors carry no
+// explicit maximumPower, so the per-connector default power is derived from the
+// station power and the station bound is the binding one; an explicit
+// connectorMaximumPower override makes the connector hardware bound binding
+// instead, to exercise the second power-derived term of the min().
+const makeTemplate = (overrides: TemplateOverrides = {}): string => {
+  const root = mkdtempSync(join(tmpdir(), 'cs-conversion-efficiency-'))
+  tmpRoots.push(root)
+  mkdirSync(join(root, 'station-templates'), { recursive: true })
+  const file = join(root, 'station-templates', 'dc.station-template.json')
+  const connectorMaximumPower =
+    overrides.connectorMaximumPower != null ? { maximumPower: overrides.connectorMaximumPower } : {}
+  const template: Record<string, unknown> = {
+    $schemaVersion: 1,
+    baseName: 'TEST-CONVERSION-EFFICIENCY',
+    chargePointModel: 'Simulator simple',
+    chargePointVendor: 'Simulator',
+    Connectors: {
+      0: {},
+      1: { bootStatus: 'Available', ...connectorMaximumPower },
+      2: { bootStatus: 'Available', ...connectorMaximumPower },
+    },
+    currentOutType: overrides.currentOutType ?? 'DC',
+    numberOfConnectors: 2,
+    power: POWER_W,
+    powerSharedByConnectors: false,
+    powerUnit: 'W',
+    randomConnectors: false,
+    ...(overrides.conversionEfficiency != null
+      ? { conversionEfficiency: overrides.conversionEfficiency }
+      : {}),
+  }
+  writeFileSync(file, JSON.stringify(template), 'utf8')
+  return file
+}
+
+const newStation = (overrides: TemplateOverrides = {}): ChargingStation =>
+  new ChargingStation(1, makeTemplate(overrides), {
+    autoStart: false,
+    baseName: 'TEST-CONVERSION-EFFICIENCY',
+    fixedName: true,
+    persistentConfiguration: false,
+    supervisionUrls: 'ws://localhost:9999/',
+  })
+
+await describe('ChargingStation AC/DC conversion efficiency', async () => {
+  afterEach(() => {
+    standardCleanup()
+    for (const root of tmpRoots.splice(0)) {
+      rmSync(root, { force: true, recursive: true })
+    }
+  })
+
+  await it('reduces the DC connector available power by the efficiency factor', () => {
+    const baseline = newStation().getConnectorMaximumAvailablePower(1)
+    const reduced = newStation({ conversionEfficiency: 0.9 }).getConnectorMaximumAvailablePower(1)
+    assert.ok(Number.isFinite(baseline) && baseline > 0)
+    assert.ok(Math.abs(reduced - baseline * 0.9) < 1e-6)
+  })
+
+  await it('reduces the binding DC connector hardware power bound by the factor', () => {
+    // Explicit per-connector hardware bound (10000 W) below the station-derived
+    // bound (power / connectors = 50000 / 2 = 25000 W) so the hardware term is
+    // the one selected by min(); it must itself be reduced by the factor.
+    const baseline = newStation({
+      connectorMaximumPower: 10000,
+    }).getConnectorMaximumAvailablePower(1)
+    const reduced = newStation({
+      connectorMaximumPower: 10000,
+      conversionEfficiency: 0.9,
+    }).getConnectorMaximumAvailablePower(1)
+    assert.strictEqual(baseline, 10000)
+    assert.ok(Math.abs(reduced - 10000 * 0.9) < 1e-6)
+  })
+
+  await it('leaves DC connector available power unchanged when efficiency is absent', () => {
+    const withoutField = newStation().getConnectorMaximumAvailablePower(1)
+    const withUnity = newStation({ conversionEfficiency: 1 }).getConnectorMaximumAvailablePower(1)
+    assert.strictEqual(withoutField, withUnity)
+  })
+
+  await it('ignores the efficiency factor on AC stations', () => {
+    const acBaseline = newStation({ currentOutType: 'AC' }).getConnectorMaximumAvailablePower(1)
+    const acWithEfficiency = newStation({
+      conversionEfficiency: 0.9,
+      currentOutType: 'AC',
+    }).getConnectorMaximumAvailablePower(1)
+    assert.strictEqual(acWithEfficiency, acBaseline)
+  })
+
+  await it('does not reduce stationInfo.maximumPower or maximumAmperage', () => {
+    const baseline = newStation()
+    const reduced = newStation({ conversionEfficiency: 0.9 })
+    assert.strictEqual(reduced.stationInfo?.maximumPower, baseline.stationInfo?.maximumPower)
+    assert.strictEqual(reduced.stationInfo?.maximumAmperage, baseline.stationInfo?.maximumAmperage)
+  })
+})
index 855e4aa2986daceba5ec6021f5408d6b953e84a5..2bbbc7fe4ee1b91492a4ab6d16597ba0c5910e47 100644 (file)
@@ -72,6 +72,26 @@ await describe('TemplateSchema', async () => {
     })
   })
 
+  await describe('conversionEfficiency', async () => {
+    await it('should accept an absent conversionEfficiency (backward compatible)', () => {
+      assert.ok(TemplateSchema.safeParse(buildMinimalTemplate()).success)
+    })
+
+    for (const conversionEfficiency of [0.5, 0.9, 1]) {
+      await it(`should accept conversionEfficiency ${conversionEfficiency.toString()}`, () => {
+        assert.ok(TemplateSchema.safeParse(buildMinimalTemplate({ conversionEfficiency })).success)
+      })
+    }
+
+    for (const conversionEfficiency of [0, -0.1, 1.5, '0.9']) {
+      await it(`should reject conversionEfficiency ${JSON.stringify(conversionEfficiency)}`, () => {
+        const result = TemplateSchema.safeParse(buildMinimalTemplate({ conversionEfficiency }))
+        assert.ok(!result.success)
+        assert.ok(result.error.issues.some(i => i.path.includes('conversionEfficiency')))
+      })
+    }
+  })
+
   await describe('deprecated keys rejection', async () => {
     for (const [legacyKey, legacyValue] of [
       ['supervisionUrl', TEST_SUPERVISION_URL],