Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ await build({
from: ['./src/assets/idtags!(-template)*.json'],
to: ['./assets'],
},
{
from: ['./src/assets/ev-profiles!(-template)*.json'],
to: ['./assets'],
},
{
from: ['./src/assets/json-schemas/**/*.json'],
to: ['./assets/json-schemas'],
Expand Down
46 changes: 46 additions & 0 deletions src/assets/ev-profiles-template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"profiles": [
{
"id": "city-ev-40kWh",
"weight": 3,
"batteryCapacityWh": 40000,
"maxPowerW": 11000,
"initialSocPercentMin": 15,
"initialSocPercentMax": 55,
"chargingCurve": [
{ "socPercent": 0, "powerFraction": 1.0 },
{ "socPercent": 75, "powerFraction": 1.0 },
{ "socPercent": 90, "powerFraction": 0.4 },
{ "socPercent": 100, "powerFraction": 0.08 }
]
},
{
"id": "long-range-ev-77kWh",
"weight": 2,
"batteryCapacityWh": 77000,
"maxPowerW": 22000,
"initialSocPercentMin": 10,
"initialSocPercentMax": 60,
"chargingCurve": [
{ "socPercent": 0, "powerFraction": 1.0 },
{ "socPercent": 70, "powerFraction": 0.85 },
{ "socPercent": 90, "powerFraction": 0.35 },
{ "socPercent": 100, "powerFraction": 0.05 }
]
},
{
"id": "dc-fast-ev-90kWh",
"weight": 1,
"batteryCapacityWh": 90000,
"maxPowerW": 150000,
"initialSocPercentMin": 10,
"initialSocPercentMax": 40,
"chargingCurve": [
{ "socPercent": 0, "powerFraction": 1.0 },
{ "socPercent": 50, "powerFraction": 0.85 },
{ "socPercent": 80, "powerFraction": 0.45 },
{ "socPercent": 100, "powerFraction": 0.05 }
]
}
]
}
93 changes: 93 additions & 0 deletions src/charging-station/ChargingStation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ import {
validateStationInfo,
} from './Helpers.js'
import { IdTagsCache } from './IdTagsCache.js'
import {
type CoherentSession,
createCoherentSession,
type EvProfilesFile,
getEvProfilesFile,
loadEvProfilesFile,
resolveRootSeed,
} from './meter-values/index.js'
import {
buildBootNotificationRequest,
createOCPPServices,
Expand Down Expand Up @@ -206,6 +214,8 @@ export class ChargingStation extends EventEmitter {

private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
private coherentEvProfiles?: EvProfilesFile
private readonly coherentSessions: Map<number | string, CoherentSession>
private configurationFile!: string
private configurationFileHash!: string
private configuredSupervisionUrl!: URL
Expand Down Expand Up @@ -241,6 +251,7 @@ export class ChargingStation extends EventEmitter {
this.sharedLRUCache = SharedLRUCache.getInstance()
this.idTagsCache = IdTagsCache.getInstance()
this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
this.coherentSessions = new Map<number | string, CoherentSession>()

this.on(ChargingStationEvents.added, () => {
parentPort?.postMessage(buildAddedMessage(this))
Expand Down Expand Up @@ -345,6 +356,38 @@ export class ChargingStation extends EventEmitter {
}
}

/**
* Creates a coherent MeterValues session for the given transaction.
* Called by OCPP response handlers immediately after a successful
* StartTransaction. When `stationInfo.coherentMeterValues` is not `true`,
* or when no valid EV profile file is loaded, this method is a no-op.
* @param transactionId - Transaction identifier from the CSMS.
* @param connectorId - Connector on which the transaction is running.
* @returns The created session or `undefined` when coherent mode is off.
*/
public createCoherentSession (
transactionId: number | string,
connectorId: number
): CoherentSession | undefined {
if (this.stationInfo?.coherentMeterValues !== true) {
return undefined
}
if (this.coherentEvProfiles == null || this.coherentEvProfiles.profiles.length === 0) {
return undefined
}
const rootSeed = resolveRootSeed(this.stationInfo)
const session = createCoherentSession(this, {
connectorId,
profiles: this.coherentEvProfiles.profiles,
rootSeed,
transactionId,
})
if (session != null) {
this.coherentSessions.set(transactionId, session)
}
return session
}

/**
* Deletes the charging station instance and optionally its persisted configuration.
* @param deleteConfiguration - Whether to delete the persisted configuration file
Expand Down Expand Up @@ -399,6 +442,19 @@ export class ChargingStation extends EventEmitter {
this.removeAllListeners()
}

/**
* Removes the coherent session for a transaction. Idempotent — safe to
* call from every reset/stop/disconnect path.
* @param transactionId - Transaction identifier.
* @returns `true` when a session was removed, `false` otherwise.
*/
public destroyCoherentSession (transactionId: number | string | undefined): boolean {
if (transactionId == null) {
return false
}
return this.coherentSessions.delete(transactionId)
}

/**
* Emit a ChargingStation event only if there are listeners registered for it.
* This optimizes performance by avoiding unnecessary event emission.
Expand Down Expand Up @@ -458,6 +514,15 @@ export class ChargingStation extends EventEmitter {
return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
}

/**
* Retrieves the coherent session for a transaction, if any.
* @param transactionId - Transaction identifier.
* @returns The session or `undefined` when none exists.
*/
public getCoherentSession (transactionId: number | string): CoherentSession | undefined {
return this.coherentSessions.get(transactionId)
}

public getConnectionTimeout (): number {
if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
return convertToInt(
Expand Down Expand Up @@ -1814,6 +1879,7 @@ export class ChargingStation extends EventEmitter {
}
}
this.saveStationInfo()
this.initializeCoherentEvProfiles()
this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl()
if (this.stationInfo.enableStatistics === true) {
this.performanceStatistics = PerformanceStatistics.getInstance(
Expand Down Expand Up @@ -1844,6 +1910,33 @@ export class ChargingStation extends EventEmitter {
}
}

/**
* Loads and validates the EV profile file when coherent MeterValues are
* enabled. Fail-soft: any error disables coherent mode for this station
* (createCoherentSession then becomes a no-op).
*/
private initializeCoherentEvProfiles (): void {
this.coherentEvProfiles = undefined
if (this.stationInfo?.coherentMeterValues !== true) {
return
}
const evProfilesFile = getEvProfilesFile(this.stationInfo)
if (evProfilesFile == null) {
logger.warn(
`${this.logPrefix()} ${moduleName}.initializeCoherentEvProfiles: coherentMeterValues=true but no evProfilesFile is configured, coherent MeterValues disabled`
)
return
}
const loaded = loadEvProfilesFile(evProfilesFile, this.logPrefix())
if (loaded == null) {
logger.warn(
`${this.logPrefix()} ${moduleName}.initializeCoherentEvProfiles: EV profiles could not be loaded, coherent MeterValues disabled`
)
return
}
this.coherentEvProfiles = loaded
}

private initializeConnectorsFromTemplate (stationTemplate: ChargingStationTemplate): void {
if (stationTemplate.Connectors == null && isEmpty(this.connectors)) {
const errorMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`
Expand Down
4 changes: 4 additions & 0 deletions src/charging-station/Helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,10 @@ export const resetConnectorStatus = (connectorStatus: ConnectorStatus | undefine
connectorStatus.transactionRemoteStarted = false
connectorStatus.transactionStarted = false
delete connectorStatus.transactionStart
// NOTE: `transactionId` is deleted here. Callers that need to identify
// the just-stopped transaction (e.g. `ChargingStation.destroyCoherentSession`)
// MUST snapshot `connectorStatus.transactionId` BEFORE invoking this
// function.
delete connectorStatus.transactionId
delete connectorStatus.transactionIdTag
delete connectorStatus.transactionGroupIdToken
Expand Down
3 changes: 3 additions & 0 deletions src/charging-station/TemplateSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,14 @@ const BaseTemplateSchema = z.looseObject({
chargePointModel: z.string().min(1),
chargePointSerialNumberPrefix: z.string().optional(),
chargePointVendor: z.string().min(1),
coherentMeterValues: z.boolean().optional(),
commandsSupport: CommandsSupportSchema.optional(),
Configuration: OcppConfigurationSchema.optional(),
Connectors: z.record(z.string().regex(/^\d+$/), ConnectorStatusSchema).optional(),
currentOutType: z.string().optional(),
customValueLimitationMeterValues: z.boolean().optional(),
enableStatistics: z.boolean().optional(),
evProfilesFile: z.string().optional(),
Evses: z.record(z.string().regex(/^\d+$/), EvseTemplateSchema).optional(),
firmwareUpgrade: FirmwareUpgradeSchema.optional(),
firmwareVersion: z.string().optional(),
Expand Down Expand Up @@ -215,6 +217,7 @@ const BaseTemplateSchema = z.looseObject({
powerSharedByConnectors: z.boolean().optional(),
powerUnit: z.string().optional(),
randomConnectors: z.boolean().optional(),
randomSeed: z.number().int().optional(),
reconnectExponentialDelay: z.boolean().optional(),
registrationMaxRetries: z.number().optional(),
remoteAuthorization: z.boolean().optional(),
Expand Down
7 changes: 7 additions & 0 deletions src/charging-station/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export {
} from './Helpers.js'
export type { IBootstrap } from './IBootstrap.js'
export { IdTagsCache } from './IdTagsCache.js'
export type {
ChargingCurvePoint,
CoherentSession,
EvProfile,
EvProfilesFile,
ICoherentContext,
} from './meter-values/index.js'
export { SharedLRUCache } from './SharedLRUCache.js'
export { applyMigration, coerceVersion, CURRENT_SCHEMA_VERSION } from './TemplateMigrations.js'
export { StrictTemplateSchema, TemplateSchema } from './TemplateSchema.js'
Expand Down
Loading
Loading