mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-16 20:49:20 +00:00
Compare commits
396 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
598108d294 | ||
![]() |
a0261dbbcc | ||
![]() |
2418122b46 | ||
![]() |
f104e60afa | ||
![]() |
ed45f27f3e | ||
![]() |
40aa5c9caf | ||
![]() |
14b1ea4eb0 | ||
![]() |
5052a339e3 | ||
![]() |
2321890dde | ||
![]() |
4cb5770ee0 | ||
![]() |
3a35561d1d | ||
![]() |
6fbec53f8a | ||
![]() |
c707934018 | ||
![]() |
efd8efa248 | ||
![]() |
979861b764 | ||
![]() |
cdc53a159c | ||
![]() |
a203ed9cc5 | ||
![]() |
5cab5f0c08 | ||
![]() |
25ea80e169 | ||
![]() |
f43b4e9e24 | ||
![]() |
160fbb2589 | ||
![]() |
c85aa664e1 | ||
![]() |
51dcbf5db7 | ||
![]() |
fa114a4a03 | ||
![]() |
d7fd58bdb9 | ||
![]() |
38b0aea8e2 | ||
![]() |
41eade9325 | ||
![]() |
e64cf41aec | ||
![]() |
02872b5e75 | ||
![]() |
e4d49bb459 | ||
![]() |
d38b7d5a82 | ||
![]() |
537c5d3197 | ||
![]() |
575df2fcf6 | ||
![]() |
c08c3c6b37 | ||
![]() |
2acf28609e | ||
![]() |
bb59d0431e | ||
![]() |
1c7b1f1462 | ||
![]() |
f32d17d924 | ||
![]() |
928a4d8dce | ||
![]() |
dd3ba93308 | ||
![]() |
7e1b179cdd | ||
![]() |
a9a2c35f06 | ||
![]() |
58b88a6919 | ||
![]() |
f937876a1b | ||
![]() |
8193f43634 | ||
![]() |
1d3f880f82 | ||
![]() |
ef2fa8d2e2 | ||
![]() |
51997b3e7c | ||
![]() |
98785b00e2 | ||
![]() |
8d3694884d | ||
![]() |
a2821a98ad | ||
![]() |
8d552ae15c | ||
![]() |
6db4c60f47 | ||
![]() |
805c0385a0 | ||
![]() |
cea6e7a9f2 | ||
![]() |
127073c01b | ||
![]() |
30fe36ae05 | ||
![]() |
58bd677832 | ||
![]() |
1a3b369dd7 | ||
![]() |
6e38216abd | ||
![]() |
efcfc1f841 | ||
![]() |
8dea50ce83 | ||
![]() |
7a5a01bdcc | ||
![]() |
bd1450a682 | ||
![]() |
c538c1ce7f | ||
![]() |
b6d59c4f64 | ||
![]() |
a758ccaf5c | ||
![]() |
e8b04cc20a | ||
![]() |
9bcb15dbc0 | ||
![]() |
1e953167b6 | ||
![]() |
979586cdb2 | ||
![]() |
cd31fad56d | ||
![]() |
ff57d88e2a | ||
![]() |
06cb5e171e | ||
![]() |
a8b70a2e13 | ||
![]() |
948019ccee | ||
![]() |
89ed109505 | ||
![]() |
fae246c503 | ||
![]() |
2411b4287d | ||
![]() |
b3308ecbe0 | ||
![]() |
3541cbff5e | ||
![]() |
838ba7ff36 | ||
![]() |
e9802f92c9 | ||
![]() |
016fd24859 | ||
![]() |
d315e81ab2 | ||
![]() |
97c38b8534 | ||
![]() |
011e2b3df5 | ||
![]() |
e3ee9a299f | ||
![]() |
d73c10f874 | ||
![]() |
9e448b46ba | ||
![]() |
9f09c46789 | ||
![]() |
fe6634551a | ||
![]() |
22a7931a7c | ||
![]() |
94f112512f | ||
![]() |
b6509dca1f | ||
![]() |
620234e708 | ||
![]() |
d50e866cec | ||
![]() |
76ad6dca02 | ||
![]() |
cdb1520a63 | ||
![]() |
bbef706a33 | ||
![]() |
835509901f | ||
![]() |
b51f9586c4 | ||
![]() |
fc83cb9559 | ||
![]() |
f5f5f829ac | ||
![]() |
930eed4500 | ||
![]() |
01a8b58054 | ||
![]() |
eba1d01fc2 | ||
![]() |
84755836c9 | ||
![]() |
c9585033cb | ||
![]() |
2d312c276f | ||
![]() |
3b0d0e9928 | ||
![]() |
8307b153e3 | ||
![]() |
dfaffe3ec5 | ||
![]() |
8d7b15cbeb | ||
![]() |
00969a67ac | ||
![]() |
a374d4e817 | ||
![]() |
f5dda39f63 | ||
![]() |
fb5d54d5fe | ||
![]() |
d392b35fdd | ||
![]() |
3ceec006ac | ||
![]() |
62a574c6bd | ||
![]() |
821c10b2bd | ||
![]() |
fa3269a098 | ||
![]() |
a9bdab4b49 | ||
![]() |
0df5b7d87b | ||
![]() |
4861fc70ce | ||
![]() |
47c443bb92 | ||
![]() |
9cb4b49597 | ||
![]() |
865523fd37 | ||
![]() |
1df35a6fe1 | ||
![]() |
e70c9d8a30 | ||
![]() |
7d6b00ea4a | ||
![]() |
e5fc985915 | ||
![]() |
71ccaa2bd0 | ||
![]() |
e127f23a08 | ||
![]() |
495f9f2373 | ||
![]() |
27274286db | ||
![]() |
85ba886029 | ||
![]() |
2f3a868e42 | ||
![]() |
a51b80f456 | ||
![]() |
f27a426879 | ||
![]() |
19ca485c28 | ||
![]() |
7deed55c2d | ||
![]() |
4c5c6f072c | ||
![]() |
f174e08ad6 | ||
![]() |
2658f95347 | ||
![]() |
311c981d1a | ||
![]() |
d6d3bf0583 | ||
![]() |
a1a601a4d3 | ||
![]() |
14776eae76 | ||
![]() |
bef4034ab8 | ||
![]() |
ad988f2a24 | ||
![]() |
6599ae0ee0 | ||
![]() |
4f1ed690cd | ||
![]() |
4ffaee6013 | ||
![]() |
e1ce19547e | ||
![]() |
039040b972 | ||
![]() |
7a1af3d346 | ||
![]() |
1e98774b62 | ||
![]() |
4b4d6c6866 | ||
![]() |
65ff83d359 | ||
![]() |
e509c804ae | ||
![]() |
992827e225 | ||
![]() |
083e97add8 | ||
![]() |
05378d18c0 | ||
![]() |
3dd465acc9 | ||
![]() |
8f6e36f781 | ||
![]() |
85fe56db57 | ||
![]() |
8e07429e47 | ||
![]() |
ced6d702b9 | ||
![]() |
25d7de4dfa | ||
![]() |
82754c0dfe | ||
![]() |
e604b022ee | ||
![]() |
6b29022822 | ||
![]() |
2e671cc5ee | ||
![]() |
f25692b98c | ||
![]() |
c4a011b261 | ||
![]() |
a935bac20b | ||
![]() |
0a3a98cb42 | ||
![]() |
adb39ca93f | ||
![]() |
5fdc340e58 | ||
![]() |
bb64dca6e6 | ||
![]() |
685788bcdf | ||
![]() |
e949aa35f3 | ||
![]() |
fc80bf0df4 | ||
![]() |
bd9740e866 | ||
![]() |
3a260a8fd9 | ||
![]() |
c87e6a5a42 | ||
![]() |
8bc3319523 | ||
![]() |
bdfcf1a2df | ||
![]() |
7f4284f2af | ||
![]() |
fd69120aa6 | ||
![]() |
5df60b17e8 | ||
![]() |
cb835b5ae6 | ||
![]() |
9eab92513a | ||
![]() |
29e8f50ab8 | ||
![]() |
aa0496b236 | ||
![]() |
06e9cec21a | ||
![]() |
0fe27088df | ||
![]() |
54d226116d | ||
![]() |
4b37e30680 | ||
![]() |
7c5f710deb | ||
![]() |
5a3ebaf683 | ||
![]() |
233da0e48f | ||
![]() |
96380d8d28 | ||
![]() |
c84a0edf20 | ||
![]() |
a3cf445c93 | ||
![]() |
3f31979f66 | ||
![]() |
44416edfd2 | ||
![]() |
351c45da75 | ||
![]() |
e27c5dad15 | ||
![]() |
dc510f22ac | ||
![]() |
1b78011f8b | ||
![]() |
a908828bf4 | ||
![]() |
55b7eb62f6 | ||
![]() |
10e8fcf3b9 | ||
![]() |
f1b0c05447 | ||
![]() |
de22bd688e | ||
![]() |
9fe35b4fb5 | ||
![]() |
f13d08d37a | ||
![]() |
a0ecb46584 | ||
![]() |
0c57df0c8e | ||
![]() |
9c902c5c69 | ||
![]() |
af412c3105 | ||
![]() |
ec43448163 | ||
![]() |
9f7e0ecd55 | ||
![]() |
e50515a17c | ||
![]() |
7c345db6fe | ||
![]() |
51c2268c1e | ||
![]() |
51feca05a5 | ||
![]() |
3889504292 | ||
![]() |
7bd6ff374a | ||
![]() |
44fa34203a | ||
![]() |
ff351c7f6d | ||
![]() |
960b00d85a | ||
![]() |
18e3eacd7f | ||
![]() |
f4a1da33c4 | ||
![]() |
49de5be44e | ||
![]() |
383657e8ce | ||
![]() |
3af970ead6 | ||
![]() |
6caec79958 | ||
![]() |
33bbd92d9b | ||
![]() |
9dba78fbcd | ||
![]() |
630d85ec78 | ||
![]() |
f0d46e8671 | ||
![]() |
db0593f0b2 | ||
![]() |
1d83c0c77a | ||
![]() |
5e5fd3a79b | ||
![]() |
c61995aab8 | ||
![]() |
37c393f857 | ||
![]() |
8e043a01c1 | ||
![]() |
c7b6b2ddb3 | ||
![]() |
522f68bf68 | ||
![]() |
7d4866234f | ||
![]() |
7aa5bcfc7c | ||
![]() |
04b59f0896 | ||
![]() |
796f9a203e | ||
![]() |
22c8cda0d7 | ||
![]() |
1cf534ccc5 | ||
![]() |
6d8c821148 | ||
![]() |
264e9665b0 | ||
![]() |
53fa8e48c0 | ||
![]() |
e406aa4144 | ||
![]() |
4953ba5077 | ||
![]() |
0a97ac0578 | ||
![]() |
56af4752f4 | ||
![]() |
81413d08ed | ||
![]() |
2bc2a476d9 | ||
![]() |
4d070a65c6 | ||
![]() |
6185fbaf26 | ||
![]() |
698a126b93 | ||
![]() |
acf921f55d | ||
![]() |
f5a78c88f8 | ||
![]() |
206ece1575 | ||
![]() |
a8028dbe10 | ||
![]() |
c605af6ccc | ||
![]() |
b7b8e6c40e | ||
![]() |
3fcb1de419 | ||
![]() |
12034fe5fc | ||
![]() |
56959d781a | ||
![]() |
9a2f025646 | ||
![]() |
12cc163058 | ||
![]() |
74971d9753 | ||
![]() |
a9157e3a9f | ||
![]() |
b96697b708 | ||
![]() |
81e6896391 | ||
![]() |
2dcaa3608d | ||
![]() |
e21671ec5e | ||
![]() |
7841f14163 | ||
![]() |
cc9f594ab4 | ||
![]() |
ebfaaeaa6b | ||
![]() |
ffa91e150d | ||
![]() |
06fa9f9a9e | ||
![]() |
9f203c42ec | ||
![]() |
5d0d34a4af | ||
![]() |
c2cfc0d3d4 | ||
![]() |
0f4810d41f | ||
![]() |
175848f2a8 | ||
![]() |
472bd66f4d | ||
![]() |
168ea32d2c | ||
![]() |
e82d6b1ea4 | ||
![]() |
6c60ca088c | ||
![]() |
83e8f935fd | ||
![]() |
71867302a4 | ||
![]() |
8bcc402c5f | ||
![]() |
72b7d2a123 | ||
![]() |
20c1183450 | ||
![]() |
0bbfbd2544 | ||
![]() |
350bd9c32f | ||
![]() |
dcca8b0a9a | ||
![]() |
f77b479e45 | ||
![]() |
216565affb | ||
![]() |
6f235c2a11 | ||
![]() |
27a770bd1d | ||
![]() |
ef15b67571 | ||
![]() |
6aad966c52 | ||
![]() |
9811f11859 | ||
![]() |
13148ec7fb | ||
![]() |
b2d7464790 | ||
![]() |
ce84e185ad | ||
![]() |
c3f5ee43b6 | ||
![]() |
e2dc1a4471 | ||
![]() |
e787e59b49 | ||
![]() |
f0ed2eba2b | ||
![]() |
2364e1e652 | ||
![]() |
cc56944d75 | ||
![]() |
69cea9fc96 | ||
![]() |
fcebc9d1ed | ||
![]() |
9350e4f961 | ||
![]() |
387e0ad03e | ||
![]() |
61fec8b290 | ||
![]() |
1228baebf4 | ||
![]() |
a30063e85c | ||
![]() |
524cebac4d | ||
![]() |
c94114a566 | ||
![]() |
b6ec7a9e64 | ||
![]() |
69be7a6d22 | ||
![]() |
58155c35f9 | ||
![]() |
7b2377291f | ||
![]() |
657ee84e39 | ||
![]() |
2e4b545265 | ||
![]() |
2de1d35dd1 | ||
![]() |
2b082b362d | ||
![]() |
dfdd0d6b4b | ||
![]() |
a00e81c03f | ||
![]() |
776e6bb418 | ||
![]() |
b31fca656e | ||
![]() |
fa783a0d2c | ||
![]() |
96c0fbaf10 | ||
![]() |
24f7801ddc | ||
![]() |
8e83e007e9 | ||
![]() |
d0db466e67 | ||
![]() |
3010bd4eb6 | ||
![]() |
069bed8815 | ||
![]() |
d2088ae5f8 | ||
![]() |
0ca5a241bb | ||
![]() |
dff32a8e84 | ||
![]() |
4a20344652 | ||
![]() |
98b969ef06 | ||
![]() |
c8cb8aecf7 | ||
![]() |
73e8875018 | ||
![]() |
02aed9c084 | ||
![]() |
89148f8fff | ||
![]() |
6bde527f5c | ||
![]() |
d62aabc01b | ||
![]() |
82299a3799 | ||
![]() |
c02f30dd7e | ||
![]() |
e91983adb4 | ||
![]() |
ff88359429 | ||
![]() |
5a60d5cbe8 | ||
![]() |
2b41ffe019 | ||
![]() |
1c23e26f93 | ||
![]() |
3d555f951d | ||
![]() |
6d39b4d7cd | ||
![]() |
4fe5d09f01 | ||
![]() |
e52af3bfb4 | ||
![]() |
0467b33cd5 | ||
![]() |
14167f6e13 | ||
![]() |
7a1aba6f81 | ||
![]() |
920f7f2ece | ||
![]() |
06fadbd70f | ||
![]() |
d4f486864f | ||
![]() |
d3a21303d9 | ||
![]() |
e1cbfdd84b | ||
![]() |
87170a4497 | ||
![]() |
ae6f8bd345 | ||
![]() |
b9496e0972 | ||
![]() |
c36a6dcd65 | ||
![]() |
19ca836b78 | ||
![]() |
8a6ea7ab50 | ||
![]() |
6721b8f265 | ||
![]() |
9393521f98 | ||
![]() |
398b24e0ab | ||
![]() |
374bcf8073 | ||
![]() |
7e3859e2f5 | ||
![]() |
490ec0d462 |
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.7
|
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||||
|
|
||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||||
nodejs \
|
nodejs \
|
||||||
yarn \
|
yarn \
|
||||||
&& curl -o - https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash \
|
&& curl -o - https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
ENV NVM_DIR /root/.nvm
|
ENV NVM_DIR /root/.nvm
|
||||||
|
|
||||||
@@ -43,9 +43,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
|
|
||||||
# Install Python dependencies from requirements.txt if it exists
|
# Install Python dependencies from requirements.txt if it exists
|
||||||
COPY requirements.txt requirements_tests.txt ./
|
COPY requirements.txt requirements_tests.txt ./
|
||||||
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
|
RUN pip3 install -U setuptools pip \
|
||||||
|
&& pip3 install -r requirements.txt -r requirements_tests.txt \
|
||||||
&& pip3 install tox \
|
&& pip3 install tox \
|
||||||
&& rm -f requirements.txt requirements_tests.txt
|
&& rm -f requirements.txt requirements_tests.txt
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
|
||||||
ENV SHELL /bin/bash
|
|
||||||
|
@@ -1,24 +1,32 @@
|
|||||||
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
|
|
||||||
{
|
{
|
||||||
"name": "Supervisor dev",
|
"name": "Supervisor dev",
|
||||||
"context": "..",
|
"context": "..",
|
||||||
"dockerFile": "Dockerfile",
|
"dockerFile": "Dockerfile",
|
||||||
"appPort": "9123:8123",
|
"appPort": "9123:8123",
|
||||||
|
"postCreateCommand": "pre-commit install",
|
||||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
"visualstudioexptteam.vscodeintellicode",
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"esbenp.prettier-vscode"
|
"esbenp.prettier-vscode"
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.pythonPath": "/usr/local/bin/python",
|
"terminal.integrated.shell.linux": "/bin/bash",
|
||||||
"python.linting.pylintEnabled": true,
|
|
||||||
"python.linting.enabled": true,
|
|
||||||
"python.formatting.provider": "black",
|
|
||||||
"python.formatting.blackArgs": ["--target-version", "py37"],
|
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
"files.trimTrailingWhitespace": true
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"python.pythonPath": "/usr/local/bin/python3",
|
||||||
|
"python.linting.pylintEnabled": true,
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.formatting.provider": "black",
|
||||||
|
"python.formatting.blackArgs": ["--target-version", "py38"],
|
||||||
|
"python.formatting.blackPath": "/usr/local/bin/black",
|
||||||
|
"python.linting.banditPath": "/usr/local/bin/bandit",
|
||||||
|
"python.linting.flake8Path": "/usr/local/bin/flake8",
|
||||||
|
"python.linting.mypyPath": "/usr/local/bin/mypy",
|
||||||
|
"python.linting.pylintPath": "/usr/local/bin/pylint",
|
||||||
|
"python.linting.pydocstylePath": "/usr/local/bin/pydocstyle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
13
.github/ISSUE_TEMPLATE.md
vendored
13
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,15 +1,15 @@
|
|||||||
<!-- READ THIS FIRST:
|
<!-- READ THIS FIRST:
|
||||||
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
|
||||||
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
|
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/core/releases
|
||||||
- Do not report issues for components here, plaese refer to https://github.com/home-assistant/home-assistant/issues
|
- Do not report issues for integrations here, please refer to https://github.com/home-assistant/core/issues
|
||||||
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
|
||||||
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
|
||||||
- If you have a problem with a Add-on, make a issue on there repository.
|
- If you have a problem with an add-on, make an issue in its repository.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Home Assistant release with the issue:**
|
**Home Assistant release with the issue:**
|
||||||
<!--
|
<!--
|
||||||
- Frontend -> Developer tools -> Info
|
- Frontend -> Configuration -> Info
|
||||||
- Or use this command: hass --version
|
- Or use this command: hass --version
|
||||||
-->
|
-->
|
||||||
|
|
||||||
@@ -20,10 +20,9 @@ Please provide details about your environment.
|
|||||||
|
|
||||||
**Supervisor logs:**
|
**Supervisor logs:**
|
||||||
<!--
|
<!--
|
||||||
- Frontend -> Hass.io -> System
|
- Frontend -> Supervisor -> System
|
||||||
- Or use this command: hassio su logs
|
- Or use this command: ha supervisor logs
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
**Description of problem:**
|
**Description of problem:**
|
||||||
|
|
||||||
|
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "06:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "06:00"
|
||||||
|
open-pull-requests-limit: 10
|
2
.github/lock.yml
vendored
2
.github/lock.yml
vendored
@@ -24,4 +24,4 @@ only: pulls
|
|||||||
|
|
||||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||||
issues:
|
issues:
|
||||||
daysUntilLock: 30
|
daysUntilLock: 30
|
||||||
|
3
.github/stale.yml
vendored
3
.github/stale.yml
vendored
@@ -6,8 +6,9 @@ daysUntilClose: 7
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
|
- rfc
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
432
.github/workflows/ci.yaml
vendored
Normal file
432
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- master
|
||||||
|
pull_request: ~
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_PYTHON: 3.8
|
||||||
|
PRE_COMMIT_HOME: ~/.cache/pre-commit
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Separate job to pre-populate the base dependency cache
|
||||||
|
# This prevent upcoming jobs to do the same individually
|
||||||
|
prepare:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8]
|
||||||
|
name: Prepare Python ${{ matrix.python-version }} dependencies
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
id: python
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-
|
||||||
|
- name: Create Python virtual environment
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
python -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -U pip setuptools
|
||||||
|
pip install -r requirements.txt -r requirements_tests.txt
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pre-commit-
|
||||||
|
- name: Install pre-commit dependencies
|
||||||
|
if: steps.cache-precommit.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit install-hooks
|
||||||
|
|
||||||
|
lint-black:
|
||||||
|
name: Check black
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run black
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
black --target-version py38 --check supervisor tests setup.py
|
||||||
|
|
||||||
|
lint-dockerfile:
|
||||||
|
name: Check Dockerfile
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Register hadolint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
|
- name: Check Dockerfile
|
||||||
|
uses: docker://hadolint/hadolint:v1.18.0
|
||||||
|
with:
|
||||||
|
args: hadolint Dockerfile
|
||||||
|
|
||||||
|
lint-executable-shebangs:
|
||||||
|
name: Check executables
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register check executables problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||||
|
- name: Run executables check
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
|
||||||
|
|
||||||
|
lint-flake8:
|
||||||
|
name: Check flake8
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register flake8 problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/flake8.json"
|
||||||
|
- name: Run flake8
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
flake8 supervisor tests
|
||||||
|
|
||||||
|
lint-isort:
|
||||||
|
name: Check isort
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run isort
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
lint-json:
|
||||||
|
name: Check JSON
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register check-json problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/check-json.json"
|
||||||
|
- name: Run check-json
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual check-json --all-files
|
||||||
|
|
||||||
|
lint-pylint:
|
||||||
|
name: Check pylint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Register pylint problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/pylint.json"
|
||||||
|
- name: Run pylint
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pylint supervisor tests
|
||||||
|
|
||||||
|
lint-pyupgrade:
|
||||||
|
name: Check pyupgrade
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Restore pre-commit environment from cache
|
||||||
|
id: cache-precommit
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.PRE_COMMIT_HOME }}
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||||
|
- name: Fail job if cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Run pyupgrade
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8]
|
||||||
|
name: Run tests Python ${{ matrix.python-version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Install additional system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends libpulse0 libudev1
|
||||||
|
- name: Register Python problem matcher
|
||||||
|
run: |
|
||||||
|
echo "::add-matcher::.github/workflows/matchers/python.json"
|
||||||
|
- name: Install Pytest Annotation plugin
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
# Ideally this should be part of our dependencies
|
||||||
|
# However this plugin is fairly new and doesn't run correctly
|
||||||
|
# on a non-GitHub environment.
|
||||||
|
pip install pytest-github-actions-annotate-failures
|
||||||
|
- name: Run pytest
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
pytest \
|
||||||
|
-qq \
|
||||||
|
--timeout=10 \
|
||||||
|
--durations=10 \
|
||||||
|
--cov supervisor \
|
||||||
|
-o console_output_style=count \
|
||||||
|
tests
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
uses: actions/upload-artifact@v2.1.4
|
||||||
|
with:
|
||||||
|
name: coverage-${{ matrix.python-version }}
|
||||||
|
path: .coverage
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: Process test coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: pytest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
|
uses: actions/setup-python@v2.1.2
|
||||||
|
id: python
|
||||||
|
with:
|
||||||
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
- name: Restore Python virtual environment
|
||||||
|
id: cache-venv
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: venv
|
||||||
|
key: |
|
||||||
|
${{ runner.os }}-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
|
||||||
|
- name: Fail job if Python cache restore failed
|
||||||
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
|
exit 1
|
||||||
|
- name: Download all coverage artifacts
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
- name: Combine coverage results
|
||||||
|
run: |
|
||||||
|
. venv/bin/activate
|
||||||
|
coverage combine coverage*/.coverage*
|
||||||
|
coverage report
|
||||||
|
coverage xml
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v1.0.13
|
14
.github/workflows/matchers/check-executables-have-shebangs.json
vendored
Normal file
14
.github/workflows/matchers/check-executables-have-shebangs.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "check-executables-have-shebangs",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):\\s(.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"message": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/check-json.json
vendored
Normal file
16
.github/workflows/matchers/check-json.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "check-json",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$",
|
||||||
|
"file": 1,
|
||||||
|
"message": 2,
|
||||||
|
"line": 3,
|
||||||
|
"column": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
30
.github/workflows/matchers/flake8.json
vendored
Normal file
30
.github/workflows/matchers/flake8.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "flake8-error",
|
||||||
|
"severity": "error",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s(E\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "flake8-warning",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.*):(\\d+):(\\d+):\\s([CDFNW]\\d{3}\\s.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.github/workflows/matchers/hadolint.json
vendored
Normal file
16
.github/workflows/matchers/hadolint.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "hadolint",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"message": 3,
|
||||||
|
"code": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
32
.github/workflows/matchers/pylint.json
vendored
Normal file
32
.github/workflows/matchers/pylint.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "pylint-error",
|
||||||
|
"severity": "error",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4,
|
||||||
|
"code": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "pylint-warning",
|
||||||
|
"severity": "warning",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"message": 4,
|
||||||
|
"code": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
18
.github/workflows/matchers/python.json
vendored
Normal file
18
.github/workflows/matchers/python.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "python",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
|
||||||
|
"message": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
.github/workflows/release-drafter.yml
vendored
Normal file
15
.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update_release_draft:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
21
.github/workflows/sentry.yaml
vendored
Normal file
21
.github/workflows/sentry.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Sentry Release
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published, prereleased]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
createSentryRelease:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code from GitHub
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Sentry Release
|
||||||
|
uses: getsentry/action-release@v1.0.1
|
||||||
|
env:
|
||||||
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||||
|
with:
|
||||||
|
environment: production
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -95,3 +95,8 @@ ENV/
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/cSpell.json
|
!.vscode/cSpell.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
/.mypy_cache/*
|
||||||
|
/.dmypy.json
|
||||||
|
34
.pre-commit-config.yaml
Normal file
34
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 20.8b1
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
args:
|
||||||
|
- --safe
|
||||||
|
- --quiet
|
||||||
|
- --target-version
|
||||||
|
- py38
|
||||||
|
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||||
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
|
rev: 3.8.3
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
additional_dependencies:
|
||||||
|
- flake8-docstrings==1.5.0
|
||||||
|
- pydocstyle==5.0.2
|
||||||
|
files: ^(supervisor|script|tests)/.+\.py$
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v3.1.0
|
||||||
|
hooks:
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
stages: [manual]
|
||||||
|
- id: check-json
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-isort
|
||||||
|
rev: v4.3.21
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v2.6.2
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: [--py37-plus]
|
18
.vscode/launch.json
vendored
Normal file
18
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Supervisor remote debug",
|
||||||
|
"type": "python",
|
||||||
|
"request": "attach",
|
||||||
|
"port": 33333,
|
||||||
|
"host": "172.30.32.2",
|
||||||
|
"pathMappings": [
|
||||||
|
{
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "/usr/src/supervisor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
180
.vscode/tasks.json
vendored
180
.vscode/tasks.json
vendored
@@ -1,92 +1,90 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "Run Testenv",
|
"label": "Run Testenv",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/test_env.sh",
|
"command": "./scripts/test_env.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Run Testenv CLI",
|
"label": "Run Testenv CLI",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "docker run --rm -ti -v /etc/machine-id:/etc/machine-id --network=hassio --add-host hassio:172.30.32.2 homeassistant/amd64-hassio-cli:dev",
|
"command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Update UI",
|
"label": "Update UI",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "./scripts/update-frontend.sh",
|
"command": "./scripts/update-frontend.sh",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "build",
|
"kind": "build",
|
||||||
"isDefault": true
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pytest",
|
"label": "Pytest",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pytest --timeout=10 tests",
|
"command": "pytest --timeout=10 tests",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Flake8",
|
"label": "Flake8",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "flake8 hassio tests",
|
"command": "flake8 supervisor tests",
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
"isDefault": true,
|
"isDefault": true
|
||||||
},
|
},
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Pylint",
|
"label": "Pylint",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pylint hassio",
|
"command": "pylint supervisor",
|
||||||
"dependsOn": [
|
"dependsOn": ["Install all Requirements"],
|
||||||
"Install all Requirements"
|
"group": {
|
||||||
],
|
"kind": "test",
|
||||||
"group": {
|
"isDefault": true
|
||||||
"kind": "test",
|
},
|
||||||
"isDefault": true,
|
"presentation": {
|
||||||
},
|
"reveal": "always",
|
||||||
"presentation": {
|
"panel": "new"
|
||||||
"reveal": "always",
|
},
|
||||||
"panel": "new"
|
"problemMatcher": []
|
||||||
},
|
}
|
||||||
"problemMatcher": []
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
|
369
API.md
369
API.md
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
## Supervisor RESTful API
|
## Supervisor RESTful API
|
||||||
|
|
||||||
Interface for Home Assistant to control things from supervisor.
|
The RESTful API for Home Assistant allows you to control things around
|
||||||
|
around the Supervisor and other components.
|
||||||
|
|
||||||
On error / Code 400:
|
On error / Code 400:
|
||||||
|
|
||||||
@@ -22,8 +23,10 @@ On success / Code 200:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
For access to API you need use a authorization header with a `Bearer` token.
|
To access the API you need use an authorization header with a `Bearer` token.
|
||||||
They are available for Add-ons and the Home Assistant using the `SUPERVISOR_TOKEN` environment variable.
|
|
||||||
|
The token is available for add-ons and Home Assistant using the
|
||||||
|
`SUPERVISOR_TOKEN` environment variable.
|
||||||
|
|
||||||
### Supervisor
|
### Supervisor
|
||||||
|
|
||||||
@@ -33,20 +36,23 @@ This API call don't need a token.
|
|||||||
|
|
||||||
- GET `/supervisor/info`
|
- GET `/supervisor/info`
|
||||||
|
|
||||||
The addons from `addons` are only installed one.
|
Shows the installed add-ons from `addons`.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"last_version": "LAST_VERSION",
|
"version_latest": "version_latest",
|
||||||
"arch": "armhf|aarch64|i386|amd64",
|
"arch": "armhf|aarch64|i386|amd64",
|
||||||
"channel": "stable|beta|dev",
|
"channel": "stable|beta|dev",
|
||||||
"timezone": "TIMEZONE",
|
"timezone": "TIMEZONE",
|
||||||
|
"healthy": "bool",
|
||||||
|
"supported": "bool",
|
||||||
"logging": "debug|info|warning|error|critical",
|
"logging": "debug|info|warning|error|critical",
|
||||||
"ip_address": "ip address",
|
"ip_address": "ip address",
|
||||||
"wait_boot": "int",
|
"wait_boot": "int",
|
||||||
"debug": "bool",
|
"debug": "bool",
|
||||||
"debug_block": "bool",
|
"debug_block": "bool",
|
||||||
|
"diagnostics": "None|bool",
|
||||||
"addons": [
|
"addons": [
|
||||||
{
|
{
|
||||||
"name": "xy bla",
|
"name": "xy bla",
|
||||||
@@ -90,11 +96,11 @@ Optional:
|
|||||||
|
|
||||||
- POST `/supervisor/reload`
|
- POST `/supervisor/reload`
|
||||||
|
|
||||||
Reload addons/version.
|
Reload the add-ons/version.
|
||||||
|
|
||||||
- GET `/supervisor/logs`
|
- GET `/supervisor/logs`
|
||||||
|
|
||||||
Output is the raw docker log.
|
Output is the raw Docker log.
|
||||||
|
|
||||||
- GET `/supervisor/stats`
|
- GET `/supervisor/stats`
|
||||||
|
|
||||||
@@ -113,7 +119,7 @@ Output is the raw docker log.
|
|||||||
|
|
||||||
- GET `/supervisor/repair`
|
- GET `/supervisor/repair`
|
||||||
|
|
||||||
Repair overlayfs issue and restore lost images
|
Repair overlayfs issue and restore lost images.
|
||||||
|
|
||||||
### Snapshot
|
### Snapshot
|
||||||
|
|
||||||
@@ -134,7 +140,6 @@ Repair overlayfs issue and restore lost images
|
|||||||
```
|
```
|
||||||
|
|
||||||
- POST `/snapshots/reload`
|
- POST `/snapshots/reload`
|
||||||
|
|
||||||
- POST `/snapshots/new/upload`
|
- POST `/snapshots/new/upload`
|
||||||
|
|
||||||
return:
|
return:
|
||||||
@@ -208,9 +213,7 @@ return:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- POST `/snapshots/{slug}/remove`
|
- POST `/snapshots/{slug}/remove`
|
||||||
|
|
||||||
- GET `/snapshots/{slug}/download`
|
- GET `/snapshots/{slug}/download`
|
||||||
|
|
||||||
- POST `/snapshots/{slug}/restore/full`
|
- POST `/snapshots/{slug}/restore/full`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -233,25 +236,29 @@ return:
|
|||||||
### Host
|
### Host
|
||||||
|
|
||||||
- POST `/host/reload`
|
- POST `/host/reload`
|
||||||
|
|
||||||
- POST `/host/shutdown`
|
- POST `/host/shutdown`
|
||||||
|
|
||||||
- POST `/host/reboot`
|
- POST `/host/reboot`
|
||||||
|
|
||||||
- GET `/host/info`
|
- GET `/host/info`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"hostname": "hostname|null",
|
|
||||||
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
|
||||||
"operating_system": "HassOS XY|Ubuntu 16.4|null",
|
|
||||||
"kernel": "4.15.7|null",
|
|
||||||
"chassis": "specific|null",
|
"chassis": "specific|null",
|
||||||
|
"cpe": "xy|null",
|
||||||
"deployment": "stable|beta|dev|null",
|
"deployment": "stable|beta|dev|null",
|
||||||
"cpe": "xy|null"
|
"disk_total": 32.0,
|
||||||
|
"disk_used": 30.0,
|
||||||
|
"disk_free": 2.0,
|
||||||
|
"features": ["shutdown", "reboot", "hostname", "services", "hassos"],
|
||||||
|
"hostname": "hostname|null",
|
||||||
|
"kernel": "4.15.7|null",
|
||||||
|
"operating_system": "HassOS XY|Ubuntu 16.4|null"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- GET `/host/logs`
|
||||||
|
|
||||||
|
Return the host log messages (dmesg).
|
||||||
|
|
||||||
- POST `/host/options`
|
- POST `/host/options`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -291,9 +298,7 @@ return:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "2.3",
|
"version": "2.3",
|
||||||
"version_cli": "7",
|
|
||||||
"version_latest": "2.4",
|
"version_latest": "2.4",
|
||||||
"version_cli_latest": "8",
|
|
||||||
"board": "ova|rpi",
|
"board": "ova|rpi",
|
||||||
"boot": "rauc boot slot"
|
"boot": "rauc boot slot"
|
||||||
}
|
}
|
||||||
@@ -307,17 +312,9 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- POST `/os/update/cli`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "optional"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/os/config/sync`
|
- POST `/os/config/sync`
|
||||||
|
|
||||||
Load host configs from a USB stick.
|
Load host configurations from an USB stick.
|
||||||
|
|
||||||
### Hardware
|
### Hardware
|
||||||
|
|
||||||
@@ -360,7 +357,7 @@ Load host configs from a USB stick.
|
|||||||
|
|
||||||
- POST `/hardware/trigger`
|
- POST `/hardware/trigger`
|
||||||
|
|
||||||
Trigger an udev reload
|
Trigger an UDEV reload.
|
||||||
|
|
||||||
### Home Assistant
|
### Home Assistant
|
||||||
|
|
||||||
@@ -369,17 +366,18 @@ Trigger an udev reload
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "INSTALL_VERSION",
|
"version": "INSTALL_VERSION",
|
||||||
"last_version": "LAST_VERSION",
|
"version_latest": "version_latest",
|
||||||
"arch": "arch",
|
"arch": "arch",
|
||||||
"machine": "Image machine type",
|
"machine": "Image machine type",
|
||||||
"ip_address": "ip address",
|
"ip_address": "ip address",
|
||||||
"image": "str",
|
"image": "str",
|
||||||
"custom": "bool -> if custom image",
|
|
||||||
"boot": "bool",
|
"boot": "bool",
|
||||||
"port": 8123,
|
"port": 8123,
|
||||||
"ssl": "bool",
|
"ssl": "bool",
|
||||||
"watchdog": "bool",
|
"watchdog": "bool",
|
||||||
"wait_boot": 600
|
"wait_boot": 600,
|
||||||
|
"audio_input": "null|profile",
|
||||||
|
"audio_output": "null|profile"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -408,16 +406,18 @@ Output is the raw Docker log.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"image": "Optional|null",
|
"image": "Optional|null",
|
||||||
"last_version": "Optional for custom image|null",
|
"version_latest": "Optional for custom image|null",
|
||||||
"port": "port for access core",
|
"port": "port for access core",
|
||||||
"ssl": "bool",
|
"ssl": "bool",
|
||||||
"refresh_token": "",
|
"refresh_token": "",
|
||||||
"watchdog": "bool",
|
"watchdog": "bool",
|
||||||
"wait_boot": 600
|
"wait_boot": 600,
|
||||||
|
"audio_input": "null|profile",
|
||||||
|
"audio_output": "null|profile"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Image with `null` and last_version with `null` reset this options.
|
Image with `null` and `version_latest` with `null` reset this options.
|
||||||
|
|
||||||
- POST/GET `/core/api`
|
- POST/GET `/core/api`
|
||||||
|
|
||||||
@@ -442,6 +442,65 @@ Proxy to Home Assistant Core websocket.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Network
|
||||||
|
|
||||||
|
Network operations over the API
|
||||||
|
|
||||||
|
#### GET `/network/info`
|
||||||
|
|
||||||
|
Get network information
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"interfaces": {
|
||||||
|
"enp0s31f6": {
|
||||||
|
"ip_address": "192.168.2.148/24",
|
||||||
|
"gateway": "192.168.2.1",
|
||||||
|
"id": "Wired connection 1",
|
||||||
|
"type": "802-3-ethernet",
|
||||||
|
"nameservers": ["192.168.2.1"],
|
||||||
|
"method": "static",
|
||||||
|
"primary": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET `/network/interface/{interface}/info`
|
||||||
|
|
||||||
|
Get information for a single interface
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ip_address": "192.168.2.148/24",
|
||||||
|
"gateway": "192.168.2.1",
|
||||||
|
"id": "Wired connection 1",
|
||||||
|
"type": "802-3-ethernet",
|
||||||
|
"nameservers": ["192.168.2.1"],
|
||||||
|
"method": "dhcp",
|
||||||
|
"primary": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST `/network/interface/{interface}/update`
|
||||||
|
|
||||||
|
Update information for a single interface
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| --------- | ---------------------------------------------------------------------- |
|
||||||
|
| `address` | The new IP address for the interface in the X.X.X.X/XX format |
|
||||||
|
| `dns` | List of DNS servers to use |
|
||||||
|
| `gateway` | The gateway the interface should use |
|
||||||
|
| `method` | Set if the interface should use DHCP or not, can be `dhcp` or `static` |
|
||||||
|
|
||||||
|
_All options are optional._
|
||||||
|
|
||||||
|
**NB!: If you change the `address` or `gateway` you may need to reconnect to the new address**
|
||||||
|
|
||||||
|
The result will be a updated object.
|
||||||
|
|
||||||
### RESTful for API add-ons
|
### RESTful for API add-ons
|
||||||
|
|
||||||
If an add-on will call itself, you can use `/addons/self/...`.
|
If an add-on will call itself, you can use `/addons/self/...`.
|
||||||
@@ -460,7 +519,7 @@ Get all available add-ons.
|
|||||||
"advanced": "bool",
|
"advanced": "bool",
|
||||||
"stage": "stable|experimental|deprecated",
|
"stage": "stable|experimental|deprecated",
|
||||||
"repository": "core|local|REP_ID",
|
"repository": "core|local|REP_ID",
|
||||||
"version": "LAST_VERSION",
|
"version": "version_latest",
|
||||||
"installed": "none|INSTALL_VERSION",
|
"installed": "none|INSTALL_VERSION",
|
||||||
"detached": "bool",
|
"detached": "bool",
|
||||||
"available": "bool",
|
"available": "bool",
|
||||||
@@ -504,8 +563,9 @@ Get all available add-ons.
|
|||||||
"homeassistant": "null|min Home Assistant Core version",
|
"homeassistant": "null|min Home Assistant Core version",
|
||||||
"repository": "12345678|null",
|
"repository": "12345678|null",
|
||||||
"version": "null|VERSION_INSTALLED",
|
"version": "null|VERSION_INSTALLED",
|
||||||
"last_version": "LAST_VERSION",
|
"version_latest": "version_latest",
|
||||||
"state": "none|started|stopped",
|
"state": "none|started|stopped",
|
||||||
|
"startup": "initialize|system|services|application|once",
|
||||||
"boot": "auto|manual",
|
"boot": "auto|manual",
|
||||||
"build": "bool",
|
"build": "bool",
|
||||||
"options": "{}",
|
"options": "{}",
|
||||||
@@ -535,6 +595,7 @@ Get all available add-ons.
|
|||||||
"stdin": "bool",
|
"stdin": "bool",
|
||||||
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
"webui": "null|http(s)://[HOST]:port/xy/zx",
|
||||||
"gpio": "bool",
|
"gpio": "bool",
|
||||||
|
"usb": "[physical_path_to_usb_device]",
|
||||||
"kernel_modules": "bool",
|
"kernel_modules": "bool",
|
||||||
"devicetree": "bool",
|
"devicetree": "bool",
|
||||||
"docker_api": "bool",
|
"docker_api": "bool",
|
||||||
@@ -549,19 +610,17 @@ Get all available add-ons.
|
|||||||
"ingress_entry": "null|/api/hassio_ingress/slug",
|
"ingress_entry": "null|/api/hassio_ingress/slug",
|
||||||
"ingress_url": "null|/api/hassio_ingress/slug/entry.html",
|
"ingress_url": "null|/api/hassio_ingress/slug/entry.html",
|
||||||
"ingress_port": "null|int",
|
"ingress_port": "null|int",
|
||||||
"ingress_panel": "null|bool"
|
"ingress_panel": "null|bool",
|
||||||
|
"watchdog": "null|bool"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- GET `/addons/{addon}/icon`
|
- GET `/addons/{addon}/icon`
|
||||||
|
|
||||||
- GET `/addons/{addon}/logo`
|
- GET `/addons/{addon}/logo`
|
||||||
|
|
||||||
- GET `/addons/{addon}/changelog`
|
- GET `/addons/{addon}/changelog`
|
||||||
|
|
||||||
- GET `/addons/{addon}/documentation`
|
- GET `/addons/{addon}/documentation`
|
||||||
|
|
||||||
- POST `/addons/{addon}/options`
|
- POST `/addons/{addon}/options`
|
||||||
|
- POST `/addons/{addon}/options/validate`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -573,11 +632,12 @@ Get all available add-ons.
|
|||||||
"options": {},
|
"options": {},
|
||||||
"audio_output": "null|0,0",
|
"audio_output": "null|0,0",
|
||||||
"audio_input": "null|0,0",
|
"audio_input": "null|0,0",
|
||||||
"ingress_panel": "bool"
|
"ingress_panel": "bool",
|
||||||
|
"watchdog": "bool"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Reset custom network/audio/options, set it `null`.
|
Reset custom network, audio and options, set it to `null`.
|
||||||
|
|
||||||
- POST `/addons/{addon}/security`
|
- POST `/addons/{addon}/security`
|
||||||
|
|
||||||
@@ -590,28 +650,22 @@ This function is not callable by itself.
|
|||||||
```
|
```
|
||||||
|
|
||||||
- POST `/addons/{addon}/start`
|
- POST `/addons/{addon}/start`
|
||||||
|
|
||||||
- POST `/addons/{addon}/stop`
|
- POST `/addons/{addon}/stop`
|
||||||
|
|
||||||
- POST `/addons/{addon}/install`
|
- POST `/addons/{addon}/install`
|
||||||
|
|
||||||
- POST `/addons/{addon}/uninstall`
|
- POST `/addons/{addon}/uninstall`
|
||||||
|
|
||||||
- POST `/addons/{addon}/update`
|
- POST `/addons/{addon}/update`
|
||||||
|
|
||||||
- GET `/addons/{addon}/logs`
|
- GET `/addons/{addon}/logs`
|
||||||
|
|
||||||
Output is the raw Docker log.
|
Output is the raw Docker log.
|
||||||
|
|
||||||
- POST `/addons/{addon}/restart`
|
- POST `/addons/{addon}/restart`
|
||||||
|
|
||||||
- POST `/addons/{addon}/rebuild`
|
- POST `/addons/{addon}/rebuild`
|
||||||
|
|
||||||
Only supported for local build addons
|
Only supported for local build add-ons.
|
||||||
|
|
||||||
- POST `/addons/{addon}/stdin`
|
- POST `/addons/{addon}/stdin`
|
||||||
|
|
||||||
Write data to add-on stdin
|
Write data to add-on stdin.
|
||||||
|
|
||||||
- GET `/addons/{addon}/stats`
|
- GET `/addons/{addon}/stats`
|
||||||
|
|
||||||
@@ -632,7 +686,7 @@ Write data to add-on stdin
|
|||||||
|
|
||||||
- POST `/ingress/session`
|
- POST `/ingress/session`
|
||||||
|
|
||||||
Create a new Session for access to ingress service.
|
Create a new session for access to the ingress service.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -659,8 +713,8 @@ Return a list of enabled panels.
|
|||||||
|
|
||||||
- VIEW `/ingress/{token}`
|
- VIEW `/ingress/{token}`
|
||||||
|
|
||||||
Ingress WebUI for this Add-on. The addon need support HASS Auth!
|
Ingress WebUI for this add-on. The add-on need support for the Home Assistant
|
||||||
Need ingress session as cookie.
|
authentication system. Needs an ingress session as cookie.
|
||||||
|
|
||||||
### discovery
|
### discovery
|
||||||
|
|
||||||
@@ -675,7 +729,10 @@ Need ingress session as cookie.
|
|||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"services": {
|
||||||
|
"ozw": ["core_zwave"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -792,10 +849,12 @@ return:
|
|||||||
"supervisor": "version",
|
"supervisor": "version",
|
||||||
"homeassistant": "version",
|
"homeassistant": "version",
|
||||||
"hassos": "null|version",
|
"hassos": "null|version",
|
||||||
|
"docker": "version",
|
||||||
"hostname": "name",
|
"hostname": "name",
|
||||||
"machine": "type",
|
"machine": "type",
|
||||||
"arch": "arch",
|
"arch": "arch",
|
||||||
"supported_arch": ["arch1", "arch2"],
|
"supported_arch": ["arch1", "arch2"],
|
||||||
|
"supported": "bool",
|
||||||
"channel": "stable|beta|dev",
|
"channel": "stable|beta|dev",
|
||||||
"logging": "debug|info|warning|error|critical",
|
"logging": "debug|info|warning|error|critical",
|
||||||
"timezone": "Europe/Zurich"
|
"timezone": "Europe/Zurich"
|
||||||
@@ -810,7 +869,7 @@ return:
|
|||||||
{
|
{
|
||||||
"host": "ip-address",
|
"host": "ip-address",
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"latest_version": "2",
|
"version_latest": "2",
|
||||||
"servers": ["dns://8.8.8.8"],
|
"servers": ["dns://8.8.8.8"],
|
||||||
"locals": ["dns://xy"]
|
"locals": ["dns://xy"]
|
||||||
}
|
}
|
||||||
@@ -853,6 +912,78 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
- GET `/cli/info`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"version_latest": "2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/cli/update`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "VERSION"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- GET `/cli/stats`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cpu_percent": 0.0,
|
||||||
|
"memory_usage": 283123,
|
||||||
|
"memory_limit": 329392,
|
||||||
|
"memory_percent": 1.4,
|
||||||
|
"network_tx": 0,
|
||||||
|
"network_rx": 0,
|
||||||
|
"blk_read": 0,
|
||||||
|
"blk_write": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multicast
|
||||||
|
|
||||||
|
- GET `/multicast/info`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"version_latest": "2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/multicast/update`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "VERSION"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/multicast/restart`
|
||||||
|
|
||||||
|
- GET `/multicast/logs`
|
||||||
|
|
||||||
|
- GET `/multicast/stats`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cpu_percent": 0.0,
|
||||||
|
"memory_usage": 283123,
|
||||||
|
"memory_limit": 329392,
|
||||||
|
"memory_percent": 1.4,
|
||||||
|
"network_tx": 0,
|
||||||
|
"network_rx": 0,
|
||||||
|
"blk_read": 0,
|
||||||
|
"blk_write": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
- GET `/audio/info`
|
- GET `/audio/info`
|
||||||
@@ -863,20 +994,73 @@ return:
|
|||||||
"version": "1",
|
"version": "1",
|
||||||
"latest_version": "2",
|
"latest_version": "2",
|
||||||
"audio": {
|
"audio": {
|
||||||
|
"card": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 1,
|
||||||
|
"driver": "...",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"description": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"input": [
|
"input": [
|
||||||
{
|
{
|
||||||
"name": "...",
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
"description": "...",
|
"description": "...",
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"default": false
|
"mute": false,
|
||||||
|
"default": false,
|
||||||
|
"card": "null|int",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "INPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"output": [
|
"output": [
|
||||||
{
|
{
|
||||||
"name": "...",
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
"description": "...",
|
"description": "...",
|
||||||
"volume": 0.3,
|
"volume": 0.3,
|
||||||
"default": false
|
"mute": false,
|
||||||
|
"default": false,
|
||||||
|
"card": "null|int",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "OUTPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"application": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "OUTPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -901,7 +1085,7 @@ return:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "...",
|
"index": "...",
|
||||||
"volume": 0.5
|
"volume": 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -910,11 +1094,47 @@ return:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "...",
|
"index": "...",
|
||||||
"volume": 0.5
|
"volume": 0.5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- POST `/audio/volume/{output|input}/application`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"volume": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/input`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/mute/{output|input}/application`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- POST `/audio/default/input`
|
- POST `/audio/default/input`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -931,6 +1151,15 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- POST `/audio/profile`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"card": "...",
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- GET `/audio/stats`
|
- GET `/audio/stats`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -946,18 +1175,18 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auth / SSO API
|
### Authentication/SSO API
|
||||||
|
|
||||||
You can use the user system on homeassistant. We handle this auth system on
|
You can use the user system from Home Assistant. The auth system can be handled
|
||||||
supervisor.
|
with the Supervisor.
|
||||||
|
|
||||||
You can call post `/auth`
|
`/auth` is accepting POST calls.
|
||||||
|
|
||||||
We support:
|
We support:
|
||||||
|
|
||||||
- Json `{ "user|name": "...", "password": "..." }`
|
- JSON: `{ "user|name": "...", "password": "..." }`
|
||||||
- application/x-www-form-urlencoded `user|name=...&password=...`
|
- `application/x-www-form-urlencoded`: `user|name=...&password=...`
|
||||||
- BasicAuth
|
- Basic Authentication
|
||||||
|
|
||||||
* POST `/auth/reset`
|
* POST `/auth/reset`
|
||||||
|
|
||||||
|
30
Dockerfile
30
Dockerfile
@@ -1,24 +1,29 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM $BUILD_FROM
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
|
ENV \
|
||||||
|
S6_SERVICES_GRACETIME=10000 \
|
||||||
|
SUPERVISOR_API=http://localhost
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN apk add --no-cache \
|
RUN \
|
||||||
eudev \
|
apk add --no-cache \
|
||||||
eudev-libs \
|
eudev \
|
||||||
git \
|
eudev-libs \
|
||||||
glib \
|
git \
|
||||||
libffi \
|
glib \
|
||||||
libpulse \
|
libffi \
|
||||||
musl \
|
libpulse \
|
||||||
openssl \
|
musl \
|
||||||
socat
|
openssl
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
ARG BUILD_ARCH
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN export MAKEFLAGS="-j$(nproc)" \
|
RUN \
|
||||||
|
export MAKEFLAGS="-j$(nproc)" \
|
||||||
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
|
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links \
|
||||||
"https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
|
"https://wheels.home-assistant.io/alpine-$(cut -d '.' -f 1-2 < /etc/alpine-release)/${BUILD_ARCH}/" \
|
||||||
-r ./requirements.txt \
|
-r ./requirements.txt \
|
||||||
@@ -26,7 +31,8 @@ RUN export MAKEFLAGS="-j$(nproc)" \
|
|||||||
|
|
||||||
# Install Home Assistant Supervisor
|
# Install Home Assistant Supervisor
|
||||||
COPY . supervisor
|
COPY . supervisor
|
||||||
RUN pip3 install --no-cache-dir -e ./supervisor \
|
RUN \
|
||||||
|
pip3 install --no-cache-dir -e ./supervisor \
|
||||||
&& python3 -m compileall ./supervisor/supervisor
|
&& python3 -m compileall ./supervisor/supervisor
|
||||||
|
|
||||||
|
|
||||||
|
4
LICENSE
4
LICENSE
@@ -178,7 +178,7 @@
|
|||||||
APPENDIX: How to apply the Apache License to your work.
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
To apply the Apache License to your work, attach the following
|
||||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
replaced with your own identifying information. (Don't include
|
replaced with your own identifying information. (Don't include
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
comment syntax for the file format. We also recommend that a
|
comment syntax for the file format. We also recommend that a
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2017 Pascal Vizeli
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
26
README.md
26
README.md
@@ -1,28 +1,26 @@
|
|||||||
[](https://dev.azure.com/home-assistant/Hass.io/_build/latest?definitionId=2&branchName=dev)
|
|
||||||
|
|
||||||
# Home Assistant Supervisor
|
# Home Assistant Supervisor
|
||||||
|
|
||||||
## First private cloud solution for home automation
|
## First private cloud solution for home automation
|
||||||
|
|
||||||
Hass.io is a Docker-based system for managing your Home Assistant installation
|
Home Assistant (former Hass.io) is a container-based system for managing your
|
||||||
and related applications. The system is controlled via Home Assistant which
|
Home Assistant Core installation and related applications. The system is
|
||||||
communicates with the Supervisor. The Supervisor provides an API to manage the
|
controlled via Home Assistant which communicates with the Supervisor. The
|
||||||
installation. This includes changing network settings or installing
|
Supervisor provides an API to manage the installation. This includes changing
|
||||||
and updating software.
|
network settings or installing and updating software.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Installation instructions can be found at <https://home-assistant.io/hassio>.
|
Installation instructions can be found at https://home-assistant.io/hassio.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The development of the supervisor is a bit tricky. Not difficult but tricky.
|
The development of the Supervisor is not difficult but tricky.
|
||||||
|
|
||||||
- You can use the builder to build your supervisor: https://github.com/home-assistant/hassio-builder
|
- You can use the builder to create your Supervisor: https://github.com/home-assistant/hassio-builder
|
||||||
- Go into a HassOS device or VM and pull your supervisor.
|
- Access a HassOS device or VM and pull your Supervisor.
|
||||||
- Set the developer modus with cli `hassio supervisor options --channel=dev`
|
- Set the developer modus with the CLI tool: `ha supervisor options --channel=dev`
|
||||||
- Tag it as `homeassistant/xy-hassio-supervisor:latest`
|
- Tag it as `homeassistant/xy-hassio-supervisor:latest`
|
||||||
- Restart the service like `systemctl restart hassos-supervisor | journalctl -fu hassos-supervisor`
|
- Restart the service with `systemctl restart hassos-supervisor | journalctl -fu hassos-supervisor`
|
||||||
- Test your changes
|
- Test your changes
|
||||||
|
|
||||||
Small Bugfix or improvements, make a PR. Significant change makes first an RFC.
|
For small bugfixes or improvements, make a PR. For significant changes open a RFC first, please. Thanks.
|
||||||
|
@@ -22,9 +22,9 @@ jobs:
|
|||||||
sudo apt-get install -y libpulse0 libudev1
|
sudo apt-get install -y libpulse0 libudev1
|
||||||
displayName: "Install Host library"
|
displayName: "Install Host library"
|
||||||
- task: UsePythonVersion@0
|
- task: UsePythonVersion@0
|
||||||
displayName: "Use Python 3.7"
|
displayName: "Use Python 3.8"
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: "3.7"
|
versionSpec: "3.8"
|
||||||
- script: pip install tox
|
- script: pip install tox
|
||||||
displayName: "Install Tox"
|
displayName: "Install Tox"
|
||||||
- script: tox
|
- script: tox
|
||||||
|
@@ -20,9 +20,9 @@ jobs:
|
|||||||
vmImage: "ubuntu-latest"
|
vmImage: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- task: UsePythonVersion@0
|
- task: UsePythonVersion@0
|
||||||
displayName: "Use Python 3.7"
|
displayName: "Use Python 3.8"
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: "3.7"
|
versionSpec: "3.8"
|
||||||
- script: |
|
- script: |
|
||||||
setup_version="$(python setup.py -V)"
|
setup_version="$(python setup.py -V)"
|
||||||
branch_version="$(Build.SourceBranchName)"
|
branch_version="$(Build.SourceBranchName)"
|
||||||
|
@@ -8,53 +8,20 @@ trigger:
|
|||||||
pr: none
|
pr: none
|
||||||
variables:
|
variables:
|
||||||
- name: versionWheels
|
- name: versionWheels
|
||||||
value: "1.6-3.7-alpine3.11"
|
value: '1.13.0-3.8-alpine3.12'
|
||||||
- group: wheels
|
resources:
|
||||||
|
repositories:
|
||||||
|
- repository: azure
|
||||||
|
type: github
|
||||||
|
name: 'home-assistant/ci-azure'
|
||||||
|
endpoint: 'home-assistant'
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: "Wheels"
|
- template: templates/azp-job-wheels.yaml@azure
|
||||||
timeoutInMinutes: 360
|
parameters:
|
||||||
pool:
|
builderVersion: '$(versionWheels)'
|
||||||
vmImage: "ubuntu-latest"
|
builderApk: 'build-base;libffi-dev;openssl-dev'
|
||||||
strategy:
|
builderPip: 'Cython'
|
||||||
maxParallel: 5
|
skipBinary: 'aiohttp'
|
||||||
matrix:
|
wheelsRequirement: 'requirements.txt'
|
||||||
amd64:
|
|
||||||
buildArch: "amd64"
|
|
||||||
i386:
|
|
||||||
buildArch: "i386"
|
|
||||||
armhf:
|
|
||||||
buildArch: "armhf"
|
|
||||||
armv7:
|
|
||||||
buildArch: "armv7"
|
|
||||||
aarch64:
|
|
||||||
buildArch: "aarch64"
|
|
||||||
steps:
|
|
||||||
- script: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --no-install-recommends \
|
|
||||||
qemu-user-static \
|
|
||||||
binfmt-support \
|
|
||||||
curl
|
|
||||||
|
|
||||||
sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc
|
|
||||||
sudo update-binfmts --enable qemu-arm
|
|
||||||
sudo update-binfmts --enable qemu-aarch64
|
|
||||||
displayName: "Initial cross build"
|
|
||||||
- script: |
|
|
||||||
mkdir -p .ssh
|
|
||||||
echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa
|
|
||||||
ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts
|
|
||||||
chmod 600 .ssh/*
|
|
||||||
displayName: "Install ssh key"
|
|
||||||
- script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels)
|
|
||||||
displayName: "Install wheels builder"
|
|
||||||
- script: |
|
|
||||||
sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \
|
|
||||||
homeassistant/$(buildArch)-wheels:$(versionWheels) \
|
|
||||||
--apk "build-base;libffi-dev;openssl-dev" \
|
|
||||||
--index $(wheelsIndex) \
|
|
||||||
--requirement requirements.txt \
|
|
||||||
--upload rsync \
|
|
||||||
--remote wheels@$(wheelsHost):/opt/wheels
|
|
||||||
displayName: "Run wheels build"
|
|
||||||
|
10
build.json
10
build.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||||
"build_from": {
|
"build_from": {
|
||||||
"aarch64": "homeassistant/aarch64-base-python:3.7-alpine3.11",
|
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12",
|
||||||
"armhf": "homeassistant/armhf-base-python:3.7-alpine3.11",
|
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.12",
|
||||||
"armv7": "homeassistant/armv7-base-python:3.7-alpine3.11",
|
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.12",
|
||||||
"amd64": "homeassistant/amd64-base-python:3.7-alpine3.11",
|
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.12",
|
||||||
"i386": "homeassistant/i386-base-python:3.7-alpine3.11"
|
"i386": "homeassistant/i386-base-python:3.8-alpine3.12"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"io.hass.type": "supervisor"
|
"io.hass.type": "supervisor"
|
||||||
|
11
codecov.yaml
Normal file
11
codecov.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
codecov:
|
||||||
|
branch: dev
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: 40
|
||||||
|
threshold: 0.09
|
||||||
|
comment: false
|
||||||
|
github_checks:
|
||||||
|
annotations: false
|
Submodule home-assistant-polymer updated: 8518f774d4...0c7c536f73
19
pylintrc
19
pylintrc
@@ -1,27 +1,30 @@
|
|||||||
[MASTER]
|
[MASTER]
|
||||||
reports=no
|
reports=no
|
||||||
|
jobs=2
|
||||||
|
|
||||||
|
good-names=id,i,j,k,ex,Run,_,fp,T
|
||||||
|
|
||||||
# Reasons disabled:
|
# Reasons disabled:
|
||||||
|
# format - handled by black
|
||||||
# locally-disabled - it spams too much
|
# locally-disabled - it spams too much
|
||||||
# duplicate-code - unavoidable
|
# duplicate-code - unavoidable
|
||||||
# cyclic-import - doesn't test if both import on load
|
# cyclic-import - doesn't test if both import on load
|
||||||
# abstract-class-little-used - prevents from setting right foundation
|
# abstract-class-little-used - prevents from setting right foundation
|
||||||
# abstract-class-not-used - is flaky, should not show up but does
|
# abstract-class-not-used - is flaky, should not show up but does
|
||||||
# unused-argument - generic callbacks and setup methods create a lot of warnings
|
# unused-argument - generic callbacks and setup methods create a lot of warnings
|
||||||
# global-statement - used for the on-demand requirement installation
|
|
||||||
# redefined-variable-type - this is Python, we're duck typing!
|
# redefined-variable-type - this is Python, we're duck typing!
|
||||||
# too-many-* - are not enforced for the sake of readability
|
# too-many-* - are not enforced for the sake of readability
|
||||||
# too-few-* - same as too-many-*
|
# too-few-* - same as too-many-*
|
||||||
# abstract-method - with intro of async there are always methods missing
|
# abstract-method - with intro of async there are always methods missing
|
||||||
|
|
||||||
disable=
|
disable=
|
||||||
|
format,
|
||||||
abstract-class-little-used,
|
abstract-class-little-used,
|
||||||
abstract-class-not-used,
|
|
||||||
abstract-method,
|
abstract-method,
|
||||||
cyclic-import,
|
cyclic-import,
|
||||||
duplicate-code,
|
duplicate-code,
|
||||||
global-statement,
|
|
||||||
locally-disabled,
|
locally-disabled,
|
||||||
|
no-else-return,
|
||||||
|
no-self-use,
|
||||||
not-context-manager,
|
not-context-manager,
|
||||||
redefined-variable-type,
|
redefined-variable-type,
|
||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
@@ -34,14 +37,6 @@ disable=
|
|||||||
too-many-return-statements,
|
too-many-return-statements,
|
||||||
too-many-statements,
|
too-many-statements,
|
||||||
unused-argument,
|
unused-argument,
|
||||||
line-too-long,
|
|
||||||
bad-continuation,
|
|
||||||
too-few-public-methods,
|
|
||||||
no-self-use,
|
|
||||||
not-async-context-manager,
|
|
||||||
too-many-locals,
|
|
||||||
too-many-branches,
|
|
||||||
no-else-return
|
|
||||||
|
|
||||||
[EXCEPTIONS]
|
[EXCEPTIONS]
|
||||||
overgeneral-exceptions=Exception
|
overgeneral-exceptions=Exception
|
||||||
|
@@ -1,18 +1,19 @@
|
|||||||
aiohttp==3.6.1
|
aiohttp==3.6.2
|
||||||
async_timeout==3.0.1
|
async_timeout==3.0.1
|
||||||
attrs==19.3.0
|
attrs==20.2.0
|
||||||
cchardet==2.1.5
|
cchardet==2.1.6
|
||||||
colorlog==4.1.0
|
colorlog==4.2.1
|
||||||
cpe==1.2.1
|
cpe==1.2.1
|
||||||
cryptography==2.8
|
cryptography==3.1
|
||||||
docker==4.2.0
|
debugpy==1.0.0rc2
|
||||||
gitpython==3.1.0
|
docker==4.3.1
|
||||||
jinja2==2.11.1
|
gitpython==3.1.8
|
||||||
packaging==20.1
|
jinja2==2.11.2
|
||||||
ptvsd==4.3.2
|
packaging==20.4
|
||||||
pulsectl==20.2.2
|
pulsectl==20.5.1
|
||||||
pytz==2019.3
|
pytz==2020.1
|
||||||
pyudev==0.22.0
|
pyudev==0.22.0
|
||||||
ruamel.yaml==0.15.100
|
ruamel.yaml==0.15.100
|
||||||
|
sentry-sdk==0.17.3
|
||||||
uvloop==0.14.0
|
uvloop==0.14.0
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
flake8==3.7.9
|
black==20.8b1
|
||||||
pylint==2.4.4
|
codecov==2.1.9
|
||||||
pytest==5.3.5
|
coverage==5.2.1
|
||||||
pytest-timeout==1.3.4
|
flake8-docstrings==1.5.0
|
||||||
|
flake8==3.8.3
|
||||||
|
pre-commit==2.7.1
|
||||||
|
pydocstyle==5.1.1
|
||||||
|
pylint==2.6.0
|
||||||
pytest-aiohttp==0.3.0
|
pytest-aiohttp==0.3.0
|
||||||
black==19.10b0
|
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
||||||
|
pytest-cov==2.10.1
|
||||||
|
pytest-timeout==1.4.2
|
||||||
|
pytest==6.0.1
|
||||||
|
pyupgrade==2.7.2
|
||||||
|
@@ -4,6 +4,9 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
udevd --daemon
|
udevd --daemon
|
||||||
|
|
||||||
bashio::log.info "Update udev informations"
|
bashio::log.info "Update udev information"
|
||||||
udevadm trigger
|
if udevadm trigger; then
|
||||||
udevadm settle
|
udevadm settle || true
|
||||||
|
else
|
||||||
|
bashio::log.warning "Triggering of udev rules fails!"
|
||||||
|
fi
|
||||||
|
@@ -2,4 +2,4 @@
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Take down the S6 supervision tree when Supervisor fails
|
# Take down the S6 supervision tree when Supervisor fails
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
s6-svscanctl -t /var/run/s6/services
|
redirfd -w 2 /dev/null s6-svscanctl -t /var/run/s6/services
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Start Service service
|
# Start Supervisor service
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
exec python3 -m supervisor
|
export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2"
|
||||||
|
|
||||||
|
exec python3 -m supervisor
|
||||||
|
8
rootfs/etc/services.d/watchdog/finish
Normal file
8
rootfs/etc/services.d/watchdog/finish
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/execlineb -S1
|
||||||
|
# ==============================================================================
|
||||||
|
# Take down the S6 supervision tree when Watchdog fails
|
||||||
|
# ==============================================================================
|
||||||
|
if { s6-test ${1} -ne 0 }
|
||||||
|
if { s6-test ${1} -ne 256 }
|
||||||
|
|
||||||
|
s6-svscanctl -t /var/run/s6/services
|
34
rootfs/etc/services.d/watchdog/run
Normal file
34
rootfs/etc/services.d/watchdog/run
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/with-contenv bashio
|
||||||
|
# ==============================================================================
|
||||||
|
# Start Watchdog service
|
||||||
|
# ==============================================================================
|
||||||
|
declare failed_count=0
|
||||||
|
declare supervisor_state
|
||||||
|
|
||||||
|
bashio::log.info "Start local supervisor watchdog..."
|
||||||
|
|
||||||
|
while [[ failed_count -lt 2 ]];
|
||||||
|
do
|
||||||
|
sleep 300
|
||||||
|
supervisor_state="$(cat /run/supervisor)"
|
||||||
|
|
||||||
|
if [[ "${supervisor_state}" = "running" ]]; then
|
||||||
|
|
||||||
|
# Check API
|
||||||
|
if bashio::supervisor.ping; then
|
||||||
|
failed_count=0
|
||||||
|
else
|
||||||
|
bashio::log.warning "Maybe found an issue on API healthy"
|
||||||
|
((failed_count++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "close stopping" = *"${supervisor_state}"* ]]; then
|
||||||
|
bashio::log.warning "Maybe found an issue on shutdown"
|
||||||
|
((failed_count++))
|
||||||
|
else
|
||||||
|
failed_count=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
basio::exit.nok "Watchdog detect issue with Supervisor - take container down!"
|
@@ -81,11 +81,6 @@ function cleanup_docker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function install_cli() {
|
|
||||||
docker pull homeassistant/amd64-hassio-cli:dev
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function setup_test_env() {
|
function setup_test_env() {
|
||||||
mkdir -p /workspaces/test_supervisor
|
mkdir -p /workspaces/test_supervisor
|
||||||
|
|
||||||
@@ -101,7 +96,7 @@ function setup_test_env() {
|
|||||||
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
||||||
-e SUPERVISOR_NAME=hassio_supervisor \
|
-e SUPERVISOR_NAME=hassio_supervisor \
|
||||||
-e SUPERVISOR_DEV=1 \
|
-e SUPERVISOR_DEV=1 \
|
||||||
-e HOMEASSISTANT_REPOSITORY="homeassistant/qemux86-64-homeassistant" \
|
-e SUPERVISOR_MACHINE="qemux86-64" \
|
||||||
homeassistant/amd64-hassio-supervisor:latest
|
homeassistant/amd64-hassio-supervisor:latest
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -117,8 +112,11 @@ function init_dbus() {
|
|||||||
mkdir -p /var/lib/dbus
|
mkdir -p /var/lib/dbus
|
||||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
||||||
|
|
||||||
# run
|
# cleanups
|
||||||
mkdir -p /run/dbus
|
mkdir -p /run/dbus
|
||||||
|
rm -f /run/dbus/pid
|
||||||
|
|
||||||
|
# run
|
||||||
dbus-daemon --system --print-address
|
dbus-daemon --system --print-address
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +126,6 @@ start_docker
|
|||||||
trap "stop_docker" ERR
|
trap "stop_docker" ERR
|
||||||
|
|
||||||
build_supervisor
|
build_supervisor
|
||||||
install_cli
|
|
||||||
cleanup_lastboot
|
cleanup_lastboot
|
||||||
cleanup_docker
|
cleanup_docker
|
||||||
init_dbus
|
init_dbus
|
||||||
|
@@ -14,5 +14,5 @@ cd hassio
|
|||||||
./script/build_hassio
|
./script/build_hassio
|
||||||
|
|
||||||
# Copy frontend
|
# Copy frontend
|
||||||
rm -f ../../supervisor/hassio/api/panel/chunk.*
|
rm -rf ../../supervisor/api/panel/*
|
||||||
cp -rf build/* ../../supervisor/api/panel/
|
cp -rf build/* ../../supervisor/api/panel/
|
||||||
|
15
setup.cfg
15
setup.cfg
@@ -11,7 +11,20 @@ default_section = THIRDPARTY
|
|||||||
forced_separate = tests
|
forced_separate = tests
|
||||||
combine_as_imports = true
|
combine_as_imports = true
|
||||||
use_parentheses = true
|
use_parentheses = true
|
||||||
|
known_first_party = supervisor,tests
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
|
exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
|
||||||
|
doctests = True
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
ignore = E501, W503
|
# E501: line too long
|
||||||
|
# W503: Line break occurred before a binary operator
|
||||||
|
# E203: Whitespace before ':'
|
||||||
|
# D202 No blank lines allowed after function docstring
|
||||||
|
# W504 line break after binary operator
|
||||||
|
ignore =
|
||||||
|
E501,
|
||||||
|
W503,
|
||||||
|
E203,
|
||||||
|
D202,
|
||||||
|
W504
|
||||||
|
13
setup.py
13
setup.py
@@ -25,7 +25,7 @@ setup(
|
|||||||
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.8",
|
||||||
],
|
],
|
||||||
keywords=["docker", "home-assistant", "api"],
|
keywords=["docker", "home-assistant", "api"],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
@@ -35,9 +35,20 @@ setup(
|
|||||||
"supervisor.docker",
|
"supervisor.docker",
|
||||||
"supervisor.addons",
|
"supervisor.addons",
|
||||||
"supervisor.api",
|
"supervisor.api",
|
||||||
|
"supervisor.dbus",
|
||||||
|
"supervisor.dbus.payloads",
|
||||||
|
"supervisor.dbus.network",
|
||||||
|
"supervisor.discovery",
|
||||||
|
"supervisor.discovery.services",
|
||||||
|
"supervisor.services",
|
||||||
|
"supervisor.services.modules",
|
||||||
|
"supervisor.homeassistant",
|
||||||
|
"supervisor.host",
|
||||||
"supervisor.misc",
|
"supervisor.misc",
|
||||||
"supervisor.utils",
|
"supervisor.utils",
|
||||||
|
"supervisor.plugins",
|
||||||
"supervisor.snapshots",
|
"supervisor.snapshots",
|
||||||
|
"supervisor.store",
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
@@ -30,8 +30,7 @@ if __name__ == "__main__":
|
|||||||
loop = initialize_event_loop()
|
loop = initialize_event_loop()
|
||||||
|
|
||||||
# Check if all information are available to setup Supervisor
|
# Check if all information are available to setup Supervisor
|
||||||
if not bootstrap.check_environment():
|
bootstrap.check_environment()
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# init executor pool
|
# init executor pool
|
||||||
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
executor = ThreadPoolExecutor(thread_name_prefix="SyncWorker")
|
||||||
@@ -48,15 +47,12 @@ if __name__ == "__main__":
|
|||||||
loop.run_until_complete(coresys.core.setup())
|
loop.run_until_complete(coresys.core.setup())
|
||||||
|
|
||||||
loop.call_soon_threadsafe(loop.create_task, coresys.core.start())
|
loop.call_soon_threadsafe(loop.create_task, coresys.core.start())
|
||||||
loop.call_soon_threadsafe(bootstrap.reg_signal, loop)
|
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, coresys)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Run Supervisor")
|
_LOGGER.info("Run Supervisor")
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
_LOGGER.info("Stopping Supervisor")
|
|
||||||
loop.run_until_complete(coresys.core.stop())
|
|
||||||
executor.shutdown(wait=False)
|
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
_LOGGER.info("Close Supervisor")
|
_LOGGER.info("Close Supervisor")
|
||||||
|
@@ -5,7 +5,7 @@ import logging
|
|||||||
import tarfile
|
import tarfile
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
|
|
||||||
from ..const import BOOT_AUTO, STATE_STARTED
|
from ..const import BOOT_AUTO, AddonStartup, AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
AddonsError,
|
AddonsError,
|
||||||
@@ -37,7 +37,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
@property
|
@property
|
||||||
def all(self) -> List[AnyAddon]:
|
def all(self) -> List[AnyAddon]:
|
||||||
"""Return a list of all add-ons."""
|
"""Return a list of all add-ons."""
|
||||||
addons = {**self.store, **self.local}
|
addons: Dict[str, AnyAddon] = {**self.store, **self.local}
|
||||||
return list(addons.values())
|
return list(addons.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -45,7 +45,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Return a list of all installed add-ons."""
|
"""Return a list of all installed add-ons."""
|
||||||
return list(self.local.values())
|
return list(self.local.values())
|
||||||
|
|
||||||
def get(self, addon_slug: str) -> Optional[AnyAddon]:
|
def get(self, addon_slug: str, local_only: bool = False) -> Optional[AnyAddon]:
|
||||||
"""Return an add-on from slug.
|
"""Return an add-on from slug.
|
||||||
|
|
||||||
Prio:
|
Prio:
|
||||||
@@ -54,12 +54,14 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""
|
"""
|
||||||
if addon_slug in self.local:
|
if addon_slug in self.local:
|
||||||
return self.local[addon_slug]
|
return self.local[addon_slug]
|
||||||
return self.store.get(addon_slug)
|
if not local_only:
|
||||||
|
return self.store.get(addon_slug)
|
||||||
|
return None
|
||||||
|
|
||||||
def from_token(self, token: str) -> Optional[Addon]:
|
def from_token(self, token: str) -> Optional[Addon]:
|
||||||
"""Return an add-on from Supervisor token."""
|
"""Return an add-on from Supervisor token."""
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if token == addon.hassio_token:
|
if token == addon.supervisor_token:
|
||||||
return addon
|
return addon
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -78,30 +80,51 @@ class AddonManager(CoreSysAttributes):
|
|||||||
# Sync DNS
|
# Sync DNS
|
||||||
await self.sync_dns()
|
await self.sync_dns()
|
||||||
|
|
||||||
async def boot(self, stage: str) -> None:
|
async def boot(self, stage: AddonStartup) -> None:
|
||||||
"""Boot add-ons with mode auto."""
|
"""Boot add-ons with mode auto."""
|
||||||
tasks = []
|
tasks: List[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if addon.boot != BOOT_AUTO or addon.startup != stage:
|
if addon.boot != BOOT_AUTO or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
tasks.append(addon.start())
|
tasks.append(addon)
|
||||||
|
|
||||||
|
# Evaluate add-ons which need to be started
|
||||||
_LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' start %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if not tasks:
|
||||||
await asyncio.wait(tasks)
|
return
|
||||||
await asyncio.sleep(self.sys_config.wait_boot)
|
|
||||||
|
|
||||||
async def shutdown(self, stage: str) -> None:
|
# Start Add-ons sequential
|
||||||
|
# avoid issue on slow IO
|
||||||
|
for addon in tasks:
|
||||||
|
try:
|
||||||
|
await addon.start()
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
_LOGGER.warning("Can't start Add-on %s: %s", addon.slug, err)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
|
||||||
|
await asyncio.sleep(self.sys_config.wait_boot)
|
||||||
|
|
||||||
|
async def shutdown(self, stage: AddonStartup) -> None:
|
||||||
"""Shutdown addons."""
|
"""Shutdown addons."""
|
||||||
tasks = []
|
tasks: List[Addon] = []
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if await addon.state() != STATE_STARTED or addon.startup != stage:
|
if addon.state != AddonState.STARTED or addon.startup != stage:
|
||||||
continue
|
continue
|
||||||
tasks.append(addon.stop())
|
tasks.append(addon)
|
||||||
|
|
||||||
|
# Evaluate add-ons which need to be stopped
|
||||||
_LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks))
|
_LOGGER.info("Phase '%s' stop %d add-ons", stage, len(tasks))
|
||||||
if tasks:
|
if not tasks:
|
||||||
await asyncio.wait(tasks)
|
return
|
||||||
|
|
||||||
|
# Stop Add-ons sequential
|
||||||
|
# avoid issue on slow IO
|
||||||
|
for addon in tasks:
|
||||||
|
try:
|
||||||
|
await addon.stop()
|
||||||
|
except Exception as err: # pylint: disable=broad-except
|
||||||
|
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
|
||||||
async def install(self, slug: str) -> None:
|
async def install(self, slug: str) -> None:
|
||||||
"""Install an add-on."""
|
"""Install an add-on."""
|
||||||
@@ -130,24 +153,31 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await addon.instance.install(store.version, store.image)
|
await addon.instance.install(store.version, store.image)
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
self.data.uninstall(addon)
|
self.data.uninstall(addon)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
else:
|
else:
|
||||||
self.local[slug] = addon
|
self.local[slug] = addon
|
||||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
|
||||||
|
# Reload ingress tokens
|
||||||
|
if addon.with_ingress:
|
||||||
|
await self.sys_ingress.reload()
|
||||||
|
|
||||||
|
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||||
|
|
||||||
async def uninstall(self, slug: str) -> None:
|
async def uninstall(self, slug: str) -> None:
|
||||||
"""Remove an add-on."""
|
"""Remove an add-on."""
|
||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.warning("Add-on %s is not installed", slug)
|
_LOGGER.warning("Add-on %s is not installed", slug)
|
||||||
return
|
return
|
||||||
addon = self.local.get(slug)
|
addon = self.local[slug]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
else:
|
||||||
|
addon.state = AddonState.UNKNOWN
|
||||||
|
|
||||||
await addon.remove_data()
|
await addon.remove_data()
|
||||||
|
|
||||||
@@ -166,6 +196,11 @@ class AddonManager(CoreSysAttributes):
|
|||||||
with suppress(HomeAssistantAPIError):
|
with suppress(HomeAssistantAPIError):
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
|
||||||
|
# Cleanup Ingress dynamic port assignment
|
||||||
|
if addon.with_ingress:
|
||||||
|
self.sys_create_task(self.sys_ingress.reload())
|
||||||
|
self.sys_ingress.del_dynamic_port(slug)
|
||||||
|
|
||||||
# Cleanup discovery data
|
# Cleanup discovery data
|
||||||
for message in self.sys_discovery.list_messages:
|
for message in self.sys_discovery.list_messages:
|
||||||
if message.addon != addon.slug:
|
if message.addon != addon.slug:
|
||||||
@@ -188,12 +223,12 @@ class AddonManager(CoreSysAttributes):
|
|||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.error("Add-on %s is not installed", slug)
|
_LOGGER.error("Add-on %s is not installed", slug)
|
||||||
raise AddonsError()
|
raise AddonsError()
|
||||||
addon = self.local.get(slug)
|
addon = self.local[slug]
|
||||||
|
|
||||||
if addon.is_detached:
|
if addon.is_detached:
|
||||||
_LOGGER.error("Add-on %s is not available inside store", slug)
|
_LOGGER.error("Add-on %s is not available inside store", slug)
|
||||||
raise AddonsError()
|
raise AddonsError()
|
||||||
store = self.store.get(slug)
|
store = self.store[slug]
|
||||||
|
|
||||||
if addon.version == store.version:
|
if addon.version == store.version:
|
||||||
_LOGGER.warning("No update available for add-on %s", slug)
|
_LOGGER.warning("No update available for add-on %s", slug)
|
||||||
@@ -205,15 +240,15 @@ class AddonManager(CoreSysAttributes):
|
|||||||
raise AddonsNotSupportedError()
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
# Update instance
|
# Update instance
|
||||||
last_state = await addon.state()
|
last_state: AddonState = addon.state
|
||||||
try:
|
try:
|
||||||
await addon.instance.update(store.version, store.image)
|
await addon.instance.update(store.version, store.image)
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerAPIError):
|
||||||
await addon.instance.cleanup()
|
await addon.instance.cleanup()
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
else:
|
else:
|
||||||
self.data.update(store)
|
self.data.update(store)
|
||||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||||
@@ -222,7 +257,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.install_apparmor()
|
await addon.install_apparmor()
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == AddonState.STARTED:
|
||||||
await addon.start()
|
await addon.start()
|
||||||
|
|
||||||
async def rebuild(self, slug: str) -> None:
|
async def rebuild(self, slug: str) -> None:
|
||||||
@@ -230,12 +265,12 @@ class AddonManager(CoreSysAttributes):
|
|||||||
if slug not in self.local:
|
if slug not in self.local:
|
||||||
_LOGGER.error("Add-on %s is not installed", slug)
|
_LOGGER.error("Add-on %s is not installed", slug)
|
||||||
raise AddonsError()
|
raise AddonsError()
|
||||||
addon = self.local.get(slug)
|
addon = self.local[slug]
|
||||||
|
|
||||||
if addon.is_detached:
|
if addon.is_detached:
|
||||||
_LOGGER.error("Add-on %s is not available inside store", slug)
|
_LOGGER.error("Add-on %s is not available inside store", slug)
|
||||||
raise AddonsError()
|
raise AddonsError()
|
||||||
store = self.store.get(slug)
|
store = self.store[slug]
|
||||||
|
|
||||||
# Check if a rebuild is possible now
|
# Check if a rebuild is possible now
|
||||||
if addon.version != store.version:
|
if addon.version != store.version:
|
||||||
@@ -246,18 +281,18 @@ class AddonManager(CoreSysAttributes):
|
|||||||
raise AddonsNotSupportedError()
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
# remove docker container but not addon config
|
# remove docker container but not addon config
|
||||||
last_state = await addon.state()
|
last_state: AddonState = addon.state
|
||||||
try:
|
try:
|
||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
await addon.instance.install(addon.version)
|
await addon.instance.install(addon.version)
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
else:
|
else:
|
||||||
self.data.update(store)
|
self.data.update(store)
|
||||||
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
_LOGGER.info("Add-on '%s' successfully rebuilt", slug)
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == AddonState.STARTED:
|
||||||
await addon.start()
|
await addon.start()
|
||||||
|
|
||||||
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||||
@@ -278,6 +313,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
|
|
||||||
# Update ingress
|
# Update ingress
|
||||||
if addon.with_ingress:
|
if addon.with_ingress:
|
||||||
|
await self.sys_ingress.reload()
|
||||||
with suppress(HomeAssistantAPIError):
|
with suppress(HomeAssistantAPIError):
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
|
||||||
@@ -323,12 +359,18 @@ class AddonManager(CoreSysAttributes):
|
|||||||
"""Sync add-ons DNS names."""
|
"""Sync add-ons DNS names."""
|
||||||
# Update hosts
|
# Update hosts
|
||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if not await addon.instance.is_running():
|
try:
|
||||||
continue
|
if not await addon.instance.is_running():
|
||||||
self.sys_dns.add_host(
|
continue
|
||||||
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
except DockerAPIError as err:
|
||||||
)
|
_LOGGER.warning("Add-on %s is corrupt: %s", addon.slug, err)
|
||||||
|
self.sys_core.healthy = False
|
||||||
|
self.sys_capture_exception(err)
|
||||||
|
else:
|
||||||
|
self.sys_plugins.dns.add_host(
|
||||||
|
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
||||||
|
)
|
||||||
|
|
||||||
# Write hosts files
|
# Write hosts files
|
||||||
with suppress(CoreDNSError):
|
with suppress(CoreDNSError):
|
||||||
self.sys_dns.write_hosts()
|
self.sys_plugins.dns.write_hosts()
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
|
import asyncio
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
@@ -11,6 +12,7 @@ import tarfile
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Awaitable, Dict, List, Optional
|
from typing import Any, Awaitable, Dict, List, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
@@ -35,23 +37,26 @@ from ..const import (
|
|||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
DNS_SUFFIX,
|
DNS_SUFFIX,
|
||||||
STATE_STARTED,
|
AddonStartup,
|
||||||
STATE_STOPPED,
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..docker.addon import DockerAddon
|
from ..docker.addon import DockerAddon
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import (
|
from ..exceptions import (
|
||||||
|
AddonConfigurationError,
|
||||||
AddonsError,
|
AddonsError,
|
||||||
AddonsNotSupportedError,
|
AddonsNotSupportedError,
|
||||||
DockerAPIError,
|
DockerAPIError,
|
||||||
HostAppArmorError,
|
HostAppArmorError,
|
||||||
JsonFileError,
|
JsonFileError,
|
||||||
)
|
)
|
||||||
|
from ..utils import check_port
|
||||||
from ..utils.apparmor import adjust_profile
|
from ..utils.apparmor import adjust_profile
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
from ..utils.tar import exclude_filter, secure_path
|
from ..utils.tar import atomic_contents_add, secure_path
|
||||||
from .model import AddonModel, Data
|
from .model import AddonModel, Data
|
||||||
from .utils import remove_data
|
from .utils import remove_data
|
||||||
from .validate import SCHEMA_ADDON_SNAPSHOT, validate_options
|
from .validate import SCHEMA_ADDON_SNAPSHOT, validate_options
|
||||||
@@ -63,24 +68,48 @@ RE_WEBUI = re.compile(
|
|||||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RE_WATCHDOG = re.compile(
|
||||||
|
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
|
||||||
|
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||||
|
|
||||||
|
WATCHDOG_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
|
||||||
class Addon(AddonModel):
|
class Addon(AddonModel):
|
||||||
"""Hold data for add-on inside Supervisor."""
|
"""Hold data for add-on inside Supervisor."""
|
||||||
|
|
||||||
def __init__(self, coresys: CoreSys, slug: str):
|
def __init__(self, coresys: CoreSys, slug: str):
|
||||||
"""Initialize data holder."""
|
"""Initialize data holder."""
|
||||||
self.coresys: CoreSys = coresys
|
super().__init__(coresys, slug)
|
||||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||||
self.slug: str = slug
|
self.state: AddonState = AddonState.UNKNOWN
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Return internal representation."""
|
||||||
|
return f"<Addon: {self.slug}>"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_progress(self) -> bool:
|
||||||
|
"""Return True if a task is in progress."""
|
||||||
|
return self.instance.in_progress
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Async initialize of object."""
|
"""Async initialize of object."""
|
||||||
with suppress(DockerAPIError):
|
with suppress(DockerAPIError):
|
||||||
await self.instance.attach(tag=self.version)
|
await self.instance.attach(tag=self.version)
|
||||||
|
|
||||||
|
# Evaluate state
|
||||||
|
if await self.instance.is_running():
|
||||||
|
self.state = AddonState.STARTED
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STOPPED
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
"""Return IP of Add-on instance."""
|
"""Return IP of add-on instance."""
|
||||||
return self.instance.ip_address
|
return self.instance.ip_address
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -129,12 +158,9 @@ class Addon(AddonModel):
|
|||||||
return {**self.data[ATTR_OPTIONS], **self.persist[ATTR_OPTIONS]}
|
return {**self.data[ATTR_OPTIONS], **self.persist[ATTR_OPTIONS]}
|
||||||
|
|
||||||
@options.setter
|
@options.setter
|
||||||
def options(self, value: Optional[Dict[str, Any]]):
|
def options(self, value: Optional[Dict[str, Any]]) -> None:
|
||||||
"""Store user add-on options."""
|
"""Store user add-on options."""
|
||||||
if value is None:
|
self.persist[ATTR_OPTIONS] = {} if value is None else deepcopy(value)
|
||||||
self.persist[ATTR_OPTIONS] = {}
|
|
||||||
else:
|
|
||||||
self.persist[ATTR_OPTIONS] = deepcopy(value)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boot(self) -> bool:
|
def boot(self) -> bool:
|
||||||
@@ -142,7 +168,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_BOOT, super().boot)
|
return self.persist.get(ATTR_BOOT, super().boot)
|
||||||
|
|
||||||
@boot.setter
|
@boot.setter
|
||||||
def boot(self, value: bool):
|
def boot(self, value: bool) -> None:
|
||||||
"""Store user boot options."""
|
"""Store user boot options."""
|
||||||
self.persist[ATTR_BOOT] = value
|
self.persist[ATTR_BOOT] = value
|
||||||
|
|
||||||
@@ -152,17 +178,32 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_AUTO_UPDATE, super().auto_update)
|
return self.persist.get(ATTR_AUTO_UPDATE, super().auto_update)
|
||||||
|
|
||||||
@auto_update.setter
|
@auto_update.setter
|
||||||
def auto_update(self, value: bool):
|
def auto_update(self, value: bool) -> None:
|
||||||
"""Set auto update."""
|
"""Set auto update."""
|
||||||
self.persist[ATTR_AUTO_UPDATE] = value
|
self.persist[ATTR_AUTO_UPDATE] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watchdog(self) -> bool:
|
||||||
|
"""Return True if watchdog is enable."""
|
||||||
|
return self.persist[ATTR_WATCHDOG]
|
||||||
|
|
||||||
|
@watchdog.setter
|
||||||
|
def watchdog(self, value: bool) -> None:
|
||||||
|
"""Set watchdog enable/disable."""
|
||||||
|
if value and self.startup == AddonStartup.ONCE:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Ignoring watchdog for %s because startup type is 'once'", self.slug
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.persist[ATTR_WATCHDOG] = value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self) -> str:
|
def uuid(self) -> str:
|
||||||
"""Return an API token for this add-on."""
|
"""Return an API token for this add-on."""
|
||||||
return self.persist[ATTR_UUID]
|
return self.persist[ATTR_UUID]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassio_token(self) -> Optional[str]:
|
def supervisor_token(self) -> Optional[str]:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return self.persist.get(ATTR_ACCESS_TOKEN)
|
return self.persist.get(ATTR_ACCESS_TOKEN)
|
||||||
|
|
||||||
@@ -189,7 +230,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_PROTECTED]
|
return self.persist[ATTR_PROTECTED]
|
||||||
|
|
||||||
@protected.setter
|
@protected.setter
|
||||||
def protected(self, value: bool):
|
def protected(self, value: bool) -> None:
|
||||||
"""Set add-on in protected mode."""
|
"""Set add-on in protected mode."""
|
||||||
self.persist[ATTR_PROTECTED] = value
|
self.persist[ATTR_PROTECTED] = value
|
||||||
|
|
||||||
@@ -199,7 +240,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist.get(ATTR_NETWORK, super().ports)
|
return self.persist.get(ATTR_NETWORK, super().ports)
|
||||||
|
|
||||||
@ports.setter
|
@ports.setter
|
||||||
def ports(self, value: Optional[Dict[str, Optional[int]]]):
|
def ports(self, value: Optional[Dict[str, Optional[int]]]) -> None:
|
||||||
"""Set custom ports of add-on."""
|
"""Set custom ports of add-on."""
|
||||||
if value is None:
|
if value is None:
|
||||||
self.persist.pop(ATTR_NETWORK, None)
|
self.persist.pop(ATTR_NETWORK, None)
|
||||||
@@ -244,10 +285,6 @@ class Addon(AddonModel):
|
|||||||
else:
|
else:
|
||||||
port = self.ports.get(f"{t_port}/tcp", t_port)
|
port = self.ports.get(f"{t_port}/tcp", t_port)
|
||||||
|
|
||||||
# for interface config or port lists
|
|
||||||
if isinstance(port, (tuple, list)):
|
|
||||||
port = port[-1]
|
|
||||||
|
|
||||||
# lookup the correct protocol from config
|
# lookup the correct protocol from config
|
||||||
if t_proto:
|
if t_proto:
|
||||||
proto = "https" if self.options.get(t_proto) else "http"
|
proto = "https" if self.options.get(t_proto) else "http"
|
||||||
@@ -273,7 +310,7 @@ class Addon(AddonModel):
|
|||||||
return self.persist[ATTR_INGRESS_PANEL]
|
return self.persist[ATTR_INGRESS_PANEL]
|
||||||
|
|
||||||
@ingress_panel.setter
|
@ingress_panel.setter
|
||||||
def ingress_panel(self, value: bool):
|
def ingress_panel(self, value: bool) -> None:
|
||||||
"""Return True if the add-on access support ingress."""
|
"""Return True if the add-on access support ingress."""
|
||||||
self.persist[ATTR_INGRESS_PANEL] = value
|
self.persist[ATTR_INGRESS_PANEL] = value
|
||||||
|
|
||||||
@@ -282,84 +319,132 @@ class Addon(AddonModel):
|
|||||||
"""Return a pulse profile for output or None."""
|
"""Return a pulse profile for output or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
return self.persist.get(ATTR_AUDIO_OUTPUT)
|
|
||||||
|
# Fallback with old audio settings
|
||||||
|
# Remove after 210
|
||||||
|
output_data = self.persist.get(ATTR_AUDIO_OUTPUT)
|
||||||
|
if output_data and RE_OLD_AUDIO.fullmatch(output_data):
|
||||||
|
return None
|
||||||
|
return output_data
|
||||||
|
|
||||||
@audio_output.setter
|
@audio_output.setter
|
||||||
def audio_output(self, value: Optional[str]):
|
def audio_output(self, value: Optional[str]):
|
||||||
"""Set/reset audio output profile settings."""
|
"""Set audio output profile settings."""
|
||||||
if value is None:
|
self.persist[ATTR_AUDIO_OUTPUT] = value
|
||||||
self.persist.pop(ATTR_AUDIO_OUTPUT, None)
|
|
||||||
else:
|
|
||||||
self.persist[ATTR_AUDIO_OUTPUT] = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_input(self) -> Optional[str]:
|
def audio_input(self) -> Optional[str]:
|
||||||
"""Return pulse profile for input or None."""
|
"""Return pulse profile for input or None."""
|
||||||
if not self.with_audio:
|
if not self.with_audio:
|
||||||
return None
|
return None
|
||||||
return self.persist.get(ATTR_AUDIO_INPUT)
|
|
||||||
|
# Fallback with old audio settings
|
||||||
|
# Remove after 210
|
||||||
|
input_data = self.persist.get(ATTR_AUDIO_INPUT)
|
||||||
|
if input_data and RE_OLD_AUDIO.fullmatch(input_data):
|
||||||
|
return None
|
||||||
|
return input_data
|
||||||
|
|
||||||
@audio_input.setter
|
@audio_input.setter
|
||||||
def audio_input(self, value: Optional[str]):
|
def audio_input(self, value: Optional[str]) -> None:
|
||||||
"""Set/reset audio input settings."""
|
"""Set audio input settings."""
|
||||||
if value is None:
|
self.persist[ATTR_AUDIO_INPUT] = value
|
||||||
self.persist.pop(ATTR_AUDIO_INPUT, None)
|
|
||||||
else:
|
|
||||||
self.persist[ATTR_AUDIO_INPUT] = value
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self):
|
def image(self) -> Optional[str]:
|
||||||
"""Return image name of add-on."""
|
"""Return image name of add-on."""
|
||||||
return self.persist.get(ATTR_IMAGE)
|
return self.persist.get(ATTR_IMAGE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def need_build(self):
|
def need_build(self) -> bool:
|
||||||
"""Return True if this add-on need a local build."""
|
"""Return True if this add-on need a local build."""
|
||||||
return ATTR_IMAGE not in self.data
|
return ATTR_IMAGE not in self.data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_data(self):
|
def path_data(self) -> Path:
|
||||||
"""Return add-on data path inside Supervisor."""
|
"""Return add-on data path inside Supervisor."""
|
||||||
return Path(self.sys_config.path_addons_data, self.slug)
|
return Path(self.sys_config.path_addons_data, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_data(self):
|
def path_extern_data(self) -> PurePath:
|
||||||
"""Return add-on data path external for Docker."""
|
"""Return add-on data path external for Docker."""
|
||||||
return PurePath(self.sys_config.path_extern_addons_data, self.slug)
|
return PurePath(self.sys_config.path_extern_addons_data, self.slug)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_options(self):
|
def path_options(self) -> Path:
|
||||||
"""Return path to add-on options."""
|
"""Return path to add-on options."""
|
||||||
return Path(self.path_data, "options.json")
|
return Path(self.path_data, "options.json")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_pulse(self):
|
def path_pulse(self) -> Path:
|
||||||
"""Return path to asound config."""
|
"""Return path to asound config."""
|
||||||
return Path(self.sys_config.path_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_pulse(self):
|
def path_extern_pulse(self) -> Path:
|
||||||
"""Return path to asound config for Docker."""
|
"""Return path to asound config for Docker."""
|
||||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||||
|
|
||||||
def save_persist(self):
|
def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
self.sys_addons.data.save_data()
|
||||||
|
|
||||||
async def write_options(self):
|
async def watchdog_application(self) -> bool:
|
||||||
|
"""Return True if application is running."""
|
||||||
|
url = super().watchdog
|
||||||
|
if not url:
|
||||||
|
return True
|
||||||
|
application = RE_WATCHDOG.match(url)
|
||||||
|
|
||||||
|
# extract arguments
|
||||||
|
t_port = application.group("t_port")
|
||||||
|
t_proto = application.group("t_proto")
|
||||||
|
s_prefix = application.group("s_prefix") or ""
|
||||||
|
s_suffix = application.group("s_suffix") or ""
|
||||||
|
|
||||||
|
# search host port for this docker port
|
||||||
|
if self.host_network:
|
||||||
|
port = self.ports.get(f"{t_port}/tcp", t_port)
|
||||||
|
else:
|
||||||
|
port = t_port
|
||||||
|
|
||||||
|
# TCP monitoring
|
||||||
|
if s_prefix == "tcp":
|
||||||
|
return await self.sys_run_in_executor(check_port, self.ip_address, port)
|
||||||
|
|
||||||
|
# lookup the correct protocol from config
|
||||||
|
if t_proto:
|
||||||
|
proto = "https" if self.options.get(t_proto) else "http"
|
||||||
|
else:
|
||||||
|
proto = s_prefix
|
||||||
|
|
||||||
|
# Make HTTP request
|
||||||
|
try:
|
||||||
|
url = f"{proto}://{self.ip_address}:{port}{s_suffix}"
|
||||||
|
async with self.sys_websession_ssl.get(
|
||||||
|
url, timeout=WATCHDOG_TIMEOUT
|
||||||
|
) as req:
|
||||||
|
if req.status < 300:
|
||||||
|
return True
|
||||||
|
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def write_options(self) -> None:
|
||||||
"""Return True if add-on options is written to data."""
|
"""Return True if add-on options is written to data."""
|
||||||
schema = self.schema
|
schema = self.schema
|
||||||
options = self.options
|
options = self.options
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
options = schema(options)
|
options = schema(options)
|
||||||
write_json_file(self.path_options, options)
|
write_json_file(self.path_options, options)
|
||||||
except vol.Invalid as ex:
|
except vol.Invalid as ex:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Add-on %s have wrong options: %s",
|
"Add-on %s has invalid options: %s",
|
||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(options, ex),
|
humanize_error(options, ex),
|
||||||
)
|
)
|
||||||
@@ -369,9 +454,9 @@ class Addon(AddonModel):
|
|||||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||||
return
|
return
|
||||||
|
|
||||||
raise AddonsError()
|
raise AddonConfigurationError()
|
||||||
|
|
||||||
async def remove_data(self):
|
async def remove_data(self) -> None:
|
||||||
"""Remove add-on data."""
|
"""Remove add-on data."""
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
return
|
return
|
||||||
@@ -379,12 +464,17 @@ class Addon(AddonModel):
|
|||||||
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
_LOGGER.info("Remove add-on data folder %s", self.path_data)
|
||||||
await remove_data(self.path_data)
|
await remove_data(self.path_data)
|
||||||
|
|
||||||
def write_pulse(self):
|
def write_pulse(self) -> None:
|
||||||
"""Write asound config to file and return True on success."""
|
"""Write asound config to file and return True on success."""
|
||||||
pulse_config = self.sys_audio.pulse_client(
|
pulse_config = self.sys_plugins.audio.pulse_client(
|
||||||
input_profile=self.audio_input, output_profile=self.audio_output
|
input_profile=self.audio_input, output_profile=self.audio_output
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cleanup wrong maps
|
||||||
|
if self.path_pulse.is_dir():
|
||||||
|
shutil.rmtree(self.path_pulse, ignore_errors=True)
|
||||||
|
|
||||||
|
# Write pulse config
|
||||||
try:
|
try:
|
||||||
with self.path_pulse.open("w") as config_file:
|
with self.path_pulse.open("w") as config_file:
|
||||||
config_file.write(pulse_config)
|
config_file.write(pulse_config)
|
||||||
@@ -392,11 +482,10 @@ class Addon(AddonModel):
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
"Add-on %s can't write pulse/client.config: %s", self.slug, err
|
||||||
)
|
)
|
||||||
raise AddonsError()
|
else:
|
||||||
|
_LOGGER.debug(
|
||||||
_LOGGER.debug(
|
"Add-on %s write pulse/client.config: %s", self.slug, self.path_pulse
|
||||||
"Add-on %s write pulse/client.config: %s", self.slug, self.path_pulse
|
)
|
||||||
)
|
|
||||||
|
|
||||||
async def install_apparmor(self) -> None:
|
async def install_apparmor(self) -> None:
|
||||||
"""Install or Update AppArmor profile for Add-on."""
|
"""Install or Update AppArmor profile for Add-on."""
|
||||||
@@ -451,12 +540,6 @@ class Addon(AddonModel):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def state(self) -> str:
|
|
||||||
"""Return running state of add-on."""
|
|
||||||
if await self.instance.is_running():
|
|
||||||
return STATE_STARTED
|
|
||||||
return STATE_STOPPED
|
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Set options and start add-on."""
|
"""Set options and start add-on."""
|
||||||
if await self.instance.is_running():
|
if await self.instance.is_running():
|
||||||
@@ -477,15 +560,21 @@ class Addon(AddonModel):
|
|||||||
# Start Add-on
|
# Start Add-on
|
||||||
try:
|
try:
|
||||||
await self.instance.run()
|
await self.instance.run()
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError(err) from err
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STARTED
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
try:
|
try:
|
||||||
return await self.instance.stop()
|
return await self.instance.stop()
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
self.state = AddonState.ERROR
|
||||||
|
raise AddonsError() from err
|
||||||
|
else:
|
||||||
|
self.state = AddonState.STOPPED
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
@@ -500,72 +589,83 @@ class Addon(AddonModel):
|
|||||||
"""
|
"""
|
||||||
return self.instance.logs()
|
return self.instance.logs()
|
||||||
|
|
||||||
|
def is_running(self) -> Awaitable[bool]:
|
||||||
|
"""Return True if Docker container is running.
|
||||||
|
|
||||||
|
Return a coroutine.
|
||||||
|
"""
|
||||||
|
return self.instance.is_running()
|
||||||
|
|
||||||
async def stats(self) -> DockerStats:
|
async def stats(self) -> DockerStats:
|
||||||
"""Return stats of container."""
|
"""Return stats of container."""
|
||||||
try:
|
try:
|
||||||
return await self.instance.stats()
|
return await self.instance.stats()
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
async def write_stdin(self, data):
|
async def write_stdin(self, data) -> None:
|
||||||
"""Write data to add-on stdin.
|
"""Write data to add-on stdin.
|
||||||
|
|
||||||
Return a coroutine.
|
Return a coroutine.
|
||||||
"""
|
"""
|
||||||
if not self.with_stdin:
|
if not self.with_stdin:
|
||||||
_LOGGER.error("Add-on don't support write to stdin!")
|
_LOGGER.error("Add-on %s does not support writing to stdin!", self.slug)
|
||||||
raise AddonsNotSupportedError()
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.instance.write_stdin(data)
|
return await self.instance.write_stdin(data)
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
async def snapshot(self, tar_file: tarfile.TarFile) -> None:
|
||||||
"""Snapshot state of an add-on."""
|
"""Snapshot state of an add-on."""
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||||
|
temp_path = Path(temp)
|
||||||
|
|
||||||
# store local image
|
# store local image
|
||||||
if self.need_build:
|
if self.need_build:
|
||||||
try:
|
try:
|
||||||
await self.instance.export_image(Path(temp, "image.tar"))
|
await self.instance.export_image(temp_path.joinpath("image.tar"))
|
||||||
except DockerAPIError:
|
except DockerAPIError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_USER: self.persist,
|
ATTR_USER: self.persist,
|
||||||
ATTR_SYSTEM: self.data,
|
ATTR_SYSTEM: self.data,
|
||||||
ATTR_VERSION: self.version,
|
ATTR_VERSION: self.version,
|
||||||
ATTR_STATE: await self.state(),
|
ATTR_STATE: self.state,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store local configs/state
|
# Store local configs/state
|
||||||
try:
|
try:
|
||||||
write_json_file(Path(temp, "addon.json"), data)
|
write_json_file(temp_path.joinpath("addon.json"), data)
|
||||||
except JsonFileError:
|
except JsonFileError as err:
|
||||||
_LOGGER.error("Can't save meta for %s", self.slug)
|
_LOGGER.error("Can't save meta for %s", self.slug)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Store AppArmor Profile
|
# Store AppArmor Profile
|
||||||
if self.sys_host.apparmor.exists(self.slug):
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
profile = Path(temp, "apparmor.txt")
|
profile = temp_path.joinpath("apparmor.txt")
|
||||||
try:
|
try:
|
||||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||||
except HostAppArmorError:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error("Can't backup AppArmor profile")
|
_LOGGER.error("Can't backup AppArmor profile")
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# write into tarfile
|
# write into tarfile
|
||||||
def _write_tarfile():
|
def _write_tarfile():
|
||||||
"""Write tar inside loop."""
|
"""Write tar inside loop."""
|
||||||
with tar_file as snapshot:
|
with tar_file as snapshot:
|
||||||
# Snapshot system
|
# Snapshot system
|
||||||
|
|
||||||
snapshot.add(temp, arcname=".")
|
snapshot.add(temp, arcname=".")
|
||||||
|
|
||||||
# Snapshot data
|
# Snapshot data
|
||||||
snapshot.add(
|
atomic_contents_add(
|
||||||
|
snapshot,
|
||||||
self.path_data,
|
self.path_data,
|
||||||
|
excludes=self.snapshot_exclude,
|
||||||
arcname="data",
|
arcname="data",
|
||||||
filter=exclude_filter(self.snapshot_exclude),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -573,7 +673,7 @@ class Addon(AddonModel):
|
|||||||
await self.sys_run_in_executor(_write_tarfile)
|
await self.sys_run_in_executor(_write_tarfile)
|
||||||
except (tarfile.TarError, OSError) as err:
|
except (tarfile.TarError, OSError) as err:
|
||||||
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't write tarfile %s: %s", tar_file, err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
||||||
|
|
||||||
@@ -590,13 +690,13 @@ class Addon(AddonModel):
|
|||||||
await self.sys_run_in_executor(_extract_tarfile)
|
await self.sys_run_in_executor(_extract_tarfile)
|
||||||
except tarfile.TarError as err:
|
except tarfile.TarError as err:
|
||||||
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
_LOGGER.error("Can't read tarfile %s: %s", tar_file, err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Read snapshot data
|
# Read snapshot data
|
||||||
try:
|
try:
|
||||||
data = read_json_file(Path(temp, "addon.json"))
|
data = read_json_file(Path(temp, "addon.json"))
|
||||||
except JsonFileError:
|
except JsonFileError as err:
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
@@ -607,14 +707,14 @@ class Addon(AddonModel):
|
|||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(data, err),
|
humanize_error(data, err),
|
||||||
)
|
)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# If available
|
# If available
|
||||||
if not self._available(data[ATTR_SYSTEM]):
|
if not self._available(data[ATTR_SYSTEM]):
|
||||||
_LOGGER.error("Add-on %s is not available for this Platform", self.slug)
|
_LOGGER.error("Add-on %s is not available for this platform", self.slug)
|
||||||
raise AddonsNotSupportedError()
|
raise AddonsNotSupportedError()
|
||||||
|
|
||||||
# Restore local add-on informations
|
# Restore local add-on information
|
||||||
_LOGGER.info("Restore config for addon %s", self.slug)
|
_LOGGER.info("Restore config for addon %s", self.slug)
|
||||||
restore_image = self._image(data[ATTR_SYSTEM])
|
restore_image = self._image(data[ATTR_SYSTEM])
|
||||||
self.sys_addons.data.restore(
|
self.sys_addons.data.restore(
|
||||||
@@ -645,7 +745,7 @@ class Addon(AddonModel):
|
|||||||
# Restore data
|
# Restore data
|
||||||
def _restore_data():
|
def _restore_data():
|
||||||
"""Restore data."""
|
"""Restore data."""
|
||||||
shutil.copytree(Path(temp, "data"), self.path_data)
|
shutil.copytree(Path(temp, "data"), self.path_data, symlinks=True)
|
||||||
|
|
||||||
_LOGGER.info("Restore data for addon %s", self.slug)
|
_LOGGER.info("Restore data for addon %s", self.slug)
|
||||||
if self.path_data.is_dir():
|
if self.path_data.is_dir():
|
||||||
@@ -654,19 +754,21 @@ class Addon(AddonModel):
|
|||||||
await self.sys_run_in_executor(_restore_data)
|
await self.sys_run_in_executor(_restore_data)
|
||||||
except shutil.Error as err:
|
except shutil.Error as err:
|
||||||
_LOGGER.error("Can't restore origin data: %s", err)
|
_LOGGER.error("Can't restore origin data: %s", err)
|
||||||
raise AddonsError() from None
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Restore AppArmor
|
# Restore AppArmor
|
||||||
profile_file = Path(temp, "apparmor.txt")
|
profile_file = Path(temp, "apparmor.txt")
|
||||||
if profile_file.exists():
|
if profile_file.exists():
|
||||||
try:
|
try:
|
||||||
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||||
except HostAppArmorError:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error("Can't restore AppArmor profile")
|
_LOGGER.error(
|
||||||
raise AddonsError() from None
|
"Can't restore AppArmor profile for add-on %s", self.slug
|
||||||
|
)
|
||||||
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == STATE_STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
_LOGGER.info("Finish restore for add-on %s", self.slug)
|
_LOGGER.info("Finish restore for add-on %s", self.slug)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
"""Supervisor add-on build environment."""
|
"""Supervisor add-on build environment."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Dict
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def base_image(self) -> str:
|
def base_image(self) -> str:
|
||||||
"""Base images for this add-on."""
|
"""Return base image for this add-on."""
|
||||||
return self._data[ATTR_BUILD_FROM].get(
|
return self._data[ATTR_BUILD_FROM].get(
|
||||||
self.sys_arch.default, f"homeassistant/{self.sys_arch.default}-base:latest"
|
self.sys_arch.default, f"homeassistant/{self.sys_arch.default}-base:latest"
|
||||||
)
|
)
|
||||||
|
@@ -12,8 +12,8 @@ from ..const import (
|
|||||||
FILE_HASSIO_ADDONS,
|
FILE_HASSIO_ADDONS,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..utils.json import JsonConfig
|
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
|
from ..utils.json import JsonConfig
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .validate import SCHEMA_ADDONS_FILE
|
from .validate import SCHEMA_ADDONS_FILE
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
"""Init file for Supervisor add-ons."""
|
"""Init file for Supervisor add-ons."""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Awaitable, Dict, List, Optional
|
from typing import Any, Awaitable, Dict, List, Optional
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ from ..const import (
|
|||||||
ATTR_HOST_PID,
|
ATTR_HOST_PID,
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_INGRESS,
|
ATTR_INGRESS,
|
||||||
|
ATTR_INIT,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@@ -56,39 +58,45 @@ from ..const import (
|
|||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
SECURITY_DEFAULT,
|
SECURITY_DEFAULT,
|
||||||
SECURITY_DISABLE,
|
SECURITY_DISABLE,
|
||||||
SECURITY_PROFILE,
|
SECURITY_PROFILE,
|
||||||
AddonStages,
|
AddonStage,
|
||||||
|
AddonStartup,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options
|
from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options
|
||||||
|
|
||||||
Data = Dict[str, Any]
|
Data = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
class AddonModel(CoreSysAttributes):
|
class AddonModel(CoreSysAttributes, ABC):
|
||||||
"""Add-on Data layout."""
|
"""Add-on Data layout."""
|
||||||
|
|
||||||
slug: str = None
|
def __init__(self, coresys: CoreSys, slug: str):
|
||||||
|
"""Initialize data holder."""
|
||||||
|
self.coresys: CoreSys = coresys
|
||||||
|
self.slug: str = slug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@abstractmethod
|
||||||
def data(self) -> Data:
|
def data(self) -> Data:
|
||||||
"""Return Add-on config/data."""
|
"""Return add-on config/data."""
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@abstractmethod
|
||||||
def is_installed(self) -> bool:
|
def is_installed(self) -> bool:
|
||||||
"""Return True if an add-on is installed."""
|
"""Return True if an add-on is installed."""
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@abstractmethod
|
||||||
def is_detached(self) -> bool:
|
def is_detached(self) -> bool:
|
||||||
"""Return True if add-on is detached."""
|
"""Return True if add-on is detached."""
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
@@ -136,7 +144,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hassio_token(self) -> Optional[str]:
|
def supervisor_token(self) -> Optional[str]:
|
||||||
"""Return access token for Supervisor API."""
|
"""Return access token for Supervisor API."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -179,7 +187,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> str:
|
def version(self) -> Optional[str]:
|
||||||
"""Return version of add-on."""
|
"""Return version of add-on."""
|
||||||
return self.data[ATTR_VERSION]
|
return self.data[ATTR_VERSION]
|
||||||
|
|
||||||
@@ -189,9 +197,9 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def startup(self) -> Optional[str]:
|
def startup(self) -> AddonStartup:
|
||||||
"""Return startup type of add-on."""
|
"""Return startup type of add-on."""
|
||||||
return self.data.get(ATTR_STARTUP)
|
return self.data[ATTR_STARTUP]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def advanced(self) -> bool:
|
def advanced(self) -> bool:
|
||||||
@@ -199,7 +207,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return self.data[ATTR_ADVANCED]
|
return self.data[ATTR_ADVANCED]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stage(self) -> AddonStages:
|
def stage(self) -> AddonStage:
|
||||||
"""Return stage mode of add-on."""
|
"""Return stage mode of add-on."""
|
||||||
return self.data[ATTR_STAGE]
|
return self.data[ATTR_STAGE]
|
||||||
|
|
||||||
@@ -211,7 +219,8 @@ class AddonModel(CoreSysAttributes):
|
|||||||
services = {}
|
services = {}
|
||||||
for data in services_list:
|
for data in services_list:
|
||||||
service = RE_SERVICE.match(data)
|
service = RE_SERVICE.match(data)
|
||||||
services[service.group("service")] = service.group("rights")
|
if service:
|
||||||
|
services[service.group("service")] = service.group("rights")
|
||||||
|
|
||||||
return services
|
return services
|
||||||
|
|
||||||
@@ -240,6 +249,11 @@ class AddonModel(CoreSysAttributes):
|
|||||||
"""Return URL to webui or None."""
|
"""Return URL to webui or None."""
|
||||||
return self.data.get(ATTR_WEBUI)
|
return self.data.get(ATTR_WEBUI)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watchdog(self) -> Optional[str]:
|
||||||
|
"""Return URL to for watchdog or None."""
|
||||||
|
return self.data.get(ATTR_WATCHDOG)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_port(self) -> Optional[int]:
|
def ingress_port(self) -> Optional[int]:
|
||||||
"""Return Ingress port."""
|
"""Return Ingress port."""
|
||||||
@@ -281,15 +295,10 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return self.data[ATTR_HOST_DBUS]
|
return self.data[ATTR_HOST_DBUS]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> Optional[List[str]]:
|
def devices(self) -> List[str]:
|
||||||
"""Return devices of add-on."""
|
"""Return devices of add-on."""
|
||||||
return self.data.get(ATTR_DEVICES, [])
|
return self.data.get(ATTR_DEVICES, [])
|
||||||
|
|
||||||
@property
|
|
||||||
def auto_uart(self) -> bool:
|
|
||||||
"""Return True if we should map all UART device."""
|
|
||||||
return self.data[ATTR_AUTO_UART]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tmpfs(self) -> Optional[str]:
|
def tmpfs(self) -> Optional[str]:
|
||||||
"""Return tmpfs of add-on."""
|
"""Return tmpfs of add-on."""
|
||||||
@@ -344,6 +353,11 @@ class AddonModel(CoreSysAttributes):
|
|||||||
"""Return Exclude list for snapshot."""
|
"""Return Exclude list for snapshot."""
|
||||||
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
return self.data.get(ATTR_SNAPSHOT_EXCLUDE, [])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_init(self) -> bool:
|
||||||
|
"""Return True if the add-on have no own init."""
|
||||||
|
return self.data[ATTR_INIT]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_stdin(self) -> bool:
|
def with_stdin(self) -> bool:
|
||||||
"""Return True if the add-on access use stdin input."""
|
"""Return True if the add-on access use stdin input."""
|
||||||
@@ -364,6 +378,16 @@ class AddonModel(CoreSysAttributes):
|
|||||||
"""Return True if the add-on access to GPIO interface."""
|
"""Return True if the add-on access to GPIO interface."""
|
||||||
return self.data[ATTR_GPIO]
|
return self.data[ATTR_GPIO]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_usb(self) -> bool:
|
||||||
|
"""Return True if the add-on need USB access."""
|
||||||
|
return self.data[ATTR_USB]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def with_uart(self) -> bool:
|
||||||
|
"""Return True if we should map all UART device."""
|
||||||
|
return self.data[ATTR_AUTO_UART]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def with_udev(self) -> bool:
|
def with_udev(self) -> bool:
|
||||||
"""Return True if the add-on have his own udev."""
|
"""Return True if the add-on have his own udev."""
|
||||||
@@ -440,7 +464,7 @@ class AddonModel(CoreSysAttributes):
|
|||||||
return self.data.get(ATTR_MACHINE, [])
|
return self.data.get(ATTR_MACHINE, [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image(self) -> str:
|
def image(self) -> Optional[str]:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
return self._image(self.data)
|
return self._image(self.data)
|
||||||
|
|
||||||
@@ -455,6 +479,8 @@ class AddonModel(CoreSysAttributes):
|
|||||||
volumes = {}
|
volumes = {}
|
||||||
for volume in self.data[ATTR_MAP]:
|
for volume in self.data[ATTR_MAP]:
|
||||||
result = RE_VOLUME.match(volume)
|
result = RE_VOLUME.match(volume)
|
||||||
|
if not result:
|
||||||
|
continue
|
||||||
volumes[result.group(1)] = result.group(2) or "ro"
|
volumes[result.group(1)] = result.group(2) or "ro"
|
||||||
|
|
||||||
return volumes
|
return volumes
|
||||||
@@ -521,17 +547,22 @@ class AddonModel(CoreSysAttributes):
|
|||||||
|
|
||||||
# Machine / Hardware
|
# Machine / Hardware
|
||||||
machine = config.get(ATTR_MACHINE)
|
machine = config.get(ATTR_MACHINE)
|
||||||
if machine and self.sys_machine not in machine:
|
if machine and f"!{self.sys_machine}" in machine:
|
||||||
|
return False
|
||||||
|
elif machine and self.sys_machine not in machine:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Home Assistant
|
# Home Assistant
|
||||||
version = config.get(ATTR_HOMEASSISTANT) or self.sys_homeassistant.version
|
version = config.get(ATTR_HOMEASSISTANT)
|
||||||
if pkg_version.parse(self.sys_homeassistant.version) < pkg_version.parse(
|
if version is None or self.sys_homeassistant.version is None:
|
||||||
version
|
return True
|
||||||
):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
try:
|
||||||
|
return pkg_version.parse(
|
||||||
|
self.sys_homeassistant.version
|
||||||
|
) >= pkg_version.parse(version)
|
||||||
|
except pkg_version.InvalidVersion:
|
||||||
|
return True
|
||||||
|
|
||||||
def _image(self, config) -> str:
|
def _image(self, config) -> str:
|
||||||
"""Generate image name from data."""
|
"""Generate image name from data."""
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Union
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -44,6 +44,7 @@ from ..const import (
|
|||||||
ATTR_INGRESS_PANEL,
|
ATTR_INGRESS_PANEL,
|
||||||
ATTR_INGRESS_PORT,
|
ATTR_INGRESS_PORT,
|
||||||
ATTR_INGRESS_TOKEN,
|
ATTR_INGRESS_TOKEN,
|
||||||
|
ATTR_INIT,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LEGACY,
|
ATTR_LEGACY,
|
||||||
ATTR_LOCATON,
|
ATTR_LOCATON,
|
||||||
@@ -74,31 +75,31 @@ from ..const import (
|
|||||||
ATTR_TMPFS,
|
ATTR_TMPFS,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
ATTR_USER,
|
ATTR_USER,
|
||||||
ATTR_UUID,
|
ATTR_UUID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
BOOT_AUTO,
|
BOOT_AUTO,
|
||||||
BOOT_MANUAL,
|
BOOT_MANUAL,
|
||||||
PRIVILEGED_ALL,
|
PRIVILEGED_ALL,
|
||||||
ROLE_ALL,
|
ROLE_ALL,
|
||||||
ROLE_DEFAULT,
|
ROLE_DEFAULT,
|
||||||
STARTUP_ALL,
|
AddonStage,
|
||||||
STARTUP_APPLICATION,
|
AddonStartup,
|
||||||
STARTUP_SERVICES,
|
AddonState,
|
||||||
STATE_STARTED,
|
|
||||||
STATE_STOPPED,
|
|
||||||
AddonStages,
|
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
from ..validate import (
|
from ..validate import (
|
||||||
DOCKER_PORTS,
|
docker_ports,
|
||||||
DOCKER_PORTS_DESCRIPTION,
|
docker_ports_description,
|
||||||
network_port,
|
network_port,
|
||||||
token,
|
token,
|
||||||
uuid_match,
|
uuid_match,
|
||||||
|
version_tag,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -120,7 +121,10 @@ V_LIST = "list"
|
|||||||
|
|
||||||
RE_SCHEMA_ELEMENT = re.compile(
|
RE_SCHEMA_ELEMENT = re.compile(
|
||||||
r"^(?:"
|
r"^(?:"
|
||||||
r"|bool|email|url|port"
|
r"|bool"
|
||||||
|
r"|email"
|
||||||
|
r"|url"
|
||||||
|
r"|port"
|
||||||
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
||||||
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
||||||
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||||
@@ -148,32 +152,33 @@ RE_DOCKER_IMAGE_BUILD = re.compile(
|
|||||||
|
|
||||||
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
SCHEMA_ELEMENT = vol.Match(RE_SCHEMA_ELEMENT)
|
||||||
|
|
||||||
|
RE_MACHINE = re.compile(
|
||||||
MACHINE_ALL = [
|
r"^!?(?:"
|
||||||
"intel-nuc",
|
r"|intel-nuc"
|
||||||
"odroid-c2",
|
r"|odroid-c2"
|
||||||
"odroid-n2",
|
r"|odroid-n2"
|
||||||
"odroid-xu",
|
r"|odroid-xu"
|
||||||
"qemuarm-64",
|
r"|qemuarm-64"
|
||||||
"qemuarm",
|
r"|qemuarm"
|
||||||
"qemux86-64",
|
r"|qemux86-64"
|
||||||
"qemux86",
|
r"|qemux86"
|
||||||
"raspberrypi",
|
r"|raspberrypi"
|
||||||
"raspberrypi2",
|
r"|raspberrypi2"
|
||||||
"raspberrypi3-64",
|
r"|raspberrypi3-64"
|
||||||
"raspberrypi3",
|
r"|raspberrypi3"
|
||||||
"raspberrypi4-64",
|
r"|raspberrypi4-64"
|
||||||
"raspberrypi4",
|
r"|raspberrypi4"
|
||||||
"tinker",
|
r"|tinker"
|
||||||
]
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _simple_startup(value):
|
def _simple_startup(value) -> str:
|
||||||
"""Simple startup schema."""
|
"""Define startup schema."""
|
||||||
if value == "before":
|
if value == "before":
|
||||||
return STARTUP_SERVICES
|
return AddonStartup.SERVICES.value
|
||||||
if value == "after":
|
if value == "after":
|
||||||
return STARTUP_APPLICATION
|
return AddonStartup.APPLICATION.value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -181,18 +186,22 @@ def _simple_startup(value):
|
|||||||
SCHEMA_ADDON_CONFIG = vol.Schema(
|
SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): vol.All(version_tag, str),
|
||||||
vol.Required(ATTR_SLUG): vol.Coerce(str),
|
vol.Required(ATTR_SLUG): vol.Coerce(str),
|
||||||
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
||||||
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
||||||
vol.Optional(ATTR_MACHINE): [vol.In(MACHINE_ALL)],
|
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
||||||
vol.Optional(ATTR_URL): vol.Url(),
|
vol.Optional(ATTR_URL): vol.Url(),
|
||||||
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.In(STARTUP_ALL)),
|
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.Coerce(AddonStartup)),
|
||||||
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.Required(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
|
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_STAGE, default=AddonStages.STABLE): vol.Coerce(AddonStages),
|
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
||||||
vol.Optional(ATTR_PORTS): DOCKER_PORTS,
|
vol.Optional(ATTR_PORTS): docker_ports,
|
||||||
vol.Optional(ATTR_PORTS_DESCRIPTION): DOCKER_PORTS_DESCRIPTION,
|
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
||||||
|
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
||||||
|
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||||
|
),
|
||||||
vol.Optional(ATTR_WEBUI): vol.Match(
|
vol.Optional(ATTR_WEBUI): vol.Match(
|
||||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||||
),
|
),
|
||||||
@@ -221,6 +230,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||||
@@ -259,7 +269,7 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
|||||||
),
|
),
|
||||||
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
||||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||||
vol.Coerce(int), vol.Range(min=10, max=120)
|
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
@@ -294,11 +304,12 @@ SCHEMA_ADDON_USER = vol.Schema(
|
|||||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
vol.Optional(ATTR_NETWORK): DOCKER_PORTS,
|
vol.Optional(ATTR_NETWORK): docker_ports,
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
)
|
)
|
||||||
@@ -324,7 +335,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
|||||||
{
|
{
|
||||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||||
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
||||||
vol.Required(ATTR_STATE): vol.In([STATE_STARTED, STATE_STOPPED]),
|
vol.Required(ATTR_STATE): vol.Coerce(AddonState),
|
||||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||||
},
|
},
|
||||||
extra=vol.REMOVE_EXTRA,
|
extra=vol.REMOVE_EXTRA,
|
||||||
@@ -371,18 +382,21 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
|||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
# if required argument
|
# if required argument
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid(f"Missing required option '{key}'")
|
raise vol.Invalid(f"Missing required option '{key}'") from None
|
||||||
|
|
||||||
# Lookup secret
|
# Lookup secret
|
||||||
if str(value).startswith("!secret "):
|
if str(value).startswith("!secret "):
|
||||||
secret: str = value.partition(" ")[2]
|
secret: str = value.partition(" ")[2]
|
||||||
value = coresys.secrets.get(secret)
|
value = coresys.homeassistant.secrets.get(secret)
|
||||||
if value is None:
|
if value is None:
|
||||||
raise vol.Invalid(f"Unknown secret {secret}")
|
raise vol.Invalid(f"Unknown secret {secret}") from None
|
||||||
|
|
||||||
# parse extend data from type
|
# parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
raise vol.Invalid(f"Unknown type {typ}") from None
|
||||||
|
|
||||||
# prepare range
|
# prepare range
|
||||||
range_args = {}
|
range_args = {}
|
||||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||||
@@ -409,13 +423,18 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
|||||||
elif typ.startswith(V_LIST):
|
elif typ.startswith(V_LIST):
|
||||||
return vol.In(match.group("list").split("|"))(str(value))
|
return vol.In(match.group("list").split("|"))(str(value))
|
||||||
|
|
||||||
raise vol.Invalid(f"Fatal error for {key} type {typ}")
|
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
|
||||||
|
|
||||||
|
|
||||||
def _nested_validate_list(coresys, typ, data_list, key):
|
def _nested_validate_list(coresys, typ, data_list, key):
|
||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
|
# Make sure it is a list
|
||||||
|
if not isinstance(data_list, list):
|
||||||
|
raise vol.Invalid(f"Invalid list for {key}") from None
|
||||||
|
|
||||||
|
# Process list
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
# Nested?
|
# Nested?
|
||||||
if isinstance(typ, dict):
|
if isinstance(typ, dict):
|
||||||
@@ -431,6 +450,11 @@ def _nested_validate_dict(coresys, typ, data_dict, key):
|
|||||||
"""Validate nested items."""
|
"""Validate nested items."""
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
|
# Make sure it is a dict
|
||||||
|
if not isinstance(data_dict, dict):
|
||||||
|
raise vol.Invalid(f"Invalid dict for {key}") from None
|
||||||
|
|
||||||
|
# Process dict
|
||||||
for c_key, c_value in data_dict.items():
|
for c_key, c_value in data_dict.items():
|
||||||
# Ignore unknown options / remove from list
|
# Ignore unknown options / remove from list
|
||||||
if c_key not in typ:
|
if c_key not in typ:
|
||||||
@@ -455,12 +479,12 @@ def _check_missing_options(origin, exists, root):
|
|||||||
for miss_opt in missing:
|
for miss_opt in missing:
|
||||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||||
continue
|
continue
|
||||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}")
|
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
|
||||||
|
|
||||||
|
|
||||||
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""Generate UI schema."""
|
"""Generate UI schema."""
|
||||||
ui_schema = []
|
ui_schema: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
# read options
|
# read options
|
||||||
for key, value in raw_schema.items():
|
for key, value in raw_schema.items():
|
||||||
@@ -481,7 +505,7 @@ def _single_ui_option(
|
|||||||
ui_schema: List[Dict[str, Any]], value: str, key: str, multiple: bool = False
|
ui_schema: List[Dict[str, Any]], value: str, key: str, multiple: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate a single element."""
|
"""Validate a single element."""
|
||||||
ui_node = {"name": key}
|
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
||||||
|
|
||||||
# If multiple
|
# If multiple
|
||||||
if multiple:
|
if multiple:
|
||||||
@@ -489,6 +513,8 @@ def _single_ui_option(
|
|||||||
|
|
||||||
# Parse extend data from type
|
# Parse extend data from type
|
||||||
match = RE_SCHEMA_ELEMENT.match(value)
|
match = RE_SCHEMA_ELEMENT.match(value)
|
||||||
|
if not match:
|
||||||
|
return
|
||||||
|
|
||||||
# Prepare range
|
# Prepare range
|
||||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||||
|
@@ -7,21 +7,24 @@ from aiohttp import web
|
|||||||
|
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
|
from .cli import APICli
|
||||||
from .discovery import APIDiscovery
|
from .discovery import APIDiscovery
|
||||||
from .dns import APICoreDNS
|
from .dns import APICoreDNS
|
||||||
from .hardware import APIHardware
|
from .hardware import APIHardware
|
||||||
from .hassos import APIHassOS
|
|
||||||
from .homeassistant import APIHomeAssistant
|
from .homeassistant import APIHomeAssistant
|
||||||
from .host import APIHost
|
from .host import APIHost
|
||||||
from .info import APIInfo
|
from .info import APIInfo
|
||||||
from .ingress import APIIngress
|
from .ingress import APIIngress
|
||||||
|
from .multicast import APIMulticast
|
||||||
|
from .network import APINetwork
|
||||||
|
from .os import APIOS
|
||||||
from .proxy import APIProxy
|
from .proxy import APIProxy
|
||||||
from .security import SecurityMiddleware
|
from .security import SecurityMiddleware
|
||||||
from .services import APIServices
|
from .services import APIServices
|
||||||
from .snapshots import APISnapshots
|
from .snapshots import APISnapshots
|
||||||
from .supervisor import APISupervisor
|
from .supervisor import APISupervisor
|
||||||
from .audio import APIAudio
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -49,7 +52,10 @@ class RestAPI(CoreSysAttributes):
|
|||||||
"""Register REST API Calls."""
|
"""Register REST API Calls."""
|
||||||
self._register_supervisor()
|
self._register_supervisor()
|
||||||
self._register_host()
|
self._register_host()
|
||||||
self._register_hassos()
|
self._register_os()
|
||||||
|
self._register_cli()
|
||||||
|
self._register_multicast()
|
||||||
|
self._register_network()
|
||||||
self._register_hardware()
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
@@ -72,6 +78,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/host/info", api_host.info),
|
web.get("/host/info", api_host.info),
|
||||||
|
web.get("/host/logs", api_host.logs),
|
||||||
web.post("/host/reboot", api_host.reboot),
|
web.post("/host/reboot", api_host.reboot),
|
||||||
web.post("/host/shutdown", api_host.shutdown),
|
web.post("/host/shutdown", api_host.shutdown),
|
||||||
web.post("/host/reload", api_host.reload),
|
web.post("/host/reload", api_host.reload),
|
||||||
@@ -84,22 +91,62 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def _register_hassos(self) -> None:
|
def _register_network(self) -> None:
|
||||||
"""Register HassOS functions."""
|
"""Register network functions."""
|
||||||
api_hassos = APIHassOS()
|
api_network = APINetwork()
|
||||||
api_hassos.coresys = self.coresys
|
api_network.coresys = self.coresys
|
||||||
|
|
||||||
self.webapp.add_routes(
|
self.webapp.add_routes(
|
||||||
[
|
[
|
||||||
web.get("/os/info", api_hassos.info),
|
web.get("/network/info", api_network.info),
|
||||||
web.post("/os/update", api_hassos.update),
|
web.get(
|
||||||
web.post("/os/update/cli", api_hassos.update_cli),
|
"/network/interface/{interface}/info", api_network.interface_info
|
||||||
web.post("/os/config/sync", api_hassos.config_sync),
|
),
|
||||||
# Remove with old Supervisor fallback
|
web.post(
|
||||||
web.get("/hassos/info", api_hassos.info),
|
"/network/interface/{interface}/update",
|
||||||
web.post("/hassos/update", api_hassos.update),
|
api_network.interface_update,
|
||||||
web.post("/hassos/update/cli", api_hassos.update_cli),
|
),
|
||||||
web.post("/hassos/config/sync", api_hassos.config_sync),
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_os(self) -> None:
|
||||||
|
"""Register OS functions."""
|
||||||
|
api_os = APIOS()
|
||||||
|
api_os.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/os/info", api_os.info),
|
||||||
|
web.post("/os/update", api_os.update),
|
||||||
|
web.post("/os/config/sync", api_os.config_sync),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_cli(self) -> None:
|
||||||
|
"""Register HA cli functions."""
|
||||||
|
api_cli = APICli()
|
||||||
|
api_cli.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/cli/info", api_cli.info),
|
||||||
|
web.get("/cli/stats", api_cli.stats),
|
||||||
|
web.post("/cli/update", api_cli.update),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _register_multicast(self) -> None:
|
||||||
|
"""Register Multicast functions."""
|
||||||
|
api_multicast = APIMulticast()
|
||||||
|
api_multicast.coresys = self.coresys
|
||||||
|
|
||||||
|
self.webapp.add_routes(
|
||||||
|
[
|
||||||
|
web.get("/multicast/info", api_multicast.info),
|
||||||
|
web.get("/multicast/stats", api_multicast.stats),
|
||||||
|
web.get("/multicast/logs", api_multicast.logs),
|
||||||
|
web.post("/multicast/update", api_multicast.update),
|
||||||
|
web.post("/multicast/restart", api_multicast.restart),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -221,6 +268,9 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||||
web.post("/addons/{addon}/update", api_addons.update),
|
web.post("/addons/{addon}/update", api_addons.update),
|
||||||
web.post("/addons/{addon}/options", api_addons.options),
|
web.post("/addons/{addon}/options", api_addons.options),
|
||||||
|
web.post(
|
||||||
|
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||||
|
),
|
||||||
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
web.get("/addons/{addon}/logs", api_addons.logs),
|
||||||
web.get("/addons/{addon}/icon", api_addons.icon),
|
web.get("/addons/{addon}/icon", api_addons.icon),
|
||||||
@@ -329,7 +379,11 @@ class RestAPI(CoreSysAttributes):
|
|||||||
web.post("/audio/update", api_audio.update),
|
web.post("/audio/update", api_audio.update),
|
||||||
web.post("/audio/restart", api_audio.restart),
|
web.post("/audio/restart", api_audio.restart),
|
||||||
web.post("/audio/reload", api_audio.reload),
|
web.post("/audio/reload", api_audio.reload),
|
||||||
|
web.post("/audio/profile", api_audio.set_profile),
|
||||||
|
web.post("/audio/volume/{source}/application", api_audio.set_volume),
|
||||||
web.post("/audio/volume/{source}", api_audio.set_volume),
|
web.post("/audio/volume/{source}", api_audio.set_volume),
|
||||||
|
web.post("/audio/mute/{source}/application", api_audio.set_mute),
|
||||||
|
web.post("/audio/mute/{source}", api_audio.set_mute),
|
||||||
web.post("/audio/default/{source}", api_audio.set_default),
|
web.post("/audio/default/{source}", api_audio.set_default),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -349,7 +403,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
try:
|
try:
|
||||||
await self._site.start()
|
await self._site.start()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.fatal("Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
|
_LOGGER.critical("Failed to create HTTP server at 0.0.0.0:80 -> %s", err)
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
|
_LOGGER.info("Start API on %s", self.sys_docker.network.supervisor)
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ from typing import Any, Awaitable, Dict, List
|
|||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from ..addons import AnyAddon
|
from ..addons import AnyAddon
|
||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
@@ -54,7 +55,6 @@ from ..const import (
|
|||||||
ATTR_INSTALLED,
|
ATTR_INSTALLED,
|
||||||
ATTR_IP_ADDRESS,
|
ATTR_IP_ADDRESS,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_LAST_VERSION,
|
|
||||||
ATTR_LOGO,
|
ATTR_LOGO,
|
||||||
ATTR_LONG_DESCRIPTION,
|
ATTR_LONG_DESCRIPTION,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
@@ -62,6 +62,7 @@ from ..const import (
|
|||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_MESSAGE,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_NETWORK,
|
ATTR_NETWORK,
|
||||||
ATTR_NETWORK_DESCRIPTION,
|
ATTR_NETWORK_DESCRIPTION,
|
||||||
@@ -78,12 +79,17 @@ from ..const import (
|
|||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_SOURCE,
|
ATTR_SOURCE,
|
||||||
ATTR_STAGE,
|
ATTR_STAGE,
|
||||||
|
ATTR_STARTUP,
|
||||||
ATTR_STATE,
|
ATTR_STATE,
|
||||||
ATTR_STDIN,
|
ATTR_STDIN,
|
||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
|
ATTR_USB,
|
||||||
|
ATTR_VALID,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
|
ATTR_WATCHDOG,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
BOOT_AUTO,
|
BOOT_AUTO,
|
||||||
BOOT_MANUAL,
|
BOOT_MANUAL,
|
||||||
@@ -91,12 +97,12 @@ from ..const import (
|
|||||||
CONTENT_TYPE_PNG,
|
CONTENT_TYPE_PNG,
|
||||||
CONTENT_TYPE_TEXT,
|
CONTENT_TYPE_TEXT,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
STATE_NONE,
|
AddonState,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.stats import DockerStats
|
from ..docker.stats import DockerStats
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..validate import DOCKER_PORTS
|
from ..validate import docker_ports
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -107,11 +113,12 @@ SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
|||||||
SCHEMA_OPTIONS = vol.Schema(
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
vol.Optional(ATTR_BOOT): vol.In([BOOT_AUTO, BOOT_MANUAL]),
|
||||||
vol.Optional(ATTR_NETWORK): vol.Maybe(DOCKER_PORTS),
|
vol.Optional(ATTR_NETWORK): vol.Maybe(docker_ports),
|
||||||
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
vol.Optional(ATTR_AUTO_UPDATE): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
vol.Optional(ATTR_INGRESS_PANEL): vol.Boolean(),
|
||||||
|
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,9 +129,7 @@ SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
|
|||||||
class APIAddons(CoreSysAttributes):
|
class APIAddons(CoreSysAttributes):
|
||||||
"""Handle RESTful API for add-on functions."""
|
"""Handle RESTful API for add-on functions."""
|
||||||
|
|
||||||
def _extract_addon(
|
def _extract_addon(self, request: web.Request) -> AnyAddon:
|
||||||
self, request: web.Request, check_installed: bool = True
|
|
||||||
) -> AnyAddon:
|
|
||||||
"""Return addon, throw an exception it it doesn't exist."""
|
"""Return addon, throw an exception it it doesn't exist."""
|
||||||
addon_slug: str = request.match_info.get("addon")
|
addon_slug: str = request.match_info.get("addon")
|
||||||
|
|
||||||
@@ -137,49 +142,49 @@ class APIAddons(CoreSysAttributes):
|
|||||||
|
|
||||||
addon = self.sys_addons.get(addon_slug)
|
addon = self.sys_addons.get(addon_slug)
|
||||||
if not addon:
|
if not addon:
|
||||||
raise APIError("Addon does not exist")
|
raise APIError(f"Addon {addon_slug} does not exist")
|
||||||
|
|
||||||
if check_installed and not addon.is_installed:
|
return addon
|
||||||
|
|
||||||
|
def _extract_addon_installed(self, request: web.Request) -> Addon:
|
||||||
|
addon = self._extract_addon(request)
|
||||||
|
if not isinstance(addon, Addon) or not addon.is_installed:
|
||||||
raise APIError("Addon is not installed")
|
raise APIError("Addon is not installed")
|
||||||
|
|
||||||
return addon
|
return addon
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list(self, request: web.Request) -> Dict[str, Any]:
|
async def list(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return all add-ons or repositories."""
|
"""Return all add-ons or repositories."""
|
||||||
data_addons = []
|
data_addons = [
|
||||||
for addon in self.sys_addons.all:
|
{
|
||||||
data_addons.append(
|
ATTR_NAME: addon.name,
|
||||||
{
|
ATTR_SLUG: addon.slug,
|
||||||
ATTR_NAME: addon.name,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_SLUG: addon.slug,
|
ATTR_ADVANCED: addon.advanced,
|
||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_STAGE: addon.stage,
|
||||||
ATTR_ADVANCED: addon.advanced,
|
ATTR_VERSION: addon.latest_version,
|
||||||
ATTR_STAGE: addon.stage,
|
ATTR_INSTALLED: addon.version if addon.is_installed else None,
|
||||||
ATTR_VERSION: addon.latest_version,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_INSTALLED: addon.version if addon.is_installed else None,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_BUILD: addon.need_build,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_URL: addon.url,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_URL: addon.url,
|
ATTR_LOGO: addon.with_logo,
|
||||||
ATTR_ICON: addon.with_icon,
|
}
|
||||||
ATTR_LOGO: addon.with_logo,
|
for addon in self.sys_addons.all
|
||||||
}
|
]
|
||||||
)
|
|
||||||
|
|
||||||
data_repositories = []
|
|
||||||
for repository in self.sys_store.all:
|
|
||||||
data_repositories.append(
|
|
||||||
{
|
|
||||||
ATTR_SLUG: repository.slug,
|
|
||||||
ATTR_NAME: repository.name,
|
|
||||||
ATTR_SOURCE: repository.source,
|
|
||||||
ATTR_URL: repository.url,
|
|
||||||
ATTR_MAINTAINER: repository.maintainer,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
data_repositories = [
|
||||||
|
{
|
||||||
|
ATTR_SLUG: repository.slug,
|
||||||
|
ATTR_NAME: repository.name,
|
||||||
|
ATTR_SOURCE: repository.source,
|
||||||
|
ATTR_URL: repository.url,
|
||||||
|
ATTR_MAINTAINER: repository.maintainer,
|
||||||
|
}
|
||||||
|
for repository in self.sys_store.all
|
||||||
|
]
|
||||||
return {ATTR_ADDONS: data_addons, ATTR_REPOSITORIES: data_repositories}
|
return {ATTR_ADDONS: data_addons, ATTR_REPOSITORIES: data_repositories}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -190,7 +195,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return add-on information."""
|
"""Return add-on information."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon: AnyAddon = self._extract_addon(request)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_NAME: addon.name,
|
ATTR_NAME: addon.name,
|
||||||
@@ -204,7 +209,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUTO_UPDATE: None,
|
ATTR_AUTO_UPDATE: None,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
ATTR_VERSION: None,
|
ATTR_VERSION: None,
|
||||||
ATTR_LAST_VERSION: addon.latest_version,
|
ATTR_VERSION_LATEST: addon.latest_version,
|
||||||
ATTR_PROTECTED: addon.protected,
|
ATTR_PROTECTED: addon.protected,
|
||||||
ATTR_RATING: rating_security(addon),
|
ATTR_RATING: rating_security(addon),
|
||||||
ATTR_BOOT: addon.boot,
|
ATTR_BOOT: addon.boot,
|
||||||
@@ -214,7 +219,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_MACHINE: addon.supported_machine,
|
ATTR_MACHINE: addon.supported_machine,
|
||||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||||
ATTR_URL: addon.url,
|
ATTR_URL: addon.url,
|
||||||
ATTR_STATE: STATE_NONE,
|
ATTR_STATE: AddonState.UNKNOWN,
|
||||||
ATTR_DETACHED: addon.is_detached,
|
ATTR_DETACHED: addon.is_detached,
|
||||||
ATTR_AVAILABLE: addon.available,
|
ATTR_AVAILABLE: addon.available,
|
||||||
ATTR_BUILD: addon.need_build,
|
ATTR_BUILD: addon.need_build,
|
||||||
@@ -239,6 +244,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUTH_API: addon.access_auth_api,
|
ATTR_AUTH_API: addon.access_auth_api,
|
||||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||||
ATTR_GPIO: addon.with_gpio,
|
ATTR_GPIO: addon.with_gpio,
|
||||||
|
ATTR_USB: addon.with_usb,
|
||||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||||
ATTR_DEVICETREE: addon.with_devicetree,
|
ATTR_DEVICETREE: addon.with_devicetree,
|
||||||
ATTR_UDEV: addon.with_udev,
|
ATTR_UDEV: addon.with_udev,
|
||||||
@@ -247,6 +253,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUDIO: addon.with_audio,
|
ATTR_AUDIO: addon.with_audio,
|
||||||
ATTR_AUDIO_INPUT: None,
|
ATTR_AUDIO_INPUT: None,
|
||||||
ATTR_AUDIO_OUTPUT: None,
|
ATTR_AUDIO_OUTPUT: None,
|
||||||
|
ATTR_STARTUP: addon.startup,
|
||||||
ATTR_SERVICES: _pretty_services(addon),
|
ATTR_SERVICES: _pretty_services(addon),
|
||||||
ATTR_DISCOVERY: addon.discovery,
|
ATTR_DISCOVERY: addon.discovery,
|
||||||
ATTR_IP_ADDRESS: None,
|
ATTR_IP_ADDRESS: None,
|
||||||
@@ -255,12 +262,13 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_INGRESS_URL: None,
|
ATTR_INGRESS_URL: None,
|
||||||
ATTR_INGRESS_PORT: None,
|
ATTR_INGRESS_PORT: None,
|
||||||
ATTR_INGRESS_PANEL: None,
|
ATTR_INGRESS_PANEL: None,
|
||||||
|
ATTR_WATCHDOG: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if addon.is_installed:
|
if isinstance(addon, Addon) and addon.is_installed:
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
ATTR_STATE: await addon.state(),
|
ATTR_STATE: addon.state,
|
||||||
ATTR_WEBUI: addon.webui,
|
ATTR_WEBUI: addon.webui,
|
||||||
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
ATTR_INGRESS_ENTRY: addon.ingress_entry,
|
||||||
ATTR_INGRESS_URL: addon.ingress_url,
|
ATTR_INGRESS_URL: addon.ingress_url,
|
||||||
@@ -271,6 +279,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_AUTO_UPDATE: addon.auto_update,
|
ATTR_AUTO_UPDATE: addon.auto_update,
|
||||||
ATTR_IP_ADDRESS: str(addon.ip_address),
|
ATTR_IP_ADDRESS: str(addon.ip_address),
|
||||||
ATTR_VERSION: addon.version,
|
ATTR_VERSION: addon.version,
|
||||||
|
ATTR_WATCHDOG: addon.watchdog,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -279,10 +288,10 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def options(self, request: web.Request) -> None:
|
async def options(self, request: web.Request) -> None:
|
||||||
"""Store user options for add-on."""
|
"""Store user options for add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
# Update secrets for validation
|
# Update secrets for validation
|
||||||
await self.sys_secrets.reload()
|
await self.sys_homeassistant.secrets.reload()
|
||||||
|
|
||||||
# Extend schema with add-on specific validation
|
# Extend schema with add-on specific validation
|
||||||
addon_schema = SCHEMA_OPTIONS.extend(
|
addon_schema = SCHEMA_OPTIONS.extend(
|
||||||
@@ -306,13 +315,28 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if ATTR_INGRESS_PANEL in body:
|
if ATTR_INGRESS_PANEL in body:
|
||||||
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
addon.ingress_panel = body[ATTR_INGRESS_PANEL]
|
||||||
await self.sys_ingress.update_hass_panel(addon)
|
await self.sys_ingress.update_hass_panel(addon)
|
||||||
|
if ATTR_WATCHDOG in body:
|
||||||
|
addon.watchdog = body[ATTR_WATCHDOG]
|
||||||
|
|
||||||
addon.save_persist()
|
addon.save_persist()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def options_validate(self, request: web.Request) -> None:
|
||||||
|
"""Validate user options for add-on."""
|
||||||
|
addon = self._extract_addon_installed(request)
|
||||||
|
data = {ATTR_MESSAGE: "", ATTR_VALID: True}
|
||||||
|
try:
|
||||||
|
addon.schema(addon.options)
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
data[ATTR_MESSAGE] = humanize_error(addon.options, ex)
|
||||||
|
data[ATTR_VALID] = False
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def security(self, request: web.Request) -> None:
|
async def security(self, request: web.Request) -> None:
|
||||||
"""Store security options for add-on."""
|
"""Store security options for add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
body: Dict[str, Any] = await api_validate(SCHEMA_SECURITY, request)
|
||||||
|
|
||||||
if ATTR_PROTECTED in body:
|
if ATTR_PROTECTED in body:
|
||||||
@@ -324,7 +348,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
stats: DockerStats = await addon.stats()
|
stats: DockerStats = await addon.stats()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -341,64 +366,57 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
def install(self, request: web.Request) -> Awaitable[None]:
|
def install(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Install add-on."""
|
"""Install add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
return asyncio.shield(addon.install())
|
return asyncio.shield(addon.install())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Uninstall add-on."""
|
"""Uninstall add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.uninstall())
|
return asyncio.shield(addon.uninstall())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request: web.Request) -> Awaitable[None]:
|
def start(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Start add-on."""
|
"""Start add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.start())
|
return asyncio.shield(addon.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Awaitable[None]:
|
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop add-on."""
|
"""Stop add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.stop())
|
return asyncio.shield(addon.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def update(self, request: web.Request) -> Awaitable[None]:
|
def update(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Update add-on."""
|
"""Update add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: Addon = self._extract_addon_installed(request)
|
||||||
|
|
||||||
if addon.latest_version == addon.version:
|
|
||||||
raise APIError("No update available!")
|
|
||||||
|
|
||||||
return asyncio.shield(addon.update())
|
return asyncio.shield(addon.update())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon: Addon = self._extract_addon_installed(request)
|
||||||
return asyncio.shield(addon.restart())
|
return asyncio.shield(addon.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Rebuild local build add-on."""
|
"""Rebuild local build add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
if not addon.need_build:
|
|
||||||
raise APIError("Only local build addons are supported")
|
|
||||||
|
|
||||||
return asyncio.shield(addon.rebuild())
|
return asyncio.shield(addon.rebuild())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return logs from add-on."""
|
"""Return logs from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
return addon.logs()
|
return addon.logs()
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def icon(self, request: web.Request) -> bytes:
|
async def icon(self, request: web.Request) -> bytes:
|
||||||
"""Return icon from add-on."""
|
"""Return icon from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_icon:
|
if not addon.with_icon:
|
||||||
raise APIError("No icon found!")
|
raise APIError(f"No icon found for add-on {addon.slug}!")
|
||||||
|
|
||||||
with addon.path_icon.open("rb") as png:
|
with addon.path_icon.open("rb") as png:
|
||||||
return png.read()
|
return png.read()
|
||||||
@@ -406,9 +424,9 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_PNG)
|
@api_process_raw(CONTENT_TYPE_PNG)
|
||||||
async def logo(self, request: web.Request) -> bytes:
|
async def logo(self, request: web.Request) -> bytes:
|
||||||
"""Return logo from add-on."""
|
"""Return logo from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_logo:
|
if not addon.with_logo:
|
||||||
raise APIError("No logo found!")
|
raise APIError(f"No logo found for add-on {addon.slug}!")
|
||||||
|
|
||||||
with addon.path_logo.open("rb") as png:
|
with addon.path_logo.open("rb") as png:
|
||||||
return png.read()
|
return png.read()
|
||||||
@@ -416,9 +434,9 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def changelog(self, request: web.Request) -> str:
|
async def changelog(self, request: web.Request) -> str:
|
||||||
"""Return changelog from add-on."""
|
"""Return changelog from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_changelog:
|
if not addon.with_changelog:
|
||||||
raise APIError("No changelog found!")
|
raise APIError(f"No changelog found for add-on {addon.slug}!")
|
||||||
|
|
||||||
with addon.path_changelog.open("r") as changelog:
|
with addon.path_changelog.open("r") as changelog:
|
||||||
return changelog.read()
|
return changelog.read()
|
||||||
@@ -426,9 +444,9 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process_raw(CONTENT_TYPE_TEXT)
|
@api_process_raw(CONTENT_TYPE_TEXT)
|
||||||
async def documentation(self, request: web.Request) -> str:
|
async def documentation(self, request: web.Request) -> str:
|
||||||
"""Return documentation from add-on."""
|
"""Return documentation from add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request, check_installed=False)
|
addon = self._extract_addon(request)
|
||||||
if not addon.with_documentation:
|
if not addon.with_documentation:
|
||||||
raise APIError("No documentation found!")
|
raise APIError(f"No documentation found for add-on {addon.slug}!")
|
||||||
|
|
||||||
with addon.path_documentation.open("r") as documentation:
|
with addon.path_documentation.open("r") as documentation:
|
||||||
return documentation.read()
|
return documentation.read()
|
||||||
@@ -436,9 +454,9 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stdin(self, request: web.Request) -> None:
|
async def stdin(self, request: web.Request) -> None:
|
||||||
"""Write to stdin of add-on."""
|
"""Write to stdin of add-on."""
|
||||||
addon: AnyAddon = self._extract_addon(request)
|
addon = self._extract_addon_installed(request)
|
||||||
if not addon.with_stdin:
|
if not addon.with_stdin:
|
||||||
raise APIError("STDIN not supported by add-on")
|
raise APIError(f"STDIN not supported the {addon.slug} add-on")
|
||||||
|
|
||||||
data = await request.read()
|
data = await request.read()
|
||||||
await asyncio.shield(addon.write_stdin(data))
|
await asyncio.shield(addon.write_stdin(data))
|
||||||
@@ -448,13 +466,10 @@ def _pretty_devices(addon: AnyAddon) -> List[str]:
|
|||||||
"""Return a simplified device list."""
|
"""Return a simplified device list."""
|
||||||
dev_list = addon.devices
|
dev_list = addon.devices
|
||||||
if not dev_list:
|
if not dev_list:
|
||||||
return None
|
return []
|
||||||
return [row.split(":")[0] for row in dev_list]
|
return [row.split(":")[0] for row in dev_list]
|
||||||
|
|
||||||
|
|
||||||
def _pretty_services(addon: AnyAddon) -> List[str]:
|
def _pretty_services(addon: AnyAddon) -> List[str]:
|
||||||
"""Return a simplified services role list."""
|
"""Return a simplified services role list."""
|
||||||
services = []
|
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
||||||
for name, access in addon.services_role.items():
|
|
||||||
services.append(f"{name}:{access}")
|
|
||||||
return services
|
|
||||||
|
@@ -8,13 +8,16 @@ import attr
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
|
ATTR_ACTIVE,
|
||||||
|
ATTR_APPLICATION,
|
||||||
ATTR_AUDIO,
|
ATTR_AUDIO,
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
|
ATTR_CARD,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_HOST,
|
ATTR_HOST,
|
||||||
|
ATTR_INDEX,
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
ATTR_LATEST_VERSION,
|
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
@@ -23,27 +26,41 @@ from ..const import (
|
|||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
ATTR_OUTPUT,
|
ATTR_OUTPUT,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_VOLUME,
|
ATTR_VOLUME,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..host.sound import SourceType
|
from ..host.sound import StreamType
|
||||||
|
from ..validate import version_tag
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
SCHEMA_VOLUME = vol.Schema(
|
SCHEMA_VOLUME = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||||
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
vol.Required(ATTR_VOLUME): vol.Coerce(float),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_MUTE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_INDEX): vol.Coerce(int),
|
||||||
|
vol.Required(ATTR_ACTIVE): vol.Boolean(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
SCHEMA_DEFAULT = vol.Schema({vol.Required(ATTR_NAME): vol.Coerce(str)})
|
||||||
|
|
||||||
|
SCHEMA_PROFILE = vol.Schema(
|
||||||
|
{vol.Required(ATTR_CARD): vol.Coerce(str), vol.Required(ATTR_NAME): vol.Coerce(str)}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIAudio(CoreSysAttributes):
|
class APIAudio(CoreSysAttributes):
|
||||||
"""Handle RESTful API for Audio functions."""
|
"""Handle RESTful API for Audio functions."""
|
||||||
@@ -52,17 +69,19 @@ class APIAudio(CoreSysAttributes):
|
|||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return Audio information."""
|
"""Return Audio information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_audio.version,
|
ATTR_VERSION: self.sys_plugins.audio.version,
|
||||||
ATTR_LATEST_VERSION: self.sys_audio.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.audio.latest_version,
|
||||||
ATTR_HOST: str(self.sys_docker.network.audio),
|
ATTR_HOST: str(self.sys_docker.network.audio),
|
||||||
ATTR_AUDIO: {
|
ATTR_AUDIO: {
|
||||||
|
ATTR_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards],
|
||||||
ATTR_INPUT: [
|
ATTR_INPUT: [
|
||||||
attr.asdict(profile)
|
attr.asdict(stream) for stream in self.sys_host.sound.inputs
|
||||||
for profile in self.sys_host.sound.input_profiles
|
|
||||||
],
|
],
|
||||||
ATTR_OUTPUT: [
|
ATTR_OUTPUT: [
|
||||||
attr.asdict(profile)
|
attr.asdict(stream) for stream in self.sys_host.sound.outputs
|
||||||
for profile in self.sys_host.sound.output_profiles
|
],
|
||||||
|
ATTR_APPLICATION: [
|
||||||
|
attr.asdict(stream) for stream in self.sys_host.sound.applications
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -70,7 +89,7 @@ class APIAudio(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_audio.stats()
|
stats = await self.sys_plugins.audio.stats()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -87,21 +106,21 @@ class APIAudio(CoreSysAttributes):
|
|||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update Audio plugin."""
|
"""Update Audio plugin."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_audio.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_plugins.audio.latest_version)
|
||||||
|
|
||||||
if version == self.sys_audio.version:
|
if version == self.sys_plugins.audio.version:
|
||||||
raise APIError("Version {} is already in use".format(version))
|
raise APIError(f"Version {version} is already in use")
|
||||||
await asyncio.shield(self.sys_audio.update(version))
|
await asyncio.shield(self.sys_plugins.audio.update(version))
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return Audio Docker logs."""
|
"""Return Audio Docker logs."""
|
||||||
return self.sys_audio.logs()
|
return self.sys_plugins.audio.logs()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Audio plugin."""
|
"""Restart Audio plugin."""
|
||||||
return asyncio.shield(self.sys_audio.restart())
|
return asyncio.shield(self.sys_plugins.audio.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def reload(self, request: web.Request) -> Awaitable[None]:
|
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||||
@@ -110,18 +129,43 @@ class APIAudio(CoreSysAttributes):
|
|||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def set_volume(self, request: web.Request) -> None:
|
async def set_volume(self, request: web.Request) -> None:
|
||||||
"""Set Audio information."""
|
"""Set audio volume on stream."""
|
||||||
source: SourceType = SourceType(request.match_info.get("source"))
|
source: StreamType = StreamType(request.match_info.get("source"))
|
||||||
|
application: bool = request.path.endswith("application")
|
||||||
body = await api_validate(SCHEMA_VOLUME, request)
|
body = await api_validate(SCHEMA_VOLUME, request)
|
||||||
|
|
||||||
await asyncio.shield(
|
await asyncio.shield(
|
||||||
self.sys_host.sound.set_volume(source, body[ATTR_NAME], body[ATTR_VOLUME])
|
self.sys_host.sound.set_volume(
|
||||||
|
source, body[ATTR_INDEX], body[ATTR_VOLUME], application
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def set_mute(self, request: web.Request) -> None:
|
||||||
|
"""Mute audio volume on stream."""
|
||||||
|
source: StreamType = StreamType(request.match_info.get("source"))
|
||||||
|
application: bool = request.path.endswith("application")
|
||||||
|
body = await api_validate(SCHEMA_MUTE, request)
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_host.sound.set_mute(
|
||||||
|
source, body[ATTR_INDEX], body[ATTR_ACTIVE], application
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def set_default(self, request: web.Request) -> None:
|
async def set_default(self, request: web.Request) -> None:
|
||||||
"""Set Audio default sources."""
|
"""Set audio default stream."""
|
||||||
source: SourceType = SourceType(request.match_info.get("source"))
|
source: StreamType = StreamType(request.match_info.get("source"))
|
||||||
body = await api_validate(SCHEMA_DEFAULT, request)
|
body = await api_validate(SCHEMA_DEFAULT, request)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME]))
|
await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME]))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def set_profile(self, request: web.Request) -> None:
|
||||||
|
"""Set audio default sources."""
|
||||||
|
body = await api_validate(SCHEMA_PROFILE, request)
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_host.sound.ativate_profile(body[ATTR_CARD], body[ATTR_NAME])
|
||||||
|
)
|
||||||
|
63
supervisor/api/cli.py
Normal file
63
supervisor/api/cli.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""Init file for Supervisor HA cli RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_BLK_READ,
|
||||||
|
ATTR_BLK_WRITE,
|
||||||
|
ATTR_CPU_PERCENT,
|
||||||
|
ATTR_MEMORY_LIMIT,
|
||||||
|
ATTR_MEMORY_PERCENT,
|
||||||
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_NETWORK_RX,
|
||||||
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..validate import version_tag
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
|
class APICli(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for HA Cli functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return HA cli information."""
|
||||||
|
return {
|
||||||
|
ATTR_VERSION: self.sys_plugins.cli.version,
|
||||||
|
ATTR_VERSION_LATEST: self.sys_plugins.cli.latest_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return resource information."""
|
||||||
|
stats = await self.sys_plugins.cli.stats()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
|
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||||
|
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||||
|
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||||
|
ATTR_NETWORK_RX: stats.network_rx,
|
||||||
|
ATTR_NETWORK_TX: stats.network_tx,
|
||||||
|
ATTR_BLK_READ: stats.blk_read,
|
||||||
|
ATTR_BLK_WRITE: stats.blk_write,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update(self, request: web.Request) -> None:
|
||||||
|
"""Update HA CLI."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_plugins.cli.latest_version)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_plugins.cli.update(version))
|
@@ -1,19 +1,19 @@
|
|||||||
"""Init file for Supervisor network RESTful API."""
|
"""Init file for Supervisor network RESTful API."""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process, api_validate
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADDON,
|
ATTR_ADDON,
|
||||||
ATTR_UUID,
|
|
||||||
ATTR_CONFIG,
|
ATTR_CONFIG,
|
||||||
ATTR_DISCOVERY,
|
ATTR_DISCOVERY,
|
||||||
ATTR_SERVICE,
|
ATTR_SERVICE,
|
||||||
|
ATTR_SERVICES,
|
||||||
|
ATTR_UUID,
|
||||||
REQUEST_FROM,
|
REQUEST_FROM,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError, APIForbidden
|
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
|
from ..exceptions import APIError, APIForbidden
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
SCHEMA_DISCOVERY = vol.Schema(
|
SCHEMA_DISCOVERY = vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -43,6 +43,7 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
"""Show register services."""
|
"""Show register services."""
|
||||||
self._check_permission_ha(request)
|
self._check_permission_ha(request)
|
||||||
|
|
||||||
|
# Get available discovery
|
||||||
discovery = []
|
discovery = []
|
||||||
for message in self.sys_discovery.list_messages:
|
for message in self.sys_discovery.list_messages:
|
||||||
discovery.append(
|
discovery.append(
|
||||||
@@ -54,7 +55,13 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {ATTR_DISCOVERY: discovery}
|
# Get available services/add-ons
|
||||||
|
services = {}
|
||||||
|
for addon in self.sys_addons.all:
|
||||||
|
for name in addon.discovery:
|
||||||
|
services.setdefault(name, []).append(addon.slug)
|
||||||
|
|
||||||
|
return {ATTR_DISCOVERY: discovery, ATTR_SERVICES: services}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def set_discovery(self, request):
|
async def set_discovery(self, request):
|
||||||
@@ -64,7 +71,7 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
|
|
||||||
# Access?
|
# Access?
|
||||||
if body[ATTR_SERVICE] not in addon.discovery:
|
if body[ATTR_SERVICE] not in addon.discovery:
|
||||||
raise APIForbidden(f"Can't use discovery!")
|
raise APIForbidden("Can't use discovery!")
|
||||||
|
|
||||||
# Process discovery message
|
# Process discovery message
|
||||||
message = self.sys_discovery.send(addon, **body)
|
message = self.sys_discovery.send(addon, **body)
|
||||||
@@ -94,7 +101,7 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
|
|
||||||
# Permission
|
# Permission
|
||||||
if message.addon != addon.slug:
|
if message.addon != addon.slug:
|
||||||
raise APIForbidden(f"Can't remove discovery message")
|
raise APIForbidden("Can't remove discovery message")
|
||||||
|
|
||||||
self.sys_discovery.remove(message)
|
self.sys_discovery.remove(message)
|
||||||
return True
|
return True
|
||||||
|
@@ -11,7 +11,6 @@ from ..const import (
|
|||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_HOST,
|
ATTR_HOST,
|
||||||
ATTR_LATEST_VERSION,
|
|
||||||
ATTR_LOCALS,
|
ATTR_LOCALS,
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
@@ -20,11 +19,12 @@ from ..const import (
|
|||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
ATTR_SERVERS,
|
ATTR_SERVERS,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..validate import dns_server_list
|
from ..validate import dns_server_list, version_tag
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -32,7 +32,7 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_SERVERS): dns_server_list})
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_SERVERS): dns_server_list})
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
class APICoreDNS(CoreSysAttributes):
|
class APICoreDNS(CoreSysAttributes):
|
||||||
@@ -42,10 +42,10 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return DNS information."""
|
"""Return DNS information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_dns.version,
|
ATTR_VERSION: self.sys_plugins.dns.version,
|
||||||
ATTR_LATEST_VERSION: self.sys_dns.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.dns.latest_version,
|
||||||
ATTR_HOST: str(self.sys_docker.network.dns),
|
ATTR_HOST: str(self.sys_docker.network.dns),
|
||||||
ATTR_SERVERS: self.sys_dns.servers,
|
ATTR_SERVERS: self.sys_plugins.dns.servers,
|
||||||
ATTR_LOCALS: self.sys_host.network.dns_servers,
|
ATTR_LOCALS: self.sys_host.network.dns_servers,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,15 +55,15 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
if ATTR_SERVERS in body:
|
if ATTR_SERVERS in body:
|
||||||
self.sys_dns.servers = body[ATTR_SERVERS]
|
self.sys_plugins.dns.servers = body[ATTR_SERVERS]
|
||||||
self.sys_create_task(self.sys_dns.restart())
|
self.sys_create_task(self.sys_plugins.dns.restart())
|
||||||
|
|
||||||
self.sys_dns.save_data()
|
self.sys_plugins.dns.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_dns.stats()
|
stats = await self.sys_plugins.dns.stats()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -80,23 +80,23 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update DNS plugin."""
|
"""Update DNS plugin."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_dns.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_plugins.dns.latest_version)
|
||||||
|
|
||||||
if version == self.sys_dns.version:
|
if version == self.sys_plugins.dns.version:
|
||||||
raise APIError("Version {} is already in use".format(version))
|
raise APIError(f"Version {version} is already in use")
|
||||||
await asyncio.shield(self.sys_dns.update(version))
|
await asyncio.shield(self.sys_plugins.dns.update(version))
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return DNS Docker logs."""
|
"""Return DNS Docker logs."""
|
||||||
return self.sys_dns.logs()
|
return self.sys_plugins.dns.logs()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart CoreDNS plugin."""
|
"""Restart CoreDNS plugin."""
|
||||||
return asyncio.shield(self.sys_dns.restart())
|
return asyncio.shield(self.sys_plugins.dns.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def reset(self, request: web.Request) -> Awaitable[None]:
|
def reset(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Reset CoreDNS plugin."""
|
"""Reset CoreDNS plugin."""
|
||||||
return asyncio.shield(self.sys_dns.reset())
|
return asyncio.shield(self.sys_plugins.dns.reset())
|
||||||
|
@@ -1,20 +1,21 @@
|
|||||||
"""Init file for Supervisor hardware RESTful API."""
|
"""Init file for Supervisor hardware RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Awaitable, Dict, List
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .utils import api_process
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_SERIAL,
|
ATTR_AUDIO,
|
||||||
ATTR_DISK,
|
ATTR_DISK,
|
||||||
ATTR_GPIO,
|
ATTR_GPIO,
|
||||||
ATTR_AUDIO,
|
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
ATTR_OUTPUT,
|
ATTR_OUTPUT,
|
||||||
|
ATTR_SERIAL,
|
||||||
|
ATTR_USB,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .utils import api_process
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,13 +26,24 @@ class APIHardware(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Show hardware info."""
|
"""Show hardware info."""
|
||||||
|
serial: List[str] = []
|
||||||
|
|
||||||
|
# Create Serial list with device links
|
||||||
|
for device in self.sys_hardware.serial_devices:
|
||||||
|
serial.append(device.path.as_posix())
|
||||||
|
for link in device.links:
|
||||||
|
serial.append(link.as_posix())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_SERIAL: list(
|
ATTR_SERIAL: serial,
|
||||||
self.sys_hardware.serial_devices | self.sys_hardware.serial_by_id
|
|
||||||
),
|
|
||||||
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
||||||
ATTR_DISK: list(self.sys_hardware.disk_devices),
|
ATTR_DISK: [
|
||||||
|
device.path.as_posix() for device in self.sys_hardware.disk_devices
|
||||||
|
],
|
||||||
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
||||||
|
ATTR_USB: [
|
||||||
|
device.path.as_posix() for device in self.sys_hardware.usb_devices
|
||||||
|
],
|
||||||
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,16 +54,16 @@ class APIHardware(CoreSysAttributes):
|
|||||||
ATTR_AUDIO: {
|
ATTR_AUDIO: {
|
||||||
ATTR_INPUT: {
|
ATTR_INPUT: {
|
||||||
profile.name: profile.description
|
profile.name: profile.description
|
||||||
for profile in self.sys_host.sound.input_profiles
|
for profile in self.sys_host.sound.inputs
|
||||||
},
|
},
|
||||||
ATTR_OUTPUT: {
|
ATTR_OUTPUT: {
|
||||||
profile.name: profile.description
|
profile.name: profile.description
|
||||||
for profile in self.sys_host.sound.output_profiles
|
for profile in self.sys_host.sound.outputs
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def trigger(self, request: web.Request) -> None:
|
def trigger(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Trigger a udev device reload."""
|
"""Trigger a udev device reload."""
|
||||||
return asyncio.shield(self.sys_hardware.udev_trigger())
|
return asyncio.shield(self.sys_hardware.udev_trigger())
|
||||||
|
@@ -1,59 +0,0 @@
|
|||||||
"""Init file for Supervisor HassOS RESTful API."""
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from typing import Any, Awaitable, Dict
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from ..const import (
|
|
||||||
ATTR_BOARD,
|
|
||||||
ATTR_BOOT,
|
|
||||||
ATTR_VERSION,
|
|
||||||
ATTR_VERSION_CLI,
|
|
||||||
ATTR_VERSION_CLI_LATEST,
|
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
)
|
|
||||||
from ..coresys import CoreSysAttributes
|
|
||||||
from .utils import api_process, api_validate
|
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
|
||||||
|
|
||||||
|
|
||||||
class APIHassOS(CoreSysAttributes):
|
|
||||||
"""Handle RESTful API for HassOS functions."""
|
|
||||||
|
|
||||||
@api_process
|
|
||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
|
||||||
"""Return HassOS information."""
|
|
||||||
return {
|
|
||||||
ATTR_VERSION: self.sys_hassos.version,
|
|
||||||
ATTR_VERSION_CLI: self.sys_hassos.version_cli,
|
|
||||||
ATTR_VERSION_LATEST: self.sys_hassos.version_latest,
|
|
||||||
ATTR_VERSION_CLI_LATEST: self.sys_hassos.version_cli_latest,
|
|
||||||
ATTR_BOARD: self.sys_hassos.board,
|
|
||||||
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
|
||||||
}
|
|
||||||
|
|
||||||
@api_process
|
|
||||||
async def update(self, request: web.Request) -> None:
|
|
||||||
"""Update HassOS."""
|
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
|
||||||
version = body.get(ATTR_VERSION, self.sys_hassos.version_latest)
|
|
||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update(version))
|
|
||||||
|
|
||||||
@api_process
|
|
||||||
async def update_cli(self, request: web.Request) -> None:
|
|
||||||
"""Update HassOS CLI."""
|
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
|
||||||
version = body.get(ATTR_VERSION, self.sys_hassos.version_cli_latest)
|
|
||||||
|
|
||||||
await asyncio.shield(self.sys_hassos.update_cli(version))
|
|
||||||
|
|
||||||
@api_process
|
|
||||||
def config_sync(self, request: web.Request) -> Awaitable[None]:
|
|
||||||
"""Trigger config reload on HassOS."""
|
|
||||||
return asyncio.shield(self.sys_hassos.config_sync())
|
|
@@ -1,38 +1,39 @@
|
|||||||
"""Init file for Supervisor Home Assistant RESTful API."""
|
"""Init file for Supervisor Home Assistant RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Coroutine, Dict, Any
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
|
ATTR_AUDIO_INPUT,
|
||||||
|
ATTR_AUDIO_OUTPUT,
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_CUSTOM,
|
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_LAST_VERSION,
|
ATTR_IP_ADDRESS,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_USAGE,
|
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
|
ATTR_MEMORY_USAGE,
|
||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
ATTR_PORT,
|
ATTR_PORT,
|
||||||
ATTR_REFRESH_TOKEN,
|
ATTR_REFRESH_TOKEN,
|
||||||
ATTR_SSL,
|
ATTR_SSL,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_WAIT_BOOT,
|
ATTR_WAIT_BOOT,
|
||||||
ATTR_WATCHDOG,
|
ATTR_WATCHDOG,
|
||||||
ATTR_IP_ADDRESS,
|
|
||||||
CONTENT_TYPE_BINARY,
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..exceptions import APIError
|
from ..exceptions import APIError
|
||||||
from ..validate import docker_image, network_port
|
from ..validate import docker_image, network_port, version_tag
|
||||||
from .utils import api_process, api_process_raw, api_validate
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -41,17 +42,18 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
SCHEMA_OPTIONS = vol.Schema(
|
SCHEMA_OPTIONS = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_BOOT): vol.Boolean(),
|
vol.Optional(ATTR_BOOT): vol.Boolean(),
|
||||||
vol.Inclusive(ATTR_IMAGE, "custom_hass"): vol.Maybe(docker_image),
|
vol.Optional(ATTR_IMAGE): docker_image,
|
||||||
vol.Inclusive(ATTR_LAST_VERSION, "custom_hass"): vol.Maybe(vol.Coerce(str)),
|
|
||||||
vol.Optional(ATTR_PORT): network_port,
|
vol.Optional(ATTR_PORT): network_port,
|
||||||
vol.Optional(ATTR_SSL): vol.Boolean(),
|
vol.Optional(ATTR_SSL): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
|
vol.Optional(ATTR_WAIT_BOOT): vol.All(vol.Coerce(int), vol.Range(min=60)),
|
||||||
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
|
vol.Optional(ATTR_REFRESH_TOKEN): vol.Maybe(vol.Coerce(str)),
|
||||||
|
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
|
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): vol.Coerce(str)})
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
class APIHomeAssistant(CoreSysAttributes):
|
class APIHomeAssistant(CoreSysAttributes):
|
||||||
@@ -62,17 +64,20 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Return host information."""
|
"""Return host information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_homeassistant.version,
|
ATTR_VERSION: self.sys_homeassistant.version,
|
||||||
ATTR_LAST_VERSION: self.sys_homeassistant.latest_version,
|
ATTR_VERSION_LATEST: self.sys_homeassistant.latest_version,
|
||||||
ATTR_MACHINE: self.sys_homeassistant.machine,
|
ATTR_MACHINE: self.sys_homeassistant.machine,
|
||||||
ATTR_IP_ADDRESS: str(self.sys_homeassistant.ip_address),
|
ATTR_IP_ADDRESS: str(self.sys_homeassistant.ip_address),
|
||||||
ATTR_ARCH: self.sys_homeassistant.arch,
|
ATTR_ARCH: self.sys_homeassistant.arch,
|
||||||
ATTR_IMAGE: self.sys_homeassistant.image,
|
ATTR_IMAGE: self.sys_homeassistant.image,
|
||||||
ATTR_CUSTOM: self.sys_homeassistant.is_custom_image,
|
|
||||||
ATTR_BOOT: self.sys_homeassistant.boot,
|
ATTR_BOOT: self.sys_homeassistant.boot,
|
||||||
ATTR_PORT: self.sys_homeassistant.api_port,
|
ATTR_PORT: self.sys_homeassistant.api_port,
|
||||||
ATTR_SSL: self.sys_homeassistant.api_ssl,
|
ATTR_SSL: self.sys_homeassistant.api_ssl,
|
||||||
ATTR_WATCHDOG: self.sys_homeassistant.watchdog,
|
ATTR_WATCHDOG: self.sys_homeassistant.watchdog,
|
||||||
ATTR_WAIT_BOOT: self.sys_homeassistant.wait_boot,
|
ATTR_WAIT_BOOT: self.sys_homeassistant.wait_boot,
|
||||||
|
ATTR_AUDIO_INPUT: self.sys_homeassistant.audio_input,
|
||||||
|
ATTR_AUDIO_OUTPUT: self.sys_homeassistant.audio_output,
|
||||||
|
# Remove end of Q3 2020
|
||||||
|
"last_version": self.sys_homeassistant.latest_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -80,9 +85,8 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
"""Set Home Assistant options."""
|
"""Set Home Assistant options."""
|
||||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||||
|
|
||||||
if ATTR_IMAGE in body and ATTR_LAST_VERSION in body:
|
if ATTR_IMAGE in body:
|
||||||
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
self.sys_homeassistant.image = body[ATTR_IMAGE]
|
||||||
self.sys_homeassistant.latest_version = body[ATTR_LAST_VERSION]
|
|
||||||
|
|
||||||
if ATTR_BOOT in body:
|
if ATTR_BOOT in body:
|
||||||
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
||||||
@@ -102,12 +106,18 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
if ATTR_REFRESH_TOKEN in body:
|
if ATTR_REFRESH_TOKEN in body:
|
||||||
self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN]
|
self.sys_homeassistant.refresh_token = body[ATTR_REFRESH_TOKEN]
|
||||||
|
|
||||||
|
if ATTR_AUDIO_INPUT in body:
|
||||||
|
self.sys_homeassistant.audio_input = body[ATTR_AUDIO_INPUT]
|
||||||
|
|
||||||
|
if ATTR_AUDIO_OUTPUT in body:
|
||||||
|
self.sys_homeassistant.audio_output = body[ATTR_AUDIO_OUTPUT]
|
||||||
|
|
||||||
self.sys_homeassistant.save_data()
|
self.sys_homeassistant.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
async def stats(self, request: web.Request) -> Dict[Any, str]:
|
||||||
"""Return resource information."""
|
"""Return resource information."""
|
||||||
stats = await self.sys_homeassistant.stats()
|
stats = await self.sys_homeassistant.core.stats()
|
||||||
if not stats:
|
if not stats:
|
||||||
raise APIError("No stats available")
|
raise APIError("No stats available")
|
||||||
|
|
||||||
@@ -128,36 +138,36 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_homeassistant.latest_version)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_homeassistant.update(version))
|
await asyncio.shield(self.sys_homeassistant.core.update(version))
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def stop(self, request: web.Request) -> Coroutine:
|
def stop(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Stop Home Assistant."""
|
"""Stop Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.stop())
|
return asyncio.shield(self.sys_homeassistant.core.stop())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def start(self, request: web.Request) -> Coroutine:
|
def start(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Start Home Assistant."""
|
"""Start Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.start())
|
return asyncio.shield(self.sys_homeassistant.core.start())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def restart(self, request: web.Request) -> Coroutine:
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Restart Home Assistant."""
|
"""Restart Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.restart())
|
return asyncio.shield(self.sys_homeassistant.core.restart())
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
def rebuild(self, request: web.Request) -> Coroutine:
|
def rebuild(self, request: web.Request) -> Awaitable[None]:
|
||||||
"""Rebuild Home Assistant."""
|
"""Rebuild Home Assistant."""
|
||||||
return asyncio.shield(self.sys_homeassistant.rebuild())
|
return asyncio.shield(self.sys_homeassistant.core.rebuild())
|
||||||
|
|
||||||
@api_process_raw(CONTENT_TYPE_BINARY)
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
def logs(self, request: web.Request) -> Coroutine:
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
"""Return Home Assistant Docker logs."""
|
"""Return Home Assistant Docker logs."""
|
||||||
return self.sys_homeassistant.logs()
|
return self.sys_homeassistant.core.logs()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def check(self, request: web.Request) -> None:
|
async def check(self, request: web.Request) -> None:
|
||||||
"""Check configuration of Home Assistant."""
|
"""Check configuration of Home Assistant."""
|
||||||
result = await self.sys_homeassistant.check_config()
|
result = await self.sys_homeassistant.core.check_config()
|
||||||
if not result.valid:
|
if not result.valid:
|
||||||
raise APIError(result.log)
|
raise APIError(result.log)
|
||||||
|
@@ -1,26 +1,29 @@
|
|||||||
"""Init file for Supervisor host RESTful API."""
|
"""Init file for Supervisor host RESTful API."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
from typing import Awaitable
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from .utils import api_process, api_validate
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_HOSTNAME,
|
|
||||||
ATTR_FEATURES,
|
|
||||||
ATTR_KERNEL,
|
|
||||||
ATTR_OPERATING_SYSTEM,
|
|
||||||
ATTR_CHASSIS,
|
ATTR_CHASSIS,
|
||||||
ATTR_DEPLOYMENT,
|
|
||||||
ATTR_STATE,
|
|
||||||
ATTR_NAME,
|
|
||||||
ATTR_DESCRIPTON,
|
|
||||||
ATTR_SERVICES,
|
|
||||||
ATTR_CPE,
|
ATTR_CPE,
|
||||||
|
ATTR_DEPLOYMENT,
|
||||||
|
ATTR_DESCRIPTON,
|
||||||
|
ATTR_DISK_FREE,
|
||||||
|
ATTR_DISK_TOTAL,
|
||||||
|
ATTR_DISK_USED,
|
||||||
|
ATTR_FEATURES,
|
||||||
|
ATTR_HOSTNAME,
|
||||||
|
ATTR_KERNEL,
|
||||||
|
ATTR_NAME,
|
||||||
|
ATTR_OPERATING_SYSTEM,
|
||||||
|
ATTR_SERVICES,
|
||||||
|
ATTR_STATE,
|
||||||
|
CONTENT_TYPE_BINARY,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SERVICE = "service"
|
SERVICE = "service"
|
||||||
|
|
||||||
@@ -36,11 +39,14 @@ class APIHost(CoreSysAttributes):
|
|||||||
return {
|
return {
|
||||||
ATTR_CHASSIS: self.sys_host.info.chassis,
|
ATTR_CHASSIS: self.sys_host.info.chassis,
|
||||||
ATTR_CPE: self.sys_host.info.cpe,
|
ATTR_CPE: self.sys_host.info.cpe,
|
||||||
ATTR_FEATURES: self.sys_host.supperted_features,
|
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
|
||||||
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
|
||||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||||
|
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
||||||
|
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
||||||
|
ATTR_DISK_USED: self.sys_host.info.used_space,
|
||||||
|
ATTR_FEATURES: self.sys_host.supported_features,
|
||||||
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||||
|
ATTR_OPERATING_SYSTEM: self.sys_host.info.operating_system,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
@@ -107,3 +113,8 @@ class APIHost(CoreSysAttributes):
|
|||||||
"""Restart a service."""
|
"""Restart a service."""
|
||||||
unit = request.match_info.get(SERVICE)
|
unit = request.match_info.get(SERVICE)
|
||||||
return asyncio.shield(self.sys_host.services.restart(unit))
|
return asyncio.shield(self.sys_host.services.restart(unit))
|
||||||
|
|
||||||
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
|
"""Return host kernel logs."""
|
||||||
|
return self.sys_host.info.get_dmesg()
|
||||||
|
@@ -7,12 +7,14 @@ from aiohttp import web
|
|||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ARCH,
|
ATTR_ARCH,
|
||||||
ATTR_CHANNEL,
|
ATTR_CHANNEL,
|
||||||
|
ATTR_DOCKER,
|
||||||
ATTR_HASSOS,
|
ATTR_HASSOS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_LOGGING,
|
ATTR_LOGGING,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
ATTR_SUPERVISOR,
|
ATTR_SUPERVISOR,
|
||||||
|
ATTR_SUPPORTED,
|
||||||
ATTR_SUPPORTED_ARCH,
|
ATTR_SUPPORTED_ARCH,
|
||||||
ATTR_TIMEZONE,
|
ATTR_TIMEZONE,
|
||||||
)
|
)
|
||||||
@@ -32,11 +34,13 @@ class APIInfo(CoreSysAttributes):
|
|||||||
ATTR_SUPERVISOR: self.sys_supervisor.version,
|
ATTR_SUPERVISOR: self.sys_supervisor.version,
|
||||||
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
|
ATTR_HOMEASSISTANT: self.sys_homeassistant.version,
|
||||||
ATTR_HASSOS: self.sys_hassos.version,
|
ATTR_HASSOS: self.sys_hassos.version,
|
||||||
|
ATTR_DOCKER: self.sys_docker.info.version,
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
ATTR_MACHINE: self.sys_machine,
|
ATTR_MACHINE: self.sys_machine,
|
||||||
ATTR_ARCH: self.sys_arch.default,
|
ATTR_ARCH: self.sys_arch.default,
|
||||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||||
|
ATTR_SUPPORTED: self.sys_core.supported,
|
||||||
ATTR_CHANNEL: self.sys_updater.channel,
|
ATTR_CHANNEL: self.sys_updater.channel,
|
||||||
ATTR_LOGGING: self.sys_config.logging,
|
ATTR_LOGGING: self.sys_config.logging,
|
||||||
ATTR_TIMEZONE: self.sys_timezone,
|
ATTR_TIMEZONE: self.sys_config.timezone,
|
||||||
}
|
}
|
||||||
|
@@ -16,11 +16,11 @@ from multidict import CIMultiDict, istr
|
|||||||
from ..addons.addon import Addon
|
from ..addons.addon import Addon
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_ADMIN,
|
ATTR_ADMIN,
|
||||||
|
ATTR_ENABLE,
|
||||||
ATTR_ICON,
|
ATTR_ICON,
|
||||||
|
ATTR_PANELS,
|
||||||
ATTR_SESSION,
|
ATTR_SESSION,
|
||||||
ATTR_TITLE,
|
ATTR_TITLE,
|
||||||
ATTR_PANELS,
|
|
||||||
ATTR_ENABLE,
|
|
||||||
COOKIE_INGRESS,
|
COOKIE_INGRESS,
|
||||||
HEADER_TOKEN,
|
HEADER_TOKEN,
|
||||||
HEADER_TOKEN_OLD,
|
HEADER_TOKEN_OLD,
|
||||||
@@ -104,7 +104,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
except aiohttp.ClientError as err:
|
except aiohttp.ClientError as err:
|
||||||
_LOGGER.error("Ingress error: %s", err)
|
_LOGGER.error("Ingress error: %s", err)
|
||||||
|
|
||||||
raise HTTPBadGateway() from None
|
raise HTTPBadGateway()
|
||||||
|
|
||||||
async def _handle_websocket(
|
async def _handle_websocket(
|
||||||
self, request: web.Request, addon: Addon, path: str
|
self, request: web.Request, addon: Addon, path: str
|
||||||
@@ -129,7 +129,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
|
|
||||||
# Support GET query
|
# Support GET query
|
||||||
if request.query_string:
|
if request.query_string:
|
||||||
url = "{}?{}".format(url, request.query_string)
|
url = f"{url}?{request.query_string}"
|
||||||
|
|
||||||
# Start proxy
|
# Start proxy
|
||||||
async with self.sys_websession.ws_connect(
|
async with self.sys_websession.ws_connect(
|
||||||
@@ -191,7 +191,11 @@ class APIIngress(CoreSysAttributes):
|
|||||||
async for data in result.content.iter_chunked(4096):
|
async for data in result.content.iter_chunked(4096):
|
||||||
await response.write(data)
|
await response.write(data)
|
||||||
|
|
||||||
except (aiohttp.ClientError, aiohttp.ClientPayloadError) as err:
|
except (
|
||||||
|
aiohttp.ClientError,
|
||||||
|
aiohttp.ClientPayloadError,
|
||||||
|
ConnectionResetError,
|
||||||
|
) as err:
|
||||||
_LOGGER.error("Stream error with %s: %s", url, err)
|
_LOGGER.error("Stream error with %s: %s", url, err)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
77
supervisor/api/multicast.py
Normal file
77
supervisor/api/multicast.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""Init file for Supervisor Multicast RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_BLK_READ,
|
||||||
|
ATTR_BLK_WRITE,
|
||||||
|
ATTR_CPU_PERCENT,
|
||||||
|
ATTR_MEMORY_LIMIT,
|
||||||
|
ATTR_MEMORY_PERCENT,
|
||||||
|
ATTR_MEMORY_USAGE,
|
||||||
|
ATTR_NETWORK_RX,
|
||||||
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
|
CONTENT_TYPE_BINARY,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import APIError
|
||||||
|
from ..validate import version_tag
|
||||||
|
from .utils import api_process, api_process_raw, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
|
class APIMulticast(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for Multicast functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return Multicast information."""
|
||||||
|
return {
|
||||||
|
ATTR_VERSION: self.sys_plugins.multicast.version,
|
||||||
|
ATTR_VERSION_LATEST: self.sys_plugins.multicast.latest_version,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def stats(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return resource information."""
|
||||||
|
stats = await self.sys_plugins.multicast.stats()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
|
ATTR_MEMORY_USAGE: stats.memory_usage,
|
||||||
|
ATTR_MEMORY_LIMIT: stats.memory_limit,
|
||||||
|
ATTR_MEMORY_PERCENT: stats.memory_percent,
|
||||||
|
ATTR_NETWORK_RX: stats.network_rx,
|
||||||
|
ATTR_NETWORK_TX: stats.network_tx,
|
||||||
|
ATTR_BLK_READ: stats.blk_read,
|
||||||
|
ATTR_BLK_WRITE: stats.blk_write,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update(self, request: web.Request) -> None:
|
||||||
|
"""Update Multicast plugin."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_plugins.multicast.latest_version)
|
||||||
|
|
||||||
|
if version == self.sys_plugins.multicast.version:
|
||||||
|
raise APIError(f"Version {version} is already in use")
|
||||||
|
await asyncio.shield(self.sys_plugins.multicast.update(version))
|
||||||
|
|
||||||
|
@api_process_raw(CONTENT_TYPE_BINARY)
|
||||||
|
def logs(self, request: web.Request) -> Awaitable[bytes]:
|
||||||
|
"""Return Multicast Docker logs."""
|
||||||
|
return self.sys_plugins.multicast.logs()
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||||
|
"""Restart Multicast plugin."""
|
||||||
|
return asyncio.shield(self.sys_plugins.multicast.restart())
|
98
supervisor/api/network.py
Normal file
98
supervisor/api/network.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""REST API for network."""
|
||||||
|
import asyncio
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import (
|
||||||
|
ATTR_ADDRESS,
|
||||||
|
ATTR_DNS,
|
||||||
|
ATTR_GATEWAY,
|
||||||
|
ATTR_ID,
|
||||||
|
ATTR_INTERFACE,
|
||||||
|
ATTR_INTERFACES,
|
||||||
|
ATTR_IP_ADDRESS,
|
||||||
|
ATTR_METHOD,
|
||||||
|
ATTR_METHODS,
|
||||||
|
ATTR_NAMESERVERS,
|
||||||
|
ATTR_PRIMARY,
|
||||||
|
ATTR_TYPE,
|
||||||
|
)
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..dbus.const import InterfaceMethodSimple
|
||||||
|
from ..dbus.network.interface import NetworkInterface
|
||||||
|
from ..dbus.network.utils import int2ip
|
||||||
|
from ..exceptions import APIError
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
SCHEMA_UPDATE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(ATTR_ADDRESS): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_METHOD): vol.In(ATTR_METHODS),
|
||||||
|
vol.Optional(ATTR_GATEWAY): vol.Coerce(str),
|
||||||
|
vol.Optional(ATTR_DNS): [str],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def interface_information(interface: NetworkInterface) -> dict:
|
||||||
|
"""Return a dict with information of a interface to be used in th API."""
|
||||||
|
return {
|
||||||
|
ATTR_IP_ADDRESS: f"{interface.ip_address}/{interface.prefix}",
|
||||||
|
ATTR_GATEWAY: interface.gateway,
|
||||||
|
ATTR_ID: interface.id,
|
||||||
|
ATTR_TYPE: interface.type,
|
||||||
|
ATTR_NAMESERVERS: [int2ip(x) for x in interface.nameservers],
|
||||||
|
ATTR_METHOD: InterfaceMethodSimple.DHCP
|
||||||
|
if interface.method == "auto"
|
||||||
|
else InterfaceMethodSimple.STATIC,
|
||||||
|
ATTR_PRIMARY: interface.primary,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APINetwork(CoreSysAttributes):
|
||||||
|
"""Handle REST API for network."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return network information."""
|
||||||
|
interfaces = {}
|
||||||
|
for interface in self.sys_host.network.interfaces:
|
||||||
|
interfaces[
|
||||||
|
self.sys_host.network.interfaces[interface].name
|
||||||
|
] = interface_information(self.sys_host.network.interfaces[interface])
|
||||||
|
|
||||||
|
return {ATTR_INTERFACES: interfaces}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def interface_info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return network information for a interface."""
|
||||||
|
req_interface = request.match_info.get(ATTR_INTERFACE)
|
||||||
|
for interface in self.sys_host.network.interfaces:
|
||||||
|
if req_interface == self.sys_host.network.interfaces[interface].name:
|
||||||
|
return interface_information(
|
||||||
|
self.sys_host.network.interfaces[interface]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def interface_update(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Update the configuration of an interface."""
|
||||||
|
req_interface = request.match_info.get(ATTR_INTERFACE)
|
||||||
|
|
||||||
|
if not self.sys_host.network.interfaces.get(req_interface):
|
||||||
|
raise APIError(f"Interface {req_interface} does not exsist")
|
||||||
|
|
||||||
|
args = await api_validate(SCHEMA_UPDATE, request)
|
||||||
|
if not args:
|
||||||
|
raise APIError("You need to supply at least one option to update")
|
||||||
|
|
||||||
|
await asyncio.shield(
|
||||||
|
self.sys_host.network.interfaces[req_interface].update_settings(**args)
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_host.reload())
|
||||||
|
|
||||||
|
return await asyncio.shield(self.interface_info(request))
|
43
supervisor/api/os.py
Normal file
43
supervisor/api/os.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""Init file for Supervisor HassOS RESTful API."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from ..const import ATTR_BOARD, ATTR_BOOT, ATTR_VERSION, ATTR_VERSION_LATEST
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..validate import version_tag
|
||||||
|
from .utils import api_process, api_validate
|
||||||
|
|
||||||
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SCHEMA_VERSION = vol.Schema({vol.Optional(ATTR_VERSION): version_tag})
|
||||||
|
|
||||||
|
|
||||||
|
class APIOS(CoreSysAttributes):
|
||||||
|
"""Handle RESTful API for OS functions."""
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
|
"""Return OS information."""
|
||||||
|
return {
|
||||||
|
ATTR_VERSION: self.sys_hassos.version,
|
||||||
|
ATTR_VERSION_LATEST: self.sys_hassos.latest_version,
|
||||||
|
ATTR_BOARD: self.sys_hassos.board,
|
||||||
|
ATTR_BOOT: self.sys_dbus.rauc.boot_slot,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
async def update(self, request: web.Request) -> None:
|
||||||
|
"""Update OS."""
|
||||||
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
|
version = body.get(ATTR_VERSION, self.sys_hassos.latest_version)
|
||||||
|
|
||||||
|
await asyncio.shield(self.sys_hassos.update(version))
|
||||||
|
|
||||||
|
@api_process
|
||||||
|
def config_sync(self, request: web.Request) -> Awaitable[None]:
|
||||||
|
"""Trigger config reload on OS."""
|
||||||
|
return asyncio.shield(self.sys_hassos.config_sync())
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright 2018 Google Inc. All Rights Reserved.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
(self.webpackJsonp=self.webpackJsonp||[]).push([[2],{176:function(e,r,n){"use strict";n.r(r),n.d(r,"codeMirror",function(){return c}),n.d(r,"codeMirrorCss",function(){return i});var a=n(54),o=n.n(a),s=n(169),t=(n(170),n(171),n(11));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
|
|
||||||
//# sourceMappingURL=chunk.92a11ac1b80e0d7839d2.js.map
|
|
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["webpack:///./src/resources/codemirror.ts"],"names":["__webpack_require__","r","__webpack_exports__","d","codeMirror","codeMirrorCss","codemirror__WEBPACK_IMPORTED_MODULE_0__","codemirror__WEBPACK_IMPORTED_MODULE_0___default","n","codemirror_lib_codemirror_css__WEBPACK_IMPORTED_MODULE_1__","_common_dom_fire_event__WEBPACK_IMPORTED_MODULE_4__","_CodeMirror","commands","save","cm","fireEvent","getWrapperElement","_codeMirrorCss"],"mappings":"sFAAAA,EAAAC,EAAAC,GAAAF,EAAAG,EAAAD,EAAA,+BAAAE,IAAAJ,EAAAG,EAAAD,EAAA,kCAAAG,IAAA,IAAAC,EAAAN,EAAA,IAAAO,EAAAP,EAAAQ,EAAAF,GAAAG,EAAAT,EAAA,KAAAU,GAAAV,EAAA,KAAAA,EAAA,KAAAA,EAAA,KAQAW,IAAYC,SAASC,KAAO,SAACC,GAC3BC,YAAUD,EAAGE,oBAAqB,gBAE7B,IAAMZ,EAAkBO,IAClBN,EAAqBY","file":"chunk.92a11ac1b80e0d7839d2.js","sourcesContent":["// @ts-ignore\nimport _CodeMirror, { Editor } from \"codemirror\";\n// @ts-ignore\nimport _codeMirrorCss from \"codemirror/lib/codemirror.css\";\nimport \"codemirror/mode/yaml/yaml\";\nimport \"codemirror/mode/jinja2/jinja2\";\nimport { fireEvent } from \"../common/dom/fire_event\";\n\n_CodeMirror.commands.save = (cm: Editor) => {\n fireEvent(cm.getWrapperElement(), \"editor-save\");\n};\nexport const codeMirror: any = _CodeMirror;\nexport const codeMirrorCss: any = _codeMirrorCss;\n"],"sourceRoot":""}
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["webpack:///./hassio/src/ingress-view/hassio-ingress-view.ts"],"names":["customElement","HassioIngressView","property","this","_addon","html","_templateObject2","name","ingress_url","_templateObject","changedProps","_get","_getPrototypeOf","prototype","call","has","addon","route","path","substr","oldRoute","get","oldAddon","undefined","_fetchData","addonSlug","_ref","_ref2","regeneratorRuntime","async","_context","prev","next","awrap","Promise","all","fetchHassioAddonInfo","hass","Error","createHassioSession","sent","_slicedToArray","ingress","t0","console","error","alert","message","history","back","stop","css","_templateObject3","LitElement"],"mappings":"6/RAmBCA,YAAc,0CACTC,smBACHC,kEACAA,mEACAA,4EAED,WACE,OAAKC,KAAKC,OAMHC,YAAPC,IAC0BH,KAAKC,OAAOG,KACpBJ,KAAKC,OAAOI,aAPrBH,YAAPI,0CAYJ,SAAkBC,GAGhB,GAFAC,EAAAC,EApBEX,EAoBFY,WAAA,eAAAV,MAAAW,KAAAX,KAAmBO,GAEdA,EAAaK,IAAI,SAAtB,CAIA,IAAMC,EAAQb,KAAKc,MAAMC,KAAKC,OAAO,GAE/BC,EAAWV,EAAaW,IAAI,SAC5BC,EAAWF,EAAWA,EAASF,KAAKC,OAAO,QAAKI,EAElDP,GAASA,IAAUM,GACrBnB,KAAKqB,WAAWR,4CAIpB,SAAyBS,GAAzB,IAAAC,EAAAC,EAAAX,EAAA,OAAAY,mBAAAC,MAAA,SAAAC,GAAA,cAAAA,EAAAC,KAAAD,EAAAE,MAAA,cAAAF,EAAAC,KAAA,EAAAD,EAAAE,KAAA,EAAAJ,mBAAAK,MAE0BC,QAAQC,IAAI,CAChCC,YAAqBjC,KAAKkC,KAAMZ,GAAhC,MAAiD,WAC/C,MAAM,IAAIa,MAAM,iCAElBC,YAAoBpC,KAAKkC,MAAzB,MAAqC,WACnC,MAAM,IAAIC,MAAM,4CAPxB,UAAAZ,EAAAI,EAAAU,KAAAb,EAAAc,EAAAf,EAAA,IAEWV,EAFXW,EAAA,IAWee,QAXf,CAAAZ,EAAAE,KAAA,cAYY,IAAIM,MAAM,wCAZtB,OAeInC,KAAKC,OAASY,EAflBc,EAAAE,KAAA,iBAAAF,EAAAC,KAAA,GAAAD,EAAAa,GAAAb,EAAA,SAkBIc,QAAQC,MAARf,EAAAa,IACAG,MAAMhB,EAAAa,GAAII,SAAW,mCACrBC,QAAQC,OApBZ,yBAAAnB,EAAAoB,SAAA,KAAA/C,KAAA,qDAwBA,WACE,OAAOgD,YAAPC,UA7D4BC","file":"chunk.990ee58006b248f55d23.js","sourcesContent":["import {\n LitElement,\n customElement,\n property,\n TemplateResult,\n html,\n PropertyValues,\n CSSResult,\n css,\n} from \"lit-element\";\nimport { HomeAssistant, Route } from \"../../../src/types\";\nimport { createHassioSession } from \"../../../src/data/hassio/supervisor\";\nimport {\n HassioAddonDetails,\n fetchHassioAddonInfo,\n} from \"../../../src/data/hassio/addon\";\nimport \"../../../src/layouts/hass-loading-screen\";\nimport \"../../../src/layouts/hass-subpage\";\n\n@customElement(\"hassio-ingress-view\")\nclass HassioIngressView extends LitElement {\n @property() public hass!: HomeAssistant;\n @property() public route!: Route;\n @property() private _addon?: HassioAddonDetails;\n\n protected render(): TemplateResult {\n if (!this._addon) {\n return html`\n <hass-loading-screen></hass-loading-screen>\n `;\n }\n\n return html`\n <hass-subpage .header=${this._addon.name} hassio>\n <iframe src=${this._addon.ingress_url}></iframe>\n </hass-subpage>\n `;\n }\n\n protected updated(changedProps: PropertyValues) {\n super.firstUpdated(changedProps);\n\n if (!changedProps.has(\"route\")) {\n return;\n }\n\n const addon = this.route.path.substr(1);\n\n const oldRoute = changedProps.get(\"route\") as this[\"route\"] | undefined;\n const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;\n\n if (addon && addon !== oldAddon) {\n this._fetchData(addon);\n }\n }\n\n private async _fetchData(addonSlug: string) {\n try {\n const [addon] = await Promise.all([\n fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {\n throw new Error(\"Failed to fetch add-on info\");\n }),\n createHassioSession(this.hass).catch(() => {\n throw new Error(\"Failed to create an ingress session\");\n }),\n ]);\n\n if (!addon.ingress) {\n throw new Error(\"This add-on does not support ingress\");\n }\n\n this._addon = addon;\n } catch (err) {\n // tslint:disable-next-line\n console.error(err);\n alert(err.message || \"Unknown error starting ingress.\");\n history.back();\n }\n }\n\n static get styles(): CSSResult {\n return css`\n iframe {\n display: block;\n width: 100%;\n height: 100%;\n border: 0;\n }\n paper-icon-button {\n color: var(--text-primary-color);\n }\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"hassio-ingress-view\": HassioIngressView;\n }\n}\n"],"sourceRoot":""}
|
|
File diff suppressed because one or more lines are too long
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
|
|
||||||
This code may only be used under the BSD style license found at
|
|
||||||
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
|
||||||
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
|
||||||
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
|
||||||
part of the polymer project is also subject to an additional IP rights grant
|
|
||||||
found at http://polymer.github.io/PATENTS.txt
|
|
||||||
*/
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
(self.webpackJsonp=self.webpackJsonp||[]).push([[12],[,function(o,t){var n=document.createElement("template");n.setAttribute("style","display: none;"),n.innerHTML='<style>\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Thin"),\n local("Roboto-Thin"),\n url(/static/fonts/roboto/Roboto-Thin.woff2) format("woff2");\nfont-weight: 100;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Thin Italic"),\n local("Roboto-ThinItalic"),\n url(/static/fonts/roboto/Roboto-ThinItalic.woff2) format("woff2");\nfont-weight: 100;\nfont-style: italic;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Light"),\n local("Roboto-Light"),\n url(/static/fonts/roboto/Roboto-Light.woff2) format("woff2");\nfont-weight: 300;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Light Italic"),\n local("Roboto-LightItalic"),\n url(/static/fonts/roboto/Roboto-LightItalic.woff2) format("woff2");\nfont-weight: 300;\nfont-style: italic;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Regular"),\n local("Roboto-Regular"),\n url(/static/fonts/roboto/Roboto-Regular.woff2) format("woff2");\nfont-weight: 400;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Italic"),\n local("Roboto-Italic"),\n url(/static/fonts/roboto/Roboto-RegularItalic.woff2) format("woff2");\nfont-weight: 400;\nfont-style: italic;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Medium"),\n local("Roboto-Medium"),\n url(/static/fonts/roboto/Roboto-Medium.woff2) format("woff2");\nfont-weight: 500;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Medium Italic"),\n local("Roboto-MediumItalic"),\n url(/static/fonts/roboto/Roboto-MediumItalic.woff2) format("woff2");\nfont-weight: 500;\nfont-style: italic;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Bold"),\n local("Roboto-Bold"),\n url(/static/fonts/roboto/Roboto-Bold.woff2) format("woff2");\nfont-weight: 700;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Bold Italic"),\n local("Roboto-BoldItalic"),\n url(/static/fonts/roboto/Roboto-BoldItalic.woff2) format("woff2");\nfont-weight: 700;\nfont-style: italic;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Black"),\n local("Roboto-Black"),\n url(/static/fonts/roboto/Roboto-Black.woff2) format("woff2");\nfont-weight: 900;\nfont-style: normal;\n}\n@font-face {\nfont-family: "Roboto";\nsrc:\n local("Roboto Black Italic"),\n local("Roboto-BlackItalic"),\n url(/static/fonts/roboto/Roboto-BlackItalic.woff2) format("woff2");\nfont-weight: 900;\nfont-style: italic;\n}\n</style>',document.head.appendChild(n.content)}]]);
|
|
||||||
//# sourceMappingURL=chunk.b2dce600432c76a53d8c.js.map
|
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user