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.deepEqual(control.playTts({ id: '1234567890123', content: '欢迎光临', volume: 7, firstPlay: 1, loop: 2, speaker: 3, style: 2, speed: 6, intonation: 4 }), { action: 'PlayTTS', content: '欢迎光临', vol: 7, firstPlay: 1, loop: 2, speaker: 3, style: 2, speed: 6, intona: 4, id: '1234567890123' }); 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', time: 60, id: '123' }); assert.deepEqual(lock.control({ id: '123', subtype: '14', subID: 'SUB001', order: 'setcard', value: 'a1b2c3d4' }), { action: 'CtrlDevice', subtype: '14', subID: 'SUB001', order: 'setcard', value: 'A1B2C3D4', id: '123' }); assert.throws( () => lock.control({ id: '123', subtype: '14', subID: 'SUB001', order: 'setkey', value: '12345' }), /6-digit passwords/ ); assert.throws( () => lock.control({ id: '123', subtype: '15', subID: 'SUB001', order: 'setcard', value: 'not-card' }), /8-digit hex card IDs/ ); const socket = new JilianSmartSocketAdapter(); assert.deepEqual(socket.read('workInfo'), { read: 'workInfo', slotNum: 1 }); assert.deepEqual(socket.read({ target: 'localtask', slotNum: 2, taskNum: 7 }), { read: 'localtask', slotNum: 2, taskNum: 7 }); assert.deepEqual(socket.read({ target: 'localtask', taskNum: 0 }), { read: 'localtask', slotNum: 1, taskNum: 0 }); assert.deepEqual(socket.read({ target: 'mqttConfig' }), { read: 'mqttConfig' }); assert.deepEqual(socket.switch({ id: '123', on: true }), { action: 'on', id: '123', slotNum: 1 }); assert.deepEqual(socket.localTask({ id: '123', taskNum: 20, action: 'off', mode: 'weekly', time: '23:00', weekdays: [1, 5] }), { action: 'localtask', slotNum: 1, taskNum: 20, tasktype: 'off', cyctype: 'weekcyc', weekdata: '1000100', actiontime: '2300', id: '123' }); assert.throws(() => socket.localTask({ id: '123', taskNum: 0, action: 'on', mode: 'once', time: '12:00' }), /greater than or equal to 1/); assert.throws(() => socket.localTask({ id: '123', taskNum: 1, action: 'on', mode: 'weekly', time: '24:00', weekdays: [1] }), /Invalid/); assert.throws(() => socket.localTask({ id: '123', taskNum: 1, action: 'on', mode: 'weekly', time: '12:00', weekdays: [1, 1] }), /weekdays must be unique/); assert.deepEqual(socket.clearTask({ id: '123', taskNum: 0 }), { action: 'clearTask', id: '123', slotNum: 1, taskNum: 0 }); assert.deepEqual(socket.parameter({ id: '123', resetHold: 1, keyLock: 0, keyOff: 1 }), { setting: 'parameter', resetHold: 1, keyLock: 0, KeyOff: 1, id: '123' }); assert.equal(socket.reboot('123').action, 'reboot'); assert.equal(socket.emptyPower('123').action, 'emptyPower'); assert.deepEqual(socket.returnFactory({ id: '123', password: 'secret' }), { action: 'returnFactory', password: 'secret', id: '123' }); assert.deepEqual(socket.setReturnKey({ id: '123', old: 'old-key', new: 'new-key' }), { action: 'setReturnKey', old: 'old-key', new: 'new-key', id: '123' }); const protection = socket.protectOff({ id: '123', ratedAmps: 16, maxPower: 3500, maxCurrent: 15, maxTemperature: 80, pullOutStop: 1, pullOutPower: 5, pullOutSec: 10, chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30 }); assert.equal(protection.setting, 'protectOff'); assert.equal('ratedAmps' in protection, false); for (const ratedAmps of [10, 63]) { const ratedProtection = socket.protectOff({ id: '123', ratedAmps, maxPower: ratedAmps * 250, maxCurrent: ratedAmps, maxTemperature: 100, pullOutStop: 0, pullOutPower: 0, pullOutSec: 1, chargeFullStop: 0, chargeFullPower: 0, chargeFullSec: 1 }); assert.equal(ratedProtection.maxCurrent, ratedAmps); } assert.throws(() => socket.protectOff({ id: '123', ratedAmps: 10, maxPower: 2600, maxCurrent: 11, maxTemperature: 80, pullOutStop: 1, pullOutPower: 5, pullOutSec: 10, chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30 }), /10A capability/); 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'); assert.equal(control.parseUplink({ DeviceID: 'BOX_001', id: '124', result: 'paraerror', action: 'PlayTTS' }).result, 'paraerror'); assert.equal(control.parseUplink({ DeviceID: 'BOX_001', id: '125', result: 'vendor-new-code', action: 'task' }).result, 'UNKNOWN_VENDOR_RESULT'); const configVariant = control.parseUplink({ DeviceID: 'BOX_001', setting: 'mqttconfig', welvoice: 'hello' }); assert.equal(configVariant.eventType, 'mqttConfig'); assert.equal(configVariant.payload.welcomevoice, 'hello'); 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); for (const eventType of ['magstate', 'taskfinish', 'Poweron', 'connected']) { const fixture = control.parseUplink({ DeviceID: 'BOX_001', event: eventType, state: 'open', timestamp: 1782120000 }); assert.equal(fixture.kind, 'EVENT'); assert.equal(fixture.eventType, eventType); } const workInfo = socket.parseUplink({ DeviceID: 'SOCKET_001', read: 'workInfo', onOff: 'on', vol: '0220.5000', cur: '01.250', pow: '00275.000', temp: '45.50', energy: '0012.3400' }); assert.deepEqual({ switchOn: workInfo.payload.switchOn, voltage: workInfo.payload.voltage, current: workInfo.payload.current, power: workInfo.payload.power, temperature: workInfo.payload.temperature, energy: workInfo.payload.energy }, { switchOn: true, voltage: '220.5', current: '1.25', power: '275', temperature: '45.5', energy: '12.34' }); const temperatureVariant = socket.parseUplink({ DeviceID: 'SOCKET_001', read: 'workInfo', temperature: '46.0', KeyOff: 1 }); assert.equal(temperatureVariant.payload.temperature, '46'); assert.equal(temperatureVariant.payload.keyOff, 1); const conflictingTemperature = socket.parseUplink({ DeviceID: 'SOCKET_001', read: 'workInfo', temp: '45', temperature: '46' }); assert.deepEqual(conflictingTemperature.protocolIssues, ['SOCKET_TEMPERATURE_CONFLICT']); for (const [action, result] of [ ['off', 'update'], ['off', 'paraerror'], ['localtask', 'error'] ]) { const response = socket.parseUplink({ DeviceID: 'SOCKET_001', id: `20${result.length}`, action, result }); assert.equal(response.kind, 'ACK'); assert.equal(response.result, result); } assert.equal(generateCommandId(1782120000000, 7), '1782120000007'); assert.match(generateCommandId(), /^\d{13}$/); const calls = []; const eventHashes = new Set(); const service = new IotMessageService({ async execute(sql, params) { calls.push({ sql, params }); if (sql.includes('FROM qipai_devices')) { if (params[0] === 'SOCKET_001') { return [[{ id: 52, tenantId: 7, storeId: 11, roomId: 31, deviceId: 'SOCKET_001', deviceType: 'SMART_SOCKET' }], []]; } return [[{ id: 51, tenantId: 7, storeId: 11, roomId: 31, deviceId: 'BOX_001', deviceType: 'CONTROL_BOX' }], []]; } if (sql.includes('INSERT INTO qipai_iot_device_events')) { const hash = params[7]; const duplicate = eventHashes.has(hash); eventHashes.add(hash); return [{ affectedRows: duplicate ? 2 : 1 }, []]; } 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 ); const recordPayload = Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', event: 'record', type: 'card', state: 'open', content: 'CARD-PRIVATE-1234', battery: 9 })); await service.handle('/devicesend/BOX_001', recordPayload); const recordInsert = calls.findLast((item) => item.sql.includes('INSERT INTO qipai_iot_device_events') ); assert.equal(recordInsert.params.some((value) => typeof value === 'string' && value.includes('CARD-PRIVATE-1234') ), false); assert.equal(calls.some((item) => item.sql.includes('INSERT INTO qipai_device_alerts') && item.params.includes('LOW_BATTERY') ), true); await service.handle('/devicesend/BOX_001', Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', id: '124', action: 'task', result: 'unconfirm' }))); assert.equal(calls.some((item) => item.sql.includes('INSERT INTO qipai_device_alerts') && item.params.includes('DEVICE_UNCONFIRM') ), true); for (const [id, result, failureCode] of [ ['125', 'busy', 'DEVICE_BUSY'], ['126', 'fail', 'DEVICE_FAIL'] ]) { await service.handle('/devicesend/BOX_001', Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', id, action: 'task', result }))); assert.equal(calls.some((item) => item.sql.includes('UPDATE qipai_iot_commands') && item.params[0] === 'FAILED' && item.params[2] === failureCode ), true); } const commandUpdatesBeforeEvents = calls.filter((item) => item.sql.includes('UPDATE qipai_iot_commands') ).length; for (const eventType of ['magstate', 'taskfinish']) { await service.handle('/devicesend/BOX_001', Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', event: eventType, state: 'close' }))); } assert.equal(calls.filter((item) => item.sql.includes('UPDATE qipai_iot_commands') ).length, commandUpdatesBeforeEvents); const deadLettersBeforeWill = calls.filter((item) => item.sql.includes('qipai_iot_dead_letters') ).length; await service.handle('/devicewill/BOX_001', Buffer.from('close')); assert.equal(calls.some((item) => item.sql.includes('UPDATE qipai_devices') && item.params[0] === 'OFFLINE' ), true); assert.equal(calls.filter((item) => item.sql.includes('qipai_iot_dead_letters') ).length, deadLettersBeforeWill); await service.handle('/devicesend/BOX_001', Buffer.from(JSON.stringify({ DeviceID: 'BOX_001', id: '127', action: 'task', result: 'vendor-new-code' }))); assert.equal(calls.some((item) => item.sql.includes('qipai_iot_dead_letters') && item.params.includes('UNKNOWN_VENDOR_RESULT') ), true); assert.equal(calls.some((item) => item.sql.includes('UPDATE qipai_iot_commands') && item.params[0] === 'FAILED' && item.params[2] === 'UNKNOWN_VENDOR_RESULT' ), true); const socketAlertsBeforeFakeFields = calls.filter((item) => item.sql.includes('INSERT INTO qipai_device_alerts') ).length; await service.handle('/devicesend/SOCKET_001', Buffer.from(JSON.stringify({ DeviceID: 'SOCKET_001', event: 'workInfo', switch: 'on', powerW: 3601, temperature: 82, overLoad: true }))); assert.equal(calls.filter((item) => item.sql.includes('INSERT INTO qipai_device_alerts') ).length, socketAlertsBeforeFakeFields, 'legacy fake fields must not generate alerts'); await service.handle('/devicesend/SOCKET_001', Buffer.from(JSON.stringify({ DeviceID: 'SOCKET_001', read: 'workInfo', onOff: 'on', vol: '220.50', cur: '1.250', pow: '275.000', temp: '45.5', energy: '12.340' }))); const workInfoInsert = calls.findLast((item) => item.sql.includes('INSERT INTO qipai_iot_device_events') ); const persistedWorkInfo = JSON.parse(workInfoInsert.params[9]).payload; assert.deepEqual({ switchOn: persistedWorkInfo.switchOn, voltage: persistedWorkInfo.voltage, current: persistedWorkInfo.current, power: persistedWorkInfo.power, temperature: persistedWorkInfo.temperature, energy: persistedWorkInfo.energy }, { switchOn: true, voltage: '220.5', current: '1.25', power: '275', temperature: '45.5', energy: '12.34' }); const protectionAlerts = new Map([ [3, 'SOCKET_PULL_OUT_STOP'], [4, 'SOCKET_CHARGE_FULL_STOP'], [5, 'SOCKET_OVERPOWER'], [6, 'SOCKET_OVERCURRENT'], [7, 'SOCKET_OVERTEMPERATURE'] ]); for (let closeReason = 1; closeReason <= 7; closeReason += 1) { const alertCountBefore = calls.filter((item) => item.sql.includes('INSERT INTO qipai_device_alerts') ).length; await service.handle('/devicesend/SOCKET_001', Buffer.from(JSON.stringify({ DeviceID: 'SOCKET_001', event: 'special', slotNum: 1, state: 'off', closeReason, energy: `${closeReason}.0` }))); const alertType = protectionAlerts.get(closeReason); if (alertType) { assert.equal(calls.some((item) => item.sql.includes('INSERT INTO qipai_device_alerts') && item.params.includes(alertType) ), true); } else { assert.equal(calls.filter((item) => item.sql.includes('INSERT INTO qipai_device_alerts') ).length, alertCountBefore, `closeReason ${closeReason} is only a local operation`); } } await service.handle('/devicesend/SOCKET_001', Buffer.from(JSON.stringify({ DeviceID: 'SOCKET_001', read: 'workInfo', temp: '45', temperature: '46' }))); assert.equal(calls.some((item) => item.sql.includes('qipai_iot_dead_letters') && item.params.includes('SOCKET_TEMPERATURE_CONFLICT') ), true); await assert.rejects( () => service.createCommand({ tenantId: '7', assetId: '51', storeId: '11', commandId: '12345678901234', commandType: 'ConctolPower', payload: {}, traceId: 'trace' }), /IOT_COMMAND_ID_INVALID/ ); await service.createCommand({ tenantId: '7', assetId: '51', storeId: '11', commandId: '128', commandType: 'CtrlDevice', traceId: 'secret-test', payload: { action: 'CtrlDevice', subtype: '14', subID: 'SUB001', order: 'setkey', value: '123456' } }); const secretCommandInsert = calls.findLast((item) => item.sql.includes('INSERT INTO qipai_iot_commands') ); assert.equal(secretCommandInsert.params[7].includes('123456'), false); assert.equal(secretCommandInsert.params[7].includes('valueHash'), true); await service.createCommand({ tenantId: '7', assetId: '52', storeId: '11', commandId: '129', commandType: 'setReturnKey', traceId: 'socket-secret-test', payload: { action: 'setReturnKey', id: '129', old: 'old-secret', new: 'new-secret' } }); const socketSecretInsert = calls.findLast((item) => item.sql.includes('INSERT INTO qipai_iot_commands') ); assert.equal(socketSecretInsert.params[7].includes('old-secret'), false); assert.equal(socketSecretInsert.params[7].includes('new-secret'), false); assert.equal(socketSecretInsert.params[7].includes('oldHash'), true); assert.equal(socketSecretInsert.params[7].includes('newHash'), true); 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/E-R1 exact wire, normalized events, ACKs and QoS 1 dedup work.');