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 './ocpp-schema.ts'
|
||||
|
||||
@@ -7,6 +7,11 @@ import { cors } from 'hono/cors'
|
||||
import { logger } from 'hono/logger'
|
||||
import { showRoutes } from 'hono/dev'
|
||||
import { auth } from './lib/auth.ts'
|
||||
import {
|
||||
isSupportedOCPP,
|
||||
SUPPORTED_OCPP_VERSIONS,
|
||||
type SupportedOCPPVersion,
|
||||
} from './constants.ts'
|
||||
|
||||
const app = new Hono<{
|
||||
Variables: {
|
||||
@@ -45,7 +50,7 @@ app.use(
|
||||
|
||||
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 session = c.get('session')
|
||||
|
||||
@@ -66,13 +71,21 @@ app.get('/', (c) => {
|
||||
})
|
||||
|
||||
app.get(
|
||||
'/ocpp',
|
||||
'/ocpp/:chargePointId',
|
||||
upgradeWebSocket((c) => {
|
||||
const chargePointId = c.req.param('chargePointId')
|
||||
|
||||
return {
|
||||
onOpen(evt, ws) {
|
||||
const subProtocol = ws.protocol || 'unknown'
|
||||
if (!isSupportedOCPP(subProtocol)) {
|
||||
ws.close(1002, 'Unsupported subprotocol')
|
||||
return
|
||||
}
|
||||
|
||||
const connInfo = getConnInfo(c)
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user