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
@@ -1934,8 +1934,8 @@ async function assertDeviceTopology(pool, context) {
const socket = await repository.createAsset(actor, {
storeId, roomId, deviceId: 'M06B_SOCKET_001', imei: '860000000000002',
iccid: '89860000000000000002', deviceType: 'SMART_SOCKET',
model: 'JL-SOCKET', firmwareVersion: '1.0.0', signalStrength: 16,
capabilities: ['POWER', 'METERING']
model: 'JL-SOCKET-16A', firmwareVersion: '1.0.0', signalStrength: 16,
capabilities: ['POWER', 'METERING', 'RATED_16A']
});
const lock = await repository.createAsset(actor, {
storeId, roomId, deviceId: 'M06B_LOCK_001', imei: '',
@@ -2281,6 +2281,150 @@ async function assertIotMessages(pool, context) {
[context.tenantId, `/devicewill/${device.deviceId}`]
);
assert.equal(Number(willDeadRows[0].total), 0);
const [socketRows] = await pool.query(
`SELECT id, store_id AS storeId, room_id AS roomId, device_id AS deviceId
FROM qipai_devices
WHERE tenant_id = ? AND device_id = 'M06B_SOCKET_001'`,
[context.tenantId]
);
const socket = socketRows[0];
assert.ok(socket, 'M06-E-R1 requires the smart socket fixture.');
await service.handle(`/devicesend/${socket.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: socket.deviceId,
read: 'workInfo',
slotNum: 1,
onOff: 'on',
vol: '0220.5000',
cur: '01.250',
pow: '00275.000',
temp: '45.50',
energy: '0012.3400',
timestamp: 1782120020
})));
const [workInfoRows] = await pool.query(
`SELECT JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.voltage')) AS voltage,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.current')) AS currentValue,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.power')) AS powerValue,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.temperature')) AS temperature,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.energy')) AS energy,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.switchOn')) AS switchOn
FROM qipai_iot_device_events
WHERE tenant_id = ? AND device_id = ? AND event_type = 'workInfo'`,
[context.tenantId, socket.id]
);
assert.deepEqual(workInfoRows, [{
voltage: '220.5', currentValue: '1.25', powerValue: '275',
temperature: '45.5', energy: '12.34', switchOn: 'true'
}]);
const socketResultFixtures = [
['1782120000008', 'off', 'update', 'DEVICE_UPDATE'],
['1782120000009', 'off', 'paraerror', 'DEVICE_PARAERROR'],
['1782120000010', 'localtask', 'error', 'DEVICE_ERROR']
];
for (const [commandId, action, result, failureCode] of socketResultFixtures) {
await service.createCommand({
tenantId: context.tenantId,
assetId: String(socket.id),
storeId: String(socket.storeId),
roomId: String(socket.roomId),
commandId,
commandType: action,
payload: { action, id: commandId, slotNum: 1 },
traceId: `m06e-${result}`
});
await service.markPublished(context.tenantId, commandId);
await service.handle(`/devicesend/${socket.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: socket.deviceId, id: commandId, action, result
})));
const [resultRows] = await pool.query(
`SELECT status, failure_code AS failureCode
FROM qipai_iot_commands WHERE tenant_id = ? AND command_id = ?`,
[context.tenantId, commandId]
);
assert.deepEqual(resultRows, [{ status: 'FAILED', failureCode }]);
}
for (let closeReason = 1; closeReason <= 7; closeReason += 1) {
await service.handle(`/devicesend/${socket.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: socket.deviceId,
event: 'special',
slotNum: 1,
state: 'off',
closeReason,
energy: `${closeReason}.0`,
timestamp: 1782120030 + closeReason
})));
}
const [socketAlertRows] = await pool.query(
`SELECT alert_type AS alertType
FROM qipai_device_alerts
WHERE tenant_id = ? AND device_id = ?
AND alert_type IN ('SOCKET_PULL_OUT_STOP', 'SOCKET_CHARGE_FULL_STOP',
'SOCKET_OVERPOWER', 'SOCKET_OVERCURRENT', 'SOCKET_OVERTEMPERATURE')
ORDER BY alert_type`,
[context.tenantId, socket.id]
);
assert.deepEqual(socketAlertRows, [
{ alertType: 'SOCKET_CHARGE_FULL_STOP' },
{ alertType: 'SOCKET_OVERCURRENT' },
{ alertType: 'SOCKET_OVERPOWER' },
{ alertType: 'SOCKET_OVERTEMPERATURE' },
{ alertType: 'SOCKET_PULL_OUT_STOP' }
]);
await service.handle(`/devicesend/${socket.deviceId}`, Buffer.from(JSON.stringify({
DeviceID: socket.deviceId,
read: 'workInfo',
temp: '45',
temperature: '46',
timestamp: 1782120040
})));
const [conflictRows] = await pool.query(
`SELECT processing_status AS processingStatus,
JSON_UNQUOTE(JSON_EXTRACT(normalized_payload, '$.payload.temperature')) AS temperature
FROM qipai_iot_device_events
WHERE tenant_id = ? AND device_id = ? AND payload_hash = SHA2(?, 256)`,
[context.tenantId, socket.id, JSON.stringify({
DeviceID: socket.deviceId,
read: 'workInfo', temp: '45', temperature: '46', timestamp: 1782120040
})]
);
assert.deepEqual(conflictRows, [{ processingStatus: 'PROTOCOL_ERROR', temperature: '45' }]);
const [conflictDeadRows] = await pool.query(
`SELECT error_code AS errorCode FROM qipai_iot_dead_letters
WHERE tenant_id = ? AND device_id = ? AND error_code = 'SOCKET_TEMPERATURE_CONFLICT'`,
[context.tenantId, socket.id]
);
assert.deepEqual(conflictDeadRows, [{ errorCode: 'SOCKET_TEMPERATURE_CONFLICT' }]);
await service.createCommand({
tenantId: context.tenantId,
assetId: String(socket.id),
storeId: String(socket.storeId),
roomId: String(socket.roomId),
commandId: '1782120000011',
commandType: 'setReturnKey',
payload: {
action: 'setReturnKey', id: '1782120000011',
old: 'old-return-secret', new: 'new-return-secret'
},
traceId: 'm06e-return-key-redaction'
});
const [returnKeyRows] = await pool.query(
`SELECT JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.old')) AS oldValue,
JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.new')) AS newValue,
JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.oldHash')) AS oldHash,
JSON_UNQUOTE(JSON_EXTRACT(request_payload, '$.newHash')) AS newHash
FROM qipai_iot_commands
WHERE tenant_id = ? AND command_id = '1782120000011'`,
[context.tenantId]
);
assert.equal(returnKeyRows[0].oldValue, null);
assert.equal(returnKeyRows[0].newValue, null);
assert.match(returnKeyRows[0].oldHash, /^[a-f0-9]{64}$/);
assert.match(returnKeyRows[0].newHash, /^[a-f0-9]{64}$/);
}
async function assertCleaningTaskTransactions(pool, context) {
@@ -2913,8 +3057,12 @@ try {
'unknown vendor result event dead-letter alert and command failure',
'CtrlDevice credential request hashing and masking',
'magstate and taskfinish remain events rather than ACKs',
'raw devicewill close marks the device offline without invalid-payload dead-letter'
,
'raw devicewill close marks the device offline without invalid-payload dead-letter',
'smart socket workInfo exact-decimal normalization',
'smart socket update paraerror and error command failures',
'special closeReason 3 through 7 alerts without 1 through 2 false alarms',
'conflicting temp and temperature protocol dead-letter',
'smart socket return-key request hashing and masking',
'concurrent cleaning claim has one winner',
'cleaning state and event transaction rollback',
'WAITING CLAIMED and REJECTED reassignment cleanup',