mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-19 22:19:21 +00:00
Compare commits
249 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
169c7ec004 | ||
![]() |
202e94615e | ||
![]() |
5fe2a815ad | ||
![]() |
a13a0b4770 | ||
![]() |
455bbc457b | ||
![]() |
d50fd3b580 | ||
![]() |
455e80b07c | ||
![]() |
291becbdf9 | ||
![]() |
33385b46a7 | ||
![]() |
df17668369 | ||
![]() |
43449c85bb | ||
![]() |
9e86eda05a | ||
![]() |
b288554d9c | ||
![]() |
bee55d08fb | ||
![]() |
7a542aeb38 | ||
![]() |
8d42513ba8 | ||
![]() |
89b7247aa2 | ||
![]() |
29132e7f4c | ||
![]() |
3fd9baf78e | ||
![]() |
f3aa3757ce | ||
![]() |
3760967f59 | ||
![]() |
f7ab8e0f7f | ||
![]() |
0e46ea12b2 | ||
![]() |
be226b2b01 | ||
![]() |
9e1239e192 | ||
![]() |
2eba3d85b0 | ||
![]() |
9b569268ab | ||
![]() |
31f5033dca | ||
![]() |
78d9c60be5 | ||
![]() |
baa86f09e5 | ||
![]() |
a4c4b39ba8 | ||
![]() |
752068bb56 | ||
![]() |
739cfbb273 | ||
![]() |
115af4cadf | ||
![]() |
ae3274e559 | ||
![]() |
c61f096dbd | ||
![]() |
ee7b5c42fd | ||
![]() |
85d527bfbc | ||
![]() |
dd561da819 | ||
![]() |
cb5932cb8b | ||
![]() |
8630adc54a | ||
![]() |
90d8832cd2 | ||
![]() |
3802b97bb6 | ||
![]() |
2de175e181 | ||
![]() |
6b7d437b00 | ||
![]() |
e2faf906de | ||
![]() |
bb44ce5cd2 | ||
![]() |
15544ae589 | ||
![]() |
e421284471 | ||
![]() |
785dc64787 | ||
![]() |
7e7e3a7876 | ||
![]() |
2b45c059e0 | ||
![]() |
14ec61f9bd | ||
![]() |
5cc72756f8 | ||
![]() |
44785ef3e2 | ||
![]() |
e60d858feb | ||
![]() |
b31ecfefcd | ||
![]() |
c342231052 | ||
![]() |
673666837e | ||
![]() |
c8f74d6c0d | ||
![]() |
7ed9de8014 | ||
![]() |
8650947f04 | ||
![]() |
a0ac8ced31 | ||
![]() |
2145bbea81 | ||
![]() |
480000ee7f | ||
![]() |
9ec2ad022e | ||
![]() |
43e40816dc | ||
![]() |
941ea3ee68 | ||
![]() |
a6e4b5159e | ||
![]() |
6f542d58d5 | ||
![]() |
b2b5fcee7d | ||
![]() |
59a82345a9 | ||
![]() |
b61a747876 | ||
![]() |
72e5d800d5 | ||
![]() |
c7aa6d4804 | ||
![]() |
b31063449d | ||
![]() |
477672459d | ||
![]() |
9c33897296 | ||
![]() |
100cfb57c5 | ||
![]() |
40b34071e7 | ||
![]() |
341833fd8f | ||
![]() |
f647fd6fea | ||
![]() |
53642f2389 | ||
![]() |
b9bdd655ab | ||
![]() |
e9e1b5b54f | ||
![]() |
be2163d635 | ||
![]() |
7f6dde3a5f | ||
![]() |
334aafee23 | ||
![]() |
1a20c18b19 | ||
![]() |
6e655b165c | ||
![]() |
d768b2fa1e | ||
![]() |
85bce1cfba | ||
![]() |
a798a2466f | ||
![]() |
2a5d8a5c82 | ||
![]() |
ea62171d98 | ||
![]() |
196389d5ee | ||
![]() |
1776021620 | ||
![]() |
c42a9124d3 | ||
![]() |
a44647b4cd | ||
![]() |
e0c3fd87c5 | ||
![]() |
ed8f2a85b7 | ||
![]() |
48f8553c75 | ||
![]() |
af4517fd1e | ||
![]() |
78e6a46318 | ||
![]() |
49ca923e51 | ||
![]() |
7ad22e0399 | ||
![]() |
bb8acc6065 | ||
![]() |
c0fa4a19e9 | ||
![]() |
3f1741dd18 | ||
![]() |
9ef02e4110 | ||
![]() |
15a6f38ebb | ||
![]() |
227f2e5a21 | ||
![]() |
517d6ee981 | ||
![]() |
184eeb7f49 | ||
![]() |
a3555c74e8 | ||
![]() |
657bafd458 | ||
![]() |
2ad5df420c | ||
![]() |
b24d489ec5 | ||
![]() |
6bb0210f1f | ||
![]() |
c1de50266a | ||
![]() |
562e02bc64 | ||
![]() |
f71ec7913a | ||
![]() |
d98baaf660 | ||
![]() |
72db591576 | ||
![]() |
509a37fc04 | ||
![]() |
17f62b6e86 | ||
![]() |
b09aee7644 | ||
![]() |
babcc0de0c | ||
![]() |
5cc47c9222 | ||
![]() |
833559a3b3 | ||
![]() |
b8a976b344 | ||
![]() |
10b14132b9 | ||
![]() |
18953f0b7c | ||
![]() |
19f5fba3aa | ||
![]() |
636bc3e61a | ||
![]() |
521037e1a6 | ||
![]() |
e024c3e38d | ||
![]() |
6a0206c1e7 | ||
![]() |
69a8a83528 | ||
![]() |
0307d700fa | ||
![]() |
919c383b41 | ||
![]() |
d331af4d5a | ||
![]() |
b5467d3c23 | ||
![]() |
3ef0040d66 | ||
![]() |
ec4dfd2172 | ||
![]() |
8f54d7c8e9 | ||
![]() |
b59e709dc0 | ||
![]() |
b236e6c886 | ||
![]() |
8acbb7d6f0 | ||
![]() |
49e4bc9381 | ||
![]() |
36106cc08d | ||
![]() |
8f1763abe2 | ||
![]() |
480eebc6cb | ||
![]() |
dccfffd979 | ||
![]() |
1a978f4762 | ||
![]() |
1fbc8f4060 | ||
![]() |
db3fc1421c | ||
![]() |
9ecd03db0e | ||
![]() |
f111ccb1b6 | ||
![]() |
a32341cc5d | ||
![]() |
f73e277230 | ||
![]() |
8d8587ca29 | ||
![]() |
88eb9511bf | ||
![]() |
e1068997ea | ||
![]() |
560e04c64a | ||
![]() |
621ec03971 | ||
![]() |
d14a47d3f7 | ||
![]() |
3e9de0c210 | ||
![]() |
01a6e074a5 | ||
![]() |
1434077f4e | ||
![]() |
3922175af1 | ||
![]() |
ec6852a8d7 | ||
![]() |
b0b908b4ae | ||
![]() |
5a00336ef1 | ||
![]() |
d53f5e21f4 | ||
![]() |
bd173fa333 | ||
![]() |
6b32fa31b6 | ||
![]() |
d1dba89e39 | ||
![]() |
b2f2806465 | ||
![]() |
3b70cd58a3 | ||
![]() |
6769bfd824 | ||
![]() |
bff4af2534 | ||
![]() |
aabf575ac5 | ||
![]() |
4aacaf6bd6 | ||
![]() |
268070e89c | ||
![]() |
4fa2134cc6 | ||
![]() |
97c35de49a | ||
![]() |
32fb550969 | ||
![]() |
3457441929 | ||
![]() |
32f0fc7a46 | ||
![]() |
c891a8f164 | ||
![]() |
991764af94 | ||
![]() |
1df447272e | ||
![]() |
f032ae757b | ||
![]() |
e529b2859e | ||
![]() |
d1cb2368fa | ||
![]() |
7b812184d1 | ||
![]() |
4f7ce1c6ee | ||
![]() |
44a5ac63db | ||
![]() |
0989ee88cc | ||
![]() |
ba8b72c2c8 | ||
![]() |
2dbb4583f4 | ||
![]() |
81ad42e029 | ||
![]() |
5d3695f8ba | ||
![]() |
c771694aa0 | ||
![]() |
1ac0fd4c10 | ||
![]() |
631ff8caef | ||
![]() |
7382182132 | ||
![]() |
4ff9da68ef | ||
![]() |
dee2998ee3 | ||
![]() |
1d43236211 | ||
![]() |
792bc610a3 | ||
![]() |
7a51c828c2 | ||
![]() |
fab6fcd5ac | ||
![]() |
c8e00ba160 | ||
![]() |
6245b6d823 | ||
![]() |
0c55bf20fc | ||
![]() |
f8fd7b5933 | ||
![]() |
6462eea2ef | ||
![]() |
3d79891249 | ||
![]() |
80763c4bbf | ||
![]() |
59102afd45 | ||
![]() |
0b085354db | ||
![]() |
e2a473baa3 | ||
![]() |
06e10fdd3c | ||
![]() |
fb4386a7ad | ||
![]() |
2d294f6841 | ||
![]() |
e09a839148 | ||
![]() |
d9c4dae739 | ||
![]() |
49853e92a4 | ||
![]() |
19620d6808 | ||
![]() |
f6bf44de1c | ||
![]() |
06fae59fc8 | ||
![]() |
5c25fcd84c | ||
![]() |
aa5297026f | ||
![]() |
841520b75e | ||
![]() |
d9e20307de | ||
![]() |
fda1b523ba | ||
![]() |
7cccbc682c | ||
![]() |
621eb4c4c0 | ||
![]() |
056926242f | ||
![]() |
84294f286f | ||
![]() |
b6bc6b8498 | ||
![]() |
845c935b39 | ||
![]() |
19d8de89df | ||
![]() |
cfae20a3ec | ||
![]() |
6db6ab96e6 | ||
![]() |
48695c6805 | ||
![]() |
d74908e3b5 |
@@ -2,6 +2,11 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.8
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
# Set Docker daemon config
|
||||
RUN \
|
||||
mkdir -p /etc/docker \
|
||||
&& echo '{"storage-driver": "vfs"}' > /etc/docker/daemon.json
|
||||
|
||||
# Install Node/Yarn for Frontent
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"appPort": "9123:8123",
|
||||
"postCreateCommand": "pre-commit install",
|
||||
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
|
||||
"containerEnv": {"NVM_DIR":"/usr/local/share/nvm"},
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
|
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Report a bug with the Supervisor
|
||||
name: Report a bug with the Supervisor on a supported System
|
||||
about: Report an issue related to the Home Assistant Supervisor.
|
||||
labels: bug
|
||||
---
|
||||
@@ -11,6 +11,10 @@ labels: bug
|
||||
- If you have a problem with an add-on, make an issue in it's repository.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Important: You can only fill a bug repport for an supported system! If you run an unsupported installation. This report would be closed without comment.
|
||||
-->
|
||||
|
||||
### Describe the issue
|
||||
|
||||
<!-- Provide as many details as possible. -->
|
||||
@@ -47,3 +51,19 @@ Paste supervisor logs here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### System Information
|
||||
|
||||
<details>
|
||||
<summary>System Information</summary>
|
||||
<!--
|
||||
- Use this command: ha info
|
||||
-->
|
||||
|
||||
```
|
||||
Paste system info here
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
|
106
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Bug Report Form
|
||||
about: Report an issue related to the Home Assistant Supervisor.
|
||||
labels: bug
|
||||
title: ""
|
||||
issue_body: true
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs with **supported** setups only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Describe the issue you are experiencing
|
||||
description: Provide a clear and concise description of what the bug is.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Environment
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What is the used version of the Supervisor?
|
||||
placeholder: supervisor-
|
||||
description: >
|
||||
Can be found in the Supervisor panel -> System tab. Starts with
|
||||
`supervisor-....`.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What type of installation are you running?
|
||||
description: >
|
||||
If you don't know, you can find it in: Configuration panel -> Info.
|
||||
options:
|
||||
- Home Assistant OS
|
||||
- Home Assistant Supervised
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Which operating system are you running on?
|
||||
options:
|
||||
- Home Assistant Operating System
|
||||
- Debian
|
||||
- Other (e.g., Raspbian/Raspberry Pi OS/Fedora)
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What is the version of your installed operating system?
|
||||
placeholder: "5.11"
|
||||
description: Can be found in the Supervisor panel -> System tab.
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: What version of Home Assistant Core is installed?
|
||||
placeholder: core-
|
||||
description: >
|
||||
Can be found in the Supervisor panel -> System tab. Starts with
|
||||
`core-....`.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# Details
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Steps to reproduce the issue
|
||||
description: |
|
||||
Please tell us exactly how to reproduce your issue.
|
||||
Provide clear and concise step by step instructions and add code snippets if needed.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Anything in the Supervisor logs that might be useful for us?
|
||||
description: >
|
||||
The Supervisor logs can be found in the Supervisor panel -> System tab.
|
||||
value: |
|
||||
```txt
|
||||
# Put your logs below this line
|
||||
|
||||
```
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Additional information
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you have any additional information for us, use the field below.
|
||||
Please note, you can attach screenshots or screen recordings here.
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,9 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Report a bug/issues with an unsupported Supervisor
|
||||
url: https://community.home-assistant.io
|
||||
about: The Community guide can help or was updated to solve your issue
|
||||
|
||||
- name: Report a bug for the Supervisor panel
|
||||
url: https://github.com/home-assistant/frontend/issues
|
||||
about: The Supervisor panel is a part of the Home Assistant frontend
|
||||
|
47
.github/release-drafter.yml
vendored
47
.github/release-drafter.yml
vendored
@@ -1,4 +1,49 @@
|
||||
change-template: "- #$NUMBER $TITLE @$AUTHOR"
|
||||
sort-direction: ascending
|
||||
|
||||
categories:
|
||||
- title: ":boom: Breaking Changes"
|
||||
label: "breaking-change"
|
||||
|
||||
- title: ":wrench: Build"
|
||||
label: "build"
|
||||
|
||||
- title: ":boar: Chore"
|
||||
label: "chore"
|
||||
|
||||
- title: ":sparkles: New Features"
|
||||
label: "new-feature"
|
||||
|
||||
- title: ":zap: Performance"
|
||||
label: "performance"
|
||||
|
||||
- title: ":recycle: Refactor"
|
||||
label: "refactor"
|
||||
|
||||
- title: ":green_heart: CI"
|
||||
label: "ci"
|
||||
|
||||
- title: ":bug: Bug Fixes"
|
||||
label: "bugfix"
|
||||
|
||||
- title: ":white_check_mark: Test"
|
||||
label: "test"
|
||||
|
||||
- title: ":arrow_up: Dependency Updates"
|
||||
label: "dependencies"
|
||||
|
||||
include-labels:
|
||||
- "breaking-change"
|
||||
- "build"
|
||||
- "chore"
|
||||
- "performance"
|
||||
- "refactor"
|
||||
- "new-feature"
|
||||
- "bugfix"
|
||||
- "dependencies"
|
||||
- "test"
|
||||
- "ci"
|
||||
|
||||
template: |
|
||||
## What's Changed
|
||||
|
||||
$CHANGES
|
||||
|
31
.github/workflows/builder.yml
vendored
31
.github/workflows/builder.yml
vendored
@@ -35,6 +35,7 @@ on:
|
||||
env:
|
||||
BUILD_NAME: supervisor
|
||||
BUILD_TYPE: supervisor
|
||||
WHEELS_TAG: 3.8-alpine3.13
|
||||
|
||||
jobs:
|
||||
init:
|
||||
@@ -45,6 +46,7 @@ jobs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
channel: ${{ steps.version.outputs.channel }}
|
||||
publish: ${{ steps.version.outputs.publish }}
|
||||
requirements: ${{ steps.requirements.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
@@ -61,6 +63,18 @@ jobs:
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
if: steps.version.outputs.publish == 'false'
|
||||
uses: jitterbit/get-changed-files@v1
|
||||
|
||||
- name: Check if requirements files changed
|
||||
id: requirements
|
||||
run: |
|
||||
if [[ "${{ steps.changed_files.outputs.all }}" =~ requirements.txt ]]; then
|
||||
echo "::set-output name=changed::true"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.arch }} supervisor
|
||||
needs: init
|
||||
@@ -74,6 +88,19 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build wheels
|
||||
if: needs.init.outputs.requirements == 'true'
|
||||
uses: home-assistant/wheels@master
|
||||
with:
|
||||
tag: ${{ env.WHEELS_TAG }}
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-host: ${{ secrets.WHEELS_HOST }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
wheels-user: wheels
|
||||
apk: "build-base;libffi-dev;openssl-dev;cargo"
|
||||
skip-binary: aiohttp
|
||||
requirements: "requirements.txt"
|
||||
|
||||
- name: Set version
|
||||
if: needs.init.outputs.publish == 'true'
|
||||
uses: home-assistant/actions/helpers/version@master
|
||||
@@ -92,7 +119,7 @@ jobs:
|
||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||
|
||||
- name: Build supervisor
|
||||
uses: home-assistant/builder@2020.11.0
|
||||
uses: home-assistant/builder@2021.02.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
@@ -134,7 +161,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build the Supervisor
|
||||
uses: home-assistant/builder@2020.11.0
|
||||
uses: home-assistant/builder@2021.02.0
|
||||
with:
|
||||
args: |
|
||||
--test \
|
||||
|
19
.github/workflows/check_pr_labels.yml
vendored
Normal file
19
.github/workflows/check_pr_labels.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Check PR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
types: [labeled, unlabeled, synchronize]
|
||||
|
||||
jobs:
|
||||
init:
|
||||
name: Check labels
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check labels
|
||||
run: |
|
||||
labels=$(jq -r '.pull_request.labels[] | .name' ${{github.event_path }})
|
||||
echo "$labels"
|
||||
if [ "$labels" == "cla-signed" ]; then
|
||||
exit 1
|
||||
fi
|
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -25,12 +25,12 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
pip install -r requirements.txt -r requirements_tests.txt
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -68,13 +68,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -112,13 +112,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -156,13 +156,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -188,13 +188,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -206,7 +206,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -229,13 +229,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -247,7 +247,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -273,13 +273,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -305,13 +305,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
exit 1
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_HOME }}
|
||||
key: |
|
||||
@@ -349,13 +349,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -390,7 +390,7 @@ jobs:
|
||||
-o console_output_style=count \
|
||||
tests
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v2.2.1
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
name: coverage-${{ matrix.python-version }}
|
||||
path: .coverage
|
||||
@@ -403,13 +403,13 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v2.1.4
|
||||
uses: actions/setup-python@v2.2.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v2.1.4
|
||||
with:
|
||||
path: venv
|
||||
key: |
|
||||
@@ -428,4 +428,4 @@ jobs:
|
||||
coverage report
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1.0.15
|
||||
uses: codecov/codecov-action@v1.2.1
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2.0.1
|
||||
- uses: dessant/lock-threads@v2.0.3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
33
.github/workflows/release-drafter.yml
vendored
33
.github/workflows/release-drafter.yml
vendored
@@ -2,14 +2,43 @@ name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
name: Release Drafter
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Find Next Version
|
||||
id: version
|
||||
run: |
|
||||
declare -i newpost
|
||||
latest=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
latestpre=$(echo "$latest" | awk '{split($0,a,"."); print a[1] "." a[2]}')
|
||||
datepre=$(date --utc '+%Y.%m')
|
||||
|
||||
|
||||
if [[ "$latestpre" == "$datepre" ]]; then
|
||||
latestpost=$(echo "$latest" | awk '{split($0,a,"."); print a[3]}')
|
||||
newpost=$latestpost+1
|
||||
else
|
||||
newpost=0
|
||||
fi
|
||||
|
||||
echo Current version: $latest
|
||||
echo New target version: $datepre.$newpost
|
||||
echo "::set-output name=version::$datepre.$newpost"
|
||||
|
||||
- name: Run Release Drafter
|
||||
uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
tag: ${{ steps.version.outputs.version }}
|
||||
name: ${{ steps.version.outputs.version }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3.0.13
|
||||
- uses: actions/stale@v3.0.17
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 60
|
||||
|
22
.vscode/tasks.json
vendored
22
.vscode/tasks.json
vendored
@@ -2,9 +2,9 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Run Testenv",
|
||||
"label": "Run Supervisor",
|
||||
"type": "shell",
|
||||
"command": "./scripts/test_env.sh",
|
||||
"command": "./scripts/run-supervisor.sh",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
@@ -16,7 +16,21 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Testenv CLI",
|
||||
"label": "Build Supervisor",
|
||||
"type": "shell",
|
||||
"command": "./scripts/build-supervisor.sh",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Run Supervisor CLI",
|
||||
"type": "shell",
|
||||
"command": "docker exec -ti hassio_cli /usr/bin/cli.sh",
|
||||
"group": {
|
||||
@@ -30,7 +44,7 @@
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Update UI",
|
||||
"label": "Update Supervisor Panel",
|
||||
"type": "shell",
|
||||
"command": "./scripts/update-frontend.sh",
|
||||
"group": {
|
||||
|
29
README.md
29
README.md
@@ -10,26 +10,23 @@ network settings or installing and updating software.
|
||||
|
||||
## Installation
|
||||
|
||||
Installation instructions can be found at https://home-assistant.io/hassio.
|
||||
Installation instructions can be found at https://home-assistant.io/getting-started.
|
||||
|
||||
## Development
|
||||
|
||||
The development of the Supervisor is not difficult but tricky.
|
||||
|
||||
- You can use the builder to create your Supervisor: https://github.com/home-assistant/hassio-builder
|
||||
- Access a HassOS device or VM and pull your Supervisor.
|
||||
- Set the developer modus with the CLI tool: `ha supervisor options --channel=dev`
|
||||
- Tag it as `homeassistant/xy-hassio-supervisor:latest`
|
||||
- Restart the service with `systemctl restart hassos-supervisor | journalctl -fu hassos-supervisor`
|
||||
- Test your changes
|
||||
|
||||
For small bugfixes or improvements, make a PR. For significant changes open a RFC first, please. Thanks.
|
||||
For small changes and bugfixes you can just follow this, but for significant changes open a RFC first.
|
||||
Development instructions can be found [here][development].
|
||||
|
||||
## Release
|
||||
|
||||
Follow is the relase circle process:
|
||||
Releases are done in 3 stages (channels) with this structure:
|
||||
|
||||
1. Merge master into dev / make sure version stay on dev
|
||||
2. Merge dev into master
|
||||
3. Bump the release on master
|
||||
4. Create a GitHub Release from master with the right version tag
|
||||
1. Pull requests are merged to the `main` branch.
|
||||
2. A new build is pushed to the `dev` stage.
|
||||
3. Releases are published.
|
||||
4. A new build is pushed to the `beta` stage.
|
||||
5. The [`stable.json`][stable] file is updated.
|
||||
6. The build that was pushed to `beta` will now be pushed to `stable`.
|
||||
|
||||
[development]: https://developers.home-assistant.io/docs/supervisor/development
|
||||
[stable]: https://github.com/home-assistant/version/blob/master/stable.json
|
||||
|
@@ -1,26 +0,0 @@
|
||||
# https://dev.azure.com/home-assistant
|
||||
|
||||
trigger:
|
||||
batch: true
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
pr: none
|
||||
variables:
|
||||
- name: versionWheels
|
||||
value: "1.13.0-3.8-alpine3.12"
|
||||
resources:
|
||||
repositories:
|
||||
- repository: azure
|
||||
type: github
|
||||
name: "home-assistant/ci-azure"
|
||||
endpoint: "home-assistant"
|
||||
|
||||
jobs:
|
||||
- template: templates/azp-job-wheels.yaml@azure
|
||||
parameters:
|
||||
builderVersion: "$(versionWheels)"
|
||||
builderApk: "build-base;libffi-dev;openssl-dev"
|
||||
builderPip: "Cython"
|
||||
skipBinary: "aiohttp"
|
||||
wheelsRequirement: "requirements.txt"
|
10
build.json
10
build.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"image": "homeassistant/{arch}-hassio-supervisor",
|
||||
"build_from": {
|
||||
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.12",
|
||||
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.12",
|
||||
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.12",
|
||||
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.12",
|
||||
"i386": "homeassistant/i386-base-python:3.8-alpine3.12"
|
||||
"aarch64": "homeassistant/aarch64-base-python:3.8-alpine3.13",
|
||||
"armhf": "homeassistant/armhf-base-python:3.8-alpine3.13",
|
||||
"armv7": "homeassistant/armv7-base-python:3.8-alpine3.13",
|
||||
"amd64": "homeassistant/amd64-base-python:3.8-alpine3.13",
|
||||
"i386": "homeassistant/i386-base-python:3.8-alpine3.13"
|
||||
},
|
||||
"labels": {
|
||||
"io.hass.type": "supervisor"
|
||||
|
Submodule home-assistant-polymer updated: 46f5589530...419e564441
@@ -1,20 +1,20 @@
|
||||
aiohttp==3.7.3
|
||||
aiohttp==3.7.4
|
||||
async_timeout==3.0.1
|
||||
atomicwrites==1.4.0
|
||||
attrs==20.3.0
|
||||
awesomeversion==21.2.3
|
||||
brotli==1.0.9
|
||||
cchardet==2.1.7
|
||||
colorlog==4.6.2
|
||||
colorlog==4.7.2
|
||||
cpe==1.2.1
|
||||
cryptography==3.2.1
|
||||
debugpy==1.2.0
|
||||
docker==4.3.1
|
||||
gitpython==3.1.11
|
||||
jinja2==2.11.2
|
||||
packaging==20.4
|
||||
pulsectl==20.5.1
|
||||
pytz==2020.4
|
||||
cryptography==3.4.6
|
||||
debugpy==1.2.1
|
||||
docker==4.4.4
|
||||
gitpython==3.1.14
|
||||
jinja2==2.11.3
|
||||
pulsectl==21.2.0
|
||||
pytz==2021.1
|
||||
pyudev==0.22.0
|
||||
ruamel.yaml==0.15.100
|
||||
sentry-sdk==0.19.4
|
||||
voluptuous==0.12.0
|
||||
sentry-sdk==0.20.3
|
||||
voluptuous==0.12.1
|
||||
|
@@ -1,14 +1,14 @@
|
||||
black==20.8b1
|
||||
codecov==2.1.10
|
||||
coverage==5.3
|
||||
codecov==2.1.11
|
||||
coverage==5.5
|
||||
flake8-docstrings==1.5.0
|
||||
flake8==3.8.4
|
||||
pre-commit==2.8.2
|
||||
pre-commit==2.10.1
|
||||
pydocstyle==5.1.1
|
||||
pylint==2.6.0
|
||||
pylint==2.7.2
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.12.0 # NB!: Versions over 0.12.0 breaks pytest-aiohttp (https://github.com/aio-libs/pytest-aiohttp/issues/16)
|
||||
pytest-cov==2.10.1
|
||||
pytest-cov==2.11.1
|
||||
pytest-timeout==1.4.2
|
||||
pytest==6.1.2
|
||||
pyupgrade==2.7.4
|
||||
pytest==6.2.2
|
||||
pyupgrade==2.10.0
|
||||
|
@@ -2,9 +2,17 @@
|
||||
# ==============================================================================
|
||||
# Start udev service
|
||||
# ==============================================================================
|
||||
|
||||
if bashio::fs.directory_exists /run/udev && ! bashio::fs.file_exists /run/.old_udev; then
|
||||
bashio::log.info "Using udev information from host"
|
||||
bashio::exit.ok
|
||||
fi
|
||||
|
||||
bashio::log.info "Setup udev backend inside container"
|
||||
udevd --daemon
|
||||
|
||||
bashio::log.info "Update udev information"
|
||||
touch /run/.old_udev
|
||||
if udevadm trigger; then
|
||||
udevadm settle || true
|
||||
else
|
||||
|
28
scripts/build-supervisor.sh
Executable file
28
scripts/build-supervisor.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
||||
|
||||
set -eE
|
||||
|
||||
DOCKER_TIMEOUT=30
|
||||
DOCKER_PID=0
|
||||
|
||||
function build_supervisor() {
|
||||
docker pull homeassistant/amd64-builder:dev
|
||||
|
||||
docker run --rm \
|
||||
--privileged \
|
||||
-v /run/docker.sock:/run/docker.sock \
|
||||
-v "$(pwd):/data" \
|
||||
homeassistant/amd64-builder:dev \
|
||||
--generic latest \
|
||||
--target /data \
|
||||
--test \
|
||||
--amd64 \
|
||||
--no-cache
|
||||
}
|
||||
|
||||
echo "Build Supervisor"
|
||||
start_docker
|
||||
trap "stop_docker" ERR
|
||||
|
||||
build_supervisor
|
75
scripts/test_env.sh → scripts/common.sh
Executable file → Normal file
75
scripts/test_env.sh → scripts/common.sh
Executable file → Normal file
@@ -1,9 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -eE
|
||||
|
||||
DOCKER_TIMEOUT=30
|
||||
DOCKER_PID=0
|
||||
|
||||
|
||||
function start_docker() {
|
||||
local starttime
|
||||
@@ -28,7 +23,6 @@ function start_docker() {
|
||||
echo "Docker was initialized"
|
||||
}
|
||||
|
||||
|
||||
function stop_docker() {
|
||||
local starttime
|
||||
local endtime
|
||||
@@ -54,17 +48,6 @@ function stop_docker() {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function build_supervisor() {
|
||||
docker pull homeassistant/amd64-builder:dev
|
||||
|
||||
docker run --rm --privileged \
|
||||
-v /run/docker.sock:/run/docker.sock -v "$(pwd):/data" \
|
||||
homeassistant/amd64-builder:dev \
|
||||
--generic dev -t /data --test --amd64 --no-cache
|
||||
}
|
||||
|
||||
|
||||
function cleanup_lastboot() {
|
||||
if [[ -f /workspaces/test_supervisor/config.json ]]; then
|
||||
echo "Cleaning up last boot"
|
||||
@@ -73,61 +56,3 @@ function cleanup_lastboot() {
|
||||
rm /tmp/config.json
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
function cleanup_docker() {
|
||||
echo "Cleaning up stopped containers..."
|
||||
docker rm $(docker ps -a -q) || true
|
||||
}
|
||||
|
||||
|
||||
function setup_test_env() {
|
||||
mkdir -p /workspaces/test_supervisor
|
||||
|
||||
echo "Start Supervisor"
|
||||
docker run --rm --privileged \
|
||||
--name hassio_supervisor \
|
||||
--security-opt seccomp=unconfined \
|
||||
--security-opt apparmor:unconfined \
|
||||
-v /run/docker.sock:/run/docker.sock \
|
||||
-v /run/dbus:/run/dbus \
|
||||
-v "/workspaces/test_supervisor":/data \
|
||||
-v /etc/machine-id:/etc/machine-id:ro \
|
||||
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
||||
-e SUPERVISOR_NAME=hassio_supervisor \
|
||||
-e SUPERVISOR_DEV=1 \
|
||||
-e SUPERVISOR_MACHINE="qemux86-64" \
|
||||
homeassistant/amd64-hassio-supervisor:latest
|
||||
|
||||
}
|
||||
|
||||
|
||||
function init_dbus() {
|
||||
if pgrep dbus-daemon; then
|
||||
echo "Dbus is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Startup dbus"
|
||||
mkdir -p /var/lib/dbus
|
||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
||||
|
||||
# cleanups
|
||||
mkdir -p /run/dbus
|
||||
rm -f /run/dbus/pid
|
||||
|
||||
# run
|
||||
dbus-daemon --system --print-address
|
||||
}
|
||||
|
||||
echo "Start Test-Env"
|
||||
|
||||
start_docker
|
||||
trap "stop_docker" ERR
|
||||
|
||||
build_supervisor
|
||||
cleanup_lastboot
|
||||
cleanup_docker
|
||||
init_dbus
|
||||
setup_test_env
|
||||
stop_docker
|
102
scripts/run-supervisor.sh
Executable file
102
scripts/run-supervisor.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
source "${BASH_SOURCE[0]%/*}/common.sh"
|
||||
source "${BASH_SOURCE[0]%/*}/build-supervisor.sh"
|
||||
|
||||
set -eE
|
||||
|
||||
DOCKER_TIMEOUT=30
|
||||
DOCKER_PID=0
|
||||
|
||||
|
||||
function cleanup_docker() {
|
||||
echo "Cleaning up stopped containers..."
|
||||
docker rm $(docker ps -a -q) || true
|
||||
}
|
||||
|
||||
|
||||
function run_supervisor() {
|
||||
mkdir -p /workspaces/test_supervisor
|
||||
|
||||
echo "Start Supervisor"
|
||||
docker run --rm --privileged \
|
||||
--name hassio_supervisor \
|
||||
--privileged \
|
||||
--security-opt seccomp=unconfined \
|
||||
--security-opt apparmor:unconfined \
|
||||
-v /run/docker.sock:/run/docker.sock:rw \
|
||||
-v /run/dbus:/run/dbus:ro \
|
||||
-v /run/udev:/run/udev:ro \
|
||||
-v "/workspaces/test_supervisor":/data:rw \
|
||||
-v /etc/machine-id:/etc/machine-id:ro \
|
||||
-v /workspaces/supervisor:/usr/src/supervisor \
|
||||
-e SUPERVISOR_SHARE="/workspaces/test_supervisor" \
|
||||
-e SUPERVISOR_NAME=hassio_supervisor \
|
||||
-e SUPERVISOR_DEV=1 \
|
||||
-e SUPERVISOR_MACHINE="qemux86-64" \
|
||||
homeassistant/amd64-hassio-supervisor:latest
|
||||
|
||||
}
|
||||
|
||||
|
||||
function init_dbus() {
|
||||
if pgrep dbus-daemon; then
|
||||
echo "Dbus is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Startup dbus"
|
||||
mkdir -p /var/lib/dbus
|
||||
cp -f /etc/machine-id /var/lib/dbus/machine-id
|
||||
|
||||
# cleanups
|
||||
mkdir -p /run/dbus
|
||||
rm -f /run/dbus/pid
|
||||
|
||||
# run
|
||||
dbus-daemon --system --print-address
|
||||
}
|
||||
|
||||
|
||||
function init_udev() {
|
||||
if pgrep systemd-udevd; then
|
||||
echo "udev is running"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Startup udev"
|
||||
|
||||
# cleanups
|
||||
mkdir -p /run/udev
|
||||
|
||||
# run
|
||||
/lib/systemd/systemd-udevd --daemon
|
||||
sleep 3
|
||||
udevadm trigger && udevadm settle
|
||||
}
|
||||
|
||||
echo "Run Supervisor"
|
||||
|
||||
start_docker
|
||||
trap "stop_docker" ERR
|
||||
|
||||
|
||||
if [ "$( docker container inspect -f '{{.State.Status}}' hassio_supervisor )" == "running" ]; then
|
||||
echo "Restarting Supervisor"
|
||||
docker rm -f hassio_supervisor
|
||||
init_dbus
|
||||
init_udev
|
||||
cleanup_lastboot
|
||||
run_supervisor
|
||||
stop_docker
|
||||
|
||||
else
|
||||
echo "Starting Supervisor"
|
||||
docker system prune -f
|
||||
build_supervisor
|
||||
cleanup_lastboot
|
||||
cleanup_docker
|
||||
init_dbus
|
||||
init_udev
|
||||
run_supervisor
|
||||
stop_docker
|
||||
fi
|
@@ -10,6 +10,7 @@ from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsJobError,
|
||||
AddonsNotSupportedError,
|
||||
CoreDNSError,
|
||||
DockerAPIError,
|
||||
@@ -147,7 +148,8 @@ class AddonManager(CoreSysAttributes):
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
]
|
||||
],
|
||||
on_condition=AddonsJobError,
|
||||
)
|
||||
async def install(self, slug: str) -> None:
|
||||
"""Install an add-on."""
|
||||
@@ -248,7 +250,8 @@ class AddonManager(CoreSysAttributes):
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
]
|
||||
],
|
||||
on_condition=AddonsJobError,
|
||||
)
|
||||
async def update(self, slug: str) -> None:
|
||||
"""Update add-on."""
|
||||
@@ -273,17 +276,18 @@ class AddonManager(CoreSysAttributes):
|
||||
|
||||
# Update instance
|
||||
last_state: AddonState = addon.state
|
||||
old_image = addon.image
|
||||
try:
|
||||
await addon.instance.update(store.version, store.image)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerError):
|
||||
await addon.instance.cleanup()
|
||||
except DockerError as err:
|
||||
raise AddonsError() from err
|
||||
else:
|
||||
self.data.update(store)
|
||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||
|
||||
_LOGGER.info("Add-on '%s' successfully updated", slug)
|
||||
self.data.update(store)
|
||||
|
||||
# Cleanup
|
||||
with suppress(DockerError):
|
||||
await addon.instance.cleanup(old_image=old_image)
|
||||
|
||||
# Setup/Fix AppArmor profile
|
||||
await addon.install_apparmor()
|
||||
@@ -297,7 +301,8 @@ class AddonManager(CoreSysAttributes):
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
]
|
||||
],
|
||||
on_condition=AddonsJobError,
|
||||
)
|
||||
async def rebuild(self, slug: str) -> None:
|
||||
"""Perform a rebuild of local build add-on."""
|
||||
@@ -339,7 +344,8 @@ class AddonManager(CoreSysAttributes):
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.HEALTHY,
|
||||
]
|
||||
],
|
||||
on_condition=AddonsJobError,
|
||||
)
|
||||
async def restore(self, slug: str, tar_file: tarfile.TarFile) -> None:
|
||||
"""Restore state of an add-on."""
|
||||
|
@@ -10,7 +10,7 @@ import secrets
|
||||
import shutil
|
||||
import tarfile
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
from typing import Any, Awaitable, Dict, List, Optional, Set
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
@@ -22,6 +22,8 @@ from ..const import (
|
||||
ATTR_AUDIO_OUTPUT,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BOOT,
|
||||
ATTR_DATA,
|
||||
ATTR_EVENT,
|
||||
ATTR_IMAGE,
|
||||
ATTR_INGRESS_ENTRY,
|
||||
ATTR_INGRESS_PANEL,
|
||||
@@ -32,8 +34,10 @@ from ..const import (
|
||||
ATTR_PORTS,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SLUG,
|
||||
ATTR_STATE,
|
||||
ATTR_SYSTEM,
|
||||
ATTR_TYPE,
|
||||
ATTR_USER,
|
||||
ATTR_UUID,
|
||||
ATTR_VERSION,
|
||||
@@ -50,18 +54,21 @@ from ..exceptions import (
|
||||
AddonConfigurationError,
|
||||
AddonsError,
|
||||
AddonsNotSupportedError,
|
||||
ConfigurationFileError,
|
||||
DockerError,
|
||||
DockerRequestError,
|
||||
HostAppArmorError,
|
||||
JsonFileError,
|
||||
)
|
||||
from ..hardware.data import Device
|
||||
from ..homeassistant.const import WSEvent, WSType
|
||||
from ..utils import check_port
|
||||
from ..utils.apparmor import adjust_profile
|
||||
from ..utils.json import read_json_file, write_json_file
|
||||
from ..utils.tar import atomic_contents_add, secure_path
|
||||
from .model import AddonModel, Data
|
||||
from .options import AddonOptions
|
||||
from .utils import remove_data
|
||||
from .validate import SCHEMA_ADDON_SNAPSHOT, validate_options
|
||||
from .validate import SCHEMA_ADDON_SNAPSHOT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -72,7 +79,7 @@ RE_WEBUI = re.compile(
|
||||
|
||||
RE_WATCHDOG = re.compile(
|
||||
r"^(?:(?P<s_prefix>https?|tcp)|\[PROTO:(?P<t_proto>\w+)\])"
|
||||
r":\/\/\[HOST\]:\[PORT:(?P<t_port>\d+)\](?P<s_suffix>.*)$"
|
||||
r":\/\/\[HOST\]:(?:\[PORT:)?(?P<t_port>\d+)\]?(?P<s_suffix>.*)$"
|
||||
)
|
||||
|
||||
RE_OLD_AUDIO = re.compile(r"\d+,\d+")
|
||||
@@ -87,12 +94,34 @@ class Addon(AddonModel):
|
||||
"""Initialize data holder."""
|
||||
super().__init__(coresys, slug)
|
||||
self.instance: DockerAddon = DockerAddon(coresys, self)
|
||||
self.state: AddonState = AddonState.UNKNOWN
|
||||
self._state: AddonState = AddonState.UNKNOWN
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return internal representation."""
|
||||
return f"<Addon: {self.slug}>"
|
||||
|
||||
@property
|
||||
def state(self) -> AddonState:
|
||||
"""Return state of the add-on."""
|
||||
return self._state
|
||||
|
||||
@state.setter
|
||||
def state(self, new_state: AddonState) -> None:
|
||||
"""Set the add-on into new state."""
|
||||
if self._state == new_state:
|
||||
return
|
||||
self._state = new_state
|
||||
self.sys_homeassistant.websocket.send_command(
|
||||
{
|
||||
ATTR_TYPE: WSType.SUPERVISOR_EVENT,
|
||||
ATTR_DATA: {
|
||||
ATTR_EVENT: WSEvent.ADDON,
|
||||
ATTR_SLUG: self.slug,
|
||||
ATTR_STATE: new_state,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def in_progress(self) -> bool:
|
||||
"""Return True if a task is in progress."""
|
||||
@@ -101,7 +130,7 @@ class Addon(AddonModel):
|
||||
async def load(self) -> None:
|
||||
"""Async initialize of object."""
|
||||
with suppress(DockerError):
|
||||
await self.instance.attach(tag=self.version)
|
||||
await self.instance.attach(version=self.version)
|
||||
|
||||
# Evaluate state
|
||||
if await self.instance.is_running():
|
||||
@@ -394,6 +423,34 @@ class Addon(AddonModel):
|
||||
"""Return path to asound config for Docker."""
|
||||
return Path(self.sys_config.path_extern_tmp, f"{self.slug}_pulse")
|
||||
|
||||
@property
|
||||
def devices(self) -> Set[Device]:
|
||||
"""Extract devices from add-on options."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
if isinstance(raw_schema, bool) or not raw_schema:
|
||||
return set()
|
||||
|
||||
# Validate devices
|
||||
options_validator = AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||
with suppress(vol.Invalid):
|
||||
options_validator(self.options)
|
||||
|
||||
return options_validator.devices
|
||||
|
||||
@property
|
||||
def pwned(self) -> Set[str]:
|
||||
"""Extract pwned data for add-on options."""
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
if isinstance(raw_schema, bool) or not raw_schema:
|
||||
return set()
|
||||
|
||||
# Validate devices
|
||||
options_validator = AddonOptions(self.coresys, raw_schema, self.name, self.slug)
|
||||
with suppress(vol.Invalid):
|
||||
options_validator(self.options)
|
||||
|
||||
return options_validator.pwned
|
||||
|
||||
def save_persist(self) -> None:
|
||||
"""Save data of add-on."""
|
||||
self.sys_addons.data.save_data()
|
||||
@@ -442,22 +499,19 @@ class Addon(AddonModel):
|
||||
|
||||
async def write_options(self) -> None:
|
||||
"""Return True if add-on options is written to data."""
|
||||
schema = self.schema
|
||||
options = self.options
|
||||
|
||||
# Update secrets for validation
|
||||
await self.sys_homeassistant.secrets.reload()
|
||||
|
||||
try:
|
||||
options = schema(options)
|
||||
options = self.schema(self.options)
|
||||
write_json_file(self.path_options, options)
|
||||
except vol.Invalid as ex:
|
||||
_LOGGER.error(
|
||||
"Add-on %s has invalid options: %s",
|
||||
self.slug,
|
||||
humanize_error(options, ex),
|
||||
humanize_error(self.options, ex),
|
||||
)
|
||||
except JsonFileError:
|
||||
except ConfigurationFileError:
|
||||
_LOGGER.error("Add-on %s can't write options", self.slug)
|
||||
else:
|
||||
_LOGGER.debug("Add-on %s write options: %s", self.slug, options)
|
||||
@@ -538,7 +592,9 @@ class Addon(AddonModel):
|
||||
|
||||
# create voluptuous
|
||||
new_schema = vol.Schema(
|
||||
vol.All(dict, validate_options(self.coresys, new_raw_schema))
|
||||
vol.All(
|
||||
dict, AddonOptions(self.coresys, new_raw_schema, self.name, self.slug)
|
||||
)
|
||||
)
|
||||
|
||||
# validate
|
||||
@@ -570,7 +626,7 @@ class Addon(AddonModel):
|
||||
try:
|
||||
await self.instance.run()
|
||||
except DockerRequestError as err:
|
||||
self.state = AddonState.STOPPED
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError() from err
|
||||
except DockerError as err:
|
||||
self.state = AddonState.ERROR
|
||||
@@ -581,8 +637,9 @@ class Addon(AddonModel):
|
||||
async def stop(self) -> None:
|
||||
"""Stop add-on."""
|
||||
try:
|
||||
return await self.instance.stop()
|
||||
await self.instance.stop()
|
||||
except DockerRequestError as err:
|
||||
self.state = AddonState.ERROR
|
||||
raise AddonsError() from err
|
||||
except DockerError as err:
|
||||
self.state = AddonState.ERROR
|
||||
@@ -653,7 +710,7 @@ class Addon(AddonModel):
|
||||
# Store local configs/state
|
||||
try:
|
||||
write_json_file(temp_path.joinpath("addon.json"), data)
|
||||
except JsonFileError as err:
|
||||
except ConfigurationFileError as err:
|
||||
_LOGGER.error("Can't save meta for %s", self.slug)
|
||||
raise AddonsError() from err
|
||||
|
||||
@@ -709,7 +766,7 @@ class Addon(AddonModel):
|
||||
# Read snapshot data
|
||||
try:
|
||||
data = read_json_file(Path(temp, "addon.json"))
|
||||
except JsonFileError as err:
|
||||
except ConfigurationFileError as err:
|
||||
raise AddonsError() from err
|
||||
|
||||
# Validate
|
||||
|
@@ -4,16 +4,24 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from ..const import ATTR_ARGS, ATTR_BUILD_FROM, ATTR_SQUASH, META_ADDON
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
from ..const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_BUILD_FROM,
|
||||
ATTR_SQUASH,
|
||||
FILE_SUFFIX_CONFIGURATION,
|
||||
META_ADDON,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..utils.json import JsonConfig
|
||||
from ..utils.common import FileConfiguration, find_one_filetype
|
||||
from .validate import SCHEMA_BUILD_CONFIG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import AnyAddon
|
||||
|
||||
|
||||
class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
class AddonBuild(FileConfiguration, CoreSysAttributes):
|
||||
"""Handle build options for add-ons."""
|
||||
|
||||
def __init__(self, coresys: CoreSys, addon: AnyAddon) -> None:
|
||||
@@ -22,7 +30,10 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
self.addon = addon
|
||||
|
||||
super().__init__(
|
||||
Path(self.addon.path_location, "build.json"), SCHEMA_BUILD_CONFIG
|
||||
find_one_filetype(
|
||||
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
||||
),
|
||||
SCHEMA_BUILD_CONFIG,
|
||||
)
|
||||
|
||||
def save_data(self):
|
||||
@@ -46,11 +57,21 @@ class AddonBuild(JsonConfig, CoreSysAttributes):
|
||||
"""Return additional Docker build arguments."""
|
||||
return self._data[ATTR_ARGS]
|
||||
|
||||
def get_docker_args(self, version):
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""Return true if the build env is valid."""
|
||||
return all(
|
||||
[
|
||||
self.addon.path_location.is_dir(),
|
||||
Path(self.addon.path_location, "Dockerfile").is_file(),
|
||||
]
|
||||
)
|
||||
|
||||
def get_docker_args(self, version: AwesomeVersion):
|
||||
"""Create a dict with Docker build arguments."""
|
||||
args = {
|
||||
"path": str(self.addon.path_location),
|
||||
"tag": f"{self.addon.image}:{version}",
|
||||
"tag": f"{self.addon.image}:{version!s}",
|
||||
"pull": True,
|
||||
"forcerm": True,
|
||||
"squash": self.squash,
|
||||
|
@@ -1,6 +1,5 @@
|
||||
"""Init file for Supervisor add-on data."""
|
||||
from copy import deepcopy
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..const import (
|
||||
@@ -13,16 +12,14 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..store.addon import AddonStore
|
||||
from ..utils.json import JsonConfig
|
||||
from ..utils.common import FileConfiguration
|
||||
from .addon import Addon
|
||||
from .validate import SCHEMA_ADDONS_FILE
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
Config = Dict[str, Any]
|
||||
|
||||
|
||||
class AddonsData(JsonConfig, CoreSysAttributes):
|
||||
class AddonsData(FileConfiguration, CoreSysAttributes):
|
||||
"""Hold data for installed Add-ons inside Supervisor."""
|
||||
|
||||
def __init__(self, coresys: CoreSys):
|
||||
|
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Any, Awaitable, Dict, List, Optional
|
||||
|
||||
from packaging import version as pkg_version
|
||||
from awesomeversion import AwesomeVersion, AwesomeVersionException
|
||||
import voluptuous as vol
|
||||
|
||||
from ..const import (
|
||||
@@ -12,7 +12,6 @@ from ..const import (
|
||||
ATTR_ARCH,
|
||||
ATTR_AUDIO,
|
||||
ATTR_AUTH_API,
|
||||
ATTR_AUTO_UART,
|
||||
ATTR_BOOT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DEVICES,
|
||||
@@ -46,6 +45,7 @@ from ..const import (
|
||||
ATTR_PORTS,
|
||||
ATTR_PORTS_DESCRIPTION,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_REALTIME,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SERVICES,
|
||||
@@ -56,6 +56,8 @@ from ..const import (
|
||||
ATTR_STDIN,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
@@ -71,7 +73,9 @@ from ..const import (
|
||||
AddonStartup,
|
||||
)
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from .validate import RE_SERVICE, RE_VOLUME, schema_ui_options, validate_options
|
||||
from ..docker.const import Capabilities
|
||||
from .options import AddonOptions, UiOptions
|
||||
from .validate import RE_SERVICE, RE_VOLUME
|
||||
|
||||
Data = Dict[str, Any]
|
||||
|
||||
@@ -183,12 +187,17 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_REPOSITORY]
|
||||
|
||||
@property
|
||||
def latest_version(self) -> str:
|
||||
def translations(self) -> dict:
|
||||
"""Return add-on translations."""
|
||||
return self.data[ATTR_TRANSLATIONS]
|
||||
|
||||
@property
|
||||
def latest_version(self) -> AwesomeVersion:
|
||||
"""Return latest version of add-on."""
|
||||
return self.data[ATTR_VERSION]
|
||||
|
||||
@property
|
||||
def version(self) -> Optional[str]:
|
||||
def version(self) -> AwesomeVersion:
|
||||
"""Return version of add-on."""
|
||||
return self.data[ATTR_VERSION]
|
||||
|
||||
@@ -296,14 +305,9 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data[ATTR_HOST_DBUS]
|
||||
|
||||
@property
|
||||
def devices(self) -> List[str]:
|
||||
"""Return devices of add-on."""
|
||||
return self.data.get(ATTR_DEVICES, [])
|
||||
|
||||
@property
|
||||
def tmpfs(self) -> Optional[str]:
|
||||
"""Return tmpfs of add-on."""
|
||||
return self.data.get(ATTR_TMPFS)
|
||||
def static_devices(self) -> List[Path]:
|
||||
"""Return static devices of add-on."""
|
||||
return [Path(node) for node in self.data.get(ATTR_DEVICES, [])]
|
||||
|
||||
@property
|
||||
def environment(self) -> Optional[Dict[str, str]]:
|
||||
@@ -311,7 +315,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return self.data.get(ATTR_ENVIRONMENT)
|
||||
|
||||
@property
|
||||
def privileged(self) -> List[str]:
|
||||
def privileged(self) -> List[Capabilities]:
|
||||
"""Return list of privilege."""
|
||||
return self.data.get(ATTR_PRIVILEGED, [])
|
||||
|
||||
@@ -387,7 +391,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
@property
|
||||
def with_uart(self) -> bool:
|
||||
"""Return True if we should map all UART device."""
|
||||
return self.data[ATTR_AUTO_UART]
|
||||
return self.data[ATTR_UART]
|
||||
|
||||
@property
|
||||
def with_udev(self) -> bool:
|
||||
@@ -399,6 +403,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on access to kernel modules."""
|
||||
return self.data[ATTR_KERNEL_MODULES]
|
||||
|
||||
@property
|
||||
def with_realtime(self) -> bool:
|
||||
"""Return True if the add-on need realtime schedule functions."""
|
||||
return self.data[ATTR_REALTIME]
|
||||
|
||||
@property
|
||||
def with_full_access(self) -> bool:
|
||||
"""Return True if the add-on want full access to hardware."""
|
||||
@@ -409,6 +418,11 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
"""Return True if the add-on read access to devicetree."""
|
||||
return self.data[ATTR_DEVICETREE]
|
||||
|
||||
@property
|
||||
def with_tmpfs(self) -> Optional[str]:
|
||||
"""Return if tmp is in memory of add-on."""
|
||||
return self.data[ATTR_TMPFS]
|
||||
|
||||
@property
|
||||
def access_auth_api(self) -> bool:
|
||||
"""Return True if the add-on access to login/auth backend."""
|
||||
@@ -522,8 +536,10 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
raw_schema = self.data[ATTR_SCHEMA]
|
||||
|
||||
if isinstance(raw_schema, bool):
|
||||
return vol.Schema(dict)
|
||||
return vol.Schema(vol.All(dict, validate_options(self.coresys, raw_schema)))
|
||||
raw_schema = {}
|
||||
return vol.Schema(
|
||||
vol.All(dict, AddonOptions(self.coresys, raw_schema, self.name, self.slug))
|
||||
)
|
||||
|
||||
@property
|
||||
def schema_ui(self) -> Optional[List[Dict[str, Any]]]:
|
||||
@@ -532,7 +548,7 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
|
||||
if isinstance(raw_schema, bool):
|
||||
return None
|
||||
return schema_ui_options(raw_schema)
|
||||
return UiOptions(self.coresys)(raw_schema)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Compaired add-on objects."""
|
||||
@@ -554,15 +570,10 @@ class AddonModel(CoreSysAttributes, ABC):
|
||||
return False
|
||||
|
||||
# Home Assistant
|
||||
version = config.get(ATTR_HOMEASSISTANT)
|
||||
if version is None or self.sys_homeassistant.version is None:
|
||||
return True
|
||||
|
||||
version: Optional[AwesomeVersion] = config.get(ATTR_HOMEASSISTANT)
|
||||
try:
|
||||
return pkg_version.parse(
|
||||
self.sys_homeassistant.version
|
||||
) >= pkg_version.parse(version)
|
||||
except pkg_version.InvalidVersion:
|
||||
return self.sys_homeassistant.version >= version
|
||||
except (AwesomeVersionException, TypeError):
|
||||
return True
|
||||
|
||||
def _image(self, config) -> str:
|
||||
|
410
supervisor/addons/options.py
Normal file
410
supervisor/addons/options.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""Add-on Options / UI rendering."""
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any, Dict, List, Set, Union
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from ..coresys import CoreSys, CoreSysAttributes
|
||||
from ..exceptions import HardwareNotFound
|
||||
from ..hardware.const import UdevSubsystem
|
||||
from ..hardware.data import Device
|
||||
from ..validate import network_port
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
_STR = "str"
|
||||
_INT = "int"
|
||||
_FLOAT = "float"
|
||||
_BOOL = "bool"
|
||||
_PASSWORD = "password"
|
||||
_EMAIL = "email"
|
||||
_URL = "url"
|
||||
_PORT = "port"
|
||||
_MATCH = "match"
|
||||
_LIST = "list"
|
||||
_DEVICE = "device"
|
||||
|
||||
RE_SCHEMA_ELEMENT = re.compile(
|
||||
r"^(?:"
|
||||
r"|bool"
|
||||
r"|email"
|
||||
r"|url"
|
||||
r"|port"
|
||||
r"|device(?:\((?P<filter>subsystem=[a-z]+)\))?"
|
||||
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
||||
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
||||
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
||||
r"|match\((?P<match>.*)\)"
|
||||
r"|list\((?P<list>.+)\)"
|
||||
r")\??$"
|
||||
)
|
||||
|
||||
_SCHEMA_LENGTH_PARTS = (
|
||||
"i_min",
|
||||
"i_max",
|
||||
"f_min",
|
||||
"f_max",
|
||||
"s_min",
|
||||
"s_max",
|
||||
"p_min",
|
||||
"p_max",
|
||||
)
|
||||
|
||||
|
||||
class AddonOptions(CoreSysAttributes):
|
||||
"""Validate Add-ons Options."""
|
||||
|
||||
def __init__(
|
||||
self, coresys: CoreSys, raw_schema: Dict[str, Any], name: str, slug: str
|
||||
):
|
||||
"""Validate schema."""
|
||||
self.coresys: CoreSys = coresys
|
||||
self.raw_schema: Dict[str, Any] = raw_schema
|
||||
self.devices: Set[Device] = set()
|
||||
self.pwned: Set[str] = set()
|
||||
self._name = name
|
||||
self._slug = slug
|
||||
|
||||
def __call__(self, struct):
|
||||
"""Create schema validator for add-ons options."""
|
||||
options = {}
|
||||
|
||||
# read options
|
||||
for key, value in struct.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if key not in self.raw_schema:
|
||||
_LOGGER.warning(
|
||||
"Option '%s' does not exist in the schema for %s (%s)",
|
||||
key,
|
||||
self._name,
|
||||
self._slug,
|
||||
)
|
||||
continue
|
||||
|
||||
typ = self.raw_schema[key]
|
||||
try:
|
||||
if isinstance(typ, list):
|
||||
# nested value list
|
||||
options[key] = self._nested_validate_list(typ[0], value, key)
|
||||
elif isinstance(typ, dict):
|
||||
# nested value dict
|
||||
options[key] = self._nested_validate_dict(typ, value, key)
|
||||
else:
|
||||
# normal value
|
||||
options[key] = self._single_validate(typ, value, key)
|
||||
except (IndexError, KeyError):
|
||||
raise vol.Invalid(
|
||||
f"Type error for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
self._check_missing_options(self.raw_schema, options, "root")
|
||||
return options
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
def _single_validate(self, typ: str, value: Any, key: str):
|
||||
"""Validate a single element."""
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid(
|
||||
f"Missing required option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Lookup secret
|
||||
if str(value).startswith("!secret "):
|
||||
secret: str = value.partition(" ")[2]
|
||||
value = self.sys_homeassistant.secrets.get(secret)
|
||||
if value is None:
|
||||
raise vol.Invalid(
|
||||
f"Unknown secret '{secret}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||
|
||||
if not match:
|
||||
raise vol.Invalid(
|
||||
f"Unknown type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# prepare range
|
||||
range_args = {}
|
||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||
group_value = match.group(group_name)
|
||||
if group_value:
|
||||
range_args[group_name[2:]] = float(group_value)
|
||||
|
||||
if typ.startswith(_STR) or typ.startswith(_PASSWORD):
|
||||
if typ.startswith(_PASSWORD) and value:
|
||||
self.pwned.add(hashlib.sha1(str(value).encode()).hexdigest())
|
||||
return vol.All(str(value), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(_INT):
|
||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(_FLOAT):
|
||||
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(_BOOL):
|
||||
return vol.Boolean()(value)
|
||||
elif typ.startswith(_EMAIL):
|
||||
return vol.Email()(value)
|
||||
elif typ.startswith(_URL):
|
||||
return vol.Url()(value)
|
||||
elif typ.startswith(_PORT):
|
||||
return network_port(value)
|
||||
elif typ.startswith(_MATCH):
|
||||
return vol.Match(match.group("match"))(str(value))
|
||||
elif typ.startswith(_LIST):
|
||||
return vol.In(match.group("list").split("|"))(str(value))
|
||||
elif typ.startswith(_DEVICE):
|
||||
try:
|
||||
device = self.sys_hardware.get_by_path(Path(value))
|
||||
except HardwareNotFound:
|
||||
raise vol.Invalid(
|
||||
f"Device '{value}' does not exists! in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Have filter
|
||||
if match.group("filter"):
|
||||
str_filter = match.group("filter")
|
||||
device_filter = _create_device_filter(str_filter)
|
||||
if device not in self.sys_hardware.filter_devices(**device_filter):
|
||||
raise vol.Invalid(
|
||||
f"Device '{value}' don't match the filter {str_filter}! in {self._name} ({self._slug})"
|
||||
)
|
||||
|
||||
# Device valid
|
||||
self.devices.add(device)
|
||||
return str(device.path)
|
||||
|
||||
raise vol.Invalid(
|
||||
f"Fatal error for option '{key}' with type '{typ}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
def _nested_validate_list(self, typ: Any, data_list: List[Any], key: str):
|
||||
"""Validate nested items."""
|
||||
options = []
|
||||
|
||||
# Make sure it is a list
|
||||
if not isinstance(data_list, list):
|
||||
raise vol.Invalid(
|
||||
f"Invalid list for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process list
|
||||
for element in data_list:
|
||||
# Nested?
|
||||
if isinstance(typ, dict):
|
||||
c_options = self._nested_validate_dict(typ, element, key)
|
||||
options.append(c_options)
|
||||
else:
|
||||
options.append(self._single_validate(typ, element, key))
|
||||
|
||||
return options
|
||||
|
||||
def _nested_validate_dict(
|
||||
self, typ: Dict[Any, Any], data_dict: Dict[Any, Any], key: str
|
||||
):
|
||||
"""Validate nested items."""
|
||||
options = {}
|
||||
|
||||
# Make sure it is a dict
|
||||
if not isinstance(data_dict, dict):
|
||||
raise vol.Invalid(
|
||||
f"Invalid dict for option '{key}' in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
# Process dict
|
||||
for c_key, c_value in data_dict.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if c_key not in typ:
|
||||
_LOGGER.warning(
|
||||
"Unknown option '%s' for %s (%s)", c_key, self._name, self._slug
|
||||
)
|
||||
continue
|
||||
|
||||
# Nested?
|
||||
if isinstance(typ[c_key], list):
|
||||
options[c_key] = self._nested_validate_list(
|
||||
typ[c_key][0], c_value, c_key
|
||||
)
|
||||
else:
|
||||
options[c_key] = self._single_validate(typ[c_key], c_value, c_key)
|
||||
|
||||
self._check_missing_options(typ, options, key)
|
||||
return options
|
||||
|
||||
def _check_missing_options(
|
||||
self, origin: Dict[Any, Any], exists: Dict[Any, Any], root: str
|
||||
) -> None:
|
||||
"""Check if all options are exists."""
|
||||
missing = set(origin) - set(exists)
|
||||
for miss_opt in missing:
|
||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||
continue
|
||||
raise vol.Invalid(
|
||||
f"Missing option '{miss_opt}' in {root} in {self._name} ({self._slug})"
|
||||
) from None
|
||||
|
||||
|
||||
class UiOptions(CoreSysAttributes):
|
||||
"""Render UI Add-ons Options."""
|
||||
|
||||
def __init__(self, coresys: CoreSys) -> None:
|
||||
"""Initialize UI option render."""
|
||||
self.coresys = coresys
|
||||
|
||||
def __call__(self, raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate UI schema."""
|
||||
ui_schema: List[Dict[str, Any]] = []
|
||||
|
||||
# read options
|
||||
for key, value in raw_schema.items():
|
||||
if isinstance(value, list):
|
||||
# nested value list
|
||||
self._nested_ui_list(ui_schema, value, key)
|
||||
elif isinstance(value, dict):
|
||||
# nested value dict
|
||||
self._nested_ui_dict(ui_schema, value, key)
|
||||
else:
|
||||
# normal value
|
||||
self._single_ui_option(ui_schema, value, key)
|
||||
|
||||
return ui_schema
|
||||
|
||||
def _single_ui_option(
|
||||
self,
|
||||
ui_schema: List[Dict[str, Any]],
|
||||
value: str,
|
||||
key: str,
|
||||
multiple: bool = False,
|
||||
) -> None:
|
||||
"""Validate a single element."""
|
||||
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
||||
|
||||
# If multiple
|
||||
if multiple:
|
||||
ui_node["multiple"] = True
|
||||
|
||||
# Parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(value)
|
||||
if not match:
|
||||
return
|
||||
|
||||
# Prepare range
|
||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||
group_value = match.group(group_name)
|
||||
if not group_value:
|
||||
continue
|
||||
if group_name[2:] == "min":
|
||||
ui_node["lengthMin"] = float(group_value)
|
||||
elif group_name[2:] == "max":
|
||||
ui_node["lengthMax"] = float(group_value)
|
||||
|
||||
# If required
|
||||
if value.endswith("?"):
|
||||
ui_node["optional"] = True
|
||||
else:
|
||||
ui_node["required"] = True
|
||||
|
||||
# Data types
|
||||
if value.startswith(_STR):
|
||||
ui_node["type"] = "string"
|
||||
elif value.startswith(_PASSWORD):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "password"
|
||||
elif value.startswith(_INT):
|
||||
ui_node["type"] = "integer"
|
||||
elif value.startswith(_FLOAT):
|
||||
ui_node["type"] = "float"
|
||||
elif value.startswith(_BOOL):
|
||||
ui_node["type"] = "boolean"
|
||||
elif value.startswith(_EMAIL):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "email"
|
||||
elif value.startswith(_URL):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "url"
|
||||
elif value.startswith(_PORT):
|
||||
ui_node["type"] = "integer"
|
||||
elif value.startswith(_MATCH):
|
||||
ui_node["type"] = "string"
|
||||
elif value.startswith(_LIST):
|
||||
ui_node["type"] = "select"
|
||||
ui_node["options"] = match.group("list").split("|")
|
||||
elif value.startswith(_DEVICE):
|
||||
ui_node["type"] = "select"
|
||||
|
||||
# Have filter
|
||||
if match.group("filter"):
|
||||
device_filter = _create_device_filter(match.group("filter"))
|
||||
ui_node["options"] = [
|
||||
(device.by_id or device.path).as_posix()
|
||||
for device in self.sys_hardware.filter_devices(**device_filter)
|
||||
]
|
||||
else:
|
||||
ui_node["options"] = [
|
||||
(device.by_id or device.path).as_posix()
|
||||
for device in self.sys_hardware.devices
|
||||
]
|
||||
|
||||
ui_schema.append(ui_node)
|
||||
|
||||
def _nested_ui_list(
|
||||
self,
|
||||
ui_schema: List[Dict[str, Any]],
|
||||
option_list: List[Any],
|
||||
key: str,
|
||||
) -> None:
|
||||
"""UI nested list items."""
|
||||
try:
|
||||
element = option_list[0]
|
||||
except IndexError:
|
||||
_LOGGER.error("Invalid schema %s", key)
|
||||
return
|
||||
|
||||
if isinstance(element, dict):
|
||||
self._nested_ui_dict(ui_schema, element, key, multiple=True)
|
||||
else:
|
||||
self._single_ui_option(ui_schema, element, key, multiple=True)
|
||||
|
||||
def _nested_ui_dict(
|
||||
self,
|
||||
ui_schema: List[Dict[str, Any]],
|
||||
option_dict: Dict[str, Any],
|
||||
key: str,
|
||||
multiple: bool = False,
|
||||
) -> None:
|
||||
"""UI nested dict items."""
|
||||
ui_node = {
|
||||
"name": key,
|
||||
"type": "schema",
|
||||
"optional": True,
|
||||
"multiple": multiple,
|
||||
}
|
||||
|
||||
nested_schema = []
|
||||
for c_key, c_value in option_dict.items():
|
||||
# Nested?
|
||||
if isinstance(c_value, list):
|
||||
self._nested_ui_list(nested_schema, c_value, c_key)
|
||||
else:
|
||||
self._single_ui_option(nested_schema, c_value, c_key)
|
||||
|
||||
ui_node["schema"] = nested_schema
|
||||
ui_schema.append(ui_node)
|
||||
|
||||
|
||||
def _create_device_filter(str_filter: str) -> Dict[str, Any]:
|
||||
"""Generate device Filter."""
|
||||
raw_filter = dict(value.split("=") for value in str_filter.split(";"))
|
||||
|
||||
clean_filter = {}
|
||||
for key, value in raw_filter.items():
|
||||
if key == "subsystem":
|
||||
clean_filter[key] = UdevSubsystem(value)
|
||||
else:
|
||||
clean_filter[key] = value
|
||||
|
||||
return clean_filter
|
@@ -6,18 +6,8 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..const import (
|
||||
PRIVILEGED_DAC_READ_SEARCH,
|
||||
PRIVILEGED_NET_ADMIN,
|
||||
PRIVILEGED_SYS_ADMIN,
|
||||
PRIVILEGED_SYS_MODULE,
|
||||
PRIVILEGED_SYS_PTRACE,
|
||||
PRIVILEGED_SYS_RAWIO,
|
||||
ROLE_ADMIN,
|
||||
ROLE_MANAGER,
|
||||
SECURITY_DISABLE,
|
||||
SECURITY_PROFILE,
|
||||
)
|
||||
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
|
||||
from ..docker.const import Capabilities
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .model import AddonModel
|
||||
@@ -46,16 +36,19 @@ def rating_security(addon: AddonModel) -> int:
|
||||
rating += 1
|
||||
|
||||
# Privileged options
|
||||
if any(
|
||||
privilege in addon.privileged
|
||||
for privilege in (
|
||||
PRIVILEGED_NET_ADMIN,
|
||||
PRIVILEGED_SYS_ADMIN,
|
||||
PRIVILEGED_SYS_RAWIO,
|
||||
PRIVILEGED_SYS_PTRACE,
|
||||
PRIVILEGED_SYS_MODULE,
|
||||
PRIVILEGED_DAC_READ_SEARCH,
|
||||
if (
|
||||
any(
|
||||
privilege in addon.privileged
|
||||
for privilege in (
|
||||
Capabilities.NET_ADMIN,
|
||||
Capabilities.SYS_ADMIN,
|
||||
Capabilities.SYS_RAWIO,
|
||||
Capabilities.SYS_PTRACE,
|
||||
Capabilities.SYS_MODULE,
|
||||
Capabilities.DAC_READ_SEARCH,
|
||||
)
|
||||
)
|
||||
or addon.with_kernel_modules
|
||||
):
|
||||
rating += -1
|
||||
|
||||
@@ -73,12 +66,8 @@ def rating_security(addon: AddonModel) -> int:
|
||||
if addon.host_pid:
|
||||
rating += -2
|
||||
|
||||
# Full Access
|
||||
if addon.with_full_access:
|
||||
rating += -2
|
||||
|
||||
# Docker Access
|
||||
if addon.access_docker_api:
|
||||
# Docker Access & full Access
|
||||
if addon.access_docker_api or addon.with_full_access:
|
||||
rating = 1
|
||||
|
||||
return max(min(6, rating), 1)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
import logging
|
||||
import re
|
||||
import secrets
|
||||
from typing import Any, Dict, List, Union
|
||||
from typing import Any, Dict
|
||||
import uuid
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -18,10 +18,10 @@ from ..const import (
|
||||
ATTR_AUDIO_INPUT,
|
||||
ATTR_AUDIO_OUTPUT,
|
||||
ATTR_AUTH_API,
|
||||
ATTR_AUTO_UART,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_BOOT,
|
||||
ATTR_BUILD_FROM,
|
||||
ATTR_CONFIGURATION,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DEVICES,
|
||||
ATTR_DEVICETREE,
|
||||
@@ -60,6 +60,7 @@ from ..const import (
|
||||
ATTR_PORTS_DESCRIPTION,
|
||||
ATTR_PRIVILEGED,
|
||||
ATTR_PROTECTED,
|
||||
ATTR_REALTIME,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SCHEMA,
|
||||
ATTR_SERVICES,
|
||||
@@ -73,6 +74,8 @@ from ..const import (
|
||||
ATTR_SYSTEM,
|
||||
ATTR_TIMEOUT,
|
||||
ATTR_TMPFS,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
@@ -82,7 +85,6 @@ from ..const import (
|
||||
ATTR_VIDEO,
|
||||
ATTR_WATCHDOG,
|
||||
ATTR_WEBUI,
|
||||
PRIVILEGED_ALL,
|
||||
ROLE_ALL,
|
||||
ROLE_DEFAULT,
|
||||
AddonBoot,
|
||||
@@ -90,9 +92,10 @@ from ..const import (
|
||||
AddonStartup,
|
||||
AddonState,
|
||||
)
|
||||
from ..coresys import CoreSys
|
||||
from ..discovery.validate import valid_discovery_service
|
||||
from ..docker.const import Capabilities
|
||||
from ..validate import (
|
||||
docker_image,
|
||||
docker_ports,
|
||||
docker_ports_description,
|
||||
network_port,
|
||||
@@ -100,51 +103,14 @@ from ..validate import (
|
||||
uuid_match,
|
||||
version_tag,
|
||||
)
|
||||
from .options import RE_SCHEMA_ELEMENT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
RE_VOLUME = re.compile(r"^(config|ssl|addons|backup|share|media)(?::(rw|ro))?$")
|
||||
RE_SERVICE = re.compile(r"^(?P<service>mqtt|mysql):(?P<rights>provide|want|need)$")
|
||||
|
||||
V_STR = "str"
|
||||
V_INT = "int"
|
||||
V_FLOAT = "float"
|
||||
V_BOOL = "bool"
|
||||
V_PASSWORD = "password"
|
||||
V_EMAIL = "email"
|
||||
V_URL = "url"
|
||||
V_PORT = "port"
|
||||
V_MATCH = "match"
|
||||
V_LIST = "list"
|
||||
|
||||
RE_SCHEMA_ELEMENT = re.compile(
|
||||
r"^(?:"
|
||||
r"|bool"
|
||||
r"|email"
|
||||
r"|url"
|
||||
r"|port"
|
||||
r"|str(?:\((?P<s_min>\d+)?,(?P<s_max>\d+)?\))?"
|
||||
r"|password(?:\((?P<p_min>\d+)?,(?P<p_max>\d+)?\))?"
|
||||
r"|int(?:\((?P<i_min>\d+)?,(?P<i_max>\d+)?\))?"
|
||||
r"|float(?:\((?P<f_min>[\d\.]+)?,(?P<f_max>[\d\.]+)?\))?"
|
||||
r"|match\((?P<match>.*)\)"
|
||||
r"|list\((?P<list>.+)\)"
|
||||
r")\??$"
|
||||
)
|
||||
|
||||
_SCHEMA_LENGTH_PARTS = (
|
||||
"i_min",
|
||||
"i_max",
|
||||
"f_min",
|
||||
"f_max",
|
||||
"s_min",
|
||||
"s_max",
|
||||
"p_min",
|
||||
"p_max",
|
||||
)
|
||||
|
||||
RE_DOCKER_IMAGE = re.compile(r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)$")
|
||||
RE_DOCKER_IMAGE_BUILD = re.compile(
|
||||
r"^([a-zA-Z\-\.:\d{}]+/)*?([\-\w{}]+)/([\-\w{}]+)(:[\.\-\w{}]+)?$"
|
||||
)
|
||||
@@ -173,34 +139,101 @@ RE_MACHINE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def _simple_startup(value) -> str:
|
||||
"""Define startup schema."""
|
||||
if value == "before":
|
||||
return AddonStartup.SERVICES.value
|
||||
if value == "after":
|
||||
return AddonStartup.APPLICATION.value
|
||||
return value
|
||||
def _warn_addon_config(config: Dict[str, Any]):
|
||||
"""Warn about miss configs."""
|
||||
name = config.get(ATTR_NAME)
|
||||
if not name:
|
||||
raise vol.Invalid("Invalid Add-on config!")
|
||||
|
||||
if config.get(ATTR_FULL_ACCESS, False) and (
|
||||
config.get(ATTR_DEVICES)
|
||||
or config.get(ATTR_UART)
|
||||
or config.get(ATTR_USB)
|
||||
or config.get(ATTR_GPIO)
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"Add-on have full device access, and selective device access in the configuration. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _migrate_addon_config(protocol=False):
|
||||
"""Migrate addon config."""
|
||||
|
||||
def _migrate(config: Dict[str, Any]):
|
||||
name = config.get(ATTR_NAME)
|
||||
if not name:
|
||||
raise vol.Invalid("Invalid Add-on config!")
|
||||
|
||||
# Startup 2018-03-30
|
||||
if config.get(ATTR_STARTUP) in ("before", "after"):
|
||||
value = config[ATTR_STARTUP]
|
||||
if protocol:
|
||||
_LOGGER.warning(
|
||||
"Add-on config 'startup' with '%s' is deprecated. Please report this to the maintainer of %s",
|
||||
value,
|
||||
name,
|
||||
)
|
||||
if value == "before":
|
||||
config[ATTR_STARTUP] = AddonStartup.SERVICES.value
|
||||
elif value == "after":
|
||||
config[ATTR_STARTUP] = AddonStartup.APPLICATION.value
|
||||
|
||||
# UART 2021-01-20
|
||||
if "auto_uart" in config:
|
||||
if protocol:
|
||||
_LOGGER.warning(
|
||||
"Add-on config 'auto_uart' is deprecated, use 'uart'. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
config[ATTR_UART] = config.pop("auto_uart")
|
||||
|
||||
# Device 2021-01-20
|
||||
if ATTR_DEVICES in config and any(":" in line for line in config[ATTR_DEVICES]):
|
||||
if protocol:
|
||||
_LOGGER.warning(
|
||||
"Add-on config 'devices' use a deprecated format, the new format uses a list of paths only. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
config[ATTR_DEVICES] = [line.split(":")[0] for line in config[ATTR_DEVICES]]
|
||||
|
||||
# TMPFS 2021-02-01
|
||||
if ATTR_TMPFS in config and not isinstance(config[ATTR_TMPFS], bool):
|
||||
if protocol:
|
||||
_LOGGER.warning(
|
||||
"Add-on config 'tmpfs' use a deprecated format, new it's only a boolean. Please report this to the maintainer of %s",
|
||||
name,
|
||||
)
|
||||
config[ATTR_TMPFS] = True
|
||||
|
||||
return config
|
||||
|
||||
return _migrate
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
_SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): vol.Coerce(str),
|
||||
vol.Required(ATTR_VERSION): vol.All(version_tag, str),
|
||||
vol.Required(ATTR_SLUG): vol.Coerce(str),
|
||||
vol.Required(ATTR_DESCRIPTON): vol.Coerce(str),
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Required(ATTR_VERSION): version_tag,
|
||||
vol.Required(ATTR_SLUG): str,
|
||||
vol.Required(ATTR_DESCRIPTON): str,
|
||||
vol.Required(ATTR_ARCH): [vol.In(ARCH_ALL)],
|
||||
vol.Optional(ATTR_MACHINE): vol.All([vol.Match(RE_MACHINE)], vol.Unique()),
|
||||
vol.Optional(ATTR_URL): vol.Url(),
|
||||
vol.Required(ATTR_STARTUP): vol.All(_simple_startup, vol.Coerce(AddonStartup)),
|
||||
vol.Required(ATTR_BOOT): vol.Coerce(AddonBoot),
|
||||
vol.Optional(ATTR_STARTUP, default=AddonStartup.APPLICATION): vol.Coerce(
|
||||
AddonStartup
|
||||
),
|
||||
vol.Optional(ATTR_BOOT, default=AddonBoot.AUTO): vol.Coerce(AddonBoot),
|
||||
vol.Optional(ATTR_INIT, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_ADVANCED, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_STAGE, default=AddonStage.STABLE): vol.Coerce(AddonStage),
|
||||
vol.Optional(ATTR_PORTS): docker_ports,
|
||||
vol.Optional(ATTR_PORTS_DESCRIPTION): docker_ports_description,
|
||||
vol.Optional(ATTR_WATCHDOG): vol.Match(
|
||||
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||
r"^(?:https?|\[PROTO:\w+\]|tcp):\/\/\[HOST\]:(\[PORT:\d+\]|\d+).*$"
|
||||
),
|
||||
vol.Optional(ATTR_WEBUI): vol.Match(
|
||||
r"^(?:https?|\[PROTO:\w+\]):\/\/\[HOST\]:\[PORT:\d+\].*$"
|
||||
@@ -209,30 +242,31 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_INGRESS_PORT, default=8099): vol.Any(
|
||||
network_port, vol.Equal(0)
|
||||
),
|
||||
vol.Optional(ATTR_INGRESS_ENTRY): vol.Coerce(str),
|
||||
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): vol.Coerce(str),
|
||||
vol.Optional(ATTR_PANEL_TITLE): vol.Coerce(str),
|
||||
vol.Optional(ATTR_INGRESS_ENTRY): str,
|
||||
vol.Optional(ATTR_PANEL_ICON, default="mdi:puzzle"): str,
|
||||
vol.Optional(ATTR_PANEL_TITLE): str,
|
||||
vol.Optional(ATTR_PANEL_ADMIN, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Maybe(version_tag),
|
||||
vol.Optional(ATTR_HOST_NETWORK, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_PID, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_IPC, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HOST_DBUS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICES): [vol.Match(r"^(.*):(.*):([rwm]{1,3})$")],
|
||||
vol.Optional(ATTR_AUTO_UART, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICES): [str],
|
||||
vol.Optional(ATTR_UDEV, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_TMPFS): vol.Match(r"^size=(\d)*[kmg](,uid=\d{1,4})?(,rw)?$"),
|
||||
vol.Optional(ATTR_TMPFS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
||||
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
|
||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): str},
|
||||
vol.Optional(ATTR_PRIVILEGED): [vol.Coerce(Capabilities)],
|
||||
vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_FULL_ACCESS, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_VIDEO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_USB, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_UART, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_DEVICETREE, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_KERNEL_MODULES, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_REALTIME, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_HASSIO_ROLE, default=ROLE_DEFAULT): vol.In(ROLE_ALL),
|
||||
vol.Optional(ATTR_HOMEASSISTANT_API, default=False): vol.Boolean(),
|
||||
@@ -242,32 +276,26 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
vol.Optional(ATTR_AUTH_API, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_SERVICES): [vol.Match(RE_SERVICE)],
|
||||
vol.Optional(ATTR_DISCOVERY): [valid_discovery_service],
|
||||
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [vol.Coerce(str)],
|
||||
vol.Required(ATTR_OPTIONS): dict,
|
||||
vol.Required(ATTR_SCHEMA): vol.Any(
|
||||
vol.Optional(ATTR_SNAPSHOT_EXCLUDE): [str],
|
||||
vol.Optional(ATTR_OPTIONS, default={}): dict,
|
||||
vol.Optional(ATTR_SCHEMA, default={}): vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Coerce(str): vol.Any(
|
||||
str: vol.Any(
|
||||
SCHEMA_ELEMENT,
|
||||
[
|
||||
vol.Any(
|
||||
SCHEMA_ELEMENT,
|
||||
{
|
||||
vol.Coerce(str): vol.Any(
|
||||
SCHEMA_ELEMENT, [SCHEMA_ELEMENT]
|
||||
)
|
||||
},
|
||||
{str: vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])},
|
||||
)
|
||||
],
|
||||
vol.Schema(
|
||||
{vol.Coerce(str): vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}
|
||||
),
|
||||
vol.Schema({str: vol.Any(SCHEMA_ELEMENT, [SCHEMA_ELEMENT])}),
|
||||
)
|
||||
}
|
||||
),
|
||||
False,
|
||||
),
|
||||
vol.Optional(ATTR_IMAGE): vol.Match(RE_DOCKER_IMAGE),
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||
),
|
||||
@@ -275,6 +303,10 @@ SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
SCHEMA_ADDON_CONFIG = vol.All(
|
||||
_migrate_addon_config(True), _warn_addon_config, _SCHEMA_ADDON_CONFIG
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_BUILD_CONFIG = vol.Schema(
|
||||
@@ -290,23 +322,38 @@ SCHEMA_BUILD_CONFIG = vol.Schema(
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
SCHEMA_TRANSLATION_CONFIGURATION = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Optional(ATTR_DESCRIPTON): vol.Maybe(str),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDON_TRANSLATIONS = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_CONFIGURATION): {str: SCHEMA_TRANSLATION_CONFIGURATION},
|
||||
vol.Optional(ATTR_NETWORK): {str: str},
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
SCHEMA_ADDON_USER = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||
vol.Optional(ATTR_IMAGE): vol.Coerce(str),
|
||||
vol.Required(ATTR_VERSION): version_tag,
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(ATTR_UUID, default=lambda: uuid.uuid4().hex): uuid_match,
|
||||
vol.Optional(ATTR_ACCESS_TOKEN): token,
|
||||
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): vol.Coerce(
|
||||
str
|
||||
),
|
||||
vol.Optional(ATTR_INGRESS_TOKEN, default=secrets.token_urlsafe): str,
|
||||
vol.Optional(ATTR_OPTIONS, default=dict): dict,
|
||||
vol.Optional(ATTR_AUTO_UPDATE, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_BOOT): vol.Coerce(AddonBoot),
|
||||
vol.Optional(ATTR_NETWORK): docker_ports,
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(vol.Coerce(str)),
|
||||
vol.Optional(ATTR_AUDIO_OUTPUT): vol.Maybe(str),
|
||||
vol.Optional(ATTR_AUDIO_INPUT): vol.Maybe(str),
|
||||
vol.Optional(ATTR_PROTECTED, default=True): vol.Boolean(),
|
||||
vol.Optional(ATTR_INGRESS_PANEL, default=False): vol.Boolean(),
|
||||
vol.Optional(ATTR_WATCHDOG, default=False): vol.Boolean(),
|
||||
@@ -314,20 +361,26 @@ SCHEMA_ADDON_USER = vol.Schema(
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDON_SYSTEM = SCHEMA_ADDON_CONFIG.extend(
|
||||
{
|
||||
vol.Required(ATTR_LOCATON): vol.Coerce(str),
|
||||
vol.Required(ATTR_REPOSITORY): vol.Coerce(str),
|
||||
}
|
||||
SCHEMA_ADDON_SYSTEM = vol.All(
|
||||
_migrate_addon_config(),
|
||||
_SCHEMA_ADDON_CONFIG.extend(
|
||||
{
|
||||
vol.Required(ATTR_LOCATON): str,
|
||||
vol.Required(ATTR_REPOSITORY): str,
|
||||
vol.Required(ATTR_TRANSLATIONS, default=dict): {
|
||||
str: SCHEMA_ADDON_TRANSLATIONS
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_ADDONS_FILE = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_USER, default=dict): {vol.Coerce(str): SCHEMA_ADDON_USER},
|
||||
vol.Optional(ATTR_SYSTEM, default=dict): {vol.Coerce(str): SCHEMA_ADDON_SYSTEM},
|
||||
}
|
||||
vol.Optional(ATTR_USER, default=dict): {str: SCHEMA_ADDON_USER},
|
||||
vol.Optional(ATTR_SYSTEM, default=dict): {str: SCHEMA_ADDON_SYSTEM},
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
@@ -336,263 +389,7 @@ SCHEMA_ADDON_SNAPSHOT = vol.Schema(
|
||||
vol.Required(ATTR_USER): SCHEMA_ADDON_USER,
|
||||
vol.Required(ATTR_SYSTEM): SCHEMA_ADDON_SYSTEM,
|
||||
vol.Required(ATTR_STATE): vol.Coerce(AddonState),
|
||||
vol.Required(ATTR_VERSION): vol.Coerce(str),
|
||||
vol.Required(ATTR_VERSION): version_tag,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def validate_options(coresys: CoreSys, raw_schema: Dict[str, Any]):
|
||||
"""Validate schema."""
|
||||
|
||||
def validate(struct):
|
||||
"""Create schema validator for add-ons options."""
|
||||
options = {}
|
||||
|
||||
# read options
|
||||
for key, value in struct.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if key not in raw_schema:
|
||||
_LOGGER.warning("Unknown options %s", key)
|
||||
continue
|
||||
|
||||
typ = raw_schema[key]
|
||||
try:
|
||||
if isinstance(typ, list):
|
||||
# nested value list
|
||||
options[key] = _nested_validate_list(coresys, typ[0], value, key)
|
||||
elif isinstance(typ, dict):
|
||||
# nested value dict
|
||||
options[key] = _nested_validate_dict(coresys, typ, value, key)
|
||||
else:
|
||||
# normal value
|
||||
options[key] = _single_validate(coresys, typ, value, key)
|
||||
except (IndexError, KeyError):
|
||||
raise vol.Invalid(f"Type error for {key}") from None
|
||||
|
||||
_check_missing_options(raw_schema, options, "root")
|
||||
return options
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
# pylint: disable=inconsistent-return-statements
|
||||
def _single_validate(coresys: CoreSys, typ: str, value: Any, key: str):
|
||||
"""Validate a single element."""
|
||||
# if required argument
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Missing required option '{key}'") from None
|
||||
|
||||
# Lookup secret
|
||||
if str(value).startswith("!secret "):
|
||||
secret: str = value.partition(" ")[2]
|
||||
value = coresys.homeassistant.secrets.get(secret)
|
||||
if value is None:
|
||||
raise vol.Invalid(f"Unknown secret {secret}") from None
|
||||
|
||||
# parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(typ)
|
||||
|
||||
if not match:
|
||||
raise vol.Invalid(f"Unknown type {typ}") from None
|
||||
|
||||
# prepare range
|
||||
range_args = {}
|
||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||
group_value = match.group(group_name)
|
||||
if group_value:
|
||||
range_args[group_name[2:]] = float(group_value)
|
||||
|
||||
if typ.startswith(V_STR) or typ.startswith(V_PASSWORD):
|
||||
return vol.All(str(value), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(V_INT):
|
||||
return vol.All(vol.Coerce(int), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(V_FLOAT):
|
||||
return vol.All(vol.Coerce(float), vol.Range(**range_args))(value)
|
||||
elif typ.startswith(V_BOOL):
|
||||
return vol.Boolean()(value)
|
||||
elif typ.startswith(V_EMAIL):
|
||||
return vol.Email()(value)
|
||||
elif typ.startswith(V_URL):
|
||||
return vol.Url()(value)
|
||||
elif typ.startswith(V_PORT):
|
||||
return network_port(value)
|
||||
elif typ.startswith(V_MATCH):
|
||||
return vol.Match(match.group("match"))(str(value))
|
||||
elif typ.startswith(V_LIST):
|
||||
return vol.In(match.group("list").split("|"))(str(value))
|
||||
|
||||
raise vol.Invalid(f"Fatal error for {key} type {typ}") from None
|
||||
|
||||
|
||||
def _nested_validate_list(coresys, typ, data_list, key):
|
||||
"""Validate nested items."""
|
||||
options = []
|
||||
|
||||
# Make sure it is a list
|
||||
if not isinstance(data_list, list):
|
||||
raise vol.Invalid(f"Invalid list for {key}") from None
|
||||
|
||||
# Process list
|
||||
for element in data_list:
|
||||
# Nested?
|
||||
if isinstance(typ, dict):
|
||||
c_options = _nested_validate_dict(coresys, typ, element, key)
|
||||
options.append(c_options)
|
||||
else:
|
||||
options.append(_single_validate(coresys, typ, element, key))
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def _nested_validate_dict(coresys, typ, data_dict, key):
|
||||
"""Validate nested items."""
|
||||
options = {}
|
||||
|
||||
# Make sure it is a dict
|
||||
if not isinstance(data_dict, dict):
|
||||
raise vol.Invalid(f"Invalid dict for {key}") from None
|
||||
|
||||
# Process dict
|
||||
for c_key, c_value in data_dict.items():
|
||||
# Ignore unknown options / remove from list
|
||||
if c_key not in typ:
|
||||
_LOGGER.warning("Unknown options %s", c_key)
|
||||
continue
|
||||
|
||||
# Nested?
|
||||
if isinstance(typ[c_key], list):
|
||||
options[c_key] = _nested_validate_list(
|
||||
coresys, typ[c_key][0], c_value, c_key
|
||||
)
|
||||
else:
|
||||
options[c_key] = _single_validate(coresys, typ[c_key], c_value, c_key)
|
||||
|
||||
_check_missing_options(typ, options, key)
|
||||
return options
|
||||
|
||||
|
||||
def _check_missing_options(origin, exists, root):
|
||||
"""Check if all options are exists."""
|
||||
missing = set(origin) - set(exists)
|
||||
for miss_opt in missing:
|
||||
if isinstance(origin[miss_opt], str) and origin[miss_opt].endswith("?"):
|
||||
continue
|
||||
raise vol.Invalid(f"Missing option {miss_opt} in {root}") from None
|
||||
|
||||
|
||||
def schema_ui_options(raw_schema: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate UI schema."""
|
||||
ui_schema: List[Dict[str, Any]] = []
|
||||
|
||||
# read options
|
||||
for key, value in raw_schema.items():
|
||||
if isinstance(value, list):
|
||||
# nested value list
|
||||
_nested_ui_list(ui_schema, value, key)
|
||||
elif isinstance(value, dict):
|
||||
# nested value dict
|
||||
_nested_ui_dict(ui_schema, value, key)
|
||||
else:
|
||||
# normal value
|
||||
_single_ui_option(ui_schema, value, key)
|
||||
|
||||
return ui_schema
|
||||
|
||||
|
||||
def _single_ui_option(
|
||||
ui_schema: List[Dict[str, Any]], value: str, key: str, multiple: bool = False
|
||||
) -> None:
|
||||
"""Validate a single element."""
|
||||
ui_node: Dict[str, Union[str, bool, float, List[str]]] = {"name": key}
|
||||
|
||||
# If multiple
|
||||
if multiple:
|
||||
ui_node["multiple"] = True
|
||||
|
||||
# Parse extend data from type
|
||||
match = RE_SCHEMA_ELEMENT.match(value)
|
||||
if not match:
|
||||
return
|
||||
|
||||
# Prepare range
|
||||
for group_name in _SCHEMA_LENGTH_PARTS:
|
||||
group_value = match.group(group_name)
|
||||
if not group_value:
|
||||
continue
|
||||
if group_name[2:] == "min":
|
||||
ui_node["lengthMin"] = float(group_value)
|
||||
elif group_name[2:] == "max":
|
||||
ui_node["lengthMax"] = float(group_value)
|
||||
|
||||
# If required
|
||||
if value.endswith("?"):
|
||||
ui_node["optional"] = True
|
||||
else:
|
||||
ui_node["required"] = True
|
||||
|
||||
# Data types
|
||||
if value.startswith(V_STR):
|
||||
ui_node["type"] = "string"
|
||||
elif value.startswith(V_PASSWORD):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "password"
|
||||
elif value.startswith(V_INT):
|
||||
ui_node["type"] = "integer"
|
||||
elif value.startswith(V_FLOAT):
|
||||
ui_node["type"] = "float"
|
||||
elif value.startswith(V_BOOL):
|
||||
ui_node["type"] = "boolean"
|
||||
elif value.startswith(V_EMAIL):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "email"
|
||||
elif value.startswith(V_URL):
|
||||
ui_node["type"] = "string"
|
||||
ui_node["format"] = "url"
|
||||
elif value.startswith(V_PORT):
|
||||
ui_node["type"] = "integer"
|
||||
elif value.startswith(V_MATCH):
|
||||
ui_node["type"] = "string"
|
||||
elif value.startswith(V_LIST):
|
||||
ui_node["type"] = "select"
|
||||
ui_node["options"] = match.group("list").split("|")
|
||||
|
||||
ui_schema.append(ui_node)
|
||||
|
||||
|
||||
def _nested_ui_list(
|
||||
ui_schema: List[Dict[str, Any]], option_list: List[Any], key: str
|
||||
) -> None:
|
||||
"""UI nested list items."""
|
||||
try:
|
||||
element = option_list[0]
|
||||
except IndexError:
|
||||
_LOGGER.error("Invalid schema %s", key)
|
||||
return
|
||||
|
||||
if isinstance(element, dict):
|
||||
_nested_ui_dict(ui_schema, element, key, multiple=True)
|
||||
else:
|
||||
_single_ui_option(ui_schema, element, key, multiple=True)
|
||||
|
||||
|
||||
def _nested_ui_dict(
|
||||
ui_schema: List[Dict[str, Any]],
|
||||
option_dict: Dict[str, Any],
|
||||
key: str,
|
||||
multiple: bool = False,
|
||||
) -> None:
|
||||
"""UI nested dict items."""
|
||||
ui_node = {"name": key, "type": "schema", "optional": True, "multiple": multiple}
|
||||
|
||||
nested_schema = []
|
||||
for c_key, c_value in option_dict.items():
|
||||
# Nested?
|
||||
if isinstance(c_value, list):
|
||||
_nested_ui_list(nested_schema, c_value, c_key)
|
||||
else:
|
||||
_single_ui_option(nested_schema, c_value, c_key)
|
||||
|
||||
ui_node["schema"] = nested_schema
|
||||
ui_schema.append(ui_node)
|
||||
|
@@ -18,6 +18,7 @@ from .homeassistant import APIHomeAssistant
|
||||
from .host import APIHost
|
||||
from .info import APIInfo
|
||||
from .ingress import APIIngress
|
||||
from .jobs import APIJobs
|
||||
from .multicast import APIMulticast
|
||||
from .network import APINetwork
|
||||
from .observer import APIObserver
|
||||
@@ -27,6 +28,7 @@ from .resolution import APIResoulution
|
||||
from .security import SecurityMiddleware
|
||||
from .services import APIServices
|
||||
from .snapshots import APISnapshots
|
||||
from .store import APIStore
|
||||
from .supervisor import APISupervisor
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
@@ -72,12 +74,14 @@ class RestAPI(CoreSysAttributes):
|
||||
self._register_network()
|
||||
self._register_observer()
|
||||
self._register_os()
|
||||
self._register_jobs()
|
||||
self._register_panel()
|
||||
self._register_proxy()
|
||||
self._register_resolution()
|
||||
self._register_services()
|
||||
self._register_snapshots()
|
||||
self._register_supervisor()
|
||||
self._register_store()
|
||||
|
||||
await self.start()
|
||||
|
||||
@@ -110,6 +114,7 @@ class RestAPI(CoreSysAttributes):
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/network/info", api_network.info),
|
||||
web.post("/network/reload", api_network.reload),
|
||||
web.get(
|
||||
"/network/interface/{interface}/info", api_network.interface_info
|
||||
),
|
||||
@@ -141,6 +146,19 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_jobs(self) -> None:
|
||||
"""Register Jobs functions."""
|
||||
api_jobs = APIJobs()
|
||||
api_jobs.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/jobs/info", api_jobs.info),
|
||||
web.post("/jobs/options", api_jobs.options),
|
||||
web.post("/jobs/reset", api_jobs.reset),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_cli(self) -> None:
|
||||
"""Register HA cli functions."""
|
||||
api_cli = APICli()
|
||||
@@ -222,6 +240,7 @@ class RestAPI(CoreSysAttributes):
|
||||
"/resolution/issue/{issue}",
|
||||
api_resolution.dismiss_issue,
|
||||
),
|
||||
web.post("/resolution/healthcheck", api_resolution.healthcheck),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -232,6 +251,7 @@ class RestAPI(CoreSysAttributes):
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/auth", api_auth.auth),
|
||||
web.post("/auth", api_auth.auth),
|
||||
web.post("/auth/reset", api_auth.reset),
|
||||
web.delete("/auth/cache", api_auth.cache),
|
||||
@@ -321,16 +341,15 @@ class RestAPI(CoreSysAttributes):
|
||||
web.get("/addons", api_addons.list),
|
||||
web.post("/addons/reload", api_addons.reload),
|
||||
web.get("/addons/{addon}/info", api_addons.info),
|
||||
web.post("/addons/{addon}/install", api_addons.install),
|
||||
web.post("/addons/{addon}/uninstall", api_addons.uninstall),
|
||||
web.post("/addons/{addon}/start", api_addons.start),
|
||||
web.post("/addons/{addon}/stop", api_addons.stop),
|
||||
web.post("/addons/{addon}/restart", api_addons.restart),
|
||||
web.post("/addons/{addon}/update", api_addons.update),
|
||||
web.post("/addons/{addon}/options", api_addons.options),
|
||||
web.post(
|
||||
"/addons/{addon}/options/validate", api_addons.options_validate
|
||||
),
|
||||
web.get("/addons/{addon}/options/config", api_addons.options_config),
|
||||
web.post("/addons/{addon}/rebuild", api_addons.rebuild),
|
||||
web.get("/addons/{addon}/logs", api_addons.logs),
|
||||
web.get("/addons/{addon}/icon", api_addons.icon),
|
||||
@@ -451,6 +470,46 @@ class RestAPI(CoreSysAttributes):
|
||||
]
|
||||
)
|
||||
|
||||
def _register_store(self) -> None:
|
||||
"""Register store endpoints."""
|
||||
api_store = APIStore()
|
||||
api_store.coresys = self.coresys
|
||||
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.get("/store", api_store.store_info),
|
||||
web.get("/store/addons", api_store.addons_list),
|
||||
web.get("/store/addons/{addon}", api_store.addons_addon_info),
|
||||
web.get("/store/addons/{addon}/{version}", api_store.addons_addon_info),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install", api_store.addons_addon_install
|
||||
),
|
||||
web.post(
|
||||
"/store/addons/{addon}/install/{version}",
|
||||
api_store.addons_addon_install,
|
||||
),
|
||||
web.post("/store/addons/{addon}/update", api_store.addons_addon_update),
|
||||
web.post(
|
||||
"/store/addons/{addon}/update/{version}",
|
||||
api_store.addons_addon_update,
|
||||
),
|
||||
web.post("/store/reload", api_store.reload),
|
||||
web.get("/store/repositories", api_store.repositories_list),
|
||||
web.get(
|
||||
"/store/repositories/{repository}",
|
||||
api_store.repositories_repository_info,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
# Reroute from legacy
|
||||
self.webapp.add_routes(
|
||||
[
|
||||
web.post("/addons/{addon}/install", api_store.addons_addon_install),
|
||||
web.post("/addons/{addon}/update", api_store.addons_addon_update),
|
||||
]
|
||||
)
|
||||
|
||||
def _register_panel(self) -> None:
|
||||
"""Register panel for Home Assistant."""
|
||||
panel_dir = Path(__file__).parent.joinpath("panel")
|
||||
|
@@ -82,6 +82,8 @@ from ..const import (
|
||||
ATTR_STARTUP,
|
||||
ATTR_STATE,
|
||||
ATTR_STDIN,
|
||||
ATTR_TRANSLATIONS,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_UPDATE_AVAILABLE,
|
||||
ATTR_URL,
|
||||
@@ -101,7 +103,7 @@ from ..const import (
|
||||
)
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..docker.stats import DockerStats
|
||||
from ..exceptions import APIError
|
||||
from ..exceptions import APIError, APIForbidden
|
||||
from ..validate import docker_ports
|
||||
from .utils import api_process, api_process_raw, api_validate
|
||||
|
||||
@@ -170,6 +172,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_INSTALLED: addon.is_installed,
|
||||
ATTR_AVAILABLE: addon.available,
|
||||
ATTR_DETACHED: addon.is_detached,
|
||||
ATTR_HOMEASSISTANT: addon.homeassistant_version,
|
||||
ATTR_REPOSITORY: addon.repository,
|
||||
ATTR_BUILD: addon.need_build,
|
||||
ATTR_URL: addon.url,
|
||||
@@ -237,7 +240,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_PRIVILEGED: addon.privileged,
|
||||
ATTR_FULL_ACCESS: addon.with_full_access,
|
||||
ATTR_APPARMOR: addon.apparmor,
|
||||
ATTR_DEVICES: _pretty_devices(addon),
|
||||
ATTR_DEVICES: addon.static_devices,
|
||||
ATTR_ICON: addon.with_icon,
|
||||
ATTR_LOGO: addon.with_logo,
|
||||
ATTR_CHANGELOG: addon.with_changelog,
|
||||
@@ -250,6 +253,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_HOMEASSISTANT_API: addon.access_homeassistant_api,
|
||||
ATTR_GPIO: addon.with_gpio,
|
||||
ATTR_USB: addon.with_usb,
|
||||
ATTR_UART: addon.with_uart,
|
||||
ATTR_KERNEL_MODULES: addon.with_kernel_modules,
|
||||
ATTR_DEVICETREE: addon.with_devicetree,
|
||||
ATTR_UDEV: addon.with_udev,
|
||||
@@ -262,6 +266,7 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_SERVICES: _pretty_services(addon),
|
||||
ATTR_DISCOVERY: addon.discovery,
|
||||
ATTR_IP_ADDRESS: None,
|
||||
ATTR_TRANSLATIONS: addon.translations,
|
||||
ATTR_INGRESS: addon.with_ingress,
|
||||
ATTR_INGRESS_ENTRY: None,
|
||||
ATTR_INGRESS_URL: None,
|
||||
@@ -286,6 +291,8 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_VERSION: addon.version,
|
||||
ATTR_UPDATE_AVAILABLE: addon.need_update,
|
||||
ATTR_WATCHDOG: addon.watchdog,
|
||||
ATTR_DEVICES: addon.static_devices
|
||||
+ [device.path for device in addon.devices],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -339,6 +346,19 @@ class APIAddons(CoreSysAttributes):
|
||||
|
||||
return data
|
||||
|
||||
@api_process
|
||||
async def options_config(self, request: web.Request) -> None:
|
||||
"""Validate user options for add-on."""
|
||||
slug: str = request.match_info.get("addon")
|
||||
if slug != "self":
|
||||
raise APIForbidden("This can be only read by the Add-on itself!")
|
||||
|
||||
addon = self._extract_addon_installed(request)
|
||||
try:
|
||||
return addon.schema(addon.options)
|
||||
except vol.Invalid:
|
||||
raise APIError("Invalid configuration data for the add-on") from None
|
||||
|
||||
@api_process
|
||||
async def security(self, request: web.Request) -> None:
|
||||
"""Store security options for add-on."""
|
||||
@@ -369,12 +389,6 @@ class APIAddons(CoreSysAttributes):
|
||||
ATTR_BLK_WRITE: stats.blk_write,
|
||||
}
|
||||
|
||||
@api_process
|
||||
def install(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Install add-on."""
|
||||
addon = self._extract_addon(request)
|
||||
return asyncio.shield(addon.install())
|
||||
|
||||
@api_process
|
||||
def uninstall(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Uninstall add-on."""
|
||||
@@ -393,12 +407,6 @@ class APIAddons(CoreSysAttributes):
|
||||
addon = self._extract_addon_installed(request)
|
||||
return asyncio.shield(addon.stop())
|
||||
|
||||
@api_process
|
||||
def update(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Update add-on."""
|
||||
addon: Addon = self._extract_addon_installed(request)
|
||||
return asyncio.shield(addon.update())
|
||||
|
||||
@api_process
|
||||
def restart(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Restart add-on."""
|
||||
@@ -468,14 +476,6 @@ class APIAddons(CoreSysAttributes):
|
||||
await asyncio.shield(addon.write_stdin(data))
|
||||
|
||||
|
||||
def _pretty_devices(addon: AnyAddon) -> List[str]:
|
||||
"""Return a simplified device list."""
|
||||
dev_list = addon.devices
|
||||
if not dev_list:
|
||||
return []
|
||||
return [row.split(":")[0] for row in dev_list]
|
||||
|
||||
|
||||
def _pretty_services(addon: AnyAddon) -> List[str]:
|
||||
"""Return a simplified services role list."""
|
||||
return [f"{name}:{access}" for name, access in addon.services_role.items()]
|
||||
|
@@ -29,6 +29,10 @@ SCHEMA_PASSWORD_RESET = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
REALM_HEADER: Dict[str, str] = {
|
||||
WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'
|
||||
}
|
||||
|
||||
|
||||
class APIAuth(CoreSysAttributes):
|
||||
"""Handle RESTful API for auth functions."""
|
||||
@@ -63,7 +67,9 @@ class APIAuth(CoreSysAttributes):
|
||||
|
||||
# BasicAuth
|
||||
if AUTHORIZATION in request.headers:
|
||||
return await self._process_basic(request, addon)
|
||||
if not await self._process_basic(request, addon):
|
||||
raise HTTPUnauthorized(headers=REALM_HEADER)
|
||||
return True
|
||||
|
||||
# Json
|
||||
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
|
||||
@@ -75,9 +81,7 @@ class APIAuth(CoreSysAttributes):
|
||||
data = await request.post()
|
||||
return await self._process_dict(request, addon, data)
|
||||
|
||||
raise HTTPUnauthorized(
|
||||
headers={WWW_AUTHENTICATE: 'Basic realm="Home Assistant Authentication"'}
|
||||
)
|
||||
raise HTTPUnauthorized(headers=REALM_HEADER)
|
||||
|
||||
@api_process
|
||||
async def reset(self, request: web.Request) -> None:
|
||||
|
@@ -1,50 +1,46 @@
|
||||
"""Init file for Supervisor hardware RESTful API."""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Awaitable, Dict, List
|
||||
from typing import Any, Awaitable, Dict
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from ..const import (
|
||||
ATTR_AUDIO,
|
||||
ATTR_DISK,
|
||||
ATTR_GPIO,
|
||||
ATTR_INPUT,
|
||||
ATTR_OUTPUT,
|
||||
ATTR_SERIAL,
|
||||
ATTR_USB,
|
||||
)
|
||||
from ..const import ATTR_AUDIO, ATTR_DEVICES, ATTR_INPUT, ATTR_NAME, ATTR_OUTPUT
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..hardware.const import (
|
||||
ATTR_ATTRIBUTES,
|
||||
ATTR_BY_ID,
|
||||
ATTR_DEV_PATH,
|
||||
ATTR_SUBSYSTEM,
|
||||
ATTR_SYSFS,
|
||||
)
|
||||
from ..hardware.data import Device
|
||||
from .utils import api_process
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def device_struct(device: Device) -> Dict[str, Any]:
|
||||
"""Return a dict with information of a interface to be used in th API."""
|
||||
return {
|
||||
ATTR_NAME: device.name,
|
||||
ATTR_SYSFS: device.sysfs,
|
||||
ATTR_DEV_PATH: device.path,
|
||||
ATTR_SUBSYSTEM: device.subsystem,
|
||||
ATTR_BY_ID: device.by_id,
|
||||
ATTR_ATTRIBUTES: device.attributes,
|
||||
}
|
||||
|
||||
|
||||
class APIHardware(CoreSysAttributes):
|
||||
"""Handle RESTful API for hardware functions."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Show hardware info."""
|
||||
serial: List[str] = []
|
||||
|
||||
# Create Serial list with device links
|
||||
for device in self.sys_hardware.serial_devices:
|
||||
serial.append(device.path.as_posix())
|
||||
for link in device.links:
|
||||
serial.append(link.as_posix())
|
||||
|
||||
return {
|
||||
ATTR_SERIAL: serial,
|
||||
ATTR_INPUT: list(self.sys_hardware.input_devices),
|
||||
ATTR_DISK: [
|
||||
device.path.as_posix() for device in self.sys_hardware.disk_devices
|
||||
],
|
||||
ATTR_GPIO: list(self.sys_hardware.gpio_devices),
|
||||
ATTR_USB: [
|
||||
device.path.as_posix() for device in self.sys_hardware.usb_devices
|
||||
],
|
||||
ATTR_AUDIO: self.sys_hardware.audio_devices,
|
||||
ATTR_DEVICES: [
|
||||
device_struct(device) for device in self.sys_hardware.devices
|
||||
]
|
||||
}
|
||||
|
||||
@api_process
|
||||
@@ -64,6 +60,6 @@ class APIHardware(CoreSysAttributes):
|
||||
}
|
||||
|
||||
@api_process
|
||||
def trigger(self, request: web.Request) -> Awaitable[None]:
|
||||
async def trigger(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Trigger a udev device reload."""
|
||||
return asyncio.shield(self.sys_hardware.udev_trigger())
|
||||
_LOGGER.debug("Ignoring DEPRECATED hardware trigger function call.")
|
||||
|
@@ -11,6 +11,7 @@ from ..const import (
|
||||
ATTR_DEPLOYMENT,
|
||||
ATTR_DESCRIPTON,
|
||||
ATTR_DISK_FREE,
|
||||
ATTR_DISK_LIFE_TIME,
|
||||
ATTR_DISK_TOTAL,
|
||||
ATTR_DISK_USED,
|
||||
ATTR_FEATURES,
|
||||
@@ -43,6 +44,7 @@ class APIHost(CoreSysAttributes):
|
||||
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_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
|
||||
ATTR_FEATURES: self.sys_host.features,
|
||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||
ATTR_KERNEL: self.sys_host.info.kernel,
|
||||
|
@@ -15,6 +15,7 @@ from ..const import (
|
||||
ATTR_LOGGING,
|
||||
ATTR_MACHINE,
|
||||
ATTR_OPERATING_SYSTEM,
|
||||
ATTR_STATE,
|
||||
ATTR_SUPERVISOR,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_SUPPORTED_ARCH,
|
||||
@@ -42,6 +43,7 @@ class APIInfo(CoreSysAttributes):
|
||||
ATTR_FEATURES: self.sys_host.features,
|
||||
ATTR_MACHINE: self.sys_machine,
|
||||
ATTR_ARCH: self.sys_arch.default,
|
||||
ATTR_STATE: self.sys_core.state,
|
||||
ATTR_SUPPORTED_ARCH: self.sys_arch.supported,
|
||||
ATTR_SUPPORTED: self.sys_core.supported,
|
||||
ATTR_CHANNEL: self.sys_updater.channel,
|
||||
|
42
supervisor/api/jobs.py
Normal file
42
supervisor/api/jobs.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Init file for Supervisor Jobs RESTful API."""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from ..coresys import CoreSysAttributes
|
||||
from ..jobs.const import ATTR_IGNORE_CONDITIONS, JobCondition
|
||||
from .utils import api_process, api_validate
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
SCHEMA_OPTIONS = vol.Schema(
|
||||
{vol.Optional(ATTR_IGNORE_CONDITIONS): [vol.Coerce(JobCondition)]}
|
||||
)
|
||||
|
||||
|
||||
class APIJobs(CoreSysAttributes):
|
||||
"""Handle RESTful API for OS functions."""
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Return JobManager information."""
|
||||
return {
|
||||
ATTR_IGNORE_CONDITIONS: self.sys_jobs.ignore_conditions,
|
||||
}
|
||||
|
||||
@api_process
|
||||
async def options(self, request: web.Request) -> None:
|
||||
"""Set options for JobManager."""
|
||||
body = await api_validate(SCHEMA_OPTIONS, request)
|
||||
|
||||
if ATTR_IGNORE_CONDITIONS in body:
|
||||
self.sys_jobs.ignore_conditions = body[ATTR_IGNORE_CONDITIONS]
|
||||
|
||||
self.sys_jobs.save_data()
|
||||
|
||||
@api_process
|
||||
async def reset(self, request: web.Request) -> None:
|
||||
"""Reset options for JobManager."""
|
||||
self.sys_jobs.reset_data()
|
@@ -1,7 +1,7 @@
|
||||
"""REST API for network."""
|
||||
import asyncio
|
||||
from ipaddress import ip_address, ip_interface
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Awaitable, Dict
|
||||
|
||||
from aiohttp import web
|
||||
import attr
|
||||
@@ -18,6 +18,7 @@ from ..const import (
|
||||
ATTR_FREQUENCY,
|
||||
ATTR_GATEWAY,
|
||||
ATTR_HOST_INTERNET,
|
||||
ATTR_ID,
|
||||
ATTR_INTERFACE,
|
||||
ATTR_INTERFACES,
|
||||
ATTR_IPV4,
|
||||
@@ -26,6 +27,7 @@ from ..const import (
|
||||
ATTR_METHOD,
|
||||
ATTR_MODE,
|
||||
ATTR_NAMESERVERS,
|
||||
ATTR_PARENT,
|
||||
ATTR_PRIMARY,
|
||||
ATTR_PSK,
|
||||
ATTR_SIGNAL,
|
||||
@@ -80,7 +82,7 @@ SCHEMA_UPDATE = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def ipconfig_struct(config: IpConfig) -> dict:
|
||||
def ipconfig_struct(config: IpConfig) -> Dict[str, Any]:
|
||||
"""Return a dict with information about ip configuration."""
|
||||
return {
|
||||
ATTR_METHOD: config.method,
|
||||
@@ -90,7 +92,7 @@ def ipconfig_struct(config: IpConfig) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def wifi_struct(config: WifiConfig) -> dict:
|
||||
def wifi_struct(config: WifiConfig) -> Dict[str, Any]:
|
||||
"""Return a dict with information about wifi configuration."""
|
||||
return {
|
||||
ATTR_MODE: config.mode,
|
||||
@@ -100,7 +102,15 @@ def wifi_struct(config: WifiConfig) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def interface_struct(interface: Interface) -> dict:
|
||||
def vlan_struct(config: VlanConfig) -> Dict[str, Any]:
|
||||
"""Return a dict with information about VLAN configuration."""
|
||||
return {
|
||||
ATTR_ID: config.id,
|
||||
ATTR_PARENT: config.interface,
|
||||
}
|
||||
|
||||
|
||||
def interface_struct(interface: Interface) -> Dict[str, Any]:
|
||||
"""Return a dict with information of a interface to be used in th API."""
|
||||
return {
|
||||
ATTR_INTERFACE: interface.name,
|
||||
@@ -111,11 +121,11 @@ def interface_struct(interface: Interface) -> dict:
|
||||
ATTR_IPV4: ipconfig_struct(interface.ipv4) if interface.ipv4 else None,
|
||||
ATTR_IPV6: ipconfig_struct(interface.ipv6) if interface.ipv6 else None,
|
||||
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
|
||||
ATTR_VLAN: wifi_struct(interface.vlan) if interface.vlan else None,
|
||||
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
|
||||
}
|
||||
|
||||
|
||||
def accesspoint_struct(accesspoint: AccessPoint) -> dict:
|
||||
def accesspoint_struct(accesspoint: AccessPoint) -> Dict[str, Any]:
|
||||
"""Return a dict for AccessPoint."""
|
||||
return {
|
||||
ATTR_MODE: accesspoint.mode,
|
||||
@@ -145,7 +155,7 @@ class APINetwork(CoreSysAttributes):
|
||||
except HostNetworkNotFound:
|
||||
pass
|
||||
|
||||
raise APIError(f"Interface {name} does not exsist") from None
|
||||
raise APIError(f"Interface {name} does not exist") from None
|
||||
|
||||
@api_process
|
||||
async def info(self, request: web.Request) -> Dict[str, Any]:
|
||||
@@ -207,6 +217,11 @@ class APINetwork(CoreSysAttributes):
|
||||
|
||||
await asyncio.shield(self.sys_host.network.apply_changes(interface))
|
||||
|
||||
@api_process
|
||||
def reload(self, request: web.Request) -> Awaitable[None]:
|
||||
"""Reload network data."""
|
||||
return asyncio.shield(self.sys_host.network.update())
|
||||
|
||||
@api_process
|
||||
async def scan_accesspoints(self, request: web.Request) -> Dict[str, Any]:
|
||||
"""Scan and return a list of available networks."""
|
||||
|
@@ -1,9 +1,9 @@
|
||||
|
||||
try {
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.4ec3f364.js')")();
|
||||
new Function("import('/api/hassio/app/frontend_latest/entrypoint.c1a28650.js')")();
|
||||
} catch (err) {
|
||||
var el = document.createElement('script');
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.1799405f.js';
|
||||
el.src = '/api/hassio/app/frontend_es5/entrypoint.54588d7a.js';
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.0aeb318c1dfa0b946242.js","sources":["webpack://home-assistant-frontend/chunk.0aeb318c1dfa0b946242.js"],"mappings":"AAAA","sourceRoot":""}
|
@@ -1,2 +0,0 @@
|
||||
!function(){"use strict";var n,t={14971:function(n,t,e){e(58556);var r,o,i=e(91107),u=e(9902),a=e.n(u),s=e(62173),f={renderMarkdown:function(n,t){var e,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return r||(r=Object.assign({},(0,s.getDefaultWhiteList)(),{"ha-icon":["icon"],"ha-svg-icon":["path"]})),i.allowSvg?(o||(o=Object.assign({},r,{svg:["xmlns","height","width"],path:["transform","stroke","d"],img:["src"]})),e=o):e=r,(0,s.filterXSS)(a()(n,t),{whiteList:e})}};(0,i.Jj)(f)}},e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={exports:{}};return t[n].call(o.exports,o,o.exports,r),o.exports}r.m=t,r.x=function(){r(14971)},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,{a:t}),t},r.d=function(n,t){for(var e in t)r.o(t,e)&&!r.o(n,e)&&Object.defineProperty(n,e,{enumerable:!0,get:t[e]})},r.f={},r.e=function(n){return Promise.all(Object.keys(r.f).reduce((function(t,e){return r.f[e](n,t),t}),[]))},r.u=function(n){return"chunk.d93e72d29a82b1218f4a.js"},r.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},n=r.x,r.x=function(){return r.e(474).then(n)},r.p="/api/hassio/app/frontend_es5/",function(){var n={971:1};r.f.i=function(t,e){n[t]||importScripts(""+r.u(t))};var t=self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[],e=t.push.bind(t);t.push=function(t){var o=t[0],i=t[1],u=t[2];for(var a in i)r.o(i,a)&&(r.m[a]=i[a]);for(u&&u(r);o.length;)n[o.pop()]=1;e(t)}}(),r.x()}();
|
||||
//# sourceMappingURL=chunk.0cd3dee90fe9e2ba789d.js.map
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.0cd3dee90fe9e2ba789d.js","sources":["webpack://home-assistant-frontend/chunk.0cd3dee90fe9e2ba789d.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.114930542eafd429baaa.js","sources":["webpack://home-assistant-frontend/chunk.114930542eafd429baaa.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.1274df0d4f9b61110840.js","sources":["webpack://home-assistant-frontend/chunk.1274df0d4f9b61110840.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.192eacaff3f9b89ec13e.js","sources":["webpack://home-assistant-frontend/chunk.192eacaff3f9b89ec13e.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.23709353009814f6ec3d.js","sources":["webpack://home-assistant-frontend/chunk.23709353009814f6ec3d.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.3a2a2f6b4ddd0c4883f8.js","sources":["webpack://home-assistant-frontend/chunk.3a2a2f6b4ddd0c4883f8.js"],"mappings":";AAAA","sourceRoot":""}
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.3bff0586eed52d338fd7.js","sources":["webpack://home-assistant-frontend/chunk.3bff0586eed52d338fd7.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.3daef259cd4ead95b329.js","sources":["webpack://home-assistant-frontend/chunk.3daef259cd4ead95b329.js"],"mappings":"AAAA","sourceRoot":""}
|
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.4c51a194fe9fc242f107.js","sources":["webpack://home-assistant-frontend/chunk.4c51a194fe9fc242f107.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.66a403e06c27cd823e0b.js","sources":["webpack://home-assistant-frontend/chunk.66a403e06c27cd823e0b.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
/*! *****************************************************************************
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
***************************************************************************** */
|
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.69339833f81fc6a02f8b.js","sources":["webpack://home-assistant-frontend/chunk.69339833f81fc6a02f8b.js"],"mappings":";AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.6f668ed3050b9fdfc6f2.js","sources":["webpack://home-assistant-frontend/chunk.6f668ed3050b9fdfc6f2.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.84c7c01d0e7eaf24fc0c.js","sources":["webpack://home-assistant-frontend/chunk.84c7c01d0e7eaf24fc0c.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.8c95d60c89a57c50576d.js","sources":["webpack://home-assistant-frontend/chunk.8c95d60c89a57c50576d.js"],"mappings":";AAAA","sourceRoot":""}
|
@@ -0,0 +1,2 @@
|
||||
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[914],{92914:function(r,o,a){"use strict";a.r(o),a.d(o,{keymap:function(){return t.$f},EditorView:function(){return t.tk},EditorState:function(){return l.yy},Prec:function(){return l.Wl},tagExtension:function(){return l.kU},defaultKeymap:function(){return p.wQ},lineNumbers:function(){return d.Eu},searchKeymap:function(){return g.Lp},highlightSelectionMatches:function(){return g.sW},history:function(){return u.m8},historyKeymap:function(){return u.f$},langs:function(){return m},tabKeyBindings:function(){return s},theme:function(){return b},highlightStyle:function(){return v}});var e=a(49619),t=a(19753),n=a(11163),c=a(49413),i=a(8987),p=a(86876),l=a(77885),d=a(11254),g=a(72360),u=a(62118),m={jinja2:n.i.define(c.b),yaml:n.i.define(i.r)},s=[{key:"Tab",run:p.at},{key:"Shift-Tab",run:p.xi}],b=t.tk.theme({$:{color:"var(--primary-text-color)",backgroundColor:"var(--code-editor-background-color, var(--card-background-color))","& ::selection":{backgroundColor:"rgba(var(--rgb-primary-color), 0.3)"},height:"var(--code-mirror-height, auto)",maxHeight:"var(--code-mirror-max-height, unset)"},$scroller:{outline:"none"},$content:{caretColor:"var(--secondary-text-color)"},$$focused:{outline:"none"},"$$focused $cursor":{borderLeftColor:"#var(--secondary-text-color)"},"$$focused $selectionBackground, $selectionBackground":{backgroundColor:"rgba(var(--rgb-primary-color), 0.3)"},$panels:{backgroundColor:"var(--primary-background-color)",color:"var(--primary-text-color)"},"$panels.top":{borderBottom:"1px solid var(--divider-color)"},"$panels.bottom":{borderTop:"1px solid var(--divider-color)"},"$panel.search input":{margin:"4px 4px 0"},$button:{border:"1px solid var(--primary-color)",padding:"0px 16px",textTransform:"uppercase",margin:"4px",background:"none",color:"var(--primary-color)",fontFamily:"var(--mdc-typography-button-font-family, var(--mdc-typography-font-family, Roboto, sans-serif))",fontSize:"var(--mdc-typography-button-font-size, 0.875rem)",height:"36px",fontWeight:"var(--mdc-typography-button-font-weight, 500)",borderRadius:"4px",letterSpacing:"var(--mdc-typography-button-letter-spacing, 0.0892857em)"},$textfield:{padding:"4px 0px 5px",borderRadius:"0",fontSize:"16px",color:"var(--primary-text-color)",border:"0",background:"none",fontFamily:"Roboto",borderBottom:"1px solid var(--paper-input-container-color, var(--secondary-text-color))",margin:"4px 4px 0","& ::placeholder":{color:"var(--paper-input-container-color, var(--secondary-text-color))"},"&:focus":{outline:"none",borderBottom:"2px solid var(--primary-color)",paddingBottom:"4px"}},$selectionMatch:{backgroundColor:"rgba(var(--rgb-primary-color), 0.1)"},$searchMatch:{backgroundColor:"rgba(var(--rgb-accent-color), .2)",outline:"1px solid rgba(var(--rgb-accent-color), .4)"},"$searchMatch.selected":{backgroundColor:"rgba(var(--rgb-accent-color), .4)",outline:"1px solid var(--accent-color)"},$gutters:{backgroundColor:"var(--paper-dialog-background-color, var(--primary-background-color))",color:"var(--paper-dialog-color, var(--secondary-text-color))",border:"none",borderRight:"1px solid var(--paper-input-container-color, var(--secondary-text-color))",paddingRight:"1px"},"$$focused $gutters":{borderRight:"2px solid var(--paper-input-container-focus-color, var(--primary-color))",paddingRight:"0"},"$gutterElementags.lineNumber":{color:"inherit"}}),v=e.Qf.define({tag:e.pJ.keyword,color:"var(--codemirror-keyword, #6262FF)"},{tag:[e.pJ.name,e.pJ.deleted,e.pJ.character,e.pJ.propertyName,e.pJ.macroName],color:"var(--codemirror-property, #905)"},{tag:[e.pJ.function(e.pJ.variableName),e.pJ.labelName],color:"var(--codemirror-variable, #07a)"},{tag:[e.pJ.color,e.pJ.constant(e.pJ.name),e.pJ.standard(e.pJ.name)],color:"var(--codemirror-qualifier, #690)"},{tag:[e.pJ.definition(e.pJ.name),e.pJ.separator],color:"var(--codemirror-def, #8DA6CE)"},{tag:[e.pJ.typeName,e.pJ.className,e.pJ.number,e.pJ.changed,e.pJ.annotation,e.pJ.modifier,e.pJ.self,e.pJ.namespace],color:"var(--codemirror-number, #ca7841)"},{tag:[e.pJ.operator,e.pJ.operatorKeyword,e.pJ.url,e.pJ.escape,e.pJ.regexp,e.pJ.link,e.pJ.special(e.pJ.string)],color:"var(--codemirror-operator, #cda869)"},{tag:e.pJ.comment,color:"var(--codemirror-comment, #777)"},{tag:e.pJ.meta,color:"var(--codemirror-meta, var(--primary-text-color))"},{tag:e.pJ.strong,fontWeight:"bold"},{tag:e.pJ.emphasis,fontStyle:"italic"},{tag:e.pJ.link,color:"var(--primary-color)",textDecoration:"underline"},{tag:e.pJ.heading,fontWeight:"bold"},{tag:e.pJ.atom,color:"var(--codemirror-atom, #F90)"},{tag:e.pJ.bool,color:"var(--codemirror-atom, #F90)"},{tag:e.pJ.special(e.pJ.variableName),color:"var(--codemirror-variable-2, #690)"},{tag:e.pJ.processingInstruction,color:"var(--secondary-text-color)"},{tag:e.pJ.string,color:"var(--codemirror-string, #07a)"},{tag:e.pJ.inserted,color:"var(--codemirror-string2, #07a)"},{tag:e.pJ.invalid,color:"var(--error-color)"})}}]);
|
||||
//# sourceMappingURL=chunk.9f79daafe7d108c4c9cc.js.map
|
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.9f79daafe7d108c4c9cc.js","sources":["webpack://home-assistant-frontend/chunk.9f79daafe7d108c4c9cc.js"],"mappings":"AAAA","sourceRoot":""}
|
@@ -1,2 +0,0 @@
|
||||
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([[528],{92914:function(n,e,r){"use strict";r.r(e),r.d(e,{codeMirror:function(){return c},codeMirrorCss:function(){return i}});var t=r(79074),s=r.n(t),o=r(49338),a=(r(22080),r(19393),r(47181));s().commands.save=function(n){(0,a.B)(n.getWrapperElement(),"editor-save")};var c=s(),i=o.Z}}]);
|
||||
//# sourceMappingURL=chunk.a7915ead0233f6fee305.js.map
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.a7915ead0233f6fee305.js","sources":["webpack://home-assistant-frontend/chunk.a7915ead0233f6fee305.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"chunk.ad46d3b20600ba78423d.js","sources":["webpack://home-assistant-frontend/chunk.ad46d3b20600ba78423d.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.ae740c7fbd846ae0a428.js","sources":["webpack://home-assistant-frontend/chunk.ae740c7fbd846ae0a428.js"],"mappings":"AAAA","sourceRoot":""}
|
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"chunk.afa043f3e39764ffdf47.js","sources":["webpack://home-assistant-frontend/chunk.afa043f3e39764ffdf47.js"],"mappings":"AAAA","sourceRoot":""}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user