feat(ocpp): implement BootNotification, Heartbeat, and StatusNotification actions with database integration
This commit is contained in:
142
apps/csms/src/ocpp/handler.ts
Normal file
142
apps/csms/src/ocpp/handler.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { WSContext } from 'hono/ws'
|
||||
import { isSupportedOCPP } from '@/constants.js'
|
||||
import {
|
||||
OCPP_MESSAGE_TYPE,
|
||||
type OcppCall,
|
||||
type OcppErrorCode,
|
||||
type OcppMessage,
|
||||
type OcppConnectionContext,
|
||||
type BootNotificationRequest,
|
||||
type BootNotificationResponse,
|
||||
type HeartbeatRequest,
|
||||
type HeartbeatResponse,
|
||||
type StatusNotificationRequest,
|
||||
type StatusNotificationResponse,
|
||||
} from './types.ts'
|
||||
import { handleBootNotification } from './actions/boot-notification.ts'
|
||||
import { handleHeartbeat } from './actions/heartbeat.ts'
|
||||
import { handleStatusNotification } from './actions/status-notification.ts'
|
||||
|
||||
// Typed dispatch map — only registered actions are accepted
|
||||
type ActionHandlerMap = {
|
||||
BootNotification: (
|
||||
payload: BootNotificationRequest,
|
||||
ctx: OcppConnectionContext,
|
||||
) => Promise<BootNotificationResponse>
|
||||
Heartbeat: (
|
||||
payload: HeartbeatRequest,
|
||||
ctx: OcppConnectionContext,
|
||||
) => Promise<HeartbeatResponse>
|
||||
StatusNotification: (
|
||||
payload: StatusNotificationRequest,
|
||||
ctx: OcppConnectionContext,
|
||||
) => Promise<StatusNotificationResponse>
|
||||
}
|
||||
|
||||
const actionHandlers: ActionHandlerMap = {
|
||||
BootNotification: handleBootNotification,
|
||||
Heartbeat: handleHeartbeat,
|
||||
StatusNotification: handleStatusNotification,
|
||||
}
|
||||
|
||||
function sendCallResult(ws: WSContext, uniqueId: string, payload: unknown): void {
|
||||
ws.send(JSON.stringify([OCPP_MESSAGE_TYPE.CALLRESULT, uniqueId, payload]))
|
||||
}
|
||||
|
||||
function sendCallError(
|
||||
ws: WSContext,
|
||||
uniqueId: string,
|
||||
errorCode: OcppErrorCode,
|
||||
errorDescription: string,
|
||||
): void {
|
||||
ws.send(
|
||||
JSON.stringify([OCPP_MESSAGE_TYPE.CALLERROR, uniqueId, errorCode, errorDescription, {}]),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that produces a hono-ws event handler object for a single
|
||||
* OCPP WebSocket connection.
|
||||
*
|
||||
* Usage in route:
|
||||
* upgradeWebSocket((c) => createOcppHandler(c.req.param('chargePointId'), remoteAddr))
|
||||
*/
|
||||
export function createOcppHandler(chargePointIdentifier: string, remoteAddr?: string) {
|
||||
const ctx: OcppConnectionContext = {
|
||||
chargePointIdentifier,
|
||||
isRegistered: false,
|
||||
}
|
||||
|
||||
return {
|
||||
onOpen(_evt: Event, ws: WSContext) {
|
||||
const subProtocol = ws.protocol ?? 'unknown'
|
||||
if (!isSupportedOCPP(subProtocol)) {
|
||||
ws.close(1002, 'Unsupported subprotocol')
|
||||
return
|
||||
}
|
||||
console.log(
|
||||
`[OCPP] ${chargePointIdentifier} connected` +
|
||||
(remoteAddr ? ` from ${remoteAddr}` : ''),
|
||||
)
|
||||
},
|
||||
|
||||
async onMessage(evt: MessageEvent, ws: WSContext) {
|
||||
let uniqueId = '(unknown)'
|
||||
try {
|
||||
const raw = evt.data
|
||||
if (typeof raw !== 'string') return
|
||||
|
||||
let message: OcppMessage
|
||||
try {
|
||||
message = JSON.parse(raw) as OcppMessage
|
||||
} catch {
|
||||
sendCallError(ws, uniqueId, 'FormationViolation', 'Invalid JSON')
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(message) || message.length < 3) {
|
||||
sendCallError(ws, uniqueId, 'FormationViolation', 'Message must be a JSON array')
|
||||
return
|
||||
}
|
||||
|
||||
const [messageType, msgUniqueId] = message
|
||||
uniqueId = String(msgUniqueId)
|
||||
|
||||
// CSMS only handles CALL messages from the charge point
|
||||
if (messageType !== OCPP_MESSAGE_TYPE.CALL) return
|
||||
|
||||
const [, , action, payload] = message as OcppCall
|
||||
|
||||
// Enforce BootNotification before any other action
|
||||
if (!ctx.isRegistered && action !== 'BootNotification') {
|
||||
sendCallError(
|
||||
ws,
|
||||
uniqueId,
|
||||
'SecurityError',
|
||||
'Charge point must send BootNotification first',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const handler = actionHandlers[action as keyof ActionHandlerMap]
|
||||
if (!handler) {
|
||||
sendCallError(ws, uniqueId, 'NotImplemented', `Action '${action}' is not implemented`)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await (
|
||||
handler as (payload: unknown, ctx: OcppConnectionContext) => Promise<unknown>
|
||||
)(payload, ctx)
|
||||
|
||||
sendCallResult(ws, uniqueId, response)
|
||||
} catch (err) {
|
||||
console.error(`[OCPP] Error handling message from ${chargePointIdentifier} (uniqueId=${uniqueId}):`, err)
|
||||
sendCallError(ws, uniqueId, 'InternalError', 'Internal server error')
|
||||
}
|
||||
},
|
||||
|
||||
onClose(evt: CloseEvent, _ws: WSContext) {
|
||||
console.log(`[OCPP] ${chargePointIdentifier} disconnected (code=${evt.code})`)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user