import assert from 'node:assert/strict'; import { JilianControlBoxAdapter, JilianSmartSocketAdapter, JilianSub1GLockAdapter } from '../dist/devices/jilian-adapters.js'; import { generateCommandId, IotMessageService } from '../dist/devices/iot-message-service.js'; import { DeviceCommandService } from '../dist/devices/device-command-service.js'; const control = new JilianControlBoxAdapter(); assert.deepEqual(control.read('basicInfo'), { read: 'basicInfo' }); assert.deepEqual(control.controlPower({ id: '1234567890123', slot1: 'on', slot3: 'off' }), { action: 'ConctolPower', id: '1234567890123', slot1: 'on', slot3: 'off' }); assert.equal(control.controlDoor({ id: '1234567890123', order: 'open' }).action, 'Crldoor'); assert.equal(control.playTts({ id: '1234567890123', content: '欢迎光临' }).action, 'PlayTTS'); assert.deepEqual(control.stopTts('1234567890123'), { action: 'stopTTS', id: '1234567890123' }); assert.equal(control.controlLed({ id: '1234567890123', minute: 30 }).action, 'CrlLED'); assert.equal(control.startTask({ id: '1234567890123', minute: 120, type: 2, subID: 'SUB001' }).action, 'task'); assert.equal(control.extendTask({ id: '1234567890123', addminute: 30 }).action, 'addtask'); assert.equal(control.cancelTask('1234567890123').action, 'canceltask'); assert.throws( () => control.controlDoor({ id: '12345678901234', order: 'open' }), /Invalid/ ); assert.throws( () => control.controlDoor({ id: '123', order: 'open', delayTime: 15 }), /less than or equal to 14/ ); const lock = new JilianSub1GLockAdapter(); assert.deepEqual(lock.pair({ id: '123', timeout: 60 }), { action: 'AddDevice', id: '123', timeout: 60 }); assert.equal(lock.control({ id: '123', subID: 'SUB001', order: 'open', delayTime: 4 }).action, 'CtrlDevice'); assert.throws( () => lock.control({ id: '123', subID: 'SUB001', order: 'setkey' }), /content is required/ ); const socket = new JilianSmartSocketAdapter(); assert.deepEqual(socket.read('workInfo'), { read: 'workInfo' }); assert.deepEqual(socket.switch({ id: '123', on: true }), { action: 'on', id: '123', slotNum: 1 }); assert.equal(socket.localTask({ id: '123', taskNum: 20, action: 'off', mode: 'weekly', time: '23:00', weekdays: [1, 5] }).action, 'localtask'); assert.deepEqual(socket.clearTask({ id: '123', taskNum: 0 }), { action: 'clearTask', id: '123', taskNum: 0 }); const ack = control.parseUplink({ DeviceID: 'BOX_001', id: '123', result: 'unconfirm', action: 'task' }); assert.equal(ack.kind, 'ACK'); assert.equal(ack.result, 'unconfirm'); assert.equal(ack.eventType, 'task'); const event = lock.parseUplink({ deviceID: 'BOX_001', event: 'record', type: 'card', state: 'open', timestamp: 1782120000 }); assert.equal(event.kind, 'EVENT'); assert.equal(event.eventType, 'record'); assert.equal(event.eventAt?.getUTCFullYear(), 2026); assert.equal(generateCommandId(1782120000000, 7), '1782120000007'); assert.match(generateCommandId(), /^\d{13}$/); const calls = []; let eventInsertCount = 0; const service = new IotMessageService({ async execute(sql, params) { calls.push({ sql, params }); if (sql.includes('FROM qipai_devices')) { return [[{ id: 51, tenantId: 7, storeId: 11, roomId: 31, deviceId: 'BOX_001', deviceType: 'CONTROL_BOX' }], []]; } if (sql.includes('INSERT INTO qipai_iot_device_events')) { eventInsertCount += 1; return [{ affectedRows: eventInsertCount === 1 ? 1 : 2 }, []]; } return [{ affectedRows: 1, insertId: 81 }, []]; } }); const payload = Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', id: '123', result: 'ok', action: 'ConctolPower', slot1: 'on' })); await service.handle('/devicesend/BOX_001', payload); await service.handle('/devicesend/BOX_001', payload); assert.equal( calls.filter((item) => item.sql.includes('UPDATE qipai_iot_commands')).length, 1, 'duplicate QoS 1 payload must not repeat command/device side effects' ); assert.equal( calls.some((item) => item.sql.includes('qipai_iot_dead_letters')), false ); await service.handle('/bad-topic/BOX_001', Buffer.from('{}')); assert.equal( calls.some((item) => item.sql.includes('qipai_iot_dead_letters')), true ); await assert.rejects( () => service.createCommand({ tenantId: '7', assetId: '51', storeId: '11', commandId: '12345678901234', commandType: 'ConctolPower', payload: {}, traceId: 'trace' }), /IOT_COMMAND_ID_INVALID/ ); const commandCalls = []; const commandService = new DeviceCommandService({ async createCommand(input) { commandCalls.push(['create', input]); }, async markPublished(tenantId, commandId) { commandCalls.push(['published', tenantId, commandId]); }, async markPublishFailed() { throw new Error('unexpected publish failure'); } }, { async publishDeviceCommand(deviceId, payloadText) { commandCalls.push(['mqtt', deviceId, JSON.parse(payloadText)]); } }); const issued = await commandService.issue({ tenantId: '7', assetId: '51', deviceId: 'BOX_001', storeId: '11', commandType: 'ConctolPower', payloadFactory: (id) => control.controlPower({ id, slot1: 'on' }), traceId: 'trace' }); assert.equal(issued.status, 'PUBLISHED'); assert.match(issued.commandId, /^\d{13}$/); assert.equal(commandCalls[0][0], 'create'); assert.equal(commandCalls[1][0], 'mqtt'); assert.equal(commandCalls[2][0], 'published'); console.log('PASS: M06-C adapters, vendor spelling, command IDs, ACKs and QoS 1 dedup work.');