Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0f4103264 | ||
|
|
975ed6c9e8 | ||
|
|
e2b6a4f04d | ||
|
|
028f73fb05 | ||
|
|
07666cc803 | ||
|
|
ebac83cb0a | ||
|
|
c16245bd51 | ||
|
|
84c8697432 | ||
|
|
6c94eda6ff | ||
|
|
1de0ca3186 | ||
|
|
ec25373195 | ||
|
|
94ae396d85 | ||
|
|
d1dd007ab0 | ||
|
|
8fc5a0ab29 | ||
|
|
64b0ee01fe | ||
|
|
c3609fc199 | ||
|
|
78ee0f513c | ||
|
|
aa8c98e5f3 | ||
|
|
ff7e462c4d | ||
|
|
4b45af6bd3 | ||
|
|
f5fba98bb0 | ||
|
|
614d7b7ef2 | ||
|
|
3341b06445 | ||
|
|
c0136def9d | ||
|
|
fb7870e1cd | ||
|
|
ec61206742 | ||
|
|
6fcfc6a5c1 | ||
|
|
847093aa4d | ||
|
|
ed78538257 | ||
|
|
f150a89f2c | ||
|
|
921dd13b82 | ||
|
|
bb2df9825e | ||
|
|
2fdef9fed7 | ||
|
|
0afc47e9a3 | ||
|
|
c0d1640ae3 | ||
|
|
7c0fc3a094 | ||
|
|
92cd7f6475 | ||
|
|
e6615d266c | ||
|
|
fd83cd3fc5 | ||
|
|
0dcca1df2f | ||
|
|
72444e6fe1 | ||
|
|
ea236abc1e | ||
|
|
92ee9a5f18 | ||
|
|
0ea2a50a4d | ||
|
|
f94f3c1101 | ||
|
|
5715fb02f2 | ||
|
|
fba6d384c1 | ||
|
|
493c57010d | ||
|
|
5d405842d2 | ||
|
|
7d1aea37f7 | ||
|
|
c53dd66c6b | ||
|
|
52b462517b | ||
|
|
15c1f801ad | ||
|
|
785e6b87cb | ||
|
|
52c8c4b290 | ||
|
|
53ff4fa841 | ||
|
|
de62cbf812 | ||
|
|
0c6272f704 | ||
|
|
ae01b69503 | ||
|
|
58fa28f578 | ||
|
|
17ebfae6a8 | ||
|
|
a963fa17ee | ||
|
|
b1eb76b26a | ||
|
|
df46e2e697 | ||
|
|
a71a91a8bd | ||
|
|
87d0760ccc | ||
|
|
0261f57e5d | ||
|
|
757e6827ce | ||
|
|
ed34e36e4e | ||
|
|
6f0fc2a453 | ||
|
|
30efa1afd9 | ||
|
|
ae1ac479d7 | ||
|
|
eb213328de | ||
|
|
142bc87fd2 | ||
|
|
47e129c035 | ||
|
|
6bec34db1c | ||
|
|
d575e2f314 | ||
|
|
649eb4e623 | ||
|
|
f6b08e679f | ||
|
|
da4c8c4040 | ||
|
|
c941ca416d | ||
|
|
89b7882b90 | ||
|
|
1baeaada07 | ||
|
|
ddc9918032 | ||
|
|
cefa92b8ee | ||
|
|
994c3f97a2 | ||
|
|
1189bd9f9c | ||
|
|
ccc307379e | ||
|
|
d67cabc515 | ||
|
|
b456a2b516 | ||
|
|
b12fb03136 | ||
|
|
453ac70f53 | ||
|
|
3746d267a7 | ||
|
|
f29592f6ef | ||
|
|
81801d68b5 | ||
|
|
69e47ab9be | ||
|
|
f12da39c33 | ||
|
|
aaf0fc3f14 | ||
|
|
bd9f42b19b | ||
|
|
e74e16f9c7 | ||
|
|
b8dc5f8191 | ||
|
|
7a616b90b6 | ||
|
|
648414ec2c | ||
|
|
d13a2440e1 | ||
|
|
3b200c05ae | ||
|
|
8ee5157ea6 | ||
|
|
9554cc8d73 | ||
|
|
925141ddb2 | ||
|
|
f261bce8a9 | ||
|
|
d500b3f0cb | ||
|
|
dcf0fd1dcb | ||
|
|
77dfd8558e | ||
|
|
0b839adff4 | ||
|
|
2472f29429 | ||
|
|
0f1ac47ce3 | ||
|
|
bdcba8ba71 | ||
|
|
5af45c3c2b | ||
|
|
d961abf59d | ||
|
|
faf94dd28d | ||
|
|
3f0897eefd | ||
|
|
cf21197e9b | ||
|
|
44edc5911c | ||
|
|
92b2e6f092 | ||
|
|
b7de2f32db | ||
|
|
e42adcb709 | ||
|
|
a2c79136b6 | ||
|
|
02286dc39a | ||
|
|
0529e22d61 | ||
|
|
0fcb809fe6 | ||
|
|
4e5dd10786 | ||
|
|
8589852124 | ||
|
|
0086080d1b | ||
|
|
5b66c35331 | ||
|
|
b56ce990db | ||
|
|
f051d0f274 | ||
|
|
7c86147fbe | ||
|
|
e27f175e59 | ||
|
|
a9873e415e | ||
|
|
ac5e09618a | ||
|
|
0bdf3914e0 | ||
|
|
f795e2e6cc | ||
|
|
1eda6ef56f | ||
|
|
6161901d0b | ||
|
|
d7f8e753c0 | ||
|
|
b798ebb2de | ||
|
|
b8477fbdb6 | ||
|
|
1b9dc20f6d | ||
|
|
2cb8efc3f4 | ||
|
|
a96ab06546 | ||
|
|
8431f1d205 | ||
|
|
6dec70f75e | ||
|
|
df0609ed25 | ||
|
|
1af75e0034 | ||
|
|
5b3f2dcca1 | ||
|
|
7b3dfb06e1 | ||
|
|
fd89128ce6 | ||
|
|
8ff0edce2a | ||
|
|
9d3df057be | ||
|
|
c78e8c2775 | ||
|
|
88c0af5c8b | ||
|
|
ce2a902a05 | ||
|
|
ffc58e5acc | ||
|
|
151bf8b393 | ||
|
|
98d55b0627 | ||
|
|
c19f9c2812 | ||
|
|
24e48a7143 | ||
|
|
a07503b1e8 | ||
|
|
2cc0ab52e6 | ||
|
|
8f22341b65 | ||
|
|
d77480c192 | ||
|
|
ee76222a41 | ||
|
|
9b98e3ebfe | ||
|
|
7d876db2f8 | ||
|
|
afef3ec90f | ||
|
|
74731a6bd4 | ||
|
|
86e58df3a7 | ||
|
|
769c75e819 | ||
|
|
dd6eac067b | ||
|
|
bf9ecb627b | ||
|
|
e627da375a | ||
|
|
89d1fb1dbb | ||
|
|
436fae6293 | ||
|
|
cf20143b75 | ||
|
|
8e336f6f93 | ||
|
|
55294d7885 | ||
|
|
5ae2247f21 | ||
|
|
489e85a8d5 | ||
|
|
5559f88ab2 | ||
|
|
fb600a8ef5 | ||
|
|
5f0515bd68 | ||
|
|
63279c44a0 | ||
|
|
111b53a530 | ||
|
|
2e5a9c20bc | ||
|
|
8e97f344fb | ||
|
|
79deca12ea | ||
|
|
3513cbc698 | ||
|
|
76280bdfc6 | ||
|
|
0928cf01af | ||
|
|
90e5ab935f | ||
|
|
7a6eb1ee0b | ||
|
|
20917a7056 | ||
|
|
c0be1be25d | ||
|
|
2152e20b6e | ||
|
|
dc3b96e87d | ||
|
|
b56ed1ca6e | ||
|
|
67ee71ce58 | ||
|
|
30b5fc8a3d | ||
|
|
f9fa9d0b60 | ||
|
|
d0f754bc10 | ||
|
|
b8afd4de67 | ||
|
|
28d674f4e5 | ||
|
|
02d75635c7 | ||
|
|
a2a1d4061d | ||
|
|
ddcf3e1590 | ||
|
|
ca077b66c3 | ||
|
|
f05423002b | ||
|
|
f294b9ef64 | ||
|
|
9111509cbb |
@ -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",
|
||||
|
||||
68
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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:
|
||||
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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
|
||||
|
||||
4
.github/workflows/nodejs.yml
vendored
@ -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
@ -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
@ -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.
|
||||
47
README.md
@ -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
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
@ -51,18 +51,45 @@ There, you will find a list of [supported robots](https://valetudo.cloud/pages/g
|
||||

|
||||
|
||||
|
||||
## 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!
|
||||
|
||||
@ -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 |
@ -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 |
@ -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 |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 365 B After Width: | Height: | Size: 365 B |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 777 B After Width: | Height: | Size: 777 B |
|
Before Width: | Height: | Size: 385 B After Width: | Height: | Size: 385 B |
|
Before Width: | Height: | Size: 412 B After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 569 B |
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 499 B |
3
assets/icons/custom/misc/robot.svg
Normal 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 |
429
assets/icons/util/robot_with_extensions.svg
Normal 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 |
3
assets/logo/valetudo_icon.svg
Normal 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 |
75
assets/misc/confused_valetudog.svg
Normal 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 |
83
assets/protobuf/midea.proto
Normal 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
@ -0,0 +1 @@
|
||||
/lib/robots/midea/generated/midea_protobufs.js
|
||||
@ -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 => {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 m² 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 we’re 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;
|
||||
|
||||
@ -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;
|
||||
@ -54,6 +54,7 @@ AutoEmptyDockAutoEmptyIntervalControlCapability.TYPE = "AutoEmptyDockAutoEmptyIn
|
||||
*
|
||||
*/
|
||||
AutoEmptyDockAutoEmptyIntervalControlCapability.INTERVAL = Object.freeze({
|
||||
OFF: "off",
|
||||
INFREQUENT: "infrequent",
|
||||
NORMAL: "normal",
|
||||
FREQUENT: "frequent",
|
||||
|
||||
@ -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;
|
||||
@ -57,7 +57,7 @@ CarpetSensorModeControlCapability.MODE = Object.freeze({
|
||||
OFF: "off",
|
||||
AVOID: "avoid",
|
||||
LIFT: "lift",
|
||||
DETACH: "detach",
|
||||
DETACH: "detach"
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -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]
|
||||
*
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
15
backend/lib/core/capabilities/MopTwistControlCapability.js
Normal 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;
|
||||
119
backend/lib/core/capabilities/ObstacleImagesCapability.js
Normal 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;
|
||||
@ -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"),
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
65
backend/lib/entities/core/ValetudoConsumable.js
Normal 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;
|
||||
33
backend/lib/entities/core/ValetudoManualMovementVector.js
Normal 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;
|
||||
@ -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) {
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValetudoConsumable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"remaining": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "number"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"percent",
|
||||
"minutes"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
ValetudoDNDConfiguration: require("./ValetudoDNDConfiguration"),
|
||||
ValetudoDataPoint: require("./ValetudoDataPoint"),
|
||||
ValetudoManualMovementVector: require("./ValetudoManualMovementVector"),
|
||||
ValetudoMapSegment: require("./ValetudoMapSegment"),
|
||||
ValetudoMapSnapshot: require("./ValetudoMapSnapshot"),
|
||||
ValetudoRestrictedZone: require("./ValetudoRestrictedZone"),
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
module.exports = {
|
||||
AttachmentStateAttribute: require("./AttachmentStateAttribute"),
|
||||
BatteryStateAttribute: require("./BatteryStateAttribute"),
|
||||
ConsumableStateAttribute: require("./ConsumableStateAttribute"),
|
||||
DockStatusStateAttribute: require("./DockStatusStateAttribute"),
|
||||
PresetSelectionStateAttribute: require("./PresetSelectionStateAttribute"),
|
||||
StatusStateAttribute: require("./StatusStateAttribute")
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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"]);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
const SimpleToggleCapabilityMqttHandle = require("./SimpleToggleCapabilityMqttHandle");
|
||||
|
||||
|
||||
class CarpetModeControlCapabilityMqttHandle extends SimpleToggleCapabilityMqttHandle {}
|
||||
|
||||
CarpetModeControlCapabilityMqttHandle.OPTIONAL = true;
|
||||
|
||||
module.exports = CarpetModeControlCapabilityMqttHandle;
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
const SimpleToggleCapabilityMqttHandle = require("./SimpleToggleCapabilityMqttHandle");
|
||||
|
||||
|
||||
class PetObstacleAvoidanceControlCapabilityMqttHandle extends SimpleToggleCapabilityMqttHandle {}
|
||||
|
||||
PetObstacleAvoidanceControlCapabilityMqttHandle.OPTIONAL = true;
|
||||
|
||||
module.exports = PetObstacleAvoidanceControlCapabilityMqttHandle;
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@ -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: {
|
||||
"": {
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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³",
|
||||
|
||||
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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()}`
|
||||
};
|
||||
}
|
||||
|
||||
13
backend/lib/mqtt/homeassistant/StateClass.js
Normal 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;
|
||||
@ -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, "_")}`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}));
|
||||
|
||||
66
backend/lib/mqtt/status/DockStatusStateMqttHandle.js
Normal 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;
|
||||
@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
AttachmentStateMqttHandle: require("./AttachmentStateMqttHandle"),
|
||||
BatteryStateMqttHandle: require("./BatteryStateMqttHandle"),
|
||||
StatusStateMqttHandle: require("./StatusStateMqttHandle"),
|
||||
DockStatusStateMqttHandle: require("./DockStatusStateMqttHandle"),
|
||||
StatusStateMqttHandle: require("./StatusStateMqttHandle")
|
||||
};
|
||||
|
||||
441
backend/lib/msmart/BEightParser.js
Normal 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;
|
||||
53
backend/lib/msmart/MSmartConst.js
Normal 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
|
||||
};
|
||||
712
backend/lib/msmart/MSmartDummycloud.js
Normal 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;
|
||||
127
backend/lib/msmart/MSmartPacket.js
Normal 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;
|
||||
93
backend/lib/msmart/MSmartProvisioningPacket.js
Normal 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;
|
||||
16
backend/lib/msmart/MSmartTimeoutError.js
Normal 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;
|
||||
41
backend/lib/msmart/dtos/MSmartActiveZonesDTO.js
Normal 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;
|
||||
34
backend/lib/msmart/dtos/MSmartCarpetBehaviorSettingsDTO.js
Normal 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;
|
||||
31
backend/lib/msmart/dtos/MSmartCleaningSettings1DTO.js
Normal 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;
|
||||
27
backend/lib/msmart/dtos/MSmartDNDConfigurationDTO.js
Normal 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;
|
||||
8
backend/lib/msmart/dtos/MSmartDTO.js
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
class MSmartDTO {
|
||||
|
||||
}
|
||||
|
||||
module.exports = MSmartDTO;
|
||||
21
backend/lib/msmart/dtos/MSmartDockStatusDTO.js
Normal 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;
|
||||
21
backend/lib/msmart/dtos/MSmartErrorDTO.js
Normal 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;
|
||||
19
backend/lib/msmart/dtos/MSmartMapListDTO.js
Normal 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;
|
||||
209
backend/lib/msmart/dtos/MSmartStatusDTO.js
Normal 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;
|
||||