feat(firmware): add OCPP schema and constants for charge point management
This commit is contained in:
8
apps/csms/src/constants.ts
Normal file
8
apps/csms/src/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const SUPPORTED_OCPP_VERSIONS = ['ocpp1.6'] as const
|
||||||
|
export type SupportedOCPPVersion = (typeof SUPPORTED_OCPP_VERSIONS)[number]
|
||||||
|
|
||||||
|
export const isSupportedOCPP = (
|
||||||
|
version: string,
|
||||||
|
): version is SupportedOCPPVersion => {
|
||||||
|
return SUPPORTED_OCPP_VERSIONS.includes(version as SupportedOCPPVersion)
|
||||||
|
}
|
||||||
254
apps/csms/src/db/ocpp-schema.ts
Normal file
254
apps/csms/src/db/ocpp-schema.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
timestamp,
|
||||||
|
varchar,
|
||||||
|
integer,
|
||||||
|
text,
|
||||||
|
index,
|
||||||
|
} from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 充电桩表
|
||||||
|
* 对应OCPP 1.6-J BootNotification.req中的基本信息
|
||||||
|
*/
|
||||||
|
export const chargePoint = pgTable('charge_point', {
|
||||||
|
id: varchar('id').primaryKey(),
|
||||||
|
chargePointIdentifier: varchar('charge_point_identifier', {
|
||||||
|
length: 100,
|
||||||
|
})
|
||||||
|
.unique()
|
||||||
|
.notNull(),
|
||||||
|
chargePointSerialNumber: varchar('charge_point_serial_number', {
|
||||||
|
length: 25,
|
||||||
|
}),
|
||||||
|
chargePointModel: varchar('charge_point_model', { length: 20 }).notNull(),
|
||||||
|
chargePointVendor: varchar('charge_point_vendor', { length: 20 }).notNull(),
|
||||||
|
firmwareVersion: varchar('firmware_version', { length: 50 }),
|
||||||
|
iccid: varchar('iccid', { length: 20 }),
|
||||||
|
imsi: varchar('imsi', { length: 20 }),
|
||||||
|
meterSerialNumber: varchar('meter_serial_number', { length: 25 }),
|
||||||
|
meterType: varchar('meter_type', { length: 25 }),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at')
|
||||||
|
.notNull()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdate(() => new Date()),
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接器表
|
||||||
|
* OCPP 1.6-J 2.2术语定义:
|
||||||
|
* "Connector" 指充电桩上可独立操作和管理的电气插座。通常对应单个物理连接器,
|
||||||
|
* 但某些情况下一个插座可能有多个物理插座类型和/或拴住的电缆/连接器安排
|
||||||
|
* 以适应不同的车辆类型(如四轮电动汽车和电动滑板车)。
|
||||||
|
*
|
||||||
|
* 连接器ID规范:
|
||||||
|
* - 第一个连接器的ID必须是1
|
||||||
|
* - 额外的连接器必须按顺序编号(不能跳过)
|
||||||
|
* - 连接器ID不能超过充电桩的总连接器数
|
||||||
|
* - ID为0保留用于主控制器(在报告时)或整个充电桩(在中央系统的操作时)
|
||||||
|
*/
|
||||||
|
export const connector = pgTable(
|
||||||
|
'connector',
|
||||||
|
{
|
||||||
|
id: varchar('id').primaryKey(),
|
||||||
|
chargePointId: varchar('charge_point_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||||||
|
/**
|
||||||
|
* 连接器编号(connectorId)
|
||||||
|
* OCPP 1.6-J 6.47 StatusNotification.req:
|
||||||
|
* "The id of the connector for which the status is reported.
|
||||||
|
* Id '0' (zero) is used if the status is for the Charge Point main controller."
|
||||||
|
* connectorId > 0 对实际连接器, connectorId = 0 表示主控制器
|
||||||
|
*/
|
||||||
|
connectorId: integer('connector_id').notNull(),
|
||||||
|
/**
|
||||||
|
* 当前状态(status字段)
|
||||||
|
* OCPP 1.6-J 7.7 ChargePointStatus:
|
||||||
|
* - Available: 连接器可用于新用户
|
||||||
|
* - Preparing: 用户提示卡、插入电缆或车辆占用停泊位时
|
||||||
|
* - Charging: 接触器闭合,允许车辆充电
|
||||||
|
* - SuspendedEVSE: EV连接但EVSE不提供能量
|
||||||
|
* - SuspendedEV: EVSE提供能量但EV不取用
|
||||||
|
* - Finishing: 交易已停止但连接器未准备好新用户
|
||||||
|
* - Reserved: 连接器因ReserveNow命令被保留
|
||||||
|
* - Unavailable: 因ChangeAvailability命令不可用(非运行态)
|
||||||
|
* - Faulted: 充电桩或连接器报告错误且不可用(非运行态)
|
||||||
|
*/
|
||||||
|
status: varchar('status', {
|
||||||
|
enum: [
|
||||||
|
'Available',
|
||||||
|
'Preparing',
|
||||||
|
'Charging',
|
||||||
|
'SuspendedEVSE',
|
||||||
|
'SuspendedEV',
|
||||||
|
'Finishing',
|
||||||
|
'Reserved',
|
||||||
|
'Unavailable',
|
||||||
|
'Faulted',
|
||||||
|
],
|
||||||
|
}).notNull(),
|
||||||
|
/**
|
||||||
|
* 错误代码(errorCode字段)
|
||||||
|
* OCPP 1.6-J 7.6 ChargePointErrorCode
|
||||||
|
*/
|
||||||
|
errorCode: varchar('error_code', {
|
||||||
|
enum: [
|
||||||
|
'NoError',
|
||||||
|
'ConnectorLockFailure',
|
||||||
|
'EVCommunicationError',
|
||||||
|
'GroundFailure',
|
||||||
|
'HighTemperature',
|
||||||
|
'InternalError',
|
||||||
|
'LocalListConflict',
|
||||||
|
'OtherError',
|
||||||
|
'OverCurrentFailure',
|
||||||
|
'OverVoltage',
|
||||||
|
'PowerMeterFailure',
|
||||||
|
'PowerSwitchFailure',
|
||||||
|
'ReaderFailure',
|
||||||
|
'ResetFailure',
|
||||||
|
'UnderVoltage',
|
||||||
|
'WeakSignal',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default('NoError'),
|
||||||
|
/**
|
||||||
|
* 供应商标识(vendorId字段)
|
||||||
|
* OCPP 1.6-J 6.47: "This identifies the vendor-specific implementation."
|
||||||
|
* CiString255Type - 不超过255个字符
|
||||||
|
*/
|
||||||
|
vendorId: varchar('vendor_id', { length: 255 }),
|
||||||
|
/**
|
||||||
|
* 供应商特定错误代码(vendorErrorCode字段)
|
||||||
|
* OCPP 1.6-J 6.47: "This contains the vendor-specific error code."
|
||||||
|
* CiString50Type - 不超过50个字符
|
||||||
|
*/
|
||||||
|
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
|
||||||
|
/**
|
||||||
|
* 附加信息(info字段)
|
||||||
|
* OCPP 1.6-J 6.47: "Additional free format information related to the error."
|
||||||
|
* CiString50Type - 不超过50个字符
|
||||||
|
*/
|
||||||
|
info: varchar('info', { length: 50 }),
|
||||||
|
/**
|
||||||
|
* 最后一次状态更新时间
|
||||||
|
* OCPP 1.6-J 6.47: "The time for which the status is reported.
|
||||||
|
* If absent time of receipt of the message will be assumed."
|
||||||
|
*/
|
||||||
|
lastStatusUpdate: timestamp('last_status_update').notNull().defaultNow(),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
updatedAt: timestamp('updated_at')
|
||||||
|
.notNull()
|
||||||
|
.defaultNow()
|
||||||
|
.$onUpdate(() => new Date()),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
chargePointIdIdx: index('idx_connector_charge_point_id').on(
|
||||||
|
table.chargePointId
|
||||||
|
),
|
||||||
|
connectorIdIdx: index('idx_connector_connector_id').on(
|
||||||
|
table.chargePointId,
|
||||||
|
table.connectorId
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接器状态历史表
|
||||||
|
* 记录StatusNotification.req的完整消息内容,用于审计和历史查询
|
||||||
|
* 对应 OCPP 1.6-J 6.47 StatusNotification.req 消息
|
||||||
|
*/
|
||||||
|
export const connectorStatusHistory = pgTable(
|
||||||
|
'connector_status_history',
|
||||||
|
{
|
||||||
|
id: varchar('id').primaryKey(),
|
||||||
|
connectorId: varchar('connector_id')
|
||||||
|
.notNull()
|
||||||
|
.references(() => connector.id, { onDelete: 'cascade' }),
|
||||||
|
/**
|
||||||
|
* 连接器编号(connectorId)
|
||||||
|
* OCPP 1.6-J 6.47: connectorId >= 0
|
||||||
|
*/
|
||||||
|
connectorNumber: integer('connector_number').notNull(),
|
||||||
|
/**
|
||||||
|
* 状态值
|
||||||
|
* OCPP 1.6-J 7.7 ChargePointStatus
|
||||||
|
*/
|
||||||
|
status: varchar('status', {
|
||||||
|
enum: [
|
||||||
|
'Available',
|
||||||
|
'Preparing',
|
||||||
|
'Charging',
|
||||||
|
'SuspendedEVSE',
|
||||||
|
'SuspendedEV',
|
||||||
|
'Finishing',
|
||||||
|
'Reserved',
|
||||||
|
'Unavailable',
|
||||||
|
'Faulted',
|
||||||
|
],
|
||||||
|
}).notNull(),
|
||||||
|
/**
|
||||||
|
* 错误代码
|
||||||
|
* OCPP 1.6-J 7.6 ChargePointErrorCode
|
||||||
|
*/
|
||||||
|
errorCode: varchar('error_code', {
|
||||||
|
enum: [
|
||||||
|
'NoError',
|
||||||
|
'ConnectorLockFailure',
|
||||||
|
'EVCommunicationError',
|
||||||
|
'GroundFailure',
|
||||||
|
'HighTemperature',
|
||||||
|
'InternalError',
|
||||||
|
'LocalListConflict',
|
||||||
|
'OtherError',
|
||||||
|
'OverCurrentFailure',
|
||||||
|
'OverVoltage',
|
||||||
|
'PowerMeterFailure',
|
||||||
|
'PowerSwitchFailure',
|
||||||
|
'ReaderFailure',
|
||||||
|
'ResetFailure',
|
||||||
|
'UnderVoltage',
|
||||||
|
'WeakSignal',
|
||||||
|
],
|
||||||
|
}).notNull(),
|
||||||
|
/**
|
||||||
|
* 附加信息
|
||||||
|
* OCPP 1.6-J 6.47: "Additional free format information related to the error."
|
||||||
|
*/
|
||||||
|
info: varchar('info', { length: 50 }),
|
||||||
|
/**
|
||||||
|
* 供应商标识
|
||||||
|
* OCPP 1.6-J 6.47: "This identifies the vendor-specific implementation."
|
||||||
|
*/
|
||||||
|
vendorId: varchar('vendor_id', { length: 255 }),
|
||||||
|
/**
|
||||||
|
* 供应商错误代码
|
||||||
|
* OCPP 1.6-J 6.47: "This contains the vendor-specific error code."
|
||||||
|
*/
|
||||||
|
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
|
||||||
|
/**
|
||||||
|
* 状态报告时间戳
|
||||||
|
* OCPP 1.6-J 6.47: "The time for which the status is reported.
|
||||||
|
* If absent time of receipt of the message will be assumed."
|
||||||
|
*/
|
||||||
|
statusTimestamp: timestamp('status_timestamp'),
|
||||||
|
/**
|
||||||
|
* 消息接收时间
|
||||||
|
*/
|
||||||
|
receivedAt: timestamp('received_at').notNull().defaultNow(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
connectorIdIdx: index('idx_status_history_connector_id').on(
|
||||||
|
table.connectorId
|
||||||
|
),
|
||||||
|
statusTimestampIdx: index('idx_status_history_timestamp').on(
|
||||||
|
table.statusTimestamp
|
||||||
|
),
|
||||||
|
receivedAtIdx: index('idx_status_history_received_at').on(
|
||||||
|
table.receivedAt
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './auth-schema.ts'
|
export * from './auth-schema.ts'
|
||||||
|
export * from './ocpp-schema.ts'
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { cors } from 'hono/cors'
|
|||||||
import { logger } from 'hono/logger'
|
import { logger } from 'hono/logger'
|
||||||
import { showRoutes } from 'hono/dev'
|
import { showRoutes } from 'hono/dev'
|
||||||
import { auth } from './lib/auth.ts'
|
import { auth } from './lib/auth.ts'
|
||||||
|
import {
|
||||||
|
isSupportedOCPP,
|
||||||
|
SUPPORTED_OCPP_VERSIONS,
|
||||||
|
type SupportedOCPPVersion,
|
||||||
|
} from './constants.ts'
|
||||||
|
|
||||||
const app = new Hono<{
|
const app = new Hono<{
|
||||||
Variables: {
|
Variables: {
|
||||||
@@ -45,7 +50,7 @@ app.use(
|
|||||||
|
|
||||||
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))
|
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))
|
||||||
|
|
||||||
app.get('/', (c) => {
|
app.get('/api', (c) => {
|
||||||
const user = c.get('user')
|
const user = c.get('user')
|
||||||
const session = c.get('session')
|
const session = c.get('session')
|
||||||
|
|
||||||
@@ -66,13 +71,21 @@ app.get('/', (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/ocpp',
|
'/ocpp/:chargePointId',
|
||||||
upgradeWebSocket((c) => {
|
upgradeWebSocket((c) => {
|
||||||
|
const chargePointId = c.req.param('chargePointId')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onOpen(evt, ws) {
|
onOpen(evt, ws) {
|
||||||
|
const subProtocol = ws.protocol || 'unknown'
|
||||||
|
if (!isSupportedOCPP(subProtocol)) {
|
||||||
|
ws.close(1002, 'Unsupported subprotocol')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const connInfo = getConnInfo(c)
|
const connInfo = getConnInfo(c)
|
||||||
console.log(
|
console.log(
|
||||||
`New connection from ${connInfo.remote.address}:${connInfo.remote.port}`,
|
`New connection from ${connInfo.remote.address}:${connInfo.remote.port} for station ${chargePointId}`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onMessage(evt, ws) {
|
onMessage(evt, ws) {
|
||||||
|
|||||||
Reference in New Issue
Block a user