Compare commits

..

262 Commits

Author SHA1 Message Date
dependabot[bot]
a841cb8282
Bump coverage from 7.9.2 to 7.10.0 (#6043) 2025-07-27 10:31:48 +02:00
dependabot[bot]
3b1b03c8a7
Bump dbus-fast from 2.44.1 to 2.44.2 (#6038)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.44.1 to 2.44.2.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.44.1...v2.44.2)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-version: 2.44.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 16:06:19 -04:00
dependabot[bot]
680428f304
Bump sentry-sdk from 2.33.0 to 2.33.2 (#6037)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.33.0 to 2.33.2.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.33.0...2.33.2)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.33.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 12:44:35 -04:00
dependabot[bot]
f34128c37e
Bump ruff from 0.12.3 to 0.12.4 (#6031)
---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.12.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 12:43:56 -04:00
dependabot[bot]
2ed0682b34
Bump sigstore/cosign-installer from 3.9.1 to 3.9.2 (#6032) 2025-07-18 10:00:58 +02:00
Stefan Agner
fbb0915ef8
Mark system as unhealthy if multiple OS installations are found (#6024)
* Add resolution check for duplicate OS installations

* Only create single issue/use separate unhealthy type

* Check MBR partition UUIDs as well

* Use partlabel

* Use generator to avoid code duplication

* Add list of devices, avoid unnecessary exception handling

* Run check only on HAOS

* Fix message formatting

* Fix and simplify pytests

* Fix UnhealthyReason sort order
2025-07-17 10:06:35 +02:00
Stefan Agner
780ae1e15c
Check for duplicate data disks only when the OS is available (#6025)
* Check for duplicate data disks only when the OS is available

Supervised installations do not have a specific data disk, so only
check for duplicate data disks on Home Assistant OS.

* Enable OS for multiple data disks check test
2025-07-16 10:43:15 +02:00
dependabot[bot]
c617358855
Bump orjson from 3.10.18 to 3.11.0 (#6028)
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.18 to 3.11.0.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.10.18...3.11.0)

---
updated-dependencies:
- dependency-name: orjson
  dependency-version: 3.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 09:24:34 +02:00
dependabot[bot]
b679c4f4d8
Bump sentry-sdk from 2.32.0 to 2.33.0 (#6027)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.32.0 to 2.33.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.32.0...2.33.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 09:20:28 +02:00
dependabot[bot]
c946c421f2
Bump debugpy from 1.8.14 to 1.8.15 (#6026)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.14 to 1.8.15.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.14...v1.8.15)

---
updated-dependencies:
- dependency-name: debugpy
  dependency-version: 1.8.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 09:19:44 +02:00
dependabot[bot]
aeabf7ea25
Bump blockbuster from 1.5.24 to 1.5.25 (#6020)
Bumps [blockbuster](https://github.com/cbornet/blockbuster) from 1.5.24 to 1.5.25.
- [Release notes](https://github.com/cbornet/blockbuster/releases)
- [Commits](https://github.com/cbornet/blockbuster/commits/v1.5.25)

---
updated-dependencies:
- dependency-name: blockbuster
  dependency-version: 1.5.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 09:18:57 +02:00
dependabot[bot]
365b838abf
Bump mypy from 1.16.1 to 1.17.0 (#6019)
Bumps [mypy](https://github.com/python/mypy) from 1.16.1 to 1.17.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.1...v1.17.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-16 09:08:57 +02:00
Stefan Agner
99c040520e
Drop ensure_builtin_repositories() (#6012)
* Drop ensure_builtin_repositories

With the new Repository classes we have the is_builtin property, so we
can easily make sure that built-ins are not removed. This allows us to
further cleanup the code by removing the ensure_builtin_repositories
function and the ALL_BUILTIN_REPOSITORIES constant.

* Make sure we add built-ins on load

* Reuse default set and avoid unnecessary copy

Reuse default set and avoid unnecessary copying during validation if
the default is not being used.
2025-07-14 22:19:06 +02:00
dependabot[bot]
eefe2f2e06
Bump aiohttp from 3.12.13 to 3.12.14 (#6014)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 11:43:55 +02:00
dependabot[bot]
a366e36b37
Bump ruff from 0.12.2 to 0.12.3 (#6016)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 11:19:08 +02:00
dependabot[bot]
27a2fde9e1
Bump astroid from 3.3.10 to 3.3.11 (#6017)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 11:18:54 +02:00
Stefan Agner
9a0f530a2f
Add Supervisor connectivity check after DNS restart (#6005)
* Add Supervisor connectivity check after DNS restart

When the DNS plug-in got restarted, check Supervisor connectivity
in case the DNS plug-in configuration change influenced Supervisor
connectivity. This is helpful when a DHCP server gets started after
Home Assistant is up. In that case the network provided DNS server
(local DNS server) becomes available after the DNS plug-in restart.

Without this change, the Supervisor connectivity will remain false
until the a Job triggers a connectivity check, for example the
periodic update check (which causes a updater and store reload) by
Core.

* Fix pytest and add coverage for new functionality
2025-07-10 11:08:10 +02:00
Stefan Agner
baf9695cf7
Refactoring around add-on store Repository classes (#5990)
* Rename repository fixture to test_repository

Also don't remove the built-in repositories. The list was incomplete,
and tests don't seem to require that anymore.

* Get rid of StoreType

The type doesn't have much value, we have constant strings anyways.

* Introduce types.py

* Use slug to determine which repository urls to return

* Simplify BuiltinRepository enum

* Mock GitRepo load

* Improve URL handling and repository creation logic

* Refactor update_repositories

* Get rid of get_from_url

It is no longer used in production code.

* More refactoring

* Address pylint

* Introduce is_git_based property to Repository class

Return all git based URLs, including the Core repository.

* Revert "Introduce is_git_based property to Repository class"

This reverts commit dfd5ad79bf23e0e127fc45d97d6f8de0e796faa0.

* Fold type.py into const.py

Align more with how Supervisor code is typically structured.

* Update supervisor/store/__init__.py

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

* Apply repository remove suggestion

* Fix tests

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2025-07-10 11:07:53 +02:00
Stefan Agner
7873c457d5
Small improvement to Copilot instructions (#6011) 2025-07-10 11:05:59 +02:00
Stefan Agner
cbc48c381f
Return 401 Unauthorized when using json/url encoded auth fails (#5844)
When authentication using JSON payload or URL encoded payload fails,
use the generic HTTP response code 401 Unauthorized instead of 400
Bad Request.

This is a more appropriate response code for authentication errors
and is consistent with the behavior of other authentication methods.
2025-07-10 08:38:00 +02:00
Franck Nijhof
11e37011bd
Add Task issue form (#6007) 2025-07-09 16:58:10 +02:00
Franck Nijhof
cfda559a90
Adjust feature request links in issue reporting (#6009) 2025-07-09 16:44:35 +02:00
Mike Degatano
806bd9f52c
Apply store reload suggestion automatically on connectivity change (#6004)
* Apply store reload suggestion automatically on connectivity change

* Use sys_bus not coresys.bus

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-09 16:43:51 +02:00
Stefan Agner
953f7d01d7
Improve DNS plug-in restart (#5999)
* Improve DNS plug-in restart

Instead of simply go by PrimaryConnectioon change, use the DnsManager
Configuration property. This property is ultimately used to write the
DNS plug-in configuration, so it is really the relevant information
we pass on to the plug-in.

* Check for changes and restart DNS plugin

* Check for changes in plug-in DNS

Cache last local (NetworkManager) provided DNS servers. Check against
this DNS server list when deciding when to restart the DNS plug-in.

* Check connectivity unthrottled in certain situations

* Fix pytest

* Fix pytest

* Improve test coverage for DNS plugins restart functionality

* Apply suggestions from code review

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>

* Debounce local DNS changes and event based connectivity checks

* Remove connection check logic

* Remove unthrottled connectivity check

* Fix delayed call

* Store restart task and cancel in case a restart is running

* Improve DNS configuration change tests

* Remove stale code

* Improve DNS plug-in tests, less mocking

* Cover multiple private functions at once

Improve tests around notify_locals_changed() to cover multiple
functions at once.

---------

Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
2025-07-09 11:35:03 +02:00
Felipe Santos
381e719a0e
Allow to force rebuild of add-ons (#6002) 2025-07-07 21:41:18 +02:00
Ruben van Dijk
296071067d
Fix multiple set-cookie headers with addons ingress (#5996) 2025-07-07 19:27:39 +02:00
dependabot[bot]
8336537f51
Bump types-docker from 7.1.0.20250523 to 7.1.0.20250705 (#6003)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-07 10:00:26 +02:00
Stefan Agner
5c90a00263
Force reload of /etc/resolv.conf on WebSession init (#6000) 2025-07-05 12:18:02 +02:00
dependabot[bot]
1f2bf77784
Bump coverage from 7.9.1 to 7.9.2 (#5992)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-04 08:54:36 +02:00
dependabot[bot]
9aa4f381b8
Bump ruff from 0.12.1 to 0.12.2 (#5993)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-04 08:47:35 +02:00
Mike Degatano
ae036ceffe
Don't backup uninstalled addons (#5988)
* Don't backup uninstalled addons

* Remove hash in backup
2025-07-04 07:05:53 +02:00
Stefan Agner
f0ea0d4a44
Add GitHub Copilot/Claude instruction (#5986)
* Add GitHub Copilot/Claude instruction

This adds an initial instruction file for GitHub Copilot and Claude
(CLAUDE.md symlinked to the same file).

* Add --ignore-missing-imports to mypy, add note to run pre-commit
2025-07-04 07:05:05 +02:00
Mike Degatano
abc44946bb
Refactor addon git repo (#5987)
* Refactor Repository into setup with inheritance

* Remove subclasses of GitRepo
2025-07-03 13:53:52 +02:00
dependabot[bot]
3e20a0937d
Bump cryptography from 45.0.4 to 45.0.5 (#5989)
Bumps [cryptography](https://github.com/pyca/cryptography) from 45.0.4 to 45.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/45.0.4...45.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-03 09:52:50 +02:00
Mike Degatano
6cebf52249
Store reset only deletes git cache after clone was successful (#5984)
* Store reset only deletes git cache after clone was successful

* Add test and fix fallback error handling

* Fix when lock is grabbed
2025-07-02 14:34:18 -04:00
Felipe Santos
bc57deb474
Use Docker BuildKit to build addons (#5974)
* Use Docker BuildKit to build addons

* Improve error message as suggested by CodeRabbit

* Fix container.remove() tests missing v=True

* Ignore squash rather than falling back to legacy builder

* Use version rather than tag to avoid confusion in run_command()

* Fix tests differently

* Use PropertyMock like other tests

* Restore position of fix_label fn

* Exempt addon builder image from unsupported checks

* Refactor tests

* Fix tests expecting wrong builder image

* Remove harcoded paths

* Fix tests

* Remove get_addon_host_path() function

* Use docker buildx build rather than docker build

Co-authored-by: Stefan Agner <stefan@agner.ch>

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-07-02 17:33:41 +02:00
Mike Degatano
38750d74a8
Refactor builtin repositories to enum (#5976) 2025-06-30 13:22:00 -04:00
Felipe Santos
d1c1a2d418
Fix docker.run_command() needing detach but not enforcing it (#5979)
* Fix `docker.run_command()` needing `detach` but not enforcing it

* Fix test
2025-06-30 16:09:19 +02:00
Felipe Santos
cf32f036c0
Fix docker_home_assistant_execute_command not honoring HA version (#5978)
* Fix `docker_home_assistant_execute_command` not honoring HA version

* Change variable name to image_with_tag

* Fix test
2025-06-30 16:08:05 +02:00
Felipe Santos
b8852872fe
Remove anonymous volumes when removing containers (#5977)
* Remove anonymous volumes when removing containers

* Add tests for docker.run_command()
2025-06-30 13:31:41 +02:00
dependabot[bot]
779f47e25d
Bump sentry-sdk from 2.31.0 to 2.32.0 (#5982)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 10:16:41 +02:00
dependabot[bot]
be8b36b560
Bump ruff from 0.12.0 to 0.12.1 (#5981)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-27 09:08:50 +02:00
dependabot[bot]
8378d434d4
Bump sentry-sdk from 2.30.0 to 2.31.0 (#5975)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.30.0 to 2.31.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.30.0...2.31.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-25 08:57:12 +02:00
Stefan Agner
0b79e09bc0
Add code documentation for Jobs decorator (#5965)
Add basic code documentation to the Jobs decorator.
2025-06-24 15:48:04 +02:00
Stefan Agner
d747a59696
Fix CLI/Observer access token property (#5973)
The access token token_validation() code in the security middleware
potentially accesses the access token property before the Supervisor
starts the CLI/Observer plugins, which leads to an KeyError when
trying to access the `access_token` property. This change ensures
that no key error is raised, but just None is returned.
2025-06-24 12:10:36 +02:00
Mike Degatano
3ee7c082ec
Add mypy to ci and precommit (#5969)
* Add mypy to ci and precommit

* Run precommit mypy in venv

* Fix issues raised in latest version of mypy
2025-06-24 11:48:03 +02:00
dependabot[bot]
3f921e50b3
Bump getsentry/action-release from 3.1.2 to 3.2.0 (#5972)
Bumps [getsentry/action-release](https://github.com/getsentry/action-release) from 3.1.2 to 3.2.0.
- [Release notes](https://github.com/getsentry/action-release/releases)
- [Changelog](https://github.com/getsentry/action-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/action-release/compare/v3.1.2...v3.2.0)

---
updated-dependencies:
- dependency-name: getsentry/action-release
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 10:08:27 +02:00
dependabot[bot]
0370320f75
Bump sigstore/cosign-installer from 3.9.0 to 3.9.1 (#5971)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.0 to 3.9.1.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.9.0...v3.9.1)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 10:08:19 +02:00
Stefan Agner
1e19e26ef3
Update request feature link (#5968)
Feature requests are now collected using the org wide GitHub Community.
Update the link accordingly.

While at it, also remove the unused ISSUE_TEMPLATE.md and align the
title to create issues with what is used in Home Assistant Core's
template.
2025-06-23 13:00:55 +02:00
Stefan Agner
e1a18eeba8
Use aiodns explicit close method (#5966) 2025-06-23 10:13:43 +02:00
Stefan Agner
b030879efd
Rename detect-blocking-io API value to match other APIs (#5964)
* Rename detect-blocking-io API value to match other APIs

For the new detect-blocking-io option, use dashes instead of
underscores in `on-at-startup` for consistency with other API
endpoints.

This is a breaking change, but since the API is really new and not
really used yet, it is fairly safe to do so.

* Fix pytest
2025-06-20 12:52:12 +02:00
dependabot[bot]
dfa1602ac6
Bump getsentry/action-release from 3.1.1 to 3.1.2 (#5963)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 10:33:47 +02:00
dependabot[bot]
bbda943583
Bump urllib3 from 2.4.0 to 2.5.0 (#5962)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-19 10:33:33 +02:00
Mike Degatano
aea15b65b7
Fix mypy issues in store, utils and all other source files (#5957)
* Fix mypy issues in store module

* Fix mypy issues in utils module

* Fix mypy issues in all remaining source files

* Fix ingress user typeddict

* Fixes from feedback

* Fix mypy issues after installing docker-types
2025-06-18 12:40:12 -04:00
dependabot[bot]
5c04249e41
Bump pytest from 8.4.0 to 8.4.1 (#5960)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.0 to 8.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0...8.4.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 15:43:22 +02:00
dependabot[bot]
456cec7ed1
Bump ruff from 0.11.13 to 0.12.0 (#5959)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.13 to 0.12.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.13...0.12.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 12:06:45 +02:00
dependabot[bot]
52a519e55c
Bump sigstore/cosign-installer from 3.8.2 to 3.9.0 (#5958)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.2 to 3.9.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.2...v3.9.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-18 10:57:20 +02:00
Stefan Agner
fcb20d0ae8
Remove bug label from issue template (#5955)
Don't label new issues with the bug label by default. We started making
use of issue types, so if anything, this should be type "Bug". However,
we prefer to leave the type unspecified until the issue has been triaged.
2025-06-17 13:10:52 +02:00
Stefan Agner
9b3f2b17bd
Remove AES cipher from backup (#5954)
AES cipher is no longer needed since Docker repository authentication
has been removed from backups in #5605.
2025-06-16 20:14:21 +02:00
Stefan Agner
3d026b9534
Expose machine ID (#5953)
Expose the unique machine ID of the local system via the Supervisor
API. This allows to identify a particular machine across reboots,
backup restores and updates. The machine ID is a stable identifier
that does not change unless the underlying hardware is changed or
the operating system is reinstalled.
2025-06-16 20:14:13 +02:00
Mike Degatano
0e8ace949a
Fix mypy issues in plugins and resolution (#5946)
* Fix mypy issues in plugins

* Fix mypy issues in resolution module

* fix misses in resolution check

* Fix signatures on evaluate methods

* nitpick fix suggestions
2025-06-16 14:12:47 -04:00
Stefan Agner
1fe6f8ad99
Bump cosign to v2.4.3 (#5945)
Follow the builder bump of 2025.02.0 and use cosign v2.4.3 for
Supervisor too.
2025-06-16 20:12:27 +02:00
dependabot[bot]
9ef2352d12
Bump sentry-sdk from 2.29.1 to 2.30.0 (#5947)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.29.1 to 2.30.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.29.1...2.30.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 11:48:34 +02:00
dependabot[bot]
2543bcae29
Bump aiohttp from 3.12.12 to 3.12.13 (#5952)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 11:48:20 +02:00
dependabot[bot]
ad9de9f73c
Bump coverage from 7.9.0 to 7.9.1 (#5949)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.9.0 to 7.9.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.9.0...7.9.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:26:18 +02:00
dependabot[bot]
a5556651ae
Bump pytest-cov from 6.2.0 to 6.2.1 (#5948)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.2.0 to 6.2.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:26:05 +02:00
dependabot[bot]
ac28deff6d
Bump aiodns from 3.4.0 to 3.5.0 (#5951)
Bumps [aiodns](https://github.com/saghul/aiodns) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/saghul/aiodns/releases)
- [Changelog](https://github.com/aio-libs/aiodns/blob/master/ChangeLog)
- [Commits](https://github.com/saghul/aiodns/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: aiodns
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:06:18 +02:00
Mike Degatano
82ee4bc441
Fix mypy issues in misc, mounts and os modules (#5942)
* Fix mypy errors in misc and mounts

* Fix mypy issues in os module

* Fix typing of capture_exception

* avoid unnecessary property call

* Fixes from feedback
2025-06-12 18:06:57 -04:00
Stefan Agner
bdbd09733a
Avoid aiodns resolver memory leak (#5941)
* Avoid aiodns resolver memory leak

In certain cases, the aiodns resolver can leak memory. This also
leads to Fatal `Python error… ffi.from_handle()`. This addresses
the issue by ensuring that the resolver is properly closed
when it is no longer needed.

* Address coderabbitai feedback

* Fix pytest

* Fix pytest
2025-06-12 11:32:53 +02:00
David Rapan
d5b5a328d7
feat: Add opt-in IPv6 for containers (#5879)
Configurable and w/ migrations between IPv4-Only and Dual-Stack

Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-06-12 11:32:24 +02:00
dependabot[bot]
52b24e177f
Bump coverage from 7.8.2 to 7.9.0 (#5944)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.8.2 to 7.9.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.8.2...7.9.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 09:27:56 +02:00
dependabot[bot]
e10c58c424
Bump pytest-cov from 6.1.1 to 6.2.0 (#5943)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.1 to 6.2.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.1...v6.2.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-12 09:27:44 +02:00
Mike Degatano
9682870c2c
Fix mypy issues in host and jobs (#5939)
* Fix mypy issues in host

* Fix mypy issues in job module

* Fix mypy issues introduced in previously fixed modules

* Apply suggestions from code review

Co-authored-by: Stefan Agner <stefan@agner.ch>

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-06-11 12:04:25 -04:00
Stefan Agner
fd0b894d6a
Fix dynamic port pytest (#5940) 2025-06-11 15:10:31 +02:00
dependabot[bot]
697515b81f
Bump aiohttp from 3.12.9 to 3.12.12 (#5937)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.12.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 11:52:14 +02:00
dependabot[bot]
d912c234fa
Bump requests from 2.32.3 to 2.32.4 (#5935)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 10:53:45 +02:00
dependabot[bot]
e8445ae8f2
Bump cryptography from 45.0.3 to 45.0.4 (#5936)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 09:23:40 +02:00
dependabot[bot]
6710439ce5
Bump ruff from 0.11.12 to 0.11.13 (#5932)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 00:41:04 -05:00
dependabot[bot]
95eec03c91
Bump aiohttp from 3.12.6 to 3.12.9 (#5930) 2025-06-05 07:43:55 +01:00
dependabot[bot]
9b686a2d9a
Bump pytest from 8.3.5 to 8.4.0 (#5929)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 10:23:35 +02:00
dependabot[bot]
063d69da90
Bump aiohttp from 3.12.4 to 3.12.6 (#5928)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 12:35:37 -05:00
dependabot[bot]
baaf04981f
Bump awesomeversion from 24.6.0 to 25.5.0 (#5926)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 01:23:53 -05:00
dependabot[bot]
bdb25a7ff8
Bump ruff from 0.11.11 to 0.11.12 (#5927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 01:23:06 -05:00
Jan Čermák
ad2d6a3156
Revert "Do not backup add-on being uninstalled (#5917)" (#5925)
This reverts commit 63fde3b4109310e95ebdcc8e3c23a04ff96ba592.

This change introduced another more severe regression, causing all
add-ons that haven't been started since Supervisor startup to cause
errors during their backup. More sophisticated check would have to be
implemented to address edge cases during backups for non-existing
add-ons (or their config actually).

Fixes #5924
2025-05-29 17:32:51 +02:00
Stefan Agner
42f885595e
Avoid early DNS plug-in start (#5922)
* Avoid early DNS plug-in start

A connectivity check can potentially be triggered before the DNS
plug-in is loaded. Avoid calling restart on the DNS plug-in before
it got initially loaded. This prevents starting before attaching.
The attaching makes sure that the DNS plug-in container is recreated
before the DNS plug-in is initially started, which is e.g. needed
by a potentially hassio network configuration change (e.g. the
migration required to enable/disable IPv6 on the hassio network,
see #5879).

* Mock DNS plug-in running
2025-05-29 11:49:19 +02:00
Stefan Agner
2a88cb9339
Improve Supervisor startup error handling (#5918)
Instead of starting a task in the background synchronously wait
for Supervisor start sequence to complete. This should be functional
equivalent, as we anyways would loop forever in the event loop just
afterwards.

The advantage is that we now can catch any exceptions during the
start sequence and report any errors with critical logging to report
those to Sentry, if enabled. It also avoids "Task exception was never
retrieved" errors. Reporting errors is especially important since we
can't use the asyncio Sentry integration (see #5729 for details).

Also handle early add-on start errors just like other add-on start
errors (make sure the finally block is executed as well). And finally,
register signal handlers synchronously. There is no real benefit in
doing them asynchronously, and it avoids a potential race condition.
2025-05-29 11:42:28 +02:00
Jan Čermák
4d1a5e2dc2
Use journal-gatewayd's new /boots endpoint to list boots (#5914)
* Use journal-gatewayd's new /boots endpoint to list boots

Current method we use for getting boots has several known downsides, for
example it can miss some incomplete boots and the performance might be
worse than what we could get by using Systemd directly. Systemd was
missing a method to get list boots through the journal-gatewayd but that
should be addressed by the new /boots endpoint added in [1] which
returns application/json-seq response containing all boots as reported
in `journalctl --list-boots`.

Implement Supervisor methods to parse this format and use the endpoint
at first, falling back to the old method if it fails.

[1] https://github.com/systemd/systemd/pull/37574

* Log info instead of warning when /boots is not present

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Split records only by RS instead of LF in journal_boots_reader

* Strip only RS, json.loads is fine with whitespace

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-05-29 11:41:23 +02:00
dependabot[bot]
705e76abe3
Bump aiohttp from 3.12.2 to 3.12.4 (#5923)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-29 00:42:40 -05:00
Jan Čermák
7f54383147
Revert "Use s6-overlay read-only mode by default (#5906)" (#5921) 2025-05-27 20:00:22 +02:00
Stefan Agner
63fde3b410
Do not backup add-on being uninstalled (#5917) 2025-05-27 14:00:54 +02:00
dependabot[bot]
5285e60cd3
Bump setuptools from 80.8.0 to 80.9.0 (#5919)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.8.0 to 80.9.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.8.0...v80.9.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 12:13:06 +02:00
J. Nick Koston
2a1e32bb36
Bump aiohttp to 3.12.2 (#5915) 2025-05-27 09:03:34 +02:00
dependabot[bot]
a2251e0729
Bump cryptography from 45.0.2 to 45.0.3 (#5912)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 17:02:04 +02:00
dependabot[bot]
1efee641ba
Bump coverage from 7.8.1 to 7.8.2 (#5913) 2025-05-26 08:41:06 +02:00
Stefan Agner
bbb8fa0b92
Ignore missing backup file on error (#5910)
When a backup error occurs, it might be that the backup file hasn't
been created yet, e.g. when there is no space or no permission on
the target backup directory. Deleting the backup file would fail
in this case. Use missing_ok instead to ignore a missing backup file
on delete.
2025-05-23 14:29:36 +02:00
Stefan Agner
7593f857e8
Fix add-on config parse messages (#5909)
With #5897 we renamed addon to addon_config and vis-versa. The log
messages were still using the previous variable names leading to
UnboundLocalError.

Fix the log messages to use the correct variable names.
2025-05-23 14:29:28 +02:00
Stefan Agner
87232cf1e4
Enable debug logging early (#5908)
Set logging level early in the bootstrap process so we can use debug
level messages in the early stages of the Supervisor.
2025-05-23 12:03:32 +02:00
dependabot[bot]
9e6a4d65cd
Bump ruff from 0.11.10 to 0.11.11 (#5907)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.10 to 0.11.11.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.10...0.11.11)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-23 10:35:24 +02:00
Stefan Agner
c80fbd77c8
Use s6-overlay read-only mode by default (#5906)
To avoid accidential writes to the Supervisor root filesystem, we might
use the Docker read-only mode at one point. This is not yet the default,
but using s6-overlay with the read-only flag seems not to have any
downsides. So enable this by default.

To start Supervisor with read-only root file system teh following
arguments have to be used: `--read-only --tmpfs /run:exec`.
2025-05-22 17:30:42 +02:00
dependabot[bot]
a452969ffe
Bump coverage from 7.8.0 to 7.8.1 (#5905)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.8.0 to 7.8.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.8.0...7.8.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 09:45:52 +02:00
Stefan Agner
89fa5c9c7a
Avoid initializing Blockbuster on Supervisor info call (#5901)
* Avoid initializing Blockbuster on Supervisor info call

Instead of creating an instance of Blockbuster to simply check if
Bluckbuster is enabled, use a global variable to store the instance
of Blockbuster and only initialize it when needed. This avoids
unnecessary initialization of Blockbuster when it is not required.

* Update supervisor/utils/blockbuster.py

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>

* Fix merge and rename singleton class to BlockBusterManager

* Fix pytest

---------

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
2025-05-21 15:06:46 +02:00
Stefan Agner
73069b628e
Bump pre-commit ruff to 0.11.10 (#5904)
Bump pre-commit ruff to 0.11.10 and address current issues.
2025-05-21 15:06:32 +02:00
Stefan Agner
8251b6c61c
Process NetworkManager PrimaryConnection changes (#5903)
Process NetworkManager interface updates in case PrimaryConnection
changes. This makes sure that the /network/interface/default/info
endpoint can be used to get the IP address of the primary interface.
2025-05-21 13:50:46 +02:00
Stefan Agner
1faf529b42
Use add-on config timestamp to determine add-on update age (#5897)
* Use add-on config timestamp to determine add-on update age

Instead of using the current timestamp when loading the add-on config,
simply use the add-on config modification timestamp. This way, we can
get a timetsamp even when Supervisor got restarted. It also simplifies
the code a bit.

* Fix pytest

* Patch stat() instead of modifing fixture files
2025-05-21 13:46:20 +02:00
dependabot[bot]
86c016b35d
Bump setuptools from 80.7.1 to 80.8.0 (#5902)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.7.1 to 80.8.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.7.1...v80.8.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-21 09:19:20 +02:00
Stefan Agner
4f35759fe3
Stop reading advanced logs on ConnectionError (#5900)
* Stop reading advanced logs on ConnectionError

If the client side connection closes with a `ConnectionError`, stop
reading the advanced logs.

This is very similar to ClientConnectionResetError which is easily
reproducable by having a log open and following in the browser and
then restaring Home Assistant. So far I wans't able to artificaially
reproduce the ConnectionError, but there are quite some reports on
Sentry so it seems to happen in real world.

* Warn on ConnectionError
2025-05-20 17:04:25 +02:00
David Rapan
3b575eedba
Add IPv6 address generation mode & privacy extensions (#5892)
* feat: Add IPv6 address generation mode & privacy extensions

Signed-off-by: David Rapan <david@rapan.cz>

* Use NetworkManager fixture for settings init tests

This fixes the test by since the extended implementation now can read
the version of NetworkManager.

* Add pytest for addr_gen_mode

---------

Signed-off-by: David Rapan <david@rapan.cz>
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-05-20 17:03:08 +02:00
Stefan Agner
6e6fe5ba39
Trigger auto-update through Core WebSocket call (#5896)
* Trigger auto-update through Core WebSocket call

Instead of auto-updating add-ons on Supervisor side trigger an update
through Core via a WebSocket command. This makes sure that the backup
is categorized correctly and all backup features like retention are
applied.

* Add pytest

* Fix pytest

* Fix pytest

* Fix pytest

* Fix pytest

* Fix pytest cleaner

* Set timestamp of add-on far into the past
2025-05-20 15:18:37 +02:00
Stefan Agner
b5a7e521ae
Copy additional backup locations in jobs (#5890)
Instead of copying the backup in the main job, lets copy them in
separate job per location. This allows to use the same backup error
handling mechanism as for add-ons and folders.

This makes the stage introduced in #5784 somewhat redundant, but
before removing it, let's see if this approach works out.
2025-05-20 15:18:23 +02:00
Stefan Agner
bac7c21fe8
Fix container image detection for aarch64 (#5898) 2025-05-20 10:24:27 +02:00
dependabot[bot]
2eb9ec20d6
Bump sentry-sdk from 2.28.0 to 2.29.1 (#5899)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.28.0 to 2.29.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.28.0...2.29.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.29.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 09:14:42 +02:00
dependabot[bot]
406348c068
Bump cryptography from 44.0.3 to 45.0.2 (#5895)
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.3 to 45.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.3...45.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 45.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 15:05:30 +02:00
dependabot[bot]
5e3f4e8ff3
Bump ruff from 0.11.9 to 0.11.10 (#5894)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 10:14:16 +02:00
dependabot[bot]
31a67bc642
Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#5893)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 10:13:58 +02:00
Stefan Agner
d0d11db7b1
Harmonize folder and add-on backup error handling (#5885)
* Harmonize folder and add-on backup error handling

Align add-on and folder backup error handling in that in both cases
errors are recorded on the respective backup Jobs, but not raised to
the caller. This allows the backup to complete successfully even if
some add-ons or folders fail to back up.

Along with this, also record errors in the per-add-on and per-folder
backup jobs, as well as the add-on and folder root job.

And finally, align the exception handling to only catch expected
exceptions for add-ons too.

* Fix pytest
2025-05-15 10:14:35 +02:00
dependabot[bot]
cbf4b4e27e
Bump setuptools from 80.4.0 to 80.7.1 (#5889)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.4.0 to 80.7.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.4.0...v80.7.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 09:44:02 +02:00
Stefan Agner
c855eaab52
Delete Backup files on error (#5880) 2025-05-13 20:51:16 +02:00
Stefan Agner
6bac751c4c
Log DNS resolver initialization errors with critical severity (#5884)
To make sure we learn about DNS resolver initialization errors, lets
log them with critical severity. This was the original intention of
PR #5882.
2025-05-13 14:42:59 +02:00
Stefan Agner
da0ae75e8e
Fallback to threaded resolver in case AsyncResolver fails (#5882)
In case the c-ares based AsyncResolver fails to initialize, let's
fallback to the threaded resolver. This would have helped for aiodns
3.3.0 issue when clients were unable to allocate more inotify watches.

This is fixed in aiodns 3.4.0, but we should still fallback to the
threaded resolver as a precautionary measure.
2025-05-13 12:37:35 +02:00
dependabot[bot]
154aeaee87
Bump sentry-sdk from 2.27.0 to 2.28.0 (#5881) 2025-05-13 08:20:44 +02:00
Stefan Agner
b9bbb99f37
Fix pytests to make them run in isolation (#5878) 2025-05-12 12:37:09 +02:00
dependabot[bot]
ff849ce692
Bump astroid from 3.3.9 to 3.3.10 (#5875)
Bumps [astroid](https://github.com/pylint-dev/astroid) from 3.3.9 to 3.3.10.
- [Release notes](https://github.com/pylint-dev/astroid/releases)
- [Changelog](https://github.com/pylint-dev/astroid/blob/main/ChangeLog)
- [Commits](https://github.com/pylint-dev/astroid/compare/v3.3.9...v3.3.10)

---
updated-dependencies:
- dependency-name: astroid
  dependency-version: 3.3.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 10:05:17 +02:00
dependabot[bot]
24456efb6b
Bump setuptools from 80.3.1 to 80.4.0 (#5876)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.3.1 to 80.4.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.3.1...v80.4.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 10:05:01 +02:00
dependabot[bot]
0cd9d04e63
Bump ruff from 0.11.8 to 0.11.9 (#5877) 2025-05-12 09:19:32 +02:00
Stefan Agner
39bd20c0e7
Handle non-existing addon config dir (#5871)
* Handle non-existing addon config dir

Since users have access to the root of all add-on config directories,
they can delete the directory of an add-ons at any time. Hence we need
to handle gracefully if it doesn't exist anymore.

* Add pytest
2025-05-09 11:07:22 +02:00
dependabot[bot]
481bbc5be8
Bump aiodns from 3.3.0 to 3.4.0 (#5870)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-09 09:50:58 +02:00
Stefan Agner
36da382af3
Handle ClientPayloadError in advanced logging (#5869)
When the systemd-journal-gatewayd service is being shutdown while
Supervisor is still trying to read logs, aiohttp throws a
ClientPayloadError, presumably because we try to read until the next
linefeed, which aiohttp cannot satisfy anymore.

Simply catch the exception just like the connection reset errors
previously in #5358 and #5715.
2025-05-06 20:33:05 +02:00
Stefan Agner
85f8107b60
Recreate aiohttp ClientSession after DNS plug-in load (#5862)
* Recreate aiohttp ClientSession after DNS plug-in load

Create a temporary ClientSession early in case we need to load version
information from the internet. This doesn't use the final DNS setup
and hence might fail to load in certain situations since we don't have
the fallback mechanims in place yet. But if the DNS container image
is present, we'll continue the setup and load the DNS plug-in. We then
can recreate the ClientSession such that it uses the DNS plug-in.

This works around an issue with aiodns, which today doesn't reload
`resolv.conf` automatically when it changes. This lead to Supervisor
using the initial `resolv.conf` as created by Docker. It meant that
we did not use the DNS plug-in (and its fallback capabilities) in
Supervisor. Also it meant that changes to the DNS setup at runtime
did not propagate to the aiohttp ClientSession (as observed in #5332).

* Mock aiohttp.ClientSession for all tests

Currently in several places pytest actually uses the aiohttp
ClientSession and reaches out to the internet. This is not ideal
for unit tests and should be avoided.

This creates several new fixtures to aid this effort: The `websession`
fixture simply returns a mocked aiohttp.ClientSession, which can be
used whenever a function is tested which needs the global websession.

A separate new fixture to mock the connectivity check named
`supervisor_internet` since this is often used through the Job
decorator which require INTERNET_SYSTEM.

And the `mock_update_data` uses the already existing update json
test data from the fixture directory instead of loading the data
from the internet.

* Log ClientSession nameserver information

When recreating the aiohttp ClientSession, log information what
nameservers exactly are going to be used.

* Refuse ClientSession initialization when API is available

Previous attempts to reinitialize the ClientSession have shown
use of the ClientSession after it was closed due to API requets
being handled in parallel to the reinitialization (see #5851).
Make sure this is not possible by refusing to reinitialize the
ClientSession when the API is available.

* Fix pytests

Also sure we don't create aiohttp ClientSession objects unnecessarily.

* Apply suggestions from code review

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>

---------

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
2025-05-06 16:23:40 +02:00
dependabot[bot]
2e44e6494f
Bump pytest-timeout from 2.3.1 to 2.4.0 (#5868) 2025-05-06 09:00:11 +02:00
dependabot[bot]
cd1cc66c77
Bump cryptography from 44.0.2 to 44.0.3 (#5866)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 02:07:51 -05:00
dependabot[bot]
b76a1f58ea
Bump setuptools from 80.1.0 to 80.3.1 (#5867)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 01:50:41 -05:00
dependabot[bot]
3fcd254d25
Bump pylint from 3.3.6 to 3.3.7 (#5865)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 01:49:50 -05:00
dependabot[bot]
3dff2abe65
Bump aiodns from 3.2.0 to 3.3.0 (#5864)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 01:07:22 -05:00
dependabot[bot]
ba91be1367
Bump ruff from 0.11.7 to 0.11.8 (#5863) 2025-05-02 09:40:45 +02:00
dependabot[bot]
25f93cd338
Bump setuptools from 80.0.1 to 80.1.0 (#5861)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.0.1 to 80.1.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.0.1...v80.1.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-01 11:07:40 +02:00
Stefan Agner
9b0044edd6
Avoid using host system socket for systemd journald tests (#5858)
Similar to #5825, make sure we mock the systemd journal gateway socket
for tests. This makes the test work on systems which have
systemd-journal-gatewayd installed.
2025-04-30 19:59:09 +02:00
Stefan Agner
9915c21243
Check local store repository for changes (#5845)
* Check local store repository for changes

Instead of simply assume that the local store repository got changed,
use mtime to check if there have been any changes to the local store.
This mimics a similar behavior to the git repository store updates.

Before this change, we end up in the updated repo code path, which
caused a re-read of all add-ons on every store reload, even though
nothing changed at all. Store reloads are triggered by Home Assistant
Core every 5 minutes.

* Fix pytest failure

Now that we actually only reload metadata if the local store changed
we have to fake the change as well to fix the store manager tests.

* Fix path cache update test for local store repository

* Take root directory into account/add pytest

* Rename utils/__init__.py tests to test_utils_init.py
2025-04-30 11:13:24 +02:00
dependabot[bot]
657cb56fb9
Bump orjson from 3.10.16 to 3.10.18 (#5855)
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.16 to 3.10.18.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.10.16...3.10.18)

---
updated-dependencies:
- dependency-name: orjson
  dependency-version: 3.10.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 10:20:26 +02:00
dependabot[bot]
1b384cebc9
Bump setuptools from 80.0.0 to 80.0.1 (#5856)
Bumps [setuptools](https://github.com/pypa/setuptools) from 80.0.0 to 80.0.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v80.0.0...v80.0.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-30 10:20:17 +02:00
Jan Čermák
61089c3507
Bump uv to 0.6.17 (#5854) 2025-04-29 16:57:48 +02:00
Stefan Agner
bc9e3eb95b
Fix race condition when removing add-on (#5850)
When uninstalling an add-on, we schedule a task to reload the ingress
tokens. This scheduled task typically ends up running right after
clearing the add-on data with `self.sys_addons.data.uninstall(self)`
(since this task is doing I/O, the race is rather deterministic).

Let's make sure we reload the ingress tokens at the end. Also simply
execute reloading synchrounsly since this is a rather quick operation
and makes sure that errors would get attributed to the right add-on
uninstall operation.
2025-04-29 16:14:33 +02:00
Stefan Agner
c1b45406d6
Improve backup upload location determination (#5848)
* Improve backup upload location determination

For local backup upload locations, check if the location is on the same
file system an thuse allows to move the backup file after upload. This
allows custom backup mounts. Currently there is no documented,
persistent way to create such mounts in with Home Assistant OS
installations, but since we might add local mounts in the future this
seems a worthwhile addition.

Fixes: #5837

* Fix pytests
2025-04-29 16:14:20 +02:00
Stefan Agner
8e714072c2
Avoid reading add-ons twice unnecessarily (#5846)
So far a store reload lead to a reload of all add-ons twice, usually
causing two messages in quick succession:
```
2025-04-25 17:01:05.058 INFO (MainThread) [supervisor.store] Loading add-ons from store: 91 all - 0 new - 0 remove
2025-04-25 17:01:05.058 INFO (MainThread) [supervisor.store] Loading add-ons from store: 91 all - 0 new - 0 remove
```

This is because when repository changes are detected, `reload()` calls
`load()` which then calls `update_repositories()` which ends up calling
`_read_addons()`, while `reload()` itself calls `_read_addons()` after
`load()` as well.

One way to fix this would be to simply remove the `_read_addons()` call
in `reload()`.

However, it seems the `update_repositories()` call (via `load()`)
is not necessary at all, as we don't add new store repositories in
`reload()`, and we already made sure the built-ins are present on
startup.

So simply call `data.update()` to update the store data cache, as it
was the case before #2225. There is no apparent reason documented why
`data.update()` was changed to a `load()` call. It might be to ensure
regularly that built-in repositories are still in the list of store
repositories. But this type of regular invariant check is often harmful
as it might hide bugs in other places.

Supervisor will still call `update_repositories()` in `load()` to
ensure all built-in repositories are present, just in case the local
configuration file got modified or corrupted.
2025-04-29 16:13:56 +02:00
Stefan Agner
88087046de
Remove deprecated ruff rule S320 (#5847) 2025-04-29 12:58:09 +02:00
Stefan Agner
53393afe8d
Revert "Recreate aiohttp session on connectivity check (#5332)" (#5851)
This reverts commit 1504278223a8d852d3b11de25f676fa8d6501a70.

It turns out that recreating the session can cause race conditions, e.g.
with API checks triggered by proxied requests running alongside. These
manifest in the following error:

AttributeError: 'NoneType' object has no attribute 'connect'
...
  File "supervisor/homeassistant/api.py", line 187, in check_api_state
    if state := await self.get_api_state():
  File "supervisor/homeassistant/api.py", line 171, in get_api_state
    data = await self.get_core_state()
  File "supervisor/homeassistant/api.py", line 145, in get_core_state
    return await self._get_json("api/core/state")
  File "supervisor/homeassistant/api.py", line 132, in _get_json
    async with self.make_request("get", path) as resp:
  File "contextlib.py", line 214, in __aenter__
    return await anext(self.gen)
  File "supervisor/homeassistant/api.py", line 106, in make_request
    async with getattr(self.sys_websession, method)(
  File "aiohttp/client.py", line 1425, in __aenter__
    self._resp: _RetType = await self._coro
  File "aiohttp/client.py", line 703, in _request
    conn = await self._connector.connect(

The only reason for the _connection in the aiohttp client to be None is
when close() gets called on the session. The only place this is being
done is the connectivity check.

So it seems that between fetching the session from the `sys_websession`
property) and actually using the connector, a connectivity check has been
run which then causes the above stack trace.

Let's not mess with the lifetime of the ClientSession object and simply
revert the change. Another solution for the original problem needs to be
found.
2025-04-28 23:48:47 +02:00
dependabot[bot]
4b5bcece64
Bump setuptools from 79.0.1 to 80.0.0 (#5849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 02:47:57 -04:00
dependabot[bot]
0e7e4f8b42
Bump ruff from 0.11.6 to 0.11.7 (#5841)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.6 to 0.11.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.6...0.11.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 15:45:14 +02:00
Stefan Agner
9470f44840
Improve /auth API request sanitation (#5843)
* Add basic test coverage for /auth API

* Check /auth API is called from an add-on

Currently the /auth API is only available for add-ons. Return 403
for calls not originating from an add-on.

* Handle bad json in auth API

Use the API specific JSON load helper which raises an APIError. This
causes the API to return a 400 error instead of a 500 error when the
JSON is invalid.

* Avoid redefining name 'mock_check_login'

* Update tests/api/test_auth.py
2025-04-25 15:17:25 +02:00
dependabot[bot]
0e55e6e67b
Bump sentry-sdk from 2.26.1 to 2.27.0 (#5842)
* Bump sentry-sdk from 2.26.1 to 2.27.0

Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.26.1 to 2.27.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.26.1...2.27.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Make use of new typing hints in filter.py

* Avoid creating unnecessary empty dict

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
2025-04-25 11:40:17 +02:00
dependabot[bot]
6116425265
Bump actions/download-artifact from 4.2.1 to 4.3.0 (#5840)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 09:01:23 +02:00
Stefan Agner
de497cdc19
Add dedicated version update refresh for main components (#5833)
* Add dedicated update information reload

Currently we have the /refresh_updates endpoint which updates the main
component versions (Core, OS, Supervisor, Plug-ins) and the add-on
store at the same time. This combined update causes more update
information reloads than necessary.

To allow fine grained update refresh control introduce a new endpoint
/reload_updates which asks Supervisor to only update main component
versions (learned through the version json files).

The /store/reload endpoint already allows to update the add-on store
separately.

* Add pytest

* Update supervisor/api/__init__.py
2025-04-24 15:46:18 +02:00
dependabot[bot]
88b41e80bb
Bump actions/setup-python from 5.5.0 to 5.6.0 (#5836)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-23 21:04:49 -10:00
dependabot[bot]
876afdb26e
Bump setuptools from 79.0.0 to 79.0.1 (#5835)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-23 21:04:32 -10:00
dependabot[bot]
9d062c8ed0
Bump sigstore/cosign-installer from 3.8.1 to 3.8.2 (#5832)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.8.1 to 3.8.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.8.1...v3.8.2)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 3.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-23 10:40:41 +02:00
Stefan Agner
122b73202b
Unify Supervisor event message functions (#5831)
* Unify Supervisor event message functions

Unify functions which send WebSocket messages of type
"supervisor/event". This deduplicates code and hopefully avoids further
diversication in the future.

While at it, remove unused HomeAssistantWSNotSupported exception. It
seems the only place this exception is used got removed in #3317.

* Test message delivery during shutdown states
2025-04-23 10:40:25 +02:00
Stefan Agner
5d07dd2c42
Add country to Supervisor info (#5826)
Similar to timezone also add country information to the Supervisor
info. This is useful to set country specific configurations such as
Wireless radio regulatory setting. This is also useful for add-ons
which need country information but only have hassio API access.
2025-04-22 16:18:23 +02:00
Jan Čermák
adfb433f57
Intercept host logs Range header for Systemd v256+ compatibility (#5827)
Since Systemd v256 the Range header must not end with a trailing colon.
We relied on this undocumented feature when following logs, and the
frontend or CLI may still use it in requests. To fix the requests
failing with new Systemd version, intercept the header and fill in the
num_entries to maximum possible value, which avoids the journal-gatewayd
returning the response prematurely and also works on older Systemd
versions.

The journal-gatewayd would still return response if follow flag is used
along with num_entries, but this behavior is unchanged and would be
better fixed in the backend.

Link: https://github.com/systemd/systemd/issues/37172
2025-04-22 09:05:49 +02:00
dependabot[bot]
198af54d1e
Bump aiohttp from 3.11.16 to 3.11.18 (#5830)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.11.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 09:02:14 +02:00
dependabot[bot]
c3e63a5669
Bump setuptools from 78.1.0 to 79.0.0 (#5829) 2025-04-21 20:29:21 +02:00
dependabot[bot]
8f27958e20
Bump ruff from 0.11.5 to 0.11.6 (#5828)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 21:43:49 -10:00
Stefan Agner
6fad7d14e1
Avoid using host system socket for logs tests (#5825)
Make sure we mock the systemd journal gateway socket for tests. This
makes the test work on systems which have systemd-journal-gatewayd
installed.
2025-04-17 16:23:34 +02:00
dependabot[bot]
f7317134e3
Bump sentry-sdk from 2.26.0 to 2.26.1 (#5824)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 09:20:46 +02:00
dependabot[bot]
9d8db27701
Bump sentry-sdk from 2.25.1 to 2.26.0 (#5822)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.25.1 to 2.26.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.25.1...2.26.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 08:38:36 +02:00
dependabot[bot]
7da3a34304
Bump codecov/codecov-action from 5.4.0 to 5.4.2 (#5823) 2025-04-15 08:25:45 +02:00
Stefan Agner
d413e0dcb9
Drop typing-extensions from requirements (#5821)
The dependency typing-extensions has been added with #3848, but not
used much. With the update to Python 3.11 in #4666 the necessary types
are now available from the Python standard library, making
typing-extensions unused. Remove the now unnecessary typing-extensions
from the dependencies.
2025-04-11 10:40:24 +02:00
dependabot[bot]
542ab0411c
Bump typing-extensions from 4.13.1 to 4.13.2 (#5817)
Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.13.1 to 4.13.2.
- [Release notes](https://github.com/python/typing_extensions/releases)
- [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/python/typing_extensions/compare/4.13.1...4.13.2)

---
updated-dependencies:
- dependency-name: typing-extensions
  dependency-version: 4.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 09:45:33 +02:00
dependabot[bot]
999789f7ce
Bump ruff from 0.11.4 to 0.11.5 (#5818)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.4 to 0.11.5.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.4...0.11.5)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 09:31:52 +02:00
dependabot[bot]
de105f8cb7
Bump debugpy from 1.8.13 to 1.8.14 (#5819)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.13 to 1.8.14.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.13...v1.8.14)

---
updated-dependencies:
- dependency-name: debugpy
  dependency-version: 1.8.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 09:31:38 +02:00
dependabot[bot]
b37b0ff744
Bump urllib3 from 2.3.0 to 2.4.0 (#5820)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.3.0...2.4.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 09:31:25 +02:00
dependabot[bot]
db330ab58a
Update wheel requirement from ~=0.45.0 to ~=0.46.1 (#5816)
Updates the requirements on [wheel](https://github.com/pypa/wheel) to permit the latest version.
- [Release notes](https://github.com/pypa/wheel/releases)
- [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst)
- [Commits](https://github.com/pypa/wheel/compare/0.45.0...0.46.1)

---
updated-dependencies:
- dependency-name: wheel
  dependency-version: 0.46.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 09:47:43 +02:00
Mike Degatano
4a00caa2e8
Fix mypy issues in docker, hardware and homeassistant modules (#5805)
* Fix mypy issues in docker and hardware modules

* Fix mypy issues in homeassistant module

* Fix async_send_command typing

* Fixes from feedback
2025-04-08 12:52:58 -04:00
Stefan Agner
59a7e9519d
Fix root path requests (#5815)
* Fix root path requests

Since #5759 we've tried to access the path explicitly. However, this
raises KeyError exception when trying to access the proxied root path
(e.g. http://supervisor/core/api/). Before #5759 get was used, which
lead to no exception, but instead inserted a `None` into the path.

It seems aiohttp doesn't provide a path when the root is accessed. So
simply convert this to no path as well by setting path to an empty
string.

* Add rudimentary pytest for regular proxy requets
2025-04-07 11:09:45 +02:00
dependabot[bot]
dedf5df5ad
Bump ruff from 0.11.3 to 0.11.4 (#5814)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.3 to 0.11.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.3...0.11.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 09:47:12 +02:00
dependabot[bot]
d09b686269
Bump pytest-cov from 6.1.0 to 6.1.1 (#5813)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.0 to 6.1.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.0...v6.1.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 09:24:59 +02:00
dependabot[bot]
9b8f03fa00
Bump dbus-fast from 2.44.0 to 2.44.1 (#5807)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.44.0 to 2.44.1.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.44.0...v2.44.1)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-version: 2.44.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 09:37:09 +02:00
dependabot[bot]
2a3d0fdf61
Bump sentry-sdk from 2.25.0 to 2.25.1 (#5804)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.25.0 to 2.25.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.25.0...2.25.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.25.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 09:18:35 +02:00
dependabot[bot]
eaae40718b
Bump ruff from 0.11.2 to 0.11.3 (#5809)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.2 to 0.11.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.2...0.11.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.11.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 09:08:08 +02:00
dependabot[bot]
5a88128cec
Bump typing-extensions from 4.12.2 to 4.13.1 (#5808)
Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.12.2 to 4.13.1.
- [Release notes](https://github.com/python/typing_extensions/releases)
- [Changelog](https://github.com/python/typing_extensions/blob/main/CHANGELOG.md)
- [Commits](https://github.com/python/typing_extensions/compare/4.12.2...4.13.1)

---
updated-dependencies:
- dependency-name: typing-extensions
  dependency-version: 4.13.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 08:52:39 +02:00
github-actions[bot]
62b3259d9c
Update frontend to version 20250401.0 (#5687)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-02 10:46:50 +02:00
dependabot[bot]
5e05af26a8
Bump dbus-fast from 2.43.0 to 2.44.0 (#5800)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.43.0 to 2.44.0.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.43.0...v2.44.0)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 10:36:31 +02:00
dependabot[bot]
c5186101d3
Bump pytest-cov from 6.0.0 to 6.1.0 (#5799)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.0.0 to 6.1.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 09:26:00 +02:00
dependabot[bot]
86cf083902
Bump aiohttp from 3.11.15 to 3.11.16 (#5801)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.15 to 3.11.16.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.11.15...v3.11.16)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 09:25:43 +02:00
Mike Degatano
5c1f7ed18d
Switch iter_chunked to iter_chunks (#5798)
* Switch iter_chunked to iter_any

* iter_chunks not iter_any
2025-04-01 21:49:33 +02:00
dependabot[bot]
d051cbcafb
Bump aiohttp from 3.11.14 to 3.11.15 (#5795)
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.11.14 to 3.11.15.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.11.14...v3.11.15)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-version: 3.11.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 11:31:00 +02:00
dependabot[bot]
798af687cf
Bump sentry-sdk from 2.24.1 to 2.25.0 (#5796)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.24.1 to 2.25.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.24.1...2.25.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-version: 2.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 10:13:01 +02:00
Mike Degatano
01a682cfaa
Fix mypy issues in backups and dbus (#5792)
* Fix mypy issues in backups module

* Fix mypy issues in dbus module

* Fix mypy issues in api after rebase

* TypedDict to dataclass and other small fixes

* Finish fixing mypy errors in dbus

* local_where must exist

* Fix references to name in tests
2025-03-31 17:03:54 -04:00
dependabot[bot]
67b9a44160
Bump coverage from 7.7.1 to 7.8.0 (#5793) 2025-03-30 21:05:11 -10:00
Stefan Agner
8fe17d9270
Improve Home Assistant Core WebSocket proxy implementation (#5790)
* Improve Home Assistant Core WebSocket proxy implementation

This change removes unnecessary task creation for every WebSocket
message and instead creates just two tasks, one for each direction.
This improves performance by about factor of 3 when measuring 1000
WebSocket requests to Core (from ~530ms to ~160ms).

While at it, also handle all WebSocket message related to closing the
WebSocket and report all other errors as warnings instead of just info.

* Improve logging and error handling

* Add WS client error test case

* Use asyncio.gather directly

* Use asyncio.wait to handle exceptions gracefully

* Drop cancellation handling and correctly wait for the other proxy task
2025-03-28 10:35:49 +01:00
Jan Čermák
0a684bdb12
Add API for swap configuration (#5770)
* Add API for swap configuration

Add HTTP API for swap size and swappiness to /os/config/swap. Individual
options can be set in JSON and are calling the DBus API added in OS
Agent 1.7.x, available since OS 15.0. Check for presence of OS of the
required version and return 404 if the criteria are not met.

* Fix type hints and reboot_required logic

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix formatting after adding suggestions from GH

* Address @mdegat01 review comments

- Improve swap options validation
- Add swap to the 'all' property of dbus agent
- Use APINotFound with reason instead of HTTPNotFound
- Reorder API routes

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-03-27 17:53:46 +01:00
Mike Degatano
9222a3c9c0
Report stage with error in jobs (#5784)
* Report stage with error in jobs

* Copy doesn't lose track of the successful copies

* Add stage to errors in api output test

* revert unneessary change to import

* Add tests for a bit more coverage of copy_additional_locations
2025-03-27 10:07:06 -04:00
Jan Čermák
92cadb4c55
Fix /supervisor/reload after refactoring (#5791)
As discussed in [1], refactoring in #5759 changed signature of the
reload method and CLI now gets unexpected schema when `ha su reload` is
called. Change the method to return None as before and add a test for a
proper body content.

[1] https://github.com/home-assistant/supervisor/pull/5759/files#diff-1b4ed26f31e52ff5fe53efdc695eebacb1e46411f23cce58295591b2b20cd3faR238
2025-03-27 10:03:57 -04:00
Mike Degatano
8b3bf547d7
Skip corrupt registry files in backups (#5789) 2025-03-27 10:32:28 +01:00
Stefan Agner
81fc15d6ac
Handle unexpected WebSocket messages during auth (#5788)
* Handle unexpected WebSocket messages during auth

When an add-on does not respond or closes the WebSocket connection
during the authentication phase Supervisor does not handle errors
gracefully. Simply log such unexpected authentication to avoid
unnecessary stack traces in the log and make such cases no longer
appear on Sentry.

* Add pytest

* Introduce a timeout of 10s
2025-03-26 22:13:59 +01:00
Stefan Agner
63b507a589
Rate limit D-Bus errors (#5787)
It seems that some systems continously run into D-Bus errors
overwhelming the system itself but also generating lots of errors on
Sentry. Rate limit D-Bus errors to 3 for every message every 30s.
2025-03-26 17:02:58 +01:00
dependabot[bot]
af9b1e5b1e
Bump orjson from 3.10.12 to 3.10.16 (#5783)
Bumps [orjson](https://github.com/ijl/orjson) from 3.10.12 to 3.10.16.
- [Release notes](https://github.com/ijl/orjson/releases)
- [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ijl/orjson/compare/3.10.12...3.10.16)

---
updated-dependencies:
- dependency-name: orjson
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 09:36:12 +01:00
dependabot[bot]
062103ae24
Bump setuptools from 78.0.2 to 78.1.0 (#5785) 2025-03-26 07:33:50 +01:00
dependabot[bot]
48807a65dd
Bump home-assistant/wheels from 2025.02.0 to 2025.03.0 (#5786) 2025-03-26 07:33:15 +01:00
Mike Degatano
0636e49fe2
Enable mypy part 1 (addons and api) (#5759)
* Fix mypy issues in addons

* Fix mypy issues in api

* fix docstring

* Brackets instead of get with default
2025-03-25 15:06:35 -04:00
dependabot[bot]
543d6efec4
Bump sentry-sdk from 2.24.0 to 2.24.1 (#5781)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.24.0 to 2.24.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.24.0...2.24.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 10:28:34 +01:00
Mike Degatano
80f7f07341
Add blockbuster option to API (#5746)
* Add blockbuster option to API

* cache not lru_cache
2025-03-25 09:40:43 +01:00
dependabot[bot]
ec721c41c1
Bump actions/setup-python from 5.4.0 to 5.5.0 (#5780)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.4.0...v5.5.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 08:23:24 +01:00
dependabot[bot]
03ca32ced4
Bump setuptools from 77.0.3 to 78.0.2 (#5782) 2025-03-25 07:48:51 +01:00
Jan Čermák
cb16a34401
Remove WipeDevice method from OS Agent DBus mock (#5744)
WipeDevice method was dropped from OS Agent code in [1]. Remove it from
the mock class to sync with the current API. There is no usage of
WipeDevice in the Supervisor codebase, only ScheduleWipeDevice is
called.

[1] https://github.com/home-assistant/os-agent/pull/225
2025-03-24 15:09:01 +01:00
dependabot[bot]
d756fd7e14
Bump dbus-fast from 2.39.6 to 2.43.0 (#5777)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.39.6 to 2.43.0.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.39.6...v2.43.0)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 11:07:01 +01:00
dependabot[bot]
c559bd47c3
Bump sentry-sdk from 2.23.1 to 2.24.0 (#5778) 2025-03-24 07:36:50 +01:00
dependabot[bot]
a2b3427be9
Bump ruff from 0.11.1 to 0.11.2 (#5779) 2025-03-24 07:28:40 +01:00
dependabot[bot]
6a2d7bad03
Bump coverage from 7.7.0 to 7.7.1 (#5776) 2025-03-24 07:23:53 +01:00
dependabot[bot]
cfdefbf043
Bump home-assistant/builder from 2025.02.0 to 2025.03.0 (#5774)
Bumps [home-assistant/builder](https://github.com/home-assistant/builder) from 2025.02.0 to 2025.03.0.
- [Release notes](https://github.com/home-assistant/builder/releases)
- [Commits](https://github.com/home-assistant/builder/compare/2025.02.0...2025.03.0)

---
updated-dependencies:
- dependency-name: home-assistant/builder
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 21:43:41 +01:00
dependabot[bot]
d7e3dc41ff
Bump actions/download-artifact from 4.2.0 to 4.2.1 (#5769)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-21 21:28:03 +01:00
dependabot[bot]
9afb50242b
Bump setuptools from 77.0.1 to 77.0.3 (#5772)
Bumps [setuptools](https://github.com/pypa/setuptools) from 77.0.1 to 77.0.3.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v77.0.1...v77.0.3)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-03-21 21:27:44 +01:00
dependabot[bot]
52b02d1235
Bump actions/upload-artifact from 4.6.1 to 4.6.2 (#5767)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.1...v4.6.2)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 21:07:23 +01:00
dependabot[bot]
84bc72d485
Bump ruff from 0.11.0 to 0.11.1 (#5771)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.0 to 0.11.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.0...0.11.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 21:01:54 +01:00
dependabot[bot]
bd772bb28a
Bump pylint from 3.3.4 to 3.3.6 (#5773)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.3.4 to 3.3.6.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.3.4...v3.3.6)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-21 20:59:17 +01:00
dependabot[bot]
fd2c7c3cc3
Bump getsentry/action-release from 3.1.0 to 3.1.1 (#5775) 2025-03-21 07:38:27 +01:00
dependabot[bot]
a7f139d3e1
Bump setuptools from 76.1.0 to 77.0.1 (#5766)
Bumps [setuptools](https://github.com/pypa/setuptools) from 76.1.0 to 77.0.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v76.1.0...v77.0.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 11:31:11 +01:00
dependabot[bot]
8a45e0fd85
Bump blockbuster from 1.5.23 to 1.5.24 (#5764)
Bumps [blockbuster](https://github.com/cbornet/blockbuster) from 1.5.23 to 1.5.24.
- [Release notes](https://github.com/cbornet/blockbuster/releases)
- [Commits](https://github.com/cbornet/blockbuster/commits)

---
updated-dependencies:
- dependency-name: blockbuster
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-20 11:08:57 +01:00
dependabot[bot]
52290b485b
Bump actions/cache from 4.2.2 to 4.2.3 (#5768) 2025-03-20 08:19:33 +01:00
dependabot[bot]
525d0fd8ea
Bump pre-commit from 4.1.0 to 4.2.0 (#5765) 2025-03-19 08:27:43 +01:00
dependabot[bot]
40c83f4c1e
Bump actions/download-artifact from 4.1.9 to 4.2.0 (#5763) 2025-03-19 07:34:25 +01:00
dependabot[bot]
99088ad880
Bump sentry-sdk from 2.22.0 to 2.23.1 (#5760)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.22.0 to 2.23.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.22.0...2.23.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 13:43:53 +01:00
dependabot[bot]
37c077205a
Bump setuptools from 76.0.0 to 76.1.0 (#5761)
Bumps [setuptools](https://github.com/pypa/setuptools) from 76.0.0 to 76.1.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v76.0.0...v76.1.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-18 12:01:03 +01:00
dependabot[bot]
ac5f9dcb59
Bump coverage from 7.6.12 to 7.7.0 (#5755) 2025-03-17 08:40:30 +01:00
dependabot[bot]
6a9269c052
Bump ruff from 0.10.0 to 0.11.0 (#5757) 2025-03-17 08:28:29 +01:00
dependabot[bot]
de615bfc1d
Bump docker/login-action from 3.3.0 to 3.4.0 (#5758) 2025-03-17 08:12:38 +01:00
dependabot[bot]
3ee639b133
Bump aiohttp from 3.11.13 to 3.11.14 (#5754)
---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-16 20:40:31 -10:00
dependabot[bot]
632e569347
Bump dbus-fast from 2.39.5 to 2.39.6 (#5756)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.39.5 to 2.39.6.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.39.5...v2.39.6)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-16 20:40:23 -10:00
Mike Degatano
cc74831113
Fix none error in mapping wireless fields (#5749) 2025-03-14 11:18:52 +01:00
dependabot[bot]
78c6868ad3
Bump dbus-fast from 2.39.3 to 2.39.5 (#5752)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.39.3 to 2.39.5.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.39.3...v2.39.5)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-14 10:06:44 +01:00
dependabot[bot]
f5f6e8b659
Bump ruff from 0.9.10 to 0.10.0 (#5751)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.10 to 0.10.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.10.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-14 09:50:13 +01:00
dependabot[bot]
c91a815cca
Bump attrs from 25.2.0 to 25.3.0 (#5750)
Bumps [attrs](https://github.com/sponsors/hynek) from 25.2.0 to 25.3.0.
- [Commits](https://github.com/sponsors/hynek/commits)

---
updated-dependencies:
- dependency-name: attrs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-14 09:45:04 +01:00
dependabot[bot]
1efe01c21f
Bump dbus-fast from 2.37.0 to 2.39.3 (#5736)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.37.0 to 2.39.3.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.37.0...v2.39.3)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 14:36:35 -04:00
dependabot[bot]
c54ff06e0f
Bump attrs from 25.1.0 to 25.2.0 (#5748)
Bumps [attrs](https://github.com/sponsors/hynek) from 25.1.0 to 25.2.0.
- [Commits](https://github.com/sponsors/hynek/commits)

---
updated-dependencies:
- dependency-name: attrs
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 16:41:48 +01:00
Mike Degatano
5facf4e790
Fix logging error for invalid password for backup (#5747)
* Fix logging error for invalid password for backup

* Improved test
2025-03-12 15:21:10 -04:00
Wendelin
34752466d5
Remove release assets tarball on frontend update (#5743)
* Remove release assets tarball on frontend update

* Fix path to the removed tarball

---------

Co-authored-by: Jan Čermák <sairon@users.noreply.github.com>
2025-03-12 10:10:27 +01:00
dependabot[bot]
20ea71f7ff
Bump astroid from 3.3.8 to 3.3.9 (#5742)
Bumps [astroid](https://github.com/pylint-dev/astroid) from 3.3.8 to 3.3.9.
- [Release notes](https://github.com/pylint-dev/astroid/releases)
- [Changelog](https://github.com/pylint-dev/astroid/blob/main/ChangeLog)
- [Commits](https://github.com/pylint-dev/astroid/compare/v3.3.8...v3.3.9)

---
updated-dependencies:
- dependency-name: astroid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 10:53:54 +01:00
dependabot[bot]
ac27e3ac0d
Bump ruff from 0.9.9 to 0.9.10 (#5741)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.9 to 0.9.10.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:15:24 +01:00
dependabot[bot]
b31e3ce234
Bump setuptools from 75.8.2 to 76.0.0 (#5740)
Bumps [setuptools](https://github.com/pypa/setuptools) from 75.8.2 to 76.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v75.8.2...v76.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-10 09:12:38 +01:00
Mike Degatano
e1c9c8b786
Finish out effort of adding and enabling blockbuster in tests (#5735)
* Finish out effort of adding and enabling blockbuster

* Skip getting addon file size until securetar fixed

* Fix test for devcontainer and blocking I/O

* Fix docker fixture and load_config to post_init
2025-03-07 13:29:24 +01:00
dependabot[bot]
23e03a95f4
Bump getsentry/action-release from 3.0.0 to 3.1.0 (#5737)
Bumps [getsentry/action-release](https://github.com/getsentry/action-release) from 3.0.0 to 3.1.0.
- [Release notes](https://github.com/getsentry/action-release/releases)
- [Changelog](https://github.com/getsentry/action-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/action-release/compare/v3.0.0...v3.1.0)

---
updated-dependencies:
- dependency-name: getsentry/action-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-07 13:12:33 +01:00
Stefan Agner
a2b8df0a6a
Use Sentry helper function to report warnings (#5734)
* Use Sentry helper function to report warnings

Don't use Sentry directly but the existing helper function.

* Add pytest that Sentry is by default off

* Address ruff

* Address ruff
2025-03-06 23:45:48 +01:00
Mike Degatano
6ef4f3cc67
Add blockbuster library and find I/O from unit tests (#5731)
* Add blockbuster library and find I/O from unit tests

* Fix lint and test issue

* Fixes from feedback

* Avoid modifying webapp object in executor

* Split su options validation and only validate timezone on change
2025-03-06 16:40:13 -05:00
dependabot[bot]
1fb4d1cc11
Bump dbus-fast from 2.35.1 to 2.37.0 (#5733)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.35.1 to 2.37.0.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.35.1...v2.37.0)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-06 12:21:13 +01:00
dependabot[bot]
65b1729314
Bump jinja2 from 3.1.5 to 3.1.6 (#5732)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.5 to 3.1.6.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.5...3.1.6)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-06 09:44:59 +01:00
Stefan Agner
c7e3d86e2d
Revert "Enable Sentry asyncio integration (#5685)" (#5729)
This essentially reverts PR #5685.

The Sentry `AsyncioIntegration` replaces the asyncio task factory with
its instrumentalized version, which reports all execeptions which
aren't handled *within* a task to Sentry.

However, we quite often run tasks and handle exceptions outside, e.g.
this commen pattern (example from `MountManager` `reload()``):

```python
results = await asyncio.gather(
    *[mount.update() for mount in mounts], return_exceptions=True
)
... create resolution issues from results with exceptions ...
```

Here, asyncio.gather() uses ensure_future(), which converts the
co-routines to tasks. These Sentry instrumented tasks will then report
exceptions to Sentry, even though we handle exceptions gracefully.

So the `AsyncioIntegration` doesn't work for our use case, and causes
unnecessary noise in Sentry. Disable it again.
2025-03-05 12:31:30 +01:00
dependabot[bot]
5d06ebe430
Bump dbus-fast from 2.34.0 to 2.35.1 (#5728)
Bumps [dbus-fast](https://github.com/bluetooth-devices/dbus-fast) from 2.34.0 to 2.35.1.
- [Release notes](https://github.com/bluetooth-devices/dbus-fast/releases)
- [Changelog](https://github.com/Bluetooth-Devices/dbus-fast/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluetooth-devices/dbus-fast/compare/v2.34.0...v2.35.1)

---
updated-dependencies:
- dependency-name: dbus-fast
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:06:01 +01:00
dependabot[bot]
5aba616ba4
Bump debugpy from 1.8.12 to 1.8.13 (#5727)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.12 to 1.8.13.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.12...v1.8.13)

---
updated-dependencies:
- dependency-name: debugpy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:05:52 +01:00
Mike Degatano
767f435090
Call core post init (#5725) 2025-03-05 09:05:23 +01:00
Stefan Agner
26024053ed
Fix add-on store repository getting removed without internet (#5717)
* Fix add-on store repository getting removed without internet

Currently, when a git command error happens in `pull()`, we declare
the repository as corrupt. Subsequent system autofix runs then execute
the reset resolution, which essentially removes the git repository from
the system.

In situations where the Internet fails right between the last
Supervisor connectivity check and the add-on store repository update
(the connectivity checks are throttled to once every 10 minutes while
connectivity is considered good), or if the outage is only partial
(e.g. reaching connectivity check works but the store repository is not
reachable), this leads to a git command error which declares the
repository as corrupt just as well, and ultimately leads to the removal
of the add-on store repository from the local system.

Run a git ls-remote first, which is used as an extra connectivity check.
This will also avoid removing the repository if Internet connectivity
works but the git provider is temporary down or not reachable.

That said, it will also fail if the repository is no longer present.
But this case needs extra handling anyways.

* Run git ls-remote in executor
2025-03-04 21:09:46 +01:00
Mike Degatano
324b059970
Move write of core state to executor (#5720) 2025-03-04 17:49:53 +01:00
Stefan Agner
76e916a07e
Make sure to close file stream after backup upload (#5723)
* Make sure to close file stream after backup upload

Currently the file stream does not get closed before importing the file
stream. It seems the test case didn't catch that, presumably because
it is a race condition if the bytes get flushed to disk or not.

Properly close the stream before continue handling the file.

* Close file stream in executor

* Add comment about closing twice is fine
2025-03-04 16:11:25 +01:00
Mike Degatano
582b128ad9
Finish migrating read_text to executor (#5698)
* Move read_text to executor

* switch to async_capture_exception

* Finish moving read_text to executor

* Cover read_bytes and some write_text calls as well

* Fix await issues

* Fix format_message
2025-03-04 11:45:44 +01:00
dependabot[bot]
c01d788c4c
Bump getsentry/action-release from 1.10.4 to 3.0.0 (#5721)
Bumps [getsentry/action-release](https://github.com/getsentry/action-release) from 1.10.4 to 3.0.0.
- [Release notes](https://github.com/getsentry/action-release/releases)
- [Changelog](https://github.com/getsentry/action-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/action-release/compare/v1.10.4...v3.0.0)

---
updated-dependencies:
- dependency-name: getsentry/action-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-04 11:44:44 +01:00
Jan Čermák
8fb66bcf18
Stop streaming logs when client closes connection (#5722)
When connection is closed by the client, the journal_logs_reader
generator still returns new lines, trying to write each one of them to
the closed transport. With debug logging enabled, this can end up in an
endless loop. To fix that, break out of the loop immediately after the
connection is reset.
2025-03-04 10:08:44 +01:00
Stefan Agner
fdd96ae21c
Fix add-on repair log message (#5719) 2025-03-03 18:49:24 -05:00
Stefan Agner
1355ef192d
Exclude non-Supervisor Server Error codes from Sentry reporting (#5718)
* Exclude non-Supervisor Server Error codes from Sentry reporting

Exclude status codes 502 Bad Gateway and 503 Service Unavailable from
Sentry aiohttp integration. These are returned by Supervisor itself
when acting as a proxy for Home Assistant Core. These aren't errors of
Supervisor.

* ruff check
2025-03-03 23:10:59 +01:00
Stefan Agner
f8bab20728
Replace non-unicode characters for add-on static files (#5712)
* Replace non-unicode characters for add-on static files

Add-on documentation and changelog get read and returned as text file.
However, in case the original author used non-unicode characters, or
the file corrupted, loading currently fails with an UnicodeDecodeError.

Let's just use the built-in replace error handling of Python, so they
appear for the user as  non-unicode characters by replacing them with
the official unicode replacement character "�".

* Remove superflous parameter for binary files

* ruff format

* Add pytests
2025-03-03 20:14:39 +01:00
Stefan Agner
9a3702bc1a
Avoid loading add-on store repositories too early (#5716)
When initially loading the store manager, update_repositories makes
sure that all repositories are actually present. If they are for some
reason corrupted or content is missing, we currently still trying
to load them which leads to an unnecessary warning:

```
2025-03-03 11:55:54.324 WARNING (SyncWorker_1) [supervisor.store.data] No repository information exists at /data/addons/git/a0d7b954
...
2025-03-03 11:55:54.343 INFO (MainThread) [supervisor.store.git] Cloning add-on https://github.com/hassio-addons/repository repository
```

Since update_repositories always loads the data, simply remove the
superfluous earlier loading attempt.

While at it, also improve the cloning/update log messages to make it
clear what repository is cloned/updated.
2025-03-03 19:01:47 +01:00
Jan Čermák
a7c6699f6a
Only log all ClientConnectionResets when returning logs (#5715)
* Suppress all ClientConnectionReset when returning logs

In #5358 we started suppressing ClientConnectionReset when logs are
returned from the Journal Gateway and the client ends connection
unexpectedly. The connection can be closed also when the headers are
returned, so ignore also that error.

Refs #5606

* Log ClientConnectionResetError as DEBUG instead of suppressing it
2025-03-03 13:51:40 +01:00
dependabot[bot]
fa7626f83a
Bump cryptography from 44.0.1 to 44.0.2 (#5709)
Bumps [cryptography](https://github.com/pyca/cryptography) from 44.0.1 to 44.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/44.0.1...44.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 13:48:42 +01:00
dependabot[bot]
84b265a2e0
Bump pytest from 8.3.4 to 8.3.5 (#5710) 2025-03-03 08:25:11 +01:00
dependabot[bot]
debcafa962
Bump ruff from 0.9.8 to 0.9.9 (#5711) 2025-03-03 08:13:55 +01:00
dependabot[bot]
4634ef82c6
Bump home-assistant/wheels from 2024.11.0 to 2025.02.0 (#5708) 2025-03-03 07:15:55 +01:00
4041 changed files with 12955 additions and 7179 deletions

View File

@ -1,69 +0,0 @@
---
name: Report a bug with the Supervisor on a supported System
about: Report an issue related to the Home Assistant Supervisor.
labels: bug
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests
- Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template!
- If you have a problem with an add-on, make an issue in it's repository.
-->
<!--
Important: You can only fill a bug repport for an supported system! If you run an unsupported installation. This report would be closed without comment.
-->
### Describe the issue
<!-- Provide as many details as possible. -->
### Steps to reproduce
<!-- What do you do to encounter the issue. -->
1. ...
2. ...
3. ...
### Enviroment details
<!-- You can find these details in the system tab of the supervisor panel, or by using the `ha` CLI. -->
- **Operating System:**: xxx
- **Supervisor version:**: xxx
- **Home Assistant version**: xxx
### Supervisor logs
<details>
<summary>Supervisor logs</summary>
<!--
- Frontend -> Supervisor -> System
- Or use this command: ha supervisor logs
- Logs are more than just errors, even if you don't think it's important, it is.
-->
```
Paste supervisor logs here
```
</details>
### System Information
<details>
<summary>System Information</summary>
<!--
- Use this command: ha info
-->
```
Paste system info here
```
</details>

View File

@ -1,6 +1,5 @@
name: Bug Report Form name: Report an issue with Home Assistant Supervisor
description: Report an issue related to the Home Assistant Supervisor. description: Report an issue related to the Home Assistant Supervisor.
labels: bug
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -9,7 +8,7 @@ body:
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr]. If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
[fr]: https://community.home-assistant.io/c/feature-requests [fr]: https://github.com/orgs/home-assistant/discussions
- type: textarea - type: textarea
validations: validations:
required: true required: true

View File

@ -13,7 +13,7 @@ contact_links:
about: Our documentation has its own issue tracker. Please report issues with the website there. about: Our documentation has its own issue tracker. Please report issues with the website there.
- name: Request a feature for the Supervisor - name: Request a feature for the Supervisor
url: https://community.home-assistant.io/c/feature-requests url: https://github.com/orgs/home-assistant/discussions
about: Request an new feature for the Supervisor. about: Request an new feature for the Supervisor.
- name: I have a question or need support - name: I have a question or need support

53
.github/ISSUE_TEMPLATE/task.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Task
description: For staff only - Create a task
type: Task
body:
- type: markdown
attributes:
value: |
## ⚠️ RESTRICTED ACCESS
**This form is restricted to Open Home Foundation staff and authorized contributors only.**
If you are a community member wanting to contribute, please:
- For bug reports: Use the [bug report form](https://github.com/home-assistant/supervisor/issues/new?template=bug_report.yml)
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
---
### For authorized contributors
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
- type: textarea
id: description
attributes:
label: Description
description: |
Provide a clear and detailed description of the task that needs to be accomplished.
Be specific about what needs to be done, why it's important, and any constraints or requirements.
placeholder: |
Describe the task, including:
- What needs to be done
- Why this task is needed
- Expected outcome
- Any constraints or requirements
validations:
required: true
- type: textarea
id: additional_context
attributes:
label: Additional context
description: |
Any additional information, links, research, or context that would be helpful.
Include links to related issues, research, prototypes, roadmap opportunities etc.
placeholder: |
- Roadmap opportunity: [link]
- Epic: [link]
- Feature request: [link]
- Technical design documents: [link]
- Prototype/mockup: [link]
- Dependencies: [links]
validations:
required: false

288
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,288 @@
# GitHub Copilot & Claude Code Instructions
This repository contains the Home Assistant Supervisor, a Python 3 based container
orchestration and management system for Home Assistant.
## Supervisor Capabilities & Features
### Architecture Overview
Home Assistant Supervisor is a Python-based container orchestration system that
communicates with the Docker daemon to manage containerized components. It is tightly
integrated with the underlying Operating System and core Operating System components
through D-Bus.
**Managed Components:**
- **Home Assistant Core**: The main home automation application running in its own
container (also provides the web interface)
- **Add-ons**: Third-party applications and services (each add-on runs in its own
container)
- **Plugins**: Built-in system services like DNS, Audio, CLI, Multicast, and Observer
- **Host System Integration**: OS-level operations and hardware access via D-Bus
- **Container Networking**: Internal Docker network management and external
connectivity
- **Storage & Backup**: Data persistence and backup management across all containers
**Key Dependencies:**
- **Docker Engine**: Required for all container operations
- **D-Bus**: System-level communication with the host OS
- **systemd**: Service management for host system operations
- **NetworkManager**: Network configuration and management
### Add-on System
**Add-on Architecture**: Add-ons are containerized applications available through
add-on stores. Each store contains multiple add-ons, and each add-on includes metadata
that tells Supervisor the version, startup configuration (permissions), and available
user configurable options. Add-on metadata typically references a container image that
Supervisor fetches during installation. If not, the Supervisor builds the container
image from a Dockerfile.
**Built-in Stores**: Supervisor comes with several pre-configured stores:
- **Core Add-ons**: Official add-ons maintained by the Home Assistant team
- **Community Add-ons**: Popular third-party add-ons repository
- **ESPHome**: Add-ons for ESPHome ecosystem integration
- **Music Assistant**: Audio and music-related add-ons
- **Local Development**: Local folder for testing custom add-ons during development
**Store Management**: Stores are Git-based repositories that are periodically updated.
When updates are available, users receive notifications.
**Add-on Lifecycle**:
- **Installation**: Supervisor fetches or builds container images based on add-on
metadata
- **Configuration**: Schema-validated options with integrated UI management
- **Runtime**: Full container lifecycle management, health monitoring
- **Updates**: Automatic or manual version management
### Update System
**Core Components**: Supervisor, Home Assistant Core, HAOS, and built-in plugins
receive version information from a central JSON file fetched from
`https://version.home-assistant.io/{channel}.json`. The `Updater` class handles
fetching this data, validating signatures, and updating internal version tracking.
**Update Channels**: Three channels (`stable`/`beta`/`dev`) determine which version
JSON file is fetched, allowing users to opt into different release streams.
**Add-on Updates**: Add-on version information comes from store repository updates, not
the central JSON file. When repositories are refreshed via the store system, add-ons
compare their local versions against repository versions to determine update
availability.
### Backup & Recovery System
**Backup Capabilities**:
- **Full Backups**: Complete system state capture including all add-ons,
configuration, and data
- **Partial Backups**: Selective backup of specific components (Home Assistant,
add-ons, folders)
- **Encrypted Backups**: Optional backup encryption with user-provided passwords
- **Multiple Storage Locations**: Local storage and remote backup destinations
**Recovery Features**:
- **One-click Restore**: Simple restoration from backup files
- **Selective Restore**: Choose specific components to restore
- **Automatic Recovery**: Self-healing for common system issues
---
## Supervisor Development
### Python Requirements
- **Compatibility**: Python 3.13+
- **Language Features**: Use modern Python features:
- Type hints with `typing` module
- f-strings (preferred over `%` or `.format()`)
- Dataclasses and enum classes
- Async/await patterns
- Pattern matching where appropriate
### Code Quality Standards
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Testing**: pytest with asyncio support
- **Language**: American English for all code, comments, and documentation
### Code Organization
**Core Structure**:
```
supervisor/
├── __init__.py # Package initialization
├── const.py # Constants and enums
├── coresys.py # Core system management
├── bootstrap.py # System initialization
├── exceptions.py # Custom exception classes
├── api/ # REST API endpoints
├── addons/ # Add-on management
├── backups/ # Backup system
├── docker/ # Docker integration
├── host/ # Host system interface
├── homeassistant/ # Home Assistant Core management
├── dbus/ # D-Bus system integration
├── hardware/ # Hardware detection and management
├── plugins/ # Plugin system
├── resolution/ # Issue detection and resolution
├── security/ # Security management
├── services/ # Service discovery and management
├── store/ # Add-on store management
└── utils/ # Utility functions
```
**Shared Constants**: Use constants from `supervisor/const.py` instead of hardcoding
values. Define new constants following existing patterns and group related constants
together.
### Supervisor Architecture Patterns
**CoreSysAttributes Inheritance Pattern**: Nearly all major classes in Supervisor
inherit from `CoreSysAttributes`, providing access to the centralized system state
via `self.coresys` and convenient `sys_*` properties.
```python
# Standard Supervisor class pattern
class MyManager(CoreSysAttributes):
"""Manage my functionality."""
def __init__(self, coresys: CoreSys):
"""Initialize manager."""
self.coresys: CoreSys = coresys
self._component: MyComponent = MyComponent(coresys)
@property
def component(self) -> MyComponent:
"""Return component handler."""
return self._component
# Access system components via inherited properties
async def do_something(self):
await self.sys_docker.containers.get("my_container")
self.sys_bus.fire_event(BusEvent.MY_EVENT, {"data": "value"})
```
**Key Inherited Properties from CoreSysAttributes**:
- `self.sys_docker` - Docker API access
- `self.sys_run_in_executor()` - Execute blocking operations
- `self.sys_create_task()` - Create async tasks
- `self.sys_bus` - Event bus for system events
- `self.sys_config` - System configuration
- `self.sys_homeassistant` - Home Assistant Core management
- `self.sys_addons` - Add-on management
- `self.sys_host` - Host system access
- `self.sys_dbus` - D-Bus system interface
**Load Pattern**: Many components implement a `load()` method which effectively
initialize the component from external sources (containers, files, D-Bus services).
### API Development
**REST API Structure**:
- **Base Path**: `/api/` for all endpoints
- **Authentication**: Bearer token authentication
- **Consistent Response Format**: `{"result": "ok", "data": {...}}` or
`{"result": "error", "message": "..."}`
- **Validation**: Use voluptuous schemas with `api_validate()`
**Use `@api_process` Decorator**: This decorator handles all standard error handling
and response formatting automatically. The decorator catches `APIError`, `HassioError`,
and other exceptions, returning appropriate HTTP responses.
```python
from ..api.utils import api_process, api_validate
@api_process
async def backup_full(self, request: web.Request) -> dict[str, Any]:
"""Create full backup."""
body = await api_validate(SCHEMA_BACKUP_FULL, request)
job = await self.sys_backups.do_backup_full(**body)
return {ATTR_JOB_ID: job.uuid}
```
### Docker Integration
- **Container Management**: Use Supervisor's Docker manager instead of direct
Docker API
- **Networking**: Supervisor manages internal Docker networks with predefined IP
ranges
- **Security**: AppArmor profiles, capability restrictions, and user namespace
isolation
- **Health Checks**: Implement health monitoring for all managed containers
### D-Bus Integration
- **Use dbus-fast**: Async D-Bus library for system integration
- **Service Management**: systemd, NetworkManager, hostname management
- **Error Handling**: Wrap D-Bus exceptions in Supervisor-specific exceptions
### Async Programming
- **All I/O operations must be async**: File operations, network calls, subprocess
execution
- **Use asyncio patterns**: Prefer `asyncio.gather()` over sequential awaits
- **Executor jobs**: Use `self.sys_run_in_executor()` for blocking operations
- **Two-phase initialization**: `__init__` for sync setup, `post_init()` for async
initialization
### Testing
- **Location**: `tests/` directory with module mirroring
- **Fixtures**: Extensive use of pytest fixtures for CoreSys setup
- **Mocking**: Mock external dependencies (Docker, D-Bus, network calls)
- **Coverage**: Minimum 90% test coverage, 100% for security-sensitive code
### Error Handling
- **Custom Exceptions**: Defined in `exceptions.py` with clear inheritance hierarchy
- **Error Propagation**: Use `from` clause for exception chaining
- **API Errors**: Use `APIError` with appropriate HTTP status codes
### Security Considerations
- **Container Security**: AppArmor profiles mandatory for add-ons, minimal
capabilities
- **Authentication**: Token-based API authentication with role-based access
- **Data Protection**: Backup encryption, secure secret management, comprehensive
input validation
### Development Commands
```bash
# Run tests, adjust paths as necessary
pytest -qsx tests/
# Linting and formatting
ruff check supervisor/
ruff format supervisor/
# Type checking
mypy --ignore-missing-imports supervisor/
# Pre-commit hooks
pre-commit run --all-files
```
Always run the pre-commit hooks at the end of code editing.
### Common Patterns to Follow
**✅ Use These Patterns**:
- Inherit from `CoreSysAttributes` for system access
- Use `@api_process` decorator for API endpoints
- Use `self.sys_run_in_executor()` for blocking operations
- Access Docker via `self.sys_docker` not direct Docker API
- Use constants from `const.py` instead of hardcoding
- Store types in (per-module) `const.py` (e.g. supervisor/store/const.py)
**❌ Avoid These Patterns**:
- Direct Docker API usage - use Supervisor's Docker manager
- Blocking operations in async context (use asyncio alternatives)
- Hardcoded values - use constants from `const.py`
- Manual error handling in API endpoints - let `@api_process` handle it
This guide provides the foundation for contributing to Home Assistant Supervisor.
Follow these patterns and guidelines to ensure code quality, security, and
maintainability.

View File

@ -106,7 +106,7 @@ jobs:
- name: Build wheels - name: Build wheels
if: needs.init.outputs.requirements == 'true' if: needs.init.outputs.requirements == 'true'
uses: home-assistant/wheels@2024.11.0 uses: home-assistant/wheels@2025.03.0
with: with:
abi: cp313 abi: cp313
tag: musllinux_1_2 tag: musllinux_1_2
@ -125,15 +125,15 @@ 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.4.0 uses: actions/setup-python@v5.6.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.8.1 uses: sigstore/cosign-installer@v3.9.2
with: with:
cosign-release: "v2.4.0" cosign-release: "v2.4.3"
- name: Install dirhash and calc hash - name: Install dirhash and calc hash
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
@ -149,7 +149,7 @@ jobs:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true' if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v3.3.0 uses: docker/login-action@v3.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -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@2025.02.0 uses: home-assistant/builder@2025.03.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@2025.02.0 uses: home-assistant/builder@2025.03.0
with: with:
args: | args: |
--test \ --test \

View File

@ -10,6 +10,7 @@ on:
env: env:
DEFAULT_PYTHON: "3.13" DEFAULT_PYTHON: "3.13"
PRE_COMMIT_CACHE: ~/.cache/pre-commit PRE_COMMIT_CACHE: ~/.cache/pre-commit
MYPY_CACHE_VERSION: 1
concurrency: concurrency:
group: "${{ github.workflow }}-${{ github.ref }}" group: "${{ github.workflow }}-${{ github.ref }}"
@ -28,12 +29,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -47,7 +48,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.2 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true lookup-only: true
@ -69,13 +70,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -87,7 +88,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.2 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@ -112,13 +113,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -130,7 +131,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.2 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@ -170,13 +171,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -188,7 +189,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.2 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@ -214,13 +215,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -232,7 +233,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.2 uses: actions/cache@v4.2.3
with: with:
path: ${{ env.PRE_COMMIT_CACHE }} path: ${{ env.PRE_COMMIT_CACHE }}
key: | key: |
@ -258,13 +259,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -286,6 +287,52 @@ jobs:
. venv/bin/activate . venv/bin/activate
pylint supervisor tests pylint supervisor tests
mypy:
name: Check mypy
runs-on: ubuntu-latest
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.6.0
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
- name: Generate partial mypy restore key
id: generate-mypy-key
run: |
mypy_version=$(cat requirements_test.txt | grep mypy | cut -d '=' -f 3)
echo "version=$mypy_version" >> $GITHUB_OUTPUT
echo "key=mypy-${{ env.MYPY_CACHE_VERSION }}-$mypy_version-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
with:
path: venv
key: >-
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_tests.txt') }}
- name: Fail job if Python cache restore failed
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
echo "Failed to restore Python virtual environment from cache"
exit 1
- name: Restore mypy cache
uses: actions/cache@v4.2.3
with:
path: .mypy_cache
key: >-
${{ runner.os }}-mypy-${{ needs.prepare.outputs.python-version }}-${{ steps.generate-mypy-key.outputs.key }}
restore-keys: >-
${{ runner.os }}-venv-${{ needs.prepare.outputs.python-version }}-mypy-${{ env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}
- name: Register mypy problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/mypy.json"
- name: Run mypy
run: |
. venv/bin/activate
mypy --ignore-missing-imports supervisor
pytest: pytest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: prepare needs: prepare
@ -294,17 +341,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.4.0 uses: actions/setup-python@v5.6.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.8.1 uses: sigstore/cosign-installer@v3.9.2
with: with:
cosign-release: "v2.4.0" cosign-release: "v2.4.3"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@v4.2.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -339,7 +386,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.6.1 uses: actions/upload-artifact@v4.6.2
with: with:
name: coverage-${{ matrix.python-version }} name: coverage-${{ matrix.python-version }}
path: .coverage path: .coverage
@ -353,13 +400,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.4.0 uses: actions/setup-python@v5.6.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.2 uses: actions/cache@v4.2.3
with: with:
path: venv path: venv
key: | key: |
@ -370,7 +417,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.9 uses: actions/download-artifact@v4.3.0
- name: Combine coverage results - name: Combine coverage results
run: | run: |
. venv/bin/activate . venv/bin/activate
@ -378,4 +425,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.4.0 uses: codecov/codecov-action@v5.4.3

16
.github/workflows/matchers/mypy.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"problemMatcher": [
{
"owner": "mypy",
"pattern": [
{
"regexp": "^(.+):(\\d+):\\s(error|warning):\\s(.+)$",
"file": 1,
"line": 2,
"severity": 3,
"message": 4
}
]
}
]
}

View File

@ -0,0 +1,58 @@
name: Restrict task creation
# yamllint disable-line rule:truthy
on:
issues:
types: [opened]
jobs:
check-authorization:
runs-on: ubuntu-latest
# Only run if this is a Task issue type (from the issue form)
if: github.event.issue.issue_type == 'Task'
steps:
- name: Check if user is authorized
uses: actions/github-script@v7
with:
script: |
const issueAuthor = context.payload.issue.user.login;
// Check if user is an organization member
try {
await github.rest.orgs.checkMembershipForUser({
org: 'home-assistant',
username: issueAuthor
});
console.log(`✅ ${issueAuthor} is an organization member`);
return; // Authorized
} catch (error) {
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
}
// Close the issue with a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
`If you would like to:\n` +
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/supervisor/issues/new?template=bug_report.yml)\n` +
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
`If you believe you should have access to create Task issues, please contact the maintainers.`
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
state: 'closed'
});
// Add a label to indicate this was auto-closed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['auto-closed']
});

View File

@ -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.10.4 uses: getsentry/action-release@v3.2.0
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 }}

View File

@ -64,6 +64,9 @@ jobs:
fileName: home_assistant_frontend_supervisor-${{ needs.check-version.outputs.latest_version }}.tar.gz fileName: home_assistant_frontend_supervisor-${{ needs.check-version.outputs.latest_version }}.tar.gz
extract: true extract: true
out-file-path: supervisor/api/panel/ out-file-path: supervisor/api/panel/
- name: Remove release assets archive
run: |
rm -f supervisor/api/panel/home_assistant_frontend_supervisor-*.tar.gz
- name: Create PR - name: Create PR
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
with: with:

View File

@ -1 +1 @@
20250221.0 20250401.0

View File

@ -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.9.1 rev: v0.11.10
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -13,3 +13,15 @@ repos:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
stages: [manual] stages: [manual]
- id: check-json - id: check-json
- repo: local
hooks:
# Run mypy through our wrapper script in order to get the possible
# pyenv and/or virtualenv activated; it may not have been e.g. if
# committing from a GUI tool that was not launched from an activated
# shell.
- id: mypy
name: mypy
entry: script/run-in-env.sh mypy --ignore-missing-imports
language: script
types_or: [python, pyi]
files: ^supervisor/.+\.(py|pyi)$

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
.github/copilot-instructions.md

View File

@ -29,7 +29,7 @@ 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.6.1 && pip3 install uv==0.6.17
# Install requirements # Install requirements
COPY requirements.txt . COPY requirements.txt .

View File

@ -12,7 +12,7 @@ cosign:
base_identity: https://github.com/home-assistant/docker-base/.* base_identity: https://github.com/home-assistant/docker-base/.*
identity: https://github.com/home-assistant/supervisor/.* identity: https://github.com/home-assistant/supervisor/.*
args: args:
COSIGN_VERSION: 2.4.0 COSIGN_VERSION: 2.4.3
labels: labels:
io.hass.type: supervisor io.hass.type: supervisor
org.opencontainers.image.title: Home Assistant Supervisor org.opencontainers.image.title: Home Assistant Supervisor

View File

@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["setuptools~=75.8.0", "wheel~=0.45.0"] requires = ["setuptools~=80.9.0", "wheel~=0.46.1"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
@ -230,6 +230,9 @@ filterwarnings = [
"ignore:pkg_resources is deprecated as an API:DeprecationWarning:dirhash", "ignore:pkg_resources is deprecated as an API:DeprecationWarning:dirhash",
"ignore::pytest.PytestUnraisableExceptionWarning", "ignore::pytest.PytestUnraisableExceptionWarning",
] ]
markers = [
"no_mock_init_websession: disable the autouse mock of init_websession for this test",
]
[tool.ruff] [tool.ruff]
lint.select = [ lint.select = [
@ -272,7 +275,6 @@ lint.select = [
"S317", # suspicious-xml-sax-usage "S317", # suspicious-xml-sax-usage
"S318", # suspicious-xml-mini-dom-usage "S318", # suspicious-xml-mini-dom-usage
"S319", # suspicious-xml-pull-dom-usage "S319", # suspicious-xml-pull-dom-usage
"S320", # suspicious-xmle-tree-usage
"S601", # paramiko-call "S601", # paramiko-call
"S602", # subprocess-popen-with-shell-equals-true "S602", # subprocess-popen-with-shell-equals-true
"S604", # call-with-shell-equals-true "S604", # call-with-shell-equals-true

View File

@ -1,29 +1,30 @@
aiodns==3.2.0 aiodns==3.5.0
aiohttp==3.11.13 aiohttp==3.12.14
atomicwrites-homeassistant==1.4.1 atomicwrites-homeassistant==1.4.1
attrs==25.1.0 attrs==25.3.0
awesomeversion==24.6.0 awesomeversion==25.5.0
blockbuster==1.5.25
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.1 cryptography==45.0.5
debugpy==1.8.12 debugpy==1.8.15
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.44 gitpython==3.1.44
jinja2==3.1.5 jinja2==3.1.6
orjson==3.10.12 log-rate-limit==1.4.2
orjson==3.11.0
pulsectl==24.12.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.4
securetar==2025.2.1 securetar==2025.2.1
sentry-sdk==2.22.0 sentry-sdk==2.33.2
setuptools==75.8.2 setuptools==80.9.0
voluptuous==0.15.2 voluptuous==0.15.2
dbus-fast==2.34.0 dbus-fast==2.44.2
typing_extensions==4.12.2
zlib-fast==0.2.1 zlib-fast==0.2.1

View File

@ -1,13 +1,16 @@
astroid==3.3.8 astroid==3.3.11
coverage==7.6.12 coverage==7.10.0
pre-commit==4.1.0 mypy==1.17.0
pylint==3.3.4 pre-commit==4.2.0
pylint==3.3.7
pytest-aiohttp==1.1.0 pytest-aiohttp==1.1.0
pytest-asyncio==0.25.2 pytest-asyncio==0.25.2
pytest-cov==6.0.0 pytest-cov==6.2.1
pytest-timeout==2.3.1 pytest-timeout==2.4.0
pytest==8.3.4 pytest==8.4.1
ruff==0.9.8 ruff==0.12.4
time-machine==2.16.0 time-machine==2.16.0
typing_extensions==4.12.2 types-docker==7.1.0.20250705
urllib3==2.3.0 types-pyyaml==6.0.12.20250516
types-requests==2.32.4.20250611
urllib3==2.5.0

30
script/run-in-env.sh Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env sh
set -eu
# Used in venv activate script.
# Would be an error if undefined.
OSTYPE="${OSTYPE-}"
# Activate pyenv and virtualenv if present, then run the specified command
# pyenv, pyenv-virtualenv
if [ -s .python-version ]; then
PYENV_VERSION=$(head -n 1 .python-version)
export PYENV_VERSION
fi
if [ -n "${VIRTUAL_ENV-}" ] && [ -f "${VIRTUAL_ENV}/bin/activate" ]; then
. "${VIRTUAL_ENV}/bin/activate"
else
# other common virtualenvs
my_path=$(git rev-parse --show-toplevel)
for venv in venv .venv .; do
if [ -f "${my_path}/${venv}/bin/activate" ]; then
. "${my_path}/${venv}/bin/activate"
break
fi
done
fi
exec "$@"

View File

@ -11,10 +11,12 @@ import zlib_fast
# Enable fast zlib before importing supervisor # Enable fast zlib before importing supervisor
zlib_fast.enable() zlib_fast.enable()
from supervisor import bootstrap # pylint: disable=wrong-import-position # noqa: E402 # pylint: disable=wrong-import-position
from supervisor.utils.logging import ( # pylint: disable=wrong-import-position # noqa: E402 from supervisor import bootstrap # noqa: E402
activate_log_queue_handler, from supervisor.utils.blockbuster import BlockBusterManager # noqa: E402
) from supervisor.utils.logging import activate_log_queue_handler # noqa: E402
# pylint: enable=wrong-import-position
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -52,6 +54,8 @@ if __name__ == "__main__":
_LOGGER.info("Initializing Supervisor setup") _LOGGER.info("Initializing Supervisor setup")
coresys = loop.run_until_complete(bootstrap.initialize_coresys()) coresys = loop.run_until_complete(bootstrap.initialize_coresys())
loop.set_debug(coresys.config.debug) loop.set_debug(coresys.config.debug)
if coresys.config.detect_blocking_io:
BlockBusterManager.activate()
loop.run_until_complete(coresys.core.connect()) loop.run_until_complete(coresys.core.connect())
loop.run_until_complete(bootstrap.supervisor_debugger(coresys)) loop.run_until_complete(bootstrap.supervisor_debugger(coresys))
@ -62,8 +66,15 @@ if __name__ == "__main__":
_LOGGER.info("Setting up Supervisor") _LOGGER.info("Setting up Supervisor")
loop.run_until_complete(coresys.core.setup()) loop.run_until_complete(coresys.core.setup())
loop.call_soon_threadsafe(loop.create_task, coresys.core.start()) bootstrap.register_signal_handlers(loop, coresys)
loop.call_soon_threadsafe(bootstrap.reg_signal, loop, coresys)
try:
loop.run_until_complete(coresys.core.start())
except Exception as err: # pylint: disable=broad-except
# Supervisor itself is running at this point, just something didn't
# start as expected. Log with traceback to get more insights for
# such cases.
_LOGGER.critical("Supervisor start failed: %s", err, exc_info=True)
try: try:
_LOGGER.info("Running Supervisor") _LOGGER.info("Running Supervisor")

View File

@ -18,7 +18,7 @@ from tempfile import TemporaryDirectory
from typing import Any, Final from typing import Any, Final
import aiohttp import aiohttp
from awesomeversion import AwesomeVersionCompareException from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
from deepmerge import Merger from deepmerge import Merger
from securetar import AddFileError, atomic_contents_add, secure_path from securetar import AddFileError, atomic_contents_add, secure_path
import voluptuous as vol import voluptuous as vol
@ -33,8 +33,6 @@ from ..const import (
ATTR_AUDIO_OUTPUT, ATTR_AUDIO_OUTPUT,
ATTR_AUTO_UPDATE, ATTR_AUTO_UPDATE,
ATTR_BOOT, ATTR_BOOT,
ATTR_DATA,
ATTR_EVENT,
ATTR_IMAGE, ATTR_IMAGE,
ATTR_INGRESS_ENTRY, ATTR_INGRESS_ENTRY,
ATTR_INGRESS_PANEL, ATTR_INGRESS_PANEL,
@ -50,7 +48,6 @@ from ..const import (
ATTR_SYSTEM, ATTR_SYSTEM,
ATTR_SYSTEM_MANAGED, ATTR_SYSTEM_MANAGED,
ATTR_SYSTEM_MANAGED_CONFIG_ENTRY, ATTR_SYSTEM_MANAGED_CONFIG_ENTRY,
ATTR_TYPE,
ATTR_USER, ATTR_USER,
ATTR_UUID, ATTR_UUID,
ATTR_VERSION, ATTR_VERSION,
@ -79,7 +76,7 @@ from ..exceptions import (
HostAppArmorError, HostAppArmorError,
) )
from ..hardware.data import Device from ..hardware.data import Device
from ..homeassistant.const import WSEvent, WSType from ..homeassistant.const import WSEvent
from ..jobs.const import JobExecutionLimit from ..jobs.const import JobExecutionLimit
from ..jobs.decorator import Job from ..jobs.decorator import Job
from ..resolution.const import ContextType, IssueType, UnhealthyReason from ..resolution.const import ContextType, IssueType, UnhealthyReason
@ -140,9 +137,7 @@ class Addon(AddonModel):
super().__init__(coresys, slug) super().__init__(coresys, slug)
self.instance: DockerAddon = DockerAddon(coresys, self) self.instance: DockerAddon = DockerAddon(coresys, self)
self._state: AddonState = AddonState.UNKNOWN self._state: AddonState = AddonState.UNKNOWN
self._manual_stop: bool = ( self._manual_stop: bool = False
self.sys_hardware.helper.last_boot != self.sys_config.last_boot
)
self._listeners: list[EventListener] = [] self._listeners: list[EventListener] = []
self._startup_event = asyncio.Event() self._startup_event = asyncio.Event()
self._startup_task: asyncio.Task | None = None self._startup_task: asyncio.Task | None = None
@ -198,15 +193,12 @@ class Addon(AddonModel):
): ):
self.sys_resolution.dismiss_issue(self.device_access_missing_issue) self.sys_resolution.dismiss_issue(self.device_access_missing_issue)
self.sys_homeassistant.websocket.send_message( self.sys_homeassistant.websocket.supervisor_event_custom(
WSEvent.ADDON,
{ {
ATTR_TYPE: WSType.SUPERVISOR_EVENT, ATTR_SLUG: self.slug,
ATTR_DATA: { ATTR_STATE: new_state,
ATTR_EVENT: WSEvent.ADDON, },
ATTR_SLUG: self.slug,
ATTR_STATE: new_state,
},
}
) )
@property @property
@ -216,6 +208,10 @@ class Addon(AddonModel):
async def load(self) -> None: async def load(self) -> None:
"""Async initialize of object.""" """Async initialize of object."""
self._manual_stop = (
await self.sys_hardware.helper.last_boot() != self.sys_config.last_boot
)
if self.is_detached: if self.is_detached:
await super().refresh_path_cache() await super().refresh_path_cache()
@ -283,28 +279,28 @@ class Addon(AddonModel):
@property @property
def with_icon(self) -> bool: def with_icon(self) -> bool:
"""Return True if an icon exists.""" """Return True if an icon exists."""
if self.is_detached: if self.is_detached or not self.addon_store:
return super().with_icon return super().with_icon
return self.addon_store.with_icon return self.addon_store.with_icon
@property @property
def with_logo(self) -> bool: def with_logo(self) -> bool:
"""Return True if a logo exists.""" """Return True if a logo exists."""
if self.is_detached: if self.is_detached or not self.addon_store:
return super().with_logo return super().with_logo
return self.addon_store.with_logo return self.addon_store.with_logo
@property @property
def with_changelog(self) -> bool: def with_changelog(self) -> bool:
"""Return True if a changelog exists.""" """Return True if a changelog exists."""
if self.is_detached: if self.is_detached or not self.addon_store:
return super().with_changelog return super().with_changelog
return self.addon_store.with_changelog return self.addon_store.with_changelog
@property @property
def with_documentation(self) -> bool: def with_documentation(self) -> bool:
"""Return True if a documentation exists.""" """Return True if a documentation exists."""
if self.is_detached: if self.is_detached or not self.addon_store:
return super().with_documentation return super().with_documentation
return self.addon_store.with_documentation return self.addon_store.with_documentation
@ -314,7 +310,7 @@ class Addon(AddonModel):
return self._available(self.data_store) return self._available(self.data_store)
@property @property
def version(self) -> str | None: def version(self) -> AwesomeVersion:
"""Return installed version.""" """Return installed version."""
return self.persist[ATTR_VERSION] return self.persist[ATTR_VERSION]
@ -364,7 +360,7 @@ class Addon(AddonModel):
@property @property
def auto_update(self) -> bool: def auto_update(self) -> bool:
"""Return if auto update is enable.""" """Return if auto update is enable."""
return self.persist.get(ATTR_AUTO_UPDATE, super().auto_update) return self.persist.get(ATTR_AUTO_UPDATE, False)
@auto_update.setter @auto_update.setter
def auto_update(self, value: bool) -> None: def auto_update(self, value: bool) -> None:
@ -462,7 +458,7 @@ class Addon(AddonModel):
return None return None
@property @property
def latest_version(self) -> str: def latest_version(self) -> AwesomeVersion:
"""Return version of add-on.""" """Return version of add-on."""
return self.data_store[ATTR_VERSION] return self.data_store[ATTR_VERSION]
@ -516,9 +512,8 @@ class Addon(AddonModel):
def webui(self) -> str | None: def webui(self) -> str | None:
"""Return URL to webui or None.""" """Return URL to webui or None."""
url = super().webui url = super().webui
if not url: if not url or not (webui := RE_WEBUI.match(url)):
return None return None
webui = RE_WEBUI.match(url)
# extract arguments # extract arguments
t_port = webui.group("t_port") t_port = webui.group("t_port")
@ -673,10 +668,9 @@ class Addon(AddonModel):
async def watchdog_application(self) -> bool: async def watchdog_application(self) -> bool:
"""Return True if application is running.""" """Return True if application is running."""
url = super().watchdog url = self.watchdog_url
if not url: if not url or not (application := RE_WATCHDOG.match(url)):
return True return True
application = RE_WATCHDOG.match(url)
# extract arguments # extract arguments
t_port = int(application.group("t_port")) t_port = int(application.group("t_port"))
@ -685,8 +679,10 @@ class Addon(AddonModel):
s_suffix = application.group("s_suffix") or "" s_suffix = application.group("s_suffix") or ""
# search host port for this docker port # search host port for this docker port
if self.host_network: if self.host_network and self.ports:
port = self.ports.get(f"{t_port}/tcp", t_port) port = self.ports.get(f"{t_port}/tcp")
if port is None:
port = t_port
else: else:
port = t_port port = t_port
@ -720,7 +716,7 @@ class Addon(AddonModel):
try: try:
options = self.schema.validate(self.options) options = self.schema.validate(self.options)
write_json_file(self.path_options, options) await self.sys_run_in_executor(write_json_file, self.path_options, options)
except vol.Invalid as ex: except vol.Invalid as ex:
_LOGGER.error( _LOGGER.error(
"Add-on %s has invalid options: %s", "Add-on %s has invalid options: %s",
@ -751,9 +747,12 @@ class Addon(AddonModel):
for listener in self._listeners: for listener in self._listeners:
self.sys_bus.remove_listener(listener) self.sys_bus.remove_listener(listener)
if self.path_data.is_dir(): def remove_data_dir():
_LOGGER.info("Removing add-on data folder %s", self.path_data) if self.path_data.is_dir():
await remove_data(self.path_data) _LOGGER.info("Removing add-on data folder %s", self.path_data)
remove_data(self.path_data)
await self.sys_run_in_executor(remove_data_dir)
async def _check_ingress_port(self): async def _check_ingress_port(self):
"""Assign a ingress port if dynamic port selection is used.""" """Assign a ingress port if dynamic port selection is used."""
@ -772,14 +771,20 @@ class Addon(AddonModel):
) )
async def install(self) -> None: async def install(self) -> None:
"""Install and setup this addon.""" """Install and setup this addon."""
if not self.addon_store:
raise AddonsError("Missing from store, cannot install!")
await 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(): def setup_data():
_LOGGER.info( if not self.path_data.is_dir():
"Creating Home Assistant add-on data folder %s", self.path_data _LOGGER.info(
) "Creating Home Assistant add-on data folder %s", self.path_data
self.path_data.mkdir() )
self.path_data.mkdir()
await self.sys_run_in_executor(setup_data)
# Setup/Fix AppArmor profile # Setup/Fix AppArmor profile
await self.install_apparmor() await self.install_apparmor()
@ -818,14 +823,17 @@ class Addon(AddonModel):
await self.unload() await self.unload()
# Remove config if present and requested def cleanup_config_and_audio():
if self.addon_config_used and remove_config: # Remove config if present and requested
await remove_data(self.path_config) if self.addon_config_used and remove_config:
remove_data(self.path_config)
# Cleanup audio settings # Cleanup audio settings
if self.path_pulse.exists(): if self.path_pulse.exists():
with suppress(OSError): with suppress(OSError):
self.path_pulse.unlink() self.path_pulse.unlink()
await self.sys_run_in_executor(cleanup_config_and_audio)
# Cleanup AppArmor profile # Cleanup AppArmor profile
with suppress(HostAppArmorError): with suppress(HostAppArmorError):
@ -838,9 +846,10 @@ class Addon(AddonModel):
await self.sys_ingress.update_hass_panel(self) await self.sys_ingress.update_hass_panel(self)
# Cleanup Ingress dynamic port assignment # Cleanup Ingress dynamic port assignment
need_ingress_token_cleanup = False
if self.with_ingress: if self.with_ingress:
need_ingress_token_cleanup = True
await self.sys_ingress.del_dynamic_port(self.slug) await self.sys_ingress.del_dynamic_port(self.slug)
self.sys_create_task(self.sys_ingress.reload())
# Cleanup discovery data # Cleanup discovery data
for message in self.sys_discovery.list_messages: for message in self.sys_discovery.list_messages:
@ -855,8 +864,12 @@ class Addon(AddonModel):
await service.del_service_data(self) await service.del_service_data(self)
# Remove from addon manager # Remove from addon manager
await self.sys_addons.data.uninstall(self)
self.sys_addons.local.pop(self.slug) self.sys_addons.local.pop(self.slug)
await self.sys_addons.data.uninstall(self)
# Cleanup Ingress tokens
if need_ingress_token_cleanup:
await self.sys_ingress.reload()
@Job( @Job(
name="addon_update", name="addon_update",
@ -869,6 +882,9 @@ 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)
if it was running. Else nothing is returned. if it was running. Else nothing is returned.
""" """
if not self.addon_store:
raise AddonsError("Missing from store, cannot update!")
old_image = self.image old_image = self.image
# Cache data to prevent races with other updates to global # Cache data to prevent races with other updates to global
store = self.addon_store.clone() store = self.addon_store.clone()
@ -925,7 +941,9 @@ class Addon(AddonModel):
except DockerError as err: except DockerError as err:
raise AddonsError() from err raise AddonsError() from err
await self.sys_addons.data.update(self.addon_store) if 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)
@ -938,22 +956,25 @@ class Addon(AddonModel):
) )
return out return out
def write_pulse(self) -> None: async def write_pulse(self) -> None:
"""Write asound config to file and return True on success.""" """Write asound config to file and return True on success."""
pulse_config = self.sys_plugins.audio.pulse_client( pulse_config = self.sys_plugins.audio.pulse_client(
input_profile=self.audio_input, output_profile=self.audio_output input_profile=self.audio_input, output_profile=self.audio_output
) )
# Cleanup wrong maps def write_pulse_config():
if self.path_pulse.is_dir(): # Cleanup wrong maps
shutil.rmtree(self.path_pulse, ignore_errors=True) if self.path_pulse.is_dir():
shutil.rmtree(self.path_pulse, ignore_errors=True)
# Write pulse config
try:
self.path_pulse.write_text(pulse_config, encoding="utf-8") self.path_pulse.write_text(pulse_config, encoding="utf-8")
try:
await self.sys_run_in_executor(write_pulse_config)
except OSError as err: except OSError as err:
if err.errno == errno.EBADMSG: if err.errno == errno.EBADMSG:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE self.sys_resolution.add_unhealthy_reason(
UnhealthyReason.OSERROR_BAD_MESSAGE
)
_LOGGER.error( _LOGGER.error(
"Add-on %s can't write pulse/client.config: %s", self.slug, err "Add-on %s can't write pulse/client.config: %s", self.slug, err
) )
@ -965,7 +986,7 @@ class Addon(AddonModel):
async def install_apparmor(self) -> None: async def install_apparmor(self) -> None:
"""Install or Update AppArmor profile for Add-on.""" """Install or Update AppArmor profile for Add-on."""
exists_local = self.sys_host.apparmor.exists(self.slug) exists_local = self.sys_host.apparmor.exists(self.slug)
exists_addon = self.path_apparmor.exists() exists_addon = await self.sys_run_in_executor(self.path_apparmor.exists)
# Nothing to do # Nothing to do
if not exists_local and not exists_addon: if not exists_local and not exists_addon:
@ -1070,7 +1091,7 @@ class Addon(AddonModel):
# Sound # Sound
if self.with_audio: if self.with_audio:
self.write_pulse() await self.write_pulse()
def _check_addon_config_dir(): def _check_addon_config_dir():
if self.path_config.is_dir(): if self.path_config.is_dir():
@ -1301,8 +1322,8 @@ class Addon(AddonModel):
arcname="data", arcname="data",
) )
# Backup config # Backup config (if used and existing, restore handles this gracefully)
if addon_config_used: if addon_config_used and self.path_config.is_dir():
atomic_contents_add( atomic_contents_add(
backup, backup,
self.path_config, self.path_config,
@ -1312,7 +1333,7 @@ class Addon(AddonModel):
arcname="config", arcname="config",
) )
wait_for_start: Awaitable[None] | None = None wait_for_start: asyncio.Task | None = None
data = { data = {
ATTR_USER: self.persist, ATTR_USER: self.persist,
@ -1338,9 +1359,7 @@ class Addon(AddonModel):
) )
_LOGGER.info("Finish backup for addon %s", self.slug) _LOGGER.info("Finish backup for addon %s", self.slug)
except (tarfile.TarError, OSError, AddFileError) as err: except (tarfile.TarError, OSError, AddFileError) as err:
raise AddonsError( raise AddonsError(f"Can't write tarfile: {err}", _LOGGER.error) from err
f"Can't write tarfile {tar_file}: {err}", _LOGGER.error
) from err
finally: finally:
if was_running: if was_running:
wait_for_start = await self.end_backup() wait_for_start = await self.end_backup()
@ -1358,7 +1377,7 @@ 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)
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: asyncio.Task | None = None
# Extract backup # Extract backup
def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]: def _extract_tarfile() -> tuple[TemporaryDirectory, dict[str, Any]]:
@ -1441,6 +1460,12 @@ 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."""
_LOGGER.info("Restoring data and config for addon %s", self.slug)
if self.path_data.is_dir():
remove_data(self.path_data)
if self.path_config.is_dir():
remove_data(self.path_config)
temp_data = Path(tmp.name, "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)
@ -1453,12 +1478,6 @@ class Addon(AddonModel):
elif self.addon_config_used: elif self.addon_config_used:
self.path_config.mkdir() self.path_config.mkdir()
_LOGGER.info("Restoring data and config for addon %s", self.slug)
if self.path_data.is_dir():
await remove_data(self.path_data)
if self.path_config.is_dir():
await remove_data(self.path_config)
try: try:
await self.sys_run_in_executor(_restore_data) await self.sys_run_in_executor(_restore_data)
except shutil.Error as err: except shutil.Error as err:
@ -1468,7 +1487,7 @@ class Addon(AddonModel):
# Restore AppArmor # Restore AppArmor
profile_file = Path(tmp.name, "apparmor.txt") profile_file = Path(tmp.name, "apparmor.txt")
if profile_file.exists(): if await self.sys_run_in_executor(profile_file.exists):
try: try:
await self.sys_host.apparmor.load_profile( await self.sys_host.apparmor.load_profile(
self.slug, profile_file self.slug, profile_file
@ -1489,7 +1508,7 @@ class Addon(AddonModel):
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: finally:
tmp.cleanup() await self.sys_run_in_executor(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
@ -1582,6 +1601,6 @@ class Addon(AddonModel):
def refresh_path_cache(self) -> Awaitable[None]: def refresh_path_cache(self) -> Awaitable[None]:
"""Refresh cache of existing paths.""" """Refresh cache of existing paths."""
if self.is_detached: if self.is_detached or not self.addon_store:
return super().refresh_path_cache() return super().refresh_path_cache()
return self.addon_store.refresh_path_cache() return self.addon_store.refresh_path_cache()

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -15,6 +15,7 @@ from ..const import (
ATTR_SQUASH, ATTR_SQUASH,
FILE_SUFFIX_CONFIGURATION, FILE_SUFFIX_CONFIGURATION,
META_ADDON, META_ADDON,
SOCKET_DOCKER,
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..docker.interface import MAP_ARCH from ..docker.interface import MAP_ARCH
@ -23,7 +24,7 @@ from ..utils.common import FileConfiguration, find_one_filetype
from .validate import SCHEMA_BUILD_CONFIG from .validate import SCHEMA_BUILD_CONFIG
if TYPE_CHECKING: if TYPE_CHECKING:
from . import AnyAddon from .manager import AnyAddon
class AddonBuild(FileConfiguration, CoreSysAttributes): class AddonBuild(FileConfiguration, CoreSysAttributes):
@ -63,7 +64,7 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
@cached_property @cached_property
def arch(self) -> str: def arch(self) -> str:
"""Return arch of the add-on.""" """Return arch of the add-on."""
return self.sys_arch.match(self.addon.arch) return self.sys_arch.match([self.addon.arch])
@property @property
def base_image(self) -> str: def base_image(self) -> str:
@ -81,13 +82,6 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
) )
return self._data[ATTR_BUILD_FROM][self.arch] return self._data[ATTR_BUILD_FROM][self.arch]
@property
def dockerfile(self) -> Path:
"""Return Dockerfile path."""
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
return self.addon.path_location.joinpath("Dockerfile")
@property @property
def squash(self) -> bool: def squash(self) -> bool:
"""Return True or False if squash is active.""" """Return True or False if squash is active."""
@ -103,49 +97,89 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
"""Return additional Docker labels.""" """Return additional Docker labels."""
return self._data[ATTR_LABELS] return self._data[ATTR_LABELS]
@property def get_dockerfile(self) -> Path:
def is_valid(self) -> bool: """Return Dockerfile path.
Must be run in executor.
"""
if self.addon.path_location.joinpath(f"Dockerfile.{self.arch}").exists():
return self.addon.path_location.joinpath(f"Dockerfile.{self.arch}")
return self.addon.path_location.joinpath("Dockerfile")
async def is_valid(self) -> bool:
"""Return true if the build env is valid.""" """Return true if the build env is valid."""
try:
def build_is_valid() -> bool:
return all( return all(
[ [
self.addon.path_location.is_dir(), self.addon.path_location.is_dir(),
self.dockerfile.is_file(), self.get_dockerfile().is_file(),
] ]
) )
try:
return await self.sys_run_in_executor(build_is_valid)
except HassioArchNotFound: except HassioArchNotFound:
return False return False
def get_docker_args(self, version: AwesomeVersion, image: str | None = None): def get_docker_args(
"""Create a dict with Docker build arguments.""" self, version: AwesomeVersion, image_tag: str
args = { ) -> dict[str, Any]:
"path": str(self.addon.path_location), """Create a dict with Docker run args."""
"tag": f"{image or self.addon.image}:{version!s}", dockerfile_path = self.get_dockerfile().relative_to(self.addon.path_location)
"dockerfile": str(self.dockerfile),
"pull": True, build_cmd = [
"forcerm": not self.sys_dev, "docker",
"squash": self.squash, "buildx",
"platform": MAP_ARCH[self.arch], "build",
"labels": { ".",
"io.hass.version": version, "--tag",
"io.hass.arch": self.arch, image_tag,
"io.hass.type": META_ADDON, "--file",
"io.hass.name": self._fix_label("name"), str(dockerfile_path),
"io.hass.description": self._fix_label("description"), "--platform",
**self.additional_labels, MAP_ARCH[self.arch],
}, "--pull",
"buildargs": { ]
"BUILD_FROM": self.base_image,
"BUILD_VERSION": version, labels = {
"BUILD_ARCH": self.sys_arch.default, "io.hass.version": version,
**self.additional_args, "io.hass.arch": self.arch,
}, "io.hass.type": META_ADDON,
"io.hass.name": self._fix_label("name"),
"io.hass.description": self._fix_label("description"),
**self.additional_labels,
} }
if self.addon.url: if self.addon.url:
args["labels"]["io.hass.url"] = self.addon.url labels["io.hass.url"] = self.addon.url
return args for key, value in labels.items():
build_cmd.extend(["--label", f"{key}={value}"])
build_args = {
"BUILD_FROM": self.base_image,
"BUILD_VERSION": version,
"BUILD_ARCH": self.sys_arch.default,
**self.additional_args,
}
for key, value in build_args.items():
build_cmd.extend(["--build-arg", f"{key}={value}"])
# The addon path will be mounted from the host system
addon_extern_path = self.sys_config.local_to_extern_path(
self.addon.path_location
)
return {
"command": build_cmd,
"volumes": {
SOCKET_DOCKER: {"bind": "/var/run/docker.sock", "mode": "rw"},
addon_extern_path: {"bind": "/addon", "mode": "ro"},
},
"working_dir": "/addon",
}
def _fix_label(self, label_name: str) -> str: def _fix_label(self, label_name: str) -> str:
"""Remove characters they are not supported.""" """Remove characters they are not supported."""

View File

@ -67,6 +67,10 @@ class AddonManager(CoreSysAttributes):
return self.store.get(addon_slug) return self.store.get(addon_slug)
return None return None
def get_local_only(self, addon_slug: str) -> Addon | None:
"""Return an installed add-on from slug."""
return self.local.get(addon_slug)
def from_token(self, token: str) -> Addon | None: def from_token(self, token: str) -> Addon | None:
"""Return an add-on from Supervisor token.""" """Return an add-on from Supervisor token."""
for addon in self.installed: for addon in self.installed:
@ -194,6 +198,7 @@ class AddonManager(CoreSysAttributes):
_LOGGER.info("Add-on '%s' successfully installed", slug) _LOGGER.info("Add-on '%s' successfully installed", slug)
@Job(name="addon_manager_uninstall")
async def uninstall(self, slug: str, *, remove_config: bool = False) -> None: async def uninstall(self, slug: str, *, remove_config: bool = False) -> None:
"""Remove an add-on.""" """Remove an add-on."""
if slug not in self.local: if slug not in self.local:
@ -261,7 +266,7 @@ class AddonManager(CoreSysAttributes):
], ],
on_condition=AddonsJobError, on_condition=AddonsJobError,
) )
async def rebuild(self, slug: str) -> asyncio.Task | None: async def rebuild(self, slug: str, *, force: bool = False) -> asyncio.Task | None:
"""Perform a rebuild of local build add-on. """Perform a rebuild of local build add-on.
Returns a Task that completes when addon has state 'started' (see addon.start) Returns a Task that completes when addon has state 'started' (see addon.start)
@ -284,7 +289,7 @@ class AddonManager(CoreSysAttributes):
raise AddonsError( raise AddonsError(
"Version changed, use Update instead Rebuild", _LOGGER.error "Version changed, use Update instead Rebuild", _LOGGER.error
) )
if not addon.need_build: if not force and not addon.need_build:
raise AddonsNotSupportedError( raise AddonsNotSupportedError(
"Can't rebuild a image based add-on", _LOGGER.error "Can't rebuild a image based add-on", _LOGGER.error
) )
@ -313,7 +318,7 @@ class AddonManager(CoreSysAttributes):
if slug not in self.local: if slug not in self.local:
_LOGGER.debug("Add-on %s is not local available for restore", slug) _LOGGER.debug("Add-on %s is not local available for restore", slug)
addon = Addon(self.coresys, slug) addon = Addon(self.coresys, slug)
had_ingress = False had_ingress: bool | None = False
else: else:
_LOGGER.debug("Add-on %s is local available for restore", slug) _LOGGER.debug("Add-on %s is local available for restore", slug)
addon = self.local[slug] addon = self.local[slug]

View File

@ -294,7 +294,7 @@ class AddonModel(JobGroup, ABC):
return self.data.get(ATTR_WEBUI) return self.data.get(ATTR_WEBUI)
@property @property
def watchdog(self) -> str | None: def watchdog_url(self) -> str | None:
"""Return URL to for watchdog or None.""" """Return URL to for watchdog or None."""
return self.data.get(ATTR_WATCHDOG) return self.data.get(ATTR_WATCHDOG)
@ -606,7 +606,7 @@ class AddonModel(JobGroup, ABC):
return AddonOptions(self.coresys, raw_schema, self.name, self.slug) return AddonOptions(self.coresys, raw_schema, self.name, self.slug)
@property @property
def schema_ui(self) -> list[dict[any, any]] | None: def schema_ui(self) -> list[dict[Any, Any]] | None:
"""Create a UI schema for add-on options.""" """Create a UI schema for add-on options."""
raw_schema = self.data[ATTR_SCHEMA] raw_schema = self.data[ATTR_SCHEMA]
@ -664,12 +664,16 @@ class AddonModel(JobGroup, ABC):
"""Validate if addon is available for current system.""" """Validate if addon is available for current system."""
return self._validate_availability(self.data, logger=_LOGGER.error) return self._validate_availability(self.data, logger=_LOGGER.error)
def __eq__(self, other): def __eq__(self, other: Any) -> bool:
"""Compaired add-on objects.""" """Compare add-on objects."""
if not isinstance(other, AddonModel): if not isinstance(other, AddonModel):
return False return False
return self.slug == other.slug return self.slug == other.slug
def __hash__(self) -> int:
"""Hash for add-on objects."""
return hash(self.slug)
def _validate_availability( def _validate_availability(
self, config, *, logger: Callable[..., None] | None = None self, config, *, logger: Callable[..., None] | None = None
) -> None: ) -> None:

View File

@ -137,7 +137,7 @@ class AddonOptions(CoreSysAttributes):
) from None ) from None
# prepare range # prepare range
range_args = {} range_args: dict[str, Any] = {}
for group_name in _SCHEMA_LENGTH_PARTS: for group_name in _SCHEMA_LENGTH_PARTS:
group_value = match.group(group_name) group_value = match.group(group_name)
if group_value: if group_value:
@ -390,14 +390,14 @@ class UiOptions(CoreSysAttributes):
multiple: bool = False, multiple: bool = False,
) -> None: ) -> None:
"""UI nested dict items.""" """UI nested dict items."""
ui_node = { ui_node: dict[str, Any] = {
"name": key, "name": key,
"type": "schema", "type": "schema",
"optional": True, "optional": True,
"multiple": multiple, "multiple": multiple,
} }
nested_schema = [] nested_schema: list[dict[str, Any]] = []
for c_key, c_value in option_dict.items(): for c_key, c_value in option_dict.items():
# Nested? # Nested?
if isinstance(c_value, list): if isinstance(c_value, list):
@ -413,7 +413,7 @@ def _create_device_filter(str_filter: str) -> dict[str, Any]:
"""Generate device Filter.""" """Generate device Filter."""
raw_filter = dict(value.split("=") for value in str_filter.split(";")) raw_filter = dict(value.split("=") for value in str_filter.split(";"))
clean_filter = {} clean_filter: dict[str, Any] = {}
for key, value in raw_filter.items(): for key, value in raw_filter.items():
if key == "subsystem": if key == "subsystem":
clean_filter[key] = UdevSubsystem(value) clean_filter[key] = UdevSubsystem(value)

View File

@ -2,9 +2,9 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
from pathlib import Path from pathlib import Path
import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE from ..const import ROLE_ADMIN, ROLE_MANAGER, SECURITY_DISABLE, SECURITY_PROFILE
@ -86,18 +86,20 @@ def rating_security(addon: AddonModel) -> int:
return max(min(8, rating), 1) return max(min(8, rating), 1)
async def remove_data(folder: Path) -> None: def remove_data(folder: Path) -> None:
"""Remove folder and reset privileged.""" """Remove folder and reset privileged.
try:
proc = await asyncio.create_subprocess_exec(
"rm", "-rf", str(folder), stdout=asyncio.subprocess.DEVNULL
)
_, error_msg = await proc.communicate() Must be run in executor.
"""
try:
subprocess.run(
["rm", "-rf", str(folder)], stdout=subprocess.DEVNULL, text=True, check=True
)
except OSError as err: except OSError as err:
error_msg = str(err) error_msg = str(err)
except subprocess.CalledProcessError as procerr:
error_msg = procerr.stderr.strip()
else: else:
if proc.returncode == 0: return
return
_LOGGER.error("Can't remove Add-on Data: %s", error_msg) _LOGGER.error("Can't remove Add-on Data: %s", error_msg)

View File

@ -1,13 +1,14 @@
"""Init file for Supervisor RESTful API.""" """Init file for Supervisor RESTful API."""
from dataclasses import dataclass
from functools import partial from functools import partial
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from aiohttp import web from aiohttp import hdrs, web
from ..const import AddonState from ..const import SUPERVISOR_DOCKER_NAME, 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 async_capture_exception from ..utils.sentry import async_capture_exception
@ -47,6 +48,14 @@ MAX_CLIENT_SIZE: int = 1024**2 * 16
MAX_LINE_SIZE: int = 24570 MAX_LINE_SIZE: int = 24570
@dataclass(slots=True, frozen=True)
class StaticResourceConfig:
"""Configuration for a static resource."""
prefix: str
path: Path
class RestAPI(CoreSysAttributes): class RestAPI(CoreSysAttributes):
"""Handle RESTful API for Supervisor.""" """Handle RESTful API for Supervisor."""
@ -73,12 +82,12 @@ class RestAPI(CoreSysAttributes):
self._site: web.TCPSite | None = None self._site: web.TCPSite | None = None
# share single host API handler for reuse in logging endpoints # share single host API handler for reuse in logging endpoints
self._api_host: APIHost | None = None self._api_host: APIHost = APIHost()
self._api_host.coresys = coresys
async def load(self) -> None: async def load(self) -> None:
"""Register REST API Calls.""" """Register REST API Calls."""
self._api_host = APIHost() static_resource_configs: list[StaticResourceConfig] = []
self._api_host.coresys = self.coresys
self._register_addons() self._register_addons()
self._register_audio() self._register_audio()
@ -98,7 +107,7 @@ class RestAPI(CoreSysAttributes):
self._register_network() self._register_network()
self._register_observer() self._register_observer()
self._register_os() self._register_os()
self._register_panel() static_resource_configs.extend(self._register_panel())
self._register_proxy() self._register_proxy()
self._register_resolution() self._register_resolution()
self._register_root() self._register_root()
@ -107,6 +116,17 @@ class RestAPI(CoreSysAttributes):
self._register_store() self._register_store()
self._register_supervisor() self._register_supervisor()
if static_resource_configs:
def process_configs() -> list[web.StaticResource]:
return [
web.StaticResource(config.prefix, config.path)
for config in static_resource_configs
]
for resource in await self.sys_run_in_executor(process_configs):
self.webapp.router.register_resource(resource)
await self.start() await self.start()
def _register_advanced_logs(self, path: str, syslog_identifier: str): def _register_advanced_logs(self, path: str, syslog_identifier: str):
@ -217,6 +237,8 @@ class RestAPI(CoreSysAttributes):
[ [
web.get("/os/info", api_os.info), web.get("/os/info", api_os.info),
web.post("/os/update", api_os.update), web.post("/os/update", api_os.update),
web.get("/os/config/swap", api_os.config_swap_info),
web.post("/os/config/swap", api_os.config_swap_options),
web.post("/os/config/sync", api_os.config_sync), web.post("/os/config/sync", api_os.config_sync),
web.post("/os/datadisk/move", api_os.migrate_data), web.post("/os/datadisk/move", api_os.migrate_data),
web.get("/os/datadisk/list", api_os.list_data), web.get("/os/datadisk/list", api_os.list_data),
@ -323,6 +345,9 @@ class RestAPI(CoreSysAttributes):
api_root.coresys = self.coresys api_root.coresys = self.coresys
self.webapp.add_routes([web.get("/info", api_root.info)]) self.webapp.add_routes([web.get("/info", api_root.info)])
self.webapp.add_routes([web.post("/reload_updates", api_root.reload_updates)])
# Discouraged
self.webapp.add_routes([web.post("/refresh_updates", api_root.refresh_updates)]) self.webapp.add_routes([web.post("/refresh_updates", api_root.refresh_updates)])
self.webapp.add_routes( self.webapp.add_routes(
[web.get("/available_updates", api_root.available_updates)] [web.get("/available_updates", api_root.available_updates)]
@ -401,7 +426,7 @@ class RestAPI(CoreSysAttributes):
async def get_supervisor_logs(*args, **kwargs): async def get_supervisor_logs(*args, **kwargs):
try: try:
return await self._api_host.advanced_logs_handler( return await self._api_host.advanced_logs_handler(
*args, identifier="hassio_supervisor", **kwargs *args, identifier=SUPERVISOR_DOCKER_NAME, **kwargs
) )
except Exception as err: # pylint: disable=broad-exception-caught except Exception as err: # pylint: disable=broad-exception-caught
# Supervisor logs are critical, so catch everything, log the exception # Supervisor logs are critical, so catch everything, log the exception
@ -504,7 +529,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/addons", api_addons.list), web.get("/addons", api_addons.list_addons),
web.post("/addons/{addon}/uninstall", api_addons.uninstall), web.post("/addons/{addon}/uninstall", api_addons.uninstall),
web.post("/addons/{addon}/start", api_addons.start), web.post("/addons/{addon}/start", api_addons.start),
web.post("/addons/{addon}/stop", api_addons.stop), web.post("/addons/{addon}/stop", api_addons.stop),
@ -572,7 +597,9 @@ class RestAPI(CoreSysAttributes):
web.post("/ingress/session", api_ingress.create_session), web.post("/ingress/session", api_ingress.create_session),
web.post("/ingress/validate_session", api_ingress.validate_session), web.post("/ingress/validate_session", api_ingress.validate_session),
web.get("/ingress/panels", api_ingress.panels), web.get("/ingress/panels", api_ingress.panels),
web.view("/ingress/{token}/{path:.*}", api_ingress.handler), web.route(
hdrs.METH_ANY, "/ingress/{token}/{path:.*}", api_ingress.handler
),
] ]
) )
@ -583,7 +610,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/backups", api_backups.list), web.get("/backups", api_backups.list_backups),
web.get("/backups/info", api_backups.info), web.get("/backups/info", api_backups.info),
web.post("/backups/options", api_backups.options), web.post("/backups/options", api_backups.options),
web.post("/backups/reload", api_backups.reload), web.post("/backups/reload", api_backups.reload),
@ -610,7 +637,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/services", api_services.list), web.get("/services", api_services.list_services),
web.get("/services/{service}", api_services.get_service), web.get("/services/{service}", api_services.get_service),
web.post("/services/{service}", api_services.set_service), web.post("/services/{service}", api_services.set_service),
web.delete("/services/{service}", api_services.del_service), web.delete("/services/{service}", api_services.del_service),
@ -624,7 +651,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/discovery", api_discovery.list), web.get("/discovery", api_discovery.list_discovery),
web.get("/discovery/{uuid}", api_discovery.get_discovery), web.get("/discovery/{uuid}", api_discovery.get_discovery),
web.delete("/discovery/{uuid}", api_discovery.del_discovery), web.delete("/discovery/{uuid}", api_discovery.del_discovery),
web.post("/discovery", api_discovery.set_discovery), web.post("/discovery", api_discovery.set_discovery),
@ -750,10 +777,9 @@ class RestAPI(CoreSysAttributes):
] ]
) )
def _register_panel(self) -> None: def _register_panel(self) -> list[StaticResourceConfig]:
"""Register panel for Home Assistant.""" """Register panel for Home Assistant."""
panel_dir = Path(__file__).parent.joinpath("panel") return [StaticResourceConfig("/app", Path(__file__).parent.joinpath("panel"))]
self.webapp.add_routes([web.static("/app", panel_dir)])
def _register_docker(self) -> None: def _register_docker(self) -> None:
"""Register docker configuration functions.""" """Register docker configuration functions."""
@ -763,6 +789,7 @@ class RestAPI(CoreSysAttributes):
self.webapp.add_routes( self.webapp.add_routes(
[ [
web.get("/docker/info", api_docker.info), web.get("/docker/info", api_docker.info),
web.post("/docker/options", api_docker.options),
web.get("/docker/registries", api_docker.registries), web.get("/docker/registries", api_docker.registries),
web.post("/docker/registries", api_docker.create_registry), web.post("/docker/registries", api_docker.create_registry),
web.delete("/docker/registries/{hostname}", api_docker.remove_registry), web.delete("/docker/registries/{hostname}", api_docker.remove_registry),

View File

@ -3,14 +3,13 @@
import asyncio import asyncio
from collections.abc import Awaitable from collections.abc import Awaitable
import logging import logging
from typing import Any from typing import Any, TypedDict
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
from voluptuous.humanize import humanize_error from voluptuous.humanize import humanize_error
from ..addons.addon import Addon from ..addons.addon import Addon
from ..addons.manager import AnyAddon
from ..addons.utils import rating_security from ..addons.utils import rating_security
from ..const import ( from ..const import (
ATTR_ADDONS, ATTR_ADDONS,
@ -37,6 +36,7 @@ from ..const import (
ATTR_DNS, ATTR_DNS,
ATTR_DOCKER_API, ATTR_DOCKER_API,
ATTR_DOCUMENTATION, ATTR_DOCUMENTATION,
ATTR_FORCE,
ATTR_FULL_ACCESS, ATTR_FULL_ACCESS,
ATTR_GPIO, ATTR_GPIO,
ATTR_HASSIO_API, ATTR_HASSIO_API,
@ -63,7 +63,6 @@ from ..const import (
ATTR_MEMORY_LIMIT, ATTR_MEMORY_LIMIT,
ATTR_MEMORY_PERCENT, ATTR_MEMORY_PERCENT,
ATTR_MEMORY_USAGE, ATTR_MEMORY_USAGE,
ATTR_MESSAGE,
ATTR_NAME, ATTR_NAME,
ATTR_NETWORK, ATTR_NETWORK,
ATTR_NETWORK_DESCRIPTION, ATTR_NETWORK_DESCRIPTION,
@ -72,7 +71,6 @@ from ..const import (
ATTR_OPTIONS, ATTR_OPTIONS,
ATTR_PRIVILEGED, ATTR_PRIVILEGED,
ATTR_PROTECTED, ATTR_PROTECTED,
ATTR_PWNED,
ATTR_RATING, ATTR_RATING,
ATTR_REPOSITORY, ATTR_REPOSITORY,
ATTR_SCHEMA, ATTR_SCHEMA,
@ -90,7 +88,6 @@ from ..const import (
ATTR_UPDATE_AVAILABLE, ATTR_UPDATE_AVAILABLE,
ATTR_URL, ATTR_URL,
ATTR_USB, ATTR_USB,
ATTR_VALID,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
ATTR_VIDEO, ATTR_VIDEO,
@ -143,15 +140,25 @@ SCHEMA_SECURITY = vol.Schema({vol.Optional(ATTR_PROTECTED): vol.Boolean()})
SCHEMA_UNINSTALL = vol.Schema( SCHEMA_UNINSTALL = vol.Schema(
{vol.Optional(ATTR_REMOVE_CONFIG, default=False): vol.Boolean()} {vol.Optional(ATTR_REMOVE_CONFIG, default=False): vol.Boolean()}
) )
SCHEMA_REBUILD = vol.Schema({vol.Optional(ATTR_FORCE, default=False): vol.Boolean()})
# pylint: enable=no-value-for-parameter # pylint: enable=no-value-for-parameter
class OptionsValidateResponse(TypedDict):
"""Response object for options validate."""
message: str
valid: bool
pwned: bool | None
class APIAddons(CoreSysAttributes): class APIAddons(CoreSysAttributes):
"""Handle RESTful API for add-on functions.""" """Handle RESTful API for add-on functions."""
def get_addon_for_request(self, request: web.Request) -> Addon: def get_addon_for_request(self, request: web.Request) -> Addon:
"""Return addon, throw an exception if it doesn't exist.""" """Return addon, throw an exception if it doesn't exist."""
addon_slug: str = request.match_info.get("addon") addon_slug: str = request.match_info["addon"]
# Lookup itself # Lookup itself
if addon_slug == "self": if addon_slug == "self":
@ -169,7 +176,7 @@ class APIAddons(CoreSysAttributes):
return addon return addon
@api_process @api_process
async def list(self, request: web.Request) -> dict[str, Any]: async def list_addons(self, request: web.Request) -> dict[str, Any]:
"""Return all add-ons or repositories.""" """Return all add-ons or repositories."""
data_addons = [ data_addons = [
{ {
@ -204,7 +211,7 @@ class APIAddons(CoreSysAttributes):
async def info(self, request: web.Request) -> dict[str, Any]: async def info(self, request: web.Request) -> dict[str, Any]:
"""Return add-on information.""" """Return add-on information."""
addon: AnyAddon = self.get_addon_for_request(request) addon: Addon = self.get_addon_for_request(request)
data = { data = {
ATTR_NAME: addon.name, ATTR_NAME: addon.name,
@ -339,10 +346,10 @@ class APIAddons(CoreSysAttributes):
await 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) -> OptionsValidateResponse:
"""Validate user options for add-on.""" """Validate user options for add-on."""
addon = self.get_addon_for_request(request) addon = self.get_addon_for_request(request)
data = {ATTR_MESSAGE: "", ATTR_VALID: True, ATTR_PWNED: False} data = OptionsValidateResponse(message="", valid=True, pwned=False)
options = await request.json(loads=json_loads) or addon.options options = await request.json(loads=json_loads) or addon.options
@ -351,8 +358,8 @@ class APIAddons(CoreSysAttributes):
try: try:
options_schema.validate(options) options_schema.validate(options)
except vol.Invalid as ex: except vol.Invalid as ex:
data[ATTR_MESSAGE] = humanize_error(options, ex) data["message"] = humanize_error(options, ex)
data[ATTR_VALID] = False data["valid"] = False
if not self.sys_security.pwned: if not self.sys_security.pwned:
return data return data
@ -363,24 +370,24 @@ class APIAddons(CoreSysAttributes):
await self.sys_security.verify_secret(secret) await self.sys_security.verify_secret(secret)
continue continue
except PwnedSecret: except PwnedSecret:
data[ATTR_PWNED] = True data["pwned"] = True
except PwnedError: except PwnedError:
data[ATTR_PWNED] = None data["pwned"] = None
break break
if self.sys_security.force and data[ATTR_PWNED] in (None, True): if self.sys_security.force and data["pwned"] in (None, True):
data[ATTR_VALID] = False data["valid"] = False
if data[ATTR_PWNED] is None: if data["pwned"] is None:
data[ATTR_MESSAGE] = "Error happening on pwned secrets check!" data["message"] = "Error happening on pwned secrets check!"
else: else:
data[ATTR_MESSAGE] = "Add-on uses pwned secrets!" data["message"] = "Add-on uses pwned secrets!"
return data return data
@api_process @api_process
async def options_config(self, request: web.Request) -> None: async def options_config(self, request: web.Request) -> None:
"""Validate user options for add-on.""" """Validate user options for add-on."""
slug: str = request.match_info.get("addon") slug: str = request.match_info["addon"]
if slug != "self": if slug != "self":
raise APIForbidden("This can be only read by the Add-on itself!") raise APIForbidden("This can be only read by the Add-on itself!")
addon = self.get_addon_for_request(request) addon = self.get_addon_for_request(request)
@ -457,7 +464,11 @@ class APIAddons(CoreSysAttributes):
async def rebuild(self, request: web.Request) -> None: async def rebuild(self, request: web.Request) -> None:
"""Rebuild local build add-on.""" """Rebuild local build add-on."""
addon = self.get_addon_for_request(request) addon = self.get_addon_for_request(request)
if start_task := await asyncio.shield(self.sys_addons.rebuild(addon.slug)): body: dict[str, Any] = await api_validate(SCHEMA_REBUILD, request)
if start_task := await asyncio.shield(
self.sys_addons.rebuild(addon.slug, force=body[ATTR_FORCE])
):
await start_task await start_task
@api_process @api_process

View File

@ -124,7 +124,7 @@ class APIAudio(CoreSysAttributes):
@api_process @api_process
async def set_volume(self, request: web.Request) -> None: async def set_volume(self, request: web.Request) -> None:
"""Set audio volume on stream.""" """Set audio volume on stream."""
source: StreamType = StreamType(request.match_info.get("source")) source: StreamType = StreamType(request.match_info["source"])
application: bool = request.path.endswith("application") application: bool = request.path.endswith("application")
body = await api_validate(SCHEMA_VOLUME, request) body = await api_validate(SCHEMA_VOLUME, request)
@ -137,7 +137,7 @@ class APIAudio(CoreSysAttributes):
@api_process @api_process
async def set_mute(self, request: web.Request) -> None: async def set_mute(self, request: web.Request) -> None:
"""Mute audio volume on stream.""" """Mute audio volume on stream."""
source: StreamType = StreamType(request.match_info.get("source")) source: StreamType = StreamType(request.match_info["source"])
application: bool = request.path.endswith("application") application: bool = request.path.endswith("application")
body = await api_validate(SCHEMA_MUTE, request) body = await api_validate(SCHEMA_MUTE, request)
@ -150,7 +150,7 @@ class APIAudio(CoreSysAttributes):
@api_process @api_process
async def set_default(self, request: web.Request) -> None: async def set_default(self, request: web.Request) -> None:
"""Set audio default stream.""" """Set audio default stream."""
source: StreamType = StreamType(request.match_info.get("source")) source: StreamType = StreamType(request.match_info["source"])
body = await api_validate(SCHEMA_DEFAULT, request) body = await api_validate(SCHEMA_DEFAULT, request)
await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME])) await asyncio.shield(self.sys_host.sound.set_default(source, body[ATTR_NAME]))

View File

@ -1,19 +1,21 @@
"""Init file for Supervisor auth/SSO RESTful API.""" """Init file for Supervisor auth/SSO RESTful API."""
import asyncio import asyncio
from collections.abc import Awaitable
import logging import logging
from typing import Any from typing import Any, cast
from aiohttp import BasicAuth, web from aiohttp import BasicAuth, web
from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE from aiohttp.hdrs import AUTHORIZATION, CONTENT_TYPE, WWW_AUTHENTICATE
from aiohttp.web import FileField
from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_exceptions import HTTPUnauthorized
from multidict import MultiDictProxy
import voluptuous as vol import voluptuous as vol
from ..addons.addon import Addon from ..addons.addon import Addon
from ..const import ATTR_NAME, ATTR_PASSWORD, ATTR_USERNAME, REQUEST_FROM from ..const import ATTR_NAME, ATTR_PASSWORD, ATTR_USERNAME, REQUEST_FROM
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIForbidden from ..exceptions import APIForbidden
from ..utils.json import json_loads
from .const import ( from .const import (
ATTR_GROUP_IDS, ATTR_GROUP_IDS,
ATTR_IS_ACTIVE, ATTR_IS_ACTIVE,
@ -23,7 +25,7 @@ from .const import (
CONTENT_TYPE_JSON, CONTENT_TYPE_JSON,
CONTENT_TYPE_URL, CONTENT_TYPE_URL,
) )
from .utils import api_process, api_validate from .utils import api_process, api_validate, json_loads
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
@ -42,7 +44,7 @@ REALM_HEADER: dict[str, str] = {
class APIAuth(CoreSysAttributes): class APIAuth(CoreSysAttributes):
"""Handle RESTful API for auth functions.""" """Handle RESTful API for auth functions."""
def _process_basic(self, request: web.Request, addon: Addon) -> bool: def _process_basic(self, request: web.Request, addon: Addon) -> Awaitable[bool]:
"""Process login request with basic auth. """Process login request with basic auth.
Return a coroutine. Return a coroutine.
@ -51,8 +53,11 @@ class APIAuth(CoreSysAttributes):
return self.sys_auth.check_login(addon, auth.login, auth.password) return self.sys_auth.check_login(addon, auth.login, auth.password)
def _process_dict( def _process_dict(
self, request: web.Request, addon: Addon, data: dict[str, str] self,
) -> bool: request: web.Request,
addon: Addon,
data: dict[str, Any] | MultiDictProxy[str | bytes | FileField],
) -> Awaitable[bool]:
"""Process login with dict data. """Process login with dict data.
Return a coroutine. Return a coroutine.
@ -60,14 +65,22 @@ class APIAuth(CoreSysAttributes):
username = data.get("username") or data.get("user") username = data.get("username") or data.get("user")
password = data.get("password") password = data.get("password")
return self.sys_auth.check_login(addon, username, password) # Test that we did receive strings and not something else, raise if so
try:
_ = username.encode and password.encode # type: ignore
except AttributeError:
raise HTTPUnauthorized(headers=REALM_HEADER) from None
return self.sys_auth.check_login(
addon, cast(str, username), cast(str, password)
)
@api_process @api_process
async def auth(self, request: web.Request) -> bool: async def auth(self, request: web.Request) -> bool:
"""Process login request.""" """Process login request."""
addon = request[REQUEST_FROM] addon = request[REQUEST_FROM]
if not addon.access_auth_api: if not isinstance(addon, Addon) or not addon.access_auth_api:
raise APIForbidden("Can't use Home Assistant auth!") raise APIForbidden("Can't use Home Assistant auth!")
# BasicAuth # BasicAuth
@ -79,13 +92,18 @@ class APIAuth(CoreSysAttributes):
# Json # Json
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON: if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_JSON:
data = await request.json(loads=json_loads) data = await request.json(loads=json_loads)
return await self._process_dict(request, addon, data) if not await self._process_dict(request, addon, data):
raise HTTPUnauthorized()
return True
# URL encoded # URL encoded
if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL: if request.headers.get(CONTENT_TYPE) == CONTENT_TYPE_URL:
data = await request.post() data = await request.post()
return await self._process_dict(request, addon, data) if not await self._process_dict(request, addon, data):
raise HTTPUnauthorized()
return True
# Advertise Basic authentication by default
raise HTTPUnauthorized(headers=REALM_HEADER) raise HTTPUnauthorized(headers=REALM_HEADER)
@api_process @api_process

View File

@ -10,9 +10,9 @@ import logging
from pathlib import Path from pathlib import Path
import re import re
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any, cast
from aiohttp import web from aiohttp import BodyPartReader, 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 voluptuous.humanize import humanize_error
@ -36,7 +36,6 @@ 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,
@ -52,7 +51,7 @@ from ..const import (
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APIForbidden, APINotFound from ..exceptions import APIError, APIForbidden, APINotFound
from ..jobs import JobSchedulerOptions from ..jobs import JobSchedulerOptions, SupervisorJob
from ..mounts.const import MountUsage from ..mounts.const import MountUsage
from ..resolution.const import UnhealthyReason from ..resolution.const import UnhealthyReason
from .const import ( from .const import (
@ -155,8 +154,8 @@ class APIBackups(CoreSysAttributes):
"""Make location attributes dictionary.""" """Make location attributes dictionary."""
return { return {
loc if loc else LOCATION_LOCAL: { loc if loc else LOCATION_LOCAL: {
ATTR_PROTECTED: backup.all_locations[loc][ATTR_PROTECTED], ATTR_PROTECTED: backup.all_locations[loc].protected,
ATTR_SIZE_BYTES: backup.all_locations[loc][ATTR_SIZE_BYTES], ATTR_SIZE_BYTES: backup.all_locations[loc].size_bytes,
} }
for loc in backup.locations for loc in backup.locations
} }
@ -187,7 +186,7 @@ class APIBackups(CoreSysAttributes):
] ]
@api_process @api_process
async def list(self, request): async def list_backups(self, request):
"""Return backup list.""" """Return backup list."""
data_backups = self._list_backups() data_backups = self._list_backups()
@ -261,7 +260,7 @@ class APIBackups(CoreSysAttributes):
def _location_to_mount(self, location: str | None) -> LOCATION_TYPE: def _location_to_mount(self, location: str | None) -> LOCATION_TYPE:
"""Convert a single location to a mount if possible.""" """Convert a single location to a mount if possible."""
if not location or location == LOCATION_CLOUD_BACKUP: if not location or location == LOCATION_CLOUD_BACKUP:
return location return cast(LOCATION_TYPE, location)
mount = self.sys_mounts.get(location) mount = self.sys_mounts.get(location)
if mount.usage != MountUsage.BACKUP: if mount.usage != MountUsage.BACKUP:
@ -295,8 +294,11 @@ class APIBackups(CoreSysAttributes):
) -> tuple[asyncio.Task, str]: ) -> tuple[asyncio.Task, str]:
"""Start backup task in background and return task and job ID.""" """Start backup task in background and return task and job ID."""
event = asyncio.Event() event = asyncio.Event()
job, backup_task = self.sys_jobs.schedule_job( job, backup_task = cast(
backup_method, JobSchedulerOptions(), *args, **kwargs tuple[SupervisorJob, asyncio.Task],
self.sys_jobs.schedule_job(
backup_method, JobSchedulerOptions(), *args, **kwargs
),
) )
async def release_on_freeze(new_state: CoreState): async def release_on_freeze(new_state: CoreState):
@ -311,10 +313,7 @@ class APIBackups(CoreSysAttributes):
try: try:
event_task = self.sys_create_task(event.wait()) event_task = self.sys_create_task(event.wait())
_, pending = await asyncio.wait( _, pending = await asyncio.wait(
( (backup_task, event_task),
backup_task,
event_task,
),
return_when=asyncio.FIRST_COMPLETED, return_when=asyncio.FIRST_COMPLETED,
) )
# It seems backup returned early (error or something), make sure to cancel # It seems backup returned early (error or something), make sure to cancel
@ -473,9 +472,9 @@ class APIBackups(CoreSysAttributes):
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)
filename = backup.all_locations[location][ATTR_PATH] filename = backup.all_locations[location].path
# If the file is missing, return 404 and trigger reload of location # If the file is missing, return 404 and trigger reload of location
if not filename.is_file(): if not await self.sys_run_in_executor(filename.is_file):
self.sys_create_task(self.sys_backups.reload(location)) self.sys_create_task(self.sys_backups.reload(location))
return web.Response(status=404) return web.Response(status=404)
@ -495,10 +494,12 @@ class APIBackups(CoreSysAttributes):
"""Upload a backup file.""" """Upload a backup file."""
location: LOCATION_TYPE = None location: LOCATION_TYPE = None
locations: list[LOCATION_TYPE] | None = None locations: list[LOCATION_TYPE] | None = None
tmp_path = self.sys_config.path_tmp
if ATTR_LOCATION in request.query: if ATTR_LOCATION in request.query:
location_names: list[str] = request.query.getall(ATTR_LOCATION) location_names: list[str] = request.query.getall(ATTR_LOCATION, [])
self._validate_cloud_backup_location(request, location_names) self._validate_cloud_backup_location(
request, cast(list[str | None], location_names)
)
# Convert empty string to None if necessary # Convert empty string to None if necessary
locations = [ locations = [
self._location_to_mount(location) self._location_to_mount(location)
@ -508,9 +509,6 @@ class APIBackups(CoreSysAttributes):
] ]
location = locations.pop(0) location = locations.pop(0)
if location and location != LOCATION_CLOUD_BACKUP:
tmp_path = location.local_where
filename: str | None = None filename: str | None = None
if ATTR_FILENAME in request.query: if ATTR_FILENAME in request.query:
filename = request.query.get(ATTR_FILENAME) filename = request.query.get(ATTR_FILENAME)
@ -519,18 +517,21 @@ class APIBackups(CoreSysAttributes):
except vol.Invalid as ex: except vol.Invalid as ex:
raise APIError(humanize_error(filename, ex)) from None raise APIError(humanize_error(filename, ex)) from None
tmp_path = await self.sys_backups.get_upload_path_for_location(location)
temp_dir: TemporaryDirectory | None = None temp_dir: TemporaryDirectory | None = None
backup_file_stream: IOBase | None = None backup_file_stream: IOBase | None = None
def open_backup_file() -> Path: def open_backup_file() -> Path:
nonlocal temp_dir, backup_file_stream nonlocal temp_dir, backup_file_stream
temp_dir = TemporaryDirectory(dir=tmp_path.as_posix()) temp_dir = TemporaryDirectory(dir=tmp_path.as_posix())
tar_file = Path(temp_dir.name, "backup.tar") tar_file = Path(temp_dir.name, "upload.tar")
backup_file_stream = tar_file.open("wb") backup_file_stream = tar_file.open("wb")
return tar_file return tar_file
def close_backup_file() -> None: def close_backup_file() -> None:
if backup_file_stream: if backup_file_stream:
# Make sure it got closed, in case of exception. It is safe to
# close the file stream twice.
backup_file_stream.close() backup_file_stream.close()
if temp_dir: if temp_dir:
temp_dir.cleanup() temp_dir.cleanup()
@ -538,9 +539,15 @@ class APIBackups(CoreSysAttributes):
try: try:
reader = await request.multipart() reader = await request.multipart()
contents = await reader.next() contents = await reader.next()
if not isinstance(contents, BodyPartReader):
raise APIError("Improperly formatted upload, could not read backup")
tar_file = await self.sys_run_in_executor(open_backup_file) tar_file = await self.sys_run_in_executor(open_backup_file)
while chunk := await contents.read_chunk(size=2**16): while chunk := await contents.read_chunk(size=2**16):
await self.sys_run_in_executor(backup_file_stream.write, chunk) await self.sys_run_in_executor(
cast(IOBase, backup_file_stream).write, chunk
)
await self.sys_run_in_executor(cast(IOBase, backup_file_stream).close)
backup = await asyncio.shield( backup = await asyncio.shield(
self.sys_backups.import_backup( self.sys_backups.import_backup(
@ -555,7 +562,9 @@ class APIBackups(CoreSysAttributes):
LOCATION_CLOUD_BACKUP, LOCATION_CLOUD_BACKUP,
None, None,
}: }:
self.sys_resolution.unhealthy = UnhealthyReason.OSERROR_BAD_MESSAGE self.sys_resolution.add_unhealthy_reason(
UnhealthyReason.OSERROR_BAD_MESSAGE
)
_LOGGER.error("Can't write new backup file: %s", err) _LOGGER.error("Can't write new backup file: %s", err)
return False return False
@ -563,8 +572,7 @@ class APIBackups(CoreSysAttributes):
return False return False
finally: finally:
if temp_dir or backup: await self.sys_run_in_executor(close_backup_file)
await self.sys_run_in_executor(close_backup_file)
if backup: if backup:
return {ATTR_SLUG: backup.slug} return {ATTR_SLUG: backup.slug}

View File

@ -80,3 +80,11 @@ class BootSlot(StrEnum):
A = "A" A = "A"
B = "B" B = "B"
class DetectBlockingIO(StrEnum):
"""Enable/Disable detection for blocking I/O in event loop."""
OFF = "off"
ON = "on"
ON_AT_STARTUP = "on-at-startup"

View File

@ -1,7 +1,9 @@
"""Init file for Supervisor network RESTful API.""" """Init file for Supervisor network RESTful API."""
import logging import logging
from typing import Any
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from ..addons.addon import Addon from ..addons.addon import Addon
@ -16,6 +18,7 @@ from ..const import (
AddonState, AddonState,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..discovery import Message
from ..exceptions import APIForbidden, APINotFound from ..exceptions import APIForbidden, APINotFound
from .utils import api_process, api_validate, require_home_assistant from .utils import api_process, api_validate, require_home_assistant
@ -32,16 +35,16 @@ SCHEMA_DISCOVERY = vol.Schema(
class APIDiscovery(CoreSysAttributes): class APIDiscovery(CoreSysAttributes):
"""Handle RESTful API for discovery functions.""" """Handle RESTful API for discovery functions."""
def _extract_message(self, request): def _extract_message(self, request: web.Request) -> Message:
"""Extract discovery message from URL.""" """Extract discovery message from URL."""
message = self.sys_discovery.get(request.match_info.get("uuid")) message = self.sys_discovery.get(request.match_info["uuid"])
if not message: if not message:
raise APINotFound("Discovery message not found") raise APINotFound("Discovery message not found")
return message return message
@api_process @api_process
@require_home_assistant @require_home_assistant
async def list(self, request): async def list_discovery(self, request: web.Request) -> dict[str, Any]:
"""Show registered and available services.""" """Show registered and available services."""
# Get available discovery # Get available discovery
discovery = [ discovery = [
@ -52,12 +55,16 @@ class APIDiscovery(CoreSysAttributes):
ATTR_CONFIG: message.config, ATTR_CONFIG: message.config,
} }
for message in self.sys_discovery.list_messages for message in self.sys_discovery.list_messages
if (addon := self.sys_addons.get(message.addon, local_only=True)) if (
and addon.state == AddonState.STARTED discovered := self.sys_addons.get_local_only(
message.addon,
)
)
and discovered.state == AddonState.STARTED
] ]
# Get available services/add-ons # Get available services/add-ons
services = {} services: dict[str, list[str]] = {}
for addon in self.sys_addons.all: for addon in self.sys_addons.all:
for name in addon.discovery: for name in addon.discovery:
services.setdefault(name, []).append(addon.slug) services.setdefault(name, []).append(addon.slug)
@ -65,7 +72,7 @@ class APIDiscovery(CoreSysAttributes):
return {ATTR_DISCOVERY: discovery, ATTR_SERVICES: services} return {ATTR_DISCOVERY: discovery, ATTR_SERVICES: services}
@api_process @api_process
async def set_discovery(self, request): async def set_discovery(self, request: web.Request) -> dict[str, str]:
"""Write data into a discovery pipeline.""" """Write data into a discovery pipeline."""
body = await api_validate(SCHEMA_DISCOVERY, request) body = await api_validate(SCHEMA_DISCOVERY, request)
addon: Addon = request[REQUEST_FROM] addon: Addon = request[REQUEST_FROM]
@ -89,7 +96,7 @@ class APIDiscovery(CoreSysAttributes):
@api_process @api_process
@require_home_assistant @require_home_assistant
async def get_discovery(self, request): async def get_discovery(self, request: web.Request) -> dict[str, Any]:
"""Read data into a discovery message.""" """Read data into a discovery message."""
message = self._extract_message(request) message = self._extract_message(request)
@ -101,7 +108,7 @@ class APIDiscovery(CoreSysAttributes):
} }
@api_process @api_process
async def del_discovery(self, request): async def del_discovery(self, request: web.Request) -> None:
"""Delete data into a discovery message.""" """Delete data into a discovery message."""
message = self._extract_message(request) message = self._extract_message(request)
addon = request[REQUEST_FROM] addon = request[REQUEST_FROM]
@ -111,4 +118,3 @@ class APIDiscovery(CoreSysAttributes):
raise APIForbidden("Can't remove discovery message") raise APIForbidden("Can't remove discovery message")
await self.sys_discovery.remove(message) await self.sys_discovery.remove(message)
return True

View File

@ -7,6 +7,7 @@ from aiohttp import web
import voluptuous as vol import voluptuous as vol
from ..const import ( from ..const import (
ATTR_ENABLE_IPV6,
ATTR_HOSTNAME, ATTR_HOSTNAME,
ATTR_LOGGING, ATTR_LOGGING,
ATTR_PASSWORD, ATTR_PASSWORD,
@ -30,10 +31,39 @@ SCHEMA_DOCKER_REGISTRY = vol.Schema(
} }
) )
# pylint: disable=no-value-for-parameter
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_ENABLE_IPV6): vol.Boolean()})
class APIDocker(CoreSysAttributes): class APIDocker(CoreSysAttributes):
"""Handle RESTful API for Docker configuration.""" """Handle RESTful API for Docker configuration."""
@api_process
async def info(self, request: web.Request):
"""Get docker info."""
data_registries = {}
for hostname, registry in self.sys_docker.config.registries.items():
data_registries[hostname] = {
ATTR_USERNAME: registry[ATTR_USERNAME],
}
return {
ATTR_VERSION: self.sys_docker.info.version,
ATTR_ENABLE_IPV6: self.sys_docker.config.enable_ipv6,
ATTR_STORAGE: self.sys_docker.info.storage,
ATTR_LOGGING: self.sys_docker.info.logging,
ATTR_REGISTRIES: data_registries,
}
@api_process
async def options(self, request: web.Request) -> None:
"""Set docker options."""
body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_ENABLE_IPV6 in body:
self.sys_docker.config.enable_ipv6 = body[ATTR_ENABLE_IPV6]
await self.sys_docker.config.save_data()
@api_process @api_process
async def registries(self, request) -> dict[str, Any]: async def registries(self, request) -> dict[str, Any]:
"""Return the list of registries.""" """Return the list of registries."""
@ -64,18 +94,3 @@ class APIDocker(CoreSysAttributes):
del self.sys_docker.config.registries[hostname] del self.sys_docker.config.registries[hostname]
await self.sys_docker.config.save_data() await self.sys_docker.config.save_data()
@api_process
async def info(self, request: web.Request):
"""Get docker info."""
data_registries = {}
for hostname, registry in self.sys_docker.config.registries.items():
data_registries[hostname] = {
ATTR_USERNAME: registry[ATTR_USERNAME],
}
return {
ATTR_VERSION: self.sys_docker.info.version,
ATTR_STORAGE: self.sys_docker.info.storage,
ATTR_LOGGING: self.sys_docker.info.logging,
ATTR_REGISTRIES: data_registries,
}

View File

@ -68,7 +68,10 @@ def filesystem_struct(fs_block: UDisks2Block) -> dict[str, Any]:
ATTR_NAME: fs_block.id_label, ATTR_NAME: fs_block.id_label,
ATTR_SYSTEM: fs_block.hint_system, ATTR_SYSTEM: fs_block.hint_system,
ATTR_MOUNT_POINTS: [ ATTR_MOUNT_POINTS: [
str(mount_point) for mount_point in fs_block.filesystem.mount_points str(mount_point)
for mount_point in (
fs_block.filesystem.mount_points if fs_block.filesystem else []
)
], ],
} }

View File

@ -118,7 +118,7 @@ class APIHomeAssistant(CoreSysAttributes):
body = await api_validate(SCHEMA_OPTIONS, request) body = await api_validate(SCHEMA_OPTIONS, request)
if ATTR_IMAGE in body: if ATTR_IMAGE in body:
self.sys_homeassistant.image = body[ATTR_IMAGE] self.sys_homeassistant.set_image(body[ATTR_IMAGE])
self.sys_homeassistant.override_image = ( self.sys_homeassistant.override_image = (
self.sys_homeassistant.image != self.sys_homeassistant.default_image self.sys_homeassistant.image != self.sys_homeassistant.default_image
) )

View File

@ -3,8 +3,9 @@
import asyncio import asyncio
from contextlib import suppress from contextlib import suppress
import logging import logging
from typing import Any
from aiohttp import ClientConnectionResetError, web from aiohttp import ClientConnectionResetError, ClientPayloadError, web
from aiohttp.hdrs import ACCEPT, RANGE from aiohttp.hdrs import ACCEPT, RANGE
import voluptuous as vol import voluptuous as vol
from voluptuous.error import CoerceInvalid from voluptuous.error import CoerceInvalid
@ -36,6 +37,7 @@ from ..host.const import (
LogFormat, LogFormat,
LogFormatter, LogFormatter,
) )
from ..host.logs import SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX
from ..utils.systemd_journal import journal_logs_reader from ..utils.systemd_journal import journal_logs_reader
from .const import ( from .const import (
ATTR_AGENT_VERSION, ATTR_AGENT_VERSION,
@ -195,20 +197,18 @@ class APIHost(CoreSysAttributes):
) -> web.StreamResponse: ) -> web.StreamResponse:
"""Return systemd-journald logs.""" """Return systemd-journald logs."""
log_formatter = LogFormatter.PLAIN log_formatter = LogFormatter.PLAIN
params = {} params: dict[str, Any] = {}
if identifier: if identifier:
params[PARAM_SYSLOG_IDENTIFIER] = identifier params[PARAM_SYSLOG_IDENTIFIER] = identifier
elif IDENTIFIER in request.match_info: elif IDENTIFIER in request.match_info:
params[PARAM_SYSLOG_IDENTIFIER] = request.match_info.get(IDENTIFIER) params[PARAM_SYSLOG_IDENTIFIER] = request.match_info[IDENTIFIER]
else: else:
params[PARAM_SYSLOG_IDENTIFIER] = self.sys_host.logs.default_identifiers params[PARAM_SYSLOG_IDENTIFIER] = self.sys_host.logs.default_identifiers
# host logs should be always verbose, no matter what Accept header is used # host logs should be always verbose, no matter what Accept header is used
log_formatter = LogFormatter.VERBOSE log_formatter = LogFormatter.VERBOSE
if BOOTID in request.match_info: if BOOTID in request.match_info:
params[PARAM_BOOT_ID] = await self._get_boot_id( params[PARAM_BOOT_ID] = await self._get_boot_id(request.match_info[BOOTID])
request.match_info.get(BOOTID)
)
if follow: if follow:
params[PARAM_FOLLOW] = "" params[PARAM_FOLLOW] = ""
@ -239,13 +239,11 @@ 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}:{SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX if follow else lines}"
elif RANGE in request.headers: elif RANGE in request.headers:
range_header = request.headers.get(RANGE) range_header = request.headers[RANGE]
else: else:
range_header = ( range_header = f"entries=:-{DEFAULT_LINES - 1}:{SYSTEMD_JOURNAL_GATEWAYD_LINES_MAX 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(
params=params, range_header=range_header, accept=LogFormat.JOURNAL params=params, range_header=range_header, accept=LogFormat.JOURNAL
@ -255,17 +253,31 @@ class APIHost(CoreSysAttributes):
response.content_type = CONTENT_TYPE_TEXT response.content_type = CONTENT_TYPE_TEXT
headers_returned = False headers_returned = False
async for cursor, line in journal_logs_reader(resp, log_formatter): async for cursor, line in journal_logs_reader(resp, log_formatter):
if not headers_returned: try:
if cursor: if not headers_returned:
response.headers["X-First-Cursor"] = cursor if cursor:
response.headers["X-Accel-Buffering"] = "no" response.headers["X-First-Cursor"] = cursor
await response.prepare(request) response.headers["X-Accel-Buffering"] = "no"
headers_returned = True await response.prepare(request)
# When client closes the connection while reading busy logs, we headers_returned = True
# sometimes get this exception. It should be safe to ignore it.
with suppress(ClientConnectionResetError):
await response.write(line.encode("utf-8") + b"\n") await response.write(line.encode("utf-8") + b"\n")
except ConnectionResetError as ex: except ClientConnectionResetError as err:
# When client closes the connection while reading busy logs, we
# sometimes get this exception. It should be safe to ignore it.
_LOGGER.debug(
"ClientConnectionResetError raised when returning journal logs: %s",
err,
)
break
except ConnectionError as err:
_LOGGER.warning(
"%s raised when returning journal logs: %s",
type(err).__name__,
err,
)
break
except (ConnectionResetError, ClientPayloadError) as ex:
# ClientPayloadError is most likely caused by the closing the connection
raise APIError( raise APIError(
"Connection reset when trying to fetch data from systemd-journald." "Connection reset when trying to fetch data from systemd-journald."
) from ex ) from ex

View File

@ -83,7 +83,7 @@ class APIIngress(CoreSysAttributes):
def _extract_addon(self, request: web.Request) -> Addon: def _extract_addon(self, request: web.Request) -> Addon:
"""Return addon, throw an exception it it doesn't exist.""" """Return addon, throw an exception it it doesn't exist."""
token = request.match_info.get("token") token = request.match_info["token"]
# Find correct add-on # Find correct add-on
addon = self.sys_ingress.get(token) addon = self.sys_ingress.get(token)
@ -132,7 +132,7 @@ class APIIngress(CoreSysAttributes):
@api_process @api_process
@require_home_assistant @require_home_assistant
async def validate_session(self, request: web.Request) -> dict[str, Any]: async def validate_session(self, request: web.Request) -> None:
"""Validate session and extending how long it's valid for.""" """Validate session and extending how long it's valid for."""
data = await api_validate(VALIDATE_SESSION_DATA, request) data = await api_validate(VALIDATE_SESSION_DATA, request)
@ -147,14 +147,14 @@ class APIIngress(CoreSysAttributes):
"""Route data to Supervisor ingress service.""" """Route data to Supervisor ingress service."""
# Check Ingress Session # Check Ingress Session
session = request.cookies.get(COOKIE_INGRESS) session = request.cookies.get(COOKIE_INGRESS, "")
if not self.sys_ingress.validate_session(session): if not self.sys_ingress.validate_session(session):
_LOGGER.warning("No valid ingress session %s", session) _LOGGER.warning("No valid ingress session %s", session)
raise HTTPUnauthorized() raise HTTPUnauthorized()
# Process requests # Process requests
addon = self._extract_addon(request) addon = self._extract_addon(request)
path = request.match_info.get("path") path = request.match_info.get("path", "")
session_data = self.sys_ingress.get_session_data(session) session_data = self.sys_ingress.get_session_data(session)
try: try:
# Websocket # Websocket
@ -183,7 +183,7 @@ class APIIngress(CoreSysAttributes):
for proto in request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") for proto in request.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",")
] ]
else: else:
req_protocols = () req_protocols = []
ws_server = web.WebSocketResponse( ws_server = web.WebSocketResponse(
protocols=req_protocols, autoclose=False, autoping=False protocols=req_protocols, autoclose=False, autoping=False
@ -279,7 +279,7 @@ class APIIngress(CoreSysAttributes):
try: try:
response.headers["X-Accel-Buffering"] = "no" 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_chunks():
await response.write(data) await response.write(data)
except ( except (
@ -309,9 +309,9 @@ class APIIngress(CoreSysAttributes):
def _init_header( def _init_header(
request: web.Request, addon: Addon, session_data: IngressSessionData | None request: web.Request, addon: Addon, session_data: IngressSessionData | None
) -> CIMultiDict | dict[str, str]: ) -> CIMultiDict[str]:
"""Create initial header.""" """Create initial header."""
headers = {} headers = CIMultiDict[str]()
if session_data is not None: if session_data is not None:
headers[HEADER_REMOTE_USER_ID] = session_data.user.id headers[HEADER_REMOTE_USER_ID] = session_data.user.id
@ -337,19 +337,20 @@ def _init_header(
istr(HEADER_REMOTE_USER_DISPLAY_NAME), istr(HEADER_REMOTE_USER_DISPLAY_NAME),
): ):
continue continue
headers[name] = value headers.add(name, value)
# Update X-Forwarded-For # Update X-Forwarded-For
forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) if request.transport:
connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) forward_for = request.headers.get(hdrs.X_FORWARDED_FOR)
headers[hdrs.X_FORWARDED_FOR] = f"{forward_for}, {connected_ip!s}" connected_ip = ip_address(request.transport.get_extra_info("peername")[0])
headers[hdrs.X_FORWARDED_FOR] = f"{forward_for}, {connected_ip!s}"
return headers return headers
def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]: def _response_header(response: aiohttp.ClientResponse) -> CIMultiDict[str]:
"""Create response header.""" """Create response header."""
headers = {} headers = CIMultiDict[str]()
for name, value in response.headers.items(): for name, value in response.headers.items():
if name in ( if name in (
@ -359,7 +360,7 @@ def _response_header(response: aiohttp.ClientResponse) -> dict[str, str]:
hdrs.CONTENT_ENCODING, hdrs.CONTENT_ENCODING,
): ):
continue continue
headers[name] = value headers.add(name, value)
return headers return headers

View File

@ -26,7 +26,7 @@ class APIJobs(CoreSysAttributes):
def _extract_job(self, request: web.Request) -> SupervisorJob: def _extract_job(self, request: web.Request) -> SupervisorJob:
"""Extract job from request or raise.""" """Extract job from request or raise."""
try: try:
return self.sys_jobs.get_job(request.match_info.get("uuid")) return self.sys_jobs.get_job(request.match_info["uuid"])
except JobNotFound: except JobNotFound:
raise APINotFound("Job does not exist") from None raise APINotFound("Job does not exist") from None
@ -71,7 +71,10 @@ class APIJobs(CoreSysAttributes):
if current_job.uuid in jobs_by_parent: if current_job.uuid in jobs_by_parent:
queue.extend( queue.extend(
[(child_jobs, job) for job in jobs_by_parent.get(current_job.uuid)] [
(child_jobs, job)
for job in jobs_by_parent.get(current_job.uuid, [])
]
) )
return job_list return job_list

View File

@ -1,11 +1,12 @@
"""Handle security part of this API.""" """Handle security part of this API."""
from collections.abc import Callable
import logging import logging
import re import re
from typing import Final from typing import Final
from urllib.parse import unquote from urllib.parse import unquote
from aiohttp.web import Request, RequestHandler, Response, middleware from aiohttp.web import Request, Response, middleware
from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized from aiohttp.web_exceptions import HTTPBadRequest, HTTPForbidden, HTTPUnauthorized
from awesomeversion import AwesomeVersion from awesomeversion import AwesomeVersion
@ -19,11 +20,11 @@ from ...const import (
ROLE_DEFAULT, ROLE_DEFAULT,
ROLE_HOMEASSISTANT, ROLE_HOMEASSISTANT,
ROLE_MANAGER, ROLE_MANAGER,
CoreState, VALID_API_STATES,
) )
from ...coresys import CoreSys, CoreSysAttributes from ...coresys import CoreSys, CoreSysAttributes
from ...utils import version_is_new_enough from ...utils import version_is_new_enough
from ..utils import api_return_error, excract_supervisor_token from ..utils import api_return_error, extract_supervisor_token
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
_CORE_VERSION: Final = AwesomeVersion("2023.3.4") _CORE_VERSION: Final = AwesomeVersion("2023.3.4")
@ -179,9 +180,7 @@ class SecurityMiddleware(CoreSysAttributes):
return unquoted return unquoted
@middleware @middleware
async def block_bad_requests( async def block_bad_requests(self, request: Request, handler: Callable) -> Response:
self, request: Request, handler: RequestHandler
) -> Response:
"""Process request and tblock commonly known exploit attempts.""" """Process request and tblock commonly known exploit attempts."""
if FILTERS.search(self._recursive_unquote(request.path)): if FILTERS.search(self._recursive_unquote(request.path)):
_LOGGER.warning( _LOGGER.warning(
@ -199,15 +198,9 @@ class SecurityMiddleware(CoreSysAttributes):
return await handler(request) return await handler(request)
@middleware @middleware
async def system_validation( async def system_validation(self, request: Request, handler: Callable) -> Response:
self, request: Request, handler: RequestHandler
) -> Response:
"""Check if core is ready to response.""" """Check if core is ready to response."""
if self.sys_core.state not in ( if self.sys_core.state not in VALID_API_STATES:
CoreState.STARTUP,
CoreState.RUNNING,
CoreState.FREEZE,
):
return api_return_error( return api_return_error(
message=f"System is not ready with state: {self.sys_core.state}" message=f"System is not ready with state: {self.sys_core.state}"
) )
@ -215,12 +208,10 @@ class SecurityMiddleware(CoreSysAttributes):
return await handler(request) return await handler(request)
@middleware @middleware
async def token_validation( async def token_validation(self, request: Request, handler: Callable) -> Response:
self, request: Request, handler: RequestHandler
) -> Response:
"""Check security access of this layer.""" """Check security access of this layer."""
request_from = None request_from: CoreSysAttributes | None = None
supervisor_token = excract_supervisor_token(request) supervisor_token = extract_supervisor_token(request)
# Blacklist # Blacklist
if BLACKLIST.match(request.path): if BLACKLIST.match(request.path):
@ -288,7 +279,7 @@ class SecurityMiddleware(CoreSysAttributes):
raise HTTPForbidden() raise HTTPForbidden()
@middleware @middleware
async def core_proxy(self, request: Request, handler: RequestHandler) -> Response: async def core_proxy(self, request: Request, handler: Callable) -> Response:
"""Validate user from Core API proxy.""" """Validate user from Core API proxy."""
if ( if (
request[REQUEST_FROM] != self.sys_homeassistant request[REQUEST_FROM] != self.sys_homeassistant

View File

@ -1,6 +1,6 @@
"""Inits file for supervisor mounts REST API.""" """Inits file for supervisor mounts REST API."""
from typing import Any from typing import Any, cast
from aiohttp import web from aiohttp import web
import voluptuous as vol import voluptuous as vol
@ -10,7 +10,7 @@ from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APINotFound from ..exceptions import APIError, APINotFound
from ..mounts.const import ATTR_DEFAULT_BACKUP_MOUNT, MountUsage from ..mounts.const import ATTR_DEFAULT_BACKUP_MOUNT, MountUsage
from ..mounts.mount import Mount from ..mounts.mount import Mount
from ..mounts.validate import SCHEMA_MOUNT_CONFIG from ..mounts.validate import SCHEMA_MOUNT_CONFIG, MountData
from .const import ATTR_MOUNTS, ATTR_USER_PATH from .const import ATTR_MOUNTS, ATTR_USER_PATH
from .utils import api_process, api_validate from .utils import api_process, api_validate
@ -26,7 +26,7 @@ class APIMounts(CoreSysAttributes):
def _extract_mount(self, request: web.Request) -> Mount: def _extract_mount(self, request: web.Request) -> Mount:
"""Extract mount from request or raise.""" """Extract mount from request or raise."""
name = request.match_info.get("mount") name = request.match_info["mount"]
if name not in self.sys_mounts: if name not in self.sys_mounts:
raise APINotFound(f"No mount exists with name {name}") raise APINotFound(f"No mount exists with name {name}")
return self.sys_mounts.get(name) return self.sys_mounts.get(name)
@ -71,10 +71,10 @@ class APIMounts(CoreSysAttributes):
@api_process @api_process
async def create_mount(self, request: web.Request) -> None: async def create_mount(self, request: web.Request) -> None:
"""Create a new mount in supervisor.""" """Create a new mount in supervisor."""
body = await api_validate(SCHEMA_MOUNT_CONFIG, request) body = cast(MountData, await api_validate(SCHEMA_MOUNT_CONFIG, request))
if body[ATTR_NAME] in self.sys_mounts: if body["name"] in self.sys_mounts:
raise APIError(f"A mount already exists with name {body[ATTR_NAME]}") raise APIError(f"A mount already exists with name {body['name']}")
mount = Mount.from_dict(self.coresys, body) mount = Mount.from_dict(self.coresys, body)
await self.sys_mounts.create_mount(mount) await self.sys_mounts.create_mount(mount)
@ -97,7 +97,10 @@ class APIMounts(CoreSysAttributes):
{vol.Optional(ATTR_NAME, default=current.name): current.name}, {vol.Optional(ATTR_NAME, default=current.name): current.name},
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
body = await api_validate(vol.All(name_schema, SCHEMA_MOUNT_CONFIG), request) body = cast(
MountData,
await api_validate(vol.All(name_schema, SCHEMA_MOUNT_CONFIG), request),
)
mount = Mount.from_dict(self.coresys, body) mount = Mount.from_dict(self.coresys, body)
await self.sys_mounts.create_mount(mount) await self.sys_mounts.create_mount(mount)

View File

@ -10,6 +10,7 @@ import voluptuous as vol
from ..const import ( from ..const import (
ATTR_ACCESSPOINTS, ATTR_ACCESSPOINTS,
ATTR_ADDR_GEN_MODE,
ATTR_ADDRESS, ATTR_ADDRESS,
ATTR_AUTH, ATTR_AUTH,
ATTR_CONNECTED, ATTR_CONNECTED,
@ -22,6 +23,7 @@ from ..const import (
ATTR_ID, ATTR_ID,
ATTR_INTERFACE, ATTR_INTERFACE,
ATTR_INTERFACES, ATTR_INTERFACES,
ATTR_IP6_PRIVACY,
ATTR_IPV4, ATTR_IPV4,
ATTR_IPV6, ATTR_IPV6,
ATTR_MAC, ATTR_MAC,
@ -38,15 +40,18 @@ from ..const import (
ATTR_TYPE, ATTR_TYPE,
ATTR_VLAN, ATTR_VLAN,
ATTR_WIFI, ATTR_WIFI,
DOCKER_IPV4_NETWORK_MASK,
DOCKER_NETWORK, DOCKER_NETWORK,
DOCKER_NETWORK_MASK,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import APIError, APINotFound, HostNetworkNotFound from ..exceptions import APIError, APINotFound, HostNetworkNotFound
from ..host.configuration import ( from ..host.configuration import (
AccessPoint, AccessPoint,
Interface, Interface,
InterfaceAddrGenMode,
InterfaceIp6Privacy,
InterfaceMethod, InterfaceMethod,
Ip6Setting,
IpConfig, IpConfig,
IpSetting, IpSetting,
VlanConfig, VlanConfig,
@ -68,6 +73,8 @@ _SCHEMA_IPV6_CONFIG = vol.Schema(
{ {
vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)], vol.Optional(ATTR_ADDRESS): [vol.Coerce(IPv6Interface)],
vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod), vol.Optional(ATTR_METHOD): vol.Coerce(InterfaceMethod),
vol.Optional(ATTR_ADDR_GEN_MODE): vol.Coerce(InterfaceAddrGenMode),
vol.Optional(ATTR_IP6_PRIVACY): vol.Coerce(InterfaceIp6Privacy),
vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv6Address), vol.Optional(ATTR_GATEWAY): vol.Coerce(IPv6Address),
vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv6Address)], vol.Optional(ATTR_NAMESERVERS): [vol.Coerce(IPv6Address)],
} }
@ -94,8 +101,8 @@ SCHEMA_UPDATE = vol.Schema(
) )
def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]: def ip4config_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
"""Return a dict with information about ip configuration.""" """Return a dict with information about IPv4 configuration."""
return { return {
ATTR_METHOD: setting.method, ATTR_METHOD: setting.method,
ATTR_ADDRESS: [address.with_prefixlen for address in config.address], ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
@ -105,6 +112,19 @@ def ipconfig_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
} }
def ip6config_struct(config: IpConfig, setting: Ip6Setting) -> dict[str, Any]:
"""Return a dict with information about IPv6 configuration."""
return {
ATTR_METHOD: setting.method,
ATTR_ADDR_GEN_MODE: setting.addr_gen_mode,
ATTR_IP6_PRIVACY: setting.ip6_privacy,
ATTR_ADDRESS: [address.with_prefixlen for address in config.address],
ATTR_NAMESERVERS: [str(address) for address in config.nameservers],
ATTR_GATEWAY: str(config.gateway) if config.gateway else None,
ATTR_READY: config.ready,
}
def wifi_struct(config: WifiConfig) -> dict[str, Any]: def wifi_struct(config: WifiConfig) -> dict[str, Any]:
"""Return a dict with information about wifi configuration.""" """Return a dict with information about wifi configuration."""
return { return {
@ -132,8 +152,12 @@ def interface_struct(interface: Interface) -> dict[str, Any]:
ATTR_CONNECTED: interface.connected, ATTR_CONNECTED: interface.connected,
ATTR_PRIMARY: interface.primary, ATTR_PRIMARY: interface.primary,
ATTR_MAC: interface.mac, ATTR_MAC: interface.mac,
ATTR_IPV4: ipconfig_struct(interface.ipv4, interface.ipv4setting), ATTR_IPV4: ip4config_struct(interface.ipv4, interface.ipv4setting)
ATTR_IPV6: ipconfig_struct(interface.ipv6, interface.ipv6setting), if interface.ipv4 and interface.ipv4setting
else None,
ATTR_IPV6: ip6config_struct(interface.ipv6, interface.ipv6setting)
if interface.ipv6 and interface.ipv6setting
else None,
ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None, ATTR_WIFI: wifi_struct(interface.wifi) if interface.wifi else None,
ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None, ATTR_VLAN: vlan_struct(interface.vlan) if interface.vlan else None,
} }
@ -179,7 +203,7 @@ class APINetwork(CoreSysAttributes):
], ],
ATTR_DOCKER: { ATTR_DOCKER: {
ATTR_INTERFACE: DOCKER_NETWORK, ATTR_INTERFACE: DOCKER_NETWORK,
ATTR_ADDRESS: str(DOCKER_NETWORK_MASK), ATTR_ADDRESS: str(DOCKER_IPV4_NETWORK_MASK),
ATTR_GATEWAY: str(self.sys_docker.network.gateway), ATTR_GATEWAY: str(self.sys_docker.network.gateway),
ATTR_DNS: str(self.sys_docker.network.dns), ATTR_DNS: str(self.sys_docker.network.dns),
}, },
@ -190,14 +214,14 @@ class APINetwork(CoreSysAttributes):
@api_process @api_process
async def interface_info(self, request: web.Request) -> dict[str, Any]: async def interface_info(self, request: web.Request) -> dict[str, Any]:
"""Return network information for a interface.""" """Return network information for a interface."""
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) interface = self._get_interface(request.match_info[ATTR_INTERFACE])
return interface_struct(interface) return interface_struct(interface)
@api_process @api_process
async def interface_update(self, request: web.Request) -> None: async def interface_update(self, request: web.Request) -> None:
"""Update the configuration of an interface.""" """Update the configuration of an interface."""
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) interface = self._get_interface(request.match_info[ATTR_INTERFACE])
# Validate data # Validate data
body = await api_validate(SCHEMA_UPDATE, request) body = await api_validate(SCHEMA_UPDATE, request)
@ -208,25 +232,31 @@ class APINetwork(CoreSysAttributes):
for key, config in body.items(): for key, config in body.items():
if key == ATTR_IPV4: if key == ATTR_IPV4:
interface.ipv4setting = IpSetting( interface.ipv4setting = IpSetting(
config.get(ATTR_METHOD, InterfaceMethod.STATIC), method=config.get(ATTR_METHOD, InterfaceMethod.STATIC),
config.get(ATTR_ADDRESS, []), address=config.get(ATTR_ADDRESS, []),
config.get(ATTR_GATEWAY), gateway=config.get(ATTR_GATEWAY),
config.get(ATTR_NAMESERVERS, []), nameservers=config.get(ATTR_NAMESERVERS, []),
) )
elif key == ATTR_IPV6: elif key == ATTR_IPV6:
interface.ipv6setting = IpSetting( interface.ipv6setting = Ip6Setting(
config.get(ATTR_METHOD, InterfaceMethod.STATIC), method=config.get(ATTR_METHOD, InterfaceMethod.STATIC),
config.get(ATTR_ADDRESS, []), addr_gen_mode=config.get(
config.get(ATTR_GATEWAY), ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT
config.get(ATTR_NAMESERVERS, []), ),
ip6_privacy=config.get(
ATTR_IP6_PRIVACY, InterfaceIp6Privacy.DEFAULT
),
address=config.get(ATTR_ADDRESS, []),
gateway=config.get(ATTR_GATEWAY),
nameservers=config.get(ATTR_NAMESERVERS, []),
) )
elif key == ATTR_WIFI: elif key == ATTR_WIFI:
interface.wifi = WifiConfig( interface.wifi = WifiConfig(
config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE), mode=config.get(ATTR_MODE, WifiMode.INFRASTRUCTURE),
config.get(ATTR_SSID, ""), ssid=config.get(ATTR_SSID, ""),
config.get(ATTR_AUTH, AuthMethod.OPEN), auth=config.get(ATTR_AUTH, AuthMethod.OPEN),
config.get(ATTR_PSK, None), psk=config.get(ATTR_PSK, None),
None, signal=None,
) )
elif key == ATTR_ENABLED: elif key == ATTR_ENABLED:
interface.enabled = config interface.enabled = config
@ -243,7 +273,7 @@ class APINetwork(CoreSysAttributes):
@api_process @api_process
async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]: async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]:
"""Scan and return a list of available networks.""" """Scan and return a list of available networks."""
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) interface = self._get_interface(request.match_info[ATTR_INTERFACE])
# Only wlan is supported # Only wlan is supported
if interface.type != InterfaceType.WIRELESS: if interface.type != InterfaceType.WIRELESS:
@ -256,8 +286,10 @@ class APINetwork(CoreSysAttributes):
@api_process @api_process
async def create_vlan(self, request: web.Request) -> None: async def create_vlan(self, request: web.Request) -> None:
"""Create a new vlan.""" """Create a new vlan."""
interface = self._get_interface(request.match_info.get(ATTR_INTERFACE)) interface = self._get_interface(request.match_info[ATTR_INTERFACE])
vlan = int(request.match_info.get(ATTR_VLAN)) vlan = int(request.match_info.get(ATTR_VLAN, -1))
if vlan < 0:
raise APIError(f"Invalid vlan specified: {vlan}")
# Only ethernet is supported # Only ethernet is supported
if interface.type != InterfaceType.ETHERNET: if interface.type != InterfaceType.ETHERNET:
@ -271,19 +303,25 @@ class APINetwork(CoreSysAttributes):
ipv4_setting = None ipv4_setting = None
if ATTR_IPV4 in body: if ATTR_IPV4 in body:
ipv4_setting = IpSetting( ipv4_setting = IpSetting(
body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO), method=body[ATTR_IPV4].get(ATTR_METHOD, InterfaceMethod.AUTO),
body[ATTR_IPV4].get(ATTR_ADDRESS, []), address=body[ATTR_IPV4].get(ATTR_ADDRESS, []),
body[ATTR_IPV4].get(ATTR_GATEWAY, None), gateway=body[ATTR_IPV4].get(ATTR_GATEWAY, None),
body[ATTR_IPV4].get(ATTR_NAMESERVERS, []), nameservers=body[ATTR_IPV4].get(ATTR_NAMESERVERS, []),
) )
ipv6_setting = None ipv6_setting = None
if ATTR_IPV6 in body: if ATTR_IPV6 in body:
ipv6_setting = IpSetting( ipv6_setting = Ip6Setting(
body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO), method=body[ATTR_IPV6].get(ATTR_METHOD, InterfaceMethod.AUTO),
body[ATTR_IPV6].get(ATTR_ADDRESS, []), addr_gen_mode=body[ATTR_IPV6].get(
body[ATTR_IPV6].get(ATTR_GATEWAY, None), ATTR_ADDR_GEN_MODE, InterfaceAddrGenMode.DEFAULT
body[ATTR_IPV6].get(ATTR_NAMESERVERS, []), ),
ip6_privacy=body[ATTR_IPV6].get(
ATTR_IP6_PRIVACY, InterfaceIp6Privacy.DEFAULT
),
address=body[ATTR_IPV6].get(ATTR_ADDRESS, []),
gateway=body[ATTR_IPV6].get(ATTR_GATEWAY, None),
nameservers=body[ATTR_IPV6].get(ATTR_NAMESERVERS, []),
) )
vlan_interface = Interface( vlan_interface = Interface(

View File

@ -3,6 +3,7 @@
import asyncio import asyncio
from collections.abc import Awaitable from collections.abc import Awaitable
import logging import logging
import re
from typing import Any from typing import Any
from aiohttp import web from aiohttp import web
@ -21,12 +22,14 @@ from ..const import (
ATTR_SERIAL, ATTR_SERIAL,
ATTR_SIZE, ATTR_SIZE,
ATTR_STATE, ATTR_STATE,
ATTR_SWAP_SIZE,
ATTR_SWAPPINESS,
ATTR_UPDATE_AVAILABLE, ATTR_UPDATE_AVAILABLE,
ATTR_VERSION, ATTR_VERSION,
ATTR_VERSION_LATEST, ATTR_VERSION_LATEST,
) )
from ..coresys import CoreSysAttributes from ..coresys import CoreSysAttributes
from ..exceptions import BoardInvalidError from ..exceptions import APINotFound, BoardInvalidError
from ..resolution.const import ContextType, IssueType, SuggestionType from ..resolution.const import ContextType, IssueType, SuggestionType
from ..validate import version_tag from ..validate import version_tag
from .const import ( from .const import (
@ -65,6 +68,15 @@ SCHEMA_GREEN_OPTIONS = vol.Schema(
vol.Optional(ATTR_SYSTEM_HEALTH_LED): vol.Boolean(), vol.Optional(ATTR_SYSTEM_HEALTH_LED): vol.Boolean(),
} }
) )
RE_SWAP_SIZE = re.compile(r"^\d+([KMG](i?B)?|B)?$", re.IGNORECASE)
SCHEMA_SWAP_OPTIONS = vol.Schema(
{
vol.Optional(ATTR_SWAP_SIZE): vol.Match(RE_SWAP_SIZE),
vol.Optional(ATTR_SWAPPINESS): vol.All(int, vol.Range(min=0, max=200)),
}
)
# pylint: enable=no-value-for-parameter # pylint: enable=no-value-for-parameter
@ -212,3 +224,53 @@ class APIOS(CoreSysAttributes):
) )
return {} return {}
@api_process
async def config_swap_info(self, request: web.Request) -> dict[str, Any]:
"""Get swap settings."""
if (
not self.coresys.os.available
or not self.coresys.os.version
or self.coresys.os.version < "15.0"
):
raise APINotFound(
"Home Assistant OS 15.0 or newer required for swap settings"
)
return {
ATTR_SWAP_SIZE: self.sys_dbus.agent.swap.swap_size,
ATTR_SWAPPINESS: self.sys_dbus.agent.swap.swappiness,
}
@api_process
async def config_swap_options(self, request: web.Request) -> None:
"""Update swap settings."""
if (
not self.coresys.os.available
or not self.coresys.os.version
or self.coresys.os.version < "15.0"
):
raise APINotFound(
"Home Assistant OS 15.0 or newer required for swap settings"
)
body = await api_validate(SCHEMA_SWAP_OPTIONS, request)
reboot_required = False
if ATTR_SWAP_SIZE in body:
old_size = self.sys_dbus.agent.swap.swap_size
await self.sys_dbus.agent.swap.set_swap_size(body[ATTR_SWAP_SIZE])
reboot_required = reboot_required or old_size != body[ATTR_SWAP_SIZE]
if ATTR_SWAPPINESS in body:
old_swappiness = self.sys_dbus.agent.swap.swappiness
await self.sys_dbus.agent.swap.set_swappiness(body[ATTR_SWAPPINESS])
reboot_required = reboot_required or old_swappiness != body[ATTR_SWAPPINESS]
if reboot_required:
self.sys_resolution.create_issue(
IssueType.REBOOT_REQUIRED,
ContextType.SYSTEM,
suggestions=[SuggestionType.EXECUTE_REBOOT],
)

View File

@ -1 +1 @@
!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")}() !function(){function d(d){var e=document.createElement("script");e.src=d,document.body.appendChild(e)}if(/Edge?\/(12[4-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Firefox\/(12[5-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\.([5-9]|\d{2,})|(1[89]|[2-9]\d|\d{3,})\.\d+)([,.]\d+|)( \(\w+\)|)( Mobile\/\w+|) Safari\/|Chrome.+OPR\/(1{2}\d|1[2-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[4-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[5-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|Android.+Chrom(ium|e)\/(12[4-9]|1[3-9]\d|[2-9]\d{2}|\d{4,})\.\d+(\.\d+|)|SamsungBrowser\/(2[5-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.35399ae87c70acf8.js')")()}catch(e){d("/api/hassio/app/frontend_es5/entrypoint.476bfed22da63267.js")}else d("/api/hassio/app/frontend_es5/entrypoint.476bfed22da63267.js")}()

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"1081.91949d686e61cc12.js","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20250401.0/src/components/ha-button-toggle-group.ts","https://raw.githubusercontent.com/home-assistant/frontend/20250401.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":"qXAWgCA,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,IACEF,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

View File

@ -1 +0,0 @@
{"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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +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"} {"version":3,"file":"12.ffa1bdc0a98802fa.js","sources":["https://raw.githubusercontent.com/home-assistant/frontend/20250401.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"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,47 @@
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @license
* Copyright 2021 Google LLC
* SPDX-LIcense-Identifier: Apache-2.0
*/
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +0,0 @@
"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