Eleven commits from the from-zero audit (three review rounds).
Fixes:
- C2 (§4.2) forbid non-triggered outbound messages in Pending state under non-strict compliance
- H2 emit StatusNotification(Finishing) before StopTransaction.req in the non-remote stop path
- H6 (§5.11) RemoteStart auto-select skips connectors reserved for a different idTag
- H9 partial: .unref() on 5 lifecycle timer sites (3 setTimeout + 2 setInterval) so they no longer block node.js shutdown; per-site safety-invariant comments
- H10 partial: initialize 5 string ChargingStation fields in the constructor; drop their definite-assignment markers
- H11 log the errors previously swallowed by .catch(() => undefined) in AbstractUIServer
Review follow-ups:
- Correct method name in stop() metrics-cleanup log prefix
- Clarify runMetricsScrape catch-log wording
- Correct spec-citation in stopTransactionOnConnector test title
Gates: format / typecheck / lint / build / test (2951 pass / 0 fail / 6 skipped) all pass at tip
ca6b0182. Skott circular-dependency count preserved at 61.
Empirically-rescinded audit findings: H4, H5, H8.
Deferred with concrete rationale: H7, H9 residual, H10 residual.
private automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel
- private configurationFile!: string
- private configurationFileHash!: string
+ private configurationFile: string
+ private configurationFileHash: string
private configuredSupervisionUrl!: URL
private readonly connectors: Map<number, ConnectorStatus>
- private connectorsConfigurationHash!: string
+ private connectorsConfigurationHash: string
private readonly evses: Map<number, EvseStatus>
- private evsesConfigurationHash!: string
+ private evsesConfigurationHash: string
private flushingMessageBuffer: boolean
private flushMessageBufferSetInterval?: NodeJS.Timeout
private readonly messageQueue: string[]
private ocppIncomingRequestService!: OCPPIncomingRequestService
private readonly sharedLRUCache: SharedLRUCache
private stopping: boolean
- private templateFileHash!: string
+ private templateFileHash: string
private templateFileWatcher?: FSWatcher
private wsConnectionRetryCount: number
private wsPingSetInterval?: NodeJS.Timeout
this.sharedLRUCache = SharedLRUCache.getInstance()
this.idTagsCache = IdTagsCache.getInstance()
this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this)
+ this.configurationFile = ''
+ this.configurationFileHash = ''
+ this.connectorsConfigurationHash = ''
+ this.evsesConfigurationHash = ''
+ this.templateFileHash = ''
this.on(ChargingStationEvents.added, () => {
parentPort?.postMessage(buildAddedMessage(this))
)
})
} else {
+ // Unref: fire-and-forget deferred firmware update must not
+ // block node.js exit.
setTimeout(() => {
this.updateFirmwareSimulation(chargingStation).catch((error: unknown) => {
logger.error(
error
)
})
- }, retrieveDate.getTime() - now)
+ }, retrieveDate.getTime() - now).unref()
}
}
)
connectorId <= chargingStation.getNumberOfConnectors();
connectorId++
) {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ const isReservedForOther =
+ connectorStatus?.status === OCPP16ChargePointStatus.Reserved &&
+ !OCPP16ServiceUtils.hasReservation(chargingStation, connectorId, commandPayload.idTag)
if (
chargingStation.isConnectorAvailable(connectorId) &&
- chargingStation.getConnectorStatus(connectorId)?.transactionStarted === false &&
- !OCPP16ServiceUtils.hasReservation(chargingStation, connectorId, commandPayload.idTag)
+ connectorStatus?.transactionStarted === false &&
+ !isReservedForOther
) {
commandPayload.connectorId = connectorId
break
connectorId: number,
reason?: StopTransactionReason
): Promise<StopTransactionResponse> {
+ const connectorStatus = chargingStation.getConnectorStatus(connectorId)
+ if (connectorStatus?.status !== OCPP16ChargePointStatus.Finishing) {
+ await sendAndSetConnectorStatus(chargingStation, {
+ connectorId,
+ status: OCPP16ChargePointStatus.Finishing,
+ } as OCPP16StatusNotificationRequest)
+ }
const rawTransactionId = chargingStation.getConnectorStatus(connectorId)?.transactionId
const transactionId = rawTransactionId != null ? convertToInt(rawTransactionId) : undefined
if (
logger.info(
`${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: Executing EVSE ${evseId.toString()} reset${hasActiveTransactions ? ' after transaction termination' : ''}`
)
+ // Unref: fire-and-forget pending EVSE reset must not block
+ // node.js exit.
setTimeout(() => {
const evse = chargingStation.getEvseStatus(evseId)
if (evse) {
`${chargingStation.logPrefix()} ${moduleName}.scheduleEvseReset: EVSE ${evseId.toString()} reset completed`
)
}
- }, OCPP20Constants.RESET_DELAY_MS)
+ }, OCPP20Constants.RESET_DELAY_MS).unref()
})
}
* @param evseId - The EVSE identifier to reset
*/
private scheduleEvseResetOnIdle (chargingStation: ChargingStation, evseId: number): void {
+ // Unref: idle-monitor poll must not block node.js exit; the interval
+ // self-clears once the EVSE is idle or its state is gone.
const monitorInterval = setInterval(() => {
const evse = chargingStation.getEvseStatus(evseId)
if (evse != null) {
} else {
clearInterval(monitorInterval)
}
- }, OCPP20Constants.RESET_IDLE_MONITOR_INTERVAL_MS)
+ }, OCPP20Constants.RESET_IDLE_MONITOR_INTERVAL_MS).unref()
}
/**
* @param chargingStation - The charging station instance
*/
private scheduleResetOnIdle (chargingStation: ChargingStation): void {
+ // Unref: idle-monitor poll must not block node.js exit; the interval
+ // self-clears once the station reports idle.
const monitorInterval = setInterval(() => {
if (this.isChargingStationIdle(chargingStation)) {
clearInterval(monitorInterval)
)
})
}
- }, OCPP20Constants.RESET_IDLE_MONITOR_INTERVAL_MS)
+ }, OCPP20Constants.RESET_IDLE_MONITOR_INTERVAL_MS).unref()
}
private selectAvailableEvse (chargingStation: ChargingStation): number | undefined {
)
queue.unshift({ ...event, retryCount })
stationState.isDrainingSecurityEvents = false
+ // Unref: fire-and-forget retry must not block node.js exit.
setTimeout(() => {
this.sendQueuedSecurityEvents(chargingStation)
- }, OCPP20Constants.SECURITY_EVENT_RETRY_DELAY_MS)
+ }, OCPP20Constants.SECURITY_EVENT_RETRY_DELAY_MS).unref()
})
}
drainNextEvent()
chargingStation.inRejectedState()) &&
commandName === RequestCommand.BOOT_NOTIFICATION) ||
(chargingStation.stationInfo?.ocppStrictCompliance === false &&
- (chargingStation.inUnknownState() || chargingStation.inPendingState())) ||
+ chargingStation.inUnknownState()) ||
chargingStation.inAcceptedState() ||
(chargingStation.inPendingState() &&
(params.triggerMessage === true || messageType === MessageType.CALL_RESULT_MESSAGE))
.finally(() => {
registry.clear()
})
- .catch(() => undefined)
+ .catch((error: unknown) => {
+ logger.warn(
+ `${this.logPrefix()} ${moduleName}.stop: metrics registry cleanup failed`,
+ error
+ )
+ })
this.metricsRegistry = undefined
}
// detachTransport() / uiService.stop() are subclass hooks — see
registry: Registry
): Promise<void> {
this.metricsScrapeChain = this.metricsScrapeChain
- .catch(() => undefined)
+ .catch((error: unknown) => {
+ logger.warn(
+ `${this.logPrefix()} ${moduleName}.runMetricsScrape: previous scrape errored; absorbing rejection so next scrape can proceed`,
+ error
+ )
+ })
.then(async () => {
this.metricsSampleCount = 0
// prom-client's sync prefix runs every collect() (each writing
})
await describe('stopTransactionOnConnector', async () => {
- await it('should send StopTransaction for OCPP 1.6 stations and return accepted: true', async () => {
+ await it('should send StatusNotification(Finishing) then StopTransaction for OCPP 1.6 stations (matches remote-stop message sequence)', async () => {
const { requestHandler, station } = createStationWithRequestHandler()
requestHandler.mock.mockImplementation(async (..._args: unknown[]) =>
Promise.resolve({ idTagInfo: { status: 'Accepted' } })
assert.strictEqual(result.accepted, true)
assert.ok(
- requestHandler.mock.calls.length >= 1,
- 'request handler should have been called at least once'
+ requestHandler.mock.calls.length >= 2,
+ 'request handler should have been called at least twice (StatusNotification + StopTransaction)'
+ )
+ assert.strictEqual(requestHandler.mock.calls[0].arguments[1], 'StatusNotification')
+ assert.strictEqual(
+ (requestHandler.mock.calls[0].arguments[2] as { status: string }).status,
+ 'Finishing'
)
- assert.strictEqual(requestHandler.mock.calls[0].arguments[1], 'StopTransaction')
+ assert.strictEqual(requestHandler.mock.calls[1].arguments[1], 'StopTransaction')
})
await it('should send TransactionEvent(Ended) for OCPP 2.0 stations and return accepted: true', async () => {