94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { useDrizzle } from '@/lib/db.js'
|
|
import dayjs from 'dayjs'
|
|
import { chargePoint } from '@/db/schema.js'
|
|
import type {
|
|
BootNotificationRequest,
|
|
BootNotificationResponse,
|
|
OcppConnectionContext,
|
|
} from '../types.ts'
|
|
|
|
const DEFAULT_HEARTBEAT_INTERVAL = 60
|
|
const BOOT_NOTIFICATION_DB_TIMEOUT_MS = 3000
|
|
|
|
function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string) {
|
|
return Promise.race<T>([
|
|
promise,
|
|
new Promise<T>((_, reject) => {
|
|
setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
}),
|
|
])
|
|
}
|
|
|
|
export async function handleBootNotification(
|
|
payload: BootNotificationRequest,
|
|
ctx: OcppConnectionContext,
|
|
): Promise<BootNotificationResponse> {
|
|
const db = useDrizzle()
|
|
const currentTime = dayjs().toISOString()
|
|
|
|
console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} received`)
|
|
|
|
try {
|
|
const [cp] = await withTimeout(
|
|
db
|
|
.insert(chargePoint)
|
|
.values({
|
|
id: crypto.randomUUID(),
|
|
chargePointIdentifier: ctx.chargePointIdentifier,
|
|
chargePointVendor: payload.chargePointVendor,
|
|
chargePointModel: payload.chargePointModel,
|
|
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
|
|
firmwareVersion: payload.firmwareVersion ?? null,
|
|
iccid: payload.iccid ?? null,
|
|
imsi: payload.imsi ?? null,
|
|
meterType: payload.meterType ?? null,
|
|
meterSerialNumber: payload.meterSerialNumber ?? null,
|
|
// New, unknown devices start as Pending — admin must manually accept them
|
|
registrationStatus: 'Pending',
|
|
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
|
lastBootNotificationAt: dayjs().toDate(),
|
|
})
|
|
.onConflictDoUpdate({
|
|
target: chargePoint.chargePointIdentifier,
|
|
set: {
|
|
chargePointVendor: payload.chargePointVendor,
|
|
chargePointModel: payload.chargePointModel,
|
|
chargePointSerialNumber: payload.chargePointSerialNumber ?? null,
|
|
firmwareVersion: payload.firmwareVersion ?? null,
|
|
iccid: payload.iccid ?? null,
|
|
imsi: payload.imsi ?? null,
|
|
meterType: payload.meterType ?? null,
|
|
meterSerialNumber: payload.meterSerialNumber ?? null,
|
|
// Do NOT override registrationStatus — preserve whatever the admin set
|
|
heartbeatInterval: DEFAULT_HEARTBEAT_INTERVAL,
|
|
lastBootNotificationAt: dayjs().toDate(),
|
|
updatedAt: dayjs().toDate(),
|
|
},
|
|
})
|
|
.returning(),
|
|
BOOT_NOTIFICATION_DB_TIMEOUT_MS,
|
|
`BootNotification persistence for ${ctx.chargePointIdentifier}`,
|
|
)
|
|
|
|
const status = cp.registrationStatus
|
|
ctx.isRegistered = status === 'Accepted'
|
|
|
|
console.log(`[OCPP] BootNotification ${ctx.chargePointIdentifier} status=${status}`)
|
|
|
|
return {
|
|
currentTime,
|
|
interval: DEFAULT_HEARTBEAT_INTERVAL,
|
|
status,
|
|
}
|
|
} catch (error) {
|
|
ctx.isRegistered = false
|
|
console.error(`[OCPP] BootNotification ${ctx.chargePointIdentifier} persistence failed:`, error)
|
|
|
|
return {
|
|
currentTime,
|
|
interval: DEFAULT_HEARTBEAT_INTERVAL,
|
|
status: 'Pending',
|
|
}
|
|
}
|
|
}
|