Compare commits

...

218 Commits

Author SHA1 Message Date
Sören Beye
a0f4103264 docs: So youve been banned 2025-10-26 12:19:34 +01:00
Sören Beye
975ed6c9e8 docs: Firewall -vvv 2025-10-26 07:32:39 +01:00
Sören Beye
e2b6a4f04d docs: Firewall 2025-10-25 13:01:23 +02:00
Sören Beye
028f73fb05 docs: Document did issues on some newer dreame x40 bots 2025-10-25 08:25:11 +02:00
Sören Beye
07666cc803 docs: Slight clarification 2025-10-24 09:42:09 +02:00
Sören Beye
ebac83cb0a docs: FAQ update 2025-10-23 23:10:19 +02:00
Sören Beye
c16245bd51 chore: Spelling 2025-10-22 21:50:45 +02:00
Sören Beye
84c8697432 feat(vendor.midea): J15 Ultra 2025-10-22 21:48:41 +02:00
Sören Beye
6c94eda6ff fix(vendor.midea): Fix J12 ota update check failing 2025-10-22 21:47:38 +02:00
Sören Beye
1de0ca3186 refactor(vendor.midea): Misc cleanup 2025-10-22 21:47:20 +02:00
Sören Beye
ec25373195 chore: Bump some transient dependencies 2025-10-22 21:46:09 +02:00
Sören Beye
94ae396d85 fix(vendor.dreame): Remove legacy payload for manual mop clean trigger that worked for 0 remaining robots 2025-10-14 21:05:27 +02:00
Sören Beye
d1dd007ab0 fix(ui): Fix statistics label 2025-10-12 17:43:42 +02:00
Sören Beye
8fc5a0ab29 feat(vendor.midea): J12 Ultra 2025-10-12 17:43:23 +02:00
Sören Beye
64b0ee01fe feat(webserver): Add another 404 page 2025-10-10 19:03:53 +02:00
Sören Beye
c3609fc199 feat(ui): More and better Achievements 2025-10-09 22:44:38 +02:00
Sören Beye
78ee0f513c feat(mqtt): HA autodiscovery manufacturer should be Valetudo 2025-10-05 17:05:15 +02:00
Sören Beye
aa8c98e5f3 feat(vendor.dreame): Add some more obstacle IDs 2025-10-05 08:07:15 +02:00
Sören Beye
ff7e462c4d fix(updater): Pass around a file descriptor to/and make things safer 2025-10-04 16:32:52 +02:00
Sören Beye
4b45af6bd3 feat(ui): Enhance AI safety guardrails 2025-10-03 20:24:04 +02:00
Sören Beye
f5fba98bb0 docs: Update bug issue template 2025-10-03 08:57:15 +02:00
Sören Beye
614d7b7ef2 docs: Update capabilities overview 2025-10-02 18:35:18 +02:00
Sören Beye
3341b06445 chore(release): 2025.10.1 2025-10-02 17:23:43 +02:00
Sören Beye
c0136def9d fix(vendor.dreame): Fix DreameAutoEmptyDockAutoEmptyIntervalControlCapabilityV1 2025-10-02 17:22:47 +02:00
Sören Beye
fb7870e1cd chore(release): 2025.10.0 2025-10-01 19:41:15 +02:00
Sören Beye
ec61206742 fix(vendor.dreame): Give the firmware some time to think 2025-09-30 20:50:01 +02:00
Sören Beye
6fcfc6a5c1 feat(vendor.dreame): MopDockMopAutoDryingControlCapability 2025-09-30 20:48:21 +02:00
Sören Beye
847093aa4d feat(vendor.roborock): MopDockMopAutoDryingControlCapability 2025-09-30 20:46:28 +02:00
Sören Beye
ed78538257 feat(vendor.midea): MopDockMopAutoDryingControlCapability 2025-09-30 20:45:51 +02:00
Sören Beye
f150a89f2c feat(core): MopDockMopAutoDryingControlCapability 2025-09-30 20:44:45 +02:00
Sören Beye
921dd13b82 feat(mqtt): Add missing deviceClass + stateClass attributes, update enums and fix units for HA 2025-09-15 21:58:31 +02:00
Sören Beye
bb2df9825e docs: Some more thoughts 2025-09-15 09:32:24 +02:00
Sören Beye
2fdef9fed7 refactor(vendor.dreame): Group all the ephemeral state into an object 2025-09-13 17:33:31 +02:00
Sören Beye
0afc47e9a3 feat(vendor.midea): Improve state reporting 2025-09-13 17:24:56 +02:00
Sören Beye
c0d1640ae3 refactor(vendor.midea): Move specific capabilities into actual implementation class 2025-09-13 10:05:18 +02:00
Sören Beye
7c0fc3a094 feat(vendor.midea): MopExtensionFurnitureLegHandlingControlCapability 2025-09-13 09:53:38 +02:00
Sören Beye
92cd7f6475 feat(vendor.dreame): MopExtensionFurnitureLegHandlingControlCapability 2025-09-13 09:52:58 +02:00
Sören Beye
e6615d266c feat(core): MopExtensionFurnitureLegHandlingControlCapability 2025-09-13 09:51:50 +02:00
Sören Beye
fd83cd3fc5 feat(ui): Improved MopExtension and MopTwist icons 2025-09-10 23:15:27 +02:00
Sören Beye
0dcca1df2f feat(webserver): You know the rules and so do I 2025-09-09 20:35:12 +02:00
Sören Beye
72444e6fe1 feat(vendor.midea): MopTwistControlCapability 2025-09-07 18:30:42 +02:00
Sören Beye
ea236abc1e feat(vendor.dreame): MopTwistControlCapability 2025-09-07 18:30:27 +02:00
Sören Beye
92ee9a5f18 feat(core): MopTwistControlCapability 2025-09-07 18:27:49 +02:00
Sören Beye
0ea2a50a4d feat(vendor.midea): Pet obstacle avoidance and stain cleaning 2025-09-07 14:46:34 +02:00
Sören Beye
f94f3c1101 feat(vendor.midea): Everything carpet 2025-09-07 14:21:35 +02:00
Sören Beye
5715fb02f2 fix(vendor.midea): Do not fail fast 2025-09-07 14:12:51 +02:00
Sören Beye
fba6d384c1 docs: Recommend Debian 12.10 live for fastboot for now 2025-09-07 14:00:19 +02:00
Sören Beye
493c57010d fix(ui): MopDockMopWashTemperatureControl should be a dockListItems 2025-09-06 15:31:43 +02:00
Sören Beye
5d405842d2 refactor(MockRobot): Smurf for consistency 2025-09-06 15:28:40 +02:00
Sören Beye
7d1aea37f7 refactor!: Merge AutoEmptyDockAutoEmptyControlCapability into AutoEmptyDockAutoEmptyIntervalControlCapability 2025-09-06 15:24:40 +02:00
Sören Beye
c53dd66c6b chore: Fix CI 2025-09-06 14:25:47 +02:00
Sören Beye
52b462517b fix(vendor.midea): Add missing return statement 2025-09-06 14:22:06 +02:00
Sören Beye
15c1f801ad fix(vendor.dreame): Fix connection timeout on startup on very recent firmwares 2025-09-06 14:18:35 +02:00
Sören Beye
785e6b87cb refactor(vendor.midea): Rename implementation class for J15PU 2025-09-06 14:18:35 +02:00
Sören Beye
52c8c4b290 feat(vendor.midea): MopDockMopWashTemperatureControlCapability 2025-09-06 14:18:35 +02:00
Sören Beye
53ff4fa841 feat(vendor.dreame): MopDockMopWashTemperatureControlCapability 2025-09-06 14:18:35 +02:00
Sören Beye
de62cbf812 feat(core): MopDockMopWashTemperatureControlCapability 2025-09-06 14:18:35 +02:00
Sören Beye
0c6272f704 fix(miio): Fix super rare bug crashing the process 2025-09-06 14:18:35 +02:00
Sören Beye
ae01b69503 feat(vendor.midea): Auto empty controls 2025-09-06 14:18:35 +02:00
Sören Beye
58fa28f578 docs: Remove honourable mention
Unfortunately, I just cannot recommend the current versions of JetBrains IDEs anymore
2025-09-06 14:18:35 +02:00
Sören Beye
17ebfae6a8 feat(vendor.midea): Obstacles galore 2025-09-06 14:18:35 +02:00
Sören Beye
a963fa17ee fix(vendor.midea): Fail any requests instantly on initial startup 2025-09-06 14:18:35 +02:00
Sören Beye
b1eb76b26a chore(assets): Commit working state 2025-09-06 14:18:35 +02:00
Sören Beye
df46e2e697 fix(vendor.dreame): Add another new modelId 2025-09-06 14:18:35 +02:00
Sören Beye
a71a91a8bd docs: Some more thoughts 2025-09-05 11:29:23 +02:00
Sören Beye
87d0760ccc fix(vendor.dreame): Add missing model ID 2025-08-31 20:57:19 +02:00
Sören Beye
0261f57e5d fix(vendor.roborock): No-Op handle two more events 2025-08-31 15:40:47 +02:00
Sören Beye
757e6827ce feat(ui): Improve MopExtensionControl Icon
Thanks, Halis!
2025-08-31 15:34:28 +02:00
Sören Beye
ed34e36e4e chore(vendor.midea): Minor cleanup 2025-08-31 15:15:47 +02:00
Sören Beye
6f0fc2a453 fix(vendor.dreame): Track emptying state of auto empty dock 2025-08-31 15:04:37 +02:00
Sören Beye
30efa1afd9 chore(release): 2025.08.0 2025-08-29 18:37:16 +02:00
Sören Beye
ae1ac479d7 feat(vendor.midea): Midea 2025-08-29 18:31:45 +02:00
Sören Beye
eb213328de fix(vendor.dreame): Minor change for newer firmwares 2025-08-29 18:30:11 +02:00
Sören Beye
142bc87fd2 chore(build): Bump to NodeJS v22.18.0 2025-08-29 18:29:48 +02:00
Sören Beye
47e129c035 fix(ui): Fix ValetudoSplash showing scrollbars with some specific browser window dimensions 2025-08-22 17:51:51 +02:00
Sören Beye
6bec34db1c fix(ui): Actually reading docs helps build better software 2025-08-22 15:25:46 +02:00
Sören Beye
d575e2f314 feat(ui): Further improve UX of AI Assistant 2025-08-21 19:34:05 +02:00
Sören Beye
649eb4e623 refactor(mqtt): Simplify segment ID validation 2025-08-21 10:05:09 +02:00
Sören Beye
f6b08e679f fix(ui): Improve mobile UX of AI Assistant 2025-08-20 18:09:35 +02:00
Sören Beye
da4c8c4040 feat(ui): Introduce Valetudo AI Assistant 2025-08-18 17:42:32 +02:00
Sören Beye
c941ca416d
docs: Tiny grammatical fix
Tiny grammatical fix ("being" is a noun, technically a gerund).

Co-authored-by: jds11111 <jay.schieber@gmail.com>
2025-08-17 17:07:01 +02:00
Sören Beye
89b7882b90 chore: Return await cleanup 2025-08-17 16:58:46 +02:00
Sören Beye
1baeaada07 fix(mqtt)!: Fix deprecation of battery feature for vacuum entity for HA 2025.8 onward 2025-08-17 16:58:33 +02:00
Sören Beye
ddc9918032 refactor!: Drop ConsumableStateAttribute in favor of ValetudoConsumable entity 2025-08-17 16:57:07 +02:00
Sören Beye
cefa92b8ee feat(vendor.dreame): CameraLightControlCapability 2025-08-17 16:57:07 +02:00
Sören Beye
994c3f97a2 feat(core): CameraLightControlCapability 2025-08-17 16:57:07 +02:00
Sören Beye
1189bd9f9c feat(vendor.dreame): MopExtensionControlCapability 2025-08-17 16:57:07 +02:00
Sören Beye
ccc307379e feat(core): MopExtensionControlCapability 2025-08-17 16:57:07 +02:00
Sören Beye
d67cabc515 chore: Variable naming cleanup 2025-08-17 16:57:07 +02:00
Sören Beye
b456a2b516 refactor(core): Use native crypto.randomUUID where possible 2025-08-17 16:57:07 +02:00
Sören Beye
b12fb03136 refactor(core): Move map polling orchestration logic into ValetudoRobot base class 2025-08-17 16:52:03 +02:00
Sören Beye
453ac70f53 fix(ui): Only show AppBar Subheaders when there are items for it 2025-08-17 16:50:58 +02:00
Sören Beye
3746d267a7 fix(core): Remove ip from default config 2025-08-17 16:50:34 +02:00
Sören Beye
f29592f6ef fix(vendor.viomi): Remove redundant second emitMapUpdated on map reset 2025-08-17 16:50:16 +02:00
Sören Beye
81801d68b5 fix(webserver): Fetching the map should not poll the state 2025-08-17 16:49:54 +02:00
Sören Beye
69e47ab9be fix(ui): Add feedback when saving virtual restrictions 2025-08-17 16:49:33 +02:00
Sören Beye
f12da39c33 docs: Some more thoughts 2025-07-22 15:13:33 +02:00
Sören Beye
aaf0fc3f14 chore: spelling 2025-07-02 07:31:15 +02:00
Sören Beye
bd9f42b19b docs: Code of Conduct 2025-07-01 20:15:18 +02:00
Sören Beye
e74e16f9c7 docs: dust_announce 2025-06-30 16:25:28 +02:00
Sören Beye
b8dc5f8191 docs: Further clarification 2025-06-15 13:05:42 +02:00
Sören Beye
7a616b90b6 docs: Dreame is at it again 2025-06-08 10:07:37 +02:00
Sören Beye
648414ec2c docs: Some more x40 notes 2025-06-07 18:35:10 +02:00
Sören Beye
d13a2440e1 docs: Further clarification 2025-06-07 09:41:28 +02:00
Sören Beye
3b200c05ae docs(vendor.roborock): Resolve assumption to explicit statement 2025-05-30 14:52:26 +02:00
Sören Beye
8ee5157ea6 docs: Reorder 2025-05-24 17:31:26 +02:00
Sören Beye
9554cc8d73 chore(release): 2025.05.0 2025-05-24 16:51:03 +02:00
Sören Beye
925141ddb2 feat(vendor.dreame): Map additional error codes 2025-05-24 16:18:15 +02:00
Sören Beye
f261bce8a9 feat(vendor.roborock): RoborockHighResolutionManualControlCapability 2025-05-24 15:39:34 +02:00
Sören Beye
d500b3f0cb feat(vendor.dreame): DreameHighResolutionManualControlCapability 2025-05-24 15:38:21 +02:00
Sören Beye
dcf0fd1dcb feat(ui): HighResolutionManualControlCapability 2025-05-24 15:38:07 +02:00
Sören Beye
77dfd8558e feat(core): HighResolutionManualControlCapability 2025-05-24 15:37:57 +02:00
Sören Beye
0b839adff4 docs: Add note to x40 2025-05-24 08:28:24 +02:00
Sören Beye
2472f29429 fix(vendor.roborock): Gracefully handle dual-identity s5 max 2025-05-11 14:11:50 +02:00
Sören Beye
0f1ac47ce3 docs: Rotate invite link used by bots 2025-05-09 08:01:43 +02:00
Sören Beye
bdcba8ba71 docs(vendor.roborock): Rephrase OTA instructions 2025-05-07 22:14:17 +02:00
Sören Beye
5af45c3c2b fix(vendor.dreame): Prevent firmware from locking up on invalid state when cleaning the dock 2025-05-06 18:47:50 +02:00
Sören Beye
d961abf59d feat(vendor.dreame): Map additional error codes 2025-05-06 18:45:07 +02:00
Sören Beye
faf94dd28d fix(vendor.dreame): Remove non-functional quirk 2025-05-06 18:45:01 +02:00
Sören Beye
3f0897eefd docs: Add another hint 2025-04-11 17:16:30 +02:00
kryma
cf21197e9b
docs: Show actual filename of stage2 livesuit image in dreame fastboot instruction illustration 2025-04-04 08:56:36 +02:00
Sören Beye
44edc5911c chore(release): 2025.03.0 2025-03-29 11:12:07 +01:00
Sören Beye
92b2e6f092
fix(ui): handle all changedTouches
Signed-off-by: Xuefer <xuefer@gmail.com>
Co-authored-by: Xuefer <xuefer@gmail.com>
2025-03-20 18:50:51 +01:00
Sören Beye
b7de2f32db fix(ui): Fix timers view broken by the grid2 migration 2025-03-12 20:48:50 +01:00
Sören Beye
e42adcb709 fix(vendor.dreame): Fix vacuum then mop 2025-03-11 17:16:01 +01:00
Sören Beye
a2c79136b6 docs: Fans 2025-03-11 13:15:37 +01:00
Sören Beye
02286dc39a
fix(ui): Calculate map label sizes using formulas
Co-authored-by: Vivia Nikolaidou <vivia@ahiru.eu>
2025-02-27 19:53:39 +01:00
Sören Beye
0529e22d61 fix(vendor.dreame): Fix model detection for US Mova S20 2025-02-27 09:39:24 +01:00
Sören Beye
0fcb809fe6 refactor(ui): Migrate from deprecated Grid to Grid2 2025-02-19 20:09:32 +01:00
Sören Beye
4e5dd10786 feat(ui): Display dock state as text 2025-02-19 19:25:40 +01:00
Sören Beye
8589852124 docs: Add hint 2025-02-14 15:26:28 +01:00
Sören Beye
0086080d1b fix: Gracefully handle iw process not spawning 2025-02-09 18:35:03 +01:00
Sören Beye
5b66c35331 fix(vendor.dreame): Fix mova p10pu device detection 2025-02-02 18:29:13 +01:00
Sören Beye
b56ce990db chore: update bug issue template 2025-01-19 18:56:37 +01:00
Sören Beye
f051d0f274 fix(ui): Fix updater changelog link contrast when using dark mode 2025-01-14 22:53:27 +01:00
Sören Beye
7c86147fbe docs: Add missing spawn wifi hint to the dreame fastboot instructions 2025-01-14 22:51:30 +01:00
Sören Beye
e27f175e59 chore(release): 2025.01.0 2025-01-12 18:21:25 +01:00
Sören Beye
a9873e415e docs: Misc update 2025-01-11 17:36:48 +01:00
Sören Beye
ac5e09618a feat(ui): Simplify and merge paths to improve performance 2025-01-10 19:44:07 +01:00
Sören Beye
0bdf3914e0 docs: Rotate invite link used by bots 2025-01-09 12:53:08 +01:00
Sören Beye
f795e2e6cc feat(vendor.dreame): Introduce Side Brush on Carpet quirk 2025-01-06 10:00:59 +01:00
Sören Beye
1eda6ef56f feat(vendor.dreame): Wheel consumable monitoring 2025-01-06 09:57:40 +01:00
Sören Beye
6161901d0b feat(core): Introduce wheel consumable subtype 2025-01-06 09:54:02 +01:00
Sören Beye
d7f8e753c0 fix!(vendor.dreame): The mop consumable has been removed from X/L40 firmwares and equivalent 2025-01-06 09:38:02 +01:00
Sören Beye
b798ebb2de feat!: The sensor consumable type is now a subtype of the new type cleaning 2025-01-06 09:13:53 +01:00
Sören Beye
b8477fbdb6 fix(vendor.dreame): Newer dreames may store obstacle images elsewhere 2025-01-06 07:52:34 +01:00
Sören Beye
1b9dc20f6d feat(updater): Add unnecessary NullUpdateProvider for good measure 2025-01-06 07:49:11 +01:00
Sören Beye
2cb8efc3f4 feat(updater): Introduce a working version comparison to the nightly update channel 2025-01-05 18:35:02 +01:00
Sören Beye
a96ab06546 feat(ui): Shuffle the MQTT settings around 2025-01-05 17:42:50 +01:00
Sören Beye
8431f1d205 docs: Remove obsolete recommendation 2025-01-05 16:04:07 +01:00
Sören Beye
6dec70f75e feat(mqtt): Remove obsolete addICBINVMapProperty setting 2025-01-05 16:00:23 +01:00
Sören Beye
df0609ed25 feat(vendor.dreame): MOVA P10 Pro Ultra 2025-01-05 08:31:14 +01:00
Sören Beye
1af75e0034 fix(mqtt): Attempt to fix the reconfigure mutex never being left 2025-01-03 09:43:27 +01:00
Sören Beye
5b3f2dcca1 docs: Rotate invite link used by bots 2024-12-21 17:27:51 +01:00
Sören Beye
7b3dfb06e1 feat(vendor.dreame): Introduce clean route quirk 2024-12-17 23:25:00 +01:00
Sören Beye
fd89128ce6 fix(docs): Wording 2024-12-17 23:18:58 +01:00
Sören Beye
8ff0edce2a docs: Why not valetudo update 2024-12-16 19:39:51 +01:00
Xuefer
9d3df057be feat(vendor.roborock): Add error mapping for error codes 27 35 2024-12-14 09:23:21 +01:00
flo269
c78e8c2775
fix(ui): Add missing word in welcomeDialog 2024-12-10 16:12:05 +01:00
Sören Beye
88c0af5c8b fix(vendor.dreame): Hide obstacles of type 200 2024-12-08 16:39:53 +01:00
Sören Beye
ce2a902a05 docs: Why not valetudo update 2024-12-03 22:47:19 +01:00
Sören Beye
ffc58e5acc docs: Point at the right stage1 livesuit image 2024-12-01 09:25:36 +01:00
Sören Beye
151bf8b393 docs: Remove some links to EOL robots 2024-11-25 23:08:38 +01:00
Sören Beye
98d55b0627 docs: Use breakout breakout sample image for dreame FEL instructions 2024-11-23 16:53:58 +01:00
Sören Beye
c19f9c2812 docs: Media & Content Creators 2024-11-17 13:01:33 +01:00
Sören Beye
24e48a7143 docs: Rotate invite link used by bots 2024-11-17 09:08:22 +01:00
Sören Beye
a07503b1e8 feat(ui): Trim host field inputs 2024-11-16 16:40:49 +01:00
Sören Beye
2cc0ab52e6 feat(mqtt): Optionally expose PetObstacleAvoidance, CarpetModeControl and CarpetSensorModeControl capabilities 2024-11-16 16:34:52 +01:00
Sören Beye
8f22341b65 refactor(mqtt): Minor cleanup 2024-11-16 16:33:54 +01:00
Sören Beye
d77480c192 fix(mqtt): Fix Home Assistant object_id generation 2024-11-16 15:58:13 +01:00
Sören Beye
ee76222a41 feat(mqtt): Publish Dock Status to MQTT 2024-11-16 15:57:57 +01:00
Sören Beye
9b98e3ebfe chore(release): 2024.11.0 2024-11-09 07:58:15 +01:00
Sören Beye
7d876db2f8 docs: Misc Update 2024-11-09 07:57:07 +01:00
Sören Beye
afef3ec90f feat(vendor.dreame): New Robots 2024-11-09 07:56:41 +01:00
Sören Beye
74731a6bd4 fix(vendor.dreame): Fix water hookup test quirk success state raising an error 2024-11-02 21:21:26 +01:00
Sören Beye
86e58df3a7 docs: Update home assistant integration page 2024-11-01 19:59:44 +01:00
Sören Beye
769c75e819 docs: Update home assistant mqtt publish example syntax 2024-11-01 11:06:39 +01:00
Sören Beye
dd6eac067b fix: Fix breakage caused by splitting changes into multiple commits 2024-11-01 10:53:59 +01:00
Sören Beye
bf9ecb627b fix(timers): Allow execution of timers if time is plausible 2024-11-01 10:49:39 +01:00
Sören Beye
e627da375a docs: Consistency 2024-11-01 10:20:28 +01:00
Sören Beye
89d1fb1dbb chore: Bump some dependencies 2024-11-01 10:17:36 +01:00
Sören Beye
436fae6293 refactor: Bake-in specifically crafted build_metadata file instead of random other files 2024-11-01 09:05:15 +01:00
Sören Beye
cf20143b75 feat(vendor.dreame): Introduce water hookup test quirk 2024-10-31 20:44:15 +01:00
Sören Beye
8e336f6f93 refactor: Remove unnecessary second call to process.memoryUsage.rss() every 2.5s 2024-10-25 17:52:46 +02:00
Sören Beye
55294d7885 chore(release): 2024.10.0 2024-10-21 19:10:32 +02:00
Sören Beye
5ae2247f21 docs: Rotate invite link used by bots 2024-10-21 15:53:55 +02:00
Sören Beye
489e85a8d5 docs: Add note regarding the common l10spuh post-root issue 2024-10-14 10:38:02 +02:00
Sören Beye
5559f88ab2 chore: Bump dependencies 2024-10-13 13:12:34 +02:00
Sören Beye
fb600a8ef5 chore: Upgrade react-router v5 to v6 2024-10-13 13:09:07 +02:00
Sören Beye
5f0515bd68 docs: Try to reduce some dreame-related confusion 2024-10-08 11:35:51 +02:00
Sören Beye
63279c44a0 feat(vendor.dreame): D10s Pro/Plus also support Obstacle Images 2024-10-02 17:30:36 +02:00
Sören Beye
111b53a530 docs: Rotate invite link used by bots 2024-09-30 09:25:40 +02:00
Vivia Nikolaidou
2e5a9c20bc fix(ui): Fix zoom level after zooming in and back out again not ending up where it started 2024-09-28 20:48:05 +02:00
Sören Beye
8e97f344fb docs: Update on the q7 max situation 2024-09-28 08:26:49 +02:00
Sören Beye
79deca12ea chore: Bump dependencies 2024-09-23 19:21:02 +02:00
Sören Beye
3513cbc698 fix(vendor.dreame): Fix MopDockCleanManualTriggerCapability for everything that is not a pure mop 2024-09-23 18:35:29 +02:00
Sören Beye
76280bdfc6 feat(vendor.dreame): Add quirk for triggering the mop dock cleaning procedure 2024-09-14 17:17:51 +02:00
Sören Beye
0928cf01af refactor(vendor.dreame): Add more AI classifier IDs and pull the constant into DreameUtils 2024-09-14 16:29:55 +02:00
Sören Beye
90e5ab935f refactor(webserver): Use rate limiters instead of semaphore in ObstacleImagesCapabilityRouter 2024-09-14 16:11:54 +02:00
Sören Beye
7a6eb1ee0b fix(webserver): Valetudo router rate limits should be global 2024-09-14 16:10:58 +02:00
Sören Beye
20917a7056 fix(vendor.dreame): Fix Mop Dock Water Heater quirk 2024-09-06 18:05:15 +02:00
Sören Beye
c0be1be25d docs: Misc 2024-09-06 16:11:20 +02:00
Sören Beye
2152e20b6e chore: Fix the wrong link in the right place 2024-09-05 22:53:09 +02:00
Sören Beye
dc3b96e87d chore: Bump some transitive dependencies 2024-09-05 21:59:49 +02:00
Sören Beye
b56ed1ca6e chore(ui): Resolve issues detected by sonarcloud 2024-09-05 17:55:08 +02:00
Sören Beye
67ee71ce58 feat(vendor.dreame): ObstacleImagesCapability 2024-09-05 17:46:43 +02:00
Sören Beye
30b5fc8a3d feat(ui): ObstacleImagesCapability 2024-09-05 17:46:26 +02:00
Sören Beye
f9fa9d0b60 feat(core): ObstacleImagesCapability 2024-09-05 17:46:00 +02:00
Sören Beye
d0f754bc10 feat: Display the actual CPU usage instead of the system load 2024-09-02 21:14:47 +02:00
Sören Beye
b8afd4de67 docs: Extend matter faq 2024-09-02 19:31:53 +02:00
Sören Beye
28d674f4e5 fix: A config reset should not reset the robot config 2024-09-02 19:08:47 +02:00
Sören Beye
02d75635c7 docs: Fix incorrect link title
Thanks @stefanmd023
2024-09-02 19:06:02 +02:00
Sören Beye
a2a1d4061d fix(ui): Fix cutting line being invisible when using the light theme
Thanks @vivia
2024-09-02 19:02:53 +02:00
Sören Beye
ddcf3e1590 chore: Update PR template 2024-08-26 12:28:05 +02:00
Sören Beye
ca077b66c3 feat(ui): Draw half-transparent path in virtual restrictions edit map to make placing restrictions correctly easier 2024-08-06 17:50:09 +02:00
Sören Beye
f05423002b docs: Misc update 2024-08-06 17:31:10 +02:00
Sören Beye
f294b9ef64 docs: Misc update 2024-07-24 22:47:44 +02:00
Sören Beye
9111509cbb docs: Rotate invite link used by bots 2024-07-09 07:36:01 +02:00
442 changed files with 23467 additions and 6784 deletions

View File

@ -52,7 +52,6 @@
"jsdoc/check-tag-names": "error",
"jsdoc/check-types": "error",
"jsdoc/implements-on-classes": "error",
"jsdoc/newline-after-description": "error",
"jsdoc/no-undefined-types": "error",
"jsdoc/require-param": "error",
"jsdoc/require-param-name": "error",

View File

@ -5,15 +5,22 @@ body:
- type: markdown
attributes:
value: |
Hi, before you open a bug report, please remember that whatever issue it is that you experience, I do not experience, as otherwise, I would've already fixed it. Thus, if you have no clue how I could reproduce you issue, please save us both the time and don't open a bug report.
I can understand that that might be frustrating and that you might feel helpless, but it is simply not my responsibility to figure out your problems for you. It is highly unenjoyable work to do that, which takes up a ton of time and in the end most of the time it's user error anyway.
If you've found something and are able to reproduce it, good! Please let me know.
Though, before you do, make sure that it's not some super niche nonsense, as I do not care about that either. FOSS enables you do to whatever niche thing you want to do, but that doesn't mean that FOSS maintainers would be required to help you with that.
Thank you for understanding and sorry if this might've felt a bit intimidating.
I'm just trying to protect my sanity here.
Hey,
before you open a bug report, please take a moment and revisit the problem at hand.
Ask yourself: "Is this really a bug, or am I just looking for help?"
From a user perspective, these two things may look like they're the same thing; they are quite different though.
Bugs are:
1. Genuinely and indisputably erroneous
2. Reproducible
3. Clearly caused/controlled by Valetudo and **not** the vendor firmware
If something just doesn't behave like you'd expect it to, that is a support request.
Additionally, do **not** open bug reports for Home Assistant deprecation warnings, unless the thing they report breaks _very soon_. Not all deprecation warnings will be fixed instantly in Valetudo, as otherwise, updating Valetudo would force people to also update Home Assistant.
- type: textarea
id: what-is-happening
attributes:
@ -22,27 +29,14 @@ body:
validations:
required: true
- type: dropdown
id: broken-or-not-as-expected
attributes:
label: Broken or not as expected?
description: |
Do you believe that that is actually broken as in "Not behaving as it was obviously intended to behave",
or is it just not as expected as in "I've expected it to do X but it did do Y instead"?
options:
- Broken
- Not as expected
default: 1
validations:
required: true
- type: textarea
id: what-should-be-happening
attributes:
label: What should be happening?
description: |
What would be the correct behavior?<br/>
If you've previously selected "Not as expected", why did you expect that? Is it reasonable to expect that?
What would be the correct behavior?
validations:
required: true
@ -59,24 +53,6 @@ body:
validations:
required: true
- type: dropdown
id: valetudo-relation
attributes:
label: Is this Valetudo-related?
description: |
Since Valetudo is not a custom firmware but just a cloud replacement running on the robot,
are you sure that the issue you're seeing relates to **the code of Valetudo** and not the vendor firmware?
options:
- Certainly Valetudo-related
- Probably Valetudo-related
- Might be Valetudo-related
- Could be Valetudo-related
- Probably not Valetudo-related
- Certainly not Valetudo-related
default: 1
validations:
required: true
- type: input
id: valetudo-version
attributes:
@ -108,14 +84,6 @@ body:
validations:
required: true
- type: textarea
id: context-reflection
attributes:
label: Context reflection
description: Thinking about what you said above about your setup, do you see anything non-standard about it that might be troublesome and cause this?
validations:
required: true
- type: textarea
id: screenshots
attributes:

View File

@ -1,13 +1,8 @@
## Type of change
**STOP**
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Docs
- [ ] Refactor/Code Cleanup
This project does currently not accept pull requests.<br/>
Please refrain from opening this PR as it will be closed automatically.
# Description
Thank you for understanding.
Please include a summary of the change and which issue is fixed (if applicable).
Please also include relevant motivation and context.
<!-- Delete if there is none -->
Fixes # (issue)
For more information, check out the `Contributing.md` in the root of the repository

View File

@ -20,6 +20,10 @@
'name': 'Generate OpenAPI Docs',
'run': 'npm run build_openapi_schema',
},
{
'name': 'Generate Code',
'run': 'npm run generate_code --workspace=backend',
},
{ 'run': 'npm run ts-check --workspace={frontend,backend}' },
{ 'run': 'npm run lint --workspace={frontend,backend}' },
{ 'run': 'npm run build --workspace=frontend' },

2
.gitignore vendored
View File

@ -14,6 +14,8 @@ build/
.vscode/
/backend/lib/res/valetudo.openapi.schema.json
/backend/lib/res/build_metadata.json
/backend/lib/robots/midea/generated/midea_protobufs.js
/docs/vendor
/docs/_site

28
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,28 @@
# Rules
1. No bad faith communication
2. No hidden agendas
3. Do not act against the interest of, or with lack of care for, the Valetudo mission
4. Participate with genuine respect for others as individuals, not treating them as obstacles or instruments
5. Do your own homework
6. Additional rules may be added if necessary
Rule interpretation can be subjective and may be exploited through bad-faith tactics like sea-lioning.
Therefore, when a violation is suspected, the burden of proof to demonstrate compliance lies with the user.
Failure to comply will lead to temporary or, in severe cases, permanent removal.
Additionally, temporary removal may be conducted when deemed necessary for signal-to-noise or de-escalative reasons.
# Rules - for normal people
1. Don't be a dick
# Appeal process
People found to be in violation of the rules have the option to appeal their case through the general process through which adults resolve misunderstandings and other conflicts.
This usually entails giving everyone involved time to cool down and think, then diplomatically reaching out in private, explaining where they came from, displaying effort being put into understanding, showing genuine regret for the situation and a desire to resolve it.

View File

@ -36,13 +36,13 @@ There, you will find a list of [supported robots](https://valetudo.cloud/pages/g
## Screenshots
### Phone/Mobile
<img src="https://user-images.githubusercontent.com/974410/211155741-d6430660-a6b2-48ab-8ddc-2217328444de.png" width=360> <img src="https://user-images.githubusercontent.com/974410/211155635-fdfb5b2b-2c3d-4a49-a0ed-a40deb04708f.png" width=360>
<img src="https://user-images.githubusercontent.com/974410/211155741-d6430660-a6b2-48ab-8ddc-2217328444de.png" width=360> <img src="https://github.com/user-attachments/assets/eaad6fe0-dd1e-4f56-b6f9-f65954aecac7" width=360>
<img src="https://user-images.githubusercontent.com/974410/211155650-7cac266c-ffeb-432d-8656-5241a5d6f227.png" width=360> <img src="https://user-images.githubusercontent.com/974410/211155656-d43ee25e-1ae6-432f-95ff-1a39d294828d.png" width=360>
### Tablet/Desktop
![image](https://user-images.githubusercontent.com/974410/211155726-4ca46998-717a-49b4-a7d0-45b0467cc10a.png)
![image](https://github.com/user-attachments/assets/dc18723f-b15f-4500-907b-bad6d7dd1a4f)
![image](https://user-images.githubusercontent.com/974410/211155836-9199616a-efde-4238-91c4-24158ba67677.png)
@ -51,18 +51,45 @@ There, you will find a list of [supported robots](https://valetudo.cloud/pages/g
![image](https://user-images.githubusercontent.com/974410/211155880-ff184776-86fe-4c2f-9556-4d556cfa12f4.png)
## Valetudo is a garden
This project is the hobby of some random guy on the internet. There is no intent to commercialize it, grow it
or expand the target audience of it. In fact, there is intent to explicitly not do that.
Think of Valetudo as a privately-owned public garden. You can visit it any time for free and enjoy it.
You can spend time there, and you can bring an infinite amount of friends with you to enjoy it.
You can walk the pathways built there. You can sit on some patch of grass and maybe watch a Duck or something.
You can leave a tip in the tip jar at the entrance if you really enjoy it and want to support it flourish.
You can take inspiration from it and bring that home to your own garden, giving it a personal twist and adapting it as needed.
You can even make friendly suggestions if you have a really good idea that ties into the vision that is already there.
But, at the end of the day, you must understand that it is still privately-owned. You're on someone else's property
over which you have no power at all. You will have to show the necessary respect. And - most importantly - you need to
understand that letting you into this garden is a gift and should be treated as such.
If you don't like this garden because you don't like how it's structured, or you feel like it's missing something, or maybe
I choose the wrong flowers to plant over there that's fine. It's just not for you then. You can leave at any time.
There is simply no ground to stand on to demand change to the garden. It doesn't matter if it would attract more people
or if all the other gardens in town are doing something in a specific way. It doesn't matter if your idea of what gardens
even are differs.<br/>
This at the end of the day is simply private property with free public access as a gift to everyone.
When it comes to software development, _everyone_ has access to infinite plots of undeveloped land that they can claim at any time.
Therefore, a garden being build with a specific vision does not take away the ability for anyone else to build their own garden with a different vision.
## Further questions?
[Valetudo Telegram group](https://t.me/+k-ukcsX2ZYg5MDky)
1. [dust_announce - Very low frequency updates about Valetudo and Rooting](https://t.me/dust_announce)
2. [Valetudo Telegram group](https://t.me/+2MsKV8kILxJhNDAy)
3. [So you've been banned?](https://valetudo.cloud/pages/general/so-youve-been-banned.html)
Any other mediums such as IRC, Matrix or Reddit are unofficial channels not connected to the project and might contain incorrect or outdated information.
## Contributing
Make sure to familiarize yourself with the [./CONTRIBUTING.md](./CONTRIBUTING.md)
## Honourable mentions
Valetudo and its companion applications are developed using JetBrains IDEs such as [WebStorm](https://www.jetbrains.com/webstorm/).
Licenses for those have been provided for free by JetBrains to the project in context of [their open source support program](https://jb.gg/OpenSourceSupport) since multiple years now.
Thanks a lot for that!

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m12.557 0c-0.95394-0.0027638-1.909 0.14678-2.8125 0.44336-1.2584 0.41308-2.3207 1.0367-3.3223 1.9512-0.13452 0.12282-0.27723 0.25953-0.31641 0.30273-0.039186 0.043201-0.12664 0.13594-0.19336 0.20703-0.30938 0.32961-0.69401 0.82808-0.96094 1.248-1.9528 3.0731-1.8579 6.996 0.21289 9.9668a2.9918 2.9917 0 0 0-0.12109 0.83984 2.9918 2.9917 0 0 0 2.9902 2.9922 2.9918 2.9917 0 0 0 1.6055-0.46875c0.019749 0.0068 0.038918 0.014934 0.058594 0.021484 0.63434 0.21074 1.3657 0.3629 1.9922 0.41602 0.12448 0.01057 0.25616 0.021501 0.29102 0.02539v0.001954c0.03485 0.0039 0.32672 0.005053 0.65039 0.001953 0.81677-0.0077 1.3296-0.07247 2.0723-0.25781 0.58679-0.14644 1.1552-0.35511 1.6992-0.61523a2.9918 2.9917 0 0 0 2.1035 0.875 2.9918 2.9917 0 0 0 2.9902-2.9922 2.9918 2.9917 0 0 0-0.86914-2.1074c0.42193-0.88548 0.70264-1.8459 0.82617-2.8633 0.05456-0.44883 0.059636-1.496 0.009766-1.9316-0.1324-1.1561-0.4219-2.138-0.92774-3.1367-0.33157-0.65467-0.68965-1.1928-1.1641-1.7559-0.23673-0.281-0.77629-0.81869-1.0527-1.0488-1.2001-0.99931-2.55-1.6462-4.0566-1.9453-0.56038-0.11125-1.1327-0.16827-1.7051-0.16992zm9.9434 0v24h1.5v-24h-1.5zm-9.9688 1.873c0.8155 6.882e-4 1.5414 0.12139 2.291 0.37891 0.80046 0.27502 1.5159 0.66746 2.1738 1.1953 0.26971 0.21638 0.74941 0.68612 0.96094 0.93945 0.5824 0.6976 1.0064 1.4474 1.2969 2.293 0.04922 0.14334 0.096853 0.28317 0.10352 0.31055l0.011719 0.048828h-4.2129l-0.050781-0.1582c-0.12298-0.38264-0.34563-0.7507-0.63281-1.0488-0.42521-0.44146-0.94467-0.71584-1.541-0.8125-0.23707-0.03848-0.64731-0.033394-0.89844 0.0097657-0.68012 0.1169-1.3019 0.49782-1.7246 1.0605-0.14572 0.194-0.35081 0.59328-0.41016 0.79883l-0.044922 0.15039h-4.1719l0.044922-0.15039c0.070285-0.2311 0.2145-0.60737 0.33203-0.86719 0.8419-1.8606 2.4964-3.2896 4.4785-3.8691 0.65835-0.1925 1.2787-0.2799 1.9941-0.2793zm0.035156 4.1758c0.21337 0.00816 0.43001 0.056286 0.63477 0.15234 0.45435 0.21316 0.77916 0.59904 0.91992 1.0879 0.03447 0.11983 0.039062 0.18158 0.039062 0.42773 0 0.33859-0.02862 0.47318-0.16211 0.74414-0.2371 0.48131-0.68145 0.81096-1.2168 0.9043-0.1423 0.024817-0.38474 0.032823-0.49414 0.015625-0.58425-0.091922-1.0505-0.43291-1.293-0.94727-0.13333-0.28278-0.1803-0.5598-0.15234-0.87305 0.019675-0.22045 0.057686-0.35672 0.16016-0.56836 0.29815-0.61589 0.92435-0.96789 1.5645-0.94336zm-7.1367 2.6016h4.498l0.052734 0.12109c0.31639 0.75541 0.97659 1.3447 1.7617 1.5723 0.48513 0.1406 1.0305 0.13978 1.5273-0.001953 0.70749-0.20179 1.3255-0.71636 1.666-1.3848 0.05328-0.10456 0.10804-0.21568 0.11914-0.24805l0.019531-0.058594h4.5645v0.29492c0 1.6368-0.54238 3.2001-1.5566 4.4766-0.24212 0.30463-0.81328 0.87566-1.1133 1.1133-1.0593 0.83896-2.2891 1.3511-3.6387 1.5137l-0.001953-0.001953c-0.13539 0.01631-0.43169 0.031646-0.69727 0.035156-0.4961 0.0065-0.51172 0.005211-0.89453-0.037109-2.0839-0.23073-3.9698-1.3791-5.123-3.1211-0.10225-0.15441-0.22248-0.34612-0.26758-0.42578-0.48323-0.85362-0.79497-1.8229-0.89258-2.7852-0.036044-0.35552-0.052495-0.78237-0.037109-0.93555l0.013672-0.12695zm14.822 4.9043a2.2439 2.2438 0 0 1 0.49609 1.4043 2.2439 2.2438 0 0 1-2.2422 2.2441 2.2439 2.2438 0 0 1-1.4004-0.50195c0.5126-0.30322 0.99529-0.65344 1.4336-1.0508 0.68514-0.6211 1.2566-1.3255 1.7129-2.0957zm-14.461 1.3633c0.50597 0.57051 1.099 1.0938 1.7168 1.5098 0.36476 0.24555 0.77511 0.47316 1.1953 0.67188a2.2439 2.2438 0 0 1-0.66992 0.10352 2.2439 2.2438 0 0 1-2.2441-2.2441 2.2439 2.2438 0 0 1 0.0019531-0.041015zm10.926 3.3184v1.8809h-5v2h5v1.8828l4.7793-2.8926-4.7793-2.8711z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m11.281 0v1.4746 0.31445a9.6696 9.6696 0 0 0-9.3672 9.3965h-0.43945-1.4746l2.9609 4.8926 2.9395-4.8945-1.4766 0.001953h-0.51172a7.6737 7.6737 0 0 1 7.3691-7.3984v0.63672 1.4746l4.8926-2.959-4.8926-2.9395zm11.219 0v24h1.5v-24h-1.5zm-7.9707 7c-0.79711-0.0023056-1.5946 0.12132-2.3496 0.36914-1.0515 0.34518-1.9404 0.86676-2.7773 1.6309-0.1124 0.10263-0.23093 0.21781-0.26367 0.25391-0.032743 0.036099-0.10636 0.11442-0.16211 0.17383-0.25851 0.27543-0.57972 0.69204-0.80273 1.043-1.6317 2.5679-1.5525 5.8437 0.17773 8.3262-0.06695 0.22795-0.10131 0.46555-0.10156 0.70312-2e-7 1.3807 1.1193 2.5 2.5 2.5 0.47491-6.55e-4 0.93937-0.13731 1.3398-0.39258 0.01651 0.0056 0.034351 0.014061 0.050781 0.019531 0.53005 0.17609 1.1406 0.30327 1.6641 0.34766 0.10402 0.0089 0.21306 0.018235 0.24219 0.021485v0.001953c0.02914 0.0033 0.27446 0.0026 0.54492 0 0.68249-0.0064 1.1099-0.059976 1.7305-0.21484 0.19846-0.04953 0.39434-0.11019 0.58789-0.17578 0.40048 0.25526 0.86493 0.39192 1.3398 0.39258 1.3807 0 2.5-1.1193 2.5-2.5 1.38e-4 -0.23456-0.032736-0.46797-0.097656-0.69336v-0.001953c0.70818-1.0102 1.1555-2.1802 1.3105-3.457 0.04559-0.37505 0.049442-1.2512 0.007812-1.6152-0.1106-0.966-0.35273-1.7866-0.77539-2.6211-0.27707-0.54703-0.57625-0.99631-0.97266-1.4668-0.19781-0.23481-0.64791-0.68465-0.87891-0.87695-1.0028-0.83504-2.1317-1.377-3.3906-1.627-0.46825-0.09296-0.94559-0.13924-1.4238-0.14062zm-0.021485 1.5645c0.68143 5.749e-4 1.2877 0.10122 1.9141 0.31641 0.66886 0.22981 1.2666 0.55891 1.8164 1 0.22537 0.18081 0.62599 0.57349 0.80274 0.78516 0.48665 0.58292 0.8413 1.2094 1.084 1.916 0.04113 0.11978 0.080368 0.23493 0.085938 0.25781l0.009765 0.042968h-3.5195l-0.042969-0.13281c-0.10276-0.31974-0.28737-0.62784-0.52734-0.87695-0.35531-0.36889-0.79076-0.59694-1.2891-0.67774-0.1981-0.032159-0.54213-0.03027-0.75195 0.00586-0.5683 0.09769-1.0862 0.41844-1.4395 0.88867-0.12177 0.16211-0.29416 0.49624-0.34375 0.66797l-0.035157 0.125h-3.4863l0.037109-0.125c0.058731-0.19319 0.17913-0.50948 0.27734-0.72656 0.70349-1.5547 2.0859-2.7482 3.7422-3.2324 0.55012-0.16085 1.0682-0.23488 1.666-0.23438zm0.029297 3.4902c0.17829 0.006699 0.3582 0.046701 0.5293 0.12695 0.37965 0.17812 0.65191 0.49966 0.76953 0.9082 0.028799 0.10013 0.033203 0.15369 0.033203 0.35938 0 0.28293-0.023238 0.39468-0.13477 0.62109-0.19812 0.40219-0.57027 0.67786-1.0176 0.75586-0.1189 0.02075-0.32069 0.028062-0.41211 0.013672-0.48819-0.07681-0.87746-0.36318-1.0801-0.79297-0.11141-0.23629-0.15227-0.46675-0.12891-0.72852 0.01644-0.18421 0.049132-0.29972 0.13477-0.47656 0.24913-0.51464 0.77178-0.80761 1.3066-0.78711zm-5.9629 2.1738h3.7578l0.042969 0.10156c0.26437 0.63123 0.8166 1.1243 1.4727 1.3145 0.40537 0.11748 0.86216 0.11643 1.2773-0.001953 0.59118-0.16862 1.1081-0.5997 1.3926-1.1582 0.04451-0.08738 0.09033-0.17998 0.09961-0.20703l0.015624-0.048828h3.8145v0.24609c0 1.3677-0.45325 2.6736-1.3008 3.7402-0.20231 0.25455-0.68096 0.73308-0.93164 0.93164-0.88511 0.70104-1.9133 1.1279-3.041 1.2637-0.11313 0.01362-0.36012 0.026367-0.58203 0.029297-0.41454 0.0055-0.4282 0.00407-0.74805-0.03125-1.7414-0.19293-3.3176-1.1537-4.2812-2.6094-0.08539-0.12899-0.18692-0.2889-0.22461-0.35547-0.40378-0.71329-0.66258-1.5221-0.74414-2.3262-0.030118-0.29708-0.044106-0.65325-0.03125-0.78125l0.011719-0.10742zm0.30273 5.2363c0.42277 0.47656 0.91735 0.91417 1.4336 1.2617 0.3048 0.20519 0.6468 0.39646 0.99805 0.5625-0.1809 0.05699-0.36892 0.085508-0.55859 0.085938-1.0355 0-1.875-0.8395-1.875-1.875 3.008e-4 -0.0114 0.0014387-0.023766 0.0019531-0.035156zm11.246 0.007812c4.69e-4 9e-3 0.001611 0.018384 0.001953 0.027344 0 1.0355-0.83948 1.875-1.875 1.875-0.18627-6.35e-4 -0.37092-0.028784-0.54883-0.083984 0.66692-0.31368 1.2851-0.72234 1.8262-1.2129 0.21185-0.19206 0.40951-0.3953 0.5957-0.60547z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m11.281 0v1.4746 0.31445c-5.1177 0.15815-9.225 4.2783-9.3672 9.3965h-0.43945-1.4746l2.9609 4.8926 2.9395-4.8945-1.4766 0.001953h-0.51172c0.14106-4.0149 3.3549-7.2414 7.3691-7.3984v0.63672 1.4746l4.8926-2.959-4.8926-2.9395zm11.219 0v24h1.5v-24h-1.5zm-7.9707 7c-0.79711-0.00231-1.5946 0.12131-2.3496 0.36914-1.0515 0.34518-1.9404 0.86675-2.7773 1.6309-0.1124 0.10263-0.23094 0.21781-0.26367 0.25391-0.03274 0.036099-0.10636 0.11442-0.16211 0.17383-0.25851 0.27543-0.57967 0.69204-0.80273 1.043-1.6317 2.5679-1.5526 5.8458 0.17773 8.3281-0.06695 0.22795-0.10131 0.46359-0.10156 0.70117 0 1.3807 1.1193 2.5 2.5 2.5 0.47491-6.25e-4 0.93937-0.13731 1.3398-0.39258 0.01651 0.0056 0.034331 0.014061 0.050781 0.019531 0.53005 0.1761 1.1406 0.30327 1.6641 0.34766 0.10402 0.0088 0.21306 0.018235 0.24219 0.021485v0.001953c0.02912 0.0033 0.27446 0.0027 0.54492 0 0.68249-0.0065 1.1099-0.059975 1.7305-0.21484 0.49031-0.12237 0.96537-0.29631 1.4199-0.51367 0.46625 0.46571 1.0988 0.72779 1.7578 0.73047 1.3807 0 2.5-1.1193 2.5-2.5-4.16e-4 -0.65958-0.26174-1.2918-0.72656-1.7598 0.35256-0.73992 0.58624-1.5424 0.68945-2.3926 0.04559-0.37505 0.049552-1.2512 0.007812-1.6152-0.11062-0.96604-0.35272-1.7865-0.77539-2.6211-0.27706-0.54704-0.57624-0.99631-0.97266-1.4668-0.19781-0.23481-0.64791-0.68464-0.87891-0.87695-1.0028-0.83505-2.1317-1.377-3.3906-1.627-0.46825-0.092961-0.94559-0.13924-1.4238-0.14062zm-0.021485 1.5645c0.68142 5.75e-4 1.2877 0.10122 1.9141 0.31641 0.66886 0.22981 1.2667 0.55892 1.8164 1 0.22536 0.18081 0.62599 0.57344 0.80274 0.78516 0.48665 0.58292 0.8413 1.2094 1.084 1.916 0.04113 0.11978 0.080378 0.23689 0.085938 0.25977l0.009765 0.041015h-3.5195l-0.042969-0.13281c-0.10276-0.31974-0.28737-0.62783-0.52734-0.87695-0.3553-0.36889-0.79076-0.59699-1.2891-0.67774-0.1981-0.032159-0.5421-0.03018-0.75195 0.00586-0.5683 0.09769-1.0862 0.41846-1.4395 0.88867-0.12177 0.16211-0.29416 0.49618-0.34375 0.66797l-0.035157 0.125h-3.4863l0.037109-0.125c0.05873-0.19311 0.17913-0.50943 0.27734-0.72656 0.70349-1.5547 2.0859-2.7482 3.7422-3.2324 0.55011-0.16085 1.0682-0.23488 1.666-0.23438zm0.029297 3.4902c0.17829 0.006799 0.35821 0.046691 0.5293 0.12695 0.37965 0.17812 0.65191 0.49973 0.76953 0.9082 0.028809 0.10013 0.033203 0.15369 0.033203 0.35938 0 0.28293-0.023229 0.39467-0.13477 0.62109-0.19812 0.40219-0.57021 0.67786-1.0176 0.75586-0.1189 0.02074-0.32069 0.028052-0.41211 0.013672-0.48819-0.07681-0.87746-0.36315-1.0801-0.79297-0.11141-0.2363-0.15227-0.46677-0.12891-0.72852 0.01644-0.18421 0.049154-0.29972 0.13477-0.47656 0.24913-0.51465 0.77178-0.80761 1.3066-0.78711zm-5.9629 2.1738h3.7578l0.042969 0.10156c0.26437 0.63124 0.81661 1.1243 1.4727 1.3145 0.40537 0.11748 0.86216 0.11653 1.2773-0.001953 0.59118-0.16862 1.1081-0.59965 1.3926-1.1582 0.04451-0.08737 0.09034-0.17998 0.09961-0.20703l0.015624-0.048828h3.8145v0.24609c0 1.3677-0.45329 2.6736-1.3008 3.7402-0.20231 0.25456-0.68096 0.73308-0.93164 0.93164-0.88511 0.70105-1.9133 1.1279-3.041 1.2637-0.11313 0.01362-0.36012 0.026367-0.58203 0.029297-0.41454 0.0055-0.42816 0.00417-0.74805-0.03125-1.7414-0.19287-3.3176-1.1537-4.2812-2.6094-0.085393-0.12898-0.18692-0.2889-0.22461-0.35547-0.40378-0.7133-0.66258-1.5221-0.74414-2.3262-0.030118-0.29708-0.044106-0.65325-0.03125-0.78125l0.011719-0.10742zm12.385 4.0977c0.26785 0.33239 0.41525 0.74695 0.41602 1.1738 0 1.0355-0.83949 1.875-1.875 1.875-0.42605-0.0026-0.83901-0.14959-1.1699-0.41797 0.42832-0.25338 0.83102-0.54688 1.1973-0.87891 0.5725-0.519 1.0504-1.1083 1.4316-1.752zm-12.084 1.1387c0.42278 0.47673 0.91931 0.91416 1.4355 1.2617 0.30479 0.20519 0.64693 0.39645 0.99805 0.5625-0.1809 0.057-0.36893 0.085488-0.55859 0.085938-1.0355 0-1.875-0.83949-1.875-1.875 3.02e-4 -0.01139-5.13e-4 -0.023766 0-0.035156z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 338 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View File

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 413 B

View File

Before

Width:  |  Height:  |  Size: 777 B

After

Width:  |  Height:  |  Size: 777 B

View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

View File

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 412 B

View File

Before

Width:  |  Height:  |  Size: 573 B

After

Width:  |  Height:  |  Size: 573 B

View File

Before

Width:  |  Height:  |  Size: 569 B

After

Width:  |  Height:  |  Size: 569 B

View File

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 499 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="m11.276 23.995c-0.0466-0.0052-0.22091-0.02108-0.38734-0.03518-0.83761-0.07102-1.8157-0.2757-2.6638-0.55746-0.99337-0.33-2.0568-0.85274-2.9284-1.4395-1.0958-0.73769-2.133-1.7267-2.9179-2.7823-2.9783-4.0056-3.1762-9.413-0.49864-13.627 0.35679-0.56156 0.87047-1.2299 1.2841-1.6706 0.089202-0.09505 0.20504-0.22008 0.25743-0.27784 0.052382-0.05776 0.2424-0.23938 0.42225-0.40359 1.3391-1.2226 2.7611-2.0579 4.4435-2.6102 1.9327-0.63444 4.0413-0.76202 6.0392-0.36539 2.0143 0.3999 3.8194 1.2658 5.4239 2.6019 0.36962 0.30779 1.0918 1.0271 1.4083 1.4028 0.63425 0.75277 1.1109 1.473 1.5542 2.3483 0.67626 1.3353 1.0646 2.6464 1.2416 4.1921 0.06671 0.58251 0.0594 1.9845-0.01355 2.5846-0.36085 2.9717-1.7049 5.5833-3.8955 7.5692-1.4391 1.3046-3.2226 2.2537-5.1295 2.7296-0.99296 0.2478-1.6763 0.33244-2.7683 0.34288-0.43274 0.0042-0.82492 0.0033-0.87152-2e-3zm1.8036-2.5389c1.8044-0.21732 3.4498-0.90148 4.866-2.0232 0.40107-0.31769 1.1639-1.0808 1.4876-1.4881 1.3561-1.7066 2.0832-3.7948 2.0832-5.9832v-0.39593h-6.1032l-0.02699 0.07868c-0.01484 0.04328-0.08526 0.19307-0.15648 0.33287-0.45518 0.89359-1.2836 1.5814-2.2295 1.8512-0.66429 0.18945-1.3948 0.19028-2.0434 0.0023-1.0497-0.30419-1.9323-1.0917-2.3553-2.1017l-0.068451-0.16341h-6.0159l-0.017211 0.17137c-0.020569 0.20481 0.00257 0.77456 0.050785 1.2499 0.13047 1.2866 0.54474 2.5818 1.1908 3.7231 0.060303 0.10651 0.22142 0.36253 0.35805 0.56891 1.5419 2.3291 4.0632 3.8657 6.8494 4.1743 0.51174 0.05666 0.53503 0.05764 1.1983 0.04883 0.35506-0.0047 0.75103-0.02429 0.93204-0.04609zm-0.73157-8.9338c0.71565-0.1248 1.3101-0.56708 1.6271-1.2106 0.17846-0.36228 0.21736-0.53994 0.21736-0.99264 0-0.32911-0.0082-0.41569-0.05429-0.5759-0.1882-0.65366-0.62305-1.1666-1.2305-1.4516-1.095-0.51369-2.4068-0.04253-2.9383 1.0554-0.137 0.28296-0.18935 0.46815-0.21565 0.76289-0.037379 0.41886 0.027773 0.78742 0.20603 1.1655 0.32423 0.68766 0.94668 1.1441 1.7278 1.267 0.14627 0.02302 0.4702 0.01324 0.66045-0.01995zm-3.8558-3.3108c0.07934-0.27474 0.35375-0.80932 0.54858-1.0687 0.56521-0.75242 1.3949-1.2637 2.3042-1.42 0.33569-0.0577 0.88574-0.06298 1.2027-0.01153 0.79729 0.12931 1.4938 0.49626 2.0623 1.0865 0.38396 0.39864 0.68007 0.89011 0.8445 1.4017l0.06809 0.21183h5.6316l-0.01606-0.06657c-0.0089-0.03661-0.06992-0.22338-0.13573-0.41503-0.38829-1.1306-0.95495-2.1324-1.7336-3.0651-0.28272-0.33863-0.92631-0.9668-1.2869-1.2561-0.87974-0.70577-1.8343-1.2312-2.9045-1.5989-1.0022-0.34431-1.9727-0.50441-3.063-0.50533-0.95648-8.08e-4 -1.7867 0.11516-2.6669 0.37253-2.6501 0.77486-4.8616 2.6858-5.9872 5.1734-0.15714 0.3473-0.35036 0.85231-0.44433 1.1613l-0.060735 0.19972h5.5793z"/>,
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,429 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="robot_with_extensions.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="8"
inkscape:cx="-67.4375"
inkscape:cy="-37.3125"
inkscape:window-width="3840"
inkscape:window-height="1537"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
d="m 11.275729,23.995059 c -0.0466,-0.0052 -0.22091,-0.02108 -0.38734,-0.03518 -0.83761,-0.07102 -1.8157003,-0.2757 -2.6638003,-0.55746 -0.99337,-0.33 -2.0568,-0.85274 -2.9284,-1.4395 -1.0958,-0.73769 -2.133,-1.7267 -2.9179,-2.7823 -2.97830002,-4.0056 -3.17620002,-9.4130001 -0.49864,-13.6269999 0.35679,-0.56156 0.87047,-1.2299 1.2841,-1.6706 0.0892,-0.09505 0.20504,-0.22008 0.25743,-0.27784 0.05238,-0.05776 0.2424,-0.23938 0.42225,-0.40359 1.3391,-1.2226 2.7611,-2.0579 4.4435,-2.61020001 1.9327003,-0.63444 4.0413003,-0.76202 6.0392003,-0.36539 2.0143,0.3999 3.8194,1.26580001 5.4239,2.60190001 0.36962,0.30779 1.0918,1.0271 1.4083,1.4028 0.63425,0.75277 1.1109,1.473 1.5542,2.3483 0.67626,1.3353 1.0646,2.6463998 1.2416,4.1920999 0.06671,0.58251 0.0594,1.9845 -0.01355,2.5846 -0.36085,2.9717 -1.7049,5.5833 -3.8955,7.5692 -1.4391,1.3046 -3.2226,2.2537 -5.1295,2.7296 -0.99296,0.2478 -1.6763,0.33244 -2.7683,0.34288 -0.43274,0.0042 -0.82492,0.0033 -0.87152,-0.002 z m 1.8036,-2.5389 c 1.8044,-0.21732 3.4498,-0.90148 4.866,-2.0232 0.40107,-0.31769 1.1639,-1.0808 1.4876,-1.4881 1.3561,-1.7066 2.0832,-3.7948 2.0832,-5.9832 v -0.39593 h -6.1032 l -0.02699,0.07868 c -0.01484,0.04328 -0.08526,0.19307 -0.15648,0.33287 -0.45518,0.89359 -1.2836,1.5814 -2.2295,1.8512 -0.66429,0.18945 -1.3948,0.19028 -2.0434,0.0023 -1.0497003,-0.30419 -1.9323003,-1.0917 -2.3553003,-2.1017 l -0.06845,-0.16341 h -6.0159 l -0.01721,0.17137 c -0.02057,0.20481 0.0026,0.77456 0.05079,1.2499 0.13047,1.2866 0.54474,2.5818 1.1908,3.7231 0.0603,0.10651 0.22142,0.36253 0.35805,0.56891 1.5419,2.3291 4.0632,3.8657 6.8494003,4.1743 0.51174,0.05666 0.53503,0.05764 1.1983,0.04883 0.35506,-0.0047 0.75103,-0.02429 0.93204,-0.04609 z m -0.73157,-8.9338 c 0.71565,-0.1248 1.3101,-0.56708 1.6271,-1.2106 0.17846,-0.36228 0.21736,-0.53994 0.21736,-0.99264 0,-0.3291101 -0.0082,-0.4156901 -0.05429,-0.5759001 -0.1882,-0.65366 -0.62305,-1.1666 -1.2305,-1.4516 -1.095,-0.5136898 -2.4068,-0.04253 -2.9383003,1.0554 -0.137,0.28296 -0.18935,0.46815 -0.21565,0.7628901 -0.03738,0.41886 0.02777,0.78742 0.20603,1.1655 0.3242303,0.68766 0.9466803,1.1441 1.7278003,1.267 0.14627,0.02302 0.4702,0.01324 0.66045,-0.01995 z M 8.4919587,9.2115589 c 0.07934,-0.27474 0.35375,-0.80932 0.54858,-1.0687 0.56521,-0.7524198 1.3949003,-1.2636998 2.3042003,-1.4199998 0.33569,-0.0577 0.88574,-0.06298 1.2027,-0.01153 0.79729,0.12931 1.4938,0.49626 2.0623,1.0865 0.38396,0.3986398 0.68007,0.8901098 0.8445,1.4016998 l 0.06809,0.21183 h 5.6316 l -0.01606,-0.06657 c -0.0089,-0.03661 -0.06992,-0.22338 -0.13573,-0.41503 -0.38829,-1.1305998 -0.95495,-2.1323998 -1.7336,-3.0650998 -0.28272,-0.33863 -0.92631,-0.9668 -1.2869,-1.2561 -0.87974,-0.70577 -1.8343,-1.2312 -2.9045,-1.5989 -1.0022,-0.34431 -1.9727,-0.50441 -3.063,-0.50533 -0.95648,-8.08e-4 -1.7867,0.11516 -2.6669003,0.37253 -2.6501,0.77486 -4.8616,2.6858 -5.9872,5.1733998 -0.15714,0.3473 -0.35036,0.85231 -0.44433,1.1613 l -0.06074,0.19972 h 5.5793 z"
id="path1" />
<path
id="path8"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 33.703355,4.1295207 a 4,4 0 0 0 -4,4 4,4 0 0 0 4,4.0000003 4,4 0 0 0 4,-4.0000003 4,4 0 0 0 -4,-4 z m 0,1 a 3,3 0 0 1 3,3 3,3 0 0 1 -3,3.0000003 3,3 0 0 1 -3,-3.0000003 3,3 0 0 1 3,-3 z" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="path13"
cx="52.71875"
cy="-27.75"
r="12" />
<path
id="path8-4-7"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m -23.541016,-25.75 a 12,12 0 0 1 -0.505859,0.927735 3,3 0 0 1 0.6875,1.910156 3,3 0 0 1 -3,3 3,3 0 0 1 -1.902344,-0.691406 12,12 0 0 1 -0.925781,0.505859 4,4 0 0 0 2.828125,1.185547 4,4 0 0 0 4,-4 4,4 0 0 0 -1.181641,-2.837891 z" />
<path
id="path8-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="M 21.830078 18.84375 A 12 12 0 0 1 20.998047 19.927734 A 3 3 0 0 1 21 20 A 3 3 0 0 1 18 23 A 3 3 0 0 1 17.091797 22.857422 A 12 12 0 0 1 15.818359 23.351562 A 4 4 0 0 0 18 24 A 4 4 0 0 0 22 20 A 4 4 0 0 0 21.830078 18.84375 z " />
<path
id="path8-2-8"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 41.011719,22 a 12,12 0 0 1 -0.832031,1.083984 3,3 0 0 1 0.002,0.07227 3,3 0 0 1 -3,3 A 3,3 0 0 1 36.273438,26.013672 12,12 0 0 1 35,26.507812 a 4,4 0 0 0 2.181641,0.648438 4,4 0 0 0 4,-4 A 4,4 0 0 0 41.011719,22 Z" />
<path
d="m -12.989,-1.4999407 v 0.52500001 h -1.05 v 1.04999982 h 1.05 v 0.52500002 l 1.05,-1.04999998 z"
id="path1-1"
style="stroke-width:0.999996" />
<path
id="path14-5"
style="opacity:0.502427;fill:#ff9dfb;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="M -38.25,-15 A 13.75,13.75 0 0 0 -52,-1.25 13.75,13.75 0 0 0 -38.25,12.5 13.75,13.75 0 0 0 -24.5,-1.25 13.75,13.75 0 0 0 -38.25,-15 Z m 0,0.75 a 12.999999,12.999999 0 0 1 13,13 12.999999,12.999999 0 0 1 -13,13 12.999999,12.999999 0 0 1 -13,-13 12.999999,12.999999 0 0 1 13,-13 z" />
<path
d="m -3.871875,-5.296875 v 0.525 1.0499998 0.525 l 1.05,-1.0499999 z"
id="path1-1-4"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
id="path14-6"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="M 11.810547,-10 A 13.75,13.75 0 0 0 9,-7.1894531 V -6.65625 H 9.5332031 A 12.999999,12.999999 0 0 1 12.34375,-9.4667969 V -10 Z" />
<path
d="M 10.484924,-6.6568941 10.113693,-7.0281252 9.371231,-7.7705872 9,-8.1418182 v 1.4849242 z"
id="path1-1-4-2-8"
style="opacity:0.557039;stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
d="m 10.858,-9.9999999 0.371231,0.371231 0.742462,0.742462 0.371231,0.3712311 0,-1.4849242 z"
id="path1-1-4-29-3"
style="opacity:0.475728;stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
id="path14-3"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="M 7.9105469,-15.9 A 13.75,13.75 0 0 0 5.1,-13.089453 v 0.533203 H 5.6332031 A 12.999999,12.999999 0 0 1 8.44375,-15.366797 V -15.9 Z" />
<path
d="M 6.4849241,-12.557076 6.113693,-12.928307 5.3712311,-13.670769 5,-14.042 v 1.484924 z"
id="path1-1-4-2-7"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
d="m 6.958,-16 0.3712311,0.371231 0.7424619,0.742462 0.371231,0.371231 2e-7,-1.484924 z"
id="path1-1-4-29-7"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
id="path14-5-8"
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="M 4.6269531 0.40039062 A 13.75 13.75 0 0 0 0.40039062 4.6269531 L 0.40039062 5.09375 L 1.0234375 5.09375 A 12.999999 12.999999 0 0 1 5.09375 1.0253906 L 5.09375 0.40039062 L 4.6269531 0.40039062 z " />
<path
d="M 3.9366289,0.07950854 4.2395709,0.50828728 4.8454547,1.3658443 5.1483968,1.7946232 5.4000702,0.33118186 Z"
id="path1-1-4-0"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
d="M 1.7936061,5.1549238 1.367165,4.8486998 0.51428337,4.2362521 0.08784235,3.9300282 0.32827636,5.3953579 Z"
id="path1-1-4-0-2"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
d="m 63.275747,8.995083 c -0.0466,-0.0052 -0.22091,-0.02108 -0.38734,-0.03518 -0.83761,-0.07102 -1.8157,-0.2757 -2.6638,-0.55746 -0.99337,-0.33 -2.0568,-0.85274 -2.9284,-1.4395 -1.0958,-0.73769 -2.133,-1.7267 -2.9179,-2.7823 -2.9783,-4.0056 -3.1762,-9.4130001 -0.49864,-13.6269999 0.35679,-0.5615601 0.87047,-1.2299001 1.2841,-1.6706001 0.0892,-0.09505 0.20504,-0.22008 0.25743,-0.27784 0.05238,-0.05776 0.2424,-0.23938 0.42225,-0.40359 1.3391,-1.2226 2.7611,-2.0579 4.4435,-2.6102 1.9327,-0.63444 4.0413,-0.76202 6.0392,-0.36539 2.0143,0.3999 3.8194,1.2658 5.4239,2.6019 0.36962,0.30779 1.0918,1.0271 1.4083,1.4028 0.63425,0.75277 1.1109,1.4730001 1.5542,2.3483001 0.67626,1.3353 1.0646,2.6463998 1.2416,4.1920999 0.06671,0.58251 0.0594,1.9845 -0.01355,2.5846 -0.36085,2.9717 -1.7049,5.5833 -3.8955,7.5692 -1.4391,1.3046 -3.2226,2.2537 -5.1295,2.7296 -0.99296,0.2478 -1.6763,0.33244 -2.7683,0.34288 -0.43274,0.0042 -0.82492,0.0033 -0.87152,-0.002 z m 1.8036,-2.5389 c 1.8044,-0.21732 3.4498,-0.90148 4.866,-2.0232 0.40107,-0.31769 1.1639,-1.0808 1.4876,-1.4881 1.3561,-1.7066 2.0832,-3.7948 2.0832,-5.9832 v -0.39593 h -6.1032 l -0.02699,0.07868 c -0.01484,0.04328 -0.08526,0.19307 -0.15648,0.33287 -0.45518,0.89359 -1.2836,1.5814 -2.2295,1.8512 -0.66429,0.18945 -1.3948,0.19028 -2.0434,0.0023 -1.0497,-0.30419 -1.9323,-1.0917 -2.3553,-2.1017 l -0.06845,-0.16341 h -6.0159 l -0.01721,0.17137 c -0.02057,0.20481 0.0026,0.77456 0.05079,1.2499 0.13047,1.2866 0.54474,2.5818 1.1908,3.7231 0.0603,0.10651 0.22142,0.36253 0.35805,0.56891 1.5419,2.3291 4.0632,3.8657 6.8494,4.1743 0.51174,0.05666 0.53503,0.05764 1.1983,0.04883 0.35506,-0.0047 0.75103,-0.02429 0.93204,-0.04609 z m -0.73157,-8.9338 c 0.71565,-0.1248 1.3101,-0.56708 1.6271,-1.2106 0.17846,-0.36228 0.21736,-0.53994 0.21736,-0.99264 0,-0.3291101 -0.0082,-0.4156901 -0.05429,-0.5759001 -0.1882,-0.65366 -0.62305,-1.1666 -1.2305,-1.4516 -1.095,-0.5136898 -2.4068,-0.04253 -2.9383,1.0554 -0.137,0.28296 -0.18935,0.46815 -0.21565,0.7628901 -0.03738,0.41886 0.02777,0.78742 0.20603,1.1655 0.32423,0.68766 0.94668,1.1441 1.7278,1.267 0.14627,0.02302 0.4702,0.01324 0.66045,-0.01995 z m -3.8558,-3.3108001 c 0.07934,-0.27474 0.35375,-0.80932 0.54858,-1.0687 0.56521,-0.7524198 1.3949,-1.2636998 2.3042,-1.4199998 0.33569,-0.0577 0.88574,-0.06298 1.2027,-0.01153 0.79729,0.12931 1.4938,0.49626 2.0623,1.0865 0.38396,0.3986398 0.68007,0.8901098 0.8445,1.4016998 l 0.06809,0.21183 h 5.6316 l -0.01606,-0.06657 c -0.0089,-0.03661 -0.06992,-0.22338 -0.13573,-0.41503 -0.38829,-1.1305998 -0.95495,-2.1323998 -1.7336,-3.0650998 -0.28272,-0.33863 -0.92631,-0.9668001 -1.2869,-1.2561001 -0.87974,-0.70577 -1.8343,-1.2312 -2.9045,-1.5989 -1.0022,-0.34431 -1.9727,-0.50441 -3.063,-0.50533 -0.95648,-8.08e-4 -1.7867,0.11516 -2.6669,0.37253 -2.6501,0.77486 -4.8616,2.6858001 -5.9872,5.1733999 -0.15714,0.3473 -0.35036,0.85231 -0.44433,1.1613 l -0.06074,0.19972 h 5.5793 z"
id="path1-9" />
<path
id="path8-2-8-1"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 2.169923,18.844 a 12,12 0 0 0 0.832031,1.083984 3,3 0 0 0 -0.002,0.07227 3,3 0 0 0 3,3 3,3 0 0 0 0.90825,-0.142582 12,12 0 0 0 1.273438,0.49414 4,4 0 0 1 -2.181641,0.648438 4,4 0 0 1 -4,-4 4,4 0 0 1 0.169922,-1.15625 z" />
<g
id="g1"
transform="rotate(45,102.33944,9.5450195)">
<g
id="g3"
transform="rotate(15,44.643611,39.877998)">
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1"
width="0.5"
height="2"
x="41.730083"
y="40.000004" />
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-6"
width="0.5"
height="2"
x="-57.740784"
y="13.140041"
transform="rotate(-120)" />
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-6-2"
width="0.5"
height="2"
x="15.259476"
y="-59.573814"
transform="rotate(120)" />
</g>
</g>
<g
id="g2"
transform="matrix(0.71742591,0.71742591,-0.71742591,0.71742591,5.4392116,-17.97936)"
style="stroke-width:0.985616">
<rect
style="fill:#000000;stroke-width:0.985616;paint-order:markers fill stroke"
id="rect2"
width="1"
height="3"
x="25.062002"
y="2.888083" />
<circle
style="fill:#000000;stroke-width:0.985614;paint-order:markers fill stroke"
id="path2"
cx="25.562002"
cy="2.9135733"
r="0.5" />
</g>
<g
id="g2-6"
transform="rotate(45,95.903475,-1.6716653)">
<g
id="g3-0"
transform="rotate(15,44.643611,39.877998)">
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-5"
width="0.5"
height="2"
x="41.730083"
y="40.000004" />
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-6-8"
width="0.5"
height="2"
x="-57.740784"
y="13.140041"
transform="rotate(-120)" />
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-6-2-4"
width="0.5"
height="2"
x="15.259476"
y="-59.573814"
transform="rotate(120)" />
</g>
</g>
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-5-7"
width="0.5"
height="2"
x="14.393301"
y="-32.929928"
transform="rotate(60)" />
<rect
style="fill:#000000;paint-order:markers fill stroke"
id="rect1-6-8-8"
width="0.5"
height="2"
x="19.086782"
y="25.93066"
transform="rotate(-60)" />
<path
id="rect1-6-2-4-2"
style="paint-order:markers fill stroke"
d="M 30.5,-3 H 30 l 0.125,-2 h 0.25 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2"
style="paint-order:markers fill stroke"
d="m 39.304463,-3.447373 h -0.5 l 0.125,-2 h 0.25 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7"
style="paint-order:markers fill stroke"
d="m 40.7254,-6.7733017 0.25,0.433013 -1.79455,0.891747 -0.125,-0.216507 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-3"
style="paint-order:markers fill stroke"
d="m 37.137881,-6.3404403 0.25,-0.4330119 1.66955,1.108252 -0.125,0.216506 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2-0"
style="paint-order:markers fill stroke"
d="m 21.517897,-6.6080337 0.499985,0.00398 -0.140914,1.9989415 -0.249992,-0.00199 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-9"
style="paint-order:markers fill stroke"
d="m 20.070534,-3.2935195 -0.246546,-0.4349883 1.801591,-0.8774369 0.123274,0.2174941 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-3-3"
style="paint-order:markers fill stroke"
d="m 23.661384,-3.6978141 -0.253438,0.4310085 -1.660677,-1.1215048 0.126719,-0.2155042 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2-0-5"
style="paint-order:markers fill stroke"
d="m 19.372102,-11.309498 -0.405097,0.293082 -1.071057,-1.693654 0.202549,-0.146537 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2-0-5-0"
style="paint-order:markers fill stroke"
d="m 16.351223,-11.436048 -0.293082,-0.405097 1.693654,-1.071057 0.146543,0.202547 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2-0-5-7"
style="paint-order:markers fill stroke"
d="m 19.502982,-14.322119 0.293084,0.405096 -1.693654,1.071057 -0.146543,-0.202547 z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-2-0-5-9"
style="paint-order:markers fill stroke"
d="m 16.486381,-14.451817 0.405098,-0.293082 1.071056,1.693653 -0.202548,0.146538 z"
sodipodi:nodetypes="ccccc" />
<g
id="g5"
transform="matrix(0.33736342,-1.374209,1.374209,0.33736342,11.396338,31.987101)"
style="stroke-width:0.706707">
<path
id="rect1-6-2-4-2-2-0-9"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 21.849385,0.00687605 22.34937,0.01085699 22.208456,2.0097985 21.958464,2.0078107 Z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-9-0"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 20.402022,3.3213905 20.155476,2.8864022 21.957067,2.0089653 22.080341,2.2264594 Z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-3-3-2"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 23.992872,2.9170958 23.739434,3.3481043 22.078758,2.2265996 22.205477,2.0110954 Z"
sodipodi:nodetypes="ccccc" />
</g>
<path
d="m -87.724253,0.995083 c -0.0466,-0.0052 -0.22091,-0.02108 -0.38734,-0.03518 -0.83761,-0.07102 -1.8157,-0.2757 -2.6638,-0.55746 -0.99337,-0.33 -2.0568,-0.85274 -2.9284,-1.4395 -1.0958,-0.73769 -2.133,-1.7267 -2.9179,-2.7823 -2.9783,-4.0056 -3.1762,-9.413 -0.49864,-13.627 0.35679,-0.56156 0.87047,-1.2299 1.2841,-1.6706 0.0892,-0.09505 0.20504,-0.22008 0.25743,-0.27784 0.05238,-0.05776 0.2424,-0.23938 0.42225,-0.40359 1.3391,-1.2226 2.7611,-2.0579 4.4435,-2.6102 1.9327,-0.63444 4.0413,-0.76202 6.0392,-0.36539 2.0143,0.3999 3.8194,1.2658 5.4239,2.6019 0.36962,0.30779 1.0918,1.0271 1.4083,1.4028 0.63425,0.75277 1.1109,1.473 1.5542,2.3483 0.67626,1.3353 1.0646,2.6464 1.2416,4.1921 0.06671,0.58251 0.0594,1.9845 -0.01355,2.5846 -0.36085,2.9717 -1.7049,5.5833 -3.8955,7.5692 -1.4391,1.3046 -3.2226,2.2537 -5.1295,2.7296 -0.99296,0.2478 -1.6763,0.33244 -2.7683,0.34288 -0.43274,0.0042 -0.82492,0.0033 -0.87152,-0.002 z m 1.8036,-2.5389 c 1.8044,-0.21732 3.4498,-0.90148 4.866,-2.0232 0.40107,-0.31769 1.1639,-1.0808 1.4876,-1.4881 1.3561,-1.7066 2.0832,-3.7948 2.0832,-5.9832 v -0.39593 h -6.1032 l -0.02699,0.07868 c -0.01484,0.04328 -0.08526,0.19307 -0.15648,0.33287 -0.45518,0.89359 -1.2836,1.5814 -2.2295,1.8512 -0.66429,0.18945 -1.3948,0.19028 -2.0434,0.0023 -1.0497,-0.30419 -1.9323,-1.0917 -2.3553,-2.1017 l -0.06845,-0.16341 h -6.0159 l -0.01721,0.17137 c -0.02057,0.20481 0.0026,0.77456 0.05079,1.2499 0.13047,1.2866 0.54474,2.5818 1.1908,3.7231 0.0603,0.10651 0.22142,0.36253 0.35805,0.56891 1.5419,2.3291 4.0632,3.8657 6.8494,4.1743 0.51174,0.05666 0.53503,0.05764 1.1983,0.04883 0.35506,-0.0047 0.75103,-0.02429 0.93204,-0.04609 z m -0.73157,-8.9338 c 0.71565,-0.1248 1.3101,-0.56708 1.6271,-1.2106 0.17846,-0.36228 0.21736,-0.53994 0.21736,-0.99264 0,-0.32911 -0.0082,-0.41569 -0.05429,-0.5759 -0.1882,-0.65366 -0.62305,-1.1666 -1.2305,-1.4516 -1.095,-0.51369 -2.4068,-0.04253 -2.9383,1.0554 -0.137,0.28296 -0.18935,0.46815 -0.21565,0.76289 -0.03738,0.41886 0.02777,0.78742 0.20603,1.1655 0.32423,0.68766 0.94668,1.1441 1.7278,1.267 0.14627,0.02302 0.4702,0.01324 0.66045,-0.01995 z m -3.8558,-3.3108 c 0.07934,-0.27474 0.35375,-0.80932 0.54858,-1.0687 0.56521,-0.75242 1.3949,-1.2637 2.3042,-1.42 0.33569,-0.0577 0.88574,-0.06298 1.2027,-0.01153 0.79729,0.12931 1.4938,0.49626 2.0623,1.0865 0.38396,0.39864 0.68007,0.89011 0.8445,1.4017 l 0.06809,0.21183 h 5.6316 l -0.01606,-0.06657 c -0.0089,-0.03661 -0.06992,-0.22338 -0.13573,-0.41503 -0.38829,-1.1306 -0.95495,-2.1324 -1.7336,-3.0651 -0.28272,-0.33863 -0.92631,-0.9668 -1.2869,-1.2561 -0.87974,-0.70577 -1.8343,-1.2312 -2.9045,-1.5989 -1.0022,-0.34431 -1.9727,-0.50441 -3.063,-0.50533 -0.95648,-8.08e-4 -1.7867,0.11516 -2.6669,0.37253 -2.6501,0.77486 -4.8616,2.6858 -5.9872,5.1734 -0.15714,0.3473 -0.35036,0.85231 -0.44433,1.1613 l -0.06074,0.19972 h 5.5793 z"
id="path1-3" />
<g
id="g2-7"
transform="matrix(0.71742591,0.71742591,-0.71742591,0.71742591,-93.56077,-40.979336)"
style="stroke-width:0.985616">
<rect
style="fill:#000000;stroke-width:0.985616;paint-order:markers fill stroke"
id="rect2-6"
width="1"
height="3"
x="25.062002"
y="2.888083" />
<circle
style="fill:#000000;stroke-width:0.985614;paint-order:markers fill stroke"
id="path2-4"
cx="25.562002"
cy="2.9135733"
r="0.5" />
</g>
<g
id="g5-8"
transform="matrix(0.33736342,-1.374209,1.374209,0.33736342,-87.603644,8.987125)"
style="stroke-width:0.706707">
<path
id="rect1-6-2-4-2-2-0-9-4"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 21.849385,0.00687605 22.34937,0.01085699 22.208456,2.0097985 21.958464,2.0078107 Z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-9-0-8"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 20.402022,3.3213905 20.155476,2.8864022 21.957067,2.0089653 22.080341,2.2264594 Z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1-6-2-4-2-7-3-3-2-0"
style="stroke-width:0.706707;paint-order:markers fill stroke"
d="M 23.992872,2.9170958 23.739434,3.3481043 22.078758,2.2265996 22.205477,2.0110954 Z"
sodipodi:nodetypes="ccccc" />
</g>
<path
d="m -34.724253,-19.004917 c -0.0466,-0.0052 -0.22091,-0.02108 -0.38734,-0.03518 -0.83761,-0.07102 -1.8157,-0.2757 -2.6638,-0.55746 -0.99337,-0.33 -2.0568,-0.85274 -2.9284,-1.4395 -1.0958,-0.73769 -2.133,-1.7267 -2.9179,-2.7823 -2.9783,-4.0056 -3.1762,-9.413 -0.49864,-13.627 0.35679,-0.56156 0.87047,-1.2299 1.2841,-1.6706 0.0892,-0.09505 0.20504,-0.22008 0.25743,-0.27784 0.05238,-0.05776 0.2424,-0.23938 0.42225,-0.40359 1.3391,-1.2226 2.7611,-2.0579 4.4435,-2.6102 1.9327,-0.63444 4.0413,-0.76202 6.0392,-0.36539 2.0143,0.3999 3.8194,1.2658 5.4239,2.6019 0.36962,0.30779 1.0918,1.0271 1.4083,1.4028 0.63425,0.75277 1.1109,1.473 1.5542,2.3483 0.67626,1.3353 1.0646,2.6464 1.2416,4.1921 0.06671,0.58251 0.0594,1.9845 -0.01355,2.5846 -0.36085,2.9717 -1.7049,5.5833 -3.8955,7.5692 -1.4391,1.3046 -3.2226,2.2537 -5.1295,2.7296 -0.99296,0.2478 -1.6763,0.33244 -2.7683,0.34288 -0.43274,0.0042 -0.82492,0.0033 -0.87152,-0.002 z m 1.8036,-2.5389 c 1.8044,-0.21732 3.4498,-0.90148 4.866,-2.0232 0.40107,-0.31769 1.1639,-1.0808 1.4876,-1.4881 1.3561,-1.7066 2.0832,-3.7948 2.0832,-5.9832 v -0.39593 h -6.1032 l -0.02699,0.07868 c -0.01484,0.04328 -0.08526,0.19307 -0.15648,0.33287 -0.45518,0.89359 -1.2836,1.5814 -2.2295,1.8512 -0.66429,0.18945 -1.3948,0.19028 -2.0434,0.0023 -1.0497,-0.30419 -1.9323,-1.0917 -2.3553,-2.1017 l -0.06845,-0.16341 h -6.0159 l -0.01721,0.17137 c -0.02057,0.20481 0.0026,0.77456 0.05079,1.2499 0.13047,1.2866 0.54474,2.5818 1.1908,3.7231 0.0603,0.10651 0.22142,0.36253 0.35805,0.56891 1.5419,2.3291 4.0632,3.8657 6.8494,4.1743 0.51174,0.05666 0.53503,0.05764 1.1983,0.04883 0.35506,-0.0047 0.75103,-0.02429 0.93204,-0.04609 z m -0.73157,-8.9338 c 0.71565,-0.1248 1.3101,-0.56708 1.6271,-1.2106 0.17846,-0.36228 0.21736,-0.53994 0.21736,-0.99264 0,-0.32911 -0.0082,-0.41569 -0.05429,-0.5759 -0.1882,-0.65366 -0.62305,-1.1666 -1.2305,-1.4516 -1.095,-0.51369 -2.4068,-0.04253 -2.9383,1.0554 -0.137,0.28296 -0.18935,0.46815 -0.21565,0.76289 -0.03738,0.41886 0.02777,0.78742 0.20603,1.1655 0.32423,0.68766 0.94668,1.1441 1.7278,1.267 0.14627,0.02302 0.4702,0.01324 0.66045,-0.01995 z m -3.8558,-3.3108 c 0.07934,-0.27474 0.35375,-0.80932 0.54858,-1.0687 0.56521,-0.75242 1.3949,-1.2637 2.3042,-1.42 0.33569,-0.0577 0.88574,-0.06298 1.2027,-0.01153 0.79729,0.12931 1.4938,0.49626 2.0623,1.0865 0.38396,0.39864 0.68007,0.89011 0.8445,1.4017 l 0.06809,0.21183 h 5.6316 l -0.01606,-0.06657 c -0.0089,-0.03661 -0.06992,-0.22338 -0.13573,-0.41503 -0.38829,-1.1306 -0.95495,-2.1324 -1.7336,-3.0651 -0.28272,-0.33863 -0.92631,-0.9668 -1.2869,-1.2561 -0.87974,-0.70577 -1.8343,-1.2312 -2.9045,-1.5989 -1.0022,-0.34431 -1.9727,-0.50441 -3.063,-0.50533 -0.95648,-8.08e-4 -1.7867,0.11516 -2.6669,0.37253 -2.6501,0.77486 -4.8616,2.6858 -5.9872,5.1734 -0.15714,0.3473 -0.35036,0.85231 -0.44433,1.1613 l -0.06074,0.19972 h 5.5793 z"
id="path1-4" />
<path
id="path8-2-4"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m -0.04490405,-29.031226 a 12,12 0 0 1 -0.832031,1.083984 3,3 0 0 1 0.002,0.07227 3,3 0 0 1 -3.00000005,3 3,3 0 0 1 -0.908203,-0.142578 12,12 0 0 1 -1.273438,0.49414 4,4 0 0 0 2.181641,0.648438 4,4 0 0 0 4.00000005,-4 4,4 0 0 0 -0.169922,-1.15625 z" />
<path
id="path14-5-8-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m -41.373029,-42.599585 a 13.75,13.75 0 0 0 -4.226562,4.226562 v 0.466797 h 0.623046 a 12.999999,12.999999 0 0 1 4.070313,-4.068359 v -0.625 z" />
<path
d="m -42.063353,-42.920467 0.302942,0.428778 0.605884,0.857557 0.302942,0.428779 0.251673,-1.463441 z"
id="path1-1-4-0-7"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
d="m -44.206376,-37.845052 -0.426441,-0.306224 -0.852882,-0.612448 -0.426441,-0.306224 0.240434,1.46533 z"
id="path1-1-4-0-2-0"
style="stroke-width:0.999996"
sodipodi:nodetypes="cccccc" />
<path
id="path8-2-8-1-0"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m -43.830059,-24.155976 a 12,12 0 0 0 0.832031,1.083984 3,3 0 0 0 -0.002,0.07227 3,3 0 0 0 3,3 3,3 0 0 0 0.90825,-0.142582 12,12 0 0 0 1.273438,0.49414 4,4 0 0 1 -2.181641,0.648438 4,4 0 0 1 -4,-4 4,4 0 0 1 0.169922,-1.15625 z" />
<circle
style="fill:#000000;stroke-width:1;paint-order:markers fill stroke"
id="path5"
cx="-12.412782"
cy="-11.375501"
r="5" />
<circle
style="fill:#ffffff;stroke-width:1;paint-order:markers fill stroke"
id="path6"
cx="-12.412782"
cy="-11.375501"
r="4.2857141" />
<path
id="path5-3"
style="fill:#000000;stroke-width:1;paint-order:markers fill stroke"
d="m -37.562497,16.205219 a 5,5 0 0 0 -5,5 5,5 0 0 0 5,5 5,5 0 0 0 5,-5 5,5 0 0 0 -5,-5 z m 0,0.712891 a 4.2857141,4.2857141 0 0 1 4.285156,4.287109 4.2857141,4.2857141 0 0 1 -4.285156,4.285156 4.2857141,4.2857141 0 0 1 -4.285156,-4.285156 4.2857141,4.2857141 0 0 1 4.285156,-4.287109 z" />
<circle
style="fill:#ffffff;stroke-width:1;paint-order:markers fill stroke"
id="path6-2-3"
cx="-62.807026"
cy="8.4191523"
r="4.2857141" />
<path
id="path5-3-4"
style="fill:#000000;stroke-width:1;paint-order:markers fill stroke"
d="m 87.343375,-56.9206 a 5,5 0 0 0 -5,5 5,5 0 0 0 5,4.999999 5,5 0 0 0 5,-4.999999 5,5 0 0 0 -5,-5 z m 0,0.712891 a 4.2857141,4.2857141 0 0 1 4.285156,4.287109 4.2857141,4.2857141 0 0 1 -4.285156,4.285156 4.2857141,4.2857141 0 0 1 -4.285156,-4.285156 4.2857141,4.2857141 0 0 1 4.285156,-4.287109 z" />
<circle
style="fill:#ffffff;stroke-width:1;paint-order:markers fill stroke"
id="path6-2-3-9"
cx="75.234169"
cy="-55.809689"
r="4.2857141" />
<path
d="m 26.529001,-62.9998 c -0.797111,-0.0023 -1.594849,0.12204 -2.349837,0.369864 -1.051482,0.345178 -1.940155,0.866726 -2.777079,1.630829 -0.112405,0.102628 -0.230934,0.216581 -0.263671,0.25268 -0.03274,0.0361 -0.105379,0.113928 -0.161128,0.173333 -0.258515,0.275429 -0.580222,0.692729 -0.803237,1.043658 -1.631661,2.567926 -1.553287,5.844832 0.176997,8.327261 -0.06695,0.227949 -0.101067,0.464277 -0.101317,0.701855 0,1.380672 1.119269,2.499929 2.499959,2.499929 0.474912,-6.55e-4 0.939815,-0.136569 1.340291,-0.391834 0.01651,0.0056 0.03361,0.01285 0.05004,0.01832 0.530053,0.176094 1.141542,0.303503 1.665035,0.34789 0.104017,0.0089 0.212572,0.01872 0.241695,0.02197 v 0.0012 c 0.02914,0.0033 0.273964,0.0038 0.544424,0.0012 0.682487,-0.0064 1.110356,-0.05997 1.730909,-0.214838 0.198464,-0.04953 0.393598,-0.110184 0.587146,-0.175775 0.400476,0.255264 0.865378,0.391177 1.340291,0.391832 1.380689,0 2.499959,-1.119257 2.499959,-2.499928 1.38e-4,-0.234556 -0.03274,-0.467963 -0.09766,-0.693355 l 0.0012,-0.0012 c 0.708176,-1.010159 1.154731,-2.181312 1.309792,-3.458151 0.04559,-0.375051 0.05023,-1.250902 0.0086,-1.614954 -0.110604,-0.966005 -0.35368,-1.786232 -0.776342,-2.620771 -0.277072,-0.547029 -0.575279,-0.996767 -0.971686,-1.467253 -0.197809,-0.234806 -0.649114,-0.684106 -0.880109,-0.876413 -1.002797,-0.835038 -2.130905,-1.376023 -3.389822,-1.625953 C 27.485203,-62.951364 27.007156,-62.998619 26.528913,-63 Z m -0.02075,1.564879 c 0.681427,5.75e-4 1.287667,0.100966 1.914033,0.316153 0.668863,0.229806 1.26654,0.558635 1.816344,0.999722 0.225366,0.180806 0.626491,0.573252 0.803237,0.784915 0.486648,0.582921 0.841298,1.208652 1.083982,1.915256 0.04113,0.119779 0.07988,0.2359 0.08545,0.258781 l 0.0098,0.04272 h -3.52047 l -0.04151,-0.133052 c -0.102762,-0.319736 -0.288577,-0.627295 -0.528549,-0.876413 -0.355307,-0.36889 -0.790738,-0.597878 -1.289041,-0.678669 -0.198099,-0.03216 -0.540918,-0.02873 -0.750739,0.0074 -0.568303,0.09769 -1.08717,0.417176 -1.440413,0.887412 -0.121767,0.162109 -0.293428,0.495999 -0.343014,0.667732 l -0.03662,0.125728 H 20.78448 l 0.03784,-0.125728 c 0.05873,-0.193188 0.178885,-0.508098 0.277096,-0.725179 0.703488,-1.554707 2.08634,-2.749296 3.742626,-3.233533 0.550117,-0.160851 1.068421,-0.233649 1.666223,-0.233149 z m 0.02929,3.488649 c 0.178291,0.0067 0.358688,0.04792 0.52978,0.128172 0.37965,0.178119 0.651426,0.49843 0.769048,0.906974 0.0288,0.100127 0.03295,0.154408 0.03295,0.360096 0,0.282929 -0.02397,0.394902 -0.135498,0.62132 -0.198121,0.402189 -0.569554,0.677605 -1.016859,0.755603 -0.118904,0.02075 -0.321176,0.02782 -0.412593,0.01343 -0.488188,-0.07681 -0.877669,-0.362437 -1.08029,-0.792225 -0.111411,-0.236292 -0.151535,-0.466961 -0.128174,-0.728728 0.01644,-0.184207 0.04864,-0.299217 0.134274,-0.476062 0.249133,-0.51464 0.772487,-0.80904 1.307353,-0.78854 z m -5.964281,2.175252 h 3.759688 l 0.04272,0.101316 c 0.264371,0.63123 0.816112,1.123342 1.472165,1.313462 0.405369,0.117483 0.861673,0.117184 1.276853,-0.0012 0.591179,-0.16862 1.108295,-0.598714 1.392791,-1.157216 0.04451,-0.08738 0.08959,-0.180465 0.09887,-0.207515 l 0.01586,-0.04882 h 3.814633 v 0.246573 c 0,1.367711 -0.453698,2.673549 -1.301228,3.740144 -0.202309,0.254554 -0.679489,0.731604 -0.930172,0.930161 -0.885111,0.701041 -1.912968,1.127593 -3.040701,1.263402 h -0.0012 c -0.113129,0.01362 -0.360356,0.02637 -0.582265,0.0293 -0.414537,0.0055 -0.428449,0.0048 -0.748298,-0.03052 -1.741387,-0.192931 -3.3173,-1.152966 -4.280971,-2.608612 -0.08539,-0.128988 -0.185695,-0.289869 -0.223381,-0.356436 -0.403782,-0.713293 -0.663053,-1.52252 -0.744614,-2.326621 -0.03012,-0.29708 -0.04459,-0.653232 -0.03174,-0.781229 l 0.01099,-0.106195 z m 0.302733,5.236662 c 0.422768,0.47656 0.918047,0.913411 1.434289,1.260964 0.304795,0.205188 0.647301,0.395463 0.998546,0.561508 -0.180902,0.05699 -0.369404,0.08623 -0.559073,0.08666 -1.035516,0 -1.874968,-0.83944 -1.874968,-1.874945 3e-4,-0.0114 7.05e-4,-0.02279 0.0012,-0.03418 z m 11.247316,0.0074 c 4.69e-4,0.009 8.58e-4,0.0179 0.0012,0.02686 0,1.035505 -0.839452,1.874948 -1.874968,1.874948 -0.186272,-6.35e-4 -0.371408,-0.02903 -0.549311,-0.08423 0.666926,-0.313684 1.286229,-0.722792 1.827344,-1.21334 0.211854,-0.192057 0.409494,-0.394063 0.595691,-0.604233 z"
id="path1-8"
style="stroke-width:0.999995"
sodipodi:nodetypes="cccccccccccccccscccccccsccsccccccccccccccccccccccccccssccccccccccccccccsccccccsccccccccccccccccscc" />
<path
id="path1-1-4-0-9"
style="stroke-width:0.999994"
d="m 23.28125,-70 v 1.474609 0.314454 a 9.6696451,9.6696451 0 0 0 -9.367187,9.396484 H 13.474609 12 l 2.960938,4.892578 2.939453,-4.894531 -1.476563,0.002 h -0.511719 a 7.6736912,7.6736912 0 0 1 7.369141,-7.398485 v 0.636719 1.47461 l 4.892578,-2.958985 z" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect2-0"
width="1.5"
height="24"
x="34.5"
y="-70" />
<path
d="m -25.989584,-89.999822 c -0.953946,-0.0028 -1.90864,0.146049 -2.812175,0.442627 -1.258367,0.413084 -2.321889,1.037236 -3.323481,1.95166 -0.13452,0.122818 -0.276371,0.259189 -0.315549,0.30239 -0.03918,0.0432 -0.126113,0.13634 -0.192831,0.207432 -0.309378,0.329614 -0.694353,0.82901 -0.961277,1.248975 -1.952771,3.073111 -1.858977,6.994678 0.211822,9.965473 a 2.9918355,2.9917366 0 0 0 -0.121252,0.83993 2.9918355,2.9917366 0 0 0 2.991836,2.991736 2.9918355,2.9917366 0 0 0 1.603998,-0.468917 c 0.01975,0.0068 0.04022,0.01536 0.05989,0.02191 0.634345,0.210738 1.366148,0.363212 1.992637,0.41633 0.124485,0.01057 0.254397,0.02241 0.289251,0.0263 v 0.0015 c 0.03485,0.0039 0.327868,0.0046 0.651539,0.0015 0.816773,-0.0077 1.328826,-0.07176 2.071473,-0.257103 0.586789,-0.146439 1.15642,-0.356337 1.700411,-0.616462 a 2.9918355,2.9917366 0 0 0 2.102137,0.875008 2.9918355,2.9917366 0 0 0 2.991836,-2.991736 2.9918355,2.9917366 0 0 0 -0.869203,-2.106483 c 0.421932,-0.885478 0.70184,-1.845749 0.825371,-2.863166 0.05456,-0.448834 0.06011,-1.496989 0.01024,-1.932661 -0.1324,-1.156086 -0.423296,-2.137674 -0.929127,-3.136391 -0.331571,-0.654666 -0.688442,-1.19288 -1.162851,-1.755925 -0.23673,-0.280999 -0.77683,-0.818688 -1.053275,-1.048828 -1.200101,-0.999314 -2.550166,-1.646726 -4.056779,-1.945825 -0.560378,-0.111248 -1.132485,-0.167799 -1.704822,-0.169452 z m -0.02483,1.872752 c 0.815499,6.88e-4 1.54102,0.120829 2.290625,0.37835 0.800465,0.275016 1.515812,0.668541 2.173718,1.196396 0.269707,0.216377 0.749753,0.685997 0.961277,0.93933 0.582397,0.697598 1.006826,1.44643 1.297258,2.292044 0.04922,0.143341 0.0956,0.283774 0.102263,0.311156 l 0.01168,0.04968 h -4.2131 l -0.04967,-0.159233 c -0.122982,-0.38264 -0.34536,-0.750705 -0.632545,-1.048832 -0.425214,-0.44146 -0.946318,-0.715526 -1.542666,-0.812182 -0.237073,-0.03848 -0.647321,-0.03439 -0.898448,0.0088 -0.680118,0.116902 -1.301073,0.499261 -1.72382,1.061991 -0.145725,0.193995 -0.35116,0.593542 -0.410502,0.799088 l -0.04382,0.150462 h -4.172198 l 0.04529,-0.150462 c 0.07028,-0.231104 0.21408,-0.607936 0.331615,-0.867753 0.841903,-1.860561 2.496837,-3.290162 4.479002,-3.869662 0.658354,-0.192495 1.278636,-0.279615 1.994058,-0.279016 z m 0.03505,4.174969 c 0.213368,0.0082 0.42926,0.05733 0.634014,0.153386 0.454348,0.213161 0.779598,0.597973 0.920364,1.086822 0.03447,0.119827 0.03945,0.183326 0.03945,0.429479 0,0.338589 -0.02867,0.472589 -0.162159,0.743552 -0.237102,0.48131 -0.681576,0.81091 -1.216928,0.904251 -0.1423,0.02482 -0.384368,0.03327 -0.493774,0.01608 -0.584247,-0.09192 -1.05036,-0.433727 -1.292848,-0.948083 -0.133331,-0.282778 -0.181349,-0.558841 -0.153392,-0.87209 0.01967,-0.220446 0.05822,-0.358081 0.160693,-0.569717 0.298149,-0.615887 0.924475,-0.968201 1.564579,-0.943669 z m -7.137771,2.603184 h 4.499421 l 0.05113,0.121247 c 0.316387,0.755414 0.976685,1.344338 1.761817,1.57186 0.485127,0.140596 1.03121,0.140237 1.528081,-0.0015 0.707493,-0.201794 1.326355,-0.716469 1.666826,-1.384875 0.05328,-0.104562 0.107222,-0.215966 0.118328,-0.248336 l 0.01899,-0.05844 h 4.56517 v 0.295061 c 0,1.636778 -0.54299,3.199512 -1.557251,4.475936 -0.242115,0.304634 -0.813183,0.875532 -1.113189,1.11315 -1.059257,0.838957 -2.289352,1.350845 -3.638969,1.51337 l -0.0015,-0.0015 c -0.135388,0.01631 -0.431257,0.03155 -0.696829,0.03506 -0.496099,0.0065 -0.512724,0.0058 -0.895531,-0.03652 -2.0839,-0.230734 -3.969878,-1.379637 -5.123156,-3.121651 -0.102247,-0.154411 -0.222287,-0.345485 -0.267389,-0.425147 -0.483226,-0.853619 -0.793509,-1.823464 -0.891118,-2.785756 -0.03604,-0.355524 -0.05337,-0.78174 -0.03798,-0.934917 l 0.01315,-0.127088 z m 14.823049,4.903904 a 2.2438765,2.2438024 0 0 1 0.496689,1.403872 2.2438765,2.2438024 0 0 1 -2.243876,2.243804 2.2438765,2.2438024 0 0 1 -1.399505,-0.501057 c 0.512599,-0.30322 0.994784,-0.652976 1.433088,-1.050323 0.685144,-0.6211 1.257318,-1.326013 1.713574,-2.096236 z m -14.461033,1.362962 c 0.505971,0.570507 1.098676,1.093106 1.716491,1.509031 0.364763,0.245553 0.774809,0.473263 1.195013,0.671974 a 2.2438765,2.2438024 0 0 1 -0.669071,0.10372 2.2438765,2.2438024 0 0 1 -2.243877,-2.243802 2.2438765,2.2438024 0 0 1 0.0015,-0.04089 z"
id="path1-83"
style="stroke-width:0.999996" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.5;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect2-0-5"
width="1.5"
height="24"
x="-16.046337"
y="-90" />
<path
id="path1-1-4-0-9-4"
style="stroke-width:0.999995"
d="m -21.82954,-71.763672 v 1.88086 h -5 v 2 h 5 V -66 l 4.779297,-2.892578 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m1.7695 0.60077c-0.034944-0.02028-0.4721 0.78602-0.69922 1.2891-0.7093 1.5708-1.0708 3.2508-1.0703 4.9766 9.6e-4 3.019 1.1261 5.8327 3.2871 8.2207 0.33396 0.36902 0.9444 0.96067 1.3457 1.3047 0.53508 0.45869 1.2073 0.95759 1.8086 1.3438 1.4976 0.96173 2.2647 1.5155 3.0215 2.1797 0.18696 0.16409 0.62122 0.59842 0.75195 0.75195 0.1999 0.23474 0.29976 0.38614 1.0332 1.5527 0.40805 0.64906 0.74645 1.1797 0.75195 1.1797s0.34391-0.53066 0.75195-1.1797c0.40805-0.64906 0.7857-1.2373 0.83789-1.3066 0.62736-0.8341 1.7943-1.7792 3.75-3.0352 0.88886-0.57082 1.5103-1.0324 2.166-1.6074 0.48571-0.4259 1.0555-0.9993 1.4707-1.4824 1.9798-2.3038 3.0227-5.0343 3.0234-7.916 5.52e-4 -2.1684-0.56392-4.2528-1.6582-6.1172-0.092426-0.15747-0.099237-0.16393-0.12305-0.14844-0.03696 0.024-2.3256 1.4209-2.6016 1.5879-0.13007 0.078724-0.23609 0.14775-0.23633 0.15234-2.83e-4 0.00459 0.04762 0.092854 0.10742 0.19727 0.61517 1.0742 0.9893 2.2515 1.1094 3.4941 0.15394 1.5931-0.13764 3.155-0.85742 4.5938-0.83184 1.6627-2.2109 3.1248-4.0879 4.3301-1.6938 1.0875-2.4944 1.6651-3.6055 2.6699l-0.046875 0.041016-0.0625-0.058594c-0.53755-0.48636-0.89701-0.78132-1.4609-1.2012-0.63009-0.4691-1.1477-0.82317-2.0469-1.4004-0.42782-0.27468-0.64454-0.42474-0.9707-0.66992-2.2692-1.7059-3.6689-3.8948-4.0078-6.2695-0.27149-1.9022 0.1016-3.8539 1.0547-5.5176 0.062968-0.10992 0.11357-0.20434 0.11328-0.20898-2.4e-4 -0.005952-2.675-1.6447-2.8496-1.7461zm10.207 2.0273c-0.24926 0.00139-0.50201 0.016306-0.71484 0.042969-1.5147 0.18974-2.9055 0.97118-3.8652 2.1719-0.26957 0.33727-0.47017 0.65403-0.66992 1.0547-0.39282 0.78792-0.58673 1.552-0.625 2.4668-0.03348 0.80014 0.2076 1.5863 0.71875 2.3516 0.51804 0.77554 1.3007 1.5292 2.3613 2.2715 0.76296 0.53405 1.6841 1.058 2.543 1.4492 0.1419 0.06463 0.26649 0.11695 0.27539 0.11719 0.0391 0.0011 0.6649-0.29143 1.002-0.46875 0.83602-0.4398 1.5255-0.87112 2.2031-1.3789 1.8514-1.3874 2.7534-2.8387 2.6934-4.3301-0.029592-0.735-0.16104-1.3758-0.41602-2.0234-0.094848-0.24089-0.31976-0.69025-0.45703-0.91211-0.9624-1.5554-2.5386-2.562-4.3477-2.7773-0.2045-0.024342-0.45191-0.036544-0.70117-0.035156zm0.02539 1.9609c0.1876 1.163e-4 0.37535 0.00847 0.48438 0.023438 0.62002 0.085152 1.1829 0.29407 1.6738 0.62109 0.95501 0.63607 1.566 1.5988 1.7402 2.7402 0.03654 0.23935 0.04551 0.66645 0.01758 0.81641-0.12084 0.64858-0.6925 1.3998-1.6445 2.1602-0.5766 0.46051-1.3002 0.93022-2.0508 1.3301-0.1188 0.06329-0.21926 0.11523-0.22266 0.11523s-0.11934-0.06206-0.25781-0.13672c-2.0931-1.1284-3.4638-2.4248-3.6582-3.4609-0.03828-0.20402-0.019826-0.65657 0.041016-0.98047 0.22906-1.2195 1.0137-2.2647 2.1113-2.8125 0.42802-0.21361 0.792-0.32615 1.2832-0.39453 0.10657-0.014835 0.29482-0.021601 0.48242-0.021484zm-0.0059 1.4102c-0.12204 1.31e-5 -0.24252 0.00685-0.32617 0.021484-0.34394 0.060192-0.62733 0.17253-0.91211 0.36133-0.5628 0.37313-0.92968 0.97351-1.0136 1.6543-0.019078 0.1547-0.00974 0.52906 0.017578 0.67969 0.063168 0.34822 0.21851 0.70566 0.43359 0.99414 0.09173 0.123 0.32214 0.35121 0.44727 0.44531 0.32899 0.24742 0.73269 0.41219 1.1289 0.45703 0.48473 0.05486 1.0374-0.08531 1.459-0.36719 0.561-0.37507 0.9253-0.95033 1.0195-1.6133 0.02339-0.16459 0.02365-0.48458 0-0.64648-0.14784-1.0124-0.91644-1.7985-1.9199-1.9648-0.08818-0.01462-0.21194-0.021497-0.33398-0.021484z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
shape-rendering="crispEdges"
version="1.1"
viewBox="0 -0.5 50 48"
id="svg3"
sodipodi:docname="confused_valetudog.svg"
width="50"
height="48"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs3" />
<sodipodi:namedview
id="namedview3"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
showguides="true"
inkscape:zoom="8.7159622"
inkscape:cx="14.857797"
inkscape:cy="30.117157"
inkscape:window-width="3840"
inkscape:window-height="1537"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg3">
<sodipodi:guide
position="21,21.872356"
orientation="-1,0"
id="guide3"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<g
id="g1">
<g
id="g3"
transform="translate(-25,-3)">
<path
d="m 31,20 h 32 m -32,1 h 1 m 30,0 h 1 m -32,1 h 1 m 30,0 h 2 m -37,1 h 5 m 31,0 h 2 m -39,1 h 2 m 36,0 h 2 m -40,1 h 1 m 38,0 h 2 m -41,1 h 1 m 39,0 h 2 m -42,1 h 2 m 39,0 h 4 m -44,1 h 2 m 41,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m 1,0 h 3 m -48,1 h 1 m 42,0 h 3 m 1,0 h 1 m -48,1 h 1 m 42,0 h 2 m 1,0 h 2 m -48,1 h 1 m 42,0 h 1 m 1,0 h 2 m -47,1 h 1 m 43,0 h 2 m -46,1 h 1 m 42,0 h 2 m -45,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -46,1 h 3 m 42,0 h 3 m -48,1 h 1 m 46,0 h 1 m -48,1 h 1 m 5,0 h 36 m 5,0 h 1 m -48,1 h 1 m 5,0 h 1 m 34,0 h 1 m 5,0 h 1 m -48,1 h 7 m 34,0 h 7"
stroke="#ffffff"
id="path1" />
<path
d="m 32,21 h 30 m -30,1 h 1 m 28,0 h 1 m -30,1 h 1 m 12,0 h 1 m 16,0 h 1 m -35,1 h 5 m 30,0 h 1 m -37,1 h 1 m 36,0 h 1 m -38,1 h 18 m 20,0 h 1 m -38,1 h 1 m 37,0 h 1 m 0,1 h 3 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m 3,0 h 1 m -46,1 h 1 m 40,0 h 1 m 2,0 h 1 m -45,1 h 1 m 40,0 h 1 m 1,0 h 1 m -44,1 h 1 m 40,0 h 2 m -43,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -44,1 h 3 m 1,0 h 38 m 1,0 h 3 m -46,1 h 1 m 3,0 h 1 m 36,0 h 1 m 3,0 h 1 m -46,1 h 5 m 36,0 h 5"
stroke="#000000"
id="path2" />
<path
d="m 33,22 h 28 m -28,1 h 12 m 1,0 h 16 m -29,1 h 30 m -35,1 h 36 m -19,1 h 20 m -36,1 h 37 m -37,1 h 38 m -38,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 1 m 38,0 h 1 m -42,1 h 3 m 38,0 h 3"
stroke="#da7446"
id="path3" />
</g>
<g
id="g4"
transform="translate(-10.5,-9)">
<path
stroke="#ffffff"
d="m 27,9 h 8 m -9,1 h 2 m 6,0 h 2 m -10,1 h 1 m 8,0 h 1 m -10,1 h 1 m 2,0 h 4 m 2,0 h 1 m -10,1 h 1 m 2,0 h 1 m 1,0 h 2 m 2,0 h 1 m -10,1 h 6 m 3,0 h 1 m -7,1 h 2 m 3,0 h 2 m -7,1 h 1 m 3,0 h 2 m -6,1 h 1 m 2,0 h 2 m -5,1 h 1 m 2,0 h 1 m -4,1 h 4 m -4,1 h 1 m 2,0 h 1 m -4,1 h 1 m 2,0 h 1 m -4,1 h 4"
id="path1-5" />
<path
stroke="#000000"
d="m 28,10 h 6 m -7,1 h 8 m -8,1 h 2 m 4,0 h 2 m -8,1 h 2 m 4,0 h 2 m -3,1 h 3 m -4,1 h 3 m -4,1 h 3 m -3,1 h 2 m -2,1 h 2 m -2,2 h 2 m -2,1 h 2"
id="path2-0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,83 @@
syntax = "proto2";
message GridIndex {
optional int32 x = 1;
optional int32 y = 2;
}
message Point {
optional float x = 1;
optional float y = 2;
optional float z = 3;
}
message PointCloud {
repeated Point points = 1;
}
message AIRect {
optional int32 x = 1;
optional int32 y = 2;
optional int32 width = 3;
optional int32 height = 4;
}
message AIImageInfo {
optional int32 confidence = 1;
optional string absolute_path = 2;
optional AIRect rect = 3;
optional float float_val_10 = 10;
optional float float_val_11 = 11;
}
message BoundingBox {
optional GridIndex corner_1 = 1;
optional GridIndex corner_2 = 2;
optional GridIndex corner_3 = 3;
optional GridIndex corner_4 = 4;
}
message SemanticObject {
optional int32 object_type = 1;
optional int32 instance_id = 2; // e.g. the 5th shoe seen
optional GridIndex center_point = 3;
// index 0: A serialized BoundingBox message.
// index 1: Some kind of hash?
repeated bytes field_4_data = 4;
optional int32 int32_val_5 = 5;
optional int32 int32_val_6 = 6;
optional int64 timestamp_us = 7;
optional PointCloud point_cloud = 8;
optional AIImageInfo ai_image_info = 9;
optional float float_val_10 = 10;
optional float float_val_11 = 11;
optional int32 int32_val_12 = 12;
optional int32 int32_val_13 = 13;
optional int32 int32_val_14 = 14;
optional int32 int32_val_15 = 15;
}
message SemanticMapInfo {
repeated SemanticObject objects = 1;
optional uint32 uint32_val_2 = 2;
optional uint32 uint32_val_3 = 3;
optional string clean_record_id = 4;
}
message TrackPoint {
optional int64 int64_val_1 = 1;
optional int32 int32_val_2 = 2;
optional string name = 3;
optional string desc = 4;
optional int32 int32_val_5 = 5;
optional int32 int32_val_6 = 6;
oneof data_oneof {
int64 oneof_int64_7 = 7;
int64 oneof_int64_8 = 8;
float oneof_float_9 = 9;
string oneof_string_10 = 10;
}
}

1
backend/.eslintignore Normal file
View File

@ -0,0 +1 @@
/lib/robots/midea/generated/midea_protobufs.js

View File

@ -84,9 +84,7 @@ class Configuration {
Logger.info(`Migrating config from ${parsedConfig._version} to ${Tools.GET_VALETUDO_VERSION()}`);
// BEGIN migration code to be removed with the next version
if (parsedConfig.ntpClient.server === "pool.ntp.org") {
parsedConfig.ntpClient.server = "valetudo.pool.ntp.org";
}
// END migration code to be removed with the next version
parsedConfig._version = Tools.GET_VALETUDO_VERSION();
@ -138,7 +136,12 @@ class Configuration {
reset() {
Logger.info("Restoring config to default settings.");
// A config reset should not reset the robot config
const robotSettings = this.settings.robot;
this.settings = structuredClone(DEFAULT_SETTINGS);
this.settings.robot = robotSettings;
this.persist();
Object.keys(this.settings).forEach(key => {

View File

@ -1,7 +1,6 @@
const ntp = require("@destinationstransfers/ntp");
const execSync = require("child_process").execSync;
const LinuxTools = require("./utils/LinuxTools");
const Logger = require("./Logger");
const States = require("./entities/core/ntpClient");
const Tools = require("./utils/Tools");
@ -142,22 +141,7 @@ class NTPClient {
setTime(date) {
if (this.config.get("embedded") === true) {
let dateString = "";
dateString += date.getFullYear().toString();
dateString += "-";
dateString += (date.getMonth() + 1).toString().padStart(2, 0);
dateString += "-";
dateString += date.getDate().toString().padStart(2, 0);
dateString += " ";
dateString += date.getHours().toString().padStart(2,0);
dateString += ":";
dateString += date.getMinutes().toString().padStart(2,0);
dateString += ":";
dateString += date.getSeconds().toString().padStart(2,0);
execSync("date -s \""+dateString+"\"");
LinuxTools.SET_TIME(date);
Logger.info("Successfully set the robot time via NTP to", date);
} else {

View File

@ -6,6 +6,7 @@ const MqttController = require("./mqtt/MqttController");
const NTPClient = require("./NTPClient");
const os = require("os");
const path = require("path");
const stream = require("stream");
const Tools = require("./utils/Tools");
const v8 = require("v8");
const ValetudoEventStore = require("./ValetudoEventStore");
@ -149,15 +150,6 @@ class Valetudo {
//@ts-ignore
//eslint-disable-next-line no-undef
global.gc();
const rssAfter = process.memoryUsage.rss();
const rssDiff = rss - rssAfter;
if (rssDiff > 0) {
Logger.debug("GC forced at " + rss + " bytes RSS freed " + rssDiff + " bytes of memory.");
} else {
Logger.debug("GC forced at " + rss + " bytes RSS was unsuccessful.");
}
}
}
@ -193,6 +185,10 @@ class Valetudo {
return;
}
// 16KiB was the default until node v22. See https://github.com/nodejs/node/pull/52037
stream.Stream.setDefaultHighWaterMark(false, 16384);
stream.Stream.setDefaultHighWaterMark(true, 16);
try {
const newOOMScoreAdj = 666;
const previousOOMScoreAdj = parseInt(fs.readFileSync("/proc/self/oom_score_adj").toString());

View File

@ -2,14 +2,17 @@ const EventEmitter = require("events").EventEmitter;
const AttributeSubscriber = require("../entities/AttributeSubscriber");
const CallbackAttributeSubscriber = require("../entities/CallbackAttributeSubscriber");
const ConsumableDepletedValetudoEvent = require("../valetudo_events/events/ConsumableDepletedValetudoEvent");
const entities = require("../entities");
const ErrorStateValetudoEvent = require("../valetudo_events/events/ErrorStateValetudoEvent");
const Logger = require("../Logger");
const NotImplementedError = require("./NotImplementedError");
const Semaphore = require("semaphore");
const Tools = require("../utils/Tools");
const {ConsumableStateAttribute, StatusStateAttribute} = require("../entities/state/attributes");
const {StatusStateAttribute} = require("../entities/state/attributes");
/**
* @abstract
*/
class ValetudoRobot {
/**
*
@ -33,6 +36,10 @@ class ValetudoRobot {
hugeMap: false
};
this.mapPollMutex = Semaphore(1);
this.mapPollTimeout = undefined;
this.postActiveStateMapPollCooldownCredits = 0;
this.initInternalSubscriptions();
}
@ -89,21 +96,6 @@ class ValetudoRobot {
* @protected
*/
initInternalSubscriptions() {
this.state.subscribe(
new CallbackAttributeSubscriber((eventType, consumable) => {
if (eventType !== AttributeSubscriber.EVENT_TYPE.DELETE) {
//@ts-ignore
if (consumable?.remaining?.value === 0) {
this.valetudoEventStore.raise(new ConsumableDepletedValetudoEvent({
type: consumable.type,
subType: consumable.subType
}));
}
}
}),
{attributeClass: ConsumableStateAttribute.name}
);
this.state.subscribe(
new CallbackAttributeSubscriber((eventType, status, prevStatus) => {
if (
@ -133,18 +125,18 @@ class ValetudoRobot {
/*
This will be displayed only once after a map larger than 120 has been uploaded to a new Valetudo process
It should serve as an unobtrusive reminder that while you can use Valetudo in a commercial environment
without any limitations whatsoever, doing so and saving money because of that without giving anything
back is simply not a very nice thing to do.
While there would be the option to introduce something like license keys or a paid version, not only
would that be futile in an open source project, but it would also likely harm perfectly fine non-commercial
uses of Valetudo in e.g., your local hackerspace, art installations, etc.
In the end, I'd rather have some people take advantage of this permissive system than making
the project worse for all of its users to prevent that.
You're welcome
*/
Logger.info("Based on your map size, it looks like you might be using Valetudo in a commercial environment.");
@ -164,6 +156,88 @@ class ValetudoRobot {
//intentional
}
/**
*
* @protected
* @abstract
* @returns {Promise<any>}
*/
async executeMapPoll() {
throw new NotImplementedError();
}
/**
* @protected
* @param {any} pollResponse Implementation specific
* @return {number} seconds
*/
determineNextMapPollInterval(pollResponse) {
let repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
let statusStateAttribute = this.state.getFirstMatchingAttribute({
attributeClass: StatusStateAttribute.name
});
let isActive = false;
if (statusStateAttribute && statusStateAttribute.isActiveState) {
isActive = true;
this.postActiveStateMapPollCooldownCredits = 3;
}
if (!isActive && this.postActiveStateMapPollCooldownCredits > 0) {
// Pretend that we're still in an active state to ensure that we catch map updates e.g. after docking
isActive = true;
this.postActiveStateMapPollCooldownCredits--;
}
if (isActive) {
repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.ACTIVE;
if (this.flags.lowmemHost) {
repollSeconds *= 2;
}
if (this.flags.hugeMap) {
repollSeconds *= 2;
}
}
return repollSeconds;
}
/**
* @public
* @returns {void}
*/
pollMap() {
this.mapPollMutex.take(() => {
let repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.DEFAULT;
// Clear pending timeout, since were starting a new poll right now.
if (this.mapPollTimeout) {
clearTimeout(this.mapPollTimeout);
this.mapPollTimeout = undefined;
}
this.executeMapPoll().then((response) => {
repollSeconds = this.determineNextMapPollInterval(response);
}).catch((err) => {
Logger.debug("Error while executing map poll", err);
repollSeconds = ValetudoRobot.MAP_POLLING_INTERVALS.ERROR;
}).finally(() => {
this.mapPollTimeout = setTimeout(() => {
this.pollMap();
}, repollSeconds * 1000);
this.mapPollMutex.leave();
});
});
}
async shutdown() {
//intentional
@ -283,6 +357,13 @@ ValetudoRobot.WELL_KNOWN_PROPERTIES = {
FIRMWARE_VERSION: "firmwareVersion"
};
const HUGE_MAP_THRESHOLD = 145 * 10000; //145m² in cm²
ValetudoRobot.MAP_POLLING_INTERVALS = Object.freeze({
DEFAULT: 60,
ACTIVE: 2,
ERROR: 30
});
const HUGE_MAP_THRESHOLD = 145 * 10_000; //145m² in cm²
module.exports = ValetudoRobot;

View File

@ -1,15 +0,0 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class AutoEmptyDockAutoEmptyControlCapability extends SimpleToggleCapability {
getType() {
return AutoEmptyDockAutoEmptyControlCapability.TYPE;
}
}
AutoEmptyDockAutoEmptyControlCapability.TYPE = "AutoEmptyDockAutoEmptyControlCapability";
module.exports = AutoEmptyDockAutoEmptyControlCapability;

View File

@ -54,6 +54,7 @@ AutoEmptyDockAutoEmptyIntervalControlCapability.TYPE = "AutoEmptyDockAutoEmptyIn
*
*/
AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL = Object.freeze({
OFF: "off",
INFREQUENT: "infrequent",
NORMAL: "normal",
FREQUENT: "frequent",

View File

@ -0,0 +1,15 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class CameraLightControlCapability extends SimpleToggleCapability {
getType() {
return CameraLightControlCapability.TYPE;
}
}
CameraLightControlCapability.TYPE = "CameraLightControlCapability";
module.exports = CameraLightControlCapability;

View File

@ -57,7 +57,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({
OFF: "off",
AVOID: "avoid",
LIFT: "lift",
DETACH: "detach",
DETACH: "detach"
});

View File

@ -1,6 +1,7 @@
const Capability = require("./Capability");
const ConsumableStateAttribute = require("../../entities/state/attributes/ConsumableStateAttribute");
const ConsumableDepletedValetudoEvent = require("../../valetudo_events/events/ConsumableDepletedValetudoEvent");
const NotImplementedError = require("../NotImplementedError");
const ValetudoConsumable = require("../../entities/core/ValetudoConsumable");
/**
* @template {import("../ValetudoRobot")} T
@ -8,10 +9,10 @@ const NotImplementedError = require("../NotImplementedError");
*/
class ConsumableMonitoringCapability extends Capability {
/**
* This function polls the current consumables state and stores the attributes in our robotState
* This function polls the current consumables state
*
* @abstract
* @returns {Promise<Array<ConsumableStateAttribute>>}
* @returns {Promise<Array<ValetudoConsumable>>}
*/
async getConsumables() {
throw new NotImplementedError();
@ -27,6 +28,22 @@ class ConsumableMonitoringCapability extends Capability {
throw new NotImplementedError();
}
// FIXME: Nothing will raise these events if MQTT isn't active, because nothing will periodically poll the capability
/**
* @protected
* @param {Array<ValetudoConsumable>} consumables
*/
raiseEventIfRequired(consumables) {
consumables.forEach(consumable => {
if (consumable?.remaining?.value === 0) {
this.robot.valetudoEventStore.raise(new ConsumableDepletedValetudoEvent({
type: consumable.type,
subType: consumable.subType
}));
}
});
}
/**
* This utility method should be called on reset by each implementation to make sure
* that there are no stale events when resetting the consumable via other ways
@ -62,9 +79,9 @@ class ConsumableMonitoringCapability extends Capability {
/**
* @typedef {object} ConsumableMeta
*
* @property {ConsumableStateAttribute.TYPE} type
* @property {ConsumableStateAttribute.SUB_TYPE} subType
* @property {ConsumableStateAttribute.UNITS} unit
* @property {ValetudoConsumable.TYPE} type
* @property {ValetudoConsumable.SUB_TYPE} subType
* @property {ValetudoConsumable.UNITS} unit
* @property {number} [maxValue]
*
*/

View File

@ -0,0 +1,62 @@
const Capability = require("./Capability");
const NotImplementedError = require("../NotImplementedError");
/**
* @template {import("../ValetudoRobot")} T
* @extends Capability<T>
*/
class HighResolutionManualControlCapability extends Capability {
/**
*
* @param {object} options
* @param {T} options.robot
* @class
*/
constructor(options) {
super(options);
}
/**
* @abstract
* @returns {Promise<void>}
*/
async enableManualControl() {
throw new NotImplementedError();
}
/**
* @abstract
* @returns {Promise<void>}
*/
async disableManualControl() {
throw new NotImplementedError();
}
/**
* @abstract
* @return {Promise<boolean>}
*/
async manualControlActive() {
throw new NotImplementedError();
}
/**
* @abstract
* @param {import("../../entities/core/ValetudoManualMovementVector")} movementVector
* @returns {Promise<void>}
*/
async manualControl(movementVector) {
throw new NotImplementedError();
}
getType() {
return HighResolutionManualControlCapability.TYPE;
}
}
HighResolutionManualControlCapability.TYPE = "HighResolutionManualControlCapability";
module.exports = HighResolutionManualControlCapability;

View File

@ -0,0 +1,15 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class MopDockMopAutoDryingControlCapability extends SimpleToggleCapability {
getType() {
return MopDockMopAutoDryingControlCapability.TYPE;
}
}
MopDockMopAutoDryingControlCapability.TYPE = "MopDockMopAutoDryingControlCapability";
module.exports = MopDockMopAutoDryingControlCapability;

View File

@ -0,0 +1,65 @@
const Capability = require("./Capability");
const NotImplementedError = require("../NotImplementedError");
/**
* @template {import("../ValetudoRobot")} T
* @extends Capability<T>
*/
class MopDockMopWashTemperatureControlCapability extends Capability {
/**
*
* @param {object} options
* @param {T} options.robot
* @class
*/
constructor(options) {
super(options);
}
/**
* @returns {Promise<MopDockMopWashTemperatureControlCapabilityTemperature>}
*/
async getTemperature() {
throw new NotImplementedError();
}
/**
*
* @param {MopDockMopWashTemperatureControlCapabilityTemperature} newTemperature
* @returns {Promise<void>}
*/
async setTemperature(newTemperature) {
throw new NotImplementedError();
}
/**
* @returns {{supportedTemperatures: Array<MopDockMopWashTemperatureControlCapabilityTemperature>}}
*/
getProperties() {
return {
supportedTemperatures: []
};
}
getType() {
return MopDockMopWashTemperatureControlCapability.TYPE;
}
}
MopDockMopWashTemperatureControlCapability.TYPE = "MopDockMopWashTemperatureControlCapability";
/**
* @typedef {string} MopDockMopWashTemperatureControlCapabilityTemperature
* @enum {string}
*
*/
MopDockMopWashTemperatureControlCapability.TEMPERATURE = Object.freeze({
COLD: "cold",
WARM: "warm",
HOT: "hot",
SCALDING: "scalding",
BOILING: "boiling"
});
module.exports = MopDockMopWashTemperatureControlCapability;

View File

@ -0,0 +1,15 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class MopExtensionControlCapability extends SimpleToggleCapability {
getType() {
return MopExtensionControlCapability.TYPE;
}
}
MopExtensionControlCapability.TYPE = "MopExtensionControlCapability";
module.exports = MopExtensionControlCapability;

View File

@ -0,0 +1,15 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class MopExtensionFurnitureLegHandlingControlCapability extends SimpleToggleCapability {
getType() {
return MopExtensionFurnitureLegHandlingControlCapability.TYPE;
}
}
MopExtensionFurnitureLegHandlingControlCapability.TYPE = "MopExtensionFurnitureLegHandlingControlCapability";
module.exports = MopExtensionFurnitureLegHandlingControlCapability;

View File

@ -0,0 +1,15 @@
const SimpleToggleCapability = require("./SimpleToggleCapability");
/**
* @template {import("../ValetudoRobot")} T
* @extends SimpleToggleCapability<T>
*/
class MopTwistControlCapability extends SimpleToggleCapability {
getType() {
return MopTwistControlCapability.TYPE;
}
}
MopTwistControlCapability.TYPE = "MopTwistControlCapability";
module.exports = MopTwistControlCapability;

View File

@ -0,0 +1,119 @@
const Capability = require("./Capability");
const Logger = require("../../Logger");
const NotImplementedError = require("../NotImplementedError");
const PointMapEntity = require("../../entities/map/PointMapEntity");
/**
*
* @template {import("../ValetudoRobot")} T
* @extends Capability<T>
*/
class ObstacleImagesCapability extends Capability {
/*
* @param {object} options
* @param {T} options.robot
* @param {import("../../utils/const").ImageFileFormat} options.fileFormat
* @param {object} options.dimensions
* @param {number} options.dimensions.height
* @param {number} options.dimensions.width
* @param {Array<import("../Quirk")>} [options.quirks]
*/
constructor(options) {
super(options);
this.fileFormat = options.fileFormat;
this.dimensions = options.dimensions;
}
/**
*
* @abstract
* @returns {Promise<boolean>}
*/
async isEnabled() {
throw new NotImplementedError();
}
/**
* @abstract
* @returns {Promise<void>}
*/
async enable() {
throw new NotImplementedError();
}
/**
* @abstract
* @returns {Promise<void>}
*/
async disable() {
throw new NotImplementedError();
}
/**
* @param {string} id
* @returns {Promise<import('stream').Readable>}
*/
async getStreamForId(id) {
if (this.robot.config.get("embedded") !== true) {
Logger.warn("Can't provide obstacle image since we're not embedded");
return null;
}
// Even if files exist, the user will expect them to not be accessible when disabled
// The downside is that each image fetch will contact the firmware and ask if the feature is enabled,
// adding delay and overhead
const isEnabled = await this.isEnabled();
if (!isEnabled) {
return null;
}
let map = this.robot.state.map;
let obstacleEntity = map.entities.find(e => {
return (
e.type === PointMapEntity.TYPE.OBSTACLE &&
e.metaData.id === id &&
e.metaData.image !== undefined
);
});
if (!obstacleEntity) {
return null;
}
return this.getStreamForImage(obstacleEntity.metaData.image);
}
/**
* This should only be called within the capability after resolving the ID
*
* @protected
* @abstract
* @param {string} image
* @returns {Promise<import('stream').Readable>}
*/
async getStreamForImage(image) {
throw new NotImplementedError();
}
/**
* @returns {{fileFormat: import("../../utils/const").ImageFileFormat, dimensions: {height: number, width: number}}}
*/
getProperties() {
return {
fileFormat: this.fileFormat,
dimensions: this.dimensions
};
}
getType() {
return ObstacleImagesCapability.TYPE;
}
}
ObstacleImagesCapability.TYPE = "ObstacleImagesCapability";
module.exports = ObstacleImagesCapability;

View File

@ -1,8 +1,8 @@
module.exports = {
AutoEmptyDockAutoEmptyControlCapability: require("./AutoEmptyDockAutoEmptyControlCapability"),
AutoEmptyDockAutoEmptyIntervalControlCapability: require("./AutoEmptyDockAutoEmptyIntervalControlCapability"),
AutoEmptyDockManualTriggerCapability: require("./AutoEmptyDockManualTriggerCapability"),
BasicControlCapability: require("./BasicControlCapability"),
CameraLightControlCapability: require("./CameraLightControlCapability"),
CarpetModeControlCapability: require("./CarpetModeControlCapability"),
CarpetSensorModeControlCapability: require("./CarpetSensorModeControlCapability"),
CollisionAvoidantNavigationControlCapability: require("./CollisionAvoidantNavigationControlCapability"),
@ -12,6 +12,7 @@ module.exports = {
DoNotDisturbCapability: require("./DoNotDisturbCapability"),
FanSpeedControlCapability: require("./FanSpeedControlCapability"),
GoToLocationCapability: require("./GoToLocationCapability"),
HighResolutionManualControlCapability: require("./HighResolutionManualControlCapability"),
KeyLockCapability: require("./KeyLockCapability"),
LocateCapability: require("./LocateCapability"),
ManualControlCapability: require("./ManualControlCapability"),
@ -23,7 +24,13 @@ module.exports = {
MappingPassCapability: require("./MappingPassCapability"),
MopDockCleanManualTriggerCapability: require("./MopDockCleanManualTriggerCapability"),
MopDockDryManualTriggerCapability: require("./MopDockDryManualTriggerCapability"),
MopDockMopAutoDryingControlCapability: require("./MopDockMopAutoDryingControlCapability"),
MopDockMopWashTemperatureControlCapability: require("./MopDockMopWashTemperatureControlCapability"),
MopExtensionControlCapability: require("./MopExtensionControlCapability"),
MopExtensionFurnitureLegHandlingControlCapability: require("./MopExtensionFurnitureLegHandlingControlCapability"),
MopTwistControlCapability: require("./MopTwistControlCapability"),
ObstacleAvoidanceControlCapability: require("./ObstacleAvoidanceControlCapability"),
ObstacleImagesCapability: require("./ObstacleImagesCapability"),
OperationModeControlCapability: require("./OperationModeControlCapability"),
PendingMapChangeHandlingCapability: require("./PendingMapChangeHandlingCapability"),
PersistentMapControlCapability: require("./PersistentMapControlCapability"),

View File

@ -241,17 +241,12 @@
"additionalProperties": false,
"required": [
"enabled",
"addICBINVMapProperty",
"cleanAttributesOnShutdown"
],
"properties": {
"enabled": {
"type": "boolean"
},
"addICBINVMapProperty": {
"type": "boolean",
"description": "If true, adds Homie definitions for ICBINV's PNG map topic as that's not possible to add in ICBINV itself."
},
"cleanAttributesOnShutdown": {
"type": "boolean"
}
@ -285,7 +280,10 @@
"TotalStatisticsCapability",
"SpeakerVolumeControlCapability",
"KeyLockCapability",
"ObstacleAvoidanceControlCapability"
"ObstacleAvoidanceControlCapability",
"PetObstacleAvoidanceControlCapability",
"CarpetModeControlCapability",
"CarpetSensorModeControlCapability"
]
}
}

View File

@ -0,0 +1,65 @@
const SerializableEntity = require("../SerializableEntity");
class ValetudoConsumable extends SerializableEntity {
/**
* @param {object} options
* @param {ValetudoConsumableType} options.type
* @param {ValetudoConsumableSubType} [options.subType]
* @param {object} [options.metaData]
* @param {object} options.remaining
* @param {number} options.remaining.value
* @param {ValetudoConsumableRemainingUnit} options.remaining.unit
*/
constructor(options) {
super(options);
this.type = options.type;
this.subType = options.subType ?? ValetudoConsumable.SUB_TYPE.NONE;
this.remaining = options.remaining;
}
}
/**
* @typedef {string} ValetudoConsumableType
* @enum {string}
*
*/
ValetudoConsumable.TYPE = Object.freeze({
FILTER: "filter",
BRUSH: "brush",
MOP: "mop",
DETERGENT: "detergent",
BIN: "bin",
CLEANING: "cleaning",
});
/**
* @typedef {string} ValetudoConsumableSubType
* @enum {string}
*
*/
ValetudoConsumable.SUB_TYPE = Object.freeze({
NONE: "none",
ALL: "all",
MAIN: "main",
SECONDARY: "secondary",
SIDE_LEFT: "side_left",
SIDE_RIGHT: "side_right",
DOCK: "dock",
SENSOR: "sensor",
WHEEL: "wheel"
});
/**
*
* @typedef {string} ValetudoConsumableRemainingUnit
* @enum {string}
*/
ValetudoConsumable.UNITS = Object.freeze({
MINUTES: "minutes",
PERCENT: "percent"
});
module.exports = ValetudoConsumable;

View File

@ -0,0 +1,33 @@
const SerializableEntity = require("../SerializableEntity");
/**
* @class ValetudoManualMovementVector
* @property {number} velocity -1 to 1
* @property {number} angle -180 to 180
*/
class ValetudoManualMovementVector extends SerializableEntity {
/**
*
* @param {object} options
* @param {number} options.velocity -1 to 1
* @param {number} options.angle -180 to 180
* @param {object} [options.metaData]
* @class
*/
constructor(options) {
super(options);
if (options.velocity < -1 || options.velocity > 1) {
throw new Error("Velocity must be between -1 and 1");
}
if (options.angle < -180 || options.angle > 180) {
throw new Error("Angle must be between -180 and 180");
}
this.velocity = options.velocity;
this.angle = options.angle;
}
}
module.exports = ValetudoManualMovementVector;

View File

@ -1,5 +1,5 @@
const crypto = require("crypto");
const SerializableEntity = require("../SerializableEntity");
const uuid = require("uuid");
class ValetudoTimer extends SerializableEntity {
@ -30,7 +30,7 @@ class ValetudoTimer extends SerializableEntity {
constructor(options) {
super(options);
this.id = options.id ?? uuid.v4();
this.id = options.id ?? crypto.randomUUID();
this.enabled = options.enabled;
if (typeof options.label === "string" && options.label.length > 0) {

View File

@ -0,0 +1,26 @@
{
"components": {
"schemas": {
"ValetudoConsumable": {
"type": "object",
"properties": {
"remaining": {
"type": "object",
"properties": {
"value": {
"type": "number"
},
"unit": {
"type": "string",
"enum": [
"percent",
"minutes"
]
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,24 @@
{
"components": {
"schemas": {
"ValetudoManualMovementVector": {
"type": "object",
"properties": {
"velocity": {
"type": "number",
"minimum": -1,
"maximum": 1
},
"angle": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"metaData": {
"type": "object"
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
module.exports = {
ValetudoDNDConfiguration: require("./ValetudoDNDConfiguration"),
ValetudoDataPoint: require("./ValetudoDataPoint"),
ValetudoManualMovementVector: require("./ValetudoManualMovementVector"),
ValetudoMapSegment: require("./ValetudoMapSegment"),
ValetudoMapSnapshot: require("./ValetudoMapSnapshot"),
ValetudoRestrictedZone: require("./ValetudoRestrictedZone"),

View File

@ -9,6 +9,7 @@ class ValetudoUpdaterApplyPendingState extends ValetudoUpdaterState {
* @param {string} options.version The version (e.g. 2021.10.0)
* @param {Date} options.releaseTimestamp The release date as found in the manifest
* @param {string} options.downloadPath The path the new binary was downloaded to
* @param {number} options.downloadPathFd file descriptor
*
* @class
*/
@ -18,6 +19,7 @@ class ValetudoUpdaterApplyPendingState extends ValetudoUpdaterState {
this.version = options.version;
this.releaseTimestamp = options.releaseTimestamp;
this.downloadPath = options.downloadPath;
this.downloadPathFd = options.downloadPathFd;
}
}

View File

@ -9,6 +9,8 @@ class PointMapEntity extends MapEntity {
* @param {object} [options.metaData]
* @param {number} [options.metaData.angle] 0-360°. 0° being North
* @param {string} [options.metaData.label]
* @param {string} [options.metaData.id]
* @param {string} [options.metaData.image] Could be a path, could also be an ID. Vendor-specific
*/
constructor(options) {
super(options);

View File

@ -1,4 +1,4 @@
const uuid = require("uuid");
const crypto = require("crypto");
const MapLayer = require("./MapLayer");
const SerializableEntity = require("../SerializableEntity");
@ -37,7 +37,7 @@ class ValetudoMap extends SerializableEntity { //TODO: Current, Historic, Etc.
this.entities = [];
this.metaData.version = 2;
this.metaData.nonce = uuid.v4();
this.metaData.nonce = crypto.randomUUID();
this.metaData.totalLayerArea = 0;

View File

@ -1,63 +0,0 @@
const StateAttribute = require("./StateAttribute");
class ConsumableStateAttribute extends StateAttribute {
/**
* @param {object} options
* @param {ConsumableStateAttributeType} options.type
* @param {ConsumableStateAttributeSubType} [options.subType]
* @param {object} [options.metaData]
* @param {object} options.remaining
* @param {number} options.remaining.value
* @param {ConsumableStateAttributeRemainingUnit} options.remaining.unit
*/
constructor(options) {
super(options);
this.type = options.type;
this.subType = options.subType ?? ConsumableStateAttribute.SUB_TYPE.NONE;
this.remaining = options.remaining;
}
}
/**
* @typedef {string} ConsumableStateAttributeType
* @enum {string}
*
*/
ConsumableStateAttribute.TYPE = Object.freeze({
FILTER: "filter",
BRUSH: "brush",
SENSOR: "sensor",
MOP: "mop",
DETERGENT: "detergent",
BIN: "bin"
});
/**
* @typedef {string} ConsumableStateAttributeSubType
* @enum {string}
*
*/
ConsumableStateAttribute.SUB_TYPE = Object.freeze({
NONE: "none",
ALL: "all",
MAIN: "main",
SECONDARY: "secondary",
SIDE_LEFT: "side_left",
SIDE_RIGHT: "side_right",
DOCK: "dock"
});
/**
*
* @typedef {string} ConsumableStateAttributeRemainingUnit
* @enum {string}
*/
ConsumableStateAttribute.UNITS = Object.freeze({
MINUTES: "minutes",
PERCENT: "percent"
});
module.exports = ConsumableStateAttribute;

View File

@ -1,33 +0,0 @@
{
"components": {
"schemas": {
"ConsumableStateAttribute": {
"allOf": [
{
"$ref": "#/components/schemas/StateAttribute"
},
{
"type": "object",
"properties": {
"remaining": {
"type": "object",
"properties": {
"value": {
"type": "number"
},
"unit": {
"type": "string",
"enum": [
"percent",
"minutes"
]
}
}
}
}
}
]
}
}
}
}

View File

@ -1,7 +1,6 @@
module.exports = {
AttachmentStateAttribute: require("./AttachmentStateAttribute"),
BatteryStateAttribute: require("./BatteryStateAttribute"),
ConsumableStateAttribute: require("./ConsumableStateAttribute"),
DockStatusStateAttribute: require("./DockStatusStateAttribute"),
PresetSelectionStateAttribute: require("./PresetSelectionStateAttribute"),
StatusStateAttribute: require("./StatusStateAttribute")

View File

@ -213,7 +213,8 @@ class MiioSocket {
this.pendingRequests[msgId].timeout_id = setTimeout(
() => {
this.pendingRequests[msgId].onTimeoutCallback();
// optional chaining due to a super rare race condition that only surfaced after 4 years
this.pendingRequests[msgId]?.onTimeoutCallback();
},
options.timeout ?? this.timeout
);

View File

@ -105,12 +105,12 @@ class RetryWrapper {
return new Promise((resolve, reject) => {
this.mutex.take(() => {
this.sendMessageHelper(msg, options).then(response => {
this.mutex.leave();
resolve(response);
}).catch(err => {
this.mutex.leave();
reject(err);
});
});
@ -205,7 +205,7 @@ class RetryWrapper {
await this.handshake(true);
}
//remove all remains of a previous attempt
// remove all remains of a previous attempt
delete(msg["id"]);
}

View File

@ -565,6 +565,16 @@ class MqttController {
return this.state === HomieCommonAttributes.STATE.READY;
}
/**
* Whether we're connected to the broker
*
* @public
* @return {boolean}
*/
get isConnected() {
return this.client && this.client.connected === true && this.client.disconnecting !== true;
}
/**
* Set device state
*
@ -573,10 +583,14 @@ class MqttController {
* @return {Promise<void>}
*/
async setState(state) {
if (this.client && this.client.connected === true && this.client.disconnecting !== true) {
if (state === this.state) { // No point in setting the same state again
return;
}
if (this.isConnected) {
await this.publish(this.currentConfig.stateTopic, state, {
// @ts-ignore
qos: MqttCommonAttributes.QOS.AT_LEAST_ONCE,
qos: MqttCommonAttributes.QOS.AT_MOST_ONCE, // Anything other than QoS 0 can potentially block for a long time
retain: true
});
}
@ -642,9 +656,20 @@ class MqttController {
Object.assign(reconfOptions, options);
}
const previousState = this.state;
try {
await this.setState(reconfOptions.reconfigState);
await cb();
// Since the ready state is used by the handles to determine whether they should publish stuff,
// we must never exit reconfigure in the READY state if we are in fact not READY
if (reconfOptions.targetState === HomieCommonAttributes.STATE.READY && !this.isConnected) {
Logger.debug(`Overriding reconfigure target state '${reconfOptions.targetState}' with '${previousState}' since we're not connected.`);
reconfOptions.targetState = previousState;
}
await this.setState(reconfOptions.targetState);
this.mutexes.reconfigure.leave();
@ -895,7 +920,7 @@ class MqttController {
//@ts-ignore
if (this.client?.stream?.writableLength > 1024 * 1024) { //Allow for 1MiB of buffered messages
Logger.warn(`Stale MQTT connection detected. Dropping message for ${topic}`);
} else if (this.asyncClient) {
} else if (this.isConnected) {
//This looks like an afterthought because it is one. :(
const hasChanged = this.messageDeduplicationCache.update(topic, message);
@ -955,7 +980,6 @@ module.exports = MqttController;
*
* @property {object} interfaces.homie
* @property {boolean} interfaces.homie.enabled
* @property {boolean} interfaces.homie.addICBINVMapProperty
* @property {boolean} interfaces.homie.cleanAttributesOnShutdown
*
* @property {object} interfaces.homeassistant

View File

@ -0,0 +1,8 @@
const SimpleToggleCapabilityMqttHandle = require("./SimpleToggleCapabilityMqttHandle");
class CarpetModeControlCapabilityMqttHandle extends SimpleToggleCapabilityMqttHandle {}
CarpetModeControlCapabilityMqttHandle.OPTIONAL = true;
module.exports = CarpetModeControlCapabilityMqttHandle;

View File

@ -0,0 +1,70 @@
const CapabilityMqttHandle = require("./CapabilityMqttHandle");
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const EntityCategory = require("../homeassistant/EntityCategory");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
class CarpetSensorModeControlCapabilityMqttHandle extends CapabilityMqttHandle {
/**
* @param {object} options
* @param {import("../handles/RobotMqttHandle")} options.parent
* @param {import("../MqttController")} options.controller MqttController instance
* @param {import("../../core/ValetudoRobot")} options.robot
* @param {import("../../core/capabilities/CarpetSensorModeControlCapability")} options.capability
*/
constructor(options) {
super(Object.assign(options, {
friendlyName: "Carpet Sensor Mode"
}));
this.capability = options.capability;
this.registerChild(
new PropertyMqttHandle({
parent: this,
controller: this.controller,
topicName: "mode",
friendlyName: "Carpet Sensor Mode",
datatype: DataType.ENUM,
format: this.capability.getProperties().supportedModes.join(","),
setter: async (value) => {
await this.capability.setMode(value);
},
getter: async () => {
return this.capability.getMode();
},
helpText: "This handle allows setting the Carpet Sensor Mode. " +
"It accepts the preset payloads specified in `$format` or in the HAss json attributes.",
helpMayChange: {
"Enum payloads": "Different robot models have different Carpet Sensor Modes. " +
"Always check `$format`/`json_attributes` during startup."
}
}).also((prop) => {
this.controller.withHass((hass) => {
prop.attachHomeAssistantComponent(
new InLineHassComponent({
hass: hass,
robot: this.robot,
name: this.capability.getType(),
friendlyName: "Carpet Sensor Mode",
componentType: ComponentType.SELECT,
autoconf: {
state_topic: prop.getBaseTopic(),
value_template: "{{ value }}",
command_topic: prop.getBaseTopic() + "/set",
options: this.capability.getProperties().supportedModes,
icon: "mdi:waves",
entity_category: EntityCategory.CONFIG,
}
})
);
});
})
);
}
}
CarpetSensorModeControlCapabilityMqttHandle.OPTIONAL = true;
module.exports = CarpetSensorModeControlCapabilityMqttHandle;

View File

@ -3,13 +3,14 @@ const CapabilityMqttHandle = require("./CapabilityMqttHandle");
const Commands = require("../common/Commands");
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const DeviceClass = require("../homeassistant/DeviceClass");
const EntityCategory = require("../homeassistant/EntityCategory");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const Logger = require("../../Logger");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const stateAttrs = require("../../entities/state/attributes");
const StateClass = require("../homeassistant/StateClass");
const Unit = require("../common/Unit");
const ValetudoConsumable = require("../../entities/core/ValetudoConsumable");
class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
/**
@ -29,6 +30,8 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
}
}));
this.capability = options.capability;
/* @type {Array<import("../../entities/core/ValetudoConsumable")>} */
this.consumables = [];
this.capability.getProperties().availableConsumables.forEach(consumable => {
this.addNewConsumable(
@ -42,13 +45,13 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
/**
* @private
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").TYPE} type
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").SUB_TYPE} subType
* @param {import("../../entities/core/ValetudoConsumable").TYPE} type
* @param {import("../../entities/core/ValetudoConsumable").SUB_TYPE} subType
* @return {string}
*/
genConsumableTopicId(type, subType) {
let name = type;
if (subType !== stateAttrs.ConsumableStateAttribute.SUB_TYPE.NONE) {
if (subType !== ValetudoConsumable.SUB_TYPE.NONE) {
name += "-" + subType;
}
return name;
@ -56,13 +59,13 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
/**
* @private
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").TYPE} type
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").SUB_TYPE} subType
* @param {import("../../entities/core/ValetudoConsumable").TYPE} type
* @param {import("../../entities/core/ValetudoConsumable").SUB_TYPE} subType
* @return {string}
*/
genConsumableFriendlyName(type, subType) {
let name = "";
if (subType !== stateAttrs.ConsumableStateAttribute.SUB_TYPE.NONE && subType !== stateAttrs.ConsumableStateAttribute.SUB_TYPE.ALL) {
if (subType !== ValetudoConsumable.SUB_TYPE.NONE && subType !== ValetudoConsumable.SUB_TYPE.ALL) {
name += SUBTYPE_MAPPING[subType] + " ";
}
name += TYPE_MAPPING[type];
@ -72,9 +75,9 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
/**
* @private
* @param {string} topicId
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").TYPE} type
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").SUB_TYPE} subType
* @param {import("../../entities/state/attributes/ConsumableStateAttribute").UNITS} unit
* @param {import("../../entities/core/ValetudoConsumable").TYPE} type
* @param {import("../../entities/core/ValetudoConsumable").SUB_TYPE} subType
* @param {import("../../entities/core/ValetudoConsumable").UNITS} unit
* @return {void}
*/
addNewConsumable(topicId, type, subType, unit) {
@ -85,28 +88,24 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
topicName: topicId,
friendlyName: this.genConsumableFriendlyName(type, subType),
datatype: DataType.INTEGER,
unit: unit === stateAttrs.ConsumableStateAttribute.UNITS.PERCENT ? Unit.PERCENT : undefined,
format: unit === stateAttrs.ConsumableStateAttribute.UNITS.PERCENT ? "0:100" : undefined,
unit: unit === ValetudoConsumable.UNITS.PERCENT ? Unit.PERCENT : undefined,
format: unit === ValetudoConsumable.UNITS.PERCENT ? "0:100" : undefined,
getter: async () => {
const newAttr = this.robot.state.getFirstMatchingAttribute({
attributeClass: stateAttrs.ConsumableStateAttribute.name,
attributeType: type,
attributeSubType: subType
});
const consumable = this.consumables.find(c => c.type === type && c.subType === subType);
if (newAttr) {
if (consumable) {
// Raw value for Home Assistant
await this.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.CONSUMABLE_VALUE + topicId
).post(newAttr.remaining.value);
).post(consumable.remaining.value);
// Convert value to seconds for Homie
return newAttr.remaining.value * (unit === stateAttrs.ConsumableStateAttribute.UNITS.PERCENT ? 1 : 60);
return consumable.remaining.value * (unit === ValetudoConsumable.UNITS.PERCENT ? 1 : 60);
}
return null;
},
helpText: unit === stateAttrs.ConsumableStateAttribute.UNITS.PERCENT ?
helpText: unit === ValetudoConsumable.UNITS.PERCENT ?
"This handle returns the consumable remaining endurance percentage." :
"This handle returns the consumable remaining endurance time as an int representing seconds remaining."
}).also((prop) => {
@ -125,9 +124,11 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
state_topic: this.controller.hassAnchorProvider.getTopicReference(
HassAnchor.REFERENCE.HASS_CONSUMABLE_STATE + topicId
),
unit_of_measurement: unit === stateAttrs.ConsumableStateAttribute.UNITS.PERCENT ? Unit.PERCENT : Unit.MINUTES,
unit_of_measurement: unit === ValetudoConsumable.UNITS.PERCENT ? Unit.PERCENT : Unit.MINUTES,
icon: "mdi:progress-wrench",
entity_category: EntityCategory.DIAGNOSTIC
entity_category: EntityCategory.DIAGNOSTIC,
state_class: StateClass.MEASUREMENT,
device_class: unit === ValetudoConsumable.UNITS.MINUTES ? DeviceClass.DURATION : undefined
},
topics: {
"": this.controller.hassAnchorProvider.getAnchor(
@ -150,6 +151,7 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
format: Commands.BASIC.PERFORM,
setter: async (value) => {
await this.capability.resetConsumable(type, subType);
await this.updateInternalState(); // FIXME: this should also republish the state of the other child
}
}).also((prop) => {
this.controller.withHass((hass) => {
@ -174,43 +176,41 @@ class ConsumableMonitoringCapabilityMqttHandle extends CapabilityMqttHandle {
}
async refresh() {
await this.capability.getConsumables();
await this.updateInternalState();
await super.refresh();
}
onStatusSubscriberEvent() {
/*
We need to override this method as otherwise, we'd end up in an endless loop
due to refresh() triggering a consumables poll triggering a statusAttribute update
triggering a refresh() triggering a consumables poll...
*/
super.refresh().then(() => { /* intentional */ }).catch(err => {
Logger.error("Error during MqttHandle refresh", err);
});
}
async updateInternalState() {
let newConsumables;
try {
newConsumables = await this.capability.getConsumables();
getInterestingStatusAttributes() {
return [{attributeClass: stateAttrs.ConsumableStateAttribute.name}];
this.consumables = newConsumables;
} catch (e) {
/* intentional */
}
}
}
const TYPE_MAPPING = Object.freeze({
[stateAttrs.ConsumableStateAttribute.TYPE.BRUSH]: "Brush",
[stateAttrs.ConsumableStateAttribute.TYPE.FILTER]: "Filter",
[stateAttrs.ConsumableStateAttribute.TYPE.SENSOR]: "Sensor cleaning",
[stateAttrs.ConsumableStateAttribute.TYPE.MOP]: "Mop",
[stateAttrs.ConsumableStateAttribute.TYPE.DETERGENT]: "Detergent",
[stateAttrs.ConsumableStateAttribute.TYPE.BIN]: "Bin",
[ValetudoConsumable.TYPE.BRUSH]: "Brush",
[ValetudoConsumable.TYPE.FILTER]: "Filter",
[ValetudoConsumable.TYPE.CLEANING]: "Cleaning",
[ValetudoConsumable.TYPE.MOP]: "Mop",
[ValetudoConsumable.TYPE.DETERGENT]: "Detergent",
[ValetudoConsumable.TYPE.BIN]: "Bin",
});
const SUBTYPE_MAPPING = Object.freeze({
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.MAIN]: "Main",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.SECONDARY]: "Secondary",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.SIDE_RIGHT]: "Right",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.SIDE_LEFT]: "Left",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.ALL]: "",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.NONE]: "",
[stateAttrs.ConsumableStateAttribute.SUB_TYPE.DOCK]: "Dock",
[ValetudoConsumable.SUB_TYPE.MAIN]: "Main",
[ValetudoConsumable.SUB_TYPE.SECONDARY]: "Secondary",
[ValetudoConsumable.SUB_TYPE.SIDE_RIGHT]: "Right",
[ValetudoConsumable.SUB_TYPE.SIDE_LEFT]: "Left",
[ValetudoConsumable.SUB_TYPE.ALL]: "",
[ValetudoConsumable.SUB_TYPE.NONE]: "",
[ValetudoConsumable.SUB_TYPE.DOCK]: "Dock",
[ValetudoConsumable.SUB_TYPE.SENSOR]: "Sensor",
[ValetudoConsumable.SUB_TYPE.WHEEL]: "Wheel",
});
ConsumableMonitoringCapabilityMqttHandle.OPTIONAL = true;

View File

@ -1,11 +1,13 @@
const CapabilityMqttHandle = require("./CapabilityMqttHandle");
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const DeviceClass = require("../homeassistant/DeviceClass");
const EntityCategory = require("../homeassistant/EntityCategory");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const Logger = require("../../Logger");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const StateClass = require("../homeassistant/StateClass");
const Unit = require("../common/Unit");
const ValetudoDataPoint = require("../../entities/core/ValetudoDataPoint");
@ -56,7 +58,9 @@ class CurrentStatisticsCapabilityMqttHandle extends CapabilityMqttHandle {
state_topic: prop.getBaseTopic(),
icon: "mdi:equalizer",
entity_category: EntityCategory.DIAGNOSTIC,
unit_of_measurement: Unit.SECONDS
unit_of_measurement: Unit.SECONDS,
device_class: DeviceClass.DURATION,
state_class: StateClass.MEASUREMENT
}
})
);
@ -92,7 +96,9 @@ class CurrentStatisticsCapabilityMqttHandle extends CapabilityMqttHandle {
state_topic: prop.getBaseTopic(),
icon: "mdi:equalizer",
entity_category: EntityCategory.DIAGNOSTIC,
unit_of_measurement: Unit.SQUARE_CENTIMETER
unit_of_measurement: Unit.SQUARE_CENTIMETER,
device_class: DeviceClass.AREA,
state_class: StateClass.MEASUREMENT
}
})
);

View File

@ -42,7 +42,7 @@ class MapSegmentationCapabilityMqttHandle extends CapabilityMqttHandle {
for (const id of reqSegments.segment_ids) {
const segment = robotSegments.find(segm => {
return (segm.id === id || parseInt(segm.id) === id);
return segm.id === `${id}`; // Ensure that it works even if the user incorrectly passes numbers
});
if (!segment) {
throw new Error(`Segment ID does not exist, or map was not loaded: ${id}`);

View File

@ -0,0 +1,8 @@
const SimpleToggleCapabilityMqttHandle = require("./SimpleToggleCapabilityMqttHandle");
class PetObstacleAvoidanceControlCapabilityMqttHandle extends SimpleToggleCapabilityMqttHandle {}
PetObstacleAvoidanceControlCapabilityMqttHandle.OPTIONAL = true;
module.exports = PetObstacleAvoidanceControlCapabilityMqttHandle;

View File

@ -30,7 +30,7 @@ class PresetSelectionCapabilityMqttHandle extends CapabilityMqttHandle {
parent: this,
controller: this.controller,
topicName: "preset",
friendlyName: CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[options.capability.getType()],
friendlyName: CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[this.capability.getType()],
datatype: DataType.ENUM,
format: this.capability.getPresets().join(","),
setter: async (value) => {
@ -63,15 +63,15 @@ class PresetSelectionCapabilityMqttHandle extends CapabilityMqttHandle {
return attr.value;
},
helpText: "This handle allows setting the " +
CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[options.capability.getType()].toLowerCase() + ". " +
CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[this.capability.getType()].toLowerCase() + ". " +
"It accepts the preset payloads specified in `$format` or in the HAss json attributes.",
helpMayChange: {
"Enum payloads": "Different robot models have different " +
CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[options.capability.getType()].toLowerCase() +
CAPABILITIES_TO_FRIENDLY_NAME_MAPPING[this.capability.getType()].toLowerCase() +
" presets. Always check `$format`/`json_attributes` during startup."
}
}).also((prop) => {
const capabilityType = options.capability.getType();
const capabilityType = this.capability.getType();
this.controller.withHass((hass) => {
prop.attachHomeAssistantComponent(

View File

@ -78,11 +78,15 @@ class SimpleToggleCapabilityMqttHandle extends CapabilityMqttHandle {
const CAPABILITIES_TO_FRIENDLY_NAME_MAPPING = {
[capabilities.KeyLockCapability.TYPE]: "Lock Keys",
[capabilities.ObstacleAvoidanceControlCapability.TYPE]: "Obstacle Avoidance",
[capabilities.PetObstacleAvoidanceControlCapability.TYPE]: "Pet Obstacle Avoidance",
[capabilities.CarpetModeControlCapability.TYPE]: "Carpet Mode",
};
const CAPABILITIES_TO_ICON_MAPPING = {
[capabilities.KeyLockCapability.TYPE]: "mdi:lock",
[capabilities.ObstacleAvoidanceControlCapability.TYPE]: "mdi:cable-data",
[capabilities.PetObstacleAvoidanceControlCapability.TYPE]: "mdi:paw",
[capabilities.CarpetModeControlCapability.TYPE]: "mdi:access-point",
};
module.exports = SimpleToggleCapabilityMqttHandle;

View File

@ -1,11 +1,13 @@
const CapabilityMqttHandle = require("./CapabilityMqttHandle");
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const DeviceClass = require("../homeassistant/DeviceClass");
const EntityCategory = require("../homeassistant/EntityCategory");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const Logger = require("../../Logger");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const StateClass = require("../homeassistant/StateClass");
const Unit = require("../common/Unit");
const ValetudoDataPoint = require("../../entities/core/ValetudoDataPoint");
@ -56,7 +58,9 @@ class TotalStatisticsCapabilityMqttHandle extends CapabilityMqttHandle {
state_topic: prop.getBaseTopic(),
icon: "mdi:equalizer",
entity_category: EntityCategory.DIAGNOSTIC,
unit_of_measurement: Unit.SECONDS
unit_of_measurement: Unit.SECONDS,
device_class: DeviceClass.DURATION,
state_class: StateClass.TOTAL_INCREASING
}
})
);
@ -92,7 +96,9 @@ class TotalStatisticsCapabilityMqttHandle extends CapabilityMqttHandle {
state_topic: prop.getBaseTopic(),
icon: "mdi:equalizer",
entity_category: EntityCategory.DIAGNOSTIC,
unit_of_measurement: Unit.SQUARE_CENTIMETER
unit_of_measurement: Unit.SQUARE_CENTIMETER,
device_class: DeviceClass.AREA,
state_class: StateClass.TOTAL_INCREASING
}
})
);
@ -126,7 +132,8 @@ class TotalStatisticsCapabilityMqttHandle extends CapabilityMqttHandle {
autoconf: {
state_topic: prop.getBaseTopic(),
icon: "mdi:equalizer",
entity_category: EntityCategory.DIAGNOSTIC
entity_category: EntityCategory.DIAGNOSTIC,
state_class: StateClass.TOTAL_INCREASING
}
})
);

View File

@ -6,6 +6,7 @@ const EntityCategory = require("../homeassistant/EntityCategory");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const StateClass = require("../homeassistant/StateClass");
const Unit = require("../common/Unit");
class WifiConfigurationCapabilityMqttHandle extends CapabilityMqttHandle {
@ -98,7 +99,8 @@ class WifiConfigurationCapabilityMqttHandle extends CapabilityMqttHandle {
),
json_attributes_template: "{{ value_json.attributes | to_json }}",
entity_category: EntityCategory.DIAGNOSTIC,
device_class: DeviceClass.SIGNAL_STRENGTH
device_class: DeviceClass.SIGNAL_STRENGTH,
state_class: StateClass.MEASUREMENT
},
topics: {
"": {

View File

@ -2,6 +2,8 @@ module.exports = {
AutoEmptyDockManualTriggerCapabilityMqttHandle: require("./AutoEmptyDockManualTriggerCapabilityMqttHandle"),
BasicControlCapabilityMqttHandle: require("./BasicControlCapabilityMqttHandle"),
CapabilityMqttHandle: require("./CapabilityMqttHandle"),
CarpetModeControlCapabilityMqttHandle: require("./CarpetModeControlCapabilityMqttHandle"),
CarpetSensorModeControlCapabilityMqttHandle: require("./CarpetSensorModeControlCapabilityMqttHandle"),
ConsumableMonitoringCapabilityMqttHandle: require("./ConsumableMonitoringCapabilityMqttHandle"),
CurrentStatisticsCapabilityMqttHandle: require("./CurrentStatisticsCapabilityMqttHandle"),
GoToLocationCapabilityMqttHandle: require("./GoToLocationCapabilityMqttHandle"),
@ -9,6 +11,7 @@ module.exports = {
LocateCapabilityMqttHandle: require("./LocateCapabilityMqttHandle"),
MapSegmentationCapabilityMqttHandle: require("./MapSegmentationCapabilityMqttHandle"),
ObstacleAvoidanceControlCapabilityMqttHandle: require("./ObstacleAvoidanceControlCapabilityMqttHandle"),
PetObstacleAvoidanceControlCapabilityMqttHandle: require("./PetObstacleAvoidanceControlCapabilityMqttHandle"),
PresetSelectionCapabilityMqttHandle: require("./PresetSelectionCapabilityMqttHandle"),
SpeakerVolumeControlCapabilityMqttHandle: require("./SpeakerVolumeControlCapabilityMqttHandle"),
TotalStatisticsCapabilityMqttHandle: require("./TotalStatisticsCapabilityMqttHandle"),

View File

@ -22,9 +22,10 @@ const Unit = Object.freeze({
PASCAL: "Pa",
PSI: "psi",
AMOUNT: "#",
// Not part of the specification, but useful
SECONDS: "seconds",
MINUTES: "minutes",
// Not part of the homie specification
SECONDS: "s",
MINUTES: "min",
SQUARE_CENTIMETER: "cm²",
SQUARE_METER: "m²",
CUBE_METER: "m³",

View File

@ -19,7 +19,10 @@ const CAPABILITY_TYPE_TO_HANDLE_MAPPING = {
[capabilities.TotalStatisticsCapability.TYPE]: capabilityHandles.TotalStatisticsCapabilityMqttHandle,
[capabilities.SpeakerVolumeControlCapability.TYPE]: capabilityHandles.SpeakerVolumeControlCapabilityMqttHandle,
[capabilities.KeyLockCapability.TYPE]: capabilityHandles.KeyLockCapabilityMqttHandle,
[capabilities.ObstacleAvoidanceControlCapability.TYPE]: capabilityHandles.ObstacleAvoidanceControlCapabilityMqttHandle
[capabilities.ObstacleAvoidanceControlCapability.TYPE]: capabilityHandles.ObstacleAvoidanceControlCapabilityMqttHandle,
[capabilities.PetObstacleAvoidanceControlCapability.TYPE]: capabilityHandles.PetObstacleAvoidanceControlCapabilityMqttHandle,
[capabilities.CarpetModeControlCapability.TYPE]: capabilityHandles.CarpetModeControlCapabilityMqttHandle,
[capabilities.CarpetSensorModeControlCapability.TYPE]: capabilityHandles.CarpetSensorModeControlCapabilityMqttHandle,
};
const STATUS_ATTR_TO_HANDLE_MAPPING = [
@ -34,6 +37,10 @@ const STATUS_ATTR_TO_HANDLE_MAPPING = [
{
matcher: {attributeClass: stateAttrs.StatusStateAttribute.name},
handle: stateHandles.StatusStateMqttHandle
},
{
matcher: {attributeClass: stateAttrs.DockStatusStateAttribute.name},
handle: stateHandles.DockStatusStateMqttHandle
}
];

View File

@ -36,35 +36,13 @@ class MapNodeMqttHandle extends NodeMqttHandle {
topicName: "map-data",
friendlyName: "Raw map data",
datatype: DataType.STRING,
format: "json, but deflated",
getter: async () => {
return this.getMapData(false);
}
})
);
// Add "I Can't Believe It's Not Valetudo" map property. Unlike Home Assistant, Homie autodiscovery attributes
// may not be changed by external services, so for proper autodiscovery support it needs to be provided by
// Valetudo itself. ICBINV may publish the data at any point in time.
if (this.controller.currentConfig.interfaces.homie.addICBINVMapProperty) {
this.registerChild(
new PropertyMqttHandle({
parent: this,
controller: this.controller,
topicName: "map",
friendlyName: "Map",
datatype: DataType.STRING,
getter: async () => {
/* intentional */
},
helpText: "This handle is only enabled if `interfaces.homie.addICBINVMapProperty` is enabled in the config. " +
"It does not actually provide map data, it only adds a Homie autodiscovery property so that " +
"'I Can't Believe It's Not Valetudo' can publish its map within the robot's topics and be " +
"autodetected by clients.\n\n" +
"ICBINV should be configured so that it publishes the map to this topic."
})
);
}
this.registerChild(
new PropertyMqttHandle({
parent: this,
@ -223,6 +201,7 @@ class MapNodeMqttHandle extends NodeMqttHandle {
});
try {
// intentional return await
return await promise;
} catch (err) {
Logger.error("Error while deflating map data for mqtt publish", err);

View File

@ -128,12 +128,16 @@ class RobotMqttHandle extends MqttHandle {
}));
}
await this.deconfigure({
cleanHomie: false,
unsubscribe: false
});
if (this.controller.isConnected) {
await this.deconfigure({
cleanHomie: false,
unsubscribe: false
});
await this.configure();
await this.configure();
} else {
Logger.debug("Skipping (de)configure on robot status attribute discovery, as we're not connected to MQTT.");
}
});
}

View File

@ -1,5 +1,5 @@
/**
* Retrieved from https://www.home-assistant.io/docs/mqtt/discovery/ on 2021-04-05.
* Retrieved from https://www.home-assistant.io/docs/mqtt/discovery/ on 2025-09-15.
*
* @enum {string}
*/
@ -8,20 +8,30 @@ const ComponentType = Object.freeze({
BINARY_SENSOR: "binary_sensor",
BUTTON: "button",
CAMERA: "camera",
CLIMATE: "climate",
COVER: "cover",
DEVICE_TRACKER: "device_tracker",
DEVICE_TRIGGER: "device_trigger",
EVENT: "event",
FAN: "fan",
HVAC: "climate",
HUMIDIFIER: "humidifier",
IMAGE: "image",
LAWN_MOWER: "lawn_mower",
LIGHT: "light",
LOCK: "lock",
NOTIFY: "notify",
NUMBER: "number",
SCENE: "scene",
SELECT: "select",
SENSOR: "sensor",
SIREN: "siren",
SWITCH: "switch",
TAG_SCANNER: "tag",
TEXT: "text",
UPDATE: "update",
VACUUM: "vacuum",
VALVE: "valve",
WATER_HEATER: "water_heater",
});
module.exports = ComponentType;

View File

@ -1,5 +1,5 @@
/**
* Retrieved from https://github.com/home-assistant/core/blob/8b1cfbc46cc79e676f75dfa4da097a2e47375b6f/homeassistant/components/sensor/const.py#L64-L416 on 2023-10-25.
* Retrieved from https://github.com/home-assistant/core/blob/4c22264b13bf4f7428ab9e911d58725dee512c78/homeassistant/components/sensor/const.py on 2025-09-15.
*
* See also https://developers.home-assistant.io/docs/core/entity/#generic-properties
*
@ -9,18 +9,24 @@ const DeviceClass = Object.freeze({
DATE: "date",
ENUM: "enum",
TIMESTAMP: "timestamp",
ABSOLUTE_HUMIDITY: "absolute_humidity",
APPARENT_POWER: "apparent_power",
AQI: "aqi",
AREA: "area",
ATMOSPHERIC_PRESSURE: "atmospheric_pressure",
BATTERY: "battery",
BLOOD_GLUCOSE_CONCENTRATION: "blood_glucose_concentration",
CO: "carbon_monoxide",
CO2: "carbon_dioxide",
CONDUCTIVITY: "conductivity",
CURRENT: "current",
DATA_RATE: "data_rate",
DATA_SIZE: "data_size",
DISTANCE: "distance",
DURATION: "duration",
ENERGY: "energy",
ENERGY_DISTANCE: "energy_distance",
ENERGY_STORAGE: "energy_storage",
FREQUENCY: "frequency",
GAS: "gas",
@ -37,11 +43,12 @@ const DeviceClass = Object.freeze({
PM1: "pm1",
PM10: "pm10",
PM25: "pm25",
POWER_FACTOR: "power_factor",
POWER: "power",
POWER_FACTOR: "power_factor",
PRECIPITATION: "precipitation",
PRECIPITATION_INTENSITY: "precipitation_intensity",
PRESSURE: "pressure",
REACTIVE_ENERGY: "reactive_energy",
REACTIVE_POWER: "reactive_power",
SIGNAL_STRENGTH: "signal_strength",
SOUND_PRESSURE: "sound_pressure",
@ -52,9 +59,11 @@ const DeviceClass = Object.freeze({
VOLATILE_ORGANIC_COMPOUNDS_PARTS: "volatile_organic_compounds_parts",
VOLTAGE: "voltage",
VOLUME: "volume",
VOLUME_FLOW_RATE: "volume_flow_rate",
VOLUME_STORAGE: "volume_storage",
WATER: "water",
WEIGHT: "weight",
WIND_DIRECTION: "wind_direction",
WIND_SPEED: "wind_speed"
});

View File

@ -1,5 +1,5 @@
/**
* Retrieved from https://github.com/home-assistant/core/blob/7abf79d1f991d58051fea0afe56e714ce60d7fdb/homeassistant/const.py#L715-L717 on 2021-11-06.
* Retrieved from https://github.com/home-assistant/core/blob/c5fc1de3df3db1250e1e21d727bb5849408964a7/homeassistant/const.py#L1074-L1087 on 2025-09-15.
*
* See also https://developers.home-assistant.io/docs/core/entity/#generic-properties
*
@ -7,8 +7,7 @@
*/
const EntityCategory = Object.freeze({
CONFIG: "config",
DIAGNOSTIC: "diagnostic",
SYSTEM: "system"
DIAGNOSTIC: "diagnostic"
});
module.exports = EntityCategory;

View File

@ -96,8 +96,6 @@ HassAnchor.TYPE = Object.freeze({
});
HassAnchor.ANCHOR = Object.freeze({
BATTERY_LEVEL: "battery_level",
BATTERY_CHARGING: "battery_charging",
CONSUMABLE_VALUE: "consumable_value_",
CURRENT_STATISTICS_TIME: "current_statistics_time",
CURRENT_STATISTICS_AREA: "current_statistics_area",

View File

@ -76,11 +76,11 @@ class HassController {
*/
getAutoconfDeviceBoilerplate() {
return {
manufacturer: this.robot.getManufacturer(),
model: this.robot.getModelName(),
manufacturer: "Valetudo",
model: `${this.robot.getManufacturer()} ${this.robot.getModelName()}`,
name: this.friendlyName,
identifiers: [this.identifier],
sw_version: `Valetudo ${Tools.GET_VALETUDO_VERSION()}`,
sw_version: Tools.GET_VALETUDO_VERSION(),
configuration_url: `http://${Tools.GET_ZEROCONF_HOSTNAME()}`
};
}

View File

@ -0,0 +1,13 @@
/**
* Retrieved from https://github.com/home-assistant/core/blob/c5fc1de3df3db1250e1e21d727bb5849408964a7/homeassistant/components/sensor/const.py#L509-L526 on 2025-09-15.
*
* @enum {string}
*/
const StateClass = Object.freeze({
MEASUREMENT: "measurement",
MEASUREMENT_ANGLE: "measurement_angle",
TOTAL: "total",
TOTAL_INCREASING: "total_increasing",
});
module.exports = StateClass;

View File

@ -30,10 +30,14 @@ class InLineHassComponent extends HassComponent {
}
}
/**
* @public
* @return {{[key: string]: any}}
*/
getAutoconf() {
return Object.assign(this.autoconf, {
name: this.friendlyName,
object_id: `${this.hass.objectId}_${this.friendlyName.toLowerCase()}`
object_id: `${this.hass.objectId}_${this.friendlyName.toLowerCase().replace(/ /g, "_")}`
});
}

View File

@ -25,7 +25,6 @@ class VacuumHassComponent extends HassComponent {
name: "Robot",
object_id: this.hass.objectId,
supported_features: [
"battery",
"status",
"start",
"stop",
@ -67,9 +66,6 @@ class VacuumHassComponent extends HassComponent {
"state": this.hass.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.VACUUM_STATE
),
"battery_level": this.hass.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.BATTERY_LEVEL
),
}
};
if (this.robot.hasCapability(capabilities.FanSpeedControlCapability.TYPE)) {

View File

@ -2,12 +2,11 @@ const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const DeviceClass = require("../homeassistant/DeviceClass");
const EntityCategory = require("../homeassistant/EntityCategory");
const HassAnchor = require("../homeassistant/HassAnchor");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const Logger = require("../../Logger");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const RobotStateNodeMqttHandle = require("../handles/RobotStateNodeMqttHandle");
const stateAttrs = require("../../entities/state/attributes");
const StateClass = require("../homeassistant/StateClass");
const Unit = require("../common/Unit");
class BatteryStateMqttHandle extends RobotStateNodeMqttHandle {
@ -37,12 +36,6 @@ class BatteryStateMqttHandle extends RobotStateNodeMqttHandle {
return null;
}
this.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.BATTERY_LEVEL
).post(batteryState.level).catch(err => {
Logger.error("Error while posting value to HassAnchor", err);
});
return batteryState.level;
}
}).also((prop) => {
@ -59,7 +52,8 @@ class BatteryStateMqttHandle extends RobotStateNodeMqttHandle {
icon: "mdi:battery",
entity_category: EntityCategory.DIAGNOSTIC,
unit_of_measurement: Unit.PERCENT,
device_class: DeviceClass.BATTERY
device_class: DeviceClass.BATTERY,
state_class: StateClass.MEASUREMENT
}
})
);
@ -79,10 +73,6 @@ class BatteryStateMqttHandle extends RobotStateNodeMqttHandle {
return null;
}
await this.controller.hassAnchorProvider.getAnchor(
HassAnchor.ANCHOR.BATTERY_CHARGING
).post(batteryState.flag === stateAttrs.BatteryStateAttribute.FLAG.CHARGING);
return batteryState.flag;
}
}));

View File

@ -0,0 +1,66 @@
const ComponentType = require("../homeassistant/ComponentType");
const DataType = require("../homie/DataType");
const EntityCategory = require("../homeassistant/EntityCategory");
const InLineHassComponent = require("../homeassistant/components/InLineHassComponent");
const PropertyMqttHandle = require("../handles/PropertyMqttHandle");
const RobotStateNodeMqttHandle = require("../handles/RobotStateNodeMqttHandle");
const stateAttrs = require("../../entities/state/attributes");
class DockStatusStateMqttHandle extends RobotStateNodeMqttHandle {
/**
* @param {object} options
* @param {import("../handles/RobotMqttHandle")} options.parent
* @param {import("../MqttController")} options.controller MqttController instance
* @param {import("../../core/ValetudoRobot")} options.robot
*/
constructor(options) {
super(Object.assign(options, {
topicName: "DockStatusStateAttribute",
friendlyName: "Dock state",
type: "Status"
}));
this.registerChild(new PropertyMqttHandle({
parent: this,
controller: this.controller,
topicName: "status",
friendlyName: "Status",
datatype: DataType.ENUM,
format: Object.values(stateAttrs.DockStatusStateAttribute.VALUE).join(","),
getter: async () => {
const dockStatus = this.robot.state.getFirstMatchingAttribute({
attributeClass: stateAttrs.DockStatusStateAttribute.name
});
if (dockStatus === null) {
return false;
}
return dockStatus.value;
},
}).also((prop) => {
this.controller.withHass((hass => {
prop.attachHomeAssistantComponent(
new InLineHassComponent({
hass: hass,
robot: this.robot,
name: "dock_status",
friendlyName: "Dock Status",
componentType: ComponentType.SENSOR,
autoconf: {
state_topic: prop.getBaseTopic(),
icon: "mdi:home",
entity_category: EntityCategory.DIAGNOSTIC
}
})
);
}));
}));
}
getInterestingStatusAttributes() {
return [{attributeClass: stateAttrs.DockStatusStateAttribute.name}];
}
}
module.exports = DockStatusStateMqttHandle;

View File

@ -1,5 +1,6 @@
module.exports = {
AttachmentStateMqttHandle: require("./AttachmentStateMqttHandle"),
BatteryStateMqttHandle: require("./BatteryStateMqttHandle"),
StatusStateMqttHandle: require("./StatusStateMqttHandle"),
DockStatusStateMqttHandle: require("./DockStatusStateMqttHandle"),
StatusStateMqttHandle: require("./StatusStateMqttHandle")
};

View File

@ -0,0 +1,441 @@
const Logger = require("../Logger");
const MSmartConst = require("./MSmartConst");
const MSmartPacket = require("./MSmartPacket");
const dtos = require("./dtos");
class BEightParser {
/**
* @param {MSmartPacket} packet
* @returns {import("./dtos/MSmartDTO")|"SKIP"|undefined} - FIXME: remove SKIP
*/
static PARSE(packet) {
const payload = packet.payload;
switch (packet.messageType) {
case MSmartPacket.MESSAGE_TYPE.SETTING: {
// 0xaa 0x01 <typeId>
switch (payload[2]) {
case 0xc4: // FIXME
// No idea. Sample: aa 01 c4 04 00 00 00 00 5c 00 9d 10 01
return "SKIP";
case 0x9d: // FIXME
// Network state? Sample: aa 01 9d 01 02 01
return "SKIP";
case 0x22: // FIXME
// bSaveMapSwitch. Sample: aa 01 22 00
return "SKIP";
default: {
Logger.warn(
`Unhandled SETTING packet with typeId '${payload[2]}'`,
packet.toHexString()
);
}
}
break;
}
case MSmartPacket.MESSAGE_TYPE.ACTION: {
// 0xaa 0x01 <typeId>
switch (payload[2]) {
case MSmartConst.ACTION.GET_STATUS: {
const data = BEightParser._parse_status_payload(payload);
return new dtos.MSmartStatusDTO(data);
}
case MSmartConst.ACTION.LIST_MAPS: {
if (payload.length < 9) {
Logger.warn("Received invalid LIST_MAPS response. Payload too short.");
return undefined;
}
const data = {
currentMapId: payload[5],
savedMapIds: []
};
const mapBitfield = payload.readUInt16LE(7);
for (let i = 0; i < 16; i++) {
if ((mapBitfield >> i) & 1) {
data.savedMapIds.push(i + 1);
}
}
return new dtos.MSmartMapListDTO(data);
}
case MSmartConst.ACTION.GET_ACTIVE_ZONES: {
const data = BEightParser._parse_active_zones_payload(payload);
return new dtos.MSmartActiveZonesDTO(data);
}
case MSmartConst.ACTION.GET_DND: {
const data = BEightParser._parse_dnd_payload(payload);
return new dtos.MSmartDndConfigurationDTO(data);
}
case MSmartConst.ACTION.GET_CLEANING_SETTINGS_1: {
const data = BEightParser._parse_cleaning_settings_1_payload(payload);
return new dtos.MSmartCleaningSettings1DTO(data);
}
case MSmartConst.ACTION.GET_CARPET_BEHAVIOR_SETTINGS: {
const data = BEightParser._parse_carpet_behavior_settings_payload(payload);
return new dtos.MSmartCarpetBehaviorSettingsDTO(data);
}
default: {
Logger.warn(
`Unhandled ACTION packet with typeId '${payload[2]}'`,
packet.toHexString()
);
}
}
break;
}
case MSmartPacket.MESSAGE_TYPE.EVENT: {
// 0xaa 0x01 <typeId>
switch (payload[2]) {
case MSmartConst.EVENT.STATUS: {
const data = BEightParser._parse_status_payload(payload);
return new dtos.MSmartStatusDTO(data);
}
case MSmartConst.EVENT.ACTIVE_ZONES: {
const data = BEightParser._parse_active_zones_payload(payload);
return new dtos.MSmartActiveZonesDTO(data);
}
case MSmartConst.EVENT.ERROR: {
return new dtos.MSmartErrorDTO({
error_type: payload[3],
error_desc: payload[4],
sta_index: payload[5],
});
}
case MSmartConst.EVENT.CLEANING_SETTINGS_1: {
const data = BEightParser._parse_cleaning_settings_1_payload(payload);
return new dtos.MSmartCleaningSettings1DTO(data);
}
case 0xA8: // FIXME
// This seems to be relating to the dock state and what it is doing
// There are also timers in here?
// payload[3]; // Mode?. 0x00:Idle?, 0x01:Clean?, 0x02:Empty, 0x03:Dry, 0x05:Wash, possibly hair cuttting?
// payload.readUInt32LE(4); // unclear
// payload.readUInt32LE(8); // timer. Seconds counting up
// payload[12]; // unclear
// payload.readUInt32LE(13); // possibly expected duration of the timer in seconds
return "SKIP";
case 0x52:
// No clue where this is coming from. Seen on the J12 about once every minute. Might be a state update?
return "SKIP";
case 0x20:
// Seems to be relating to map state?
return "SKIP";
case 0x21:
// No clue
return "SKIP";
default: {
Logger.warn(
`Unhandled EVENT packet with typeId '${payload[2]}'`,
packet.toHexString()
);
}
}
break;
}
case MSmartPacket.MESSAGE_TYPE.DOCK: {
if (
payload[0] === 0x66 &&
payload[1] === 0x06
) {
return new dtos.MSmartDockStatusDTO({
dust_collection_count: payload[2],
fluid_1_ok: !!payload[5],
fluid_2_ok: !!payload[6],
});
} else {
Logger.warn("Unhandled DOCK packet", packet.toHexString());
}
break;
}
default: {
Logger.warn(
`Unhandled packet with messageType '${packet.messageType}'. ${packet.payload.subarray(0, 3).toString("hex")}`,
packet.toHexString()
);
}
}
return undefined;
}
/**
*
* @private
* @param {Buffer} payload
* @returns {object}
*/
static _parse_status_payload(payload) {
const data = {};
data.work_status = payload[3]; // 1 - 23
data.function_type = payload[4]; // 1 - 6
data.control_type = payload[5]; // 0 - 2
data.move_direction = payload[6]; // 0 - 8
data.work_mode = payload[7]; // 0 - 15
data.fan_level = payload[8]; // 0 - 5
// work_area and work_time each have +4 additional bits in payload[22]. - INSANE
data.work_area = ((payload[22] & 0b00001111) << 8) + payload[9];
data.water_level = payload[10]; // 0 - 3, OR high-res starting at >= 100
data.voice_level = payload[11]; // 0 - 100
// 12 => have_reserve_tank ??
data.battery_percent = payload[13]; // 0 - 100
// work_area and work_time each have +4 additional bits in payload[22]. - INSANE - TODO validate. It was [23] before the LLM suggested something else
data.work_time = (((payload[22] & 0b11110000) >> 4) << 8) + payload[14];
data.uv_switch = !!(payload[15] & 0b00000001);
data.wifi_switch = !!(payload[15] & 0b00000010);
data.voice_switch = !!(payload[15] & 0b00000100);
data.command_source = !!(payload[15] & 0b01000000);
data.device_error = !!(payload[15] & 0b10000000);
data.error_type = payload[16];
data.error_desc = payload[17];
const mopStatusByte = payload[18];
data.has_mop = !!(mopStatusByte & 0b00000001); // Mops attached bool
data.has_vibrate_mop = !!(mopStatusByte & 0b00000010);
data.carpet_switch = payload[19]; // bool, apparently superseded and just relevant for the j12?
// 20 is unknown
data.cleaning_type = payload[21]; // 0 - 6
data.vibrate_mode = payload[23] ? "careful" : "efficient"; // TODO: should this be string?
data.vibrate_switch = !!payload[24];
data.electrolyzed_water = !!payload[25];
data.electrolyzed_water_status = payload[26];
data.dustDragSwitch = !!(payload[27] & 0x01);
data.dustDragStatus = !!(payload[27] & 0x02);
data.dustTimes = payload[28];
data.dustedTimes = payload[29];
data.chargeDockType = payload[30];
const stationStatusBits = payload[33];
data.fluid_1_ok = !!(stationStatusBits & 0b00000100);
data.fluid_2_ok = !!(stationStatusBits & 0b00001000);
data.dustbag_installed = !!(stationStatusBits & 0b00100000);
data.dustbag_full = !!(stationStatusBits & 0b00010000);
data.mopMode = payload[34];
data.station_error_code = payload[35]; // 106 = freshwater empty, 152 = wastewater full, otherwise unknown
data.station_work_status = payload[36]; // 0 - 89 but with holes
data.job_state = payload[37]; // > 0 means "resumable" flag unless any other flag takes precedence
data.whole_process_state = payload[38];
data.continuous_clean_mode = !!payload[41];
// 42 is unknown
data.clean_sequence_switch = !!payload[43];
const childLockBits = payload[45];
data.child_lock_enabled = !!(childLockBits & 0b00000001);
data.child_lock_follows_dnd = !!(childLockBits & 0b00000010);
// 46-49 seem to be a 4 byte number? async_number? not sure
const generalSwitchBits1 = payload[50]; // Also known as general_switch
data.personal_clean_prefer_switch = !!(generalSwitchBits1 & 0b00000001);
data.station_inject_fluid_switch = !!(generalSwitchBits1 & 0b00000010);
data.station_inject_soft_fluid_switch = !!(generalSwitchBits1 & 0b00000100);
data.carpet_evade_switch = !!(generalSwitchBits1 & 0b00010000);
data.station_first_fast_wash_switch = !!(generalSwitchBits1 & 0b01000000);
data.pet_mode_switch = !!(generalSwitchBits1 & 0b10000000);
data.station_capability_flags = payload[52];
const generalSwitchBits2 = payload[53];
data.stain_clean_switch = !!(generalSwitchBits2 & 0b00000001);
data.ai_obstacle_switch = !!(generalSwitchBits2 & 0b00000010);
data.cross_bridge_switch = !!(generalSwitchBits2 & 0b00000100);
data.camera_led_switch = !!(generalSwitchBits2 & 0b00001000);
data.map_3d_switch = !!(generalSwitchBits2 & 0b00010000);
// Master toggle. When disabled, everything else will disable itself
// This is set to true once the user accepts some ToS
data.ai_recognition_switch = !!(generalSwitchBits2 & 0b00100000);
data.test_mode_type = payload[54];
data.hot_water_wash_mode = payload[55];
const generalSwitchBits3 = payload[56];
data.station_self_fluid_2_switch = !!(generalSwitchBits3 & 0b00000001);
data.slam_version_switch = !!(generalSwitchBits3 & 0b00000010);
data.hot_dry_charge_plate_switch = !!(generalSwitchBits3 & 0b00000100);
data.telnet_switch = !!(generalSwitchBits3 & 0b00001000);
data.mop_auto_dry_switch = !!(generalSwitchBits3 & 0b00010000);
data.ai_grade_avoidance_mode = !!(generalSwitchBits3 & 0b00100000);
data.tail_sweep_clean_switch = !!(generalSwitchBits3 & 0b01000000);
data.pound_sign_switch = !!(generalSwitchBits3 & 0b10000000); // TODO: naming - this is the criss cross pattern with multiple iterations
data.stationCleanFrequency = payload[57];
data.beautify_map_grade = payload[58];
data.collect_dust_mode = payload[59];
data.session_id = payload[61];
data.transaction_id = payload[62];
const generalSwitchBits4 = payload[63];
data.bridge_boost_switch = !!(generalSwitchBits4 & 0b00001000);
data.narrow_zone_recharge_switch = !!(generalSwitchBits4 & 0b00010000);
data.verification_map_switch = !!(generalSwitchBits4 & 0b00100000);
if (payload.length >= 67) {
const generalSwitchBits5 = payload[65];
data.wake_up_switch = !!(generalSwitchBits5 & 0b00000001);
data.ai_carpet_avoid_switch = !!(generalSwitchBits5 & 0b00000010);
data.carpet_evade_adaptive_switch = !!(generalSwitchBits5 & 0b00000100);
data.stuck_mark_switch = !!(generalSwitchBits5 & 0b00001000);
data.mop_extend_switch = !!(generalSwitchBits5 & 0b00100000);
data.zigzag_to_end_switch = !!(generalSwitchBits5 & 0b01000000);
data.remaining_area = payload.readUInt16LE(66);
const generalSwitchBits6 = payload[68];
data.ai_avoidance_switch = !!(generalSwitchBits6 & 0b00001000);
data.gap_deep_cleaning_switch = !!(generalSwitchBits6 & 0b00010000);
data.furniture_legs_cleaning_switch = !!(generalSwitchBits6 & 0b00100000);
data.edge_deep_vacuum_switch = !!(generalSwitchBits6 & 0b10000000);
}
if (payload.length >= 71) {
const generalSwitchBits7 = payload[70];
// possibly to know which room is which? What does the firmware do with it?
data.furniture_identify_switch = !!(generalSwitchBits7 & 0b00000001);
data.frequent_auto_empty = !!(generalSwitchBits7 & 0b00000010);
data.fall_detection_switch = !!(generalSwitchBits7 & 0b00000100);
data.obstacle_image_upload_switch = !!(generalSwitchBits7 & 0b00001000);
data.threshold_recognition_switch = !!(generalSwitchBits7 & 0b01000000);
data.curtain_recognition_switch = !!(generalSwitchBits7 & 0b10000000);
}
if (payload.length >= 74) {
const generalSwitchBits8 = payload[73];
data.adb_switch = !!(generalSwitchBits8 & 0b00000001);
data.station_v2_switch = !!(generalSwitchBits8 & 0b00000010);
data.static_stain_recognition_switch = !!(generalSwitchBits8 & 0b00000100);
data.stairless_mode_switch = !!(generalSwitchBits8 & 0b00001000);
}
return data;
}
/**
* @private
* @param {Buffer} payload
* @returns {object}
*/
static _parse_active_zones_payload(payload) {
const zoneCount = payload.readUInt8(3);
const zones = [];
let offset = 4;
for (let i = 0; i < zoneCount; i++) {
if (offset + 10 > payload.length) {
Logger.warn("Malformed ACTIVE_ZONES payload. Not enough data for all declared zones.");
break;
}
zones.push({
index: payload.readUInt8(offset),
passes: payload.readUInt8(offset + 1),
pA: {
x: payload.readUInt16LE(offset + 2),
y: payload.readUInt16LE(offset + 4),
},
pC: {
x: payload.readUInt16LE(offset + 6),
y: payload.readUInt16LE(offset + 8),
}
});
offset += 10;
}
return { zones: zones };
}
/**
*
* @private
* @param {Buffer} payload
* @returns {object}
*/
static _parse_dnd_payload(payload) {
return {
enabled: payload[3] !== 0,
start: {
hour: payload[4],
minute: payload[5],
},
end: {
hour: payload[6],
minute: payload[7]
}
};
}
/**
* @private
* @param {Buffer} payload
* @returns {object}
*/
static _parse_cleaning_settings_1_payload(payload) {
const data = {};
data.route_type = payload[3];
data.cut_hair_level = payload[4];
data.collect_suction_level = payload[7];
data.exhibition_switch = !!payload[8];
data.ai_grade_avoidance_mode = payload[9];
data.cut_hair_super_switch = !!payload[10];
data.turbidity_re_mop_switch = payload[11];
return data;
}
/**
* @private
* @param {Buffer} payload
* @returns {object}
*/
static _parse_carpet_behavior_settings_payload(payload) {
const data = {};
data.carpet_behavior = payload[3];
data.parameter_bitfield = payload[4];
data.clean_carpet_first = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CLEAN_CARPET_FIRST);
data.deep_carpet_cleaning = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.DEEP_CARPET_CLEANING);
data.carpet_suction_boost = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.CARPET_SUCTION_BOOST);
data.enhanced_carpet_avoidance = !!(data.parameter_bitfield & dtos.MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT.ENHANCED_CARPET_AVOIDANCE);
return data;
}
}
module.exports = BEightParser;

View File

@ -0,0 +1,53 @@
const SETTING = Object.freeze({
SET_WORK_STATUS: 0x01, // This is the state, the high-level state machine is in. To get the robot to do things, you set it
DO_MANUAL_CONTROL_CMD: 0x02,
START_SEGMENT_CLEANUP: 0x03,
START_ZONE_CLEANUP: 0x05,
SET_VIRTUAL_WALLS: 0x20,
SET_RESTRICTED_ZONES: 0x21,
MAP_MANAGEMENT: 0x24,
JOIN_SEGMENTS: 0x26,
SPLIT_SEGMENT: 0x27,
SET_VALID_MAP_IDS: 0x2D, // Used by the cloud to sync cloud state with device state. The cloud being higher prio
SET_FAN_SPEED: 0x50,
SET_WATER_GRADE: 0x51,
SET_CARPET_MODE: 0x52, // J12. Not sure about newer robots
SET_DOCK_INTERVALS: 0x56,
SET_OPERATION_MODE: 0x58,
TRIGGER_STATION_ACTION: 0x5A,
SET_CARPET_BEHAVIOR_SETTINGS: 0x5E,
SET_STAIRLESS_MODE: 0x63,
SET_DND: 0x92,
SET_VOLUME: 0x93,
SET_VARIOUS_TOGGLES: 0x9C,
SET_HOT_WASH: 0xC5,
SET_AUTO_EMPTY_DURATION: 0xC7,
SET_CLEANING_SETTINGS_1: 0xC9, // FIXME: naming
});
const ACTION = Object.freeze({
GET_STATUS: 0x01,
LIST_MAPS: 0x20,
POLL_MAP: 0x22,
GET_DOCK_POSITION: 0x24,
GET_ACTIVE_ZONES: 0x27,
LOCATE: 0x57,
// 0x59 seems to maybe provide a bunch of feature bits? Reporting capabilities of the robot
GET_DND: 0x90,
GET_CLEANING_SETTINGS_1: 0xAA, // FIXME: naming
GET_CARPET_BEHAVIOR_SETTINGS: 0xAB
});
const EVENT = Object.freeze({
STATUS: 0x01,
ACTIVE_ZONES: 0x22,
ERROR: 0xA3,
CLEANING_SETTINGS_1: 0xAA // FIXME: naming
});
module.exports = {
SETTING: SETTING,
ACTION: ACTION,
EVENT: EVENT
};

View File

@ -0,0 +1,712 @@
const crypto = require("crypto");
const express = require("express");
const https = require("https");
const Logger = require("../Logger");
const MSmartPacket = require("./MSmartPacket");
const MSmartTimeoutError = require("./MSmartTimeoutError");
const Semaphore = require("semaphore");
const tls = require("tls");
const {createBroker} = require("aedes");
// For this to work in all situations, we need to patch out the forced timesync within the firmware,
// as otherwise it will never start up if access to the hardcoded NTP servers in the FW is blocked
// J15PU FW 490 also added an additional check that pings either google or baidu and refuses to start up otherwise
// That too needs to be patched out
class MSmartDummycloud {
/**
* @param {object} options
* @param {import("../utils/DummyCloudCertManager")} options.dummyCloudCertManager
* @param {string} options.bindIP
* @param {number=} options.timeout timeout in milliseconds to wait for a response
* @param {(packet: import("./MSmartPacket")) => boolean} options.onIncomingCloudMessage
* @param {string} options.dummyClientCert
* @param {string} options.dummyClientKey
* @param {() => void} options.onConnected
* @param {(req: any, res: any) => boolean} options.onHttpRequest
* @param {(type: string, data: any) => void} options.onUpload - TODO naming
* @param {(type: string, value: any) => void} options.onEvent - TODO naming
*/
constructor(options) {
this.dummyCloudCertManager = options.dummyCloudCertManager;
this.bindIP = options.bindIP;
this.timeout = options.timeout ?? 5000;
this.onConnected = options.onConnected;
this.onIncomingCloudMessage = options.onIncomingCloudMessage;
this.onHttpRequest = options.onHttpRequest;
this.onUpload = options.onUpload;
this.onEvent = options.onEvent;
this.dummyClientCert = options.dummyClientCert;
this.dummyClientKey = options.dummyClientKey;
this.sendCommandMutex = Semaphore(1);
this.mqttBroker = createBroker();
this.mqttServer = tls.createServer({
SNICallback: (hostname, callback) => {
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
callback(null, tls.createSecureContext({ key: key, cert: cert }));
}
});
this.httpServer = https.createServer({
SNICallback: (hostname, callback) => {
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
callback(null, tls.createSecureContext({ key: key, cert: cert }));
}
});
this.commandTopic = "device/unknown/down";
this.aiCommandTopic = "ai/unknown/down";
this.mapCommandTopic = "map/unknown/down";
/**
* @type {Object.<string, {
* timeout_id?: NodeJS.Timeout,
* onTimeoutCallback: () => void,
* resolve: (result: any) => void,
* reject: (err: any) => void,
* command: string
* }>}
*/
this.pendingRequests = {};
this.setupMQTT();
this.setupHTTP();
}
setupMQTT() {
this.mqttServer = tls.createServer({
SNICallback: (hostname, callback) => {
const { key, cert } = this.dummyCloudCertManager.getCertificate(hostname);
callback(null, tls.createSecureContext({ key: key, cert: cert }));
}
}, this.mqttBroker.handle);
this.mqttServer.listen(MSmartDummycloud.MQTT_PORT, this.bindIP, () => {
Logger.info(`MSmartDummycloud MQTT listening on ${this.bindIP}:${MSmartDummycloud.MQTT_PORT}`);
});
this.mqttServer.on("error", (err) => {
Logger.error("MSmartDummycloud MQTT Server Error:", err);
});
this.mqttBroker.on("client", (client) => {
Logger.info(`MSmartDummycloud MQTT client connected: ${client.id}`);
this.onConnected();
});
this.mqttBroker.on("subscribe", (subscriptions) => {
Logger.debug("Subscriptions", subscriptions);
subscriptions.forEach(subscription => {
if (subscription.topic.endsWith("/down")) {
if (subscription.topic.startsWith("device/")) {
this.commandTopic = subscription.topic;
Logger.debug(`MSmartDummycloud device command topic: ${this.commandTopic}`);
} else if (subscription.topic.startsWith("ai/")) {
this.aiCommandTopic = subscription.topic;
Logger.debug(`MSmartDummycloud AI command topic: ${this.aiCommandTopic}`);
} else if (subscription.topic.startsWith("map/")) { // J12 (and older?)
this.mapCommandTopic = subscription.topic;
Logger.debug(`MSmartDummycloud map command topic: ${this.mapCommandTopic}`);
}
}
});
});
this.mqttBroker.on("clientDisconnect", (client) => {
Logger.info(`MSmartDummycloud MQTT client disconnected: ${client.id}`);
});
this.mqttBroker.on("publish", async (packet, client) => {
if (!client) {
return; // messages without client are outgoing
}
Logger.trace(`MSmartDummycloud MQTT Message on '${packet.topic}':`, packet.payload.toString());
try {
const message = JSON.parse(packet.payload.toString());
this.handleIncomingCloudMessage({topic: packet.topic, payload: message});
} catch (e) {
Logger.warn("MSmartDummycloud failed to parse incoming message", e);
}
});
}
setupHTTP() {
const app = express();
app.use(express.json());
app.post("/acl/device/register", (req, res) => {
const incomingHostname = req.hostname;
Logger.info(`Handling provisioning request for: ${incomingHostname}.`, req.body);
const responsePayload = {
"errorCode": "0",
"msg": "success",
"data": {
"deviceId": req.body.uuid,
"mqttInfo": {
"clientId": req.body.uuid,
"serverAddress": incomingHostname,
"authType": 1,
"certificatePem": this.dummyClientCert,
"privateKey": this.dummyClientKey
},
"extra": {
"mapHost": `http://${incomingHostname}`,
"otaHost": `http://${incomingHostname}`,
"logHost": `http://${incomingHostname}`,
"voiceHost": `http://${incomingHostname}`,
"videoHost": `https://${incomingHostname}`
}
}
};
Logger.info("Constructed Response Payload:", responsePayload);
res.status(200).json(responsePayload);
});
app.get("/m7-server/actuator/health/ping", (req, res) => {
Logger.trace("Handling /m7-server/actuator/health/ping");
res.status(200).json({"status": "UP"});
});
app.get("/", (req, res, next) => {
if (req.hostname.endsWith("ipify.org") || req.hostname.endsWith("ipify.cn")) {
Logger.info(`Handling IP lookup request for ${req.hostname}.`);
res.status(200).send(req.ip);
} else {
next();
}
});
app.post("/v1/dev2pro/m7/map/part/get", (req, res) => {
Logger.debug(`Handling part get for: ${req.body.mapPart}`);
Logger.debug(req.body);
res.status(200).json({ "data": {} });
});
app.post("/v1/dev2pro/m7/map/list/:part", (req, res) => {
if (req.body) {
Logger.trace(`${req.url}: `, req.body);
this.onUpload(req.params.part, req.body.data); //TODO: perhaps validate types
}
res.status(200).send();
});
app.post("/v1/dev2pro/m7/map/list/mop/:part", (req, res) => {
if (req.body) {
Logger.trace(`${req.url}: `, req.body);
this.onUpload(`mop_${req.params.part}`, req.body.data); //TODO: perhaps validate types
}
res.status(200).send();
});
app.post("/v1/dev2pro/m7/map/part/upload", (req, res) => {
if (req.body) {
Logger.trace(`${req.url}: `, req.body);
this.onUpload(req.body.mapPart, req.body.data);
}
res.status(200).send();
});
app.post("/v1/dev2pro/cruise/list/points", (req, res) => {
if (req.body) {
Logger.trace(`${req.url}: `, req.body);
this.onUpload("points", req.body.data);
}
res.status(200).send();
});
app.post("/v1/dev2pro/m7/work/status/upload", (req, res) => {
Logger.debug("Received a historical record for a finished or aborted cleanup.");
res.status(200).json({ msg: "OK", code: "0" });
});
/*
This is used to ask the cloud for a URL to a voice pack by id
I think it also regularly checks for updates to voicepacks? Not sure.
Reference reply for request
{
"sn8": "750Y000R",
"id": "561",
"md5": "958386132015a99f300ee9a372273b4a"
}
{
"code": "0",
"data": "https://<cdn_url>/m7-voice-full/750Y000R-561-18-<somestring>.zip",
"md5": "052e67dd8843cdfc8f89fc130ea21db5",
"msg": "OK",
"nonce": "<nonce>",
"voiceId": "1198"
}
*/
app.post("/v1/dev2pro/m7/voice/check", (req, res) => {
Logger.debug(`Handling request for Voice with ID '${req.body.id}'`);
res.status(200).json({
"code": "0",
"data": "",
"md5": "",
"msg": "OK",
"nonce": req.headers["nonce"] ?? "",
"voiceId": ""
});
});
/*
Always respond with "no update available".
For reference, example reply for an available update:
{
"code":"0",
"msg":"OK",
"nonce":"<nonce>",
"data":{
"id":91,
"sn8":"750Y000R",
"moduleBranchCode":63,
"type":0,
"name":"V2.0.0.20240927_rc",
"md5":"9478bb7890c6cef753e484804c117e26",
"url":"https://<cdn_url>/<filename>.zip",
"size":32044516,
"version":2,
"minModuleVersion":240,
"maxModuleVersion":9999,
"releaseMode":0,
"whitelistDeviceIds":[
],
"historicalVersions":[
],
"lastOperator":"lixin224",
"updateTime":"2025-05-23 19:25:48"
}
}
*/
app.post("/package-management/v1/dev2pro/check", (req, res) => {
Logger.debug(`Handling AI Model update check. Currently installed version: '${req.body.packageVersion}'`);
res.status(200).json({
"code": "0",
"msg": "OK",
"nonce": req.headers["nonce"] ?? "",
"data": null
});
});
app.post("/v1/ota/version/check", (req, res) => {
const requestedModule = req.body.isModule || "0";
Logger.debug(`Handling OTA check for module type: ${requestedModule}`);
const responsePayload = {
"errorCode": "0",
"msg": "success",
"reason": "success",
"data": {
"isModule": requestedModule,
"hasNew": "0",
"md5": "",
"productName": "",
"sn8": "",
"url": "",
"version": "",
"forceUpdate": "",
"fwSign": "",
"sh256": "",
"rsaSign": ""
}
};
res.status(200).json(responsePayload);
});
app.post("/v1/ota/status/update", (req, res) => {
Logger.debug("Handling OTA status update request.");
if (req.body) {
Logger.debug("OTA Status Update Body:", req.body);
}
res.status(200).json({ msg: "OK", code: "0" });
});
app.post("/logService/v1/dev/event-tracking", (req, res) => {
if (req.body) {
Logger.trace(`${req.url}: `, req.body);
this.onEvent(req.body.type, req.body.value);
}
res.status(200).send();
});
app.post("/v3/dev2pro/login", (req, res) => {
Logger.debug("Received login request");
if (req.body) {
Logger.debug("Body:", req.body);
}
res.status(200).json({ data: "i-am-not-a-token" });
});
app.post("/v3/dev2pro/ability", (req, res) => {
Logger.debug("Received ability request");
if (req.body) {
Logger.debug("Body:", req.body);
}
res.status(200).json({
data: {
videoImageEnc: false
// There could be more in this. So far, I just found the single key and didn't check what the cloud actually sends
// TBD: is false the correct thing to set here?
}
});
});
/*
This seems to be used by the firmware to pull a temporary AES encryption key on boot to.. maybe encrypt pictures with?
The response itself, even though sent via HTTPS, is a json containing base64, which is the requested key + IV AES encrypted with a static one
Persistent might mean image uploads perhaps? Compared with its RTC sibling
*/
app.post("/v3/dev2pro/enc/persistent/key", (req, res) => {
Logger.debug("Handling persistent key request");
const transportKey = "Midea@api-device";
const algorithm = "aes-128-cbc";
// 16-byte key + 16-byte IV
const plaintextPayload = Buffer.alloc(32, 0);
const transportIv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, transportKey, transportIv);
const encryptedPayload = Buffer.concat([cipher.update(plaintextPayload), cipher.final()]);
res.status(200).json({
code: "0",
msg: "success",
requestId: transportIv.toString("hex"),
data: encryptedPayload.toString("base64"),
});
});
// FIXME: this is a duplicate of the persistent route.
// I am just guessing that this might be the correct response as well
// TODO: check internal logs of the firmware and see if anything complains
app.post("/v3/dev2pro/enc/rtc/key", (req, res) => {
Logger.debug("Handling rtc key request");
Logger.debug("Handling persistent key request");
const transportKey = "Midea@api-device";
const algorithm = "aes-128-cbc";
// 16-byte key + 16-byte IV
const plaintextPayload = Buffer.alloc(32, 0);
const transportIv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, transportKey, transportIv);
const encryptedPayload = Buffer.concat([cipher.update(plaintextPayload), cipher.final()]);
res.status(200).json({
code: "0",
msg: "success",
requestId: transportIv.toString("hex"),
data: encryptedPayload.toString("base64"),
});
});
// This route receives events that might be protobufs(?) as multipart/form-data. They do seem debug-only? Not sure.
// Let's see if we can get away without _actually_ handling them
app.post("/v3/dev2pro/robot/event", (req, res) => {
res.status(200).send();
});
app.post("/v1/biz/file/device/uploadFileUrl", (req, res) => {
Logger.trace("Received request for a new presigned file upload URL");
res.status(200).json({
code: "0",
msg: "OK",
data: {
url: `https://${req.hostname}/_valetudo/fileUpload?ts=${Date.now()}`
}
});
});
app.put("/_valetudo/fileUpload", (req, res) => {
Logger.trace("Received file upload");
res.status(200).send();
});
app.all("*", (req, res) => {
if (this.onHttpRequest) {
const handled = this.onHttpRequest(req, res);
if (handled) {
return;
}
}
Logger.info("Unhandled MSmartDummycloud HTTP Request", {
protocol: req.secure ? "HTTPS" : "HTTP",
host: req.headers.host,
method: req.method,
path: req.path,
headers: req.headers,
body: req.body ?? null
});
res.status(200).json({ msg: "OK", code: "0" });
});
this.httpServer.on("request", app);
this.httpServer.listen(MSmartDummycloud.HTTP_PORT, this.bindIP, () => {
Logger.info(`MSmartDummycloud HTTPS listening on ${this.bindIP}:${MSmartDummycloud.HTTP_PORT}`);
});
this.httpServer.on("error", (err) => {
Logger.error("MSmartDummycloud HTTPS Server Error:", err);
});
}
handleIncomingCloudMessage(msg) {
const { topic, payload } = msg;
if (typeof payload.data === "string") {
try {
const responseBuffer = Buffer.from(payload.data, "hex");
const responsePacket = MSmartPacket.FROM_BYTES(responseBuffer);
Logger.trace("Parsed incoming message:", {
topic: topic,
nonce: payload.nonce,
deviceType: responsePacket.deviceType,
messageType: responsePacket.messageType,
payload: responsePacket.payload,
payloadLength: responsePacket.payload.length
});
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.resolve(responsePacket);
delete this.pendingRequests[payload.nonce];
Logger.debug(`MSmartDummycloud received response for nonce ${payload.nonce}`);
return;
}
if (!this.onIncomingCloudMessage(responsePacket)) {
Logger.info("Unhandled message received:", responsePacket);
}
} catch (parseError) {
Logger.warn("Failed to parse incoming message:", parseError);
if (payload.nonce && this.pendingRequests[payload.nonce]) {
const pendingRequest = this.pendingRequests[payload.nonce];
clearTimeout(pendingRequest.timeout_id);
pendingRequest.reject(new Error(`Failed to parse MSmart response: ${parseError.message}`));
delete this.pendingRequests[payload.nonce];
}
}
} else if (payload.protocol === "map") { // Observed on the J12
Logger.trace("Received map-type message:", {
topic: topic,
nonce: payload.nonce,
data: payload.data
});
if (typeof payload.data.fullMap === "string") {
this.onUpload("map", payload.data.fullMap);
} else {
Logger.warn("Unhandled map-type message");
}
} else if (payload.protocol === "track") { // Observed on the J12
Logger.trace("Received track-type message:", {
topic: topic,
nonce: payload.nonce,
data: payload.data
});
if (typeof payload.data.fullTrack === "string") {
this.onUpload("track", payload.data.fullTrack);
} else {
Logger.warn("Unhandled track-type message");
}
} else {
Logger.warn("Unhandled MQTT message");
}
}
/**
* @param {string|object} command
* @param {object} [options]
* @param {number} [options.timeout] - milliseconds
* @param {"device"|"ai"|"map"} [options.target] - defaults to "device"
* @param {boolean} [options.fireAndForget]
* @returns {Promise<import("./MSmartPacket")>}
*/
sendCommand(command, options) {
return new Promise((resolve, reject) => {
this.sendCommandMutex.take(() => {
this.actualSendCommand(command, options).then(response => {
this.sendCommandMutex.leave();
resolve(response);
}).catch(err => {
this.sendCommandMutex.leave();
reject(err);
});
});
});
}
/**
* @private
*
* @param {string|object} command
* @param {object} [options]
* @param {number} [options.timeout] - milliseconds
* @param {"device"|"ai"|"map"} [options.target] - defaults to "device"
* @param {boolean} [options.fireAndForget]
* @returns {Promise<import("./MSmartPacket")>}
*/
actualSendCommand(command, options = {}) {
return new Promise((resolve, reject) => {
const nonce = crypto.randomUUID();
const payload = JSON.stringify({
data: command,
nonce: nonce,
version: 1,
timestamp: Math.round(Date.now() / 1000),
productId: "valetudo"
});
const target = options?.target ?? "device";
let targetTopic;
switch (target) {
case "ai":
targetTopic = this.aiCommandTopic;
break;
case "device":
targetTopic = this.commandTopic;
break;
case "map":
targetTopic = this.mapCommandTopic;
break;
}
const fireAndForget = !!options?.fireAndForget;
if (!fireAndForget) {
this.pendingRequests[nonce] = {
resolve: resolve,
reject: reject,
command: command,
onTimeoutCallback: () => {
Logger.debug(`Request with nonce ${nonce} timed out`);
delete this.pendingRequests[nonce];
reject(new MSmartTimeoutError({nonce: nonce, command: command}));
}
};
this.pendingRequests[nonce].timeout_id = setTimeout(
() => {
this.pendingRequests[nonce].onTimeoutCallback();
},
options?.timeout ?? this.timeout
);
}
Logger.trace(`Sending command to ${targetTopic}`, payload);
this.mqttBroker.publish(
{
cmd: "publish",
topic: targetTopic,
payload: Buffer.from(payload),
qos: 0,
retain: false,
dup: false,
},
(error) => {
if (error) {
Logger.error(`Error publishing message: ${error}`);
if (this.pendingRequests[nonce]) {
clearTimeout(this.pendingRequests[nonce].timeout_id);
delete this.pendingRequests[nonce];
}
reject(error);
} else if (options.fireAndForget) {
// This is a bit janky, but it allows us to have the return type always be an MSmartPacket
resolve(new MSmartPacket({messageType: 0, payload: Buffer.alloc(0)}));
}
}
);
});
}
async shutdown() {
Logger.debug("MSmartDummycloud shutdown in progress...");
await new Promise((resolve) => {
this.httpServer.close(() => {
Logger.info("MSmartDummycloud HTTPS server shut down");
resolve();
});
});
await new Promise((resolve) => {
this.mqttBroker.close(() => {
this.mqttServer.close(() => {
Logger.info("MSmartDummycloud MQTT server shut down");
resolve();
});
});
});
Logger.debug("MSmartDummycloud shutdown done");
}
}
MSmartDummycloud.MQTT_PORT = 8883;
MSmartDummycloud.HTTP_PORT = 443;
module.exports = MSmartDummycloud;

View File

@ -0,0 +1,127 @@
class MSmartPacket {
/**
* @param {object} options
* @param {number} [options.deviceType]
* @param {number} [options.messageId]
* @param {number} [options.protocolVersion]
* @param {number} [options.deviceProtocolVersion]
* @param {number} options.messageType
* @param {Buffer} options.payload
*/
constructor(options) {
this.deviceType = options.deviceType ?? MSmartPacket.DEVICE_TYPE.VACUUM;
this.messageId = options.messageId ?? 0;
this.protocolVersion = options.protocolVersion ?? 0;
this.deviceProtocolVersion = options.deviceProtocolVersion ?? 0;
this.messageType = options.messageType;
this.payload = options.payload;
}
/**
* Serializes the packet into a Buffer for transmission.
* @returns {Buffer}
*/
toBytes() {
const length = 10 + this.payload.length;
if (length > 255) {
throw new Error(`Invalid MSmartPacket! Length ${length} > 255)`);
}
const header = Buffer.alloc(10);
header[0] = 0xAA;
header[1] = length;
header[2] = this.deviceType;
header[3] = 0x00;
header[4] = 0x00;
header[5] = 0x00;
header[6] = this.messageId;
header[7] = this.protocolVersion;
header[8] = this.deviceProtocolVersion;
header[9] = this.messageType;
const dataToChecksum = Buffer.concat([header.subarray(1), this.payload]);
const checksum = MSmartPacket.calculateChecksum(dataToChecksum);
return Buffer.concat([header, this.payload, Buffer.from([checksum])]);
}
/**
* @returns {string}
*/
toHexString() {
return this.toBytes().toString("hex");
}
/**
* @param {Buffer} bytes
* @returns {MSmartPacket}
*/
static FROM_BYTES(bytes) {
if (bytes.length < 11) {
throw new Error(`Packet too short. Expected at least 11 bytes, got ${bytes.length}.`);
}
if (bytes[0] !== 0xAA) {
throw new Error(`Invalid Magic Byte. Expected 0xAA, got 0x${bytes[0].toString(16)}.`);
}
if (bytes[1] !== bytes.length - 1) {
throw new Error(`Length mismatch. Length byte is ${bytes[1]}, but packet length is ${bytes.length}.`);
}
const dataToChecksum = bytes.subarray(1, bytes.length - 1);
const expectedChecksum = MSmartPacket.calculateChecksum(dataToChecksum);
const actualChecksum = bytes[bytes.length - 1];
if (actualChecksum !== expectedChecksum) {
throw new Error(`Checksum mismatch. Calculated 0x${expectedChecksum.toString(16)}, but got 0x${actualChecksum.toString(16)}.`);
}
return new MSmartPacket({
deviceType: bytes[2],
messageId: bytes[6],
protocolVersion: bytes[7],
deviceProtocolVersion: bytes[8],
messageType: bytes[9],
payload: bytes.subarray(10, bytes.length - 1)
});
}
/**
* @param {Buffer} data
* @returns {number}
*/
static calculateChecksum(data) {
const sum = data.reduce((acc, val) => acc + val, 0);
return (~sum + 1) & 0xFF;
}
/**
*
* @param {number} commandId
* @param {Buffer} [actualPayload]
* @return {Buffer}
*/
static buildPayload(commandId, actualPayload) {
const header = Buffer.from([0xaa, 0x01, commandId]);
if (actualPayload === undefined) {
return header;
}
return Buffer.concat([
header,
actualPayload
]);
}
}
MSmartPacket.DEVICE_TYPE = Object.freeze({
VACUUM: 0xb8
});
MSmartPacket.MESSAGE_TYPE = Object.freeze({
SETTING: 0x02,
ACTION: 0x03,
EVENT: 0x04,
DOCK: 0x06
});
module.exports = MSmartPacket;

View File

@ -0,0 +1,93 @@
class MSmartProvisioningPacket {
/**
* @param {object} options
* @param {number} options.commandId
* @param {Buffer} options.payload
*/
constructor(options) {
this.commandId = options.commandId;
this.payload = options.payload;
}
/**
* @returns {Buffer}
*/
toBytes() {
const coreCommand = Buffer.alloc(4 + this.payload.length);
coreCommand.writeUInt16BE(this.commandId, 0);
coreCommand.writeUInt16BE(this.payload.length, 2);
this.payload.copy(coreCommand, 4);
const checksum = MSmartProvisioningPacket.calculateChecksum(coreCommand);
const finalPacket = Buffer.alloc(7 + this.payload.length);
finalPacket[0] = 0xEE;
finalPacket[1] = 0x01;
coreCommand.copy(finalPacket, 2);
finalPacket[finalPacket.length - 1] = checksum;
return finalPacket;
}
/**
* @param {Buffer} bytes
* @returns {MSmartProvisioningPacket}
*/
static FROM_BYTES(bytes) {
if (bytes.length < 7) {
throw new Error(`Packet too short. Expected at least 7 bytes, got ${bytes.length}.`);
}
if (bytes[0] !== 0xEE || bytes[1] !== 0x01) {
throw new Error(`Invalid Magic Header. Expected 0xEE01, got 0x${bytes.toString("hex", 0, 2)}.`);
}
const coreCommand = bytes.subarray(2, bytes.length - 1);
const expectedChecksum = MSmartProvisioningPacket.calculateChecksum(coreCommand);
const actualChecksum = bytes[bytes.length - 1];
if (actualChecksum !== expectedChecksum) {
throw new Error(`Checksum mismatch. Calculated 0x${expectedChecksum.toString(16)}, but got 0x${actualChecksum.toString(16)}.`);
}
const commandId = coreCommand.readUInt16BE(0);
const payloadLength = coreCommand.readUInt16BE(2);
if (coreCommand.length !== 4 + payloadLength) {
throw new Error(`Payload length mismatch. Header says ${payloadLength}, but actual was ${coreCommand.length - 4}.`);
}
return new MSmartProvisioningPacket({
commandId: commandId,
payload: coreCommand.subarray(4)
});
}
/**
* @param {Buffer} data
* @returns {number}
*/
static calculateChecksum(data) {
const sum = data.reduce((acc, val) => acc + val, 0);
return sum & 0xFF;
}
}
MSmartProvisioningPacket.RESPONSE_ID_OFFSET = 0b1000000000000000; // FIXME: naming
MSmartProvisioningPacket.COMMAND_IDS = Object.freeze({
CMD_ALL_INFO: 208,
CMD_UUID_INFO: 213,
// The robot can also send "commands"
CMD_NOTIFY_PROGRESS: 222,
CMD_NOTIFY_RESULT: 223
});
MSmartProvisioningPacket.RESPONSE_IDS = Object.freeze({
CMD_ALL_INFO: MSmartProvisioningPacket.COMMAND_IDS.CMD_ALL_INFO + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
CMD_UUID_INFO: MSmartProvisioningPacket.COMMAND_IDS.CMD_UUID_INFO + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
CMD_NOTIFY_PROGRESS: MSmartProvisioningPacket.COMMAND_IDS.CMD_NOTIFY_PROGRESS + MSmartProvisioningPacket.RESPONSE_ID_OFFSET,
CMD_NOTIFY_RESULT: MSmartProvisioningPacket.COMMAND_IDS.CMD_NOTIFY_RESULT + MSmartProvisioningPacket.RESPONSE_ID_OFFSET
});
module.exports = MSmartProvisioningPacket;

View File

@ -0,0 +1,16 @@
class MSmartTimeoutError extends Error {
/**
* @param {object} context
* @param {string} context.nonce
* @param {string} context.command
*/
constructor(context) {
super(`Request with nonce ${context.nonce} timed out`);
this.name = "MSmartTimeoutError";
this.nonce = context.nonce;
this.command = context.command;
}
}
module.exports = MSmartTimeoutError;

View File

@ -0,0 +1,41 @@
const MSmartDTO = require("./MSmartDTO");
/**
* @typedef {object} MideaMapPoint
* @property {number} x
* @property {number} y
*/
/**
* @typedef {object} MideaActiveZone
* @property {number} index
* @property {number} passes
* @property {MideaMapPoint} pA - top-left
* @property {MideaMapPoint} pC - bottom-right
*/
/**
* @typedef {object} MideaActiveZonesData
* @property {MideaActiveZone[]} zones
*/
/**
* @class MSmartActiveZonesDTO
* @extends MSmartDTO
*/
class MSmartActiveZonesDTO extends MSmartDTO {
/**
* @param {MideaActiveZonesData} data
*/
constructor(data) {
super();
/** @type {MideaActiveZone[]} */
this.zones = data.zones;
Object.freeze(this);
}
}
module.exports = MSmartActiveZonesDTO;

View File

@ -0,0 +1,34 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartCarpetBehaviorSettingsDTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} data.carpet_behavior 0 = avoid, 1 = ignore, 2 = adapt, 3 = cross
* @param {number} data.parameter_bitfield The raw bitfield byte for carpet sub-settings
* @param {boolean} data.clean_carpet_first
* @param {boolean} data.deep_carpet_cleaning
* @param {boolean} data.carpet_suction_boost
* @param {boolean} data.enhanced_carpet_avoidance
*/
constructor(data) {
super();
this.carpet_behavior = data.carpet_behavior;
this.parameter_bitfield = data.parameter_bitfield;
this.clean_carpet_first = data.clean_carpet_first;
this.deep_carpet_cleaning = data.deep_carpet_cleaning;
this.carpet_suction_boost = data.carpet_suction_boost;
this.enhanced_carpet_avoidance = data.enhanced_carpet_avoidance;
Object.freeze(this);
}
}
MSmartCarpetBehaviorSettingsDTO.PARAMETER_BIT = Object.freeze({
CLEAN_CARPET_FIRST: 0b0001,
DEEP_CARPET_CLEANING: 0b0010,
CARPET_SUCTION_BOOST: 0b0100,
ENHANCED_CARPET_AVOIDANCE: 0b1000
});
module.exports = MSmartCarpetBehaviorSettingsDTO;

View File

@ -0,0 +1,31 @@
const MSmartDTO = require("./MSmartDTO");
// FIXME: naming
class MSmartCleaningSettings1DTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} data.route_type
* @param {number} data.cut_hair_level 0-2
* @param {number} data.collect_suction_level 0-1
* @param {boolean} data.exhibition_switch
* @param {number} data.ai_grade_avoidance_mode
* @param {boolean} data.cut_hair_super_switch
* @param {number} data.turbidity_re_mop_switch
*/
constructor(data) {
super();
this.route_type = data.route_type;
this.cut_hair_level = data.cut_hair_level;
this.collect_suction_level = data.collect_suction_level;
this.exhibition_switch = data.exhibition_switch;
this.ai_grade_avoidance_mode = data.ai_grade_avoidance_mode;
this.cut_hair_super_switch = data.cut_hair_super_switch;
this.turbidity_re_mop_switch = data.turbidity_re_mop_switch;
Object.freeze(this);
}
}
module.exports = MSmartCleaningSettings1DTO;

View File

@ -0,0 +1,27 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartDndConfigurationDTO extends MSmartDTO {
/**
* @param {object} data
* @param {boolean} data.enabled
*
* @param {object} data.start
* @param {number} data.start.hour
* @param {number} data.start.minute
*
* @param {object} data.end
* @param {number} data.end.hour
* @param {number} data.end.minute
*/
constructor(data) {
super();
this.enabled = data.enabled;
this.start = data.start;
this.end = data.end;
Object.freeze(this);
}
}
module.exports = MSmartDndConfigurationDTO;

View File

@ -0,0 +1,8 @@
/**
* @abstract
*/
class MSmartDTO {
}
module.exports = MSmartDTO;

View File

@ -0,0 +1,21 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartDockStatusDTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} data.dust_collection_count
* @param {boolean} data.fluid_1_ok
* @param {boolean} data.fluid_2_ok
*/
constructor(data) {
super();
this.dust_collection_count = data.dust_collection_count;
this.fluid_1_ok = data.fluid_1_ok;
this.fluid_2_ok = data.fluid_2_ok;
Object.freeze(this);
}
}
module.exports = MSmartDockStatusDTO;

View File

@ -0,0 +1,21 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartErrorDTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} data.error_type
* @param {number} data.error_desc
* @param {number} data.sta_index - FIXME: figure out what this means
*/
constructor(data) {
super();
this.error_type = data.error_type;
this.error_desc = data.error_desc;
this.sta_index = data.sta_index;
Object.freeze(this);
}
}
module.exports = MSmartErrorDTO;

View File

@ -0,0 +1,19 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartMapListDTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} data.currentMapId
* @param {Array<number>} data.savedMapIds
*/
constructor(data) {
super();
this.currentMapId = data.currentMapId;
this.savedMapIds = data.savedMapIds;
Object.freeze(this);
}
}
module.exports = MSmartMapListDTO;

View File

@ -0,0 +1,209 @@
const MSmartDTO = require("./MSmartDTO");
class MSmartStatusDTO extends MSmartDTO {
/**
* @param {object} data
* @param {number} [data.work_status]
* @param {number} [data.function_type]
* @param {number} [data.control_type]
* @param {number} [data.move_direction]
* @param {number} [data.work_mode]
* @param {number} [data.fan_level]
* @param {number} [data.work_area]
* @param {number} [data.water_level]
* @param {number} [data.voice_level]
* @param {number} [data.battery_percent]
* @param {number} [data.work_time]
* @param {boolean} [data.uv_switch] - TODO: VALIDATE
* @param {boolean} [data.wifi_switch] - TODO: VALIDATE
* @param {boolean} [data.voice_switch] - TODO: VALIDATE
* @param {boolean} [data.command_source] - TODO: VALIDATE
* @param {boolean} [data.device_error] - TODO: VALIDATE
* @param {number} [data.error_type]
* @param {number} [data.error_desc]
* @param {boolean} [data.has_mop]
* @param {boolean} [data.has_vibrate_mop]
* @param {number} [data.carpet_switch]
* @param {number} [data.district_status]
* @param {number} [data.cleaning_type]
* @param {string} [data.vibrate_mode]
* @param {boolean} [data.vibrate_switch]
* @param {boolean} [data.electrolyzed_water]
* @param {number} [data.electrolyzed_water_status]
* @param {boolean} [data.dustDragSwitch]
* @param {boolean} [data.dustDragStatus]
* @param {number} [data.dustTimes]
* @param {number} [data.dustedTimes]
* @param {number} [data.chargeDockType]
* @param {boolean} [data.fluid_1_ok]
* @param {boolean} [data.fluid_2_ok]
* @param {boolean} [data.dustbag_installed]
* @param {boolean} [data.dustbag_full]
* @param {number} [data.mopMode]
* @param {number} [data.station_error_code]
* @param {number} [data.station_work_status]
* @param {number} [data.job_state]
* @param {number} [data.whole_process_state]
* @param {boolean} [data.continuous_clean_mode]
* @param {boolean} [data.clean_sequence_switch]
* @param {boolean} [data.child_lock_enabled]
* @param {boolean} [data.child_lock_follows_dnd]
* @param {boolean} [data.personal_clean_prefer_switch]
* @param {boolean} [data.station_inject_fluid_switch]
* @param {boolean} [data.station_inject_soft_fluid_switch]
* @param {boolean} [data.carpet_evade_switch]
* @param {boolean} [data.station_first_fast_wash_switch]
* @param {boolean} [data.pet_mode_switch]
* @param {number} [data.station_capability_flags]
* @param {boolean} [data.stain_clean_switch]
* @param {boolean} [data.ai_obstacle_switch]
* @param {boolean} [data.cross_bridge_switch]
* @param {boolean} [data.camera_led_switch]
* @param {boolean} [data.map_3d_switch]
* @param {boolean} [data.ai_recognition_switch]
* @param {number} [data.test_mode_type]
* @param {number} [data.hot_water_wash_mode]
* @param {boolean} [data.station_self_fluid_2_switch]
* @param {boolean} [data.slam_version_switch]
* @param {boolean} [data.hot_dry_charge_plate_switch]
* @param {boolean} [data.telnet_switch]
* @param {boolean} [data.mop_auto_dry_switch]
* @param {boolean} [data.ai_grade_avoidance_mode]
* @param {boolean} [data.tail_sweep_clean_switch]
* @param {boolean} [data.pound_sign_switch]
* @param {number} [data.stationCleanFrequency]
* @param {number} [data.beautify_map_grade]
* @param {number} [data.collect_dust_mode]
* @param {number} [data.session_id]
* @param {number} [data.transaction_id]
* @param {boolean} [data.bridge_boost_switch]
* @param {boolean} [data.narrow_zone_recharge_switch]
* @param {boolean} [data.verification_map_switch]
* @param {boolean} [data.wake_up_switch]
* @param {boolean} [data.ai_carpet_avoid_switch]
* @param {boolean} [data.carpet_evade_adaptive_switch]
* @param {boolean} [data.stuck_mark_switch]
* @param {boolean} [data.mop_extend_switch]
* @param {boolean} [data.zigzag_to_end_switch]
* @param {number} [data.remaining_area]
* @param {boolean} [data.ai_avoidance_switch]
* @param {boolean} [data.gap_deep_cleaning_switch]
* @param {boolean} [data.furniture_legs_cleaning_switch]
* @param {boolean} [data.edge_deep_vacuum_switch]
* @param {boolean} [data.furniture_identify_switch]
* @param {boolean} [data.frequent_auto_empty]
* @param {boolean} [data.fall_detection_switch]
* @param {boolean} [data.obstacle_image_upload_switch]
* @param {boolean} [data.threshold_recognition_switch]
* @param {boolean} [data.curtain_recognition_switch]
* @param {boolean} [data.adb_switch]
* @param {boolean} [data.station_v2_switch]
* @param {boolean} [data.static_stain_recognition_switch]
* @param {boolean} [data.stairless_mode_switch]
*/
constructor(data) {
super();
this.work_status = data.work_status;
this.function_type = data.function_type;
this.control_type = data.control_type;
this.move_direction = data.move_direction;
this.work_mode = data.work_mode;
this.fan_level = data.fan_level;
this.work_area = data.work_area;
this.water_level = data.water_level;
this.voice_level = data.voice_level;
this.battery_percent = data.battery_percent;
this.work_time = data.work_time;
this.uv_switch = data.uv_switch;
this.wifi_switch = data.wifi_switch;
this.voice_switch = data.voice_switch;
this.command_source = data.command_source;
this.device_error = data.device_error;
this.error_type = data.error_type;
this.error_desc = data.error_desc;
this.has_mop = data.has_mop;
this.has_vibrate_mop = data.has_vibrate_mop;
this.carpet_switch = data.carpet_switch;
this.district_status = data.district_status;
this.cleaning_type = data.cleaning_type;
this.vibrate_mode = data.vibrate_mode;
this.vibrate_switch = data.vibrate_switch;
this.electrolyzed_water = data.electrolyzed_water;
this.electrolyzed_water_status = data.electrolyzed_water_status;
this.dustDragSwitch = data.dustDragSwitch;
this.dustDragStatus = data.dustDragStatus;
this.dustTimes = data.dustTimes;
this.dustedTimes = data.dustedTimes;
this.chargeDockType = data.chargeDockType;
this.fluid_1_ok = data.fluid_1_ok;
this.fluid_2_ok = data.fluid_2_ok;
this.dustbag_installed = data.dustbag_installed;
this.dustbag_full = data.dustbag_full;
this.mopMode = data.mopMode;
this.station_error_code = data.station_error_code;
this.station_work_status = data.station_work_status;
this.job_state = data.job_state;
this.whole_process_state = data.whole_process_state;
this.continuous_clean_mode = data.continuous_clean_mode;
this.clean_sequence_switch = data.clean_sequence_switch;
this.child_lock_enabled = data.child_lock_enabled;
this.child_lock_follows_dnd = data.child_lock_follows_dnd;
this.personal_clean_prefer_switch = data.personal_clean_prefer_switch;
this.station_inject_fluid_switch = data.station_inject_fluid_switch;
this.station_inject_soft_fluid_switch = data.station_inject_soft_fluid_switch;
this.carpet_evade_switch = data.carpet_evade_switch;
this.station_first_fast_wash_switch = data.station_first_fast_wash_switch;
this.pet_mode_switch = data.pet_mode_switch;
this.station_capability_flags = data.station_capability_flags;
this.stain_clean_switch = data.stain_clean_switch;
this.ai_obstacle_switch = data.ai_obstacle_switch;
this.cross_bridge_switch = data.cross_bridge_switch;
this.camera_led_switch = data.camera_led_switch;
this.map_3d_switch = data.map_3d_switch;
this.ai_recognition_switch = data.ai_recognition_switch;
this.test_mode_type = data.test_mode_type;
this.hot_water_wash_mode = data.hot_water_wash_mode;
this.station_self_fluid_2_switch = data.station_self_fluid_2_switch;
this.slam_version_switch = data.slam_version_switch;
this.hot_dry_charge_plate_switch = data.hot_dry_charge_plate_switch;
this.telnet_switch = data.telnet_switch;
this.mop_auto_dry_switch = data.mop_auto_dry_switch;
this.ai_grade_avoidance_mode = data.ai_grade_avoidance_mode;
this.tail_sweep_clean_switch = data.tail_sweep_clean_switch;
this.pound_sign_switch = data.pound_sign_switch;
this.stationCleanFrequency = data.stationCleanFrequency;
this.beautify_map_grade = data.beautify_map_grade;
this.collect_dust_mode = data.collect_dust_mode;
this.session_id = data.session_id;
this.transaction_id = data.transaction_id;
this.bridge_boost_switch = data.bridge_boost_switch;
this.narrow_zone_recharge_switch = data.narrow_zone_recharge_switch;
this.verification_map_switch = data.verification_map_switch;
this.wake_up_switch = data.wake_up_switch;
this.ai_carpet_avoid_switch = data.ai_carpet_avoid_switch;
this.carpet_evade_adaptive_switch = data.carpet_evade_adaptive_switch;
this.stuck_mark_switch = data.stuck_mark_switch;
this.mop_extend_switch = data.mop_extend_switch;
this.zigzag_to_end_switch = data.zigzag_to_end_switch;
this.remaining_area = data.remaining_area;
this.ai_avoidance_switch = data.ai_avoidance_switch;
this.gap_deep_cleaning_switch = data.gap_deep_cleaning_switch;
this.furniture_legs_cleaning_switch = data.furniture_legs_cleaning_switch;
this.edge_deep_vacuum_switch = data.edge_deep_vacuum_switch;
this.furniture_identify_switch = data.furniture_identify_switch;
this.frequent_auto_empty = data.frequent_auto_empty;
this.fall_detection_switch = data.fall_detection_switch;
this.obstacle_image_upload_switch = data.obstacle_image_upload_switch;
this.threshold_recognition_switch = data.threshold_recognition_switch;
this.curtain_recognition_switch = data.curtain_recognition_switch;
this.adb_switch = data.adb_switch;
this.station_v2_switch = data.station_v2_switch;
this.static_stain_recognition_switch = data.static_stain_recognition_switch;
this.stairless_mode_switch = data.stairless_mode_switch;
Object.freeze(this);
}
}
module.exports = MSmartStatusDTO;

Some files were not shown because too many files have changed in this diff Show More