iobroker.proscenic 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/admin/i18n/de.json +9 -0
  4. package/admin/i18n/en.json +9 -0
  5. package/admin/i18n/es.json +9 -0
  6. package/admin/i18n/fr.json +9 -0
  7. package/admin/i18n/it.json +9 -0
  8. package/admin/i18n/nl.json +9 -0
  9. package/admin/i18n/pl.json +9 -0
  10. package/admin/i18n/pt.json +9 -0
  11. package/admin/i18n/ru.json +9 -0
  12. package/admin/i18n/uk.json +9 -0
  13. package/admin/i18n/zh-cn.json +9 -0
  14. package/admin/jsonConfig.json +85 -0
  15. package/admin/proscenic-m7-pro.png +0 -0
  16. package/admin/proscenic.png +0 -0
  17. package/build/domain/commands.js +129 -0
  18. package/build/domain/commands.js.map +7 -0
  19. package/build/domain/consumables.js +63 -0
  20. package/build/domain/consumables.js.map +7 -0
  21. package/build/domain/live-map.js +829 -0
  22. package/build/domain/live-map.js.map +7 -0
  23. package/build/domain/maintenance-message.js +128 -0
  24. package/build/domain/maintenance-message.js.map +7 -0
  25. package/build/domain/map.js +58 -0
  26. package/build/domain/map.js.map +7 -0
  27. package/build/domain/reconnect-policy.js +45 -0
  28. package/build/domain/reconnect-policy.js.map +7 -0
  29. package/build/domain/status.js +108 -0
  30. package/build/domain/status.js.map +7 -0
  31. package/build/main.js +529 -0
  32. package/build/main.js.map +7 -0
  33. package/build/objects/object-definitions.js +214 -0
  34. package/build/objects/object-definitions.js.map +7 -0
  35. package/build/objects/projector.js +231 -0
  36. package/build/objects/projector.js.map +7 -0
  37. package/build/protocol/crypto.js +81 -0
  38. package/build/protocol/crypto.js.map +7 -0
  39. package/build/protocol/gateway-client.js +136 -0
  40. package/build/protocol/gateway-client.js.map +7 -0
  41. package/build/protocol/rest-client.js +184 -0
  42. package/build/protocol/rest-client.js.map +7 -0
  43. package/build/protocol/types.js +17 -0
  44. package/build/protocol/types.js.map +7 -0
  45. package/io-package.json +125 -0
  46. package/package.json +80 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 C@ptain Ch@os
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # ioBroker.proscenic
2
+
3
+ Development-stage ioBroker adapter for supported Proscenic vacuum robots,
4
+ initially the Proscenic M7 Pro (`M7_PRO`, model `811_LDS`) through the legacy
5
+ Proscenic cloud/gateway path.
6
+
7
+ Manufacturer/product information: [Proscenic robot vacuums](https://www.proscenic.com/collections/robot-vacuums).
8
+
9
+ ## Project status
10
+
11
+ This repository contains a TypeScript ioBroker adapter that can authenticate
12
+ against the verified legacy European Proscenic cloud, enumerate one real M7 Pro,
13
+ connect to the cloud gateway, project normalized runtime states, and expose the
14
+ first real-device-validated control and maintenance objects.
15
+
16
+ Verified on one owner-local Proscenic M7 Pro so far:
17
+
18
+ - cloud login, token acquisition, device discovery, gateway discovery, framed
19
+ socket reception, decryption, bounded reconnect, and adapter unload cleanup;
20
+ - status projection for battery, cleaning mode, fan, mop/water candidates,
21
+ features, safe map metadata, and redacted maintenance diagnostics;
22
+ - writable command buttons for start, pause, continue, return to station, fan
23
+ modes, deep cleaning, and dust collection;
24
+ - consumable counters for filter, side brush, main brush, and sensors;
25
+ - recent maintenance/message history, including dust-bag messages;
26
+ - experimental local-development live-map rendering for VIS debugging, including
27
+ app-oriented map colors, no-go areas, room-zone candidates, charger marker,
28
+ and an interpolated pose trail.
29
+
30
+ The adapter is not a public release yet. The supported hardware claim is limited
31
+ to the tested M7 Pro (`M7_PRO` / `811_LDS`) and the legacy cloud backend. Other
32
+ models, accounts, regions, firmware versions, Tuya-local control, room-cleaning
33
+ commands, final map storage/retention policy, and production release readiness
34
+ still require separate evidence.
35
+
36
+ Raw payloads, serial numbers, gateway endpoints, device addresses, credentials,
37
+ packet captures, private maps, and home-layout data are not committed or exposed
38
+ as public fixtures. The current live-map object is explicitly experimental and
39
+ intended for local VIS/debug use.
40
+
41
+ ## Project identity
42
+
43
+ - GitHub repository: `ioBroker.proscenic`
44
+ - npm package: `iobroker.proscenic`
45
+ - adapter ID: `proscenic`
46
+ - namespace: `proscenic.<instance>`
47
+ - license: MIT
48
+
49
+ ## Security and privacy
50
+
51
+ Never publish Proscenic credentials, session tokens, serial numbers, device or
52
+ gateway addresses, private network data, maps, packet captures, or logs.
53
+ Security policy and contribution requirements are documented in
54
+ [SECURITY.md](SECURITY.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
55
+
56
+ ## Documentation
57
+
58
+ - [Project definition](docs/PROJECT_DEFINITION.md)
59
+ - [Architecture](docs/ARCHITECTURE.md)
60
+ - [Upstream research and provenance](docs/UPSTREAM_RESEARCH.md)
61
+ - [Publication checklist](docs/PUBLICATION_CHECKLIST.md)
62
+ - [Architecture decisions](docs/decisions/README.md)
63
+
64
+ ## Changelog
65
+
66
+ ### 0.1.0 (2026-09-14)
67
+
68
+ - Added the first real-device-validated command object tree for the M7 Pro:
69
+ start, pause, continue, return, fan modes, deep cleaning, and dust collection.
70
+ - Added status projection for the verified legacy Proscenic cloud/gateway path.
71
+ - Added consumable runtime objects with derived remaining percentage and overdue
72
+ hours.
73
+ - Added bounded maintenance/message history objects for recent device messages.
74
+ - Added experimental local live-map rendering for VIS debugging with app-oriented
75
+ colors, no-go overlays, room-zone candidates, charger marker, and pose trail.
76
+ - Added the M7 Pro product image and a display-prefixed device code.
77
+ - Improved command value normalization for visualization tools.
78
+
79
+ ### 0.0.1 (2026-09-10)
80
+
81
+ - Initial adapter skeleton and project foundation.
82
+
83
+ ## Independence
84
+
85
+ This is an independent interoperability project and is not affiliated with,
86
+ endorsed by, or sponsored by Proscenic. Product and company names belong to
87
+ their respective owners.
88
+
89
+ ## License
90
+
91
+ MIT License. See [LICENSE](LICENSE).
92
+
93
+ Copyright (c) 2026 C@ptain Ch@os
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic Adaptereinstellungen",
3
+ "region": "Region",
4
+ "appVendor": "App-Anbieter",
5
+ "cloudEmail": "Cloud-E-Mail-Adresse",
6
+ "cloudPassword": "Cloud-Passwort",
7
+ "preferredDeviceCode": "Bevorzugter Gerätecode",
8
+ "preferredDeviceCodeHelp": "Optionaler Filter für das erste unterstützte Zielgerät. Für den verifizierten Proscenic M7 Pro M7_PRO beibehalten."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Optional filter for the first supported target. Keep M7_PRO for the verified Proscenic M7 Pro."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Filtro opcional para el primer dispositivo compatible. Mantén M7_PRO para el Proscenic M7 Pro verificado."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Filtre optionnel pour le premier appareil pris en charge. Conservez M7_PRO pour le Proscenic M7 Pro vérifié."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Filtro opzionale per il primo dispositivo supportato. Mantieni M7_PRO per il Proscenic M7 Pro verificato."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Optioneel filter voor het eerste ondersteunde apparaat. Laat M7_PRO staan voor de geverifieerde Proscenic M7 Pro."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Opcjonalny filtr dla pierwszego obsługiwanego urządzenia. Pozostaw M7_PRO dla zweryfikowanego Proscenic M7 Pro."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Filtro opcional para o primeiro dispositivo compatível. Mantenha M7_PRO para o Proscenic M7 Pro verificado."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Необязательный фильтр для первого поддерживаемого устройства. Оставьте M7_PRO для проверенного Proscenic M7 Pro."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "Необов’язковий фільтр для першого підтримуваного пристрою. Залиште M7_PRO для перевіреного Proscenic M7 Pro."
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "proscenic adapter settings": "Proscenic adapter settings",
3
+ "region": "Region",
4
+ "appVendor": "App vendor",
5
+ "cloudEmail": "Cloud email address",
6
+ "cloudPassword": "Cloud password",
7
+ "preferredDeviceCode": "Preferred device code",
8
+ "preferredDeviceCodeHelp": "第一个受支持设备的可选筛选条件。已验证的 Proscenic M7 Pro 请保留 M7_PRO。"
9
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "i18n": true,
3
+ "type": "panel",
4
+ "items": {
5
+ "adapterLogo": {
6
+ "type": "staticImage",
7
+ "src": "proscenic.png",
8
+ "newLine": true,
9
+ "xs": 12,
10
+ "sm": 12,
11
+ "md": 12,
12
+ "lg": 12,
13
+ "xl": 12,
14
+ "style": {
15
+ "width": 140,
16
+ "height": 140,
17
+ "objectFit": "contain",
18
+ "margin": "8px 0 16px 0"
19
+ }
20
+ },
21
+ "region": {
22
+ "type": "select",
23
+ "label": "region",
24
+ "options": [
25
+ {
26
+ "label": "Europe",
27
+ "value": "eu"
28
+ }
29
+ ],
30
+ "newLine": true,
31
+ "xs": 12,
32
+ "sm": 12,
33
+ "md": 6,
34
+ "lg": 4,
35
+ "xl": 4
36
+ },
37
+ "vendor": {
38
+ "type": "select",
39
+ "label": "appVendor",
40
+ "options": [
41
+ {
42
+ "label": "Proscenic",
43
+ "value": "proscenic"
44
+ }
45
+ ],
46
+ "newLine": false,
47
+ "xs": 12,
48
+ "sm": 12,
49
+ "md": 6,
50
+ "lg": 4,
51
+ "xl": 4
52
+ },
53
+ "username": {
54
+ "type": "text",
55
+ "label": "cloudEmail",
56
+ "newLine": true,
57
+ "xs": 12,
58
+ "sm": 12,
59
+ "md": 12,
60
+ "lg": 6,
61
+ "xl": 6
62
+ },
63
+ "password": {
64
+ "type": "password",
65
+ "label": "cloudPassword",
66
+ "newLine": false,
67
+ "xs": 12,
68
+ "sm": 12,
69
+ "md": 12,
70
+ "lg": 6,
71
+ "xl": 6
72
+ },
73
+ "deviceCode": {
74
+ "type": "text",
75
+ "label": "preferredDeviceCode",
76
+ "help": "preferredDeviceCodeHelp",
77
+ "newLine": true,
78
+ "xs": 12,
79
+ "sm": 12,
80
+ "md": 12,
81
+ "lg": 6,
82
+ "xl": 6
83
+ }
84
+ }
85
+ }
Binary file
Binary file
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var commands_exports = {};
20
+ __export(commands_exports, {
21
+ COMMAND_DEFINITIONS: () => COMMAND_DEFINITIONS,
22
+ buildCommandRequest: () => buildCommandRequest,
23
+ commandForStateId: () => commandForStateId,
24
+ normalizeCommandButtonValue: () => normalizeCommandButtonValue
25
+ });
26
+ module.exports = __toCommonJS(commands_exports);
27
+ const COMMAND_DEFINITIONS = [
28
+ { id: "commands.start", command: "start" },
29
+ { id: "commands.pause", command: "pause" },
30
+ { id: "commands.continue", command: "continue" },
31
+ { id: "commands.return", command: "return" },
32
+ { id: "commands.fan.quiet", command: "fanQuiet" },
33
+ { id: "commands.fan.standard", command: "fanStandard" },
34
+ { id: "commands.fan.strong", command: "fanStrong" },
35
+ { id: "commands.deepCleaning", command: "deepCleaning" },
36
+ { id: "commands.collectDust", command: "collectDust" }
37
+ ];
38
+ function commandForStateId(id) {
39
+ var _a;
40
+ return (_a = COMMAND_DEFINITIONS.find((definition) => definition.id === id)) == null ? void 0 : _a.command;
41
+ }
42
+ function normalizeCommandButtonValue(value) {
43
+ if (value === true || value === 1) {
44
+ return true;
45
+ }
46
+ if (value === false || value === 0 || value === null) {
47
+ return false;
48
+ }
49
+ if (typeof value === "string") {
50
+ const normalized = value.trim().toLowerCase();
51
+ if (normalized === "true" || normalized === "1" || normalized === "on") {
52
+ return true;
53
+ }
54
+ if (normalized === "false" || normalized === "0" || normalized === "off" || normalized === "") {
55
+ return false;
56
+ }
57
+ }
58
+ return void 0;
59
+ }
60
+ function buildCommandRequest(command, serial, username, now = /* @__PURE__ */ new Date()) {
61
+ const encodedSerial = encodeURIComponent(serial);
62
+ const encodedUsername = encodeURIComponent(username);
63
+ switch (command) {
64
+ case "start":
65
+ return {
66
+ path: `/instructions/cmd21005/${encodedSerial}?username=${encodedUsername}`,
67
+ body: "cleanMode=sweepOnly"
68
+ };
69
+ case "pause":
70
+ return {
71
+ path: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,
72
+ body: "mode=pause"
73
+ };
74
+ case "continue":
75
+ return {
76
+ path: `/instructions/${encodedSerial}/21017?username=${encodedUsername}`,
77
+ body: "pauseOrContinue=continue"
78
+ };
79
+ case "return":
80
+ return {
81
+ path: `/instructions/${encodedSerial}/21012?username=${encodedUsername}`,
82
+ body: "charge=start"
83
+ };
84
+ case "fanQuiet":
85
+ return {
86
+ path: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,
87
+ body: "setMode=quiet"
88
+ };
89
+ case "fanStandard":
90
+ return {
91
+ path: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,
92
+ body: "setMode=auto"
93
+ };
94
+ case "fanStrong":
95
+ return {
96
+ path: `/instructions/${encodedSerial}/21022?username=${encodedUsername}`,
97
+ body: "setMode=strong"
98
+ };
99
+ case "deepCleaning":
100
+ return {
101
+ path: `/instructions/cmd21005_2/${encodedSerial}?username=${encodedUsername}`,
102
+ body: "mode=depthTotalClean"
103
+ };
104
+ case "collectDust":
105
+ return {
106
+ path: `/instructions/cmd/${encodedSerial}?username=${encodedUsername}`,
107
+ contentType: "application/json;charset=UTF-8",
108
+ body: JSON.stringify({
109
+ dInfo: {
110
+ ts: now.getTime().toString(),
111
+ userId: username
112
+ },
113
+ data: {
114
+ cmd: "startDustCenter",
115
+ value: 0
116
+ },
117
+ infoType: 21024
118
+ })
119
+ };
120
+ }
121
+ }
122
+ // Annotate the CommonJS export names for ESM import in node:
123
+ 0 && (module.exports = {
124
+ COMMAND_DEFINITIONS,
125
+ buildCommandRequest,
126
+ commandForStateId,
127
+ normalizeCommandButtonValue
128
+ });
129
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var consumables_exports = {};
20
+ __export(consumables_exports, {
21
+ normalizeConsumables21015: () => normalizeConsumables21015
22
+ });
23
+ module.exports = __toCommonJS(consumables_exports);
24
+ const CONSUMABLE_INTERVAL_HOURS = {
25
+ filter: 150,
26
+ sideBrush: 200,
27
+ mainBrush: 300,
28
+ sensors: 30
29
+ };
30
+ function normalizeConsumables21015(data) {
31
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
32
+ return void 0;
33
+ }
34
+ const record = data;
35
+ const consumables = Object.fromEntries(
36
+ Object.keys(CONSUMABLE_INTERVAL_HOURS).map((component) => [component, normalizeConsumable(component, record[component])]).filter((entry) => entry[1] !== void 0)
37
+ );
38
+ return Object.keys(consumables).length > 0 ? consumables : void 0;
39
+ }
40
+ function normalizeConsumable(component, value) {
41
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
42
+ return void 0;
43
+ }
44
+ const usedSeconds = Math.trunc(value);
45
+ const intervalHours = CONSUMABLE_INTERVAL_HOURS[component];
46
+ const intervalSeconds = intervalHours * 3600;
47
+ const remainingPercent = roundToTwoDecimals((1 - usedSeconds / intervalSeconds) * 100);
48
+ const overdueHours = roundToTwoDecimals(Math.max(0, (usedSeconds - intervalSeconds) / 3600));
49
+ return {
50
+ usedSeconds,
51
+ intervalHours,
52
+ remainingPercent,
53
+ overdueHours
54
+ };
55
+ }
56
+ function roundToTwoDecimals(value) {
57
+ return Math.round(value * 100) / 100;
58
+ }
59
+ // Annotate the CommonJS export names for ESM import in node:
60
+ 0 && (module.exports = {
61
+ normalizeConsumables21015
62
+ });
63
+ //# sourceMappingURL=consumables.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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;",
6
+ "names": []
7
+ }