iobroker.proscenic 0.1.4 → 0.3.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/README.md CHANGED
@@ -2,9 +2,6 @@
2
2
 
3
3
  <img width="147" height="139" alt="image" src="https://github.com/user-attachments/assets/c5d6a8c4-e6e4-42d6-be61-2c79e37f5a60" />
4
4
 
5
-
6
-
7
-
8
5
  Development-stage ioBroker adapter for supported Proscenic vacuum robots,
9
6
  initially the Proscenic M7 Pro (`M7_PRO`, model `811_LDS`) through the legacy
10
7
  Proscenic cloud/gateway path.
@@ -22,30 +19,53 @@ Verified on Proscenic M7 Pro so far:
22
19
 
23
20
  <img width="1136" height="378" alt="image" src="https://github.com/user-attachments/assets/8a53c100-3ec4-41a0-8254-29cdf28c7000" />
24
21
 
25
-
26
22
  - cloud login, token acquisition, device discovery, gateway discovery, framed
27
23
  socket reception, decryption, bounded reconnect, and adapter unload cleanup;
28
24
  - status projection for battery, cleaning mode, fan, mop/water candidates,
29
25
  features, safe map metadata, and redacted maintenance diagnostics;
30
26
  - writable command buttons for start, pause, continue, return to station, fan
31
- modes, deep cleaning, and dust collection;
27
+ modes, deep cleaning, dust collection, and guarded saved-zone cleaning;
32
28
  - consumable counters for filter, side brush, main brush, and sensors;
33
29
  - recent maintenance/message history, including dust-bag messages;
34
30
  - local-development live-map rendering for VIS, including
35
- app-oriented map colors, no-go areas, room-zone candidates, charger marker,
36
- and an interpolated pose trail.
31
+ app-oriented map colors, saved zones, no-go areas, charger marker,
32
+ a task-scoped pose trail, and SVG animation between gateway positions. A
33
+ static PNG fallback and a same-origin iframe viewer URL remain available for
34
+ other visualization modes. In the iframe viewer, visible saved zones can be
35
+ selected directly on the map and started with an in-map action button.
37
36
 
38
37
  The adapter is an early public beta. The supported hardware claim is limited to
39
38
  the tested M7 Pro (`M7_PRO` / `811_LDS`) and the legacy cloud backend. Other
40
- models, accounts, regions, firmware versions, Tuya-local control, room-cleaning
41
- commands, and final map storage/retention policy still require separate
42
- evidence.
39
+ models, accounts, regions, firmware versions, Tuya-local control, final
40
+ real-device validation of zone cleaning, and map storage/retention policy still
41
+ require separate evidence.
43
42
 
44
43
  Raw payloads, serial numbers, gateway endpoints, device addresses, credentials,
45
44
  packet captures, private maps, and home-layout data are not committed or exposed
46
45
  as public fixtures. The current live-map object is explicitly experimental and
47
46
  intended for local VIS/debug use.
48
47
 
48
+ ## Saved-zone cleaning
49
+
50
+ `commands.zones.available` lists the currently selectable saved zones from the
51
+ authoritative 21004 catalog. Write a JSON array of one or more listed numeric
52
+ IDs to `commands.zones.selectedIds`, then trigger `commands.zones.start`.
53
+
54
+ The adapter refreshes and validates the catalog and active map before it sends
55
+ one partial-cleaning request. Empty, stale, unknown, duplicate, forbidden, or
56
+ malformed selections are rejected. REST acceptance is reported through the
57
+ normal `commands.last*` diagnostics and is followed by a bounded status
58
+ confirmation wait.
59
+
60
+ `map.live.showZoneOverlays` controls both visibility and interactive selection
61
+ in the `map.live.viewerUrl` iframe. When enabled, unselected zones are shown as
62
+ transparent white overlays with contrasting blue-gray boundaries and selected
63
+ zones as transparent light-green overlays. Clicking a valid zone toggles its ID in
64
+ `commands.zones.selectedIds`. As soon as at least one valid zone is selected,
65
+ the viewer shows a compact start action which writes to `commands.zones.start`.
66
+ Turning the switch off hides and disables the interactive overlays without
67
+ modifying upstream saved zones.
68
+
49
69
  ## Project identity
50
70
 
51
71
  - GitHub repository: `ioBroker.proscenic`
@@ -71,6 +91,40 @@ Security policy and contribution requirements are documented in
71
91
 
72
92
  ## Changelog
73
93
 
94
+ ### Unreleased
95
+
96
+ ### 0.3.0 (2026-09-20)
97
+
98
+ - Added a derived robot activity state for dashboards and strengthened command
99
+ delivery with bounded cloud-session recovery and explicit confirmation
100
+ diagnostics.
101
+ - Changed the primary experimental live-map image to an animated SVG while
102
+ retaining an explicit static PNG fallback.
103
+ - Prevented transient charging status reports during active cleaning from
104
+ erasing the current task's accumulated pose trail.
105
+ - Added `map.live.viewerUrl` with an interactive, same-origin live-map viewer
106
+ that updates without reloading its iframe, uses the configured canvas color,
107
+ and presents the centered map without an internal title bar.
108
+ - Added guarded consumable-reset button candidates with a fresh read before the
109
+ write, `21016` gateway confirmation, exact non-target counter validation, and
110
+ a persistent `21015` readback. The sensor reset has been validated on the
111
+ real device; the remaining component resets still need separate validation.
112
+ - Added guarded saved-zone cleaning through `commands.zones.selectedIds` and
113
+ `commands.zones.start`, with a fresh 21004 validation before the 30000
114
+ request. Added `map.live.showZoneOverlays` for VIS and direct click selection
115
+ plus a compact contextual start button in the same-origin map viewer. The
116
+ authoritative 21004 catalog now distinguishes selectable zones from
117
+ forbidden areas and keeps stale map selections from being executed.
118
+
119
+ ### 0.2.0 (2026-09-16)
120
+
121
+ - Added writable live-map color controls for the white background and blue map
122
+ area used by the rendered PNG.
123
+ - Preserved the live-map cleaning trail across room-to-room path changes during
124
+ the same cleaning task.
125
+ - Clarified device availability diagnostics and the distinction between cloud,
126
+ gateway, and last-known robot online states.
127
+
74
128
  ### 0.1.4 (2026-09-15)
75
129
 
76
130
  - Refresh existing ioBroker object metadata during adapter startup so role,
@@ -100,7 +154,7 @@ Security policy and contribution requirements are documented in
100
154
  hours.
101
155
  - Added bounded maintenance/message history objects for recent device messages.
102
156
  - Added experimental local live-map rendering for VIS debugging with app-oriented
103
- colors, no-go overlays, room-zone candidates, charger marker, and pose trail.
157
+ colors, no-go overlays, saved-zone overlays, charger marker, and pose trail.
104
158
  - Added the M7 Pro product image and a display-prefixed device code.
105
159
  - Improved command value normalization for visualization tools.
106
160
 
@@ -21,7 +21,9 @@ __export(commands_exports, {
21
21
  COMMAND_DEFINITIONS: () => COMMAND_DEFINITIONS,
22
22
  buildCommandRequest: () => buildCommandRequest,
23
23
  commandForStateId: () => commandForStateId,
24
- normalizeCommandButtonValue: () => normalizeCommandButtonValue
24
+ isStatusConfirmationForCommand: () => isStatusConfirmationForCommand,
25
+ normalizeCommandButtonValue: () => normalizeCommandButtonValue,
26
+ supportsStatusConfirmation: () => supportsStatusConfirmation
25
27
  });
26
28
  module.exports = __toCommonJS(commands_exports);
27
29
  const COMMAND_DEFINITIONS = [
@@ -39,6 +41,30 @@ function commandForStateId(id) {
39
41
  var _a;
40
42
  return (_a = COMMAND_DEFINITIONS.find((definition) => definition.id === id)) == null ? void 0 : _a.command;
41
43
  }
44
+ function supportsStatusConfirmation(command) {
45
+ return command !== "collectDust";
46
+ }
47
+ function isStatusConfirmationForCommand(command, status) {
48
+ switch (command) {
49
+ case "start":
50
+ case "continue":
51
+ case "deepCleaning":
52
+ case "zoneCleaning":
53
+ return status.mode === "sweep";
54
+ case "pause":
55
+ return status.mode === "pause";
56
+ case "return":
57
+ return status.mode === "backcharge" || status.mode === "charge" || status.mode === "fullcharge";
58
+ case "fanQuiet":
59
+ return status.fanMode === "quiet";
60
+ case "fanStandard":
61
+ return status.fanMode === "auto";
62
+ case "fanStrong":
63
+ return status.fanMode === "strong";
64
+ case "collectDust":
65
+ return false;
66
+ }
67
+ }
42
68
  function normalizeCommandButtonValue(value) {
43
69
  if (value === true || value === 1) {
44
70
  return true;
@@ -124,6 +150,8 @@ function buildCommandRequest(command, serial, username, now = /* @__PURE__ */ ne
124
150
  COMMAND_DEFINITIONS,
125
151
  buildCommandRequest,
126
152
  commandForStateId,
127
- normalizeCommandButtonValue
153
+ isStatusConfirmationForCommand,
154
+ normalizeCommandButtonValue,
155
+ supportsStatusConfirmation
128
156
  });
129
157
  //# sourceMappingURL=commands.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/commands.ts"],
4
- "sourcesContent": ["export type RobotCommand =\n\t| \"start\"\n\t| \"pause\"\n\t| \"continue\"\n\t| \"return\"\n\t| \"fanQuiet\"\n\t| \"fanStandard\"\n\t| \"fanStrong\"\n\t| \"deepCleaning\"\n\t| \"collectDust\";\n\nexport interface CommandRequest {\n\tpath: string;\n\tbody: string;\n\tcontentType?: string;\n}\n\nexport interface CommandDefinition {\n\tid: string;\n\tcommand: RobotCommand;\n}\n\nexport const COMMAND_DEFINITIONS: readonly CommandDefinition[] = [\n\t{ id: \"commands.start\", command: \"start\" },\n\t{ id: \"commands.pause\", command: \"pause\" },\n\t{ id: \"commands.continue\", command: \"continue\" },\n\t{ id: \"commands.return\", command: \"return\" },\n\t{ id: \"commands.fan.quiet\", command: \"fanQuiet\" },\n\t{ id: \"commands.fan.standard\", command: \"fanStandard\" },\n\t{ id: \"commands.fan.strong\", command: \"fanStrong\" },\n\t{ id: \"commands.deepCleaning\", command: \"deepCleaning\" },\n\t{ id: \"commands.collectDust\", command: \"collectDust\" },\n];\n\nexport function commandForStateId(id: string): RobotCommand | undefined {\n\treturn COMMAND_DEFINITIONS.find(definition => definition.id === id)?.command;\n}\n\nexport function normalizeCommandButtonValue(value: ioBroker.StateValue | undefined): boolean | undefined {\n\tif (value === true || value === 1) {\n\t\treturn true;\n\t}\n\n\tif (value === false || value === 0 || value === null) {\n\t\treturn false;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif (normalized === \"true\" || normalized === \"1\" || normalized === \"on\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (normalized === \"false\" || normalized === \"0\" || normalized === \"off\" || normalized === \"\") {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nexport function buildCommandRequest(\n\tcommand: RobotCommand,\n\tserial: string,\n\tusername: string,\n\tnow = new Date(),\n): CommandRequest {\n\tconst encodedSerial = encodeURIComponent(serial);\n\tconst encodedUsername = encodeURIComponent(username);\n\n\tswitch (command) {\n\t\tcase \"start\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd21005/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tbody: \"cleanMode=sweepOnly\",\n\t\t\t};\n\t\tcase \"pause\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,\n\t\t\t\tbody: \"mode=pause\",\n\t\t\t};\n\t\tcase \"continue\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,\n\t\t\t\tbody: \"pauseOrContinue=continue\",\n\t\t\t};\n\t\tcase \"return\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21012?username=${encodedUsername}`,\n\t\t\t\tbody: \"charge=start\",\n\t\t\t};\n\t\tcase \"fanQuiet\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=quiet\",\n\t\t\t};\n\t\tcase \"fanStandard\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=auto\",\n\t\t\t};\n\t\tcase \"fanStrong\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=strong\",\n\t\t\t};\n\t\tcase \"deepCleaning\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd21005_2/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tbody: \"mode=depthTotalClean\",\n\t\t\t};\n\t\tcase \"collectDust\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tcontentType: \"application/json;charset=UTF-8\",\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tdInfo: {\n\t\t\t\t\t\tts: now.getTime().toString(),\n\t\t\t\t\t\tuserId: username,\n\t\t\t\t\t},\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tcmd: \"startDustCenter\",\n\t\t\t\t\t\tvalue: 0,\n\t\t\t\t\t},\n\t\t\t\t\tinfoType: 21024,\n\t\t\t\t}),\n\t\t\t};\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type RobotCommand =\n\t| \"start\"\n\t| \"pause\"\n\t| \"continue\"\n\t| \"return\"\n\t| \"fanQuiet\"\n\t| \"fanStandard\"\n\t| \"fanStrong\"\n\t| \"deepCleaning\"\n\t| \"collectDust\";\n\nexport type ConfirmableRobotCommand = RobotCommand | \"zoneCleaning\";\n\nexport interface CommandRequest {\n\tpath: string;\n\tbody: string;\n\tcontentType?: string;\n}\n\nexport interface CommandDefinition {\n\tid: string;\n\tcommand: RobotCommand;\n}\n\nexport interface CommandStatusSnapshot {\n\tmode?: string;\n\tfanMode?: string;\n}\n\nexport const COMMAND_DEFINITIONS: readonly CommandDefinition[] = [\n\t{ id: \"commands.start\", command: \"start\" },\n\t{ id: \"commands.pause\", command: \"pause\" },\n\t{ id: \"commands.continue\", command: \"continue\" },\n\t{ id: \"commands.return\", command: \"return\" },\n\t{ id: \"commands.fan.quiet\", command: \"fanQuiet\" },\n\t{ id: \"commands.fan.standard\", command: \"fanStandard\" },\n\t{ id: \"commands.fan.strong\", command: \"fanStrong\" },\n\t{ id: \"commands.deepCleaning\", command: \"deepCleaning\" },\n\t{ id: \"commands.collectDust\", command: \"collectDust\" },\n];\n\nexport function commandForStateId(id: string): RobotCommand | undefined {\n\treturn COMMAND_DEFINITIONS.find(definition => definition.id === id)?.command;\n}\n\nexport function supportsStatusConfirmation(command: ConfirmableRobotCommand): boolean {\n\treturn command !== \"collectDust\";\n}\n\nexport function isStatusConfirmationForCommand(\n\tcommand: ConfirmableRobotCommand,\n\tstatus: CommandStatusSnapshot,\n): boolean {\n\tswitch (command) {\n\t\tcase \"start\":\n\t\tcase \"continue\":\n\t\tcase \"deepCleaning\":\n\t\tcase \"zoneCleaning\":\n\t\t\treturn status.mode === \"sweep\";\n\t\tcase \"pause\":\n\t\t\treturn status.mode === \"pause\";\n\t\tcase \"return\":\n\t\t\treturn status.mode === \"backcharge\" || status.mode === \"charge\" || status.mode === \"fullcharge\";\n\t\tcase \"fanQuiet\":\n\t\t\treturn status.fanMode === \"quiet\";\n\t\tcase \"fanStandard\":\n\t\t\treturn status.fanMode === \"auto\";\n\t\tcase \"fanStrong\":\n\t\t\treturn status.fanMode === \"strong\";\n\t\tcase \"collectDust\":\n\t\t\treturn false;\n\t}\n}\n\nexport function normalizeCommandButtonValue(value: ioBroker.StateValue | undefined): boolean | undefined {\n\tif (value === true || value === 1) {\n\t\treturn true;\n\t}\n\n\tif (value === false || value === 0 || value === null) {\n\t\treturn false;\n\t}\n\n\tif (typeof value === \"string\") {\n\t\tconst normalized = value.trim().toLowerCase();\n\t\tif (normalized === \"true\" || normalized === \"1\" || normalized === \"on\") {\n\t\t\treturn true;\n\t\t}\n\t\tif (normalized === \"false\" || normalized === \"0\" || normalized === \"off\" || normalized === \"\") {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\nexport function buildCommandRequest(\n\tcommand: RobotCommand,\n\tserial: string,\n\tusername: string,\n\tnow = new Date(),\n): CommandRequest {\n\tconst encodedSerial = encodeURIComponent(serial);\n\tconst encodedUsername = encodeURIComponent(username);\n\n\tswitch (command) {\n\t\tcase \"start\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd21005/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tbody: \"cleanMode=sweepOnly\",\n\t\t\t};\n\t\tcase \"pause\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,\n\t\t\t\tbody: \"mode=pause\",\n\t\t\t};\n\t\tcase \"continue\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,\n\t\t\t\tbody: \"pauseOrContinue=continue\",\n\t\t\t};\n\t\tcase \"return\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21012?username=${encodedUsername}`,\n\t\t\t\tbody: \"charge=start\",\n\t\t\t};\n\t\tcase \"fanQuiet\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=quiet\",\n\t\t\t};\n\t\tcase \"fanStandard\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=auto\",\n\t\t\t};\n\t\tcase \"fanStrong\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,\n\t\t\t\tbody: \"setMode=strong\",\n\t\t\t};\n\t\tcase \"deepCleaning\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd21005_2/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tbody: \"mode=depthTotalClean\",\n\t\t\t};\n\t\tcase \"collectDust\":\n\t\t\treturn {\n\t\t\t\tpath: `/instructions/cmd/${encodedSerial}?username=${encodedUsername}`,\n\t\t\t\tcontentType: \"application/json;charset=UTF-8\",\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tdInfo: {\n\t\t\t\t\t\tts: now.getTime().toString(),\n\t\t\t\t\t\tuserId: username,\n\t\t\t\t\t},\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tcmd: \"startDustCenter\",\n\t\t\t\t\t\tvalue: 0,\n\t\t\t\t\t},\n\t\t\t\t\tinfoType: 21024,\n\t\t\t\t}),\n\t\t\t};\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -18,15 +18,89 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var consumables_exports = {};
20
20
  __export(consumables_exports, {
21
- normalizeConsumables21015: () => normalizeConsumables21015
21
+ CONSUMABLE_RESET_DEFINITIONS: () => CONSUMABLE_RESET_DEFINITIONS,
22
+ buildConsumableResetRequest: () => buildConsumableResetRequest,
23
+ consumableCounterSnapshotsEqual: () => consumableCounterSnapshotsEqual,
24
+ consumableResetForStateId: () => consumableResetForStateId,
25
+ isConsumableResetConfirmation: () => isConsumableResetConfirmation,
26
+ normalizeConsumableCounterSnapshot: () => normalizeConsumableCounterSnapshot,
27
+ normalizeConsumables21015: () => normalizeConsumables21015,
28
+ runSafeConsumableReset: () => runSafeConsumableReset
22
29
  });
23
30
  module.exports = __toCommonJS(consumables_exports);
31
+ const CONSUMABLE_RESET_DEFINITIONS = [
32
+ { id: "consumables.filter.reset", component: "filter", name: "Reset filter runtime" },
33
+ { id: "consumables.sideBrush.reset", component: "sideBrush", name: "Reset side brush runtime" },
34
+ { id: "consumables.mainBrush.reset", component: "mainBrush", name: "Reset main brush runtime" },
35
+ { id: "consumables.sensors.reset", component: "sensors", name: "Reset sensor runtime" }
36
+ ];
24
37
  const CONSUMABLE_INTERVAL_HOURS = {
25
38
  filter: 150,
26
39
  sideBrush: 200,
27
40
  mainBrush: 300,
28
41
  sensors: 30
29
42
  };
43
+ const CONSUMABLE_COUNTER_FIELDS = [
44
+ "filter",
45
+ "mainBrush",
46
+ "sideBrush",
47
+ "sensors",
48
+ "battery"
49
+ ];
50
+ function consumableResetForStateId(id) {
51
+ var _a;
52
+ return (_a = CONSUMABLE_RESET_DEFINITIONS.find((definition) => definition.id === id)) == null ? void 0 : _a.component;
53
+ }
54
+ function normalizeConsumableCounterSnapshot(data) {
55
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
56
+ return void 0;
57
+ }
58
+ const record = data;
59
+ const keys = Object.keys(record);
60
+ if (keys.length !== CONSUMABLE_COUNTER_FIELDS.length || keys.some((key) => !CONSUMABLE_COUNTER_FIELDS.includes(key))) {
61
+ return void 0;
62
+ }
63
+ const entries = CONSUMABLE_COUNTER_FIELDS.map((field) => [field, record[field]]);
64
+ if (entries.some(([, value]) => typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)) {
65
+ return void 0;
66
+ }
67
+ return Object.fromEntries(entries);
68
+ }
69
+ function buildConsumableResetRequest(component, snapshot, serial, username) {
70
+ return {
71
+ path: `/instructions/cmd21016/${encodeURIComponent(serial)}?username=${encodeURIComponent(username)}`,
72
+ contentType: "application/json;charset=UTF-8",
73
+ body: JSON.stringify({ ...snapshot, [component]: 0 })
74
+ };
75
+ }
76
+ function isConsumableResetConfirmation(before, after, component) {
77
+ return CONSUMABLE_COUNTER_FIELDS.every((field) => after[field] === (field === component ? 0 : before[field]));
78
+ }
79
+ function consumableCounterSnapshotsEqual(expected, actual) {
80
+ return CONSUMABLE_COUNTER_FIELDS.every((field) => actual[field] === expected[field]);
81
+ }
82
+ async function runSafeConsumableReset(component, operations) {
83
+ await operations.onProgress("reading-before-reset");
84
+ const before = await operations.readSnapshot();
85
+ if (before[component] === 0) {
86
+ await operations.onProgress("already-zero");
87
+ return "already-zero";
88
+ }
89
+ await operations.onProgress("sending");
90
+ const echoed = await operations.sendReset(before);
91
+ if (!isConsumableResetConfirmation(before, echoed, component)) {
92
+ throw new Error("Consumable reset echo changed unexpected counters or did not reset the target");
93
+ }
94
+ await operations.onProgress("echo-confirmed");
95
+ await operations.onProgress("verifying-readback");
96
+ const persisted = await operations.readSnapshot();
97
+ const expected = { ...before, [component]: 0 };
98
+ if (!consumableCounterSnapshotsEqual(expected, persisted)) {
99
+ throw new Error("Consumable reset did not persist exactly across the verification read");
100
+ }
101
+ await operations.onProgress("verified");
102
+ return "verified";
103
+ }
30
104
  function normalizeConsumables21015(data) {
31
105
  if (data === null || typeof data !== "object" || Array.isArray(data)) {
32
106
  return void 0;
@@ -58,6 +132,13 @@ function roundToTwoDecimals(value) {
58
132
  }
59
133
  // Annotate the CommonJS export names for ESM import in node:
60
134
  0 && (module.exports = {
61
- normalizeConsumables21015
135
+ CONSUMABLE_RESET_DEFINITIONS,
136
+ buildConsumableResetRequest,
137
+ consumableCounterSnapshotsEqual,
138
+ consumableResetForStateId,
139
+ isConsumableResetConfirmation,
140
+ normalizeConsumableCounterSnapshot,
141
+ normalizeConsumables21015,
142
+ runSafeConsumableReset
62
143
  });
63
144
  //# sourceMappingURL=consumables.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/consumables.ts"],
4
- "sourcesContent": ["export type ConsumableComponent = \"filter\" | \"sideBrush\" | \"mainBrush\" | \"sensors\";\n\nexport interface ConsumableState {\n\tusedSeconds: number;\n\tintervalHours: number;\n\tremainingPercent: number;\n\toverdueHours: number;\n}\n\nexport type ConsumableStates = Partial<Record<ConsumableComponent, ConsumableState>>;\n\nconst CONSUMABLE_INTERVAL_HOURS: Record<ConsumableComponent, number> = {\n\tfilter: 150,\n\tsideBrush: 200,\n\tmainBrush: 300,\n\tsensors: 30,\n};\n\nexport function normalizeConsumables21015(data: unknown): ConsumableStates | undefined {\n\tif (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n\t\treturn undefined;\n\t}\n\n\tconst record = data as Record<string, unknown>;\n\tconst consumables = Object.fromEntries(\n\t\t(Object.keys(CONSUMABLE_INTERVAL_HOURS) as ConsumableComponent[])\n\t\t\t.map(component => [component, normalizeConsumable(component, record[component])] as const)\n\t\t\t.filter((entry): entry is readonly [ConsumableComponent, ConsumableState] => entry[1] !== undefined),\n\t) as ConsumableStates;\n\n\treturn Object.keys(consumables).length > 0 ? consumables : undefined;\n}\n\nfunction normalizeConsumable(component: ConsumableComponent, value: unknown): ConsumableState | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\n\tconst usedSeconds = Math.trunc(value);\n\tconst intervalHours = CONSUMABLE_INTERVAL_HOURS[component];\n\tconst intervalSeconds = intervalHours * 3_600;\n\tconst remainingPercent = roundToTwoDecimals((1 - usedSeconds / intervalSeconds) * 100);\n\tconst overdueHours = roundToTwoDecimals(Math.max(0, (usedSeconds - intervalSeconds) / 3_600));\n\n\treturn {\n\t\tusedSeconds,\n\t\tintervalHours,\n\t\tremainingPercent,\n\t\toverdueHours,\n\t};\n}\n\nfunction roundToTwoDecimals(value: number): number {\n\treturn Math.round(value * 100) / 100;\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,MAAM,4BAAiE;AAAA,EACtE,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,WAAW;AAAA,EACX,SAAS;AACV;AAEO,SAAS,0BAA0B,MAA6C;AACtF,MAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;AACrE,WAAO;AAAA,EACR;AAEA,QAAM,SAAS;AACf,QAAM,cAAc,OAAO;AAAA,IACzB,OAAO,KAAK,yBAAyB,EACpC,IAAI,eAAa,CAAC,WAAW,oBAAoB,WAAW,OAAO,SAAS,CAAC,CAAC,CAAU,EACxF,OAAO,CAAC,UAAoE,MAAM,CAAC,MAAM,MAAS;AAAA,EACrG;AAEA,SAAO,OAAO,KAAK,WAAW,EAAE,SAAS,IAAI,cAAc;AAC5D;AAEA,SAAS,oBAAoB,WAAgC,OAA6C;AACzG,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG;AACtE,WAAO;AAAA,EACR;AAEA,QAAM,cAAc,KAAK,MAAM,KAAK;AACpC,QAAM,gBAAgB,0BAA0B,SAAS;AACzD,QAAM,kBAAkB,gBAAgB;AACxC,QAAM,mBAAmB,oBAAoB,IAAI,cAAc,mBAAmB,GAAG;AACrF,QAAM,eAAe,mBAAmB,KAAK,IAAI,IAAI,cAAc,mBAAmB,IAAK,CAAC;AAE5F,SAAO;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AACD;AAEA,SAAS,mBAAmB,OAAuB;AAClD,SAAO,KAAK,MAAM,QAAQ,GAAG,IAAI;AAClC;",
4
+ "sourcesContent": ["import type { CommandRequest } from \"./commands\";\n\nexport type ConsumableComponent = \"filter\" | \"sideBrush\" | \"mainBrush\" | \"sensors\";\n\nexport type ConsumableCounterField = ConsumableComponent | \"battery\";\n\nexport type ConsumableCounterSnapshot = Record<ConsumableCounterField, number>;\n\nexport interface ConsumableResetDefinition {\n\tid: string;\n\tcomponent: ConsumableComponent;\n\tname: string;\n}\n\nexport type ConsumableResetResult =\n\t\"reading-before-reset\" | \"sending\" | \"echo-confirmed\" | \"verifying-readback\" | \"verified\" | \"already-zero\";\n\nexport interface SafeConsumableResetOperations {\n\treadSnapshot: () => Promise<ConsumableCounterSnapshot>;\n\tsendReset: (before: ConsumableCounterSnapshot) => Promise<ConsumableCounterSnapshot>;\n\tonProgress: (result: ConsumableResetResult) => Promise<void>;\n}\n\nexport const CONSUMABLE_RESET_DEFINITIONS: readonly ConsumableResetDefinition[] = [\n\t{ id: \"consumables.filter.reset\", component: \"filter\", name: \"Reset filter runtime\" },\n\t{ id: \"consumables.sideBrush.reset\", component: \"sideBrush\", name: \"Reset side brush runtime\" },\n\t{ id: \"consumables.mainBrush.reset\", component: \"mainBrush\", name: \"Reset main brush runtime\" },\n\t{ id: \"consumables.sensors.reset\", component: \"sensors\", name: \"Reset sensor runtime\" },\n];\n\nexport interface ConsumableState {\n\tusedSeconds: number;\n\tintervalHours: number;\n\tremainingPercent: number;\n\toverdueHours: number;\n}\n\nexport type ConsumableStates = Partial<Record<ConsumableComponent, ConsumableState>>;\n\nconst CONSUMABLE_INTERVAL_HOURS: Record<ConsumableComponent, number> = {\n\tfilter: 150,\n\tsideBrush: 200,\n\tmainBrush: 300,\n\tsensors: 30,\n};\n\nconst CONSUMABLE_COUNTER_FIELDS: readonly ConsumableCounterField[] = [\n\t\"filter\",\n\t\"mainBrush\",\n\t\"sideBrush\",\n\t\"sensors\",\n\t\"battery\",\n];\n\nexport function consumableResetForStateId(id: string): ConsumableComponent | undefined {\n\treturn CONSUMABLE_RESET_DEFINITIONS.find(definition => definition.id === id)?.component;\n}\n\nexport function normalizeConsumableCounterSnapshot(data: unknown): ConsumableCounterSnapshot | undefined {\n\tif (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n\t\treturn undefined;\n\t}\n\n\tconst record = data as Record<string, unknown>;\n\tconst keys = Object.keys(record);\n\tif (\n\t\tkeys.length !== CONSUMABLE_COUNTER_FIELDS.length ||\n\t\tkeys.some(key => !CONSUMABLE_COUNTER_FIELDS.includes(key as ConsumableCounterField))\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst entries = CONSUMABLE_COUNTER_FIELDS.map(field => [field, record[field]] as const);\n\tif (entries.some(([, value]) => typeof value !== \"number\" || !Number.isSafeInteger(value) || value < 0)) {\n\t\treturn undefined;\n\t}\n\n\treturn Object.fromEntries(entries) as ConsumableCounterSnapshot;\n}\n\nexport function buildConsumableResetRequest(\n\tcomponent: ConsumableComponent,\n\tsnapshot: ConsumableCounterSnapshot,\n\tserial: string,\n\tusername: string,\n): CommandRequest {\n\treturn {\n\t\tpath: `/instructions/cmd21016/${encodeURIComponent(serial)}?username=${encodeURIComponent(username)}`,\n\t\tcontentType: \"application/json;charset=UTF-8\",\n\t\tbody: JSON.stringify({ ...snapshot, [component]: 0 }),\n\t};\n}\n\nexport function isConsumableResetConfirmation(\n\tbefore: ConsumableCounterSnapshot,\n\tafter: ConsumableCounterSnapshot,\n\tcomponent: ConsumableComponent,\n): boolean {\n\treturn CONSUMABLE_COUNTER_FIELDS.every(field => after[field] === (field === component ? 0 : before[field]));\n}\n\nexport function consumableCounterSnapshotsEqual(\n\texpected: ConsumableCounterSnapshot,\n\tactual: ConsumableCounterSnapshot,\n): boolean {\n\treturn CONSUMABLE_COUNTER_FIELDS.every(field => actual[field] === expected[field]);\n}\n\nexport async function runSafeConsumableReset(\n\tcomponent: ConsumableComponent,\n\toperations: SafeConsumableResetOperations,\n): Promise<\"verified\" | \"already-zero\"> {\n\tawait operations.onProgress(\"reading-before-reset\");\n\tconst before = await operations.readSnapshot();\n\tif (before[component] === 0) {\n\t\tawait operations.onProgress(\"already-zero\");\n\t\treturn \"already-zero\";\n\t}\n\n\tawait operations.onProgress(\"sending\");\n\tconst echoed = await operations.sendReset(before);\n\tif (!isConsumableResetConfirmation(before, echoed, component)) {\n\t\tthrow new Error(\"Consumable reset echo changed unexpected counters or did not reset the target\");\n\t}\n\n\tawait operations.onProgress(\"echo-confirmed\");\n\tawait operations.onProgress(\"verifying-readback\");\n\tconst persisted = await operations.readSnapshot();\n\tconst expected = { ...before, [component]: 0 };\n\tif (!consumableCounterSnapshotsEqual(expected, persisted)) {\n\t\tthrow new Error(\"Consumable reset did not persist exactly across the verification read\");\n\t}\n\n\tawait operations.onProgress(\"verified\");\n\treturn \"verified\";\n}\n\nexport function normalizeConsumables21015(data: unknown): ConsumableStates | undefined {\n\tif (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n\t\treturn undefined;\n\t}\n\n\tconst record = data as Record<string, unknown>;\n\tconst consumables = Object.fromEntries(\n\t\t(Object.keys(CONSUMABLE_INTERVAL_HOURS) as ConsumableComponent[])\n\t\t\t.map(component => [component, normalizeConsumable(component, record[component])] as const)\n\t\t\t.filter((entry): entry is readonly [ConsumableComponent, ConsumableState] => entry[1] !== undefined),\n\t) as ConsumableStates;\n\n\treturn Object.keys(consumables).length > 0 ? consumables : undefined;\n}\n\nfunction normalizeConsumable(component: ConsumableComponent, value: unknown): ConsumableState | undefined {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\treturn undefined;\n\t}\n\n\tconst usedSeconds = Math.trunc(value);\n\tconst intervalHours = CONSUMABLE_INTERVAL_HOURS[component];\n\tconst intervalSeconds = intervalHours * 3_600;\n\tconst remainingPercent = roundToTwoDecimals((1 - usedSeconds / intervalSeconds) * 100);\n\tconst overdueHours = roundToTwoDecimals(Math.max(0, (usedSeconds - intervalSeconds) / 3_600));\n\n\treturn {\n\t\tusedSeconds,\n\t\tintervalHours,\n\t\tremainingPercent,\n\t\toverdueHours,\n\t};\n}\n\nfunction roundToTwoDecimals(value: number): number {\n\treturn Math.round(value * 100) / 100;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }