@7365admin1/core 3.131.0 → 3.132.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +58 -13
- package/dist/index.js +250 -129
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +247 -129
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/ounitech-api-2025-10.test.mjs +148 -4
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { after, before, test } from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import { ROBOT_CLOUD_PATHS, OUNITECH_TIMESTAMP_SKEW_S } from "./.build/utils/robot-cloud.util.mjs";
|
|
8
8
|
import { RobotCloudError, useRobotCloudService } from "./.build/services/robot-cloud.service.mjs";
|
|
9
|
-
import { createOunitechAdapter, ounitechRobotFromHomeRow, ounitechFault } from "./.build/services/ounitech.adapter.mjs";
|
|
9
|
+
import { createOunitechAdapter, ounitechRobotFromHomeRow, ounitechFault, ounitechJobWords, OUNITECH_FRESH_MS } from "./.build/services/ounitech.adapter.mjs";
|
|
10
10
|
|
|
11
11
|
/* --------------------------------------------------------------------------
|
|
12
12
|
* Ounitech "Robot Cloud API Document (2025.10.22)", received 2026-09-28.
|
|
@@ -53,8 +53,45 @@ const ROWS = [
|
|
|
53
53
|
robotId: 427, robotSn: "ERROR1", modelCode: "Q3W", sceneId: 438, floorNo: 2,
|
|
54
54
|
robotState: { x: 0, y: 0, theta: 0, statusFlag: 7, properties: [prop("battery-percent", 55)] },
|
|
55
55
|
},
|
|
56
|
+
// Prod 2026-09-30: the list said offline while the live state (below) answered Idle.
|
|
57
|
+
{
|
|
58
|
+
robotId: 430, robotSn: "LISTOFFLINE", modelCode: "Q3", sceneId: 438, floorNo: 3,
|
|
59
|
+
robotState: { x: 0, y: 0, theta: 0, statusFlag: 99, properties: [prop("battery-percent", 0)] },
|
|
60
|
+
},
|
|
56
61
|
];
|
|
57
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The shape of get-robot-state-v2.0's reply for robot 424 (Q3, 4G) on prod,
|
|
65
|
+
* 2026-09-30 ~02:04Z, while our UI showed it offline. Readings as the cloud
|
|
66
|
+
* sends them: every value a string, labels Chinese. No credential in it.
|
|
67
|
+
*/
|
|
68
|
+
const RECORDED_UPDATE_ON = Date.parse("2026-09-30T02:04:08Z");
|
|
69
|
+
const s = (key, value, type = "string", unit = "") => ({ key, name: "", type, value: String(value), unit });
|
|
70
|
+
const recordedState = (robotId, updateOn = RECORDED_UPDATE_ON) => ({
|
|
71
|
+
robotId,
|
|
72
|
+
updateOn,
|
|
73
|
+
x: 0.4213, y: -1.0875, theta: 1.5708,
|
|
74
|
+
statusFlag: 1,
|
|
75
|
+
statusName: "待机",
|
|
76
|
+
subStatusFlag: 0,
|
|
77
|
+
subStatusName: "",
|
|
78
|
+
rtFaultList: [],
|
|
79
|
+
properties: [
|
|
80
|
+
s("battery-percent", 100, "int", "%"),
|
|
81
|
+
s("left-battery-percent", 100, "int", "%"),
|
|
82
|
+
s("right-battery-percent", 100, "int", "%"),
|
|
83
|
+
s("clean-water", 62, "int", "%"),
|
|
84
|
+
s("dirty-water", 8, "int", "%"),
|
|
85
|
+
s("job", "已结束"),
|
|
86
|
+
s("status", "待机"),
|
|
87
|
+
s("top-cover", 1, "int"),
|
|
88
|
+
s("clean-water-tank", 1, "int"),
|
|
89
|
+
s("waste-water-tank", 1, "int"),
|
|
90
|
+
s("collision-left", 0, "int"),
|
|
91
|
+
s("collision-right", 0, "int"),
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
58
95
|
let server;
|
|
59
96
|
let base;
|
|
60
97
|
const seen = [];
|
|
@@ -95,6 +132,8 @@ before(async () => {
|
|
|
95
132
|
case ROBOT_CLOUD_PATHS.robotList:
|
|
96
133
|
return res.end(JSON.stringify({ robotList: [] })); // chapter 1: owner-bound only
|
|
97
134
|
case ROBOT_CLOUD_PATHS.robotState:
|
|
135
|
+
if (body.robotId === 430 || body.robotId === 424) return ok({ robotState: recordedState(body.robotId, Date.now() - 8000) });
|
|
136
|
+
if (body.robotId === 425) return ok({ robotState: { robotId: 425, updateOn: Date.now() - 3_600_000, statusFlag: 99, properties: [] } });
|
|
98
137
|
return ok({ robotState: { robotId: body.robotId, statusFlag: 7, rtFaultList: [{ errCode: "621", errMsg: "Right wheel driver reset" }] } });
|
|
99
138
|
case ROBOT_CLOUD_PATHS.floorMaps:
|
|
100
139
|
return ok({
|
|
@@ -131,9 +170,11 @@ const adapter = (opts = {}) => createOunitechAdapter({ appid: APPID, secret: SEC
|
|
|
131
170
|
test("the robot list is the chapter-6 list, signed for its own path, credentials in the body", async () => {
|
|
132
171
|
seen.length = 0;
|
|
133
172
|
const robots = await adapter().listRobots();
|
|
134
|
-
assert.deepEqual(robots.map((r) => r.robotSn), ["F8FCA9A1B1C4C", "OFFLINE1", "WORKING1", "ERROR1"]);
|
|
135
|
-
assert.equal(seen.length, 1);
|
|
173
|
+
assert.deepEqual(robots.map((r) => r.robotSn), ["F8FCA9A1B1C4C", "OFFLINE1", "WORKING1", "ERROR1", "LISTOFFLINE"]);
|
|
136
174
|
assert.equal(seen[0].path, "/portal/home/get-robot-list");
|
|
175
|
+
// Live state only for the rows the list can't vouch for (99, 7) - never for a known-good status.
|
|
176
|
+
assert.deepEqual(seen.slice(1).map((x) => x.body.robotId).sort(), [425, 427, 430]);
|
|
177
|
+
assert.ok(seen.slice(1).every((x) => x.path === ROBOT_CLOUD_PATHS.robotState));
|
|
137
178
|
assert.deepEqual(Object.keys(seen[0].body).sort(), ["appid", "sign", "timestamp"]);
|
|
138
179
|
assert.equal(JSON.stringify(seen).includes(SECRET), false);
|
|
139
180
|
});
|
|
@@ -219,7 +260,7 @@ test("refusals are the host's own plain words, with the credential hint", async
|
|
|
219
260
|
|
|
220
261
|
test("within the 300 s window is accepted", async () => {
|
|
221
262
|
const early = adapter({ now: () => new Date(Date.now() - (OUNITECH_TIMESTAMP_SKEW_S - 30) * 1000) });
|
|
222
|
-
assert.equal((await early.listRobots()).length,
|
|
263
|
+
assert.equal((await early.listRobots()).length, 5);
|
|
223
264
|
});
|
|
224
265
|
|
|
225
266
|
test("diagnose: every read answered, chapter 1 shows why the old list was empty, never a command", async () => {
|
|
@@ -240,3 +281,106 @@ test("row mapping holds on its own (pure)", () => {
|
|
|
240
281
|
assert.equal(r.floorKnown, false);
|
|
241
282
|
assert.equal(ounitechRobotFromHomeRow({ robotSn: "Y" }).mode, "UNAVAILABLE");
|
|
242
283
|
});
|
|
284
|
+
|
|
285
|
+
test("prod 2026-09-30: list says offline, live state answers Idle -> Online / Idle / battery 100 (fleet, detail, Diagnose)", async () => {
|
|
286
|
+
const a = adapter();
|
|
287
|
+
const listed = (await a.listRobots()).find((r) => r.robotSn === "LISTOFFLINE");
|
|
288
|
+
const detail = await a.getRobot("LISTOFFLINE");
|
|
289
|
+
for (const r of [listed, detail]) {
|
|
290
|
+
assert.equal(r.online, true);
|
|
291
|
+
assert.equal(r.mode, "IDLE");
|
|
292
|
+
assert.equal(r.statusName, "Idle");
|
|
293
|
+
assert.equal(r.batteryPercent, 100);
|
|
294
|
+
assert.deepEqual(r.position, { x: 0.4213, y: -1.0875, theta: 1.5708 });
|
|
295
|
+
assert.equal(r.fault, undefined, "empty rtFaultList is no fault");
|
|
296
|
+
}
|
|
297
|
+
const steps = await a.diagnose("LISTOFFLINE");
|
|
298
|
+
const live = steps.find((x) => x.step.startsWith("live state")).data;
|
|
299
|
+
assert.equal(live.online, true);
|
|
300
|
+
assert.equal(live.status, "Idle");
|
|
301
|
+
assert.equal(live.job, "job finished");
|
|
302
|
+
assert.equal(live.battery, 100);
|
|
303
|
+
assert.equal(live.cleanWater, 62);
|
|
304
|
+
assert.equal(live.dirtyWater, 8);
|
|
305
|
+
assert.equal(live.fault, null);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("live state mapping, recorded reply (pure)", () => {
|
|
309
|
+
const row = { robotId: 424, robotSn: "F8FCA9A1B1C4C", modelCode: "Q3", floorNo: 3, robotState: { statusFlag: 99, properties: [prop("battery-percent", 0)] } };
|
|
310
|
+
const now = RECORDED_UPDATE_ON + 20_000;
|
|
311
|
+
const r = ounitechRobotFromHomeRow(row, recordedState(424), now);
|
|
312
|
+
assert.equal(r.online, true);
|
|
313
|
+
assert.equal(r.statusName, "Idle");
|
|
314
|
+
assert.equal(r.batteryPercent, 100);
|
|
315
|
+
assert.equal(ounitechJobWords(recordedState(424)), "job finished");
|
|
316
|
+
|
|
317
|
+
// statusFlag as a numeric string
|
|
318
|
+
assert.equal(ounitechRobotFromHomeRow({ robotSn: "S", robotState: { statusFlag: "1" } }, null, now).mode, "IDLE");
|
|
319
|
+
// no statusFlag: the Chinese status reading supplies it
|
|
320
|
+
const noFlag = { ...recordedState(424), statusFlag: undefined };
|
|
321
|
+
assert.equal(ounitechRobotFromHomeRow({ robotSn: "C" }, noFlag, now).statusName, "Idle");
|
|
322
|
+
const charging = { properties: [s("status", "充电中")] };
|
|
323
|
+
assert.equal(ounitechRobotFromHomeRow({ robotSn: "C" }, charging, now).statusName, "Charging");
|
|
324
|
+
// no status at all: a fresh updateOn is online, a stale one is not
|
|
325
|
+
const bare = (age) => ({ updateOn: now - age, properties: [s("battery-percent", 70, "int", "%")] });
|
|
326
|
+
const fresh = ounitechRobotFromHomeRow({ robotSn: "F" }, bare(60_000), now);
|
|
327
|
+
assert.equal(fresh.online, true);
|
|
328
|
+
assert.equal(fresh.mode, "UNAVAILABLE", "never dispatchable without a known status");
|
|
329
|
+
assert.equal(fresh.batteryPercent, 70);
|
|
330
|
+
const stale = ounitechRobotFromHomeRow({ robotSn: "F" }, bare(OUNITECH_FRESH_MS + 1000), now);
|
|
331
|
+
assert.equal(stale.online, false);
|
|
332
|
+
assert.equal(stale.statusName, "Unknown");
|
|
333
|
+
assert.equal(stale.batteryPercent, null);
|
|
334
|
+
// the live state saying 99 beats a fresh-looking list row
|
|
335
|
+
const off = ounitechRobotFromHomeRow({ robotSn: "O", robotState: { statusFlag: 1 } }, { statusFlag: 99, updateOn: now }, now);
|
|
336
|
+
assert.equal(off.online, false);
|
|
337
|
+
assert.equal(off.statusName, "Offline");
|
|
338
|
+
// a fault listed live is named even without status 7
|
|
339
|
+
const faulty = ounitechRobotFromHomeRow({ robotSn: "E" }, { statusFlag: 1, rtFaultList: [{ errCode: "302", errMsg: "Clean water tank empty" }] }, now);
|
|
340
|
+
assert.deepEqual(faulty.fault, { code: "302", message: "Clean water tank empty" });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("cleaning readings: water levels, tanks and covers, job progress only while cleaning (pure)", async () => {
|
|
344
|
+
const { ounitechCleaning } = await import("./.build/services/ounitech.adapter.mjs");
|
|
345
|
+
const now = RECORDED_UPDATE_ON + 20_000;
|
|
346
|
+
const row = { robotId: 424, robotSn: "F8FCA9A1B1C4C", modelCode: "Q3", robotState: { statusFlag: 99 } };
|
|
347
|
+
// The recorded prod reply: idle, clean 62 %, dirty 8 %, every tank/cover fitted.
|
|
348
|
+
assert.deepEqual(ounitechRobotFromHomeRow(row, recordedState(424), now).cleaning, {
|
|
349
|
+
cleanWater: { level: "medium", percent: 62 },
|
|
350
|
+
dirtyWater: { level: "low", percent: 8 },
|
|
351
|
+
parts: [
|
|
352
|
+
{ part: "top cover", ok: true },
|
|
353
|
+
{ part: "clean-water tank", ok: true },
|
|
354
|
+
{ part: "dirty-water tank", ok: true },
|
|
355
|
+
],
|
|
356
|
+
job: null,
|
|
357
|
+
});
|
|
358
|
+
const st = (props) => ({ properties: props.map(([k, v]) => s(k, v)) });
|
|
359
|
+
// Cleaning: this job's % and area; low clean water, full dirty water, cover open.
|
|
360
|
+
const c = ounitechCleaning(st([["clean-water", 12], ["dirty-water", 91], ["top-cover", "false"], ["dust-box", "true"], ["acc-percent", 40], ["acc-area", "120.46"], ["count-area", "300"]]), true);
|
|
361
|
+
assert.deepEqual(c, {
|
|
362
|
+
cleanWater: { level: "low", percent: 12 },
|
|
363
|
+
dirtyWater: { level: "high", percent: 91 },
|
|
364
|
+
parts: [{ part: "top cover", ok: false }, { part: "dust box", ok: true }],
|
|
365
|
+
job: { percent: 40, areaDone: 120.5, areaPlanned: 300 },
|
|
366
|
+
});
|
|
367
|
+
// Not cleaning: no job line even with acc-* readings.
|
|
368
|
+
assert.equal(ounitechCleaning(st([["acc-percent", 40]]), false), null);
|
|
369
|
+
// No % sent: the robot's own level word.
|
|
370
|
+
assert.deepEqual(ounitechCleaning(st([["clean-water-level", "中"], ["dirty-water-level", "高"]]), false).dirtyWater, { level: "high", percent: null });
|
|
371
|
+
// Nothing reported: null, never guessed.
|
|
372
|
+
assert.equal(ounitechCleaning({ properties: [] }, true), null);
|
|
373
|
+
// Job % falls back to the list's taskState.
|
|
374
|
+
assert.deepEqual(ounitechCleaning({ properties: [] }, true, 55.4).job, { percent: 55, areaDone: null, areaPlanned: null });
|
|
375
|
+
// Offline: nothing.
|
|
376
|
+
assert.equal(ounitechRobotFromHomeRow(row, { ...recordedState(424), statusFlag: 99 }, now).cleaning, undefined);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("cleaning readings reach the fleet state (waterTanks brands only)", async () => {
|
|
380
|
+
const { fleetStateItem } = await import("./.build/services/robot-fleet.service.mjs");
|
|
381
|
+
const cleaning = { cleanWater: { level: "low", percent: 10 }, dirtyWater: null, parts: [], job: null };
|
|
382
|
+
assert.deepEqual(fleetStateItem("x", { online: true, cleaning }).cleaning, cleaning);
|
|
383
|
+
assert.equal(fleetStateItem("x", { online: true }).cleaning, null);
|
|
384
|
+
const r = await adapter().getRobot("LISTOFFLINE");
|
|
385
|
+
assert.equal(r.cleaning.cleanWater.percent, 62);
|
|
386
|
+
});
|