fix(M06-E-R1): 校正智慧插座协议

This commit is contained in:
Codex
2026-08-10 19:10:25 +08:00
parent 3ff68d103b
commit aed36f0f82
7 changed files with 876 additions and 113 deletions
+160 -15
View File
@@ -73,17 +73,69 @@ assert.throws(
);
const socket = new JilianSmartSocketAdapter();
assert.deepEqual(socket.read('workInfo'), { read: 'workInfo' });
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.equal(socket.localTask({
assert.deepEqual(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
}), {
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'
@@ -116,6 +168,39 @@ for (const eventType of ['magstate', 'taskfinish', 'Poweron', 'connected']) {
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}$/);
@@ -240,21 +325,68 @@ assert.equal(calls.some((item) =>
&& 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.some((item) =>
assert.equal(calls.filter((item) =>
item.sql.includes('INSERT INTO qipai_device_alerts')
&& item.params.includes('SOCKET_OVERLOAD')
), true);
).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('INSERT INTO qipai_device_alerts')
&& item.params.includes('SOCKET_POWER_LIMIT')
), true);
assert.equal(calls.some((item) =>
item.sql.includes('INSERT INTO qipai_device_alerts')
&& item.params.includes('SOCKET_TEMPERATURE_LIMIT')
item.sql.includes('qipai_iot_dead_letters')
&& item.params.includes('SOCKET_TEMPERATURE_CONFLICT')
), true);
await assert.rejects(
@@ -280,6 +412,19 @@ const secretCommandInsert = calls.findLast((item) =>
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]); },
@@ -304,4 +449,4 @@ 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.');
console.log('PASS: M06-C/E-R1 exact wire, normalized events, ACKs and QoS 1 dedup work.');