if (outcome.status === 'rejected') {
const reason: unknown = outcome.reason
const msg = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason)
- console.error(` ✗ #${tasks[i].id} failed: ${msg}`)
+ console.error(` ✗ #${tasks[i]?.id ?? String(i)} failed: ${msg}`)
}
}
- Classify the issue type: `bug-fix`, `feature`, or `refactor`.
- Assess your confidence: `high` (clear scope, obvious approach), `medium` (some ambiguity), or `low` (unclear scope, multiple valid approaches).
- Formulate a root cause hypothesis: what is broken or missing, and why. This is a hypothesis for the implementer to validate — not a directive.
- - Define 2-4 acceptance criteria: concrete, verifiable conditions that must be true when the implementation is complete. Focus on observable behavior, not implementation details.
+ - Define 2-4 acceptance criteria: concrete, verifiable conditions that must be true when the implementation is complete. Focus on code structure, algorithmic and logic, not runtime behavior.
4. Output the plan in this exact format:
```
- Do not implement anything. Only produce the plan.
-- Acceptance criteria must be testable by reading code or running tests — no subjective assessments.
+- Acceptance criteria must be verifiable by static code inspection of the diff.
- Root cause hypothesis should be specific (mention modules, patterns, or behaviors) — not a restatement of the issue title.
## Completion
{{ACCEPTANCE_CRITERIA}}
-If acceptance criteria are listed above, verify that the implementation satisfies each one. Report a HIGH finding for any criterion that is not met. Do NOT evaluate whether the actor followed a specific implementation approach — only whether the observable outcome matches the criteria. If no criteria are listed, skip this section.
+If acceptance criteria are listed above, assess from the diff whether the implementation satisfies each one. Report a HIGH finding for any unmet criterion. Only judge observable outcomes, not implementation approach. If no criteria are listed, skip this section.
## Output Format
* Runs the full validation suite.
* @param cwd - Working directory (worktree path).
* @param spec - Optional task specification (used for logging).
+ * @param signal - Optional abort signal for cooperative cancellation.
* @returns `true` if validation passed, `false` otherwise.
*/
-export async function runValidation (cwd: string, spec?: TaskSpec): Promise<boolean> {
+export async function runValidation (
+ cwd: string,
+ spec?: TaskSpec,
+ signal?: AbortSignal
+): Promise<boolean> {
try {
await execFileAsync('sh', ['-c', VALIDATION_COMMAND], {
cwd,
maxBuffer: 8 * 1024 * 1024,
+ signal,
timeout: VALIDATION_TIMEOUT_MS,
})
return true
} catch (err: unknown) {
+ if (signal?.aborted === true) {
+ throw err
+ }
if (err && typeof err === 'object' && 'killed' in err && (err as { killed: boolean }).killed) {
const label = spec ? `#${spec.id}` : 'mid-loop'
console.warn(` ${label}: Validation timed out after ${String(VALIDATION_TIMEOUT_MS)}ms.`)
| firmwareVersionPattern | | Semantic versioning regular expression: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string | string | charging stations firmware version pattern |
| firmwareVersion | | undefined | string | charging stations firmware version |
| power | | | float \| float[] | charging stations maximum power value(s) |
-| powerSharedByConnectors | true/false | false | boolean | charging stations power shared by its connectors |
+| 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 |
| voltageOut | | AC:230/DC:400 | integer | charging stations voltage out |
"0": {},
"1": {
"bootStatus": "Available",
+ "maximumPower": 50000,
"MeterValues": [
...
{
"Connectors": {
"1": {
"bootStatus": "Available",
+ "maximumPower": 22080,
"MeterValues": [
...
{
"Connectors": {
"0": {},
"1": {
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
]
},
"2": {
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
"Connectors": {
"0": {},
"1": {
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
]
},
"2": {
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
"0": {},
"1": {
"bootStatus": "Available",
+ "maximumPower": 75000,
"MeterValues": [
{
"unit": "Percent",
},
"2": {
"bootStatus": "Preparing",
+ "maximumPower": 75000,
"MeterValues": [
{
"unit": "Percent",
},
"3": {
"bootStatus": "Faulted",
+ "maximumPower": 75000,
"MeterValues": [
{
"unit": "Percent",
"0": {},
"1": {
"bootStatus": "Available",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"2": {
"bootStatus": "Preparing",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"3": {
"bootStatus": "Faulted",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
"0": {},
"1": {
"bootStatus": "Available",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"2": {
"bootStatus": "Preparing",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"3": {
"bootStatus": "Faulted",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
"0": {},
"1": {
"bootStatus": "Available",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"2": {
"bootStatus": "Preparing",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"3": {
"bootStatus": "SuspendedEVSE",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"4": {
"bootStatus": "SuspendedEV",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"5": {
"bootStatus": "Finishing",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"6": {
"bootStatus": "Reserved",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"7": {
"bootStatus": "Charging",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"8": {
"bootStatus": "Unavailable",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
},
"9": {
"bootStatus": "Faulted",
+ "maximumPower": 50000,
"MeterValues": [
{
"unit": "Percent",
getChargingStationChargingProfilesLimit,
getChargingStationId,
getConnectorChargingProfilesLimit,
+ getDefaultConnectorMaximumPower,
getDefaultVoltageOut,
getHashId,
getIdTagsFile,
return Number.POSITIVE_INFINITY
}
const connectorMaximumPower = maximumPower / (this.powerDivider ?? 1)
+ const connectorHardwareMaximumPower = this.getConnectorStatus(connectorId)?.maximumPower
const chargingStationChargingProfilesLimit =
(getChargingStationChargingProfilesLimit(this) ?? Number.POSITIVE_INFINITY) /
(this.powerDivider ?? 1)
const connectorChargingProfilesLimit = getConnectorChargingProfilesLimit(this, connectorId)
return min(
Number.isNaN(connectorMaximumPower) ? Number.POSITIVE_INFINITY : connectorMaximumPower,
+ connectorHardwareMaximumPower == null || Number.isNaN(connectorHardwareMaximumPower)
+ ? Number.POSITIVE_INFINITY
+ : connectorHardwareMaximumPower,
connectorAmperageLimitationLimit == null || Number.isNaN(connectorAmperageLimitationLimit)
? Number.POSITIVE_INFINITY
: connectorAmperageLimitationLimit,
)
this.connectors.set(connectorId, clone(connectorStatus))
}
- initializeConnectorsMapStatus(this.connectors, this.logPrefix())
+ initializeConnectorsMapStatus(
+ this.connectors,
+ this.logPrefix(),
+ getDefaultConnectorMaximumPower(stationTemplate)
+ )
this.saveConnectorsStatus()
} else {
logger.warn(
),
}
this.evses.set(evseId, evseStatus)
- initializeConnectorsMapStatus(evseStatus.connectors, this.logPrefix())
+ initializeConnectorsMapStatus(
+ evseStatus.connectors,
+ this.logPrefix(),
+ getDefaultConnectorMaximumPower(stationTemplate)
+ )
}
this.saveEvsesStatus()
} else {
CurrentType,
type EvseTemplate,
OCPPVersion,
+ PowerUnits,
RecurrencyKindType,
type Reservation,
ReservationTerminationReason,
return isEmpty(evses) ? 0 : Object.keys(evses).length
}
+export const getDefaultConnectorMaximumPower = (
+ stationTemplate: ChargingStationTemplate
+): number | undefined => {
+ let maximumPower: number | undefined
+ if (isNotEmptyArray<number>(stationTemplate.power)) {
+ const powerArrayRandomIndex = Math.floor(secureRandom() * stationTemplate.power.length)
+ maximumPower =
+ stationTemplate.powerUnit === PowerUnits.KILO_WATT
+ ? stationTemplate.power[powerArrayRandomIndex] * 1000
+ : stationTemplate.power[powerArrayRandomIndex]
+ } else if (typeof stationTemplate.power === 'number') {
+ maximumPower =
+ stationTemplate.powerUnit === PowerUnits.KILO_WATT
+ ? stationTemplate.power * 1000
+ : stationTemplate.power
+ }
+ if (maximumPower == null) {
+ return undefined
+ }
+ if (stationTemplate.powerSharedByConnectors === true) {
+ return maximumPower
+ }
+ const staticCount =
+ stationTemplate.Evses != null
+ ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ getMaxNumberOfEvses(stationTemplate.Evses) - (stationTemplate.Evses['0'] != null ? 1 : 0)
+ : getMaxNumberOfConnectors(stationTemplate.Connectors) -
+ (stationTemplate.Connectors?.['0'] != null ? 1 : 0)
+ return staticCount > 0 ? maximumPower / staticCount : undefined
+}
+
const getMaxNumberOfConnectors = (
connectors: Record<string, ConnectorStatus> | undefined
): number => {
export const initializeConnectorsMapStatus = (
connectors: Map<number, ConnectorStatus>,
- logPrefix: string
+ logPrefix: string,
+ defaultMaximumPower?: number
): void => {
for (const [connectorId, connectorStatus] of connectors) {
if (connectorId > 0 && connectorStatus.transactionStarted === true) {
connectorStatus.availability = AvailabilityType.Operative
connectorStatus.chargingProfiles ??= []
} else if (connectorId > 0 && connectorStatus.transactionStarted == null) {
- initializeConnectorStatus(connectorStatus)
+ initializeConnectorStatus(connectorStatus, defaultMaximumPower)
}
}
}
if (maximumPower == null) {
return limit
}
- const connectorMaximumPower = maximumPower / (chargingStation.powerDivider ?? 1)
+ const connectorMaximumPower =
+ chargingStation.getConnectorStatus(connectorId)?.maximumPower ??
+ maximumPower / (chargingStation.powerDivider ?? 1)
if (limit > connectorMaximumPower) {
logger.error(
`${chargingStation.logPrefix()} ${moduleName}.getConnectorChargingProfilesLimit: Charging profile id ${getChargingProfileId(chargingProfilesLimit.chargingProfile)} limit ${limit.toString()} is greater than connector ${connectorId.toString()} maximum ${connectorMaximumPower.toString()}: %j`,
}
}
-const initializeConnectorStatus = (connectorStatus: ConnectorStatus): void => {
+const initializeConnectorStatus = (
+ connectorStatus: ConnectorStatus,
+ defaultMaximumPower?: number
+): void => {
connectorStatus.availability = AvailabilityType.Operative
connectorStatus.idTagLocalAuthorized = false
connectorStatus.idTagAuthorized = false
connectorStatus.energyActiveImportRegisterValue = 0
connectorStatus.transactionEnergyActiveImportRegisterValue = 0
connectorStatus.chargingProfiles ??= []
+ if (defaultMaximumPower != null) {
+ connectorStatus.maximumPower ??= defaultMaximumPower
+ }
}
const warnDeprecatedTemplateKey = (
idTagLocalAuthorized?: boolean
localAuthorizeIdTag?: string
locked?: boolean
+ maximumPower?: number // In W
MeterValues: SampledValueTemplate[]
publicKeySentInTransaction?: boolean
remoteStartId?: number