From 0f47b3d3823255aa4951008bb3a2b7d65ad1c5f8 Mon Sep 17 00:00:00 2001 From: Timothy Yin Date: Tue, 10 Mar 2026 10:14:39 +0800 Subject: [PATCH] feat(firmware): add OCPP schema and constants for charge point management --- apps/csms/src/constants.ts | 8 + apps/csms/src/db/ocpp-schema.ts | 254 ++++++++++++++++++++++++++++++++ apps/csms/src/db/schema.ts | 1 + apps/csms/src/index.ts | 19 ++- 4 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 apps/csms/src/constants.ts create mode 100644 apps/csms/src/db/ocpp-schema.ts diff --git a/apps/csms/src/constants.ts b/apps/csms/src/constants.ts new file mode 100644 index 0000000..c0bfb0d --- /dev/null +++ b/apps/csms/src/constants.ts @@ -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) +} diff --git a/apps/csms/src/db/ocpp-schema.ts b/apps/csms/src/db/ocpp-schema.ts new file mode 100644 index 0000000..b6c70eb --- /dev/null +++ b/apps/csms/src/db/ocpp-schema.ts @@ -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 + ), + }) +) diff --git a/apps/csms/src/db/schema.ts b/apps/csms/src/db/schema.ts index c3c6835..4d88575 100644 --- a/apps/csms/src/db/schema.ts +++ b/apps/csms/src/db/schema.ts @@ -1 +1,2 @@ export * from './auth-schema.ts' +export * from './ocpp-schema.ts' diff --git a/apps/csms/src/index.ts b/apps/csms/src/index.ts index bf87462..a6f7c17 100644 --- a/apps/csms/src/index.ts +++ b/apps/csms/src/index.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) {