@7365admin1/core 3.132.0 → 3.133.1
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 +142 -1
- package/dist/index.js +536 -116
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +525 -116
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/robot-ounitech-company-creds.e2e.test.mjs +3 -1
- package/test/e2e/robot-vendor-errors.e2e.test.mjs +2 -2
- package/test/ounitech-api-2025-10.test.mjs +190 -10
- package/test/robot-vendor-errors.test.mjs +3 -0
package/package.json
CHANGED
|
@@ -114,7 +114,9 @@ describe("Ounitech: base URL, server-verified sign, UTC timestamp, one credentia
|
|
|
114
114
|
]);
|
|
115
115
|
const text = JSON.stringify(res.body);
|
|
116
116
|
for (const s of [APPID, SECRET, "+6500000000", "Vendor Owner"]) assert.equal(text.includes(s), false, s);
|
|
117
|
-
|
|
117
|
+
// The list call (a map read may follow it: robots are placed on their own map).
|
|
118
|
+
assert.deepEqual(mock.calls.findLast((c) => c.path === "/portal/home/get-robot-list"), { path: "/portal/home/get-robot-list", signOk: true, utcNow: true, leakedSecret: false });
|
|
119
|
+
assert.ok(mock.calls.every((c) => !c.leakedSecret));
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
it("the first robot is added with typed credentials and a picked serial; stored encrypted", async () => {
|
|
@@ -144,8 +144,8 @@ describe("robot vendor failures are handled answers; diagnose names the step", {
|
|
|
144
144
|
mode = "ok";
|
|
145
145
|
const ok = await h.api(`/robots/${id.robot}/diagnose`, { sid: staffSid });
|
|
146
146
|
assert.equal(ok.body.failedStep, null, JSON.stringify(ok.body));
|
|
147
|
-
// credentials, sign-in, get robot, maps, then the
|
|
148
|
-
assert.equal(ok.body.steps.length,
|
|
147
|
+
// credentials, sign-in, get robot, maps, then the 11 read-only 2025.10 calls (all answered).
|
|
148
|
+
assert.equal(ok.body.steps.length, 15, JSON.stringify(ok.body.steps.map((s) => s.step)));
|
|
149
149
|
assert.ok(ok.body.steps.every((s) => s.ok), JSON.stringify(ok.body.steps.filter((s) => !s.ok)));
|
|
150
150
|
assert.match(ok.body.steps[2].detail, /model Q3, battery 88%, floor 1/);
|
|
151
151
|
assert.ok(ok.body.clock?.vendorDate, "the vendor's clock is reported");
|
|
@@ -6,7 +6,22 @@ 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 {
|
|
9
|
+
import {
|
|
10
|
+
createOunitechAdapter,
|
|
11
|
+
ounitechRobotFromHomeRow,
|
|
12
|
+
ounitechFault,
|
|
13
|
+
ounitechJobWords,
|
|
14
|
+
OUNITECH_FRESH_MS,
|
|
15
|
+
ounitechMapShare,
|
|
16
|
+
ounitechAlertRecord,
|
|
17
|
+
ounitechReadings,
|
|
18
|
+
ounitechTaskRecord,
|
|
19
|
+
ounitechConsumable,
|
|
20
|
+
ounitechPowerPlan,
|
|
21
|
+
ounitechTime,
|
|
22
|
+
pngSize,
|
|
23
|
+
clearOunitechCaches,
|
|
24
|
+
} from "./.build/services/ounitech.adapter.mjs";
|
|
10
25
|
|
|
11
26
|
/* --------------------------------------------------------------------------
|
|
12
27
|
* Ounitech "Robot Cloud API Document (2025.10.22)", received 2026-09-28.
|
|
@@ -92,6 +107,20 @@ const recordedState = (robotId, updateOn = RECORDED_UPDATE_ON) => ({
|
|
|
92
107
|
],
|
|
93
108
|
});
|
|
94
109
|
|
|
110
|
+
/** get-task-list rows in the document's shape (2025.10.22, robot 318 / 160). */
|
|
111
|
+
const TASKS = [
|
|
112
|
+
{ taskId: 161745, robotId: 318, sceneId: 332, mapId: 1, timeFrom: "2025-10-20 14:22:09", timeTo: "2025-10-20 14:22:33", typeFlag: 24, subTypeFlag: 201, statusFlag: "0", cleanedArea: 0.8225, cleanedMileage: 1.2946, accArea: 0.8225, countArea: 20.57, accMile: 1.2946, accTime: 14, accPercent: 0, zoneno: 1, source: "app,38786,2025-10-20 14:22:05.808", statusName: "Finished", typeName: "准备清洁", subTypeName: "吸尘", sceneMap: null, wallList: [] },
|
|
113
|
+
{ taskId: 148257, robotId: 160, sceneId: 174, mapId: 2700, timeFrom: "2025-09-03 08:15:02", timeTo: "2025-09-03 08:31:00", durationSeconds: 946, typeFlag: 17, statusFlag: "0", cleanedArea: 65.86, cleanedMileage: 234.3402, accArea: 65.86, countArea: 67.43, accTime: 946, accPercent: 100, zoneno: 2, source: "plan,3059,2025-09-03 08:15:00.794", statusName: "Finished", floorNo: 1 },
|
|
114
|
+
{ taskId: 161739, robotId: 318, mapId: 1, timeFrom: "2025-10-20 14:09:13", timeTo: "2025-10-20 14:09:19", typeFlag: 17, statusFlag: "0", cleanedArea: 0, accTime: 0, accPercent: 0, source: "btn,1,2025-10-20 14:09:10.481", statusName: "Finished", typeName: "脱离基站中", subTypeName: "洗地" },
|
|
115
|
+
{ taskId: 161741, robotId: 318, mapId: 1, timeFrom: "2025-10-20 14:12:23", timeTo: "2025-10-20 14:13:49", statusName: "Finished" },
|
|
116
|
+
];
|
|
117
|
+
/** get-alert-list rows as robot 424 sent them on prod (2026-09-30) and the document's resolved form. */
|
|
118
|
+
const ALERTS = [
|
|
119
|
+
{ deviceFlag: 1, deviceName: "Q3标准版-F8FCA9A1B1C4C", sceneId: 438, errMsg: "734 - (提示)IMU零点偏移过大( 5)", createTime: "2026-09-30 08:32:04" },
|
|
120
|
+
{ deviceFlag: 1, errMsg: "540 - (提示)地检-悬崖打开( 5)", createTime: "2026-09-30 07:02:30" },
|
|
121
|
+
{ deviceFlag: 1, errCode: "621 - 右轮驱动器重置( 4)(Resolved)", errMsg: "621 - 右轮驱动器重置( 4)(Resolved)", createTime: "2025-10-20 14:23:45" },
|
|
122
|
+
];
|
|
123
|
+
|
|
95
124
|
let server;
|
|
96
125
|
let base;
|
|
97
126
|
const seen = [];
|
|
@@ -107,6 +136,16 @@ before(async () => {
|
|
|
107
136
|
let raw = "";
|
|
108
137
|
req.on("data", (c) => (raw += c));
|
|
109
138
|
req.on("end", () => {
|
|
139
|
+
// The signed map image (COS in production): a PNG whose header says 200 x 100 px.
|
|
140
|
+
if (req.method === "GET" && req.url.startsWith("/img/")) {
|
|
141
|
+
const png = Buffer.alloc(33);
|
|
142
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13]).copy(png, 0);
|
|
143
|
+
png.write("IHDR", 12, "latin1");
|
|
144
|
+
png.writeUInt32BE(200, 16);
|
|
145
|
+
png.writeUInt32BE(100, 20);
|
|
146
|
+
res.writeHead(206, { "content-type": "image/png" });
|
|
147
|
+
return res.end(png);
|
|
148
|
+
}
|
|
110
149
|
if (!/application\/json/.test(req.headers["content-type"] ?? "")) return text(res, 400, "Content-Type must be application/json");
|
|
111
150
|
let body;
|
|
112
151
|
try {
|
|
@@ -138,18 +177,25 @@ before(async () => {
|
|
|
138
177
|
case ROBOT_CLOUD_PATHS.floorMaps:
|
|
139
178
|
return ok({
|
|
140
179
|
mapList: [
|
|
141
|
-
{ mapId: 1, floorNo:
|
|
180
|
+
{ mapId: 1, floorNo: 3, mapName: "L3", latestFlag: true, backgroundFileKey: "/k1.png", partitionFileKey: "/k1_zone.png", originX: -1, originY: -5, resolution: 0.05, totalArea: 10, baseX: 0, baseY: 0, baseTheta: 0, wallList: [{ wallId: 1, pointList: [[0, 0], [1, 1]] }], areaList: [{ areaId: 9, areaName: "Lobby", rgba: "FF0B0B83", x: 10, y: 20, pathType: 0 }] },
|
|
142
181
|
{ mapId: 2, floorNo: 1, mapName: "L1 old", latestFlag: false, backgroundFileKey: "/k0.png" },
|
|
143
182
|
],
|
|
144
183
|
});
|
|
145
184
|
case ROBOT_CLOUD_PATHS.filePreview:
|
|
146
|
-
return ok({ fileList: body.fileKeys.map((k) => ({ fileKey: k, fileUrl:
|
|
185
|
+
return ok({ fileList: body.fileKeys.map((k) => ({ fileKey: k, fileUrl: `${base}/img${k}?sign=x` })) });
|
|
147
186
|
case ROBOT_CLOUD_PATHS.plansV2:
|
|
148
187
|
return ok({ total: 1, planList: [{ planId: 2274, robotId: body.robotId, dateList: [1, 2, 3], startTime: "08:30", endTime: "12:30", enableFlag: false, cleanList: [{ floorNo: 3, rangeFlag: 1, cleanTimes: 1, modeFlag: 1, subModeFlag: 0, levelFlag: 1 }] }] });
|
|
149
188
|
case ROBOT_CLOUD_PATHS.taskList:
|
|
150
|
-
return ok({ total: 4, taskList:
|
|
189
|
+
return ok({ total: 4, taskList: TASKS });
|
|
151
190
|
case ROBOT_CLOUD_PATHS.alertList:
|
|
152
|
-
return ok({ total:
|
|
191
|
+
return ok({ total: 3, alertList: ALERTS.map((a) => ({ deviceId: body.deviceId, ...a })) });
|
|
192
|
+
case ROBOT_CLOUD_PATHS.workHours:
|
|
193
|
+
return ok({ unitlist: [{ unitid: 51, modelcode: "Q3W", unitcode: "left_side_brush_plastic", unitname: "左边刷毛", orderno: 1, workminutes: 42330, workdays: 30 }] });
|
|
194
|
+
case ROBOT_CLOUD_PATHS.haltPlans:
|
|
195
|
+
return ok({ total: 1, planList: [{ planId: 4, robotId: body.robotId, dateList: [1, 2, 3, 4, 5], actionTime: "20:00", actionFlag: 0, enableFlag: true, remark: "1-5 晚8点关机" }] });
|
|
196
|
+
case ROBOT_CLOUD_PATHS.stationState:
|
|
197
|
+
if (body.robotId === 425) return res.end(JSON.stringify({ ok: false, msg: "no station bound (e=59)" })); // robot 424 on prod
|
|
198
|
+
return ok({ state: { stationId: 4560, updateOn: Date.now(), statusFlag: "0", statusName: "Offline", properties: [s("clean-water", "3.300", "double", "升"), s("charge-err", "false", "bool")] } });
|
|
153
199
|
case ROBOT_CLOUD_PATHS.liftList:
|
|
154
200
|
return ok({ liftlist: [{ liftid: 45, liftno: 3701, liftname: "Lift A", floorno: 13, onlineflag: 1, enableflag: true, simno: "868327073829804" }] });
|
|
155
201
|
case ROBOT_CLOUD_PATHS.mapList:
|
|
@@ -162,6 +208,8 @@ before(async () => {
|
|
|
162
208
|
server.listen(0, "127.0.0.1");
|
|
163
209
|
await once(server, "listening");
|
|
164
210
|
base = `http://127.0.0.1:${server.address().port}`;
|
|
211
|
+
// Maps are cached per AppID + scene; warm them so the call-order tests below see only their own reads.
|
|
212
|
+
await createOunitechAdapter({ appid: APPID, secret: SECRET }, { baseUrl: base, timeoutMs: 2000 }).getMaps("F8FCA9A1B1C4C");
|
|
165
213
|
});
|
|
166
214
|
after(() => server.close());
|
|
167
215
|
|
|
@@ -173,8 +221,9 @@ test("the robot list is the chapter-6 list, signed for its own path, credentials
|
|
|
173
221
|
assert.deepEqual(robots.map((r) => r.robotSn), ["F8FCA9A1B1C4C", "OFFLINE1", "WORKING1", "ERROR1", "LISTOFFLINE"]);
|
|
174
222
|
assert.equal(seen[0].path, "/portal/home/get-robot-list");
|
|
175
223
|
// Live state only for the rows the list can't vouch for (99, 7) - never for a known-good status.
|
|
176
|
-
|
|
177
|
-
assert.
|
|
224
|
+
const live = seen.filter((x) => x.path === ROBOT_CLOUD_PATHS.robotState);
|
|
225
|
+
assert.deepEqual(live.map((x) => x.body.robotId).sort(), [425, 427, 430]);
|
|
226
|
+
assert.equal(seen.length, 1 + live.length, "maps come from the cache - no extra vendor reads per poll");
|
|
178
227
|
assert.deepEqual(Object.keys(seen[0].body).sort(), ["appid", "sign", "timestamp"]);
|
|
179
228
|
assert.equal(JSON.stringify(seen).includes(SECRET), false);
|
|
180
229
|
});
|
|
@@ -219,21 +268,22 @@ test("status 7 costs one extra read, get-robot-state-v2.0 by robotId, to name th
|
|
|
219
268
|
});
|
|
220
269
|
|
|
221
270
|
test("maps: get-floor-maps by the robot's sceneId, latest only, walls left out", async () => {
|
|
271
|
+
clearOunitechCaches();
|
|
222
272
|
seen.length = 0;
|
|
223
273
|
const maps = await adapter().getMaps("F8FCA9A1B1C4C");
|
|
224
|
-
assert.deepEqual(seen.map((s) => s.path), [ROBOT_CLOUD_PATHS.homeRobotList, ROBOT_CLOUD_PATHS.floorMaps]);
|
|
274
|
+
assert.deepEqual(seen.map((s) => s.path), [ROBOT_CLOUD_PATHS.homeRobotList, ROBOT_CLOUD_PATHS.floorMaps, ROBOT_CLOUD_PATHS.filePreview]);
|
|
225
275
|
assert.equal(seen[1].body.sceneId, 438);
|
|
226
276
|
assert.equal(seen[1].body.enableFlag, 1);
|
|
227
277
|
assert.equal(maps.length, 1);
|
|
228
278
|
assert.equal(maps[0].backgroundFileKey, "/k1.png");
|
|
229
|
-
assert.deepEqual(maps[0].areaList, [{ areaName: "Lobby", rgba: "FF0B0B83", x: 10, y: 20 }]);
|
|
279
|
+
assert.deepEqual(maps[0].areaList, [{ areaId: 9, areaName: "Lobby", rgba: "FF0B0B83", x: 10, y: 20 }]);
|
|
230
280
|
assert.equal("wallList" in maps[0], false);
|
|
231
281
|
});
|
|
232
282
|
|
|
233
283
|
test("map images: upload/preview by file key, fileUrl read as url", async () => {
|
|
234
284
|
seen.length = 0;
|
|
235
285
|
const files = await adapter().getMapImages("F8FCA9A1B1C4C", ["/k1.png"]);
|
|
236
|
-
assert.deepEqual(files, [{ fileKey: "/k1.png", url:
|
|
286
|
+
assert.deepEqual(files, [{ fileKey: "/k1.png", url: `${base}/img/k1.png?sign=x` }]);
|
|
237
287
|
assert.deepEqual(seen[0].body.fileKeys, ["/k1.png"]);
|
|
238
288
|
});
|
|
239
289
|
|
|
@@ -384,3 +434,133 @@ test("cleaning readings reach the fleet state (waterTanks brands only)", async (
|
|
|
384
434
|
const r = await adapter().getRobot("LISTOFFLINE");
|
|
385
435
|
assert.equal(r.cleaning.cleanWater.percent, 62);
|
|
386
436
|
});
|
|
437
|
+
|
|
438
|
+
/* ---------------------------------------------- real map, robot pin, records */
|
|
439
|
+
|
|
440
|
+
test("maps carry the real image: mapId, signed imageUrl + partition, PNG size, areas and dock placed", async () => {
|
|
441
|
+
const [m] = await adapter().getMaps("F8FCA9A1B1C4C");
|
|
442
|
+
assert.equal(m.mapId, "1");
|
|
443
|
+
assert.equal(m.floor, 3);
|
|
444
|
+
assert.equal(m.mapDisplayName, "L3");
|
|
445
|
+
assert.equal(m.imageUrl, `${base}/img/k1.png?sign=x`);
|
|
446
|
+
assert.equal(m.partitionImageUrl, `${base}/img/k1_zone.png?sign=x`);
|
|
447
|
+
assert.equal(m.width, 200);
|
|
448
|
+
assert.equal(m.height, 100);
|
|
449
|
+
assert.deepEqual(m.areas, [{ name: "Lobby", at: { x: 5, y: 20 } }]);
|
|
450
|
+
// base (0, 0): x = (0 + 1) / 0.05 = 20 px of 200; y up = (0 + 5) / 0.05 = 100 px of 100 -> top edge.
|
|
451
|
+
assert.deepEqual(m.dock, { x: 10, y: 0 });
|
|
452
|
+
assert.equal(m.positionApproximate, true);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("the robot is placed on its own map: mapId + planPosition from x/y, origin and resolution", async () => {
|
|
456
|
+
const r = await adapter().getRobot("F8FCA9A1B1C4C");
|
|
457
|
+
assert.equal(r.mapId, "1");
|
|
458
|
+
// x (1.5 + 1) / 0.05 = 50 px -> 25 %; y up (-2.25 + 5) / 0.05 = 55 px -> 45 % from the top.
|
|
459
|
+
assert.deepEqual(r.planPosition, { x: 25, y: 45 });
|
|
460
|
+
const off = await adapter().getRobot("OFFLINE1");
|
|
461
|
+
assert.equal(off.planPosition, undefined, "offline: never placed");
|
|
462
|
+
const otherFloor = await adapter().getRobot("WORKING1");
|
|
463
|
+
assert.equal(otherFloor.mapId, undefined, "no map for floor 2: not placed");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("prod robot 424 numbers (pure): 317 x 255 px map, origin -10.79 / -3.828, 0.05 m/px", () => {
|
|
467
|
+
const frame = { width: 317, height: 255, resolution: 0.05, originX: -10.79, originY: -3.828 };
|
|
468
|
+
assert.deepEqual(ounitechMapShare(frame, 0.14812549284101845, 0.008918337419457247), { x: 69.01, y: 69.91 });
|
|
469
|
+
assert.equal(ounitechMapShare(frame, 99, 0), null, "off the image: not drawn");
|
|
470
|
+
assert.equal(ounitechMapShare({ ...frame, width: null }, 0, 0), null);
|
|
471
|
+
const header = new Uint8Array(24);
|
|
472
|
+
header.set([0x89, 0x50, 0x4e, 0x47], 0);
|
|
473
|
+
header.set([73, 72, 68, 82], 12);
|
|
474
|
+
new DataView(header.buffer).setUint32(16, 317);
|
|
475
|
+
new DataView(header.buffer).setUint32(20, 255);
|
|
476
|
+
assert.deepEqual(pngSize(header), { width: 317, height: 255 });
|
|
477
|
+
assert.equal(pngSize(new Uint8Array(24)), null, "not a PNG");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("records: jobs, alert history, consumables, power schedule, lifts, readings, base station - read-only and cached", async () => {
|
|
481
|
+
clearOunitechCaches();
|
|
482
|
+
seen.length = 0;
|
|
483
|
+
const a = adapter();
|
|
484
|
+
const r = await a.getRecords("F8FCA9A1B1C4C");
|
|
485
|
+
assert.equal(r.supported, true);
|
|
486
|
+
assert.equal(seen.some((x) => COMMAND_PATHS.has(x.path)), false, "never a command");
|
|
487
|
+
const task = seen.find((x) => x.path === ROBOT_CLOUD_PATHS.taskList).body;
|
|
488
|
+
assert.equal(task.robotId, 424);
|
|
489
|
+
assert.match(task.timeFrom, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
490
|
+
assert.equal(r.tasks.error, undefined);
|
|
491
|
+
assert.equal(r.tasks.items.length, 4);
|
|
492
|
+
assert.equal(r.tasks.items[0].id, "161745", "newest first");
|
|
493
|
+
assert.deepEqual(
|
|
494
|
+
{ ...r.tasks.items[0] },
|
|
495
|
+
{ id: "161745", start: "2025-10-20T06:22:09.000Z", end: "2025-10-20T06:22:33.000Z", durationSec: 14, areaM2: 0.8, plannedM2: 20.6, percent: 0, distanceM: 1, result: "Finished", job: "Getting ready", mode: "Vacuum", source: "Ounitech app", map: "L3", floorNo: 3 },
|
|
496
|
+
);
|
|
497
|
+
assert.deepEqual(r.alerts.items.map((x) => [x.code, x.level, x.message, x.resolved]), [
|
|
498
|
+
["734", "Notice", "IMU zero offset too large", false],
|
|
499
|
+
["540", "Notice", "Floor check: cliff sensor switched on", false],
|
|
500
|
+
["621", null, "Right wheel driver reset", true],
|
|
501
|
+
]);
|
|
502
|
+
assert.deepEqual(r.consumables.items, [{ part: "Left side brush", hours: 705.5, days: 30 }]);
|
|
503
|
+
assert.deepEqual(r.power.items, [{ days: [1, 2, 3, 4, 5], time: "20:00", action: "off", enabled: true }]);
|
|
504
|
+
assert.deepEqual(r.lifts.items, [{ name: "Lift A", floorNo: 13, online: true, enabled: true }]);
|
|
505
|
+
assert.ok(r.readings.items.some((x) => x.label === "Left battery" && x.value === "100%"));
|
|
506
|
+
assert.equal(r.station.items[0].status, "Offline");
|
|
507
|
+
assert.ok(r.station.items[0].readings.some((x) => x.label === "Station clean water" && x.value === "3.300 L"));
|
|
508
|
+
assert.equal(JSON.stringify(r).includes("868327073829804"), false, "lift SIM number left out");
|
|
509
|
+
const calls = seen.length;
|
|
510
|
+
await a.getRecords("F8FCA9A1B1C4C");
|
|
511
|
+
assert.equal(seen.length, calls, "second read within 2 min comes from the cache (the vendor rate-limits)");
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("records: a robot not on the account says so", async () => {
|
|
515
|
+
const r = await adapter().getRecords("NOPE");
|
|
516
|
+
assert.equal(r.supported, true);
|
|
517
|
+
assert.match(r.error, /not on this Ounitech account/);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("alert, reading, task and schedule mapping (pure, recorded shapes)", () => {
|
|
521
|
+
const a = ounitechAlertRecord({ errMsg: "785 - (提示)定位准备完成( 5)", createTime: "2026-09-30 07:02:57" });
|
|
522
|
+
assert.deepEqual(a, { code: "785", level: "Notice", message: "Localisation ready", vendorText: "定位准备完成", resolved: false, at: "2026-09-29T23:02:57.000Z" });
|
|
523
|
+
assert.equal(ounitechAlertRecord({ errMsg: "999 - (警告)未知( 3)" }).message, "Robot alert 999: 未知", "unknown code: code first, the vendor's words kept");
|
|
524
|
+
// Robot 424's other prod alerts (2026-09-30) read in English.
|
|
525
|
+
assert.equal(ounitechAlertRecord({ errMsg: "589 - (提示)定时关机( 5)" }).message, "Switched off by the timed power schedule");
|
|
526
|
+
assert.equal(ounitechAlertRecord({ errMsg: "205 - 边刷驱动器离线( 4)" }).message, "Side brush driver offline");
|
|
527
|
+
assert.equal(ounitechAlertRecord({ errMsg: "900 - plain words" }).message, "plain words");
|
|
528
|
+
assert.equal(ounitechConsumable({ unitcode: "scraper", workminutes: 60 }).part, "Scraper");
|
|
529
|
+
assert.equal(ounitechAlertRecord({ errMsg: "999 - (警告)未知( 3)" }).level, "Warning");
|
|
530
|
+
|
|
531
|
+
// Robot 424's live readings on prod (2026-09-30); IP and SIM left out.
|
|
532
|
+
const rows = ounitechReadings([
|
|
533
|
+
s("left-battery-percent", "100"), s("right-battery-percent", "0"), s("bat-vol-left", "29.4"), s("clean-water-level", "低"),
|
|
534
|
+
s("top-cover", "true"), s("dust-box", "false"), s("collision-left", "false"), s("acc-time", "600"), s("job", "已结束"),
|
|
535
|
+
s("status", "待机"), s("RouterSignalStrength", "-79 dBm"), s("RobotIP", "10.0.0.8"), s("RouterIccid", "8986000000"), s("NewKey", "7"), s("McuSW", "-"),
|
|
536
|
+
]);
|
|
537
|
+
const by = Object.fromEntries(rows.map((x) => [x.key, x]));
|
|
538
|
+
assert.equal(by["left-battery-percent"].value, "100%");
|
|
539
|
+
assert.equal(by["bat-vol-left"].value, "29.4 V");
|
|
540
|
+
assert.equal(by["clean-water-level"].value, "Low");
|
|
541
|
+
assert.equal(by["top-cover"].value, "Yes");
|
|
542
|
+
assert.equal(by["top-cover"].attention, false);
|
|
543
|
+
assert.equal(by["dust-box"].attention, true, "dust box not fitted needs a person");
|
|
544
|
+
assert.equal(by["acc-time"].value, "10 min");
|
|
545
|
+
assert.equal(by.job.value, "job finished");
|
|
546
|
+
assert.equal(by.status.value, "Idle");
|
|
547
|
+
assert.equal(by.RouterSignalStrength.group, "Connection");
|
|
548
|
+
assert.equal(by.NewKey.group, "Other readings", "an undocumented reading still shows");
|
|
549
|
+
assert.equal("RobotIP" in by || "RouterIccid" in by, false, "network identifiers stay in Diagnose");
|
|
550
|
+
assert.equal("McuSW" in by, false, "'-' = not reported");
|
|
551
|
+
|
|
552
|
+
const t = ounitechTaskRecord({ taskId: 1, mapId: 5, source: "plan,1,x", subTypeName: "尘推", timeFrom: "2026-09-30 08:00:00" }, [], 480);
|
|
553
|
+
assert.equal(t.source, "Schedule");
|
|
554
|
+
assert.equal(t.mode, "Dust mop");
|
|
555
|
+
assert.equal(t.map, null);
|
|
556
|
+
assert.equal(t.start, "2026-09-30T00:00:00.000Z");
|
|
557
|
+
assert.equal(ounitechTime("garbage"), null);
|
|
558
|
+
assert.deepEqual(ounitechConsumable({ unitcode: "new_part_x", workminutes: 90 }), { part: "New part x", hours: 1.5, days: null });
|
|
559
|
+
assert.equal(ounitechPowerPlan({ dateList: [6, 7, 9], actionTime: "07:30", actionFlag: 1 }).action, "on");
|
|
560
|
+
assert.deepEqual(ounitechPowerPlan({ dateList: [6, 7, 9] }).days, [6, 7]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("records: 'no station bound (e=59)' means no base station - no error shown", async () => {
|
|
564
|
+
const r = await adapter().getRecords("OFFLINE1");
|
|
565
|
+
assert.deepEqual(r.station, { items: [] });
|
|
566
|
+
});
|
|
@@ -229,6 +229,9 @@ test("diagnose: every step passes on a healthy account, with the vendor's clock"
|
|
|
229
229
|
["cleaning schedule (get-plans-v2312)", true],
|
|
230
230
|
["cleaning records, last 7 days (get-task-list)", true],
|
|
231
231
|
["alerts, last 7 days (get-alert-list)", true],
|
|
232
|
+
["consumable hours (get-work-hours)", true],
|
|
233
|
+
["power schedule (halt-plan/get)", true],
|
|
234
|
+
["base station (get-station-detail-state)", true],
|
|
232
235
|
["lifts (get-lift-list)", true],
|
|
233
236
|
["current job (robot list taskState)", true],
|
|
234
237
|
]);
|