const countConnectors = (data: ChargingStationData): number =>
isNotEmptyArray(data.connectors)
? data.connectors.length
- : data.evses.reduce((n, evse) => n + evse.evseStatus.connectors.size, 0)
+ : data.evses.reduce((n, evse) => n + evse.evseStatus.connectors.length, 0)
/**
* Iterate connectors under the same OCPP 1.6 vs OCPP 2.0.x source split as
return
}
for (const evse of data.evses) {
- for (const [connectorId, connectorStatus] of evse.evseStatus.connectors) {
- yield { connectorId, connectorStatus, evseId: evse.evseId }
+ for (const entry of evse.evseStatus.connectors) {
+ yield entry
}
}
}
import type { ChargingStationInfo } from './ChargingStationInfo.js'
import type { ChargingStationOcppConfiguration } from './ChargingStationOcppConfiguration.js'
import type { ConnectorEntry } from './ConnectorStatus.js'
-import type { EvseEntry } from './Evse.js'
+import type { EvseEntryData } from './Evse.js'
import type { JsonObject } from './JsonType.js'
import type { BootNotificationResponse } from './ocpp/Responses.js'
import type { Statistics } from './Statistics.js'
automaticTransactionGenerator?: ATGConfiguration
bootNotificationResponse?: BootNotificationResponse
connectors: ConnectorEntry[]
- evses: EvseEntry[]
+ evses: EvseEntryData[]
ocppConfiguration: ChargingStationOcppConfiguration
started: boolean
stationInfo: ChargingStationInfo
-import type { ConnectorStatus } from './ConnectorStatus.js'
+import type { ConnectorEntry, ConnectorStatus } from './ConnectorStatus.js'
import type { SampledValueTemplate } from './MeasurandPerPhaseSampledValueTemplates.js'
import type { AvailabilityType } from './ocpp/Requests.js'
readonly evseStatus: EvseStatus
}
+/**
+ * JSON-wire projection of {@link EvseEntry} carried by `ChargingStationData.evses`;
+ * `evseStatus.connectors` is a serialization-safe `ConnectorEntry[]`, whereas the
+ * in-memory {@link EvseEntry} keeps a `Map`. Mirrors the ui-common `EvseEntry` wire
+ * shape (duplicated because packages share no re-exports).
+ */
+export interface EvseEntryData {
+ readonly evseId: number
+ readonly evseStatus: EvseStatusData
+}
+
export interface EvseStatus {
availability: AvailabilityType
connectors: Map<number, ConnectorStatus>
MeterValues?: SampledValueTemplate[]
}
+/**
+ * JSON-wire projection of {@link EvseStatus}: `connectors` is a `ConnectorEntry[]`
+ * (a `Map` serializes to `{}`). `MeterValues` is intentionally omitted — the producer
+ * `buildEvseEntries` never emits it and no UI-facing consumer reads it off the wire.
+ */
+export interface EvseStatusData {
+ readonly availability: AvailabilityType
+ readonly connectors: readonly ConnectorEntry[]
+}
+
export interface EvseTemplate {
Connectors: Record<string, ConnectorStatus>
MeterValues?: SampledValueTemplate[]
export type { ConnectorEntry, ConnectorStatus } from './ConnectorStatus.js'
export type { EmptyObject } from './EmptyObject.js'
export type { HandleErrorParams } from './Error.js'
-export type { EvseEntry, EvseStatus, EvseTemplate } from './Evse.js'
+export type { EvseEntry, EvseEntryData, EvseStatus, EvseStatusData, EvseTemplate } from './Evse.js'
export { FileType } from './FileType.js'
export type { JsonObject, JsonType } from './JsonType.js'
export { MapStringifyFormat } from './MapStringifyFormat.js'
ChargingStationAutomaticTransactionGeneratorConfiguration,
ConnectorEntry,
ConnectorStatus,
- EvseEntry,
+ EvseEntryData,
EvseStatusConfiguration,
} from '../types/index.js'
.toArray()
}
-export const buildEvseEntries = (chargingStation: ChargingStation): EvseEntry[] => {
+export const buildEvseEntries = (chargingStation: ChargingStation): EvseEntryData[] => {
return chargingStation
.iterateEvses()
.map(({ evseId, evseStatus }) => ({
),
},
}))
- .toArray() as unknown as EvseEntry[]
+ .toArray()
}
export const buildEvsesStatus = (
import assert from 'node:assert/strict'
import { afterEach, beforeEach, describe, it } from 'node:test'
+import type { ChargingStation } from '../../../src/charging-station/index.js'
import type {
ChargingStationData,
+ EvseStatus,
TemplateStatistics,
UIServerConfiguration,
} from '../../../src/types/index.js'
OCPP16AvailabilityType,
OCPPVersion,
} from '../../../src/types/index.js'
+import { buildEvseEntries } from '../../../src/utils/ChargingStationConfigurationUtils.js'
import { logger } from '../../../src/utils/index.js'
import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
import {
evseId: 1,
evseStatus: {
availability: OCPP16AvailabilityType.Operative,
- connectors: new Map([
- [
- 1,
- {
+ connectors: [
+ {
+ connectorId: 1,
+ connectorStatus: {
availability: OCPP16AvailabilityType.Operative,
MeterValues: [],
status: ConnectorStatusEnum.Available,
},
- ],
- ]),
- MeterValues: [],
+ evseId: 1,
+ },
+ ],
},
},
- ] as ChargingStationData['evses'],
+ ] satisfies ChargingStationData['evses'],
})
)
server.mockListen(t)
assert.match(statusLine, /status="Available"/)
})
+ // issue #2046: the earlier EVSE-mode test hand-builds a synthetic Map, so it
+ // never exercised the array shape the worker->main producer actually emits.
+ // These feed the REAL buildEvseEntries output into the scrape.
+ const buildEvseWireEntries = (evses: Map<number, EvseStatus>): ChargingStationData['evses'] => {
+ const stub: Pick<ChargingStation, 'iterateEvses'> = {
+ * iterateEvses () {
+ for (const [evseId, evseStatus] of evses) {
+ yield { evseId, evseStatus }
+ }
+ },
+ }
+ return buildEvseEntries(stub as ChargingStation)
+ }
+
+ const evseStatusOf = (
+ connectorIds: number[],
+ status = ConnectorStatusEnum.Available
+ ): EvseStatus => ({
+ availability: OCPP16AvailabilityType.Operative,
+ connectors: new Map(
+ connectorIds.map(connectorId => [
+ connectorId,
+ { availability: OCPP16AvailabilityType.Operative, MeterValues: [], status },
+ ])
+ ),
+ })
+
+ await it('should serve 200 with the ACTUAL buildEvseEntries wire shape and sum connectors across EVSEs (issue #2046)', async t => {
+ const evsesWire = buildEvseWireEntries(
+ new Map<number, EvseStatus>([
+ [1, evseStatusOf([1, 2])],
+ [2, evseStatusOf([1])],
+ ])
+ )
+ assert.ok(
+ Array.isArray(evsesWire[0].evseStatus.connectors),
+ 'buildEvseEntries must emit evseStatus.connectors as an array (wire contract)'
+ )
+ server.addStation(buildStationData('station-2046', { connectors: [], evses: evsesWire }))
+ server.mockListen(t)
+
+ server.start()
+ const res = new MockServerResponse()
+ server.emitRequest(buildMetricsRequest(), res)
+ await awaitFinish(res)
+ assert.strictEqual(res.statusCode, 200)
+ const body = res.body ?? ''
+ assert.match(body, /^# HELP /m)
+ assert.match(body, /^# TYPE /m)
+ assert.match(body, /simulator_station_connectors_total\{[^}]*hash_id="station-2046"[^}]*\}\s+3/)
+ const statusLine = body
+ .split('\n')
+ .find(
+ l =>
+ l.startsWith('simulator_connector_status_info{') &&
+ l.includes('hash_id="station-2046"') &&
+ l.endsWith(' 1')
+ )
+ assert.ok(statusLine != null, 'simulator_connector_status_info value line not found')
+ })
+
+ await it('should count connector id 0 / evse id 0 in connectors_total (issue #2046)', async t => {
+ const evsesWire = buildEvseWireEntries(
+ new Map<number, EvseStatus>([
+ [0, evseStatusOf([0])],
+ [1, evseStatusOf([1])],
+ ])
+ )
+ server.addStation(buildStationData('station-2046-zero', { connectors: [], evses: evsesWire }))
+ server.mockListen(t)
+
+ server.start()
+ const res = new MockServerResponse()
+ server.emitRequest(buildMetricsRequest(), res)
+ await awaitFinish(res)
+ assert.strictEqual(res.statusCode, 200)
+ const body = res.body ?? ''
+ assert.match(
+ body,
+ /simulator_station_connectors_total\{[^}]*hash_id="station-2046-zero"[^}]*\}\s+2/
+ )
+ })
+
+ await it('should report connectors_total 0 (not NaN) for an EVSE with an empty connectors array (issue #2046)', async t => {
+ const evsesWire = buildEvseWireEntries(new Map<number, EvseStatus>([[1, evseStatusOf([])]]))
+ server.addStation(buildStationData('station-2046-empty', { connectors: [], evses: evsesWire }))
+ server.mockListen(t)
+
+ server.start()
+ const res = new MockServerResponse()
+ server.emitRequest(buildMetricsRequest(), res)
+ await awaitFinish(res)
+ assert.strictEqual(res.statusCode, 200)
+ const body = res.body ?? ''
+ assert.match(
+ body,
+ /simulator_station_connectors_total\{[^}]*hash_id="station-2046-empty"[^}]*\}\s+0/
+ )
+ })
+
await it('should detect off-by-one at soft cap boundary (strict-greater-than semantics)', async t => {
const warnSpy = t.mock.method(logger, 'warn', () => undefined)
import { afterEach, describe, it } from 'node:test'
import type { ChargingStation } from '../../src/charging-station/index.js'
-import type { ConnectorEntry, ConnectorStatus, EvseStatus } from '../../src/types/index.js'
+import type { ConnectorStatus, EvseStatus } from '../../src/types/index.js'
import { AvailabilityType } from '../../src/types/index.js'
import {
})
await describe('buildEvseEntries', async () => {
- await it('should return entries with evseId, evseStatus containing availability and connectors Map', () => {
+ await it('should return entries with evseId, evseStatus containing availability and connectors array', () => {
const { station } = createMockChargingStation({
connectorsCount: 1,
evseConfiguration: { evsesCount: 1 },
assert.strictEqual(result.length, 2)
assert.strictEqual(result[0].evseId, 0)
assert.strictEqual(result[0].evseStatus.availability, AvailabilityType.Operative)
- assert.strictEqual((result[0].evseStatus.connectors as unknown as ConnectorEntry[]).length, 0)
+ assert.strictEqual(result[0].evseStatus.connectors.length, 0)
assert.strictEqual(result[1].evseId, 1)
- const connectors1 = result[1].evseStatus.connectors as unknown as ConnectorEntry[]
+ const connectors1 = result[1].evseStatus.connectors
assert.strictEqual(connectors1.length, 1)
assert.strictEqual(connectors1[0].connectorId, 1)
assert.ok(!('transactionEndedMeterValues' in connectors1[0].connectorStatus))
assert.strictEqual(result.length, 2)
assert.strictEqual(result[0].evseId, 0)
assert.strictEqual(result[1].evseId, 3)
- const connectors3 = result[1].evseStatus.connectors as unknown as ConnectorEntry[]
+ const connectors3 = result[1].evseStatus.connectors
assert.strictEqual(connectors3.length, 2)
assert.ok(connectors3.some(c => c.connectorId === 2))
assert.ok(connectors3.some(c => c.connectorId === 5))