mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-08-26 01:19:21 +00:00
Compare commits
140 Commits
2024.12.1
...
fix-error-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0786e06eb9 | ||
![]() |
5b18fb6b12 | ||
![]() |
d42ec12ae8 | ||
![]() |
86133f8ecd | ||
![]() |
12c951f62d | ||
![]() |
fcb3e2eb55 | ||
![]() |
176e511180 | ||
![]() |
696dcf6149 | ||
![]() |
8030b346e0 | ||
![]() |
53d97ce0c6 | ||
![]() |
77523f7bec | ||
![]() |
f4d69f1811 | ||
![]() |
cf5a0dc548 | ||
![]() |
a8cc3ae6ef | ||
![]() |
362bd8fd21 | ||
![]() |
2274de969f | ||
![]() |
dfed251c7a | ||
![]() |
151d4bdd73 | ||
![]() |
c5d4ebcd48 | ||
![]() |
0ad559adcd | ||
![]() |
39f5b91f12 | ||
![]() |
ddee79d209 | ||
![]() |
ff111253d5 | ||
![]() |
31193abb7b | ||
![]() |
ae266e1692 | ||
![]() |
c315a15816 | ||
![]() |
3bd732147c | ||
![]() |
ddbde93a6d | ||
![]() |
6db11a8ade | ||
![]() |
42e78408a7 | ||
![]() |
15e8940c7f | ||
![]() |
644ec45ded | ||
![]() |
a8d2743f56 | ||
![]() |
0acef4a6e6 | ||
![]() |
5733db94aa | ||
![]() |
da8c6cf111 | ||
![]() |
802ee25a8b | ||
![]() |
ce8b107f1e | ||
![]() |
32936e5de0 | ||
![]() |
c35746c3e1 | ||
![]() |
392dd9f904 | ||
![]() |
d8f792950b | ||
![]() |
1f6cdc3018 | ||
![]() |
616f1903b7 | ||
![]() |
997a51fc42 | ||
![]() |
cda6325be4 | ||
![]() |
c8cc6fe003 | ||
![]() |
34939cfe52 | ||
![]() |
37bc703bbb | ||
![]() |
5f8e41b441 | ||
![]() |
606db3585c | ||
![]() |
4054749eb2 | ||
![]() |
ad5827d33f | ||
![]() |
249464e928 | ||
![]() |
3bc55c054a | ||
![]() |
4c108eea64 | ||
![]() |
9b2dbd634d | ||
![]() |
2cb2a48184 | ||
![]() |
ed5a0b511e | ||
![]() |
1475dcb50b | ||
![]() |
5cd7f6fd84 | ||
![]() |
52cc17fa3f | ||
![]() |
fa6949f4e4 | ||
![]() |
63a4cee770 | ||
![]() |
7aed0c1b0d | ||
![]() |
de592a6ef4 | ||
![]() |
ff7086c0d0 | ||
![]() |
ef0352ecd6 | ||
![]() |
7348745049 | ||
![]() |
2078044062 | ||
![]() |
d254937590 | ||
![]() |
9a8e52d1fc | ||
![]() |
6e7fac5493 | ||
![]() |
129a37a1f4 | ||
![]() |
01382e774e | ||
![]() |
9164d35615 | ||
![]() |
58df65541c | ||
![]() |
4c04f364a3 | ||
![]() |
7f39538231 | ||
![]() |
be98e0c0f4 | ||
![]() |
9491b1ff89 | ||
![]() |
30cbb039d0 | ||
![]() |
1aabca9489 | ||
![]() |
28a87db515 | ||
![]() |
05b648629f | ||
![]() |
d1d8446480 | ||
![]() |
8b897ba537 | ||
![]() |
c8f1b222c0 | ||
![]() |
257e2ceb82 | ||
![]() |
67a27cae40 | ||
![]() |
8ff9c08e82 | ||
![]() |
1b0aa30881 | ||
![]() |
2a8d2d2b48 | ||
![]() |
44bd787276 | ||
![]() |
690f1c07a7 | ||
![]() |
8e185a8413 | ||
![]() |
1f7df73964 | ||
![]() |
a10afc45b1 | ||
![]() |
61a2101d8a | ||
![]() |
088832c253 | ||
![]() |
a545b680b3 | ||
![]() |
805017eabf | ||
![]() |
b7412b0679 | ||
![]() |
fff3bfd01e | ||
![]() |
5f165a79ba | ||
![]() |
0d3acd1aca | ||
![]() |
463f196472 | ||
![]() |
52d5df6778 | ||
![]() |
ce75c85e65 | ||
![]() |
12fd61142d | ||
![]() |
0073227785 | ||
![]() |
89a215cc1f | ||
![]() |
b2aece8208 | ||
![]() |
600bf91c4f | ||
![]() |
da6bdfa795 | ||
![]() |
5d4894a1ba | ||
![]() |
d4c047bd01 | ||
![]() |
6183b9719c | ||
![]() |
f02d67ee47 | ||
![]() |
bd156ebb53 | ||
![]() |
b07236b544 | ||
![]() |
5928a31fc4 | ||
![]() |
3a71ea7003 | ||
![]() |
96900b1f1b | ||
![]() |
65b39661a6 | ||
![]() |
18251ae8ae | ||
![]() |
c418e0ea76 | ||
![]() |
74b009ccd7 | ||
![]() |
d2631bf398 | ||
![]() |
c62358d851 | ||
![]() |
e3af04701a | ||
![]() |
c2f6e319f2 | ||
![]() |
61b37877be | ||
![]() |
e72c5a037b | ||
![]() |
578383411c | ||
![]() |
dbd37d6575 | ||
![]() |
c7cf1e7593 | ||
![]() |
c06fb069ab | ||
![]() |
b6c2259bd7 | ||
![]() |
d0b7cc8ab3 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Supervisor dev",
|
"name": "Supervisor dev",
|
||||||
"image": "ghcr.io/home-assistant/devcontainer:supervisor",
|
"image": "ghcr.io/home-assistant/devcontainer:2-supervisor",
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||||
},
|
},
|
||||||
@@ -44,5 +44,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mounts": ["type=volume,target=/var/lib/docker"]
|
"mounts": [
|
||||||
|
"type=volume,target=/var/lib/docker",
|
||||||
|
"type=volume,target=/mnt/supervisor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -26,7 +26,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: What type of installation are you running?
|
label: What type of installation are you running?
|
||||||
description: >
|
description: >
|
||||||
If you don't know, can be found in [Settings -> System -> Repairs -> System Information](https://my.home-assistant.io/redirect/system_health/).
|
If you don't know, can be found in [Settings -> System -> Repairs -> (three dot menu) -> System Information](https://my.home-assistant.io/redirect/system_health/).
|
||||||
It is listed as the `Installation Type` value.
|
It is listed as the `Installation Type` value.
|
||||||
options:
|
options:
|
||||||
- Home Assistant OS
|
- Home Assistant OS
|
||||||
@@ -72,9 +72,9 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: System Health information
|
label: System information
|
||||||
description: >
|
description: >
|
||||||
System Health information can be found in the top right menu in [Settings -> System -> Repairs](https://my.home-assistant.io/redirect/repairs/).
|
The System information can be found in [Settings -> System -> Repairs -> (three dot menu) -> System Information](https://my.home-assistant.io/redirect/system_health/).
|
||||||
Click the copy button at the bottom of the pop-up and paste it here.
|
Click the copy button at the bottom of the pop-up and paste it here.
|
||||||
|
|
||||||
[](https://my.home-assistant.io/redirect/system_health/)
|
[](https://my.home-assistant.io/redirect/system_health/)
|
||||||
@@ -83,8 +83,9 @@ body:
|
|||||||
label: Supervisor diagnostics
|
label: Supervisor diagnostics
|
||||||
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
|
placeholder: "drag-and-drop the diagnostics data file here (do not copy-and-paste the content)"
|
||||||
description: >-
|
description: >-
|
||||||
Supervisor diagnostics can be found in [Settings -> Integrations](https://my.home-assistant.io/redirect/integrations/).
|
Supervisor diagnostics can be found in [Settings -> Devices & services](https://my.home-assistant.io/redirect/integrations/).
|
||||||
Find the card that says `Home Assistant Supervisor`, open its menu and select 'Download diagnostics'.
|
Find the card that says `Home Assistant Supervisor`, open it, and select the three dot menu of the Supervisor integration entry
|
||||||
|
and select 'Download diagnostics'.
|
||||||
|
|
||||||
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
|
**Please drag-and-drop the downloaded file into the textbox below. Do not copy and paste its contents.**
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
12
.github/workflows/builder.yml
vendored
12
.github/workflows/builder.yml
vendored
@@ -33,7 +33,7 @@ on:
|
|||||||
- setup.py
|
- setup.py
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
BUILD_NAME: supervisor
|
BUILD_NAME: supervisor
|
||||||
BUILD_TYPE: supervisor
|
BUILD_TYPE: supervisor
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
if: needs.init.outputs.requirements == 'true'
|
if: needs.init.outputs.requirements == 'true'
|
||||||
uses: home-assistant/wheels@2024.11.0
|
uses: home-assistant/wheels@2024.11.0
|
||||||
with:
|
with:
|
||||||
abi: cp312
|
abi: cp313
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
@@ -125,13 +125,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.publish == 'true'
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@v3.8.1
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.0"
|
cosign-release: "v2.4.0"
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
run: echo "BUILD_ARGS=--test" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build supervisor
|
- name: Build supervisor
|
||||||
uses: home-assistant/builder@2024.08.2
|
uses: home-assistant/builder@2025.02.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -207,7 +207,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build the Supervisor
|
- name: Build the Supervisor
|
||||||
if: needs.init.outputs.publish != 'true'
|
if: needs.init.outputs.publish != 'true'
|
||||||
uses: home-assistant/builder@2024.08.2
|
uses: home-assistant/builder@2025.02.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
--test \
|
--test \
|
||||||
|
52
.github/workflows/ci.yaml
vendored
52
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
pull_request: ~
|
pull_request: ~
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PYTHON: "3.12"
|
DEFAULT_PYTHON: "3.13"
|
||||||
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
PRE_COMMIT_CACHE: ~/.cache/pre-commit
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -28,12 +28,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
pip install -r requirements.txt -r requirements_tests.txt
|
pip install -r requirements.txt -r requirements_tests.txt
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -69,13 +69,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -112,13 +112,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -170,13 +170,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -214,13 +214,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -232,7 +232,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
key: |
|
key: |
|
||||||
@@ -258,13 +258,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -294,17 +294,17 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@v3.8.1
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.0"
|
cosign-release: "v2.4.0"
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -339,7 +339,7 @@ jobs:
|
|||||||
-o console_output_style=count \
|
-o console_output_style=count \
|
||||||
tests
|
tests
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
uses: actions/upload-artifact@v4.5.0
|
uses: actions/upload-artifact@v4.6.1
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.python-version }}
|
name: coverage-${{ matrix.python-version }}
|
||||||
path: .coverage
|
path: .coverage
|
||||||
@@ -353,13 +353,13 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
|
||||||
uses: actions/setup-python@v5.3.0
|
uses: actions/setup-python@v5.4.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ needs.prepare.outputs.python-version }}
|
python-version: ${{ needs.prepare.outputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.0
|
uses: actions/cache@v4.2.2
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: |
|
key: |
|
||||||
@@ -370,7 +370,7 @@ jobs:
|
|||||||
echo "Failed to restore Python virtual environment from cache"
|
echo "Failed to restore Python virtual environment from cache"
|
||||||
exit 1
|
exit 1
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.1.8
|
uses: actions/download-artifact@v4.1.9
|
||||||
- name: Combine coverage results
|
- name: Combine coverage results
|
||||||
run: |
|
run: |
|
||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
@@ -378,4 +378,4 @@ jobs:
|
|||||||
coverage report
|
coverage report
|
||||||
coverage xml
|
coverage xml
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v5.1.2
|
uses: codecov/codecov-action@v5.4.0
|
||||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
echo "version=$datepre.$newpost" >> "$GITHUB_OUTPUT"
|
echo "version=$datepre.$newpost" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Run Release Drafter
|
- name: Run Release Drafter
|
||||||
uses: release-drafter/release-drafter@v6.0.0
|
uses: release-drafter/release-drafter@v6.1.0
|
||||||
with:
|
with:
|
||||||
tag: ${{ steps.version.outputs.version }}
|
tag: ${{ steps.version.outputs.version }}
|
||||||
name: ${{ steps.version.outputs.version }}
|
name: ${{ steps.version.outputs.version }}
|
||||||
|
2
.github/workflows/sentry.yaml
vendored
2
.github/workflows/sentry.yaml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Sentry Release
|
- name: Sentry Release
|
||||||
uses: getsentry/action-release@v1.7.0
|
uses: getsentry/action-release@v1.10.4
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v9.0.0
|
- uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
|
79
.github/workflows/update_frontend.yml
vendored
Normal file
79
.github/workflows/update_frontend.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
name: Update frontend
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule: # once a day
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.check_version.outputs.skip || steps.check_existing_pr.outputs.skip }}
|
||||||
|
current_version: ${{ steps.check_version.outputs.current_version }}
|
||||||
|
latest_version: ${{ steps.latest_frontend_version.outputs.latest_tag }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Get latest frontend release
|
||||||
|
id: latest_frontend_version
|
||||||
|
uses: abatilo/release-info-action@v1.3.3
|
||||||
|
with:
|
||||||
|
owner: home-assistant
|
||||||
|
repo: frontend
|
||||||
|
- name: Check if version is up to date
|
||||||
|
id: check_version
|
||||||
|
run: |
|
||||||
|
current_version="$(cat .ha-frontend-version)"
|
||||||
|
latest_version="${{ steps.latest_frontend_version.outputs.latest_tag }}"
|
||||||
|
echo "current_version=${current_version}" >> $GITHUB_OUTPUT
|
||||||
|
echo "LATEST_VERSION=${latest_version}" >> $GITHUB_ENV
|
||||||
|
if [[ ! "$current_version" < "$latest_version" ]]; then
|
||||||
|
echo "Frontend version is up to date"
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Check if there is no open PR with this version
|
||||||
|
if: steps.check_version.outputs.skip != 'true'
|
||||||
|
id: check_existing_pr
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
PR=$(gh pr list --state open --base main --json title --search "Update frontend to version $LATEST_VERSION")
|
||||||
|
if [[ "$PR" != "[]" ]]; then
|
||||||
|
echo "Skipping - There is already a PR open for version $LATEST_VERSION"
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
create-pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: check-version
|
||||||
|
if: needs.check-version.outputs.skip != 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Clear www folder
|
||||||
|
run: |
|
||||||
|
rm -rf supervisor/api/panel/*
|
||||||
|
- name: Update version file
|
||||||
|
run: |
|
||||||
|
echo "${{ needs.check-version.outputs.latest_version }}" > .ha-frontend-version
|
||||||
|
- name: Download release assets
|
||||||
|
uses: robinraju/release-downloader@v1
|
||||||
|
with:
|
||||||
|
repository: 'home-assistant/frontend'
|
||||||
|
tag: ${{ needs.check-version.outputs.latest_version }}
|
||||||
|
fileName: home_assistant_frontend_supervisor-${{ needs.check-version.outputs.latest_version }}.tar.gz
|
||||||
|
extract: true
|
||||||
|
out-file-path: supervisor/api/panel/
|
||||||
|
- name: Create PR
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
commit-message: "Update frontend to version ${{ needs.check-version.outputs.latest_version }}"
|
||||||
|
branch: autoupdate-frontend
|
||||||
|
base: main
|
||||||
|
draft: true
|
||||||
|
sign-commits: true
|
||||||
|
title: "Update frontend to version ${{ needs.check-version.outputs.latest_version }}"
|
||||||
|
body: >
|
||||||
|
Update frontend from ${{ needs.check-version.outputs.current_version }} to
|
||||||
|
[${{ needs.check-version.outputs.latest_version }}](https://github.com/home-assistant/frontend/releases/tag/${{ needs.check-version.outputs.latest_version }})
|
||||||
|
|
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
|||||||
[submodule "home-assistant-polymer"]
|
|
||||||
path = home-assistant-polymer
|
|
||||||
url = https://github.com/home-assistant/home-assistant-polymer
|
|
||||||
branch = dev
|
|
1
.ha-frontend-version
Normal file
1
.ha-frontend-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
20250221.0
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.5.7
|
rev: v0.9.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@@ -8,7 +8,7 @@ repos:
|
|||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
files: ^((supervisor|tests)/.+)?[^/]+\.py$
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
|
12
Dockerfile
12
Dockerfile
@@ -9,7 +9,8 @@ ENV \
|
|||||||
|
|
||||||
ARG \
|
ARG \
|
||||||
COSIGN_VERSION \
|
COSIGN_VERSION \
|
||||||
BUILD_ARCH
|
BUILD_ARCH \
|
||||||
|
QEMU_CPU
|
||||||
|
|
||||||
# Install base
|
# Install base
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
@@ -28,22 +29,23 @@ RUN \
|
|||||||
\
|
\
|
||||||
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
|
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
|
||||||
&& chmod a+x /usr/bin/cosign \
|
&& chmod a+x /usr/bin/cosign \
|
||||||
&& pip3 install uv==0.2.21
|
&& pip3 install uv==0.6.1
|
||||||
|
|
||||||
# Install requirements
|
# Install requirements
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN \
|
RUN \
|
||||||
if [ "${BUILD_ARCH}" = "i386" ]; then \
|
if [ "${BUILD_ARCH}" = "i386" ]; then \
|
||||||
linux32 uv pip install --no-build -r requirements.txt; \
|
setarch="linux32"; \
|
||||||
else \
|
else \
|
||||||
uv pip install --no-build -r requirements.txt; \
|
setarch=""; \
|
||||||
fi \
|
fi \
|
||||||
|
&& ${setarch} uv pip install --compile-bytecode --no-cache --no-build -r requirements.txt \
|
||||||
&& rm -f requirements.txt
|
&& rm -f requirements.txt
|
||||||
|
|
||||||
# Install Home Assistant Supervisor
|
# Install Home Assistant Supervisor
|
||||||
COPY . supervisor
|
COPY . supervisor
|
||||||
RUN \
|
RUN \
|
||||||
pip3 install -e ./supervisor \
|
uv pip install --no-cache -e ./supervisor \
|
||||||
&& python3 -m compileall ./supervisor/supervisor
|
&& python3 -m compileall ./supervisor/supervisor
|
||||||
|
|
||||||
|
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.20
|
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.13-alpine3.21
|
||||||
armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.20
|
armhf: ghcr.io/home-assistant/armhf-base-python:3.13-alpine3.21
|
||||||
armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.20
|
armv7: ghcr.io/home-assistant/armv7-base-python:3.13-alpine3.21
|
||||||
amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.20
|
amd64: ghcr.io/home-assistant/amd64-base-python:3.13-alpine3.21
|
||||||
i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.20
|
i386: ghcr.io/home-assistant/i386-base-python:3.13-alpine3.21
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
Submodule home-assistant-polymer deleted from 46f0e0212d
@@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools~=75.6.0", "wheel~=0.45.0"]
|
requires = ["setuptools~=75.8.0", "wheel~=0.45.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
@@ -12,7 +12,7 @@ authors = [
|
|||||||
{ name = "The Home Assistant Authors", email = "hello@home-assistant.io" },
|
{ name = "The Home Assistant Authors", email = "hello@home-assistant.io" },
|
||||||
]
|
]
|
||||||
keywords = ["docker", "home-assistant", "api"]
|
keywords = ["docker", "home-assistant", "api"]
|
||||||
requires-python = ">=3.12.0"
|
requires-python = ">=3.13.0"
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
"Homepage" = "https://www.home-assistant.io/"
|
"Homepage" = "https://www.home-assistant.io/"
|
||||||
@@ -31,7 +31,7 @@ include-package-data = true
|
|||||||
include = ["supervisor*"]
|
include = ["supervisor*"]
|
||||||
|
|
||||||
[tool.pylint.MAIN]
|
[tool.pylint.MAIN]
|
||||||
py-version = "3.12"
|
py-version = "3.13"
|
||||||
# Use a conservative default here; 2 should speed up most setups and not hurt
|
# Use a conservative default here; 2 should speed up most setups and not hurt
|
||||||
# any too bad. Override on command line as appropriate.
|
# any too bad. Override on command line as appropriate.
|
||||||
jobs = 2
|
jobs = 2
|
||||||
@@ -147,7 +147,7 @@ disable = [
|
|||||||
# "pointless-statement", # B018, ruff catches new occurrences, needs more work
|
# "pointless-statement", # B018, ruff catches new occurrences, needs more work
|
||||||
"raise-missing-from", # TRY200
|
"raise-missing-from", # TRY200
|
||||||
# "redefined-builtin", # A001, ruff is way more stricter, needs work
|
# "redefined-builtin", # A001, ruff is way more stricter, needs work
|
||||||
"try-except-raise", # TRY302
|
"try-except-raise", # TRY203
|
||||||
"unused-argument", # ARG001, we don't use it
|
"unused-argument", # ARG001, we don't use it
|
||||||
"unused-format-string-argument", #F507
|
"unused-format-string-argument", #F507
|
||||||
"unused-format-string-key", # F504
|
"unused-format-string-key", # F504
|
||||||
@@ -223,6 +223,7 @@ testpaths = ["tests"]
|
|||||||
norecursedirs = [".git"]
|
norecursedirs = [".git"]
|
||||||
log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s"
|
log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s"
|
||||||
log_date_format = "%Y-%m-%d %H:%M:%S"
|
log_date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
asyncio_default_fixture_loop_scope = "function"
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
filterwarnings = [
|
filterwarnings = [
|
||||||
"error",
|
"error",
|
||||||
@@ -289,7 +290,7 @@ lint.select = [
|
|||||||
"T20", # flake8-print
|
"T20", # flake8-print
|
||||||
"TID251", # Banned imports
|
"TID251", # Banned imports
|
||||||
"TRY004", # Prefer TypeError exception for invalid type
|
"TRY004", # Prefer TypeError exception for invalid type
|
||||||
"TRY302", # Remove exception handler; error is immediately re-raised
|
"TRY203", # Remove exception handler; error is immediately re-raised
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"W", # pycodestyle
|
"W", # pycodestyle
|
||||||
]
|
]
|
||||||
|
@@ -1,29 +1,29 @@
|
|||||||
aiodns==3.2.0
|
aiodns==3.2.0
|
||||||
aiohttp==3.11.11
|
aiohttp==3.11.13
|
||||||
atomicwrites-homeassistant==1.4.1
|
atomicwrites-homeassistant==1.4.1
|
||||||
attrs==24.3.0
|
attrs==25.1.0
|
||||||
awesomeversion==24.6.0
|
awesomeversion==24.6.0
|
||||||
brotli==1.1.0
|
brotli==1.1.0
|
||||||
ciso8601==2.3.2
|
ciso8601==2.3.2
|
||||||
colorlog==6.9.0
|
colorlog==6.9.0
|
||||||
cpe==1.3.1
|
cpe==1.3.1
|
||||||
cryptography==44.0.0
|
cryptography==44.0.1
|
||||||
debugpy==1.8.11
|
debugpy==1.8.12
|
||||||
deepmerge==2.0
|
deepmerge==2.0
|
||||||
dirhash==0.5.0
|
dirhash==0.5.0
|
||||||
docker==7.1.0
|
docker==7.1.0
|
||||||
faust-cchardet==2.1.19
|
faust-cchardet==2.1.19
|
||||||
gitpython==3.1.43
|
gitpython==3.1.44
|
||||||
jinja2==3.1.5
|
jinja2==3.1.5
|
||||||
orjson==3.10.12
|
orjson==3.10.12
|
||||||
pulsectl==24.11.0
|
pulsectl==24.12.0
|
||||||
pyudev==0.24.3
|
pyudev==0.24.3
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
securetar==2024.11.0
|
securetar==2025.2.1
|
||||||
sentry-sdk==2.19.2
|
sentry-sdk==2.22.0
|
||||||
setuptools==75.6.0
|
setuptools==75.8.2
|
||||||
voluptuous==0.15.2
|
voluptuous==0.15.2
|
||||||
dbus-fast==2.24.4
|
dbus-fast==2.34.0
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
zlib-fast==0.2.0
|
zlib-fast==0.2.1
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
astroid==3.3.7
|
astroid==3.3.8
|
||||||
coverage==7.6.9
|
coverage==7.6.12
|
||||||
pre-commit==4.0.1
|
pre-commit==4.1.0
|
||||||
pylint==3.3.2
|
pylint==3.3.4
|
||||||
pytest-aiohttp==1.0.5
|
pytest-aiohttp==1.1.0
|
||||||
pytest-asyncio==0.23.6
|
pytest-asyncio==0.25.2
|
||||||
pytest-cov==6.0.0
|
pytest-cov==6.0.0
|
||||||
pytest-timeout==2.3.1
|
pytest-timeout==2.3.1
|
||||||
pytest==8.3.4
|
pytest==8.3.4
|
||||||
ruff==0.8.4
|
ruff==0.9.8
|
||||||
time-machine==2.16.0
|
time-machine==2.16.0
|
||||||
typing_extensions==4.12.2
|
typing_extensions==4.12.2
|
||||||
urllib3==2.3.0
|
urllib3==2.3.0
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
source "/etc/supervisor_scripts/common"
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Update frontend
|
|
||||||
git submodule update --init --recursive --remote
|
|
||||||
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
||||||
cd home-assistant-polymer
|
|
||||||
nvm install
|
|
||||||
script/bootstrap
|
|
||||||
|
|
||||||
# Download translations
|
|
||||||
./script/translations_download
|
|
||||||
|
|
||||||
# build frontend
|
|
||||||
cd hassio
|
|
||||||
./script/build_hassio
|
|
||||||
|
|
||||||
# Copy frontend
|
|
||||||
rm -rf ../../supervisor/api/panel/*
|
|
||||||
cp -rf build/* ../../supervisor/api/panel/
|
|
||||||
|
|
||||||
# Reset frontend git
|
|
||||||
cd ..
|
|
||||||
git reset --hard HEAD
|
|
2
setup.py
2
setup.py
@@ -19,7 +19,7 @@ def _get_supervisor_version():
|
|||||||
for line in CONSTANTS.split("/n"):
|
for line in CONSTANTS.split("/n"):
|
||||||
if match := RE_SUPERVISOR_VERSION.match(line):
|
if match := RE_SUPERVISOR_VERSION.match(line):
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
return "99.9.9dev"
|
return "9999.09.9.dev9999"
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@@ -54,8 +54,7 @@ if __name__ == "__main__":
|
|||||||
loop.set_debug(coresys.config.debug)
|
loop.set_debug(coresys.config.debug)
|
||||||
loop.run_until_complete(coresys.core.connect())
|
loop.run_until_complete(coresys.core.connect())
|
||||||
|
|
||||||
bootstrap.supervisor_debugger(coresys)
|
loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
|
||||||
bootstrap.migrate_system_env(coresys)
|
|
||||||
|
|
||||||
# Signal health startup for container
|
# Signal health startup for container
|
||||||
run_os_startup_check_cleanup()
|
run_os_startup_check_cleanup()
|
||||||
|
@@ -6,6 +6,7 @@ from contextlib import suppress
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import errno
|
import errno
|
||||||
|
from functools import partial
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
@@ -19,7 +20,7 @@ from typing import Any, Final
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from awesomeversion import AwesomeVersionCompareException
|
from awesomeversion import AwesomeVersionCompareException
|
||||||
from deepmerge import Merger
|
from deepmerge import Merger
|
||||||
from securetar import atomic_contents_add, secure_path
|
from securetar import AddFileError, atomic_contents_add, secure_path
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import humanize_error
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ from ..store.addon import AddonStore
|
|||||||
from ..utils import check_port
|
from ..utils import check_port
|
||||||
from ..utils.apparmor import adjust_profile
|
from ..utils.apparmor import adjust_profile
|
||||||
from ..utils.json import read_json_file, write_json_file
|
from ..utils.json import read_json_file, write_json_file
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import async_capture_exception
|
||||||
from .const import (
|
from .const import (
|
||||||
WATCHDOG_MAX_ATTEMPTS,
|
WATCHDOG_MAX_ATTEMPTS,
|
||||||
WATCHDOG_RETRY_SECONDS,
|
WATCHDOG_RETRY_SECONDS,
|
||||||
@@ -242,7 +243,7 @@ class Addon(AddonModel):
|
|||||||
await self.instance.install(self.version, default_image, arch=self.arch)
|
await self.instance.install(self.version, default_image, arch=self.arch)
|
||||||
|
|
||||||
self.persist[ATTR_IMAGE] = default_image
|
self.persist[ATTR_IMAGE] = default_image
|
||||||
self.save_persist()
|
await self.save_persist()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip_address(self) -> IPv4Address:
|
def ip_address(self) -> IPv4Address:
|
||||||
@@ -666,9 +667,9 @@ class Addon(AddonModel):
|
|||||||
"""Is add-on loaded."""
|
"""Is add-on loaded."""
|
||||||
return bool(self._listeners)
|
return bool(self._listeners)
|
||||||
|
|
||||||
def save_persist(self) -> None:
|
async def save_persist(self) -> None:
|
||||||
"""Save data of add-on."""
|
"""Save data of add-on."""
|
||||||
self.sys_addons.data.save_data()
|
await self.sys_addons.data.save_data()
|
||||||
|
|
||||||
async def watchdog_application(self) -> bool:
|
async def watchdog_application(self) -> bool:
|
||||||
"""Return True if application is running."""
|
"""Return True if application is running."""
|
||||||
@@ -771,7 +772,7 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
async def install(self) -> None:
|
async def install(self) -> None:
|
||||||
"""Install and setup this addon."""
|
"""Install and setup this addon."""
|
||||||
self.sys_addons.data.install(self.addon_store)
|
await self.sys_addons.data.install(self.addon_store)
|
||||||
await self.load()
|
await self.load()
|
||||||
|
|
||||||
if not self.path_data.is_dir():
|
if not self.path_data.is_dir():
|
||||||
@@ -789,7 +790,7 @@ class Addon(AddonModel):
|
|||||||
self.latest_version, self.addon_store.image, arch=self.arch
|
self.latest_version, self.addon_store.image, arch=self.arch
|
||||||
)
|
)
|
||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
self.sys_addons.data.uninstall(self)
|
await self.sys_addons.data.uninstall(self)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
# Add to addon manager
|
# Add to addon manager
|
||||||
@@ -838,23 +839,23 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# Cleanup Ingress dynamic port assignment
|
# Cleanup Ingress dynamic port assignment
|
||||||
if self.with_ingress:
|
if self.with_ingress:
|
||||||
|
await self.sys_ingress.del_dynamic_port(self.slug)
|
||||||
self.sys_create_task(self.sys_ingress.reload())
|
self.sys_create_task(self.sys_ingress.reload())
|
||||||
self.sys_ingress.del_dynamic_port(self.slug)
|
|
||||||
|
|
||||||
# Cleanup discovery data
|
# Cleanup discovery data
|
||||||
for message in self.sys_discovery.list_messages:
|
for message in self.sys_discovery.list_messages:
|
||||||
if message.addon != self.slug:
|
if message.addon != self.slug:
|
||||||
continue
|
continue
|
||||||
self.sys_discovery.remove(message)
|
await self.sys_discovery.remove(message)
|
||||||
|
|
||||||
# Cleanup services data
|
# Cleanup services data
|
||||||
for service in self.sys_services.list_services:
|
for service in self.sys_services.list_services:
|
||||||
if self.slug not in service.active:
|
if self.slug not in service.active:
|
||||||
continue
|
continue
|
||||||
service.del_service_data(self)
|
await service.del_service_data(self)
|
||||||
|
|
||||||
# Remove from addon manager
|
# Remove from addon manager
|
||||||
self.sys_addons.data.uninstall(self)
|
await self.sys_addons.data.uninstall(self)
|
||||||
self.sys_addons.local.pop(self.slug)
|
self.sys_addons.local.pop(self.slug)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
@@ -883,7 +884,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.info("Add-on '%s' successfully updated", self.slug)
|
_LOGGER.info("Add-on '%s' successfully updated", self.slug)
|
||||||
self.sys_addons.data.update(store)
|
await self.sys_addons.data.update(store)
|
||||||
await self._check_ingress_port()
|
await self._check_ingress_port()
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
@@ -924,7 +925,7 @@ class Addon(AddonModel):
|
|||||||
except DockerError as err:
|
except DockerError as err:
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
self.sys_addons.data.update(self.addon_store)
|
await self.sys_addons.data.update(self.addon_store)
|
||||||
await self._check_ingress_port()
|
await self._check_ingress_port()
|
||||||
_LOGGER.info("Add-on '%s' successfully rebuilt", self.slug)
|
_LOGGER.info("Add-on '%s' successfully rebuilt", self.slug)
|
||||||
|
|
||||||
@@ -976,11 +977,21 @@ class Addon(AddonModel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Need install/update
|
# Need install/update
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_folder:
|
tmp_folder: TemporaryDirectory | None = None
|
||||||
profile_file = Path(tmp_folder, "apparmor.txt")
|
|
||||||
|
|
||||||
|
def install_update_profile() -> Path:
|
||||||
|
nonlocal tmp_folder
|
||||||
|
tmp_folder = TemporaryDirectory(dir=self.sys_config.path_tmp)
|
||||||
|
profile_file = Path(tmp_folder.name, "apparmor.txt")
|
||||||
adjust_profile(self.slug, self.path_apparmor, profile_file)
|
adjust_profile(self.slug, self.path_apparmor, profile_file)
|
||||||
|
return profile_file
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile_file = await self.sys_run_in_executor(install_update_profile)
|
||||||
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||||
|
finally:
|
||||||
|
if tmp_folder:
|
||||||
|
await self.sys_run_in_executor(tmp_folder.cleanup)
|
||||||
|
|
||||||
async def uninstall_apparmor(self) -> None:
|
async def uninstall_apparmor(self) -> None:
|
||||||
"""Remove AppArmor profile for Add-on."""
|
"""Remove AppArmor profile for Add-on."""
|
||||||
@@ -1052,7 +1063,7 @@ class Addon(AddonModel):
|
|||||||
|
|
||||||
# Access Token
|
# Access Token
|
||||||
self.persist[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
|
self.persist[ATTR_ACCESS_TOKEN] = secrets.token_hex(56)
|
||||||
self.save_persist()
|
await self.save_persist()
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
await self.write_options()
|
await self.write_options()
|
||||||
@@ -1207,6 +1218,25 @@ class Addon(AddonModel):
|
|||||||
await self._backup_command(self.backup_post)
|
await self._backup_command(self.backup_post)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _is_excluded_by_filter(
|
||||||
|
self, origin_path: Path, arcname: str, item_arcpath: PurePath
|
||||||
|
) -> bool:
|
||||||
|
"""Filter out files from backup based on filters provided by addon developer.
|
||||||
|
|
||||||
|
This tests the dev provided filters against the full path of the file as
|
||||||
|
Supervisor sees them using match. This is done for legacy reasons, testing
|
||||||
|
against the relative path makes more sense and may be changed in the future.
|
||||||
|
"""
|
||||||
|
full_path = origin_path / item_arcpath.relative_to(arcname)
|
||||||
|
|
||||||
|
for exclude in self.backup_exclude:
|
||||||
|
if not full_path.match(exclude):
|
||||||
|
continue
|
||||||
|
_LOGGER.debug("Ignoring %s because of %s", full_path, exclude)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="addon_backup",
|
name="addon_backup",
|
||||||
limit=JobExecutionLimit.GROUP_ONCE,
|
limit=JobExecutionLimit.GROUP_ONCE,
|
||||||
@@ -1218,46 +1248,45 @@ class Addon(AddonModel):
|
|||||||
Returns a Task that completes when addon has state 'started' (see start)
|
Returns a Task that completes when addon has state 'started' (see start)
|
||||||
for cold backup. Else nothing is returned.
|
for cold backup. Else nothing is returned.
|
||||||
"""
|
"""
|
||||||
wait_for_start: Awaitable[None] | None = None
|
|
||||||
|
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
def _addon_backup(
|
||||||
temp_path = Path(temp)
|
store_image: bool,
|
||||||
|
metadata: dict[str, Any],
|
||||||
|
apparmor_profile: str | None,
|
||||||
|
addon_config_used: bool,
|
||||||
|
):
|
||||||
|
"""Start the backup process."""
|
||||||
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
||||||
|
temp_path = Path(temp)
|
||||||
|
|
||||||
# store local image
|
# store local image
|
||||||
if self.need_build:
|
if store_image:
|
||||||
|
try:
|
||||||
|
self.instance.export_image(temp_path.joinpath("image.tar"))
|
||||||
|
except DockerError as err:
|
||||||
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
# Store local configs/state
|
||||||
try:
|
try:
|
||||||
await self.instance.export_image(temp_path.joinpath("image.tar"))
|
write_json_file(temp_path.joinpath("addon.json"), metadata)
|
||||||
except DockerError as err:
|
except ConfigurationFileError as err:
|
||||||
raise AddonsError() from err
|
|
||||||
|
|
||||||
data = {
|
|
||||||
ATTR_USER: self.persist,
|
|
||||||
ATTR_SYSTEM: self.data,
|
|
||||||
ATTR_VERSION: self.version,
|
|
||||||
ATTR_STATE: _MAP_ADDON_STATE.get(self.state, self.state),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Store local configs/state
|
|
||||||
try:
|
|
||||||
write_json_file(temp_path.joinpath("addon.json"), data)
|
|
||||||
except ConfigurationFileError as err:
|
|
||||||
raise AddonsError(
|
|
||||||
f"Can't save meta for {self.slug}", _LOGGER.error
|
|
||||||
) from err
|
|
||||||
|
|
||||||
# Store AppArmor Profile
|
|
||||||
if self.sys_host.apparmor.exists(self.slug):
|
|
||||||
profile = temp_path.joinpath("apparmor.txt")
|
|
||||||
try:
|
|
||||||
await self.sys_host.apparmor.backup_profile(self.slug, profile)
|
|
||||||
except HostAppArmorError as err:
|
|
||||||
raise AddonsError(
|
raise AddonsError(
|
||||||
"Can't backup AppArmor profile", _LOGGER.error
|
f"Can't save meta for {self.slug}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
# write into tarfile
|
# Store AppArmor Profile
|
||||||
def _write_tarfile():
|
if apparmor_profile:
|
||||||
"""Write tar inside loop."""
|
profile_backup_file = temp_path.joinpath("apparmor.txt")
|
||||||
|
try:
|
||||||
|
self.sys_host.apparmor.backup_profile(
|
||||||
|
apparmor_profile, profile_backup_file
|
||||||
|
)
|
||||||
|
except HostAppArmorError as err:
|
||||||
|
raise AddonsError(
|
||||||
|
"Can't backup AppArmor profile", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
|
# Write tarfile
|
||||||
with tar_file as backup:
|
with tar_file as backup:
|
||||||
# Backup metadata
|
# Backup metadata
|
||||||
backup.add(temp, arcname=".")
|
backup.add(temp, arcname=".")
|
||||||
@@ -1266,32 +1295,56 @@ class Addon(AddonModel):
|
|||||||
atomic_contents_add(
|
atomic_contents_add(
|
||||||
backup,
|
backup,
|
||||||
self.path_data,
|
self.path_data,
|
||||||
excludes=self.backup_exclude,
|
file_filter=partial(
|
||||||
|
self._is_excluded_by_filter, self.path_data, "data"
|
||||||
|
),
|
||||||
arcname="data",
|
arcname="data",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Backup config
|
# Backup config
|
||||||
if self.addon_config_used:
|
if addon_config_used:
|
||||||
atomic_contents_add(
|
atomic_contents_add(
|
||||||
backup,
|
backup,
|
||||||
self.path_config,
|
self.path_config,
|
||||||
excludes=self.backup_exclude,
|
file_filter=partial(
|
||||||
|
self._is_excluded_by_filter, self.path_config, "config"
|
||||||
|
),
|
||||||
arcname="config",
|
arcname="config",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_running = await self.begin_backup()
|
wait_for_start: Awaitable[None] | None = None
|
||||||
try:
|
|
||||||
_LOGGER.info("Building backup for add-on %s", self.slug)
|
data = {
|
||||||
await self.sys_run_in_executor(_write_tarfile)
|
ATTR_USER: self.persist,
|
||||||
except (tarfile.TarError, OSError) as err:
|
ATTR_SYSTEM: self.data,
|
||||||
raise AddonsError(
|
ATTR_VERSION: self.version,
|
||||||
f"Can't write tarfile {tar_file}: {err}", _LOGGER.error
|
ATTR_STATE: _MAP_ADDON_STATE.get(self.state, self.state),
|
||||||
) from err
|
}
|
||||||
finally:
|
apparmor_profile = (
|
||||||
if is_running:
|
self.slug if self.sys_host.apparmor.exists(self.slug) else None
|
||||||
wait_for_start = await self.end_backup()
|
)
|
||||||
|
|
||||||
|
was_running = await self.begin_backup()
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Building backup for add-on %s", self.slug)
|
||||||
|
await self.sys_run_in_executor(
|
||||||
|
partial(
|
||||||
|
_addon_backup,
|
||||||
|
store_image=self.need_build,
|
||||||
|
metadata=data,
|
||||||
|
apparmor_profile=apparmor_profile,
|
||||||
|
addon_config_used=self.addon_config_used,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.info("Finish backup for addon %s", self.slug)
|
||||||
|
except (tarfile.TarError, OSError, AddFileError) as err:
|
||||||
|
raise AddonsError(
|
||||||
|
f"Can't write tarfile {tar_file}: {err}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
finally:
|
||||||
|
if was_running:
|
||||||
|
wait_for_start = await self.end_backup()
|
||||||
|
|
||||||
_LOGGER.info("Finish backup for addon %s", self.slug)
|
|
||||||
return wait_for_start
|
return wait_for_start
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
@@ -1306,30 +1359,36 @@ class Addon(AddonModel):
|
|||||||
if addon is started after restore. Else nothing is returned.
|
if addon is started after restore. Else nothing is returned.
|
||||||
"""
|
"""
|
||||||
wait_for_start: Awaitable[None] | None = None
|
wait_for_start: Awaitable[None] | None = None
|
||||||
with TemporaryDirectory(dir=self.sys_config.path_tmp) as temp:
|
|
||||||
# extract backup
|
# Extract backup
|
||||||
def _extract_tarfile():
|
def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]:
|
||||||
"""Extract tar backup."""
|
"""Extract tar backup."""
|
||||||
|
tmp = TemporaryDirectory(dir=self.sys_config.path_tmp)
|
||||||
|
try:
|
||||||
with tar_file as backup:
|
with tar_file as backup:
|
||||||
backup.extractall(
|
backup.extractall(
|
||||||
path=Path(temp),
|
path=tmp.name,
|
||||||
members=secure_path(backup),
|
members=secure_path(backup),
|
||||||
filter="fully_trusted",
|
filter="fully_trusted",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
data = read_json_file(Path(tmp.name, "addon.json"))
|
||||||
await self.sys_run_in_executor(_extract_tarfile)
|
except:
|
||||||
except tarfile.TarError as err:
|
tmp.cleanup()
|
||||||
raise AddonsError(
|
raise
|
||||||
f"Can't read tarfile {tar_file}: {err}", _LOGGER.error
|
|
||||||
) from err
|
|
||||||
|
|
||||||
# Read backup data
|
return tmp, data
|
||||||
try:
|
|
||||||
data = read_json_file(Path(temp, "addon.json"))
|
|
||||||
except ConfigurationFileError as err:
|
|
||||||
raise AddonsError() from err
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
tmp, data = await self.sys_run_in_executor(_extract_tarfile)
|
||||||
|
except tarfile.TarError as err:
|
||||||
|
raise AddonsError(
|
||||||
|
f"Can't read tarfile {tar_file}: {err}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
except ConfigurationFileError as err:
|
||||||
|
raise AddonsError() from err
|
||||||
|
|
||||||
|
try:
|
||||||
# Validate
|
# Validate
|
||||||
try:
|
try:
|
||||||
data = SCHEMA_ADDON_BACKUP(data)
|
data = SCHEMA_ADDON_BACKUP(data)
|
||||||
@@ -1349,7 +1408,7 @@ class Addon(AddonModel):
|
|||||||
# Restore local add-on information
|
# Restore local add-on information
|
||||||
_LOGGER.info("Restore config for addon %s", self.slug)
|
_LOGGER.info("Restore config for addon %s", self.slug)
|
||||||
restore_image = self._image(data[ATTR_SYSTEM])
|
restore_image = self._image(data[ATTR_SYSTEM])
|
||||||
self.sys_addons.data.restore(
|
await self.sys_addons.data.restore(
|
||||||
self.slug, data[ATTR_USER], data[ATTR_SYSTEM], restore_image
|
self.slug, data[ATTR_USER], data[ATTR_SYSTEM], restore_image
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1363,7 +1422,7 @@ class Addon(AddonModel):
|
|||||||
if not await self.instance.exists():
|
if not await self.instance.exists():
|
||||||
_LOGGER.info("Restore/Install of image for addon %s", self.slug)
|
_LOGGER.info("Restore/Install of image for addon %s", self.slug)
|
||||||
|
|
||||||
image_file = Path(temp, "image.tar")
|
image_file = Path(tmp.name, "image.tar")
|
||||||
if image_file.is_file():
|
if image_file.is_file():
|
||||||
with suppress(DockerError):
|
with suppress(DockerError):
|
||||||
await self.instance.import_image(image_file)
|
await self.instance.import_image(image_file)
|
||||||
@@ -1382,13 +1441,13 @@ class Addon(AddonModel):
|
|||||||
# Restore data and config
|
# Restore data and config
|
||||||
def _restore_data():
|
def _restore_data():
|
||||||
"""Restore data and config."""
|
"""Restore data and config."""
|
||||||
temp_data = Path(temp, "data")
|
temp_data = Path(tmp.name, "data")
|
||||||
if temp_data.is_dir():
|
if temp_data.is_dir():
|
||||||
shutil.copytree(temp_data, self.path_data, symlinks=True)
|
shutil.copytree(temp_data, self.path_data, symlinks=True)
|
||||||
else:
|
else:
|
||||||
self.path_data.mkdir()
|
self.path_data.mkdir()
|
||||||
|
|
||||||
temp_config = Path(temp, "config")
|
temp_config = Path(tmp.name, "config")
|
||||||
if temp_config.is_dir():
|
if temp_config.is_dir():
|
||||||
shutil.copytree(temp_config, self.path_config, symlinks=True)
|
shutil.copytree(temp_config, self.path_config, symlinks=True)
|
||||||
elif self.addon_config_used:
|
elif self.addon_config_used:
|
||||||
@@ -1408,7 +1467,7 @@ class Addon(AddonModel):
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
# Restore AppArmor
|
# Restore AppArmor
|
||||||
profile_file = Path(temp, "apparmor.txt")
|
profile_file = Path(tmp.name, "apparmor.txt")
|
||||||
if profile_file.exists():
|
if profile_file.exists():
|
||||||
try:
|
try:
|
||||||
await self.sys_host.apparmor.load_profile(
|
await self.sys_host.apparmor.load_profile(
|
||||||
@@ -1416,7 +1475,8 @@ class Addon(AddonModel):
|
|||||||
)
|
)
|
||||||
except HostAppArmorError as err:
|
except HostAppArmorError as err:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"Can't restore AppArmor profile for add-on %s", self.slug
|
"Can't restore AppArmor profile for add-on %s",
|
||||||
|
self.slug,
|
||||||
)
|
)
|
||||||
raise AddonsError() from err
|
raise AddonsError() from err
|
||||||
|
|
||||||
@@ -1428,7 +1488,8 @@ class Addon(AddonModel):
|
|||||||
# Run add-on
|
# Run add-on
|
||||||
if data[ATTR_STATE] == AddonState.STARTED:
|
if data[ATTR_STATE] == AddonState.STARTED:
|
||||||
wait_for_start = await self.start()
|
wait_for_start = await self.start()
|
||||||
|
finally:
|
||||||
|
tmp.cleanup()
|
||||||
_LOGGER.info("Finished restore for add-on %s", self.slug)
|
_LOGGER.info("Finished restore for add-on %s", self.slug)
|
||||||
return wait_for_start
|
return wait_for_start
|
||||||
|
|
||||||
@@ -1469,7 +1530,7 @@ class Addon(AddonModel):
|
|||||||
except AddonsError as err:
|
except AddonsError as err:
|
||||||
attempts = attempts + 1
|
attempts = attempts + 1
|
||||||
_LOGGER.error("Watchdog restart of addon %s failed!", self.name)
|
_LOGGER.error("Watchdog restart of addon %s failed!", self.name)
|
||||||
capture_exception(err)
|
await async_capture_exception(err)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@@ -34,16 +34,29 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
|
|||||||
self.coresys: CoreSys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self.addon = addon
|
self.addon = addon
|
||||||
|
|
||||||
|
# Search for build file later in executor
|
||||||
|
super().__init__(None, SCHEMA_BUILD_CONFIG)
|
||||||
|
|
||||||
|
def _get_build_file(self) -> Path:
|
||||||
|
"""Get build file.
|
||||||
|
|
||||||
|
Must be run in executor.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
build_file = find_one_filetype(
|
return find_one_filetype(
|
||||||
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
self.addon.path_location, "build", FILE_SUFFIX_CONFIGURATION
|
||||||
)
|
)
|
||||||
except ConfigurationFileError:
|
except ConfigurationFileError:
|
||||||
build_file = self.addon.path_location / "build.json"
|
return self.addon.path_location / "build.json"
|
||||||
|
|
||||||
super().__init__(build_file, SCHEMA_BUILD_CONFIG)
|
async def read_data(self) -> None:
|
||||||
|
"""Load data from file."""
|
||||||
|
if not self._file:
|
||||||
|
self._file = await self.sys_run_in_executor(self._get_build_file)
|
||||||
|
|
||||||
def save_data(self):
|
await super().read_data()
|
||||||
|
|
||||||
|
async def save_data(self):
|
||||||
"""Ignore save function."""
|
"""Ignore save function."""
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
|
@@ -38,7 +38,7 @@ class AddonsData(FileConfiguration, CoreSysAttributes):
|
|||||||
"""Return local add-on data."""
|
"""Return local add-on data."""
|
||||||
return self._data[ATTR_SYSTEM]
|
return self._data[ATTR_SYSTEM]
|
||||||
|
|
||||||
def install(self, addon: AddonStore) -> None:
|
async def install(self, addon: AddonStore) -> None:
|
||||||
"""Set addon as installed."""
|
"""Set addon as installed."""
|
||||||
self.system[addon.slug] = deepcopy(addon.data)
|
self.system[addon.slug] = deepcopy(addon.data)
|
||||||
self.user[addon.slug] = {
|
self.user[addon.slug] = {
|
||||||
@@ -46,26 +46,28 @@ class AddonsData(FileConfiguration, CoreSysAttributes):
|
|||||||
ATTR_VERSION: addon.version,
|
ATTR_VERSION: addon.version,
|
||||||
ATTR_IMAGE: addon.image,
|
ATTR_IMAGE: addon.image,
|
||||||
}
|
}
|
||||||
self.save_data()
|
await self.save_data()
|
||||||
|
|
||||||
def uninstall(self, addon: Addon) -> None:
|
async def uninstall(self, addon: Addon) -> None:
|
||||||
"""Set add-on as uninstalled."""
|
"""Set add-on as uninstalled."""
|
||||||
self.system.pop(addon.slug, None)
|
self.system.pop(addon.slug, None)
|
||||||
self.user.pop(addon.slug, None)
|
self.user.pop(addon.slug, None)
|
||||||
self.save_data()
|
await self.save_data()
|
||||||
|
|
||||||
def update(self, addon: AddonStore) -> None:
|
async def update(self, addon: AddonStore) -> None:
|
||||||
"""Update version of add-on."""
|
"""Update version of add-on."""
|
||||||
self.system[addon.slug] = deepcopy(addon.data)
|
self.system[addon.slug] = deepcopy(addon.data)
|
||||||
self.user[addon.slug].update(
|
self.user[addon.slug].update(
|
||||||
{ATTR_VERSION: addon.version, ATTR_IMAGE: addon.image}
|
{ATTR_VERSION: addon.version, ATTR_IMAGE: addon.image}
|
||||||
)
|
)
|
||||||
self.save_data()
|
await self.save_data()
|
||||||
|
|
||||||
def restore(self, slug: str, user: Config, system: Config, image: str) -> None:
|
async def restore(
|
||||||
|
self, slug: str, user: Config, system: Config, image: str
|
||||||
|
) -> None:
|
||||||
"""Restore data to add-on."""
|
"""Restore data to add-on."""
|
||||||
self.user[slug] = deepcopy(user)
|
self.user[slug] = deepcopy(user)
|
||||||
self.system[slug] = deepcopy(system)
|
self.system[slug] = deepcopy(system)
|
||||||
|
|
||||||
self.user[slug][ATTR_IMAGE] = image
|
self.user[slug][ATTR_IMAGE] = image
|
||||||
self.save_data()
|
await self.save_data()
|
||||||
|
@@ -5,7 +5,7 @@ from collections.abc import Awaitable
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import logging
|
import logging
|
||||||
import tarfile
|
import tarfile
|
||||||
from typing import Union
|
from typing import Self, Union
|
||||||
|
|
||||||
from attr import evolve
|
from attr import evolve
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ from ..exceptions import (
|
|||||||
from ..jobs.decorator import Job, JobCondition
|
from ..jobs.decorator import Job, JobCondition
|
||||||
from ..resolution.const import ContextType, IssueType, SuggestionType
|
from ..resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from ..store.addon import AddonStore
|
from ..store.addon import AddonStore
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import async_capture_exception
|
||||||
from .addon import Addon
|
from .addon import Addon
|
||||||
from .const import ADDON_UPDATE_CONDITIONS
|
from .const import ADDON_UPDATE_CONDITIONS
|
||||||
from .data import AddonsData
|
from .data import AddonsData
|
||||||
@@ -74,6 +74,11 @@ class AddonManager(CoreSysAttributes):
|
|||||||
return addon
|
return addon
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def load_config(self) -> Self:
|
||||||
|
"""Load config in executor."""
|
||||||
|
await self.data.read_data()
|
||||||
|
return self
|
||||||
|
|
||||||
async def load(self) -> None:
|
async def load(self) -> None:
|
||||||
"""Start up add-on management."""
|
"""Start up add-on management."""
|
||||||
# Refresh cache for all store addons
|
# Refresh cache for all store addons
|
||||||
@@ -165,7 +170,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
await addon.stop()
|
await addon.stop()
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
_LOGGER.warning("Can't stop Add-on %s: %s", addon.slug, err)
|
||||||
capture_exception(err)
|
await async_capture_exception(err)
|
||||||
|
|
||||||
@Job(
|
@Job(
|
||||||
name="addon_manager_install",
|
name="addon_manager_install",
|
||||||
@@ -383,7 +388,7 @@ class AddonManager(CoreSysAttributes):
|
|||||||
reference=addon.slug,
|
reference=addon.slug,
|
||||||
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
suggestions=[SuggestionType.EXECUTE_REPAIR],
|
||||||
)
|
)
|
||||||
capture_exception(err)
|
await async_capture_exception(err)
|
||||||
else:
|
else:
|
||||||
add_host_coros.append(
|
add_host_coros.append(
|
||||||
self.sys_plugins.dns.add_host(
|
self.sys_plugins.dns.add_host(
|
||||||
|
@@ -210,18 +210,6 @@ class AddonModel(JobGroup, ABC):
|
|||||||
"""Return description of add-on."""
|
"""Return description of add-on."""
|
||||||
return self.data[ATTR_DESCRIPTON]
|
return self.data[ATTR_DESCRIPTON]
|
||||||
|
|
||||||
@property
|
|
||||||
def long_description(self) -> str | None:
|
|
||||||
"""Return README.md as long_description."""
|
|
||||||
readme = Path(self.path_location, "README.md")
|
|
||||||
|
|
||||||
# If readme not exists
|
|
||||||
if not readme.exists():
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Return data
|
|
||||||
return readme.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def repository(self) -> str:
|
def repository(self) -> str:
|
||||||
"""Return repository of add-on."""
|
"""Return repository of add-on."""
|
||||||
@@ -646,6 +634,21 @@ class AddonModel(JobGroup, ABC):
|
|||||||
"""Return breaking versions of addon."""
|
"""Return breaking versions of addon."""
|
||||||
return self.data[ATTR_BREAKING_VERSIONS]
|
return self.data[ATTR_BREAKING_VERSIONS]
|
||||||
|
|
||||||
|
async def long_description(self) -> str | None:
|
||||||
|
"""Return README.md as long_description."""
|
||||||
|
|
||||||
|
def read_readme() -> str | None:
|
||||||
|
readme = Path(self.path_location, "README.md")
|
||||||
|
|
||||||
|
# If readme not exists
|
||||||
|
if not readme.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Return data
|
||||||
|
return readme.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
return await self.sys_run_in_executor(read_readme)
|
||||||
|
|
||||||
def refresh_path_cache(self) -> Awaitable[None]:
|
def refresh_path_cache(self) -> Awaitable[None]:
|
||||||
"""Refresh cache of existing paths."""
|
"""Refresh cache of existing paths."""
|
||||||
|
|
||||||
|
@@ -46,6 +46,7 @@ def rating_security(addon: AddonModel) -> int:
|
|||||||
privilege in addon.privileged
|
privilege in addon.privileged
|
||||||
for privilege in (
|
for privilege in (
|
||||||
Capabilities.BPF,
|
Capabilities.BPF,
|
||||||
|
Capabilities.CHECKPOINT_RESTORE,
|
||||||
Capabilities.DAC_READ_SEARCH,
|
Capabilities.DAC_READ_SEARCH,
|
||||||
Capabilities.NET_ADMIN,
|
Capabilities.NET_ADMIN,
|
||||||
Capabilities.NET_RAW,
|
Capabilities.NET_RAW,
|
||||||
|
@@ -10,7 +10,7 @@ from aiohttp import web
|
|||||||
from ..const import AddonState
|
from ..const import AddonState
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
|
from ..exceptions import APIAddonNotInstalled, HostNotSupportedError
|
||||||
from ..utils.sentry import capture_exception
|
from ..utils.sentry import async_capture_exception
|
||||||
from .addons import APIAddons
|
from .addons import APIAddons
|
||||||
from .audio import APIAudio
|
from .audio import APIAudio
|
||||||
from .auth import APIAuth
|
from .auth import APIAuth
|
||||||
@@ -412,7 +412,7 @@ class RestAPI(CoreSysAttributes):
|
|||||||
if not isinstance(err, HostNotSupportedError):
|
if not isinstance(err, HostNotSupportedError):
|
||||||
# No need to capture HostNotSupportedError to Sentry, the cause
|
# No need to capture HostNotSupportedError to Sentry, the cause
|
||||||
# is known and reported to the user using the resolution center.
|
# is known and reported to the user using the resolution center.
|
||||||
capture_exception(err)
|
await async_capture_exception(err)
|
||||||
kwargs.pop("follow", None) # Follow is not supported for Docker logs
|
kwargs.pop("follow", None) # Follow is not supported for Docker logs
|
||||||
return await api_supervisor.logs(*args, **kwargs)
|
return await api_supervisor.logs(*args, **kwargs)
|
||||||
|
|
||||||
|
@@ -212,7 +212,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_HOSTNAME: addon.hostname,
|
ATTR_HOSTNAME: addon.hostname,
|
||||||
ATTR_DNS: addon.dns,
|
ATTR_DNS: addon.dns,
|
||||||
ATTR_DESCRIPTON: addon.description,
|
ATTR_DESCRIPTON: addon.description,
|
||||||
ATTR_LONG_DESCRIPTION: addon.long_description,
|
ATTR_LONG_DESCRIPTION: await addon.long_description(),
|
||||||
ATTR_ADVANCED: addon.advanced,
|
ATTR_ADVANCED: addon.advanced,
|
||||||
ATTR_STAGE: addon.stage,
|
ATTR_STAGE: addon.stage,
|
||||||
ATTR_REPOSITORY: addon.repository,
|
ATTR_REPOSITORY: addon.repository,
|
||||||
@@ -322,7 +322,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if ATTR_WATCHDOG in body:
|
if ATTR_WATCHDOG in body:
|
||||||
addon.watchdog = body[ATTR_WATCHDOG]
|
addon.watchdog = body[ATTR_WATCHDOG]
|
||||||
|
|
||||||
addon.save_persist()
|
await addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def sys_options(self, request: web.Request) -> None:
|
async def sys_options(self, request: web.Request) -> None:
|
||||||
@@ -336,7 +336,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
if ATTR_SYSTEM_MANAGED_CONFIG_ENTRY in body:
|
if ATTR_SYSTEM_MANAGED_CONFIG_ENTRY in body:
|
||||||
addon.system_managed_config_entry = body[ATTR_SYSTEM_MANAGED_CONFIG_ENTRY]
|
addon.system_managed_config_entry = body[ATTR_SYSTEM_MANAGED_CONFIG_ENTRY]
|
||||||
|
|
||||||
addon.save_persist()
|
await addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def options_validate(self, request: web.Request) -> None:
|
async def options_validate(self, request: web.Request) -> None:
|
||||||
@@ -402,7 +402,7 @@ class APIAddons(CoreSysAttributes):
|
|||||||
_LOGGER.warning("Changing protected flag for %s!", addon.slug)
|
_LOGGER.warning("Changing protected flag for %s!", addon.slug)
|
||||||
addon.protected = body[ATTR_PROTECTED]
|
addon.protected = body[ATTR_PROTECTED]
|
||||||
|
|
||||||
addon.save_persist()
|
await addon.save_persist()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
@@ -99,7 +99,7 @@ class APIAuth(CoreSysAttributes):
|
|||||||
@api_process
|
@api_process
|
||||||
async def cache(self, request: web.Request) -> None:
|
async def cache(self, request: web.Request) -> None:
|
||||||
"""Process cache reset request."""
|
"""Process cache reset request."""
|
||||||
self.sys_auth.reset_data()
|
await self.sys_auth.reset_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def list_users(self, request: web.Request) -> dict[str, list[dict[str, Any]]]:
|
async def list_users(self, request: web.Request) -> dict[str, list[dict[str, Any]]]:
|
||||||
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import errno
|
import errno
|
||||||
|
from io import IOBase
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
@@ -14,6 +15,7 @@ from typing import Any
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.hdrs import CONTENT_DISPOSITION
|
from aiohttp.hdrs import CONTENT_DISPOSITION
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from voluptuous.humanize import humanize_error
|
||||||
|
|
||||||
from ..backups.backup import Backup
|
from ..backups.backup import Backup
|
||||||
from ..backups.const import LOCATION_CLOUD_BACKUP, LOCATION_TYPE
|
from ..backups.const import LOCATION_CLOUD_BACKUP, LOCATION_TYPE
|
||||||
@@ -26,6 +28,7 @@ from ..const import (
|
|||||||
ATTR_DATE,
|
ATTR_DATE,
|
||||||
ATTR_DAYS_UNTIL_STALE,
|
ATTR_DAYS_UNTIL_STALE,
|
||||||
ATTR_EXTRA,
|
ATTR_EXTRA,
|
||||||
|
ATTR_FILENAME,
|
||||||
ATTR_FOLDERS,
|
ATTR_FOLDERS,
|
||||||
ATTR_HOMEASSISTANT,
|
ATTR_HOMEASSISTANT,
|
||||||
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
|
ATTR_HOMEASSISTANT_EXCLUDE_DATABASE,
|
||||||
@@ -33,9 +36,11 @@ from ..const import (
|
|||||||
ATTR_LOCATION,
|
ATTR_LOCATION,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
ATTR_PASSWORD,
|
ATTR_PASSWORD,
|
||||||
|
ATTR_PATH,
|
||||||
ATTR_PROTECTED,
|
ATTR_PROTECTED,
|
||||||
ATTR_REPOSITORIES,
|
ATTR_REPOSITORIES,
|
||||||
ATTR_SIZE,
|
ATTR_SIZE,
|
||||||
|
ATTR_SIZE_BYTES,
|
||||||
ATTR_SLUG,
|
ATTR_SLUG,
|
||||||
ATTR_SUPERVISOR_VERSION,
|
ATTR_SUPERVISOR_VERSION,
|
||||||
ATTR_TIMEOUT,
|
ATTR_TIMEOUT,
|
||||||
@@ -53,8 +58,8 @@ from ..resolution.const import UnhealthyReason
|
|||||||
from .const import (
|
from .const import (
|
||||||
ATTR_ADDITIONAL_LOCATIONS,
|
ATTR_ADDITIONAL_LOCATIONS,
|
||||||
ATTR_BACKGROUND,
|
ATTR_BACKGROUND,
|
||||||
|
ATTR_LOCATION_ATTRIBUTES,
|
||||||
ATTR_LOCATIONS,
|
ATTR_LOCATIONS,
|
||||||
ATTR_SIZE_BYTES,
|
|
||||||
CONTENT_TYPE_TAR,
|
CONTENT_TYPE_TAR,
|
||||||
)
|
)
|
||||||
from .utils import api_process, api_validate
|
from .utils import api_process, api_validate
|
||||||
@@ -63,6 +68,8 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
ALL_ADDONS_FLAG = "ALL"
|
ALL_ADDONS_FLAG = "ALL"
|
||||||
|
|
||||||
|
LOCATION_LOCAL = ".local"
|
||||||
|
|
||||||
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
|
RE_SLUGIFY_NAME = re.compile(r"[^A-Za-z0-9]+")
|
||||||
RE_BACKUP_FILENAME = re.compile(r"^[^\\\/]+\.tar$")
|
RE_BACKUP_FILENAME = re.compile(r"^[^\\\/]+\.tar$")
|
||||||
|
|
||||||
@@ -78,12 +85,23 @@ def _ensure_list(item: Any) -> list:
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_local_location(item: str | None) -> str | None:
|
||||||
|
"""Convert local location value."""
|
||||||
|
if item in {LOCATION_LOCAL, ""}:
|
||||||
|
return None
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
|
SCHEMA_FOLDERS = vol.All([vol.In(_ALL_FOLDERS)], vol.Unique())
|
||||||
|
SCHEMA_LOCATION = vol.All(vol.Maybe(str), _convert_local_location)
|
||||||
|
SCHEMA_LOCATION_LIST = vol.All(_ensure_list, [SCHEMA_LOCATION], vol.Unique())
|
||||||
|
|
||||||
SCHEMA_RESTORE_FULL = vol.Schema(
|
SCHEMA_RESTORE_FULL = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_BACKGROUND, default=False): vol.Boolean(),
|
vol.Optional(ATTR_BACKGROUND, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_LOCATION): vol.Maybe(str),
|
vol.Optional(ATTR_LOCATION): SCHEMA_LOCATION,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,18 +109,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
|||||||
{
|
{
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||||
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
vol.Optional(ATTR_ADDONS): vol.All([str], vol.Unique()),
|
||||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(_ALL_FOLDERS)], vol.Unique()),
|
vol.Optional(ATTR_FOLDERS): SCHEMA_FOLDERS,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_BACKUP_FULL = vol.Schema(
|
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Optional(ATTR_NAME): str,
|
vol.Optional(ATTR_NAME): str,
|
||||||
|
vol.Optional(ATTR_FILENAME): vol.Match(RE_BACKUP_FILENAME),
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(str),
|
||||||
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
vol.Optional(ATTR_COMPRESSED): vol.Maybe(vol.Boolean()),
|
||||||
vol.Optional(ATTR_LOCATION): vol.All(
|
vol.Optional(ATTR_LOCATION): SCHEMA_LOCATION_LIST,
|
||||||
_ensure_list, [vol.Maybe(str)], vol.Unique()
|
|
||||||
),
|
|
||||||
vol.Optional(ATTR_HOMEASSISTANT_EXCLUDE_DATABASE): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT_EXCLUDE_DATABASE): vol.Boolean(),
|
||||||
vol.Optional(ATTR_BACKGROUND, default=False): vol.Boolean(),
|
vol.Optional(ATTR_BACKGROUND, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_EXTRA): dict,
|
vol.Optional(ATTR_EXTRA): dict,
|
||||||
@@ -114,30 +131,14 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
|||||||
vol.Optional(ATTR_ADDONS): vol.Or(
|
vol.Optional(ATTR_ADDONS): vol.Or(
|
||||||
ALL_ADDONS_FLAG, vol.All([str], vol.Unique())
|
ALL_ADDONS_FLAG, vol.All([str], vol.Unique())
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_FOLDERS): vol.All([vol.In(_ALL_FOLDERS)], vol.Unique()),
|
vol.Optional(ATTR_FOLDERS): SCHEMA_FOLDERS,
|
||||||
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
vol.Optional(ATTR_HOMEASSISTANT): vol.Boolean(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
SCHEMA_OPTIONS = vol.Schema(
|
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_DAYS_UNTIL_STALE): days_until_stale})
|
||||||
{
|
SCHEMA_FREEZE = vol.Schema({vol.Optional(ATTR_TIMEOUT): vol.All(int, vol.Range(min=1))})
|
||||||
vol.Optional(ATTR_DAYS_UNTIL_STALE): days_until_stale,
|
SCHEMA_REMOVE = vol.Schema({vol.Optional(ATTR_LOCATION): SCHEMA_LOCATION_LIST})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SCHEMA_FREEZE = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(ATTR_TIMEOUT): vol.All(int, vol.Range(min=1)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SCHEMA_REMOVE = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(ATTR_LOCATION): vol.All(
|
|
||||||
_ensure_list, [vol.Maybe(str)], vol.Unique()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class APIBackups(CoreSysAttributes):
|
class APIBackups(CoreSysAttributes):
|
||||||
@@ -150,6 +151,16 @@ class APIBackups(CoreSysAttributes):
|
|||||||
raise APINotFound("Backup does not exist")
|
raise APINotFound("Backup does not exist")
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
|
def _make_location_attributes(self, backup: Backup) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Make location attributes dictionary."""
|
||||||
|
return {
|
||||||
|
loc if loc else LOCATION_LOCAL: {
|
||||||
|
ATTR_PROTECTED: backup.all_locations[loc][ATTR_PROTECTED],
|
||||||
|
ATTR_SIZE_BYTES: backup.all_locations[loc][ATTR_SIZE_BYTES],
|
||||||
|
}
|
||||||
|
for loc in backup.locations
|
||||||
|
}
|
||||||
|
|
||||||
def _list_backups(self):
|
def _list_backups(self):
|
||||||
"""Return list of backups."""
|
"""Return list of backups."""
|
||||||
return [
|
return [
|
||||||
@@ -163,6 +174,7 @@ class APIBackups(CoreSysAttributes):
|
|||||||
ATTR_LOCATION: backup.location,
|
ATTR_LOCATION: backup.location,
|
||||||
ATTR_LOCATIONS: backup.locations,
|
ATTR_LOCATIONS: backup.locations,
|
||||||
ATTR_PROTECTED: backup.protected,
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_LOCATION_ATTRIBUTES: self._make_location_attributes(backup),
|
||||||
ATTR_COMPRESSED: backup.compressed,
|
ATTR_COMPRESSED: backup.compressed,
|
||||||
ATTR_CONTENT: {
|
ATTR_CONTENT: {
|
||||||
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
ATTR_HOMEASSISTANT: backup.homeassistant_version is not None,
|
||||||
@@ -201,7 +213,7 @@ class APIBackups(CoreSysAttributes):
|
|||||||
if ATTR_DAYS_UNTIL_STALE in body:
|
if ATTR_DAYS_UNTIL_STALE in body:
|
||||||
self.sys_backups.days_until_stale = body[ATTR_DAYS_UNTIL_STALE]
|
self.sys_backups.days_until_stale = body[ATTR_DAYS_UNTIL_STALE]
|
||||||
|
|
||||||
self.sys_backups.save_data()
|
await self.sys_backups.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload(self, _):
|
async def reload(self, _):
|
||||||
@@ -234,6 +246,7 @@ class APIBackups(CoreSysAttributes):
|
|||||||
ATTR_SIZE_BYTES: backup.size_bytes,
|
ATTR_SIZE_BYTES: backup.size_bytes,
|
||||||
ATTR_COMPRESSED: backup.compressed,
|
ATTR_COMPRESSED: backup.compressed,
|
||||||
ATTR_PROTECTED: backup.protected,
|
ATTR_PROTECTED: backup.protected,
|
||||||
|
ATTR_LOCATION_ATTRIBUTES: self._make_location_attributes(backup),
|
||||||
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
|
ATTR_SUPERVISOR_VERSION: backup.supervisor_version,
|
||||||
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
ATTR_HOMEASSISTANT: backup.homeassistant_version,
|
||||||
ATTR_LOCATION: backup.location,
|
ATTR_LOCATION: backup.location,
|
||||||
@@ -296,13 +309,18 @@ class APIBackups(CoreSysAttributes):
|
|||||||
BusEvent.SUPERVISOR_STATE_CHANGE, release_on_freeze
|
BusEvent.SUPERVISOR_STATE_CHANGE, release_on_freeze
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await asyncio.wait(
|
event_task = self.sys_create_task(event.wait())
|
||||||
|
_, pending = await asyncio.wait(
|
||||||
(
|
(
|
||||||
backup_task,
|
backup_task,
|
||||||
self.sys_create_task(event.wait()),
|
event_task,
|
||||||
),
|
),
|
||||||
return_when=asyncio.FIRST_COMPLETED,
|
return_when=asyncio.FIRST_COMPLETED,
|
||||||
)
|
)
|
||||||
|
# It seems backup returned early (error or something), make sure to cancel
|
||||||
|
# the event task to avoid "Task was destroyed but it is pending!" errors.
|
||||||
|
if event_task in pending:
|
||||||
|
event_task.cancel()
|
||||||
return (backup_task, job.uuid)
|
return (backup_task, job.uuid)
|
||||||
finally:
|
finally:
|
||||||
self.sys_bus.remove_listener(listener)
|
self.sys_bus.remove_listener(listener)
|
||||||
@@ -440,23 +458,35 @@ class APIBackups(CoreSysAttributes):
|
|||||||
else:
|
else:
|
||||||
self._validate_cloud_backup_location(request, backup.location)
|
self._validate_cloud_backup_location(request, backup.location)
|
||||||
|
|
||||||
return self.sys_backups.remove(backup, locations=locations)
|
await self.sys_backups.remove(backup, locations=locations)
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def download(self, request: web.Request):
|
async def download(self, request: web.Request):
|
||||||
"""Download a backup file."""
|
"""Download a backup file."""
|
||||||
backup = self._extract_slug(request)
|
backup = self._extract_slug(request)
|
||||||
# Query will give us '' for /backups, convert value to None
|
# Query will give us '' for /backups, convert value to None
|
||||||
location = request.query.get(ATTR_LOCATION, backup.location) or None
|
location = _convert_local_location(
|
||||||
|
request.query.get(ATTR_LOCATION, backup.location)
|
||||||
|
)
|
||||||
self._validate_cloud_backup_location(request, location)
|
self._validate_cloud_backup_location(request, location)
|
||||||
if location not in backup.all_locations:
|
if location not in backup.all_locations:
|
||||||
raise APIError(f"Backup {backup.slug} is not in location {location}")
|
raise APIError(f"Backup {backup.slug} is not in location {location}")
|
||||||
|
|
||||||
_LOGGER.info("Downloading backup %s", backup.slug)
|
_LOGGER.info("Downloading backup %s", backup.slug)
|
||||||
response = web.FileResponse(backup.all_locations[location])
|
filename = backup.all_locations[location][ATTR_PATH]
|
||||||
|
# If the file is missing, return 404 and trigger reload of location
|
||||||
|
if not filename.is_file():
|
||||||
|
self.sys_create_task(self.sys_backups.reload(location))
|
||||||
|
return web.Response(status=404)
|
||||||
|
|
||||||
|
response = web.FileResponse(filename)
|
||||||
response.content_type = CONTENT_TYPE_TAR
|
response.content_type = CONTENT_TYPE_TAR
|
||||||
|
|
||||||
|
download_filename = filename.name
|
||||||
|
if download_filename == f"{backup.slug}.tar":
|
||||||
|
download_filename = f"{RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
||||||
response.headers[CONTENT_DISPOSITION] = (
|
response.headers[CONTENT_DISPOSITION] = (
|
||||||
f"attachment; filename={RE_SLUGIFY_NAME.sub('_', backup.name)}.tar"
|
f"attachment; filename={download_filename}"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -471,7 +501,9 @@ class APIBackups(CoreSysAttributes):
|
|||||||
self._validate_cloud_backup_location(request, location_names)
|
self._validate_cloud_backup_location(request, location_names)
|
||||||
# Convert empty string to None if necessary
|
# Convert empty string to None if necessary
|
||||||
locations = [
|
locations = [
|
||||||
self._location_to_mount(location) if location else None
|
self._location_to_mount(location)
|
||||||
|
if _convert_local_location(location)
|
||||||
|
else None
|
||||||
for location in location_names
|
for location in location_names
|
||||||
]
|
]
|
||||||
location = locations.pop(0)
|
location = locations.pop(0)
|
||||||
@@ -479,35 +511,60 @@ class APIBackups(CoreSysAttributes):
|
|||||||
if location and location != LOCATION_CLOUD_BACKUP:
|
if location and location != LOCATION_CLOUD_BACKUP:
|
||||||
tmp_path = location.local_where
|
tmp_path = location.local_where
|
||||||
|
|
||||||
with TemporaryDirectory(dir=tmp_path.as_posix()) as temp_dir:
|
filename: str | None = None
|
||||||
tar_file = Path(temp_dir, "backup.tar")
|
if ATTR_FILENAME in request.query:
|
||||||
|
filename = request.query.get(ATTR_FILENAME)
|
||||||
|
try:
|
||||||
|
vol.Match(RE_BACKUP_FILENAME)(filename)
|
||||||
|
except vol.Invalid as ex:
|
||||||
|
raise APIError(humanize_error(filename, ex)) from None
|
||||||
|
|
||||||
|
temp_dir: TemporaryDirectory | None = None
|
||||||
|
backup_file_stream: IOBase | None = None
|
||||||
|
|
||||||
|
def open_backup_file() -> Path:
|
||||||
|
nonlocal temp_dir, backup_file_stream
|
||||||
|
temp_dir = TemporaryDirectory(dir=tmp_path.as_posix())
|
||||||
|
tar_file = Path(temp_dir.name, "backup.tar")
|
||||||
|
backup_file_stream = tar_file.open("wb")
|
||||||
|
return tar_file
|
||||||
|
|
||||||
|
def close_backup_file() -> None:
|
||||||
|
if backup_file_stream:
|
||||||
|
backup_file_stream.close()
|
||||||
|
if temp_dir:
|
||||||
|
temp_dir.cleanup()
|
||||||
|
|
||||||
|
try:
|
||||||
reader = await request.multipart()
|
reader = await request.multipart()
|
||||||
contents = await reader.next()
|
contents = await reader.next()
|
||||||
try:
|
tar_file = await self.sys_run_in_executor(open_backup_file)
|
||||||
with tar_file.open("wb") as backup:
|
while chunk := await contents.read_chunk(size=2**16):
|
||||||
while True:
|
await self.sys_run_in_executor(backup_file_stream.write, chunk)
|
||||||
chunk = await contents.read_chunk()
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
backup.write(chunk)
|
|
||||||
|
|
||||||
except OSError as err:
|
|
||||||
if err.errno == errno.EBADMSG and location in {
|
|
||||||
LOCATION_CLOUD_BACKUP,
|
|
||||||
None,
|
|
||||||
}:
|
|
||||||
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
|
||||||
_LOGGER.error("Can't write new backup file: %s", err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
backup = await asyncio.shield(
|
backup = await asyncio.shield(
|
||||||
self.sys_backups.import_backup(
|
self.sys_backups.import_backup(
|
||||||
tar_file, location=location, additional_locations=locations
|
tar_file,
|
||||||
|
filename,
|
||||||
|
location=location,
|
||||||
|
additional_locations=locations,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno == errno.EBADMSG and location in {
|
||||||
|
LOCATION_CLOUD_BACKUP,
|
||||||
|
None,
|
||||||
|
}:
|
||||||
|
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE
|
||||||
|
_LOGGER.error("Can't write new backup file: %s", err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if temp_dir or backup:
|
||||||
|
await self.sys_run_in_executor(close_backup_file)
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
return {ATTR_SLUG: backup.slug}
|
return {ATTR_SLUG: backup.slug}
|
||||||
|
@@ -47,6 +47,7 @@ ATTR_JOBS = "jobs"
|
|||||||
ATTR_LLMNR = "llmnr"
|
ATTR_LLMNR = "llmnr"
|
||||||
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
ATTR_LLMNR_HOSTNAME = "llmnr_hostname"
|
||||||
ATTR_LOCAL_ONLY = "local_only"
|
ATTR_LOCAL_ONLY = "local_only"
|
||||||
|
ATTR_LOCATION_ATTRIBUTES = "location_attributes"
|
||||||
ATTR_LOCATIONS = "locations"
|
ATTR_LOCATIONS = "locations"
|
||||||
ATTR_MDNS = "mdns"
|
ATTR_MDNS = "mdns"
|
||||||
ATTR_MODEL = "model"
|
ATTR_MODEL = "model"
|
||||||
@@ -59,7 +60,6 @@ ATTR_REVISION = "revision"
|
|||||||
ATTR_SAFE_MODE = "safe_mode"
|
ATTR_SAFE_MODE = "safe_mode"
|
||||||
ATTR_SEAT = "seat"
|
ATTR_SEAT = "seat"
|
||||||
ATTR_SIGNED = "signed"
|
ATTR_SIGNED = "signed"
|
||||||
ATTR_SIZE_BYTES = "size_bytes"
|
|
||||||
ATTR_STARTUP_TIME = "startup_time"
|
ATTR_STARTUP_TIME = "startup_time"
|
||||||
ATTR_STATUS = "status"
|
ATTR_STATUS = "status"
|
||||||
ATTR_SUBSYSTEM = "subsystem"
|
ATTR_SUBSYSTEM = "subsystem"
|
||||||
|
@@ -83,7 +83,7 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Process discovery message
|
# Process discovery message
|
||||||
message = self.sys_discovery.send(addon, **body)
|
message = await self.sys_discovery.send(addon, **body)
|
||||||
|
|
||||||
return {ATTR_UUID: message.uuid}
|
return {ATTR_UUID: message.uuid}
|
||||||
|
|
||||||
@@ -110,5 +110,5 @@ class APIDiscovery(CoreSysAttributes):
|
|||||||
if message.addon != addon.slug:
|
if message.addon != addon.slug:
|
||||||
raise APIForbidden("Can't remove discovery message")
|
raise APIForbidden("Can't remove discovery message")
|
||||||
|
|
||||||
self.sys_discovery.remove(message)
|
await self.sys_discovery.remove(message)
|
||||||
return True
|
return True
|
||||||
|
@@ -78,7 +78,7 @@ class APICoreDNS(CoreSysAttributes):
|
|||||||
if restart_required:
|
if restart_required:
|
||||||
self.sys_create_task(self.sys_plugins.dns.restart())
|
self.sys_create_task(self.sys_plugins.dns.restart())
|
||||||
|
|
||||||
self.sys_plugins.dns.save_data()
|
await self.sys_plugins.dns.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> dict[str, Any]:
|
async def stats(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
@@ -53,7 +53,7 @@ class APIDocker(CoreSysAttributes):
|
|||||||
for hostname, registry in body.items():
|
for hostname, registry in body.items():
|
||||||
self.sys_docker.config.registries[hostname] = registry
|
self.sys_docker.config.registries[hostname] = registry
|
||||||
|
|
||||||
self.sys_docker.config.save_data()
|
await self.sys_docker.config.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def remove_registry(self, request: web.Request):
|
async def remove_registry(self, request: web.Request):
|
||||||
@@ -63,7 +63,7 @@ class APIDocker(CoreSysAttributes):
|
|||||||
raise APINotFound(f"Hostname {hostname} does not exist in registries")
|
raise APINotFound(f"Hostname {hostname} does not exist in registries")
|
||||||
|
|
||||||
del self.sys_docker.config.registries[hostname]
|
del self.sys_docker.config.registries[hostname]
|
||||||
self.sys_docker.config.save_data()
|
await self.sys_docker.config.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def info(self, request: web.Request):
|
async def info(self, request: web.Request):
|
||||||
|
@@ -149,7 +149,7 @@ class APIHomeAssistant(CoreSysAttributes):
|
|||||||
ATTR_BACKUPS_EXCLUDE_DATABASE
|
ATTR_BACKUPS_EXCLUDE_DATABASE
|
||||||
]
|
]
|
||||||
|
|
||||||
self.sys_homeassistant.save_data()
|
await self.sys_homeassistant.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def stats(self, request: web.Request) -> dict[Any, str]:
|
async def stats(self, request: web.Request) -> dict[Any, str]:
|
||||||
|
@@ -98,10 +98,10 @@ class APIHost(CoreSysAttributes):
|
|||||||
ATTR_VIRTUALIZATION: self.sys_host.info.virtualization,
|
ATTR_VIRTUALIZATION: self.sys_host.info.virtualization,
|
||||||
ATTR_CPE: self.sys_host.info.cpe,
|
ATTR_CPE: self.sys_host.info.cpe,
|
||||||
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
ATTR_DEPLOYMENT: self.sys_host.info.deployment,
|
||||||
ATTR_DISK_FREE: self.sys_host.info.free_space,
|
ATTR_DISK_FREE: await self.sys_host.info.free_space(),
|
||||||
ATTR_DISK_TOTAL: self.sys_host.info.total_space,
|
ATTR_DISK_TOTAL: await self.sys_host.info.total_space(),
|
||||||
ATTR_DISK_USED: self.sys_host.info.used_space,
|
ATTR_DISK_USED: await self.sys_host.info.used_space(),
|
||||||
ATTR_DISK_LIFE_TIME: self.sys_host.info.disk_life_time,
|
ATTR_DISK_LIFE_TIME: await self.sys_host.info.disk_life_time(),
|
||||||
ATTR_FEATURES: self.sys_host.features,
|
ATTR_FEATURES: self.sys_host.features,
|
||||||
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
ATTR_HOSTNAME: self.sys_host.info.hostname,
|
||||||
ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname,
|
ATTR_LLMNR_HOSTNAME: self.sys_host.info.llmnr_hostname,
|
||||||
@@ -239,12 +239,12 @@ class APIHost(CoreSysAttributes):
|
|||||||
# return 2 lines at minimum.
|
# return 2 lines at minimum.
|
||||||
lines = max(2, lines)
|
lines = max(2, lines)
|
||||||
# entries=cursor[[:num_skip]:num_entries]
|
# entries=cursor[[:num_skip]:num_entries]
|
||||||
range_header = f"entries=:-{lines-1}:{'' if follow else lines}"
|
range_header = f"entries=:-{lines - 1}:{'' if follow else lines}"
|
||||||
elif RANGE in request.headers:
|
elif RANGE in request.headers:
|
||||||
range_header = request.headers.get(RANGE)
|
range_header = request.headers.get(RANGE)
|
||||||
else:
|
else:
|
||||||
range_header = (
|
range_header = (
|
||||||
f"entries=:-{DEFAULT_LINES-1}:{'' if follow else DEFAULT_LINES}"
|
f"entries=:-{DEFAULT_LINES - 1}:{'' if follow else DEFAULT_LINES}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self.sys_host.logs.journald_logs(
|
async with self.sys_host.logs.journald_logs(
|
||||||
@@ -258,6 +258,7 @@ class APIHost(CoreSysAttributes):
|
|||||||
if not headers_returned:
|
if not headers_returned:
|
||||||
if cursor:
|
if cursor:
|
||||||
response.headers["X-First-Cursor"] = cursor
|
response.headers["X-First-Cursor"] = cursor
|
||||||
|
response.headers["X-Accel-Buffering"] = "no"
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
headers_returned = True
|
headers_returned = True
|
||||||
# When client closes the connection while reading busy logs, we
|
# When client closes the connection while reading busy logs, we
|
||||||
|
@@ -277,6 +277,7 @@ class APIIngress(CoreSysAttributes):
|
|||||||
response.content_type = content_type
|
response.content_type = content_type
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
response.headers["X-Accel-Buffering"] = "no"
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
async for data in result.content.iter_chunked(4096):
|
async for data in result.content.iter_chunked(4096):
|
||||||
await response.write(data)
|
await response.write(data)
|
||||||
|
@@ -31,9 +31,16 @@ class APIJobs(CoreSysAttributes):
|
|||||||
raise APINotFound("Job does not exist") from None
|
raise APINotFound("Job does not exist") from None
|
||||||
|
|
||||||
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
|
def _list_jobs(self, start: SupervisorJob | None = None) -> list[dict[str, Any]]:
|
||||||
"""Return current job tree."""
|
"""Return current job tree.
|
||||||
|
|
||||||
|
Jobs are added to cache as they are created so by default they are in oldest to newest.
|
||||||
|
This is correct ordering for child jobs as it makes logical sense to present those in
|
||||||
|
the order they occurred within the parent. For the list as a whole, sort from newest
|
||||||
|
to oldest as its likely any client is most interested in the newer ones.
|
||||||
|
"""
|
||||||
|
# Initially sort oldest to newest so all child lists end up in correct order
|
||||||
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
|
jobs_by_parent: dict[str | None, list[SupervisorJob]] = {}
|
||||||
for job in self.sys_jobs.jobs:
|
for job in sorted(self.sys_jobs.jobs):
|
||||||
if job.internal:
|
if job.internal:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -42,11 +49,15 @@ class APIJobs(CoreSysAttributes):
|
|||||||
else:
|
else:
|
||||||
jobs_by_parent[job.parent_id].append(job)
|
jobs_by_parent[job.parent_id].append(job)
|
||||||
|
|
||||||
|
# After parent-child organization, sort the root jobs only from newest to oldest
|
||||||
job_list: list[dict[str, Any]] = []
|
job_list: list[dict[str, Any]] = []
|
||||||
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
|
queue: list[tuple[list[dict[str, Any]], SupervisorJob]] = (
|
||||||
[(job_list, start)]
|
[(job_list, start)]
|
||||||
if start
|
if start
|
||||||
else [(job_list, job) for job in jobs_by_parent.get(None, [])]
|
else [
|
||||||
|
(job_list, job)
|
||||||
|
for job in sorted(jobs_by_parent.get(None, []), reverse=True)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
while queue:
|
while queue:
|
||||||
@@ -81,14 +92,14 @@ class APIJobs(CoreSysAttributes):
|
|||||||
if ATTR_IGNORE_CONDITIONS in body:
|
if ATTR_IGNORE_CONDITIONS in body:
|
||||||
self.sys_jobs.ignore_conditions = body[ATTR_IGNORE_CONDITIONS]
|
self.sys_jobs.ignore_conditions = body[ATTR_IGNORE_CONDITIONS]
|
||||||
|
|
||||||
self.sys_jobs.save_data()
|
await self.sys_jobs.save_data()
|
||||||
|
|
||||||
await self.sys_resolution.evaluate.evaluate_system()
|
await self.sys_resolution.evaluate.evaluate_system()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reset(self, request: web.Request) -> None:
|
async def reset(self, request: web.Request) -> None:
|
||||||
"""Reset options for JobManager."""
|
"""Reset options for JobManager."""
|
||||||
self.sys_jobs.reset_data()
|
await self.sys_jobs.reset_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def job_info(self, request: web.Request) -> dict[str, Any]:
|
async def job_info(self, request: web.Request) -> dict[str, Any]:
|
||||||
|
@@ -66,7 +66,7 @@ class APIMounts(CoreSysAttributes):
|
|||||||
else:
|
else:
|
||||||
self.sys_mounts.default_backup_mount = mount
|
self.sys_mounts.default_backup_mount = mount
|
||||||
|
|
||||||
self.sys_mounts.save_data()
|
await self.sys_mounts.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def create_mount(self, request: web.Request) -> None:
|
async def create_mount(self, request: web.Request) -> None:
|
||||||
@@ -87,7 +87,7 @@ class APIMounts(CoreSysAttributes):
|
|||||||
if not self.sys_mounts.default_backup_mount:
|
if not self.sys_mounts.default_backup_mount:
|
||||||
self.sys_mounts.default_backup_mount = mount
|
self.sys_mounts.default_backup_mount = mount
|
||||||
|
|
||||||
self.sys_mounts.save_data()
|
await self.sys_mounts.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def update_mount(self, request: web.Request) -> None:
|
async def update_mount(self, request: web.Request) -> None:
|
||||||
@@ -110,7 +110,7 @@ class APIMounts(CoreSysAttributes):
|
|||||||
elif self.sys_mounts.default_backup_mount == mount:
|
elif self.sys_mounts.default_backup_mount == mount:
|
||||||
self.sys_mounts.default_backup_mount = None
|
self.sys_mounts.default_backup_mount = None
|
||||||
|
|
||||||
self.sys_mounts.save_data()
|
await self.sys_mounts.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def delete_mount(self, request: web.Request) -> None:
|
async def delete_mount(self, request: web.Request) -> None:
|
||||||
@@ -122,7 +122,7 @@ class APIMounts(CoreSysAttributes):
|
|||||||
if mount.usage == MountUsage.BACKUP:
|
if mount.usage == MountUsage.BACKUP:
|
||||||
self.sys_create_task(self.sys_backups.reload())
|
self.sys_create_task(self.sys_backups.reload())
|
||||||
|
|
||||||
self.sys_mounts.save_data()
|
await self.sys_mounts.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def reload_mount(self, request: web.Request) -> None:
|
async def reload_mount(self, request: web.Request) -> None:
|
||||||
|
@@ -169,7 +169,7 @@ class APIOS(CoreSysAttributes):
|
|||||||
body[ATTR_SYSTEM_HEALTH_LED]
|
body[ATTR_SYSTEM_HEALTH_LED]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.sys_dbus.agent.board.green.save_data()
|
await self.sys_dbus.agent.board.green.save_data()
|
||||||
|
|
||||||
@api_process
|
@api_process
|
||||||
async def boards_yellow_info(self, request: web.Request) -> dict[str, Any]:
|
async def boards_yellow_info(self, request: web.Request) -> dict[str, Any]:
|
||||||
@@ -196,7 +196,7 @@ class APIOS(CoreSysAttributes):
|
|||||||
if ATTR_POWER_LED in body:
|
if ATTR_POWER_LED in body:
|
||||||
await self.sys_dbus.agent.board.yellow.set_power_led(body[ATTR_POWER_LED])
|
await self.sys_dbus.agent.board.yellow.set_power_led(body[ATTR_POWER_LED])
|
||||||
|
|
||||||
self.sys_dbus.agent.board.yellow.save_data()
|
await self.sys_dbus.agent.board.yellow.save_data()
|
||||||
self.sys_resolution.create_issue(
|
self.sys_resolution.create_issue(
|
||||||
IssueType.REBOOT_REQUIRED,
|
IssueType.REBOOT_REQUIRED,
|
||||||
ContextType.SYSTEM,
|
ContextType.SYSTEM,
|
||||||
|
@@ -1 +1 @@
|
|||||||
!function(){function d(d){var e=document.createElement("script");e.src=d,document.body.appendChild(e)}if(/Edge?\/(12[1-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Firefox\/(1{2}[5-9]|1[2-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Chrom(ium|e)\/(109|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|(Maci|X1{2}).+ Version\/(17\.([3-9]|\d{2,})|(1[89]|[2-9]\d|\d{3,})\.\d+)([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/(10[6-9]|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(15[._]([6-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})[._]\d+)([._]\d+|)|Android:?[ /-](12[1-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})(\.\d+|)(\.\d+|)|Mobile Safari.+OPR\/([89]\d|\d{3,})\.\d+\.\d+|Android.+Firefox\/(12[1-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Android.+Chrom(ium|e)\/(12[1-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|SamsungBrowser\/(2[4-9]|[3-9]\d|\d{3,})\.\d+|Home As{2}istant\/[\d.]+ \(.+; macOS (1[2-9]|[2-9]\d|\d{3,})\.\d+(\.\d+)?\)/.test(navigator.userAgent))try{new Function("import('/api/hassio/app/frontend_latest/entrypoint.553a7707b827808b.js')")()}catch(e){d("/api/hassio/app/frontend_es5/entrypoint.ff48ee24e0742761.js")}else d("/api/hassio/app/frontend_es5/entrypoint.ff48ee24e0742761.js")}()
|
!function(){function d(d){var e=document.createElement("script");e.src=d,document.body.appendChild(e)}if(/Edge?\/(12[2-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Firefox\/(12[4-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Chrom(ium|e)\/(109|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|(Maci|X1{2}).+ Version\/(17\.([4-9]|\d{2,})|(1[89]|[2-9]\d|\d{3,})\.\d+)([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/(10[89]|1[1-9]\d|[2-9]\d{2}|\d{4,})\.\d+\.\d+|(CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS|CPU iPad OS)[ +]+(15[._]([6-9]|\d{2,})|(1[6-9]|[2-9]\d|\d{3,})[._]\d+)([._]\d+|)|Android:?[ /-](12[3-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})(\.\d+|)(\.\d+|)|Mobile Safari.+OPR\/([89]\d|\d{3,})\.\d+\.\d+|Android.+Firefox\/(12[4-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Android.+Chrom(ium|e)\/(12[3-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|SamsungBrowser\/(2[4-9]|[3-9]\d|\d{3,})\.\d+|Home As{2}istant\/[\d.]+ \(.+; macOS (1[2-9]|[2-9]\d|\d{3,})\.\d+(\.\d+)?\)/.test(navigator.userAgent))try{new Function("import('/api/hassio/app/frontend_latest/entrypoint.9ac99222ee42fbb3.js')")()}catch(e){d("/api/hassio/app/frontend_es5/entrypoint.85ccafe1fda9d9a5.js")}else d("/api/hassio/app/frontend_es5/entrypoint.85ccafe1fda9d9a5.js")}()
|
BIN
supervisor/api/panel/entrypoint.js.br
Normal file
BIN
supervisor/api/panel/entrypoint.js.br
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1081.e647cbe586ff9dd0.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1081.e647cbe586ff9dd0.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1081.e647cbe586ff9dd0.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1081.e647cbe586ff9dd0.js.gz
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"1081.e647cbe586ff9dd0.js","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20250221.0/src/components/ha-button-toggle-group.ts","https://raw.githubusercontent.com/home-assistant/frontend/20250221.0/src/components/ha-selector/ha-selector-button-toggle.ts"],"names":["_decorate","customElement","_initialize","_LitElement","F","constructor","args","d","kind","decorators","property","attribute","key","value","type","Boolean","queryAll","html","_t","_","this","buttons","map","button","iconPath","_t2","label","active","_handleClick","_t3","styleMap","width","fullWidth","length","dense","_this$_buttons","_buttons","forEach","async","updateComplete","shadowRoot","querySelector","style","margin","ev","currentTarget","fireEvent","static","css","_t4","LitElement","HaButtonToggleSelector","_this$selector$button","_this$selector$button2","_this$selector$button3","options","selector","button_toggle","option","translationKey","translation_key","localizeValue","localizedLabel","sort","a","b","caseInsensitiveStringCompare","hass","locale","language","toggleButtons","item","_valueChanged","_ev$detail","_this$value","stopPropagation","detail","target","disabled","undefined"],"mappings":"sXAWgCA,EAAAA,EAAAA,GAAA,EAD/BC,EAAAA,EAAAA,IAAc,4BAAyB,SAAAC,EAAAC,GAkIvC,OAAAC,EAlID,cACgCD,EAAoBE,WAAAA,IAAAC,GAAA,SAAAA,GAAAJ,EAAA,QAApBK,EAAA,EAAAC,KAAA,QAAAC,WAAA,EAC7BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,UAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEC,UAAW,aAAcG,KAAMC,WAAUH,IAAA,YAAAC,KAAAA,GAAA,OAClC,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAEvBC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,QAAAC,KAAAA,GAAA,OAAgB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAEhDO,EAAAA,EAAAA,IAAS,eAAaJ,IAAA,WAAAC,WAAA,IAAAL,KAAA,SAAAI,IAAA,SAAAC,MAEvB,WACE,OAAOI,EAAAA,EAAAA,IAAIC,IAAAA,EAAAC,CAAA,uBAELC,KAAKC,QAAQC,KAAKC,GAClBA,EAAOC,UACHP,EAAAA,EAAAA,IAAIQ,IAAAA,EAAAN,CAAA,2GACOI,EAAOG,MACRH,EAAOC,SACND,EAAOV,MACNO,KAAKO,SAAWJ,EAAOV,MACxBO,KAAKQ,eAEhBX,EAAAA,EAAAA,IAAIY,IAAAA,EAAAV,CAAA,iHACMW,EAAAA,EAAAA,GAAS,CACfC,MAAOX,KAAKY,UACL,IAAMZ,KAAKC,QAAQY,OAAtB,IACA,YAGGb,KAAKc,MACLX,EAAOV,MACNO,KAAKO,SAAWJ,EAAOV,MACxBO,KAAKQ,aACXL,EAAOG,SAKxB,GAAC,CAAAlB,KAAA,SAAAI,IAAA,UAAAC,MAED,WAAoB,IAAAsB,EAEL,QAAbA,EAAAf,KAAKgB,gBAAQ,IAAAD,GAAbA,EAAeE,SAAQC,gBACff,EAAOgB,eAEXhB,EAAOiB,WAAYC,cAAc,UACjCC,MAAMC,OAAS,GAAG,GAExB,GAAC,CAAAnC,KAAA,SAAAI,IAAA,eAAAC,MAED,SAAqB+B,GACnBxB,KAAKO,OAASiB,EAAGC,cAAchC,OAC/BiC,EAAAA,EAAAA,GAAU1B,KAAM,gBAAiB,CAAEP,MAAOO,KAAKO,QACjD,GAAC,CAAAnB,KAAA,QAAAuC,QAAA,EAAAnC,IAAA,SAAAC,KAAAA,GAAA,OAEemC,EAAAA,EAAAA,IAAGC,IAAAA,EAAA9B,CAAA,u0CAzDoB+B,EAAAA,I,MCD5BC,GAAsBnD,EAAAA,EAAAA,GAAA,EADlCC,EAAAA,EAAAA,IAAc,+BAA4B,SAAAC,EAAAC,GA4F1C,OAAAC,EA5FD,cACmCD,EAAoBE,WAAAA,IAAAC,GAAA,SAAAA,GAAAJ,EAAA,QAApBK,EAAA,EAAAC,KAAA,QAAAC,WAAA,EAChCC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,OAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,WAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,gBAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAG9BC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,KAAAA,GAAA,OAAmB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAEnDC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,KAAAA,GAAA,OAAmB,CAAI,IAAAL,KAAA,SAAAI,IAAA,SAAAC,MAEnD,WAAmB,IAAAuC,EAAAC,EAAAC,EACjB,MAAMC,GACuB,QAA3BH,EAAAhC,KAAKoC,SAASC,qBAAa,IAAAL,GAAS,QAATA,EAA3BA,EAA6BG,eAAO,IAAAH,OAAA,EAApCA,EAAsC9B,KAAKoC,GACvB,iBAAXA,EACFA,EACA,CAAE7C,MAAO6C,EAAQhC,MAAOgC,OAC1B,GAEDC,EAA4C,QAA9BN,EAAGjC,KAAKoC,SAASC,qBAAa,IAAAJ,OAAA,EAA3BA,EAA6BO,gBAEhDxC,KAAKyC,eAAiBF,GACxBJ,EAAQlB,SAASqB,IACf,MAAMI,EAAiB1C,KAAKyC,cAC1B,GAAGF,aAA0BD,EAAO7C,SAElCiD,IACFJ,EAAOhC,MAAQoC,EACjB,IAI2B,QAA/BR,EAAIlC,KAAKoC,SAASC,qBAAa,IAAAH,GAA3BA,EAA6BS,MAC/BR,EAAQQ,MAAK,CAACC,EAAGC,KACfC,EAAAA,EAAAA,GACEF,EAAEtC,MACFuC,EAAEvC,MACFN,KAAK+C,KAAKC,OAAOC,YAKvB,MAAMC,EAAgCf,EAAQjC,KAAKiD,IAAkB,CACnE7C,MAAO6C,EAAK7C,MACZb,MAAO0D,EAAK1D,UAGd,OAAOI,EAAAA,EAAAA,IAAIC,IAAAA,EAAAC,CAAA,iHACPC,KAAKM,MAEM4C,EACDlD,KAAKP,MACEO,KAAKoD,cAG5B,GAAC,CAAAhE,KAAA,SAAAI,IAAA,gBAAAC,MAED,SAAsB+B,GAAI,IAAA6B,EAAAC,EACxB9B,EAAG+B,kBAEH,MAAM9D,GAAiB,QAAT4D,EAAA7B,EAAGgC,cAAM,IAAAH,OAAA,EAATA,EAAW5D,QAAS+B,EAAGiC,OAAOhE,MACxCO,KAAK0D,eAAsBC,IAAVlE,GAAuBA,KAAqB,QAAhB6D,EAAMtD,KAAKP,aAAK,IAAA6D,EAAAA,EAAI,MAGrE5B,EAAAA,EAAAA,GAAU1B,KAAM,gBAAiB,CAC/BP,MAAOA,GAEX,GAAC,CAAAL,KAAA,QAAAuC,QAAA,EAAAnC,IAAA,SAAAC,KAAAA,GAAA,OAEemC,EAAAA,EAAAA,IAAGvB,IAAAA,EAAAN,CAAA,wLA5EuB+B,EAAAA,G"}
|
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1121.6a80ad1fbfcedf85.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1121.6a80ad1fbfcedf85.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1121.6a80ad1fbfcedf85.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1121.6a80ad1fbfcedf85.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1173.df00e6361fed8e6c.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1173.df00e6361fed8e6c.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1173.df00e6361fed8e6c.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1173.df00e6361fed8e6c.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([["12"],{5739:function(e,a,t){t.a(e,(async function(e,i){try{t.r(a),t.d(a,{HaNavigationSelector:function(){return b}});var n,r=t(63038),d=t(27862),o=t(52565),l=t(92776),u=t(5776),s=t(21475),c=(t(38419),t(57243)),h=t(50778),v=t(36522),k=t(63297),f=e([k]);k=(f.then?(await f)():f)[0];var b=(0,s.Z)([(0,h.Mo)("ha-selector-navigation")],(function(e,a){var t=function(a){function t(){var a;(0,o.Z)(this,t);for(var i=arguments.length,n=new Array(i),r=0;r<i;r++)n[r]=arguments[r];return a=(0,l.Z)(this,t,[].concat(n)),e(a),a}return(0,u.Z)(t,a),(0,d.Z)(t)}(a);return{F:t,d:[{kind:"field",decorators:[(0,h.Cb)({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[(0,h.Cb)({attribute:!1})],key:"selector",value:void 0},{kind:"field",decorators:[(0,h.Cb)()],key:"value",value:void 0},{kind:"field",decorators:[(0,h.Cb)()],key:"label",value:void 0},{kind:"field",decorators:[(0,h.Cb)()],key:"helper",value:void 0},{kind:"field",decorators:[(0,h.Cb)({type:Boolean,reflect:!0})],key:"disabled",value:function(){return!1}},{kind:"field",decorators:[(0,h.Cb)({type:Boolean})],key:"required",value:function(){return!0}},{kind:"method",key:"render",value:function(){return(0,c.dy)(n||(n=(0,r.Z)([' <ha-navigation-picker .hass="','" .label="','" .value="','" .required="','" .disabled="','" .helper="','" @value-changed="','"></ha-navigation-picker> '])),this.hass,this.label,this.value,this.required,this.disabled,this.helper,this._valueChanged)}},{kind:"method",key:"_valueChanged",value:function(e){(0,v.B)(this,"value-changed",{value:e.detail.value})}}]}}),c.oi);i()}catch(y){i(y)}}))}}]);
|
|
||||||
//# sourceMappingURL=12.cd76ff0e6ff4d214.js.map
|
|
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"12.cd76ff0e6ff4d214.js","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20241127.8/src/components/ha-selector/ha-selector-navigation.ts"],"names":["HaNavigationSelector","_decorate","customElement","_initialize","_LitElement","_LitElement2","_this","_classCallCheck","_len","arguments","length","args","Array","_key","_callSuper","concat","_inherits","_createClass","F","d","kind","decorators","property","attribute","key","value","type","Boolean","reflect","html","_templateObject","_taggedTemplateLiteral","this","hass","label","required","disabled","helper","_valueChanged","ev","fireEvent","detail","LitElement"],"mappings":"oYAOA,IACaA,GAAoBC,EAAAA,EAAAA,GAAA,EADhCC,EAAAA,EAAAA,IAAc,4BAAyB,SAAAC,EAAAC,GAAA,IAC3BJ,EAAoB,SAAAK,GAAA,SAAAL,IAAA,IAAAM,GAAAC,EAAAA,EAAAA,GAAA,KAAAP,GAAA,QAAAQ,EAAAC,UAAAC,OAAAC,EAAA,IAAAC,MAAAJ,GAAAK,EAAA,EAAAA,EAAAL,EAAAK,IAAAF,EAAAE,GAAAJ,UAAAI,GAAA,OAAAP,GAAAQ,EAAAA,EAAAA,GAAA,KAAAd,EAAA,GAAAe,OAAAJ,IAAAR,EAAAG,GAAAA,CAAA,QAAAU,EAAAA,EAAAA,GAAAhB,EAAAK,IAAAY,EAAAA,EAAAA,GAAAjB,EAAA,EAAAI,GAAA,OAAAc,EAApBlB,EAAoBmB,EAAA,EAAAC,KAAA,QAAAC,WAAA,EAC9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,OAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,WAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,QAASC,SAAS,KAAOJ,IAAA,WAAAC,MAAA,kBAAmB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAElEC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,MAAA,kBAAmB,CAAI,IAAAL,KAAA,SAAAI,IAAA,SAAAC,MAEnD,WACE,OAAOI,EAAAA,EAAAA,IAAIC,IAAAA,GAAAC,EAAAA,EAAAA,GAAA,+JAECC,KAAKC,KACJD,KAAKE,MACLF,KAAKP,MACFO,KAAKG,SACLH,KAAKI,SACPJ,KAAKK,OACEL,KAAKM,cAG5B,GAAC,CAAAlB,KAAA,SAAAI,IAAA,gBAAAC,MAED,SAAsBc,IACpBC,EAAAA,EAAAA,GAAUR,KAAM,gBAAiB,CAAEP,MAAOc,EAAGE,OAAOhB,OACtD,IAAC,GA/BuCiB,EAAAA,I"}
|
|
2
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js
Normal file
2
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([["12"],{5739:function(e,a,t){t.a(e,(async function(e,i){try{t.r(a),t.d(a,{HaNavigationSelector:()=>c});var d=t(73577),r=(t(71695),t(47021),t(57243)),n=t(50778),l=t(36522),o=t(63297),s=e([o]);o=(s.then?(await s)():s)[0];let u,h=e=>e,c=(0,d.Z)([(0,n.Mo)("ha-selector-navigation")],(function(e,a){return{F:class extends a{constructor(...a){super(...a),e(this)}},d:[{kind:"field",decorators:[(0,n.Cb)({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[(0,n.Cb)({attribute:!1})],key:"selector",value:void 0},{kind:"field",decorators:[(0,n.Cb)()],key:"value",value:void 0},{kind:"field",decorators:[(0,n.Cb)()],key:"label",value:void 0},{kind:"field",decorators:[(0,n.Cb)()],key:"helper",value:void 0},{kind:"field",decorators:[(0,n.Cb)({type:Boolean,reflect:!0})],key:"disabled",value(){return!1}},{kind:"field",decorators:[(0,n.Cb)({type:Boolean})],key:"required",value(){return!0}},{kind:"method",key:"render",value:function(){return(0,r.dy)(u||(u=h` <ha-navigation-picker .hass="${0}" .label="${0}" .value="${0}" .required="${0}" .disabled="${0}" .helper="${0}" @value-changed="${0}"></ha-navigation-picker> `),this.hass,this.label,this.value,this.required,this.disabled,this.helper,this._valueChanged)}},{kind:"method",key:"_valueChanged",value:function(e){(0,l.B)(this,"value-changed",{value:e.detail.value})}}]}}),r.oi);i()}catch(u){i(u)}}))}}]);
|
||||||
|
//# sourceMappingURL=12.ffa1bdc0a98802fa.js.map
|
BIN
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/12.ffa1bdc0a98802fa.js.gz
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"12.ffa1bdc0a98802fa.js","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20250221.0/src/components/ha-selector/ha-selector-navigation.ts"],"names":["HaNavigationSelector","_decorate","customElement","_initialize","_LitElement","F","constructor","args","d","kind","decorators","property","attribute","key","value","type","Boolean","reflect","html","_t","_","this","hass","label","required","disabled","helper","_valueChanged","ev","fireEvent","detail","LitElement"],"mappings":"mVAQaA,GAAoBC,EAAAA,EAAAA,GAAA,EADhCC,EAAAA,EAAAA,IAAc,4BAAyB,SAAAC,EAAAC,GAiCvC,OAAAC,EAjCD,cACiCD,EAAoBE,WAAAA,IAAAC,GAAA,SAAAA,GAAAJ,EAAA,QAApBK,EAAA,EAAAC,KAAA,QAAAC,WAAA,EAC9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,OAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,IAAS,CAAEC,WAAW,KAAQC,IAAA,WAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAE9BC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,QAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,OAAUE,IAAA,SAAAC,WAAA,IAAAL,KAAA,QAAAC,WAAA,EAEVC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,QAASC,SAAS,KAAOJ,IAAA,WAAAC,KAAAA,GAAA,OAAmB,CAAK,IAAAL,KAAA,QAAAC,WAAA,EAElEC,EAAAA,EAAAA,IAAS,CAAEI,KAAMC,WAAUH,IAAA,WAAAC,KAAAA,GAAA,OAAmB,CAAI,IAAAL,KAAA,SAAAI,IAAA,SAAAC,MAEnD,WACE,OAAOI,EAAAA,EAAAA,IAAIC,IAAAA,EAAAC,CAAA,mKAECC,KAAKC,KACJD,KAAKE,MACLF,KAAKP,MACFO,KAAKG,SACLH,KAAKI,SACPJ,KAAKK,OACEL,KAAKM,cAG5B,GAAC,CAAAlB,KAAA,SAAAI,IAAA,gBAAAC,MAED,SAAsBc,IACpBC,EAAAA,EAAAA,GAAUR,KAAM,gBAAiB,CAAEP,MAAOc,EAAGE,OAAOhB,OACtD,IAAC,GA/BuCiB,EAAAA,I"}
|
@@ -0,0 +1,2 @@
|
|||||||
|
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([["1236"],{4121:function(){Intl.PluralRules&&"function"==typeof Intl.PluralRules.__addLocaleData&&Intl.PluralRules.__addLocaleData({data:{categories:{cardinal:["one","other"],ordinal:["one","two","few","other"]},fn:function(e,n){var t=String(e).split("."),a=!t[1],l=Number(t[0])==e,o=l&&t[0].slice(-1),r=l&&t[0].slice(-2);return n?1==o&&11!=r?"one":2==o&&12!=r?"two":3==o&&13!=r?"few":"other":1==e&&a?"one":"other"}},locale:"en"})}}]);
|
||||||
|
//# sourceMappingURL=1236.64ca65d0ea4d76d4.js.map
|
BIN
supervisor/api/panel/frontend_es5/1236.64ca65d0ea4d76d4.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1236.64ca65d0ea4d76d4.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1236.64ca65d0ea4d76d4.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1236.64ca65d0ea4d76d4.js.gz
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"1236.64ca65d0ea4d76d4.js","sources":["/unknown/node_modules/@formatjs/intl-pluralrules/locale-data/en.js"],"names":["Intl","PluralRules","__addLocaleData","n","ord","s","String","split","v0","t0","Number","n10","slice","n100"],"mappings":"wHAEIA,KAAKC,aAA2D,mBAArCD,KAAKC,YAAYC,iBAC9CF,KAAKC,YAAYC,gBAAgB,CAAC,KAAO,CAAC,WAAa,CAAC,SAAW,CAAC,MAAM,SAAS,QAAU,CAAC,MAAM,MAAM,MAAM,UAAU,GAAK,SAASC,EAAGC,GAC3I,IAAIC,EAAIC,OAAOH,GAAGI,MAAM,KAAMC,GAAMH,EAAE,GAAII,EAAKC,OAAOL,EAAE,KAAOF,EAAGQ,EAAMF,GAAMJ,EAAE,GAAGO,OAAO,GAAIC,EAAOJ,GAAMJ,EAAE,GAAGO,OAAO,GACvH,OAAIR,EAAmB,GAAPO,GAAoB,IAARE,EAAa,MAC9B,GAAPF,GAAoB,IAARE,EAAa,MAClB,GAAPF,GAAoB,IAARE,EAAa,MACzB,QACQ,GAALV,GAAUK,EAAK,MAAQ,OAChC,GAAG,OAAS,M"}
|
@@ -1,2 +0,0 @@
|
|||||||
(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([["1236"],{4121:function(e,n,t){t(451),t(23509),Intl.PluralRules&&"function"==typeof Intl.PluralRules.__addLocaleData&&Intl.PluralRules.__addLocaleData({data:{categories:{cardinal:["one","other"],ordinal:["one","two","few","other"]},fn:function(e,n){var t=String(e).split("."),a=!t[1],l=Number(t[0])==e,o=l&&t[0].slice(-1),r=l&&t[0].slice(-2);return n?1==o&&11!=r?"one":2==o&&12!=r?"two":3==o&&13!=r?"few":"other":1==e&&a?"one":"other"}},locale:"en"})}}]);
|
|
||||||
//# sourceMappingURL=1236.71e83acee5b952f8.js.map
|
|
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"1236.71e83acee5b952f8.js","sources":["/unknown/node_modules/@formatjs/intl-pluralrules/locale-data/en.js"],"names":["Intl","PluralRules","__addLocaleData","n","ord","s","String","split","v0","t0","Number","n10","slice","n100"],"mappings":"6IAEIA,KAAKC,aAA2D,mBAArCD,KAAKC,YAAYC,iBAC9CF,KAAKC,YAAYC,gBAAgB,CAAC,KAAO,CAAC,WAAa,CAAC,SAAW,CAAC,MAAM,SAAS,QAAU,CAAC,MAAM,MAAM,MAAM,UAAU,GAAK,SAASC,EAAGC,GAC3I,IAAIC,EAAIC,OAAOH,GAAGI,MAAM,KAAMC,GAAMH,EAAE,GAAII,EAAKC,OAAOL,EAAE,KAAOF,EAAGQ,EAAMF,GAAMJ,EAAE,GAAGO,OAAO,GAAIC,EAAOJ,GAAMJ,EAAE,GAAGO,OAAO,GACvH,OAAIR,EAAmB,GAAPO,GAAoB,IAARE,EAAa,MAC9B,GAAPF,GAAoB,IAARE,EAAa,MAClB,GAAPF,GAAoB,IAARE,EAAa,MACzB,QACQ,GAALV,GAAUK,EAAK,MAAQ,OAChC,GAAG,OAAS,M"}
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1258.bb6811ca0567a5d6.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1258.bb6811ca0567a5d6.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1258.bb6811ca0567a5d6.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1258.bb6811ca0567a5d6.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1295.d3a5058b570b3a9e.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1295.d3a5058b570b3a9e.js.br
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1327.0e87d3390ae69a74.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1327.0e87d3390ae69a74.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1327.0e87d3390ae69a74.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1327.0e87d3390ae69a74.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/1352.7d375e47a6d46f74.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/1352.7d375e47a6d46f74.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/1352.7d375e47a6d46f74.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/1352.7d375e47a6d46f74.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
supervisor/api/panel/frontend_es5/137.a19e63f43d6e9cc2.js.br
Normal file
BIN
supervisor/api/panel/frontend_es5/137.a19e63f43d6e9cc2.js.br
Normal file
Binary file not shown.
BIN
supervisor/api/panel/frontend_es5/137.a19e63f43d6e9cc2.js.gz
Normal file
BIN
supervisor/api/panel/frontend_es5/137.a19e63f43d6e9cc2.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";(self.webpackChunkhome_assistant_frontend=self.webpackChunkhome_assistant_frontend||[]).push([["1390"],{48825:function(t,a,i){i.r(a);var e=i(73577),o=(i(71695),i(40251),i(47021),i(31622),i(57243)),n=i(50778),r=i(27486),s=i(36522),l=(i(73729),i(29073),i(56785)),c=i(74617),d=i(28008);let u,h,m,p=t=>t;const v=(0,r.Z)((()=>[{name:"default_backup_mount",required:!0,selector:{backup_location:{}}}]));(0,e.Z)([(0,n.Mo)("dialog-hassio-backup-location")],(function(t,a){return{F:class extends a{constructor(...a){super(...a),t(this)}},d:[{kind:"field",decorators:[(0,n.Cb)({attribute:!1})],key:"hass",value:void 0},{kind:"field",decorators:[(0,n.SB)()],key:"_dialogParams",value:void 0},{kind:"field",decorators:[(0,n.SB)()],key:"_data",value:void 0},{kind:"field",decorators:[(0,n.SB)()],key:"_waiting",value:void 0},{kind:"field",decorators:[(0,n.SB)()],key:"_error",value:void 0},{kind:"method",key:"showDialog",value:async function(t){this._dialogParams=t}},{kind:"method",key:"closeDialog",value:function(){this._data=void 0,this._error=void 0,this._waiting=void 0,this._dialogParams=void 0,(0,s.B)(this,"dialog-closed",{dialog:this.localName})}},{kind:"method",key:"render",value:function(){return this._dialogParams?(0,o.dy)(u||(u=p` <ha-dialog open scrimClickAction escapeKeyAction .heading="${0}" @closed="${0}"> ${0} <ha-form .hass="${0}" .data="${0}" .schema="${0}" .computeLabel="${0}" .computeHelper="${0}" @value-changed="${0}" dialogInitialFocus></ha-form> <mwc-button slot="secondaryAction" @click="${0}" dialogInitialFocus> ${0} </mwc-button> <mwc-button .disabled="${0}" slot="primaryAction" @click="${0}"> ${0} </mwc-button> </ha-dialog> `),this._dialogParams.supervisor.localize("dialog.backup_location.title"),this.closeDialog,this._error?(0,o.dy)(h||(h=p`<ha-alert alert-type="error">${0}</ha-alert>`),this._error):o.Ld,this.hass,this._data,v(),this._computeLabelCallback,this._computeHelperCallback,this._valueChanged,this.closeDialog,this._dialogParams.supervisor.localize("common.cancel"),this._waiting||!this._data,this._changeMount,this._dialogParams.supervisor.localize("common.save")):o.Ld}},{kind:"field",key:"_computeLabelCallback",value(){return t=>this._dialogParams.supervisor.localize(`dialog.backup_location.options.${t.name}.name`)||t.name}},{kind:"field",key:"_computeHelperCallback",value(){return t=>this._dialogParams.supervisor.localize(`dialog.backup_location.options.${t.name}.description`)}},{kind:"method",key:"_valueChanged",value:function(t){const a=t.detail.value.default_backup_mount;this._data={default_backup_mount:"/backup"===a?null:a}}},{kind:"method",key:"_changeMount",value:async function(){if(this._data){this._error=void 0,this._waiting=!0;try{await(0,c.Cl)(this.hass,this._data)}catch(t){return this._error=(0,l.js)(t),void(this._waiting=!1)}this.closeDialog()}}},{kind:"get",static:!0,key:"styles",value:function(){return[d.Qx,d.yu,(0,o.iv)(m||(m=p`.delete-btn{--mdc-theme-primary:var(--error-color)}`))]}}]}}),o.oi)},74617:function(t,a,i){i.d(a,{Cl:()=>r,eX:()=>o,mw:()=>e,rE:()=>n});i(40251);let e=function(t){return t.BIND="bind",t.CIFS="cifs",t.NFS="nfs",t}({}),o=function(t){return t.BACKUP="backup",t.MEDIA="media",t.SHARE="share",t}({});const n=async t=>t.callWS({type:"supervisor/api",endpoint:"/mounts",method:"get",timeout:null}),r=async(t,a)=>t.callWS({type:"supervisor/api",endpoint:"/mounts/options",method:"post",timeout:null,data:a})},30338:function(t,a,i){var e=i(97934),o=i(71998),n=i(4576),r=i(36760);t.exports=function(t,a){a&&"string"==typeof t||o(t);var i=r(t);return n(o(void 0!==i?e(i,t):t))}},60933:function(t,a,i){var e=i(40810),o=i(57877),n=i(63983),r=i(12360),s=i(13053),l=i(47645);e({target:"Array",proto:!0},{flatMap:function(t){var a,i=r(this),e=s(i);return n(t),(a=l(i,0)).length=o(a,i,i,e,0,1,t,arguments.length>1?arguments[1]:void 0),a}})},32126:function(t,a,i){i(35709)("flatMap")},25677:function(t,a,i){var e=i(40810),o=i(97934),n=i(63983),r=i(71998),s=i(4576),l=i(30338),c=i(79995),d=i(14181),u=i(92288),h=c((function(){for(var t,a,i=this.iterator,e=this.mapper;;){if(a=this.inner)try{if(!(t=r(o(a.next,a.iterator))).done)return t.value;this.inner=null}catch(n){d(i,"throw",n)}if(t=r(o(this.next,i)),this.done=!!t.done)return;try{this.inner=l(e(t.value,this.counter++),!1)}catch(n){d(i,"throw",n)}}}));e({target:"Iterator",proto:!0,real:!0,forced:u},{flatMap:function(t){return r(this),n(t),new h(s(this),{mapper:t,inner:null})}})},34810:function(t,a,i){i(25677)}}]);
|
||||||
|
//# sourceMappingURL=1390.f8ddc371bbdbc7c7.js.map
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user