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
+133 -8
View File
@@ -19,7 +19,8 @@ const service = new DeviceControlService({
if (sql.includes("device_type = 'SMART_SOCKET'")) {
return [[{
id: 52, deviceId: 'SOCKET_001', storeId: 11, roomId: 31,
deviceType: 'SMART_SOCKET', status: 'ONLINE'
deviceType: 'SMART_SOCKET', status: 'ONLINE', model: 'JL-SOCKET-16A',
capabilities: ['POWER', 'METERING', 'RATED_16A']
}], []];
}
return [[{
@@ -80,7 +81,15 @@ await service.controlSubLock(context, {
assert.equal(issued.at(-1).payload.value, '123456654321');
assert.equal('content' in issued.at(-1).payload, false);
await service.readSmartSocket(context, 'workInfo');
assert.deepEqual(issued.at(-1).payload, { read: 'workInfo' });
assert.deepEqual(issued.at(-1).payload, { read: 'workInfo', slotNum: 1 });
await service.readSmartSocket(context, {
target: 'localtask', slotNum: 2, taskNum: 0
});
assert.deepEqual(issued.at(-1).payload, {
read: 'localtask', slotNum: 2, taskNum: 0
});
await service.readSmartSocket(context, { target: 'mqttConfig' });
assert.deepEqual(issued.at(-1).payload, { read: 'mqttConfig' });
await service.switchSmartSocket(context, { on: false, slotNum: 2 });
assert.deepEqual(issued.at(-1).payload, {
action: 'off', id: '1782120000001', slotNum: 2
@@ -89,10 +98,38 @@ await service.scheduleSmartSocket(context, {
taskNum: 1, action: 'on', mode: 'weekly', time: '18:30', weekdays: [5, 6]
});
assert.equal(issued.at(-1).payload.action, 'localtask');
assert.equal(issued.at(-1).payload.switch, 'on');
assert.deepEqual(issued.at(-1).payload, {
action: 'localtask', slotNum: 1, taskNum: 1, tasktype: 'on',
cyctype: 'weekcyc', weekdata: '0000110', actiontime: '1830',
id: '1782120000001'
});
await service.clearSmartSocketTask(context, 1);
assert.equal(issued.at(-1).payload.action, 'clearTask');
assert.equal(audits.length, 15);
assert.deepEqual(issued.at(-1).payload, {
action: 'clearTask', slotNum: 1, taskNum: 1, id: '1782120000001'
});
await service.configureSmartSocketProtection(context, {
maxPower: 3500, maxCurrent: 15, maxTemperature: 80,
pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
});
assert.equal(issued.at(-1).payload.setting, 'protectOff');
assert.equal('ratedAmps' in issued.at(-1).payload, false);
await assert.rejects(
() => service.configureSmartSocketProtection(context, {
maxPower: 3500, maxCurrent: 17, maxTemperature: 80,
pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
}),
/16A capability/
);
await service.configureSmartSocketParameter(context, {
resetHold: 1, keyLock: 0, keyOff: 1
});
assert.deepEqual(issued.at(-1).payload, {
setting: 'parameter', resetHold: 1, keyLock: 0, KeyOff: 1,
id: '1782120000001'
});
assert.equal(audits.length, 20);
assert.match(audits[0], /DEVICE_COMMAND_REQUESTED/);
await assert.rejects(
@@ -128,6 +165,50 @@ await service.controlSubLock(platformContext, {
assert.equal(issued.at(-1).payload.value, 'all');
assert.equal(issued.at(-1).payload.subtype, '15');
await assert.rejects(
() => service.emptySmartSocketEnergy(context, 'CONFIRM_EMPTY_SOCKET_ENERGY'),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.emptySmartSocketEnergy(platformContext, 'CONFIRM_EMPTY_SOCKET_ENERGY');
assert.deepEqual(issued.at(-1).payload, {
action: 'emptyPower', id: '1782120000001'
});
await assert.rejects(
() => service.resetSmartSocket(platformContext, {
password: 'return-secret', dangerConfirmation: 'WRONG'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.resetSmartSocket(platformContext, {
password: 'return-secret', dangerConfirmation: 'CONFIRM_SOCKET_FACTORY_RESET'
});
assert.equal(issued.at(-1).payload.action, 'returnFactory');
assert.equal(issued.at(-1).payload.password, 'return-secret');
assert.equal('dangerConfirmation' in issued.at(-1).payload, false);
await service.changeSmartSocketReturnKey(platformContext, {
old: 'old-secret', new: 'new-secret',
dangerConfirmation: 'CONFIRM_CHANGE_SOCKET_RETURN_KEY'
});
assert.deepEqual(issued.at(-1).payload, {
action: 'setReturnKey', old: 'old-secret', new: 'new-secret',
id: '1782120000001'
});
await assert.rejects(
() => service.clearSmartSocketTask(context, {
taskNum: 0, dangerConfirmation: 'CONFIRM_CLEAR_ALL_SOCKET_TASKS'
}),
(error) => error instanceof DeviceControlError
&& error.code === 'DEVICE_DANGEROUS_ACTION_FORBIDDEN'
);
await service.clearSmartSocketTask(platformContext, {
slotNum: 2, taskNum: 0, dangerConfirmation: 'CONFIRM_CLEAR_ALL_SOCKET_TASKS'
});
assert.deepEqual(issued.at(-1).payload, {
action: 'clearTask', slotNum: 2, taskNum: 0, id: '1782120000001'
});
await assert.rejects(
() => service.controlPower({
...context,
@@ -180,10 +261,16 @@ const app = await buildApp({
async cancelTask() { return {}; },
async pairSubLock() { return {}; },
async controlSubLock() { return {}; },
async readSmartSocket() { return {}; },
async readSmartSocket(_context, input) { routed = input; return {}; },
async switchSmartSocket(_context, input) { routed = input; return { status: 'PUBLISHED' }; },
async scheduleSmartSocket() { return {}; },
async clearSmartSocketTask() { return {}; }
async clearSmartSocketTask() { return {}; },
async rebootSmartSocket() { return {}; },
async emptySmartSocketEnergy() { return {}; },
async resetSmartSocket(_context, input) { routed = input; return {}; },
async changeSmartSocketReturnKey() { return {}; },
async configureSmartSocketProtection(_context, input) { routed = input; return {}; },
async configureSmartSocketParameter() { return {}; }
}
}
});
@@ -222,6 +309,44 @@ const socketResponse = await app.inject({
});
assert.equal(socketResponse.statusCode, 200);
assert.equal(routed.on, true);
const socketTaskRead = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/read',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', target: 'localtask', slotNum: 1, taskNum: 0 }
});
assert.equal(socketTaskRead.statusCode, 200);
assert.equal(routed.target, 'localtask');
assert.equal(routed.taskNum, 0);
const invalidSocketTaskRead = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/read',
headers: { authorization: `Bearer ${token}` },
payload: { storeId: '11', roomId: '31', target: 'localtask' }
});
assert.equal(invalidSocketTaskRead.statusCode, 400);
const invalidSocketTask = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/task',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', roomId: '31', taskNum: 1, action: 'on',
mode: 'weekly', time: '24:00', weekdays: [1]
}
});
assert.equal(invalidSocketTask.statusCode, 400);
const socketProtection = await app.inject({
method: 'POST',
url: '/admin-api/device-control/socket/protection',
headers: { authorization: `Bearer ${token}` },
payload: {
storeId: '11', roomId: '31', maxPower: 3500, maxCurrent: 15,
maxTemperature: 80, pullOutStop: 1, pullOutPower: 5, pullOutSec: 10,
chargeFullStop: 1, chargeFullPower: 8, chargeFullSec: 30
}
});
assert.equal(socketProtection.statusCode, 200);
assert.equal(routed.maxCurrent, 15);
const subLockResponse = await app.inject({
method: 'POST',
url: '/admin-api/device-control/sub-lock/action',
@@ -274,4 +399,4 @@ assert.equal(routedContext.traceId, 'm08a-open-door');
assert.deepEqual(routedContext.access.capabilities, ['device.write']);
await app.close();
console.log('PASS: M06-E device control commands include control-box, Sub-1G and smart socket.');
console.log('PASS: M06-E-R1 socket controls, capability limits and dangerous gates work.');
+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.');
@@ -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',