feat(M06-C): 完成设备协议适配与消息幂等
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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.');
|
||||
Reference in New Issue
Block a user