mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-09-23 11:59:42 +00:00
Compare commits
236 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 |
@@ -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
|
||||||
|
|
||||||
@@ -46,6 +46,3 @@ COPY requirements.txt requirements_tests.txt ./
|
|||||||
RUN pip3 install -r requirements.txt -r requirements_tests.txt \
|
RUN 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/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -7,7 +7,7 @@ exemptLabels:
|
|||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
# 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.12
|
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 }}
|
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
|
||||||
|
32
.pre-commit-config.yaml
Normal file
32
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 19.10b0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
args:
|
||||||
|
- --safe
|
||||||
|
- --quiet
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@@ -60,7 +60,7 @@
|
|||||||
{
|
{
|
||||||
"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
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Pylint",
|
"label": "Pylint",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pylint hassio",
|
"command": "pylint supervisor",
|
||||||
"dependsOn": ["Install all Requirements"],
|
"dependsOn": ["Install all Requirements"],
|
||||||
"group": {
|
"group": {
|
||||||
"kind": "test",
|
"kind": "test",
|
||||||
|
371
API.md
371
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,7 +36,7 @@ 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
|
||||||
{
|
{
|
||||||
@@ -42,11 +45,14 @@ The addons from `addons` are only installed one.
|
|||||||
"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,28 +236,28 @@ 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`
|
- GET `/host/logs`
|
||||||
|
|
||||||
Return host dmesg
|
Return the host log messages (dmesg).
|
||||||
|
|
||||||
- POST `/host/options`
|
- POST `/host/options`
|
||||||
|
|
||||||
@@ -311,7 +314,7 @@ Return host dmesg
|
|||||||
|
|
||||||
- POST `/os/config/sync`
|
- POST `/os/config/sync`
|
||||||
|
|
||||||
Load host configs from a USB stick.
|
Load host configurations from an USB stick.
|
||||||
|
|
||||||
### Hardware
|
### Hardware
|
||||||
|
|
||||||
@@ -354,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
|
||||||
|
|
||||||
@@ -368,7 +371,6 @@ Trigger an udev reload
|
|||||||
"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",
|
||||||
@@ -415,7 +417,7 @@ Output is the raw Docker log.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Image with `null` and version_latest with `null` reset this options.
|
Image with `null` and `version_latest` with `null` reset this options.
|
||||||
|
|
||||||
- POST/GET `/core/api`
|
- POST/GET `/core/api`
|
||||||
|
|
||||||
@@ -552,13 +554,9 @@ Get all available add-ons.
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 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`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -575,7 +573,7 @@ Get all available add-ons.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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`
|
||||||
|
|
||||||
@@ -588,28 +586,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`
|
||||||
|
|
||||||
@@ -630,7 +622,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
|
||||||
{
|
{
|
||||||
@@ -657,8 +649,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
|
||||||
|
|
||||||
@@ -673,7 +665,10 @@ Need ingress session as cookie.
|
|||||||
"uuid": "uuid",
|
"uuid": "uuid",
|
||||||
"config": {}
|
"config": {}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"services": {
|
||||||
|
"ozw": ["core_zwave"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -790,10 +785,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"
|
||||||
@@ -851,57 +848,6 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### DNS
|
|
||||||
|
|
||||||
- GET `/dns/info`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"host": "ip-address",
|
|
||||||
"version": "1",
|
|
||||||
"version_latest": "2",
|
|
||||||
"servers": ["dns://8.8.8.8"],
|
|
||||||
"locals": ["dns://xy"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/dns/options`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"servers": ["dns://8.8.8.8"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/dns/update`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "VERSION"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- POST `/dns/restart`
|
|
||||||
|
|
||||||
- POST `/dns/reset`
|
|
||||||
|
|
||||||
- GET `/dns/logs`
|
|
||||||
|
|
||||||
- GET `/dns/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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
- GET `/cli/info`
|
- GET `/cli/info`
|
||||||
@@ -936,18 +882,247 @@ return:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auth / SSO API
|
### Multicast
|
||||||
|
|
||||||
You can use the user system on homeassistant. We handle this auth system on
|
- GET `/multicast/info`
|
||||||
supervisor.
|
|
||||||
|
|
||||||
You can call post `/auth`
|
```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
|
||||||
|
|
||||||
|
- GET `/audio/info`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"host": "ip-address",
|
||||||
|
"version": "1",
|
||||||
|
"latest_version": "2",
|
||||||
|
"audio": {
|
||||||
|
"card": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 1,
|
||||||
|
"driver": "...",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"description": "...",
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"description": "...",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"default": false,
|
||||||
|
"card": "null|int",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"stream_index": 0,
|
||||||
|
"stream_type": "INPUT",
|
||||||
|
"volume": 0.3,
|
||||||
|
"mute": false,
|
||||||
|
"addon": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"name": "...",
|
||||||
|
"index": 0,
|
||||||
|
"description": "...",
|
||||||
|
"volume": 0.3,
|
||||||
|
"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": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/update`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": "VERSION"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/restart`
|
||||||
|
|
||||||
|
- POST `/audio/reload`
|
||||||
|
|
||||||
|
- GET `/audio/logs`
|
||||||
|
|
||||||
|
- POST `/audio/volume/input`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"volume": 0.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/volume/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"index": "...",
|
||||||
|
"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`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/default/output`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- POST `/audio/profile`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"card": "...",
|
||||||
|
"name": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- GET `/audio/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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication/SSO API
|
||||||
|
|
||||||
|
You can use the user system from Home Assistant. The auth system can be handled
|
||||||
|
with the Supervisor.
|
||||||
|
|
||||||
|
`/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`
|
||||||
|
|
||||||
|
12
Dockerfile
12
Dockerfile
@@ -1,8 +1,12 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM $BUILD_FROM
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
|
ENV \
|
||||||
|
S6_SERVICES_GRACETIME=10000
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
RUN apk add --no-cache \
|
RUN \
|
||||||
|
apk add --no-cache \
|
||||||
eudev \
|
eudev \
|
||||||
eudev-libs \
|
eudev-libs \
|
||||||
git \
|
git \
|
||||||
@@ -18,7 +22,8 @@ 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,7 +8,7 @@ trigger:
|
|||||||
pr: none
|
pr: none
|
||||||
variables:
|
variables:
|
||||||
- name: versionWheels
|
- name: versionWheels
|
||||||
value: '1.6.1-3.7-alpine3.11'
|
value: '1.13.0-3.8-alpine3.12'
|
||||||
resources:
|
resources:
|
||||||
repositories:
|
repositories:
|
||||||
- repository: azure
|
- repository: azure
|
||||||
@@ -23,4 +23,5 @@ jobs:
|
|||||||
builderVersion: '$(versionWheels)'
|
builderVersion: '$(versionWheels)'
|
||||||
builderApk: 'build-base;libffi-dev;openssl-dev'
|
builderApk: 'build-base;libffi-dev;openssl-dev'
|
||||||
builderPip: 'Cython'
|
builderPip: 'Cython'
|
||||||
|
skipBinary: 'aiohttp'
|
||||||
wheelsRequirement: 'requirements.txt'
|
wheelsRequirement: 'requirements.txt'
|
||||||
|
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"
|
||||||
|
9
codecov.yaml
Normal file
9
codecov.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
codecov:
|
||||||
|
branch: dev
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: 40
|
||||||
|
threshold: 0.09
|
||||||
|
comment: false
|
Submodule home-assistant-polymer updated: ddb525f6cd...77b25f5132
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==19.3.0
|
||||||
cchardet==2.1.6
|
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.0
|
||||||
docker==4.2.0
|
debugpy==1.0.0rc1
|
||||||
gitpython==3.1.0
|
docker==4.3.0
|
||||||
jinja2==2.11.1
|
gitpython==3.1.7
|
||||||
packaging==20.3
|
jinja2==2.11.2
|
||||||
ptvsd==4.3.2
|
packaging==20.4
|
||||||
pulsectl==20.2.4
|
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.16.4
|
||||||
uvloop==0.14.0
|
uvloop==0.14.0
|
||||||
voluptuous==0.11.7
|
voluptuous==0.11.7
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
flake8==3.7.9
|
|
||||||
pylint==2.4.4
|
|
||||||
pytest==5.4.1
|
|
||||||
pytest-timeout==1.3.4
|
|
||||||
pytest-aiohttp==0.3.0
|
|
||||||
black==19.10b0
|
black==19.10b0
|
||||||
|
codecov==2.1.8
|
||||||
|
coverage==5.2.1
|
||||||
|
flake8-docstrings==1.5.0
|
||||||
|
flake8==3.8.3
|
||||||
|
pre-commit==2.6.0
|
||||||
|
pydocstyle==5.0.2
|
||||||
|
pylint==2.5.3
|
||||||
|
pytest-aiohttp==0.3.0
|
||||||
|
pytest-cov==2.10.0
|
||||||
|
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
|
||||||
|
@@ -96,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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
|
3
setup.py
3
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,
|
||||||
@@ -37,6 +37,7 @@ setup(
|
|||||||
"supervisor.api",
|
"supervisor.api",
|
||||||
"supervisor.misc",
|
"supervisor.misc",
|
||||||
"supervisor.utils",
|
"supervisor.utils",
|
||||||
|
"supervisor.plugins",
|
||||||
"supervisor.snapshots",
|
"supervisor.snapshots",
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
@@ -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, STATE_STARTED, AddonStartup
|
||||||
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,7 +54,9 @@ 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]
|
||||||
|
if not local_only:
|
||||||
return self.store.get(addon_slug)
|
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."""
|
||||||
@@ -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
|
||||||
|
|
||||||
|
# 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)
|
await asyncio.sleep(self.sys_config.wait_boot)
|
||||||
|
|
||||||
async def shutdown(self, stage: str) -> None:
|
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 await addon.state() != STATE_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."""
|
||||||
@@ -132,9 +155,14 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.instance.install(store.version, store.image)
|
await addon.instance.install(store.version, store.image)
|
||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
self.data.uninstall(addon)
|
self.data.uninstall(addon)
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
else:
|
else:
|
||||||
self.local[slug] = addon
|
self.local[slug] = addon
|
||||||
|
|
||||||
|
# Reload ingress tokens
|
||||||
|
if addon.with_ingress:
|
||||||
|
await self.sys_ingress.reload()
|
||||||
|
|
||||||
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
_LOGGER.info("Add-on '%s' successfully installed", slug)
|
||||||
|
|
||||||
async def uninstall(self, slug: str) -> None:
|
async def uninstall(self, slug: str) -> None:
|
||||||
@@ -142,12 +170,12 @@ class AddonManager(CoreSysAttributes):
|
|||||||
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:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
await addon.remove_data()
|
await addon.remove_data()
|
||||||
|
|
||||||
@@ -166,6 +194,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 +221,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)
|
||||||
@@ -213,7 +246,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
with suppress(DockerAPIError):
|
with suppress(DockerAPIError):
|
||||||
await addon.instance.cleanup()
|
await addon.instance.cleanup()
|
||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
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)
|
||||||
@@ -230,12 +263,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:
|
||||||
@@ -251,7 +284,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.instance.remove()
|
await addon.instance.remove()
|
||||||
await addon.instance.install(addon.version)
|
await addon.instance.install(addon.version)
|
||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
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)
|
||||||
@@ -278,6 +311,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)
|
||||||
|
|
||||||
@@ -325,10 +359,10 @@ class AddonManager(CoreSysAttributes):
|
|||||||
for addon in self.installed:
|
for addon in self.installed:
|
||||||
if not await addon.instance.is_running():
|
if not await addon.instance.is_running():
|
||||||
continue
|
continue
|
||||||
self.sys_dns.add_host(
|
self.sys_plugins.dns.add_host(
|
||||||
ipv4=addon.ip_address, names=[addon.hostname], write=False
|
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()
|
||||||
|
@@ -43,6 +43,7 @@ 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,
|
||||||
@@ -51,7 +52,7 @@ from ..exceptions import (
|
|||||||
)
|
)
|
||||||
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
|
||||||
@@ -71,9 +72,8 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Async initialize of object."""
|
"""Async initialize of object."""
|
||||||
@@ -82,7 +82,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
@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
|
||||||
@@ -131,12 +131,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:
|
||||||
@@ -144,7 +141,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
|
||||||
|
|
||||||
@@ -154,7 +151,7 @@ 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
|
||||||
|
|
||||||
@@ -191,7 +188,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
|
||||||
|
|
||||||
@@ -201,7 +198,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)
|
||||||
@@ -233,6 +230,8 @@ class Addon(AddonModel):
|
|||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
webui = RE_WEBUI.match(url)
|
webui = RE_WEBUI.match(url)
|
||||||
|
if not webui:
|
||||||
|
return None
|
||||||
|
|
||||||
# extract arguments
|
# extract arguments
|
||||||
t_port = webui.group("t_port")
|
t_port = webui.group("t_port")
|
||||||
@@ -275,7 +274,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
|
||||||
|
|
||||||
@@ -311,50 +310,50 @@ class Addon(AddonModel):
|
|||||||
return input_data
|
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 audio input settings."""
|
"""Set audio input settings."""
|
||||||
self.persist[ATTR_AUDIO_INPUT] = value
|
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 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
|
||||||
@@ -367,7 +366,7 @@ class Addon(AddonModel):
|
|||||||
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),
|
||||||
)
|
)
|
||||||
@@ -377,9 +376,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
|
||||||
@@ -387,9 +386,9 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -490,14 +489,14 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
await self.instance.run()
|
await self.instance.run()
|
||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
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:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
async def restart(self) -> None:
|
async def restart(self) -> None:
|
||||||
"""Restart add-on."""
|
"""Restart add-on."""
|
||||||
@@ -517,31 +516,33 @@ class Addon(AddonModel):
|
|||||||
try:
|
try:
|
||||||
return await self.instance.stats()
|
return await self.instance.stats()
|
||||||
except DockerAPIError:
|
except DockerAPIError:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
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:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
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:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
ATTR_USER: self.persist,
|
ATTR_USER: self.persist,
|
||||||
@@ -552,32 +553,34 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# 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:
|
||||||
_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()
|
||||||
|
|
||||||
# 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:
|
||||||
_LOGGER.error("Can't backup AppArmor profile")
|
_LOGGER.error("Can't backup AppArmor profile")
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -585,7 +588,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()
|
||||||
|
|
||||||
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
_LOGGER.info("Finish snapshot for addon %s", self.slug)
|
||||||
|
|
||||||
@@ -602,13 +605,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()
|
||||||
|
|
||||||
# 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:
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
@@ -619,14 +622,14 @@ class Addon(AddonModel):
|
|||||||
self.slug,
|
self.slug,
|
||||||
humanize_error(data, err),
|
humanize_error(data, err),
|
||||||
)
|
)
|
||||||
raise AddonsError() from None
|
raise AddonsError()
|
||||||
|
|
||||||
# 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(
|
||||||
@@ -657,7 +660,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():
|
||||||
@@ -666,7 +669,7 @@ 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()
|
||||||
|
|
||||||
# Restore AppArmor
|
# Restore AppArmor
|
||||||
profile_file = Path(temp, "apparmor.txt")
|
profile_file = Path(temp, "apparmor.txt")
|
||||||
@@ -674,8 +677,10 @@ class Addon(AddonModel):
|
|||||||
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:
|
||||||
_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()
|
||||||
|
|
||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == STATE_STARTED:
|
if data[ATTR_STATE] == STATE_STARTED:
|
||||||
|
@@ -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
|
||||||
|
|
||||||
@@ -64,32 +65,36 @@ from ..const import (
|
|||||||
SECURITY_DISABLE,
|
SECURITY_DISABLE,
|
||||||
SECURITY_PROFILE,
|
SECURITY_PROFILE,
|
||||||
AddonStages,
|
AddonStages,
|
||||||
|
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:
|
||||||
@@ -180,7 +185,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]
|
||||||
|
|
||||||
@@ -190,9 +195,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:
|
||||||
@@ -212,6 +217,7 @@ 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)
|
||||||
|
if service:
|
||||||
services[service.group("service")] = service.group("rights")
|
services[service.group("service")] = service.group("rights")
|
||||||
|
|
||||||
return services
|
return services
|
||||||
@@ -282,7 +288,7 @@ 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, [])
|
||||||
|
|
||||||
@@ -446,7 +452,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)
|
||||||
|
|
||||||
@@ -461,6 +467,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
|
||||||
@@ -527,16 +535,21 @@ 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
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
return pkg_version.parse(
|
||||||
|
self.sys_homeassistant.version
|
||||||
|
) >= pkg_version.parse(version)
|
||||||
|
except pkg_version.InvalidVersion:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _image(self, config) -> str:
|
def _image(self, config) -> str:
|
||||||
|
@@ -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
|
||||||
@@ -85,12 +85,10 @@ from ..const import (
|
|||||||
PRIVILEGED_ALL,
|
PRIVILEGED_ALL,
|
||||||
ROLE_ALL,
|
ROLE_ALL,
|
||||||
ROLE_DEFAULT,
|
ROLE_DEFAULT,
|
||||||
STARTUP_ALL,
|
|
||||||
STARTUP_APPLICATION,
|
|
||||||
STARTUP_SERVICES,
|
|
||||||
STATE_STARTED,
|
STATE_STARTED,
|
||||||
STATE_STOPPED,
|
STATE_STOPPED,
|
||||||
AddonStages,
|
AddonStages,
|
||||||
|
AddonStartup,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSys
|
from ..coresys import CoreSys
|
||||||
from ..discovery.validate import valid_discovery_service
|
from ..discovery.validate import valid_discovery_service
|
||||||
@@ -100,6 +98,7 @@ from ..validate import (
|
|||||||
network_port,
|
network_port,
|
||||||
token,
|
token,
|
||||||
uuid_match,
|
uuid_match,
|
||||||
|
version_tag,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
@@ -121,7 +120,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+)?\))?"
|
||||||
@@ -149,32 +151,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
|
||||||
|
|
||||||
|
|
||||||
@@ -182,13 +185,13 @@ 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_INIT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||||
@@ -359,7 +362,7 @@ def validate_options(coresys: CoreSys, raw_schema: Dict[str, Any]):
|
|||||||
# normal value
|
# normal value
|
||||||
options[key] = _single_validate(coresys, typ, value, key)
|
options[key] = _single_validate(coresys, typ, value, key)
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
raise vol.Invalid(f"Type error for {key}") from None
|
raise vol.Invalid(f"Type error for {key}")
|
||||||
|
|
||||||
_check_missing_options(raw_schema, options, "root")
|
_check_missing_options(raw_schema, options, "root")
|
||||||
return options
|
return options
|
||||||
@@ -385,6 +388,9 @@ def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
|||||||
# 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}")
|
||||||
|
|
||||||
# prepare range
|
# prepare range
|
||||||
range_args = {}
|
range_args = {}
|
||||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||||
@@ -418,6 +424,11 @@ 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}")
|
||||||
|
|
||||||
|
# Process list
|
||||||
for element in data_list:
|
for element in data_list:
|
||||||
# Nested?
|
# Nested?
|
||||||
if isinstance(typ, dict):
|
if isinstance(typ, dict):
|
||||||
@@ -433,6 +444,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}")
|
||||||
|
|
||||||
|
# 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:
|
||||||
@@ -462,7 +478,7 @@ def _check_missing_options(origin, exists, root):
|
|||||||
|
|
||||||
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():
|
||||||
@@ -483,7 +499,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:
|
||||||
@@ -491,6 +507,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:
|
||||||
|
@@ -13,11 +13,12 @@ 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 .os import APIOS
|
|
||||||
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 .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
|
||||||
@@ -52,6 +53,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
self._register_host()
|
self._register_host()
|
||||||
self._register_os()
|
self._register_os()
|
||||||
self._register_cli()
|
self._register_cli()
|
||||||
|
self._register_multicast()
|
||||||
self._register_hardware()
|
self._register_hardware()
|
||||||
self._register_homeassistant()
|
self._register_homeassistant()
|
||||||
self._register_proxy()
|
self._register_proxy()
|
||||||
@@ -113,6 +115,21 @@ class RestAPI(CoreSysAttributes):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
def _register_hardware(self) -> None:
|
def _register_hardware(self) -> None:
|
||||||
"""Register hardware functions."""
|
"""Register hardware functions."""
|
||||||
api_hardware = APIHardware()
|
api_hardware = APIHardware()
|
||||||
@@ -363,7 +380,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)
|
||||||
|
|
||||||
|
@@ -54,7 +54,6 @@ from ..const import (
|
|||||||
ATTR_INSTALLED,
|
ATTR_INSTALLED,
|
||||||
ATTR_IP_ADDRESS,
|
ATTR_IP_ADDRESS,
|
||||||
ATTR_KERNEL_MODULES,
|
ATTR_KERNEL_MODULES,
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
ATTR_LOGO,
|
ATTR_LOGO,
|
||||||
ATTR_LONG_DESCRIPTION,
|
ATTR_LONG_DESCRIPTION,
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
@@ -83,6 +82,7 @@ from ..const import (
|
|||||||
ATTR_UDEV,
|
ATTR_UDEV,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
ATTR_VERSION,
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
ATTR_VIDEO,
|
ATTR_VIDEO,
|
||||||
ATTR_WEBUI,
|
ATTR_WEBUI,
|
||||||
BOOT_AUTO,
|
BOOT_AUTO,
|
||||||
@@ -122,9 +122,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,19 +135,20 @@ 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_NAME: addon.name,
|
||||||
ATTR_SLUG: addon.slug,
|
ATTR_SLUG: addon.slug,
|
||||||
@@ -166,11 +165,10 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
ATTR_LOGO: addon.with_logo,
|
ATTR_LOGO: addon.with_logo,
|
||||||
}
|
}
|
||||||
)
|
for addon in self.sys_addons.all
|
||||||
|
]
|
||||||
|
|
||||||
data_repositories = []
|
data_repositories = [
|
||||||
for repository in self.sys_store.all:
|
|
||||||
data_repositories.append(
|
|
||||||
{
|
{
|
||||||
ATTR_SLUG: repository.slug,
|
ATTR_SLUG: repository.slug,
|
||||||
ATTR_NAME: repository.name,
|
ATTR_NAME: repository.name,
|
||||||
@@ -178,8 +176,8 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_URL: repository.url,
|
ATTR_URL: repository.url,
|
||||||
ATTR_MAINTAINER: repository.maintainer,
|
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 +188,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,
|
||||||
@@ -257,7 +255,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_INGRESS_PANEL: None,
|
ATTR_INGRESS_PANEL: None,
|
||||||
}
|
}
|
||||||
|
|
||||||
if addon.is_installed:
|
if isinstance(addon, Addon) and addon.is_installed:
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
ATTR_STATE: await addon.state(),
|
ATTR_STATE: await addon.state(),
|
||||||
@@ -279,7 +277,7 @@ 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_secrets.reload()
|
||||||
@@ -312,7 +310,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
@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 +322,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 +340,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 +398,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 +408,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 +418,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 +428,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 +440,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
|
|
||||||
|
@@ -18,7 +18,6 @@ from ..const import (
|
|||||||
ATTR_HOST,
|
ATTR_HOST,
|
||||||
ATTR_INDEX,
|
ATTR_INDEX,
|
||||||
ATTR_INPUT,
|
ATTR_INPUT,
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
@@ -27,17 +26,19 @@ 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 StreamType
|
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(
|
||||||
{
|
{
|
||||||
@@ -68,8 +69,8 @@ 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_VERSION_LATEST: 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_CARD: [attr.asdict(card) for card in self.sys_host.sound.cards],
|
||||||
@@ -88,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,
|
||||||
@@ -105,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]:
|
||||||
|
@@ -7,8 +7,6 @@ from aiohttp import web
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_VERSION,
|
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
ATTR_BLK_READ,
|
ATTR_BLK_READ,
|
||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
@@ -17,13 +15,16 @@ from ..const import (
|
|||||||
ATTR_MEMORY_USAGE,
|
ATTR_MEMORY_USAGE,
|
||||||
ATTR_NETWORK_RX,
|
ATTR_NETWORK_RX,
|
||||||
ATTR_NETWORK_TX,
|
ATTR_NETWORK_TX,
|
||||||
|
ATTR_VERSION,
|
||||||
|
ATTR_VERSION_LATEST,
|
||||||
)
|
)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..validate import version_tag
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, 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})
|
||||||
|
|
||||||
|
|
||||||
class APICli(CoreSysAttributes):
|
class APICli(CoreSysAttributes):
|
||||||
@@ -33,14 +34,14 @@ class APICli(CoreSysAttributes):
|
|||||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||||
"""Return HA cli information."""
|
"""Return HA cli information."""
|
||||||
return {
|
return {
|
||||||
ATTR_VERSION: self.sys_cli.version,
|
ATTR_VERSION: self.sys_plugins.cli.version,
|
||||||
ATTR_VERSION_LATEST: self.sys_cli.latest_version,
|
ATTR_VERSION_LATEST: self.sys_plugins.cli.latest_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
@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_cli.stats()
|
stats = await self.sys_plugins.cli.stats()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ATTR_CPU_PERCENT: stats.cpu_percent,
|
ATTR_CPU_PERCENT: stats.cpu_percent,
|
||||||
@@ -57,6 +58,6 @@ class APICli(CoreSysAttributes):
|
|||||||
async def update(self, request: web.Request) -> None:
|
async def update(self, request: web.Request) -> None:
|
||||||
"""Update HA CLI."""
|
"""Update HA CLI."""
|
||||||
body = await api_validate(SCHEMA_VERSION, request)
|
body = await api_validate(SCHEMA_VERSION, request)
|
||||||
version = body.get(ATTR_VERSION, self.sys_cli.latest_version)
|
version = body.get(ATTR_VERSION, self.sys_plugins.cli.latest_version)
|
||||||
|
|
||||||
await asyncio.shield(self.sys_cli.update(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_VERSION_LATEST,
|
|
||||||
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_VERSION_LATEST: 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,20 @@
|
|||||||
"""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
|
||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
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__)
|
||||||
|
|
||||||
@@ -52,6 +52,6 @@ class APIHardware(CoreSysAttributes):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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,7 +1,7 @@
|
|||||||
"""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 Any, Coroutine, Dict
|
from typing import Any, Awaitable, Dict
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -14,10 +14,8 @@ from ..const import (
|
|||||||
ATTR_BLK_WRITE,
|
ATTR_BLK_WRITE,
|
||||||
ATTR_BOOT,
|
ATTR_BOOT,
|
||||||
ATTR_CPU_PERCENT,
|
ATTR_CPU_PERCENT,
|
||||||
ATTR_CUSTOM,
|
|
||||||
ATTR_IMAGE,
|
ATTR_IMAGE,
|
||||||
ATTR_IP_ADDRESS,
|
ATTR_IP_ADDRESS,
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
ATTR_MACHINE,
|
ATTR_MACHINE,
|
||||||
ATTR_MEMORY_LIMIT,
|
ATTR_MEMORY_LIMIT,
|
||||||
ATTR_MEMORY_PERCENT,
|
ATTR_MEMORY_PERCENT,
|
||||||
@@ -28,13 +26,14 @@ from ..const import (
|
|||||||
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,
|
||||||
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__)
|
||||||
@@ -43,8 +42,7 @@ _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_VERSION_LATEST, "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(),
|
||||||
@@ -55,7 +53,7 @@ SCHEMA_OPTIONS = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
||||||
@@ -71,7 +69,6 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
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,
|
||||||
@@ -88,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_VERSION_LATEST 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_VERSION_LATEST]
|
|
||||||
|
|
||||||
if ATTR_BOOT in body:
|
if ATTR_BOOT in body:
|
||||||
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
self.sys_homeassistant.boot = body[ATTR_BOOT]
|
||||||
@@ -145,27 +141,27 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
await asyncio.shield(self.sys_homeassistant.update(version))
|
await asyncio.shield(self.sys_homeassistant.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.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.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.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.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.logs()
|
||||||
|
|
||||||
|
@@ -11,6 +11,9 @@ from ..const import (
|
|||||||
ATTR_CPE,
|
ATTR_CPE,
|
||||||
ATTR_DEPLOYMENT,
|
ATTR_DEPLOYMENT,
|
||||||
ATTR_DESCRIPTON,
|
ATTR_DESCRIPTON,
|
||||||
|
ATTR_DISK_FREE,
|
||||||
|
ATTR_DISK_TOTAL,
|
||||||
|
ATTR_DISK_USED,
|
||||||
ATTR_FEATURES,
|
ATTR_FEATURES,
|
||||||
ATTR_HOSTNAME,
|
ATTR_HOSTNAME,
|
||||||
ATTR_KERNEL,
|
ATTR_KERNEL,
|
||||||
@@ -39,11 +42,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
|
||||||
|
@@ -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,10 +34,12 @@ 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_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())
|
@@ -6,18 +6,14 @@ from typing import Any, Awaitable, Dict
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from ..const import (
|
from ..const import ATTR_BOARD, ATTR_BOOT, ATTR_VERSION, ATTR_VERSION_LATEST
|
||||||
ATTR_BOARD,
|
|
||||||
ATTR_BOOT,
|
|
||||||
ATTR_VERSION,
|
|
||||||
ATTR_VERSION_LATEST,
|
|
||||||
)
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..validate import version_tag
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, 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})
|
||||||
|
|
||||||
|
|
||||||
class APIOS(CoreSysAttributes):
|
class APIOS(CoreSysAttributes):
|
||||||
|
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
@@ -1,2 +0,0 @@
|
|||||||
(self.webpackJsonp=self.webpackJsonp||[]).push([[2],{177: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(170),t=(n(171),n(172),n(11));o.a.commands.save=function(e){Object(t.a)(e.getWrapperElement(),"editor-save")};var c=o.a,i=s.a}}]);
|
|
||||||
//# sourceMappingURL=chunk.26756b56961f7bf94974.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.26756b56961f7bf94974.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.
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.
@@ -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","_callee","addonSlug","_ref","_ref2","regeneratorRuntime","wrap","_context","prev","next","Promise","all","fetchHassioAddonInfo","hass","Error","createHassioSession","sent","_slicedToArray","ingress","t0","console","error","alert","message","history","back","stop","css","_templateObject3","LitElement"],"mappings":"snSAmBCA,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,0FAIpB,SAAAS,EAAyBC,GAAzB,IAAAC,EAAAC,EAAAZ,EAAA,OAAAa,mBAAAC,KAAA,SAAAC,GAAA,cAAAA,EAAAC,KAAAD,EAAAE,MAAA,cAAAF,EAAAC,KAAA,EAAAD,EAAAE,KAAA,EAE0BC,QAAQC,IAAI,CAChCC,YAAqBjC,KAAKkC,KAAMX,GAAhC,MAAiD,WAC/C,MAAM,IAAIY,MAAM,iCAElBC,YAAoBpC,KAAKkC,MAAzB,MAAqC,WACnC,MAAM,IAAIC,MAAM,2CAPxB,UAAAX,EAAAI,EAAAS,KAAAZ,EAAAa,EAAAd,EAAA,IAEWX,EAFXY,EAAA,IAWec,QAXf,CAAAX,EAAAE,KAAA,cAYY,IAAIK,MAAM,wCAZtB,OAeInC,KAAKC,OAASY,EAflBe,EAAAE,KAAA,iBAAAF,EAAAC,KAAA,GAAAD,EAAAY,GAAAZ,EAAA,SAkBIa,QAAQC,MAARd,EAAAY,IACAG,MAAMf,EAAAY,GAAII,SAAW,mCACrBC,QAAQC,OApBZ,yBAAAlB,EAAAmB,SAAAzB,EAAAtB,KAAA,yRAwBA,WACE,OAAOgD,YAAPC,UA7D4BC","file":"chunk.35929da61d769e57c884.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
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,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
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,189 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright (c) 2017 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2019 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2016 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2017 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*! *****************************************************************************
|
|
||||||
Copyright (c) Microsoft Corporation. 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
|
|
||||||
|
|
||||||
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
|
||||||
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
|
||||||
MERCHANTABLITY OR NON-INFRINGEMENT.
|
|
||||||
|
|
||||||
See the Apache Version 2.0 License for specific language governing permissions
|
|
||||||
and limitations under the License.
|
|
||||||
***************************************************************************** */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright (c) 2018 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2019 Google Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2016 Google Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2014 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
@license
|
|
||||||
Copyright (c) 2016 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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* Fuse.js v3.4.4 - Lightweight fuzzy-search (http://fusejs.io)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2012-2017 Kirollos Risk (http://kiro.me)
|
|
||||||
* All Rights Reserved. Apache Software License 2.0
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*/
|
|
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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user