import { z } from 'zod'; const commandId = z.string().regex(/^\d{1,13}$/); const resultCode = z.enum([ 'ok', 'fail', 'busy', 'unconfirm', 'timeout', 'full', 'unknown' ]); const vendorMessage = z.object({ id: commandId.optional(), DeviceID: z.string().min(1).max(64).optional(), deviceID: z.string().min(1).max(64).optional(), IMEI: z.string().max(64).optional(), result: resultCode.optional(), event: z.string().max(64).optional(), action: z.string().max(64).optional(), read: z.string().max(64).optional(), timestamp: z.union([z.string(), z.number()]).optional() }).passthrough(); export type NormalizedVendorMessage = { kind: 'ACK' | 'EVENT' | 'SNAPSHOT'; commandId: string | null; eventType: string; result: z.infer | null; deviceId: string | null; eventAt: Date | null; payload: Record; }; export interface ProtocolAdapter { parseUplink(payload: unknown): NormalizedVendorMessage; } export class JilianControlBoxAdapter implements ProtocolAdapter { read(target: 'basicInfo' | 'mqttConfig' | 'startVoice' | 'task' | 'taskconfig') { return z.object({ read: z.literal(target) }).parse({ read: target }); } controlPower(input: { id: string; slot1?: 'on' | 'off'; slot2?: 'on' | 'off'; slot3?: 'on' | 'off'; slotall?: 'on' | 'off'; }) { return z.object({ action: z.literal('ConctolPower'), id: commandId, slot1: z.enum(['on', 'off']).optional(), slot2: z.enum(['on', 'off']).optional(), slot3: z.enum(['on', 'off']).optional(), slotall: z.enum(['on', 'off']).optional() }).refine((value) => value.slot1 || value.slot2 || value.slot3 || value.slotall) .parse({ action: 'ConctolPower', ...input }); } controlDoor(input: { id: string; order: 'open' | 'close'; holdopen?: 0 | 1; delayTime?: number; }) { return z.object({ action: z.literal('Crldoor'), id: commandId, order: z.enum(['open', 'close']), holdopen: z.union([z.literal(0), z.literal(1)]).default(0), delayTime: z.number().int().min(1).max(14).default(4) }).parse({ action: 'Crldoor', ...input }); } playTts(input: { id: string; content: string; volume?: number; playCount?: number; priority?: number; speaker?: number; style?: number; speed?: number; pitch?: number; }) { return z.object({ action: z.literal('PlayTTS'), id: commandId, content: z.string().trim().min(1).max(500), volume: z.number().int().min(0).max(100).default(80), playCount: z.number().int().min(1).max(10).default(1), priority: z.number().int().min(0).max(10).default(0), speaker: z.number().int().min(0).max(20).default(0), style: z.number().int().min(0).max(20).default(0), speed: z.number().int().min(-500).max(500).default(0), pitch: z.number().int().min(-500).max(500).default(0) }).parse({ action: 'PlayTTS', ...input }); } stopTts(id: string) { return z.object({ action: z.literal('stopTTS'), id: commandId }) .parse({ action: 'stopTTS', id }); } controlLed(input: { id: string; minute: number }) { return z.object({ action: z.literal('CrlLED'), id: commandId, minute: z.number().int().min(0).max(10080) }).parse({ action: 'CrlLED', ...input }); } startTask(input: { id: string; minute: number; type: 1 | 2 | 3; subID?: string; holdopen?: 0 | 1; delayTime?: number; }) { return z.object({ action: z.literal('task'), id: commandId, minute: z.number().int().min(1).max(10080), type: z.union([z.literal(1), z.literal(2), z.literal(3)]), subID: z.string().min(1).max(64).optional(), holdopen: z.union([z.literal(0), z.literal(1)]).default(0), delayTime: z.number().int().min(1).max(14).default(4) }).parse({ action: 'task', ...input }); } extendTask(input: { id: string; addminute: number }) { return z.object({ action: z.literal('addtask'), id: commandId, addminute: z.number().int().min(1).max(10080) }).parse({ action: 'addtask', ...input }); } cancelTask(id: string) { return z.object({ action: z.literal('canceltask'), id: commandId }) .parse({ action: 'canceltask', id }); } parseUplink(payload: unknown) { return normalizeVendorMessage(payload); } } export class JilianSub1GLockAdapter implements ProtocolAdapter { pair(input: { id: string; timeout?: number }) { return z.object({ action: z.literal('AddDevice'), id: commandId, timeout: z.number().int().min(10).max(300).default(60) }).parse({ action: 'AddDevice', ...input }); } control(input: { id: string; subID: string; order: 'open' | 'close' | 'setkey' | 'delkey' | 'setcard' | 'delcard' | 'factoryreset'; holdopen?: 0 | 1; delayTime?: number; content?: string; }) { return z.object({ action: z.literal('CtrlDevice'), id: commandId, subID: z.string().min(1).max(64), order: z.enum([ 'open', 'close', 'setkey', 'delkey', 'setcard', 'delcard', 'factoryreset' ]), holdopen: z.union([z.literal(0), z.literal(1)]).optional(), delayTime: z.number().int().min(1).max(14).optional(), content: z.string().min(1).max(128).optional() }).superRefine((value, context) => { if (['setkey', 'setcard'].includes(value.order) && !value.content) { context.addIssue({ code: z.ZodIssueCode.custom, message: 'content is required' }); } }).parse({ action: 'CtrlDevice', ...input }); } parseUplink(payload: unknown) { return normalizeVendorMessage(payload); } } export class JilianSmartSocketAdapter implements ProtocolAdapter { read(target: 'basicInfo' | 'workInfo') { return z.object({ read: z.literal(target) }).parse({ read: target }); } switch(input: { id: string; on: boolean; slotNum?: number }) { return z.object({ action: z.enum(['on', 'off']), id: commandId, slotNum: z.number().int().min(1).max(20).default(1) }).parse({ action: input.on ? 'on' : 'off', id: input.id, slotNum: input.slotNum }); } localTask(input: { id: string; taskNum: number; action: 'on' | 'off'; mode: 'once' | 'daily' | 'weekly'; time: string; weekdays?: number[]; }) { return z.object({ action: z.literal('localtask'), id: commandId, taskNum: z.number().int().min(1).max(20), switch: z.enum(['on', 'off']), mode: z.enum(['once', 'daily', 'weekly']), time: z.string().regex(/^\d{2}:\d{2}$/), weekdays: z.array(z.number().int().min(1).max(7)).max(7).optional() }).parse({ action: 'localtask', id: input.id, taskNum: input.taskNum, switch: input.action, mode: input.mode, time: input.time, weekdays: input.weekdays }); } clearTask(input: { id: string; taskNum: number }) { return z.object({ action: z.literal('clearTask'), id: commandId, taskNum: z.number().int().min(0).max(20) }).parse({ action: 'clearTask', ...input }); } parseUplink(payload: unknown) { return normalizeVendorMessage(payload); } } function normalizeVendorMessage(payload: unknown): NormalizedVendorMessage { const parsed = vendorMessage.parse(payload); const record = parsed as Record; const eventName = parsed.event ?? parsed.action ?? parsed.read ?? 'snapshot'; return { kind: parsed.result ? 'ACK' : parsed.event ? 'EVENT' : 'SNAPSHOT', commandId: parsed.id ?? null, eventType: eventName, result: parsed.result ?? null, deviceId: parsed.DeviceID ?? parsed.deviceID ?? null, eventAt: parseEventAt(parsed.timestamp), payload: record }; } function parseEventAt(value: string | number | undefined): Date | null { if (value === undefined) return null; const date = typeof value === 'number' ? new Date(value < 10_000_000_000 ? value * 1000 : value) : new Date(value); return Number.isNaN(date.getTime()) ? null : date; }