Compare commits

...

69 Commits

Author SHA1 Message Date
Mike Degatano
17ee234be4 Fix resp may be undefined end_backup issue (#5224) 2024-08-05 17:02:11 +02:00
dependabot[bot]
61034dfa7b Bump attrs from 23.2.0 to 24.1.0 (#5219)
Bumps [attrs](https://github.com/sponsors/hynek) from 23.2.0 to 24.1.0.
- [Commits](https://github.com/sponsors/hynek/commits)

---
updated-dependencies:
- dependency-name: attrs
  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>
2024-08-05 09:41:21 +02:00
dependabot[bot]
185cd362fb Bump coverage from 7.6.0 to 7.6.1 (#5222)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.6.0 to 7.6.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.6.0...7.6.1)

---
updated-dependencies:
- dependency-name: coverage
  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>
2024-08-05 09:38:06 +02:00
dependabot[bot]
e2ca357774 Bump dirhash from 0.4.1 to 0.5.0 (#5223)
Bumps [dirhash](https://github.com/andhus/dirhash-python) from 0.4.1 to 0.5.0.
- [Release notes](https://github.com/andhus/dirhash-python/releases)
- [Changelog](https://github.com/andhus/dirhash-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/andhus/dirhash-python/compare/v0.4.1...v0.5.0)

---
updated-dependencies:
- dependency-name: dirhash
  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>
2024-08-05 09:37:40 +02:00
dependabot[bot]
3dea7fc4e8 Bump actions/upload-artifact from 4.3.4 to 4.3.5 (#5217) 2024-08-05 08:36:00 +02:00
dependabot[bot]
01ba591bc9 Bump setuptools from 72.0.0 to 72.1.0 (#5212)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-30 10:12:58 +02:00
dependabot[bot]
640b7d46e3 Bump ruff from 0.5.4 to 0.5.5 (#5208)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 12:01:43 +02:00
dependabot[bot]
d6560c51ee Bump pre-commit from 3.7.1 to 3.8.0 (#5210)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 11:39:50 +02:00
dependabot[bot]
3e9b1938c6 Bump pytest from 8.3.1 to 8.3.2 (#5207)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.1 to 8.3.2.
- [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.3.1...8.3.2)

---
updated-dependencies:
- dependency-name: pytest
  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>
2024-07-29 11:26:14 +02:00
dependabot[bot]
44ce8de71f Bump setuptools from 71.1.0 to 72.0.0 (#5211)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-29 11:08:13 +02:00
Mike Degatano
0bbd15bfda Restrict stopping core during migrations with force option (#5205) 2024-07-25 17:14:45 +02:00
Mike Degatano
591b9a4d87 Stop backup if pre backup failed in Core (#5203)
* Stop backup if pre backup failed in Core

* Fix API tests

* Partial backup in ci since there's no Home assistant

* Add ssl folder to partial backup

* Allow backups when Home Assistant is not running

* Undo change to skip db test
2024-07-25 17:08:43 +02:00
Mike Degatano
5ee7d16687 Add hard-coded image fallback for plugins for offline start (#5204) 2024-07-25 13:45:38 +02:00
Erik Montnemery
4ab4350c58 Add support for offline DB migration (#5202)
* Add support for offline DB migration

* Format code
2024-07-23 15:27:16 -04:00
dependabot[bot]
4ea7133fa8 Bump docker/login-action from 3.2.0 to 3.3.0 (#5201)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-23 09:22:20 +02:00
dependabot[bot]
627d67f9d0 Bump cryptography from 42.0.8 to 43.0.0 (#5199)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 11:59:07 +02:00
dependabot[bot]
eb37655598 Bump pylint from 3.2.5 to 3.2.6 (#5198)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.5 to 3.2.6.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.5...v3.2.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>
2024-07-22 10:07:15 +02:00
dependabot[bot]
19b62dd0d4 Bump pytest from 8.2.2 to 8.3.1 (#5197)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 10:04:13 +02:00
dependabot[bot]
b2ad1ceea3 Bump sentry-sdk from 2.9.0 to 2.10.0 (#5188)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 10:03:25 +02:00
dependabot[bot]
c1545b5b78 Bump ruff from 0.5.3 to 0.5.4 (#5196)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.5.3 to 0.5.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.5.3...0.5.4)

---
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>
2024-07-22 09:45:47 +02:00
dependabot[bot]
2c2f04ba85 Bump setuptools from 71.0.3 to 71.1.0 (#5200)
Bumps [setuptools](https://github.com/pypa/setuptools) from 71.0.3 to 71.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/v71.0.3...v71.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>
2024-07-22 09:45:31 +02:00
Andrew Leech
77e7bf51b7 Only read wifi strength from active connections. (#5184) 2024-07-19 12:36:25 +02:00
Mike Degatano
a42d71dcef Small cleanup to pyproject.toml (#5191) 2024-07-19 09:09:13 +02:00
dependabot[bot]
1ff0432f4d Bump ruff from 0.5.2 to 0.5.3 (#5192)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-19 09:07:06 +02:00
dependabot[bot]
54afd6e1c8 Bump setuptools from 71.0.1 to 71.0.3 (#5193)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-19 09:06:43 +02:00
dependabot[bot]
458c493a74 Bump ruff from 0.5.1 to 0.5.2 (#5186)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-18 08:52:23 +02:00
dependabot[bot]
8ac8ecb17e Bump setuptools from 70.3.0 to 71.0.1 (#5190)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-18 08:51:36 +02:00
Mike Degatano
eac167067e Ignore false positive no-member error (#5189) 2024-07-18 08:43:59 +02:00
Mike Degatano
aa7f4aafeb Migrate supervisor devcontainer from pip to uv (#5170) 2024-07-12 10:26:45 +02:00
dependabot[bot]
d2183fa12b Bump coverage from 7.5.4 to 7.6.0 (#5185)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.5.4 to 7.6.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.5.4...7.6.0)

---
updated-dependencies:
- dependency-name: coverage
  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>
2024-07-12 10:25:20 +02:00
dependabot[bot]
928f32bb4f Bump sentry-sdk from 2.8.0 to 2.9.0 (#5183)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-11 08:57:33 +02:00
dependabot[bot]
cbe21303c4 Bump actions/setup-python from 5.1.0 to 5.1.1 (#5182)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: actions/setup-python
  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>
2024-07-11 08:17:35 +02:00
dependabot[bot]
94987c04b8 Bump setuptools from 70.2.0 to 70.3.0 (#5181)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-10 08:37:23 +02:00
dependabot[bot]
d4ba46a846 Bump home-assistant/wheels from 2024.01.0 to 2024.07.1 (#5178)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-09 08:29:09 +02:00
dependabot[bot]
1a22d83895 Bump sentry-sdk from 2.7.1 to 2.8.0 (#5179)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-09 08:27:46 +02:00
dependabot[bot]
6b73bf5c28 Bump aiohttp-fast-url-dispatcher from 0.3.0 to 0.3.1 (#5175) 2024-07-08 08:41:48 +02:00
dependabot[bot]
c9c9451c36 Bump ruff from 0.5.0 to 0.5.1 (#5176) 2024-07-08 08:39:34 +02:00
dependabot[bot]
1882d448ea Bump actions/download-artifact from 4.1.7 to 4.1.8 (#5174) 2024-07-08 08:35:57 +02:00
dependabot[bot]
2f11c9c9e3 Bump actions/upload-artifact from 4.3.3 to 4.3.4 (#5173) 2024-07-08 08:34:07 +02:00
Mike Degatano
02bdc4b555 Use uv instead of pip in Supervisor (#5152)
* Migrate supervisor image from pip to uv

* Set python paths

* Put in i386 conditional to match core

* Semicolons within if statements
2024-07-03 11:51:16 -04:00
dependabot[bot]
1a1ee50d9d Bump voluptuous from 0.15.1 to 0.15.2 (#5168)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 09:57:11 +02:00
dependabot[bot]
50dc09d1a9 Bump setuptools from 70.1.1 to 70.2.0 (#5167)
Bumps [setuptools](https://github.com/pypa/setuptools) from 70.1.1 to 70.2.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/v70.1.1...v70.2.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>
2024-07-02 09:29:38 +02:00
dependabot[bot]
130efd340c Bump time-machine from 2.14.1 to 2.14.2 (#5165)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 08:47:00 +02:00
dependabot[bot]
00bc13c049 Bump pylint from 3.2.4 to 3.2.5 (#5166)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 08:43:39 +02:00
dependabot[bot]
3caad67f61 Bump sentry-sdk from 2.7.0 to 2.7.1 (#5164)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-30 15:27:42 +02:00
dependabot[bot]
13783f0d4a Bump ruff from 0.4.10 to 0.5.0 (#5163)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 08:39:34 +02:00
dependabot[bot]
eae97ba3f4 Bump pylint from 3.2.3 to 3.2.4 (#5160)
Bumps [pylint](https://github.com/pylint-dev/pylint) from 3.2.3 to 3.2.4.
- [Release notes](https://github.com/pylint-dev/pylint/releases)
- [Commits](https://github.com/pylint-dev/pylint/compare/v3.2.3...v3.2.4)

---
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>
2024-06-27 09:05:11 +02:00
dependabot[bot]
134dad7357 Bump sentry-sdk from 2.6.0 to 2.7.0 (#5161)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.6.0 to 2.7.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.6.0...2.7.0)

---
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>
2024-06-27 09:04:42 +02:00
dependabot[bot]
1c4d2e8dec Bump voluptuous from 0.15.0 to 0.15.1 (#5162)
Bumps [voluptuous](https://github.com/alecthomas/voluptuous) from 0.15.0 to 0.15.1.
- [Release notes](https://github.com/alecthomas/voluptuous/releases)
- [Changelog](https://github.com/alecthomas/voluptuous/blob/master/CHANGELOG.md)
- [Commits](https://github.com/alecthomas/voluptuous/compare/0.15.0...0.15.1)

---
updated-dependencies:
- dependency-name: voluptuous
  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>
2024-06-27 08:48:44 +02:00
dependabot[bot]
f2d7be3aac Bump dbus-fast from 2.21.3 to 2.22.1 (#5159)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 09:26:21 +02:00
dependabot[bot]
d06edb2dd6 Bump debugpy from 1.8.1 to 1.8.2 (#5158)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 08:50:33 +02:00
dependabot[bot]
7fa15b334a Bump setuptools from 70.1.0 to 70.1.1 (#5156)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-26 08:35:34 +02:00
Mike Degatano
ffb4e2d6d7 Bump Supervisor to Alpine 3.20 (#5151) 2024-06-25 08:37:10 +02:00
dependabot[bot]
bd8047ae9c Bump awesomeversion from 24.2.0 to 24.6.0 (#5153)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-25 08:25:24 +02:00
Mike Degatano
49bc0624af Ignore dns-data field in network settings update (#5147) 2024-06-24 15:47:58 -04:00
dependabot[bot]
5e1d764eb3 Bump coverage from 7.5.3 to 7.5.4 (#5150)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 08:41:30 +02:00
dependabot[bot]
0064d93d75 Bump dirhash from 0.4.0 to 0.4.1 (#5149)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 08:40:15 +02:00
dependabot[bot]
5a838ecfe7 Bump voluptuous from 0.14.2 to 0.15.0 (#5148)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 08:36:04 +02:00
Pascal Vizeli
c37b5effd7 Add music assistant to the builtin repositories (#5128)
* Add music assistant to the builtin repositories

* Fix tests

* Fix tests some more

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2024-06-21 12:17:21 +02:00
dependabot[bot]
ca7f3e8acb Bump ruff from 0.4.9 to 0.4.10 (#5143)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-21 09:27:10 +02:00
Stefan Agner
b0cdb91d5e Fix uninstall of add-on on fixup (#5142) 2024-06-21 08:54:46 +02:00
Mike Degatano
4829eb8ae1 Await uninstall of addon in fixup (#5141) 2024-06-20 21:50:45 +02:00
Mike Degatano
1bb814b793 Home Assistant watchdog attempts safe mode after max fails (#5124)
* Home Assistant watchdog attempts safe mode after max fails

* Remove duplicate line

* Refactor and logging change from feedback

* Update supervisor/misc/tasks.py

* Fix log text check in test

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2024-06-20 21:50:29 +02:00
dependabot[bot]
918fcb7d62 Bump sentry-sdk from 2.5.1 to 2.6.0 (#5140)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-20 11:39:26 +02:00
dependabot[bot]
bbfd899564 Bump setuptools from 70.0.0 to 70.1.0 (#5139)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-20 09:11:45 +02:00
dependabot[bot]
12c4d9da87 Bump urllib3 from 2.2.1 to 2.2.2 (#5138)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 07:52:04 +02:00
dependabot[bot]
6b4fd9b6b8 Bump ruff from 0.4.8 to 0.4.9 (#5137)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.8 to 0.4.9.
- [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/v0.4.8...v0.4.9)

---
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>
2024-06-17 09:05:58 +02:00
dependabot[bot]
07c22f4a60 Bump codecov/codecov-action from 4.4.1 to 4.5.0 (#5136)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-14 09:27:41 +02:00
dependabot[bot]
252e1e2ac0 Bump actions/checkout from 4.1.6 to 4.1.7 (#5134)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.6...v4.1.7)

---
updated-dependencies:
- dependency-name: actions/checkout
  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>
2024-06-13 09:17:05 +02:00
45 changed files with 726 additions and 259 deletions

View File

@@ -4,8 +4,12 @@
"containerEnv": {
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"remoteEnv": {
"PATH": "${containerEnv:VIRTUAL_ENV}/bin:${containerEnv:PATH}"
},
"appPort": ["9123:8123", "7357:4357"],
"postCreateCommand": "bash devcontainer_bootstrap",
"postCreateCommand": "bash devcontainer_setup",
"postStartCommand": "bash devcontainer_bootstrap",
"runArgs": ["-e", "GIT_EDITOR=code --wait", "--privileged"],
"customizations": {
"vscode": {
@@ -19,17 +23,21 @@
"GitHub.vscode-pull-request-github"
],
"settings": {
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
"python.pythonPath": "/home/vscode/.local/ha-venv/bin/python",
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "/usr/bin/zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"python.pythonPath": "/usr/local/bin/python3",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}

View File

@@ -53,7 +53,7 @@ jobs:
requirements: ${{ steps.requirements.outputs.changed }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
with:
fetch-depth: 0
@@ -92,7 +92,7 @@ jobs:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
with:
fetch-depth: 0
@@ -106,7 +106,7 @@ jobs:
- name: Build wheels
if: needs.init.outputs.requirements == 'true'
uses: home-assistant/wheels@2024.01.0
uses: home-assistant/wheels@2024.07.1
with:
abi: cp312
tag: musllinux_1_2
@@ -125,7 +125,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.publish == 'true'
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -149,7 +149,7 @@ jobs:
- name: Login to GitHub Container Registry
if: needs.init.outputs.publish == 'true'
uses: docker/login-action@v3.2.0
uses: docker/login-action@v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -178,7 +178,7 @@ jobs:
steps:
- name: Checkout the repository
if: needs.init.outputs.publish == 'true'
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Initialize git
if: needs.init.outputs.publish == 'true'
@@ -203,7 +203,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Build the Supervisor
if: needs.init.outputs.publish != 'true'

View File

@@ -25,10 +25,10 @@ jobs:
name: Prepare Python dependencies
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python
id: python
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
@@ -67,9 +67,9 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -110,9 +110,9 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -153,7 +153,7 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Register hadolint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
@@ -168,9 +168,9 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -212,9 +212,9 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -256,9 +256,9 @@ jobs:
needs: prepare
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -288,9 +288,9 @@ jobs:
name: Run tests Python ${{ needs.prepare.outputs.python-version }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -335,7 +335,7 @@ jobs:
-o console_output_style=count \
tests
- name: Upload coverage artifact
uses: actions/upload-artifact@v4.3.3
uses: actions/upload-artifact@v4.3.5
with:
name: coverage-${{ matrix.python-version }}
path: .coverage
@@ -346,9 +346,9 @@ jobs:
needs: ["pytest", "prepare"]
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Set up Python ${{ needs.prepare.outputs.python-version }}
uses: actions/setup-python@v5.1.0
uses: actions/setup-python@v5.1.1
id: python
with:
python-version: ${{ needs.prepare.outputs.python-version }}
@@ -365,7 +365,7 @@ jobs:
echo "Failed to restore Python virtual environment from cache"
exit 1
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.1.7
uses: actions/download-artifact@v4.1.8
- name: Combine coverage results
run: |
. venv/bin/activate
@@ -373,4 +373,4 @@ jobs:
coverage report
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4.4.1
uses: codecov/codecov-action@v4.5.0

View File

@@ -11,7 +11,7 @@ jobs:
name: Release Drafter
steps:
- name: Checkout the repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
with:
fetch-depth: 0

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.6
uses: actions/checkout@v4.1.7
- name: Sentry Release
uses: getsentry/action-release@v1.7.0
env:

View File

@@ -4,7 +4,8 @@ FROM ${BUILD_FROM}
ENV \
S6_SERVICES_GRACETIME=10000 \
SUPERVISOR_API=http://localhost \
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1
CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 \
UV_SYSTEM_PYTHON=true
ARG \
COSIGN_VERSION \
@@ -26,14 +27,17 @@ RUN \
yaml \
\
&& curl -Lso /usr/bin/cosign "https://github.com/home-assistant/cosign/releases/download/${COSIGN_VERSION}/cosign_${BUILD_ARCH}" \
&& chmod a+x /usr/bin/cosign
&& chmod a+x /usr/bin/cosign \
&& pip3 install uv==0.2.21
# Install requirements
COPY requirements.txt .
RUN \
export MAKEFLAGS="-j$(nproc)" \
&& pip3 install --only-binary=:all: \
-r ./requirements.txt \
if [ "${BUILD_ARCH}" = "i386" ]; then \
linux32 uv pip install --no-build -r requirements.txt; \
else \
uv pip install --no-build -r requirements.txt; \
fi \
&& rm -f requirements.txt
# Install Home Assistant Supervisor

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-hassio-supervisor
build_from:
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.19
armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.19
armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.19
amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.19
i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.19
aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.20
armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.20
armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.20
amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.20
i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.20
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -31,7 +31,7 @@ include-package-data = true
include = ["supervisor*"]
[tool.pylint.MAIN]
py-version = "3.11"
py-version = "3.12"
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
jobs = 2
@@ -234,6 +234,7 @@ select = [
"B014", # Exception handler with duplicate exception
"B023", # Function definition does not bind loop variable {name}
"B026", # Star-arg unpacking after a keyword argument is strongly discouraged
"B904", # Use raise from to specify exception cause
"C", # complexity
"COM818", # Trailing comma on bare tuple prohibited
"D", # docstrings
@@ -247,7 +248,6 @@ select = [
"N804", # First argument of a class method should be named cls
"N805", # First argument of a method should be named self
"N815", # Variable {name} in class scope should not be mixedCase
"PGH001", # No builtin eval() allowed
"PGH004", # Use specific rule codes when using noqa
"PLC0414", # Useless import alias. Import alias does not rename original package.
"PLC", # pylint
@@ -286,7 +286,6 @@ select = [
"T20", # flake8-print
"TID251", # Banned imports
"TRY004", # Prefer TypeError exception for invalid type
"TRY200", # Use raise from to specify exception cause
"TRY302", # Remove exception handler; error is immediately re-raised
"UP", # pyupgrade
"W", # pycodestyle

View File

@@ -1,17 +1,17 @@
aiodns==3.2.0
aiohttp==3.9.5
aiohttp-fast-url-dispatcher==0.3.0
aiohttp-fast-url-dispatcher==0.3.1
atomicwrites-homeassistant==1.4.1
attrs==23.2.0
awesomeversion==24.2.0
attrs==24.1.0
awesomeversion==24.6.0
brotli==1.1.0
ciso8601==2.3.1
colorlog==6.8.2
cpe==1.2.1
cryptography==42.0.8
debugpy==1.8.1
cryptography==43.0.0
debugpy==1.8.2
deepmerge==1.1.1
dirhash==0.4.0
dirhash==0.5.0
docker==7.1.0
faust-cchardet==2.1.19
gitpython==3.1.43
@@ -22,9 +22,9 @@ pyudev==0.24.3
PyYAML==6.0.1
requests==2.32.3
securetar==2024.2.1
sentry-sdk==2.5.1
setuptools==70.0.0
voluptuous==0.14.2
dbus-fast==2.21.3
sentry-sdk==2.10.0
setuptools==72.1.0
voluptuous==0.15.2
dbus-fast==2.22.1
typing_extensions==4.12.2
zlib-fast==0.2.0

View File

@@ -1,12 +1,12 @@
coverage==7.5.3
pre-commit==3.7.1
pylint==3.2.3
coverage==7.6.1
pre-commit==3.8.0
pylint==3.2.6
pytest-aiohttp==1.0.5
pytest-asyncio==0.23.6
pytest-cov==5.0.0
pytest-timeout==2.3.1
pytest==8.2.2
ruff==0.4.8
time-machine==2.14.1
pytest==8.3.2
ruff==0.5.5
time-machine==2.14.2
typing_extensions==4.12.2
urllib3==2.2.1
urllib3==2.2.2

View File

@@ -36,6 +36,7 @@ ATTR_DT_UTC = "dt_utc"
ATTR_EJECTABLE = "ejectable"
ATTR_FALLBACK = "fallback"
ATTR_FILESYSTEMS = "filesystems"
ATTR_FORCE = "force"
ATTR_GROUP_IDS = "group_ids"
ATTR_IDENTIFIERS = "identifiers"
ATTR_IS_ACTIVE = "is_active"

View File

@@ -1,4 +1,5 @@
"""Init file for Supervisor Home Assistant RESTful API."""
import asyncio
from collections.abc import Awaitable
import logging
@@ -34,9 +35,9 @@ from ..const import (
ATTR_WATCHDOG,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError
from ..exceptions import APIDBMigrationInProgress, APIError
from ..validate import docker_image, network_port, version_tag
from .const import ATTR_SAFE_MODE
from .const import ATTR_FORCE, ATTR_SAFE_MODE
from .utils import api_process, api_validate
_LOGGER: logging.Logger = logging.getLogger(__name__)
@@ -66,6 +67,13 @@ SCHEMA_UPDATE = vol.Schema(
SCHEMA_RESTART = vol.Schema(
{
vol.Optional(ATTR_SAFE_MODE, default=False): vol.Boolean(),
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
}
)
SCHEMA_STOP = vol.Schema(
{
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
}
)
@@ -73,6 +81,17 @@ SCHEMA_RESTART = vol.Schema(
class APIHomeAssistant(CoreSysAttributes):
"""Handle RESTful API for Home Assistant functions."""
async def _check_offline_migration(self, force: bool = False) -> None:
"""Check and raise if there's an offline DB migration in progress."""
if (
not force
and (state := await self.sys_homeassistant.api.get_api_state())
and state.offline_db_migration
):
raise APIDBMigrationInProgress(
"Offline database migration in progress, try again after it has completed"
)
@api_process
async def info(self, request: web.Request) -> dict[str, Any]:
"""Return host information."""
@@ -154,6 +173,7 @@ class APIHomeAssistant(CoreSysAttributes):
async def update(self, request: web.Request) -> None:
"""Update Home Assistant."""
body = await api_validate(SCHEMA_UPDATE, request)
await self._check_offline_migration()
await asyncio.shield(
self.sys_homeassistant.core.update(
@@ -163,9 +183,12 @@ class APIHomeAssistant(CoreSysAttributes):
)
@api_process
def stop(self, request: web.Request) -> Awaitable[None]:
async def stop(self, request: web.Request) -> Awaitable[None]:
"""Stop Home Assistant."""
return asyncio.shield(self.sys_homeassistant.core.stop())
body = await api_validate(SCHEMA_STOP, request)
await self._check_offline_migration(force=body[ATTR_FORCE])
return await asyncio.shield(self.sys_homeassistant.core.stop())
@api_process
def start(self, request: web.Request) -> Awaitable[None]:
@@ -176,6 +199,7 @@ class APIHomeAssistant(CoreSysAttributes):
async def restart(self, request: web.Request) -> None:
"""Restart Home Assistant."""
body = await api_validate(SCHEMA_RESTART, request)
await self._check_offline_migration(force=body[ATTR_FORCE])
await asyncio.shield(
self.sys_homeassistant.core.restart(safe_mode=body[ATTR_SAFE_MODE])
@@ -185,6 +209,7 @@ class APIHomeAssistant(CoreSysAttributes):
async def rebuild(self, request: web.Request) -> None:
"""Rebuild Home Assistant."""
body = await api_validate(SCHEMA_RESTART, request)
await self._check_offline_migration(force=body[ATTR_FORCE])
await asyncio.shield(
self.sys_homeassistant.core.rebuild(safe_mode=body[ATTR_SAFE_MODE])

View File

@@ -28,7 +28,7 @@ from ..const import (
ATTR_TIMEZONE,
)
from ..coresys import CoreSysAttributes
from ..exceptions import APIError, HostLogError
from ..exceptions import APIDBMigrationInProgress, APIError, HostLogError
from ..host.const import (
PARAM_BOOT_ID,
PARAM_FOLLOW,
@@ -46,6 +46,7 @@ from .const import (
ATTR_BROADCAST_MDNS,
ATTR_DT_SYNCHRONIZED,
ATTR_DT_UTC,
ATTR_FORCE,
ATTR_IDENTIFIERS,
ATTR_LLMNR_HOSTNAME,
ATTR_STARTUP_TIME,
@@ -64,10 +65,29 @@ DEFAULT_RANGE = 100
SCHEMA_OPTIONS = vol.Schema({vol.Optional(ATTR_HOSTNAME): str})
# pylint: disable=no-value-for-parameter
SCHEMA_SHUTDOWN = vol.Schema(
{
vol.Optional(ATTR_FORCE, default=False): vol.Boolean(),
}
)
# pylint: enable=no-value-for-parameter
class APIHost(CoreSysAttributes):
"""Handle RESTful API for host functions."""
async def _check_ha_offline_migration(self, force: bool) -> None:
"""Check if HA has an offline migration in progress and raise if not forced."""
if (
not force
and (state := await self.sys_homeassistant.api.get_api_state())
and state.offline_db_migration
):
raise APIDBMigrationInProgress(
"Home Assistant offline database migration in progress, please wait until complete before shutting down host"
)
@api_process
async def info(self, request):
"""Return host information."""
@@ -109,14 +129,20 @@ class APIHost(CoreSysAttributes):
)
@api_process
def reboot(self, request):
async def reboot(self, request):
"""Reboot host."""
return asyncio.shield(self.sys_host.control.reboot())
body = await api_validate(SCHEMA_SHUTDOWN, request)
await self._check_ha_offline_migration(force=body[ATTR_FORCE])
return await asyncio.shield(self.sys_host.control.reboot())
@api_process
def shutdown(self, request):
async def shutdown(self, request):
"""Poweroff host."""
return asyncio.shield(self.sys_host.control.shutdown())
body = await api_validate(SCHEMA_SHUTDOWN, request)
await self._check_ha_offline_migration(force=body[ATTR_FORCE])
return await asyncio.shield(self.sys_host.control.shutdown())
@api_process
def reload(self, request):

View File

@@ -1,4 +1,5 @@
"""Backup manager."""
from __future__ import annotations
import asyncio
@@ -259,11 +260,6 @@ class BackupManager(FileConfiguration, JobGroup):
self.sys_core.state = CoreState.FREEZE
async with backup:
# Backup add-ons
if addon_list:
self._change_stage(BackupJobStage.ADDONS, backup)
addon_start_tasks = await backup.store_addons(addon_list)
# HomeAssistant Folder is for v1
if homeassistant:
self._change_stage(BackupJobStage.HOME_ASSISTANT, backup)
@@ -273,6 +269,11 @@ class BackupManager(FileConfiguration, JobGroup):
else homeassistant_exclude_database
)
# Backup add-ons
if addon_list:
self._change_stage(BackupJobStage.ADDONS, backup)
addon_start_tasks = await backup.store_addons(addon_list)
# Backup folders
if folder_list:
self._change_stage(BackupJobStage.FOLDERS, backup)

View File

@@ -113,8 +113,11 @@ class CoreSys:
"""Return system timezone."""
if self.config.timezone:
return self.config.timezone
# pylint bug with python 3.12.4 (https://github.com/pylint-dev/pylint/issues/9811)
# pylint: disable=no-member
if self.host.info.timezone:
return self.host.info.timezone
# pylint: enable=no-member
return "UTC"
@property

View File

@@ -44,6 +44,7 @@ IPV4_6_IGNORE_FIELDS = [
"addresses",
"address-data",
"dns",
"dns-data",
"gateway",
"method",
]

View File

@@ -1,4 +1,5 @@
"""Core Exceptions."""
from collections.abc import Callable
@@ -339,6 +340,12 @@ class APIAddonNotInstalled(APIError):
"""Not installed addon requested at addons API."""
class APIDBMigrationInProgress(APIError):
"""Service is unavailable due to an offline DB migration is in progress."""
status = 503
# Service / Discovery

View File

@@ -1,6 +1,7 @@
"""Home Assistant control object."""
import asyncio
from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
import logging
from typing import Any
@@ -21,6 +22,14 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
GET_CORE_STATE_MIN_VERSION: AwesomeVersion = AwesomeVersion("2023.8.0.dev20230720")
@dataclass(frozen=True)
class APIState:
"""Container for API state response."""
core_state: str
offline_db_migration: bool
class HomeAssistantAPI(CoreSysAttributes):
"""Home Assistant core object for handle it."""
@@ -132,7 +141,7 @@ class HomeAssistantAPI(CoreSysAttributes):
"""Return Home Assistant core state."""
return await self._get_json("api/core/state")
async def get_api_state(self) -> str | None:
async def get_api_state(self) -> APIState | None:
"""Return state of Home Assistant Core or None."""
# Skip check on landingpage
if (
@@ -161,12 +170,17 @@ class HomeAssistantAPI(CoreSysAttributes):
data = await self.get_config()
# Older versions of home assistant does not expose the state
if data:
return data.get("state", "RUNNING")
state = data.get("state", "RUNNING")
# Recorder state was added in HA Core 2024.8
recorder_state = data.get("recorder_state", {})
migrating = recorder_state.get("migration_in_progress", False)
live_migration = recorder_state.get("migration_is_live", False)
return APIState(state, migrating and not live_migration)
return None
async def check_api_state(self) -> bool:
"""Return Home Assistant Core state if up."""
if state := await self.get_api_state():
return state == "RUNNING"
return state.core_state == "RUNNING" or state.offline_db_migration
return False

View File

@@ -7,7 +7,9 @@ from awesomeversion import AwesomeVersion
from ..const import CoreState
ATTR_ERROR = "error"
ATTR_OVERRIDE_IMAGE = "override_image"
ATTR_SUCCESS = "success"
LANDINGPAGE: AwesomeVersion = AwesomeVersion("landingpage")
WATCHDOG_RETRY_SECONDS = 10
WATCHDOG_MAX_ATTEMPTS = 5

View File

@@ -49,6 +49,10 @@ SECONDS_BETWEEN_API_CHECKS: Final[int] = 5
STARTUP_API_RESPONSE_TIMEOUT: Final[timedelta] = timedelta(minutes=3)
# All stages plus event start timeout and some wiggle rooom
STARTUP_API_CHECK_RUNNING_TIMEOUT: Final[timedelta] = timedelta(minutes=15)
# While database migration is running, the timeout will be extended
DATABASE_MIGRATION_TIMEOUT: Final[timedelta] = timedelta(
seconds=SECONDS_BETWEEN_API_CHECKS * 10
)
RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
@@ -490,11 +494,15 @@ class HomeAssistantCore(JobGroup):
_LOGGER.info("Home Assistant Core state changed to %s", state)
last_state = state
if state == "RUNNING":
if state.core_state == "RUNNING":
_LOGGER.info("Detect a running Home Assistant instance")
self._error_state = False
return
if state.offline_db_migration:
# Keep extended the deadline while database migration is active
deadline = datetime.now() + DATABASE_MIGRATION_TIMEOUT
self._error_state = True
if timeout:
raise HomeAssistantStartupTimeout(

View File

@@ -1,4 +1,5 @@
"""Home Assistant control object."""
import asyncio
from datetime import timedelta
import errno
@@ -22,6 +23,7 @@ from ..const import (
ATTR_BACKUPS_EXCLUDE_DATABASE,
ATTR_BOOT,
ATTR_IMAGE,
ATTR_MESSAGE,
ATTR_PORT,
ATTR_REFRESH_TOKEN,
ATTR_SSL,
@@ -48,7 +50,7 @@ from ..utils import remove_folder
from ..utils.common import FileConfiguration
from ..utils.json import read_json_file, write_json_file
from .api import HomeAssistantAPI
from .const import ATTR_OVERRIDE_IMAGE, LANDINGPAGE, WSType
from .const import ATTR_ERROR, ATTR_OVERRIDE_IMAGE, ATTR_SUCCESS, LANDINGPAGE, WSType
from .core import HomeAssistantCore
from .secrets import HomeAssistantSecrets
from .validate import SCHEMA_HASS_CONFIG
@@ -345,21 +347,38 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
async def begin_backup(self) -> None:
"""Inform Home Assistant a backup is beginning."""
try:
await self.websocket.async_send_command({ATTR_TYPE: WSType.BACKUP_START})
except HomeAssistantWSError:
_LOGGER.warning(
"Preparing backup of Home Assistant Core failed. Check HA Core logs."
resp = await self.websocket.async_send_command(
{ATTR_TYPE: WSType.BACKUP_START}
)
except HomeAssistantWSError as err:
raise HomeAssistantBackupError(
"Preparing backup of Home Assistant Core failed. Check HA Core logs.",
_LOGGER.error,
) from err
if resp and not resp.get(ATTR_SUCCESS):
raise HomeAssistantBackupError(
f"Preparing backup of Home Assistant Core failed due to: {resp.get(ATTR_ERROR, {}).get(ATTR_MESSAGE, "")}. Check HA Core logs.",
_LOGGER.error,
)
@Job(name="home_assistant_module_end_backup")
async def end_backup(self) -> None:
"""Inform Home Assistant the backup is ending."""
try:
await self.websocket.async_send_command({ATTR_TYPE: WSType.BACKUP_END})
resp = await self.websocket.async_send_command(
{ATTR_TYPE: WSType.BACKUP_END}
)
except HomeAssistantWSError:
_LOGGER.warning(
"Error during Home Assistant Core backup. Check HA Core logs."
"Error resuming normal operations after backup of Home Assistant Core. Check HA Core logs."
)
else:
if resp and not resp.get(ATTR_SUCCESS):
_LOGGER.warning(
"Error resuming normal operations after backup of Home Assistant Core due to: %s. Check HA Core logs.",
resp.get(ATTR_ERROR, {}).get(ATTR_MESSAGE, ""),
)
@Job(name="home_assistant_module_backup")
async def backup(

View File

@@ -191,7 +191,7 @@ class Interface:
mode = WifiMode(inet.settings.wireless.mode)
# Signal
if inet.wireless:
if inet.wireless and inet.wireless.active:
signal = inet.wireless.active.strength
else:
signal = None

View File

@@ -174,17 +174,6 @@ class Tasks(CoreSysAttributes):
self._cache[HASS_WATCHDOG_API_FAILURES] = 0
return
# Give up after 5 reanimation failures in a row. Supervisor cannot fix this issue.
reanimate_fails = self._cache.get(HASS_WATCHDOG_REANIMATE_FAILURES, 0)
if reanimate_fails >= HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
if reanimate_fails == HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
_LOGGER.critical(
"Watchdog cannot reanimate Home Assistant Core, failed all %s attempts.",
reanimate_fails,
)
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] += 1
return
# Init cache data
api_fails = self._cache.get(HASS_WATCHDOG_API_FAILURES, 0)
@@ -195,16 +184,38 @@ class Tasks(CoreSysAttributes):
_LOGGER.warning("Watchdog missed an Home Assistant Core API response.")
return
_LOGGER.error(
"Watchdog missed %s Home Assistant Core API responses in a row. Restarting Home Assistant Core API!",
HASS_WATCHDOG_MAX_API_ATTEMPTS,
)
# After 5 reanimation attempts switch to safe mode. If that fails, give up
reanimate_fails = self._cache.get(HASS_WATCHDOG_REANIMATE_FAILURES, 0)
if reanimate_fails > HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
return
if safe_mode := reanimate_fails == HASS_WATCHDOG_MAX_REANIMATE_ATTEMPTS:
_LOGGER.critical(
"Watchdog cannot reanimate Home Assistant Core, failed all %s attempts. Restarting into safe mode",
reanimate_fails,
)
else:
_LOGGER.error(
"Watchdog missed %s Home Assistant Core API responses in a row. Restarting Home Assistant Core!",
HASS_WATCHDOG_MAX_API_ATTEMPTS,
)
try:
await self.sys_homeassistant.core.restart()
if safe_mode:
await self.sys_homeassistant.core.rebuild(safe_mode=True)
else:
await self.sys_homeassistant.core.restart()
except HomeAssistantError as err:
_LOGGER.error("Home Assistant watchdog reanimation failed!")
if reanimate_fails == 0:
if reanimate_fails == 0 or safe_mode:
capture_exception(err)
if safe_mode:
_LOGGER.critical(
"Safe mode restart failed. Watchdog cannot bring Home Assistant online."
)
else:
_LOGGER.error("Home Assistant watchdog reanimation failed!")
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] = reanimate_fails + 1
else:
self._cache[HASS_WATCHDOG_REANIMATE_FAILURES] = 0

View File

@@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-audio
"""
import errno
import logging
from pathlib import Path, PurePath
@@ -73,7 +74,9 @@ class PluginAudio(PluginBase):
@property
def default_image(self) -> str:
"""Return default image for audio plugin."""
return self.sys_updater.image_audio
if self.sys_updater.image_audio:
return self.sys_updater.image_audio
return super().default_image
@property
def latest_version(self) -> AwesomeVersion | None:

View File

@@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-cli
"""
from collections.abc import Awaitable
import logging
import secrets
@@ -42,7 +43,9 @@ class PluginCli(PluginBase):
@property
def default_image(self) -> str:
"""Return default image for cli plugin."""
return self.sys_updater.image_cli
if self.sys_updater.image_cli:
return self.sys_updater.image_cli
return super().default_image
@property
def latest_version(self) -> AwesomeVersion | None:

View File

@@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-dns
"""
import asyncio
from contextlib import suppress
import errno
@@ -111,7 +112,9 @@ class PluginDns(PluginBase):
@property
def default_image(self) -> str:
"""Return default image for dns plugin."""
return self.sys_updater.image_dns
if self.sys_updater.image_dns:
return self.sys_updater.image_dns
return super().default_image
@property
def latest_version(self) -> AwesomeVersion | None:

View File

@@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-multicast
"""
import logging
from awesomeversion import AwesomeVersion
@@ -44,7 +45,9 @@ class PluginMulticast(PluginBase):
@property
def default_image(self) -> str:
"""Return default image for multicast plugin."""
return self.sys_updater.image_multicast
if self.sys_updater.image_multicast:
return self.sys_updater.image_multicast
return super().default_image
@property
def latest_version(self) -> AwesomeVersion | None:

View File

@@ -2,6 +2,7 @@
Code: https://github.com/home-assistant/plugin-observer
"""
import logging
import secrets
@@ -47,7 +48,9 @@ class PluginObserver(PluginBase):
@property
def default_image(self) -> str:
"""Return default image for observer plugin."""
return self.sys_updater.image_observer
if self.sys_updater.image_observer:
return self.sys_updater.image_observer
return super().default_image
@property
def latest_version(self) -> AwesomeVersion | None:

View File

@@ -26,7 +26,7 @@ class FixupAddonExecuteRemove(FixupBase):
# Remove addon
_LOGGER.info("Remove addon: %s", reference)
try:
addon.uninstall()
await addon.uninstall(remove_config=False)
except AddonsError as err:
_LOGGER.error("Could not remove %s due to %s", reference, err)
raise ResolutionFixupError() from None

View File

@@ -8,11 +8,13 @@ from .const import StoreType
URL_COMMUNITY_ADDONS = "https://github.com/hassio-addons/repository"
URL_ESPHOME = "https://github.com/esphome/home-assistant-addon"
URL_MUSIC_ASSISTANT = "https://github.com/music-assistant/home-assistant-addon"
BUILTIN_REPOSITORIES = {
StoreType.CORE,
StoreType.LOCAL,
URL_COMMUNITY_ADDONS,
URL_ESPHOME,
URL_MUSIC_ASSISTANT,
}
# pylint: disable=no-value-for-parameter

View File

@@ -348,15 +348,15 @@ async def test_api_backup_errors(
assert job["done"] is True
assert job["reference"] == slug
assert job["errors"] == []
assert job["child_jobs"][0]["name"] == "backup_store_addons"
assert job["child_jobs"][0]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][0]["reference"] == slug
assert job["child_jobs"][0]["child_jobs"][0]["name"] == "backup_addon_save"
assert job["child_jobs"][0]["child_jobs"][0]["reference"] == "local_ssh"
assert job["child_jobs"][0]["child_jobs"][0]["errors"] == [
assert job["child_jobs"][1]["name"] == "backup_store_addons"
assert job["child_jobs"][1]["reference"] == slug
assert job["child_jobs"][1]["child_jobs"][0]["name"] == "backup_addon_save"
assert job["child_jobs"][1]["child_jobs"][0]["reference"] == "local_ssh"
assert job["child_jobs"][1]["child_jobs"][0]["errors"] == [
{"type": "BackupError", "message": "Can't create backup for local_ssh"}
]
assert job["child_jobs"][1]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][1]["reference"] == slug
assert job["child_jobs"][2]["name"] == "backup_store_folders"
assert job["child_jobs"][2]["reference"] == slug
assert {j["reference"] for j in job["child_jobs"][2]["child_jobs"]} == {
@@ -366,9 +366,14 @@ async def test_api_backup_errors(
"ssl",
}
with patch.object(
HomeAssistant, "backup", side_effect=HomeAssistantBackupError("Backup error")
), patch.object(Addon, "backup"):
with (
patch.object(
HomeAssistant,
"backup",
side_effect=HomeAssistantBackupError("Backup error"),
),
patch.object(Addon, "backup"),
):
resp = await api_client.post(
f"/backups/new/{backup_type}",
json={"name": f"{backup_type} backup"} | options,
@@ -384,10 +389,9 @@ async def test_api_backup_errors(
assert job["errors"] == (
err := [{"type": "HomeAssistantBackupError", "message": "Backup error"}]
)
assert job["child_jobs"][0]["name"] == "backup_store_addons"
assert job["child_jobs"][1]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][1]["errors"] == err
assert len(job["child_jobs"]) == 2
assert job["child_jobs"][0]["name"] == "backup_store_homeassistant"
assert job["child_jobs"][0]["errors"] == err
assert len(job["child_jobs"]) == 1
async def test_backup_immediate_errors(api_client: TestClient, coresys: CoreSys):
@@ -426,14 +430,17 @@ async def test_restore_immediate_errors(
assert resp.status == 400
assert "only a partial backup" in (await resp.json())["message"]
with patch.object(
Backup,
"supervisor_version",
new=PropertyMock(return_value=AwesomeVersion("2024.01.0")),
), patch.object(
Supervisor,
"version",
new=PropertyMock(return_value=AwesomeVersion("2023.12.0")),
with (
patch.object(
Backup,
"supervisor_version",
new=PropertyMock(return_value=AwesomeVersion("2024.01.0")),
),
patch.object(
Supervisor,
"version",
new=PropertyMock(return_value=AwesomeVersion("2023.12.0")),
),
):
resp = await api_client.post(
f"/backups/{mock_partial_backup.slug}/restore/partial",
@@ -442,9 +449,10 @@ async def test_restore_immediate_errors(
assert resp.status == 400
assert "Must update supervisor" in (await resp.json())["message"]
with patch.object(
Backup, "protected", new=PropertyMock(return_value=True)
), patch.object(Backup, "set_password", return_value=False):
with (
patch.object(Backup, "protected", new=PropertyMock(return_value=True)),
patch.object(Backup, "set_password", return_value=False),
):
resp = await api_client.post(
f"/backups/{mock_partial_backup.slug}/restore/partial",
json={"background": True, "homeassistant": True},

View File

@@ -8,6 +8,7 @@ from awesomeversion import AwesomeVersion
import pytest
from supervisor.coresys import CoreSys
from supervisor.homeassistant.api import APIState
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
@@ -142,3 +143,48 @@ async def test_api_rebuild(
assert container.remove.call_count == 4
assert container.start.call_count == 2
assert safe_mode_marker.exists()
@pytest.mark.parametrize("action", ["rebuild", "restart", "stop", "update"])
async def test_migration_blocks_stopping_core(
api_client: TestClient,
coresys: CoreSys,
action: str,
):
"""Test that an offline db migration in progress stops users from stopping/restarting core."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
resp = await api_client.post(f"/homeassistant/{action}")
assert resp.status == 503
result = await resp.json()
assert (
result["message"]
== "Offline database migration in progress, try again after it has completed"
)
async def test_force_rebuild_during_migration(api_client: TestClient, coresys: CoreSys):
"""Test force option rebuilds even during a migration."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
with patch.object(HomeAssistantCore, "rebuild") as rebuild:
await api_client.post("/homeassistant/rebuild", json={"force": True})
rebuild.assert_called_once()
async def test_force_restart_during_migration(api_client: TestClient, coresys: CoreSys):
"""Test force option restarts even during a migration."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
with patch.object(HomeAssistantCore, "restart") as restart:
await api_client.post("/homeassistant/restart", json={"force": True})
restart.assert_called_once()
async def test_force_stop_during_migration(api_client: TestClient, coresys: CoreSys):
"""Test force option stops even during a migration."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
with patch.object(HomeAssistantCore, "stop") as stop:
await api_client.post("/homeassistant/stop", json={"force": True})
stop.assert_called_once()

View File

@@ -1,13 +1,15 @@
"""Test Host API."""
from unittest.mock import ANY, MagicMock
from unittest.mock import ANY, MagicMock, patch
from aiohttp.test_utils import TestClient
import pytest
from supervisor.coresys import CoreSys
from supervisor.dbus.resolved import Resolved
from supervisor.homeassistant.api import APIState
from supervisor.host.const import LogFormat, LogFormatter
from supervisor.host.control import SystemControl
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.systemd import Systemd as SystemdService
@@ -324,3 +326,41 @@ async def test_advanced_logs_errors(api_client: TestClient):
content
== "Invalid content type requested. Only text/plain and text/x-log supported for now."
)
@pytest.mark.parametrize("action", ["reboot", "shutdown"])
async def test_migration_blocks_shutdown(
api_client: TestClient,
coresys: CoreSys,
action: str,
):
"""Test that an offline db migration in progress stops users from shuting down or rebooting system."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
resp = await api_client.post(f"/host/{action}")
assert resp.status == 503
result = await resp.json()
assert (
result["message"]
== "Home Assistant offline database migration in progress, please wait until complete before shutting down host"
)
async def test_force_reboot_during_migration(api_client: TestClient, coresys: CoreSys):
"""Test force option reboots even during a migration."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
with patch.object(SystemControl, "reboot") as reboot:
await api_client.post("/host/reboot", json={"force": True})
reboot.assert_called_once()
async def test_force_shutdown_during_migration(
api_client: TestClient, coresys: CoreSys
):
"""Test force option shutdown even during a migration."""
coresys.homeassistant.api.get_api_state.return_value = APIState("NOT_RUNNING", True)
with patch.object(SystemControl, "shutdown") as shutdown:
await api_client.post("/host/shutdown", json={"force": True})
shutdown.assert_called_once()

View File

@@ -31,6 +31,7 @@ from supervisor.exceptions import (
DockerError,
)
from supervisor.homeassistant.api import HomeAssistantAPI
from supervisor.homeassistant.const import WSType
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
from supervisor.jobs.const import JobCondition
@@ -335,9 +336,14 @@ async def test_fail_invalid_full_backup(
backup_instance.protected = False
backup_instance.supervisor_version = "2022.08.4"
with patch.object(
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
), pytest.raises(BackupInvalidError):
with (
patch.object(
type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
await manager.do_restore_full(backup_instance)
@@ -364,9 +370,14 @@ async def test_fail_invalid_partial_backup(
await manager.do_restore_partial(backup_instance, homeassistant=True)
backup_instance.supervisor_version = "2022.08.4"
with patch.object(
type(coresys.supervisor), "version", new=PropertyMock(return_value="2022.08.3")
), pytest.raises(BackupInvalidError):
with (
patch.object(
type(coresys.supervisor),
"version",
new=PropertyMock(return_value="2022.08.3"),
),
pytest.raises(BackupInvalidError),
):
await manager.do_restore_partial(backup_instance)
@@ -766,7 +777,11 @@ async def test_backup_to_local_with_default(
async def test_backup_to_default(
coresys: CoreSys, tmp_supervisor_data, path_extern, mount_propagation, mock_is_mount
coresys: CoreSys,
tmp_supervisor_data,
path_extern,
mount_propagation,
mock_is_mount,
):
"""Test making backup to default mount."""
# Add a default backup mount
@@ -926,9 +941,15 @@ async def test_backup_with_healthcheck(
nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object(
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD)
), patch.object(DockerAddon, "is_running", side_effect=[True, False, False]):
with (
patch.object(DockerAddon, "run", new=container_events_task),
patch.object(
AddonModel,
"backup_mode",
new=PropertyMock(return_value=AddonBackupMode.COLD),
),
patch.object(DockerAddon, "is_running", side_effect=[True, False, False]),
):
backup = await coresys.backups.do_backup_partial(
homeassistant=False, addons=["local_ssh"]
)
@@ -1000,10 +1021,11 @@ async def test_restore_with_healthcheck(
nonlocal _container_events_task
_container_events_task = asyncio.create_task(container_events())
with patch.object(DockerAddon, "run", new=container_events_task), patch.object(
DockerAddon, "is_running", return_value=False
), patch.object(AddonModel, "_validate_availability"), patch.object(
Addon, "with_ingress", new=PropertyMock(return_value=False)
with (
patch.object(DockerAddon, "run", new=container_events_task),
patch.object(DockerAddon, "is_running", return_value=False),
patch.object(AddonModel, "_validate_availability"),
patch.object(Addon, "with_ingress", new=PropertyMock(return_value=False)),
):
await coresys.backups.do_restore_partial(backup, addons=["local_ssh"])
@@ -1054,16 +1076,22 @@ async def test_backup_progress(
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
with patch.object(
AddonModel, "backup_mode", new=PropertyMock(return_value=AddonBackupMode.COLD)
), patch("supervisor.addons.addon.asyncio.Event.wait"):
with (
patch.object(
AddonModel,
"backup_mode",
new=PropertyMock(return_value=AddonBackupMode.COLD),
),
patch("supervisor.addons.addon.asyncio.Event.wait"),
):
full_backup: Backup = await coresys.backups.do_backup_full()
await asyncio.sleep(0)
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name")
if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_full_backup"
]
assert messages == [
@@ -1075,10 +1103,10 @@ async def test_backup_progress(
_make_backup_message_for_assert(
reference=full_backup.slug, stage="docker_config"
),
_make_backup_message_for_assert(reference=full_backup.slug, stage="addons"),
_make_backup_message_for_assert(
reference=full_backup.slug, stage="home_assistant"
),
_make_backup_message_for_assert(reference=full_backup.slug, stage="addons"),
_make_backup_message_for_assert(reference=full_backup.slug, stage="folders"),
_make_backup_message_for_assert(
reference=full_backup.slug, stage="finishing_file"
@@ -1100,7 +1128,8 @@ async def test_backup_progress(
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name")
if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_backup"
]
assert messages == [
@@ -1162,18 +1191,21 @@ async def test_restore_progress(
# Install another addon to be uninstalled
request.getfixturevalue("install_addon_example")
with patch("supervisor.addons.addon.asyncio.Event.wait"), patch.object(
HomeAssistant, "restore"
), patch.object(HomeAssistantCore, "update"), patch.object(
AddonModel, "_validate_availability"
), patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)):
with (
patch("supervisor.addons.addon.asyncio.Event.wait"),
patch.object(HomeAssistant, "restore"),
patch.object(HomeAssistantCore, "update"),
patch.object(AddonModel, "_validate_availability"),
patch.object(AddonModel, "with_ingress", new=PropertyMock(return_value=False)),
):
await coresys.backups.do_restore_full(full_backup)
await asyncio.sleep(0)
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name")
if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_full_restore"
]
assert messages == [
@@ -1242,7 +1274,8 @@ async def test_restore_progress(
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name")
if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_restore"
]
assert messages == [
@@ -1277,8 +1310,9 @@ async def test_restore_progress(
addon_backup: Backup = await coresys.backups.do_backup_partial(addons=["local_ssh"])
ha_ws_client.async_send_command.reset_mock()
with patch.object(AddonModel, "_validate_availability"), patch.object(
HomeAssistantCore, "start"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(HomeAssistantCore, "start"),
):
await coresys.backups.do_restore_partial(addon_backup, addons=["local_ssh"])
await asyncio.sleep(0)
@@ -1286,7 +1320,8 @@ async def test_restore_progress(
messages = [
call.args[0]
for call in ha_ws_client.async_send_command.call_args_list
if call.args[0]["data"].get("data", {}).get("name")
if call.args[0]["type"] == WSType.SUPERVISOR_EVENT
and call.args[0]["data"].get("data", {}).get("name")
== "backup_manager_partial_restore"
]
assert messages == [
@@ -1338,10 +1373,13 @@ async def test_freeze_thaw(
container.exec_run.return_value = (0, None)
ha_ws_client.ha_version = AwesomeVersion("2022.1.0")
with patch.object(
AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup")
), patch.object(
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup")
with (
patch.object(
AddonModel, "backup_pre", new=PropertyMock(return_value="pre_backup")
),
patch.object(
AddonModel, "backup_post", new=PropertyMock(return_value="post_backup")
),
):
# Run the freeze
await coresys.backups.freeze_all()
@@ -1465,11 +1503,12 @@ async def test_restore_only_reloads_ingress_on_change(
async def mock_is_running(*_) -> bool:
return True
with patch.object(
HomeAssistantCore, "is_running", new=mock_is_running
), patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
), patch.object(HomeAssistantAPI, "make_request") as make_request:
with (
patch.object(HomeAssistantCore, "is_running", new=mock_is_running),
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
patch.object(HomeAssistantAPI, "make_request") as make_request,
):
make_request.return_value.__aenter__.return_value.status = 200
# Has ingress before and after - not called
@@ -1518,8 +1557,9 @@ async def test_restore_new_addon(
await coresys.addons.uninstall("local_example")
assert "local_example" not in coresys.addons.local
with patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
):
assert await coresys.backups.do_restore_partial(
backup, addons=["local_example"]
@@ -1554,8 +1594,9 @@ async def test_restore_preserves_data_config(
assert install_addon_example.path_config.exists()
assert test_config2.exists()
with patch.object(AddonModel, "_validate_availability"), patch.object(
DockerAddon, "attach"
with (
patch.object(AddonModel, "_validate_availability"),
patch.object(DockerAddon, "attach"),
):
assert await coresys.backups.do_restore_partial(
backup, addons=["local_example"]
@@ -1660,8 +1701,9 @@ async def test_skip_homeassistant_database(
write_json_file(test_db, {"hello": "world"})
write_json_file(test_db_wal, {"hello": "world"})
with patch.object(HomeAssistantCore, "update"), patch.object(
HomeAssistantCore, "start"
with (
patch.object(HomeAssistantCore, "update"),
patch.object(HomeAssistantCore, "start"),
):
await coresys.backups.do_restore_partial(backup, homeassistant=True)
@@ -1735,8 +1777,9 @@ async def test_reload_error(
)
mock_is_mount.return_value = False
with patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir), patch(
"supervisor.backups.manager.Path.glob", return_value=[]
with (
patch("supervisor.backups.manager.Path.is_dir", new=mock_is_dir),
patch("supervisor.backups.manager.Path.glob", return_value=[]),
):
err.errno = errno.EBUSY
await coresys.backups.reload()
@@ -1787,3 +1830,39 @@ async def test_monitoring_after_partial_restore(
backup_instance.restore_addons.assert_called_once_with([TEST_ADDON_SLUG])
assert coresys.core.state == CoreState.RUNNING
coresys.docker.unload.assert_not_called()
@pytest.mark.parametrize(
"pre_backup_error",
[
{
"code": "pre_backup_actions_failed",
"message": "Database migration in progress",
},
{"code": "unknown_command", "message": "Unknown command."},
],
)
async def test_core_pre_backup_actions_failed(
coresys: CoreSys,
ha_ws_client: AsyncMock,
caplog: pytest.LogCaptureFixture,
pre_backup_error: dict[str, str],
tmp_supervisor_data,
path_extern,
):
"""Test pre-backup actions failed in HA core stops backup."""
coresys.core.state = CoreState.RUNNING
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
ha_ws_client.ha_version = AwesomeVersion("2024.7.0")
ha_ws_client.async_send_command.return_value = {
"error": pre_backup_error,
"id": 1,
"success": False,
"type": "result",
}
assert not await coresys.backups.do_backup_full()
assert (
f"Preparing backup of Home Assistant Core failed due to: {pre_backup_error['message']}"
in caplog.text
)

View File

@@ -42,6 +42,7 @@ from supervisor.coresys import CoreSys
from supervisor.dbus.network import NetworkManager
from supervisor.docker.manager import DockerAPI
from supervisor.docker.monitor import DockerMonitor
from supervisor.homeassistant.api import APIState
from supervisor.host.logs import LogsControl
from supervisor.os.manager import OSManager
from supervisor.store.addon import AddonStore
@@ -360,7 +361,9 @@ async def coresys(
)
# WebSocket
coresys_obj.homeassistant.api.get_api_state = AsyncMock(return_value="RUNNING")
coresys_obj.homeassistant.api.get_api_state = AsyncMock(
return_value=APIState("RUNNING", False)
)
coresys_obj.homeassistant._websocket._client = AsyncMock(
ha_version=AwesomeVersion("2021.2.4")
)

View File

@@ -67,6 +67,7 @@ async def test_update(
assert settings["ipv4"]["method"] == Variant("s", "auto")
assert "gateway" not in settings["ipv4"]
assert "dns" not in settings["ipv4"]
assert "dns-data" not in settings["ipv4"]
assert "address-data" not in settings["ipv4"]
assert "addresses" not in settings["ipv4"]
assert len(settings["ipv4"]["route-data"].value) == 1
@@ -83,6 +84,7 @@ async def test_update(
assert settings["ipv6"]["method"] == Variant("s", "auto")
assert "gateway" not in settings["ipv6"]
assert "dns" not in settings["ipv6"]
assert "dns-data" not in settings["ipv6"]
assert "address-data" not in settings["ipv6"]
assert "addresses" not in settings["ipv6"]
assert settings["ipv6"]["addr-gen-mode"] == Variant("i", 0)

View File

@@ -31,6 +31,7 @@ SETTINGS_FIXTURE: dict[str, dict[str, Variant]] = {
),
"addresses": Variant("aau", [[2483202240, 24, 16951488]]),
"dns": Variant("au", [16951488]),
"dns-data": Variant("as", ["192.168.2.1"]),
"dns-search": Variant("as", []),
"gateway": Variant("s", "192.168.2.1"),
"method": Variant("s", "auto"),

View File

@@ -0,0 +1,5 @@
{
"name": "Music Assistant",
"url": "https://github.com/music-assistant/core",
"maintainer": "Music Assistant <marcelveldt@users.noreply.github.com>"
}

View File

@@ -21,6 +21,7 @@ from supervisor.exceptions import (
HomeAssistantError,
HomeAssistantJobError,
)
from supervisor.homeassistant.api import APIState
from supervisor.homeassistant.core import HomeAssistantCore
from supervisor.homeassistant.module import HomeAssistant
from supervisor.updater import Updater
@@ -316,6 +317,42 @@ async def test_api_check_success(
assert "Detect a running Home Assistant instance" in caplog.text
async def test_api_check_database_migration(
coresys: CoreSys, container: MagicMock, caplog: pytest.LogCaptureFixture
):
"""Test attempts to contact the API timeout."""
calls = []
def mock_api_state(*args):
calls.append(None)
if len(calls) > 50:
return APIState("RUNNING", False)
else:
return APIState("NOT_RUNNING", True)
container.status = "stopped"
coresys.homeassistant.version = AwesomeVersion("2023.9.0")
coresys.homeassistant.api.get_api_state.side_effect = mock_api_state
async def mock_instance_start(*_):
container.status = "running"
with (
patch.object(DockerHomeAssistant, "start", new=mock_instance_start),
patch.object(DockerAPI, "container_is_initialized", return_value=True),
travel(datetime(2023, 10, 2, 0, 0, 0), tick=False) as traveller,
):
async def mock_sleep(*args):
traveller.shift(timedelta(minutes=1))
with patch("supervisor.homeassistant.core.asyncio.sleep", new=mock_sleep):
await coresys.homeassistant.core.start()
assert coresys.homeassistant.api.get_api_state.call_count == 51
assert "Detect a running Home Assistant instance" in caplog.text
async def test_core_loads_wrong_image_for_machine(
coresys: CoreSys, container: MagicMock
):

View File

@@ -5,12 +5,17 @@ import errno
from pathlib import Path
from unittest.mock import AsyncMock, patch
from pytest import LogCaptureFixture
from pytest import LogCaptureFixture, raises
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.docker.interface import DockerInterface
from supervisor.exceptions import (
HomeAssistantBackupError,
HomeAssistantWSConnectionError,
)
from supervisor.homeassistant.secrets import HomeAssistantSecrets
from supervisor.homeassistant.websocket import HomeAssistantWebSocket
async def test_load(
@@ -21,12 +26,14 @@ async def test_load(
secrets.write("hello: world\n")
# Unwrap read_secrets to prevent throttling between tests
with patch.object(DockerInterface, "attach") as attach, patch.object(
DockerInterface, "check_image"
) as check_image, patch.object(
HomeAssistantSecrets,
"_read_secrets",
new=HomeAssistantSecrets._read_secrets.__wrapped__,
with (
patch.object(DockerInterface, "attach") as attach,
patch.object(DockerInterface, "check_image") as check_image,
patch.object(
HomeAssistantSecrets,
"_read_secrets",
new=HomeAssistantSecrets._read_secrets.__wrapped__,
),
):
await coresys.homeassistant.load()
@@ -70,3 +77,34 @@ def test_write_pulse_error(coresys: CoreSys, caplog: LogCaptureFixture):
assert "can't write pulse/client.config" in caplog.text
assert coresys.core.healthy is False
async def test_begin_backup_ws_error(coresys: CoreSys):
"""Test WS error when beginning backup."""
# pylint: disable-next=protected-access
coresys.homeassistant.websocket._client.async_send_command.side_effect = (
HomeAssistantWSConnectionError
)
with (
patch.object(HomeAssistantWebSocket, "_can_send", return_value=True),
raises(
HomeAssistantBackupError,
match="Preparing backup of Home Assistant Core failed. Check HA Core logs.",
),
):
await coresys.homeassistant.begin_backup()
async def test_end_backup_ws_error(coresys: CoreSys, caplog: LogCaptureFixture):
"""Test WS error when ending backup."""
# pylint: disable-next=protected-access
coresys.homeassistant.websocket._client.async_send_command.side_effect = (
HomeAssistantWSConnectionError
)
with patch.object(HomeAssistantWebSocket, "_can_send", return_value=True):
await coresys.homeassistant.end_backup()
assert (
"Error resuming normal operations after backup of Home Assistant Core. Check HA Core logs."
in caplog.text
)

View File

@@ -46,7 +46,7 @@ async def test_watchdog_homeassistant_api(
restart.assert_called_once()
assert "Watchdog missed an Home Assistant Core API response." not in caplog.text
assert (
"Watchdog missed 2 Home Assistant Core API responses in a row. Restarting Home Assistant Core API!"
"Watchdog missed 2 Home Assistant Core API responses in a row. Restarting Home Assistant Core!"
in caplog.text
)
@@ -109,31 +109,48 @@ async def test_watchdog_homeassistant_api_reanimation_limit(
HomeAssistantAPI, "check_api_state", return_value=False
), patch.object(
HomeAssistantCore, "restart", side_effect=(err := HomeAssistantError())
) as restart:
) as restart, patch.object(
HomeAssistantCore, "rebuild", side_effect=err
) as rebuild:
for _ in range(5):
await tasks._watchdog_homeassistant_api()
restart.assert_not_called()
await tasks._watchdog_homeassistant_api()
restart.assert_called_once()
restart.assert_called_once_with()
assert "Home Assistant watchdog reanimation failed!" in caplog.text
rebuild.assert_not_called()
restart.reset_mock()
capture_exception.assert_called_once_with(err)
# Next time it should try safe mode
caplog.clear()
await tasks._watchdog_homeassistant_api()
rebuild.assert_not_called()
await tasks._watchdog_homeassistant_api()
rebuild.assert_called_once_with(safe_mode=True)
restart.assert_not_called()
assert "Watchdog missed an Home Assistant Core API response." not in caplog.text
assert "Watchdog found a problem with Home Assistant API!" not in caplog.text
assert (
"Watchdog cannot reanimate Home Assistant Core, failed all 5 attempts."
"Watchdog cannot reanimate Home Assistant Core, failed all 5 attempts. Restarting into safe mode"
in caplog.text
)
assert (
"Safe mode restart failed. Watchdog cannot bring Home Assistant online."
in caplog.text
)
# After safe mode has failed too, no more restart attempts
rebuild.reset_mock()
caplog.clear()
await tasks._watchdog_homeassistant_api()
assert "Watchdog missed an Home Assistant Core API response." in caplog.text
caplog.clear()
await tasks._watchdog_homeassistant_api()
restart.assert_not_called()
assert not caplog.text
restart.assert_not_called()
rebuild.assert_not_called()

View File

@@ -1,4 +1,5 @@
"""Test base plugin functionality."""
import asyncio
from unittest.mock import MagicMock, Mock, PropertyMock, patch
@@ -60,14 +61,17 @@ async def fixture_plugin(
)
async def test_plugin_watchdog(coresys: CoreSys, plugin: PluginBase) -> None:
"""Test plugin watchdog works correctly."""
with patch.object(type(plugin.instance), "attach"), patch.object(
type(plugin.instance), "is_running", return_value=True
with (
patch.object(type(plugin.instance), "attach"),
patch.object(type(plugin.instance), "is_running", return_value=True),
):
await plugin.load()
with patch.object(type(plugin), "rebuild") as rebuild, patch.object(
type(plugin), "start"
) as start, patch.object(type(plugin.instance), "current_state") as current_state:
with (
patch.object(type(plugin), "rebuild") as rebuild,
patch.object(type(plugin), "start") as start,
patch.object(type(plugin.instance), "current_state") as current_state,
):
current_state.return_value = ContainerState.UNHEALTHY
coresys.bus.fire_event(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE,
@@ -168,9 +172,10 @@ async def test_plugin_watchdog_max_failed_attempts(
container.status = "stopped"
container.attrs = {"State": {"ExitCode": 1}}
with patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0), patch.object(
type(plugin), "start", side_effect=error
) as start:
with (
patch("supervisor.plugins.base.WATCHDOG_RETRY_SECONDS", 0),
patch.object(type(plugin), "start", side_effect=error) as start,
):
await plugin.watchdog_container(
DockerContainerStateEvent(
name=plugin.instance.name,
@@ -198,17 +203,18 @@ async def test_plugin_load_running_container(
) -> None:
"""Test plugins load and attach to a running container."""
test_version = AwesomeVersion("2022.7.3")
with patch.object(
type(coresys.bus), "register_event"
) as register_event, patch.object(
type(plugin.instance), "attach"
) as attach, patch.object(type(plugin), "install") as install, patch.object(
type(plugin), "start"
) as start, patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
), patch.object(type(plugin.instance), "is_running", return_value=True):
with (
patch.object(type(coresys.bus), "register_event") as register_event,
patch.object(type(plugin.instance), "attach") as attach,
patch.object(type(plugin), "install") as install,
patch.object(type(plugin), "start") as start,
patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
),
patch.object(type(plugin.instance), "is_running", return_value=True),
):
await plugin.load()
register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@@ -230,17 +236,18 @@ async def test_plugin_load_stopped_container(
) -> None:
"""Test plugins load and start existing container."""
test_version = AwesomeVersion("2022.7.3")
with patch.object(
type(coresys.bus), "register_event"
) as register_event, patch.object(
type(plugin.instance), "attach"
) as attach, patch.object(type(plugin), "install") as install, patch.object(
type(plugin), "start"
) as start, patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
), patch.object(type(plugin.instance), "is_running", return_value=False):
with (
patch.object(type(coresys.bus), "register_event") as register_event,
patch.object(type(plugin.instance), "attach") as attach,
patch.object(type(plugin), "install") as install,
patch.object(type(plugin), "start") as start,
patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
),
patch.object(type(plugin.instance), "is_running", return_value=False),
):
await plugin.load()
register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@@ -262,17 +269,20 @@ async def test_plugin_load_missing_container(
) -> None:
"""Test plugins load and create and start container."""
test_version = AwesomeVersion("2022.7.3")
with patch.object(
type(coresys.bus), "register_event"
) as register_event, patch.object(
type(plugin.instance), "attach", side_effect=DockerError()
) as attach, patch.object(type(plugin), "install") as install, patch.object(
type(plugin), "start"
) as start, patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
), patch.object(type(plugin.instance), "is_running", return_value=False):
with (
patch.object(type(coresys.bus), "register_event") as register_event,
patch.object(
type(plugin.instance), "attach", side_effect=DockerError()
) as attach,
patch.object(type(plugin), "install") as install,
patch.object(type(plugin), "start") as start,
patch.object(
type(plugin.instance),
"get_latest_version",
return_value=test_version,
),
patch.object(type(plugin.instance), "is_running", return_value=False),
):
await plugin.load()
register_event.assert_any_call(
BusEvent.DOCKER_CONTAINER_STATE_CHANGE, plugin.watchdog_container
@@ -301,9 +311,12 @@ async def test_update_fails_if_out_of_date(
"""Test update of plugins fail when supervisor is out of date."""
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
with patch.object(
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
), pytest.raises(error):
with (
patch.object(
type(coresys.supervisor), "need_update", new=PropertyMock(return_value=True)
),
pytest.raises(error),
):
await plugin.update()
@@ -316,10 +329,14 @@ async def test_repair_failed(
coresys: CoreSys, capture_exception: Mock, plugin: PluginBase
):
"""Test repair failed."""
with patch.object(DockerInterface, "exists", return_value=False), patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
), patch(
"supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted
with (
patch.object(DockerInterface, "exists", return_value=False),
patch.object(
DockerInterface, "arch", new=PropertyMock(return_value=CpuArch.AMD64)
),
patch(
"supervisor.security.module.cas_validate", side_effect=CodeNotaryUntrusted
),
):
await plugin.repair()
@@ -360,3 +377,16 @@ async def test_load_with_incorrect_image(
platform="linux/amd64",
)
assert plugin.image == correct_image
@pytest.mark.parametrize(
"plugin",
[PluginAudio, PluginCli, PluginDns, PluginMulticast, PluginObserver],
indirect=True,
)
async def test_default_image_fallback(
coresys: CoreSys, container: MagicMock, plugin: PluginBase
):
"""Test default image falls back to hard-coded constant if we fail to fetch version file."""
assert getattr(coresys.updater, f"image_{plugin.slug}") is None
assert plugin.default_image == f"ghcr.io/home-assistant/amd64-hassio-{plugin.slug}"

View File

@@ -163,6 +163,7 @@ async def test_preinstall_valid_repository(
assert store_manager.get("local").validate()
assert store_manager.get("a0d7b954").validate()
assert store_manager.get("5c53de3b").validate()
assert store_manager.get("d5369777").validate()
@pytest.mark.parametrize("use_update", [True, False])

View File

@@ -39,11 +39,11 @@ async def test_default_load(coresys: CoreSys):
):
await store_manager.load()
assert len(store_manager.all) == 4
assert len(store_manager.all) == 5
assert isinstance(store_manager.get("core"), Repository)
assert isinstance(store_manager.get("local"), Repository)
assert len(store_manager.repository_urls) == 2
assert len(store_manager.repository_urls) == 3
assert (
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
)
@@ -51,6 +51,11 @@ async def test_default_load(coresys: CoreSys):
"https://github.com/esphome/home-assistant-addon"
in store_manager.repository_urls
)
assert (
"https://github.com/music-assistant/home-assistant-addon"
in store_manager.repository_urls
)
# NOTE: When adding new stores, make sure to add it to tests/fixtures/addons/git/
assert refresh_cache_calls == {"local_ssh", "local_example", "core_samba"}
@@ -77,11 +82,11 @@ async def test_load_with_custom_repository(coresys: CoreSys):
):
await store_manager.load()
assert len(store_manager.all) == 5
assert len(store_manager.all) == 6
assert isinstance(store_manager.get("core"), Repository)
assert isinstance(store_manager.get("local"), Repository)
assert len(store_manager.repository_urls) == 3
assert len(store_manager.repository_urls) == 4
assert (
"https://github.com/hassio-addons/repository" in store_manager.repository_urls
)
@@ -89,6 +94,10 @@ async def test_load_with_custom_repository(coresys: CoreSys):
"https://github.com/esphome/home-assistant-addon"
in store_manager.repository_urls
)
assert (
"https://github.com/music-assistant/home-assistant-addon"
in store_manager.repository_urls
)
assert "http://example.com" in store_manager.repository_urls
@@ -105,11 +114,11 @@ async def test_load_from_core_config(coresys: CoreSys):
), patch("pathlib.Path.exists", return_value=True):
await coresys.store.load()
assert len(coresys.store.all) == 5
assert len(coresys.store.all) == 6
assert isinstance(coresys.store.get("core"), Repository)
assert isinstance(coresys.store.get("local"), Repository)
assert len(coresys.store.repository_urls) == 3
assert len(coresys.store.repository_urls) == 4
assert (
"https://github.com/hassio-addons/repository" in coresys.store.repository_urls
)
@@ -117,6 +126,10 @@ async def test_load_from_core_config(coresys: CoreSys):
"https://github.com/esphome/home-assistant-addon"
in coresys.store.repository_urls
)
assert (
"https://github.com/music-assistant/home-assistant-addon"
in coresys.store.repository_urls
)
assert "http://example.com" in coresys.store.repository_urls
assert coresys.config.addons_repositories == []
@@ -243,12 +256,12 @@ async def test_install_unavailable_addon(
async def test_reload(coresys: CoreSys):
"""Test store reload."""
await coresys.store.load()
assert len(coresys.store.all) == 4
assert len(coresys.store.all) == 5
with patch.object(GitRepo, "pull") as git_pull:
await coresys.store.reload()
assert git_pull.call_count == 3
assert git_pull.call_count == 4
async def test_addon_version_timestamp(coresys: CoreSys, install_addon_example: Addon):

View File

@@ -49,12 +49,13 @@ async def test_repository_validate(repo_list: list[str], valid: bool):
"""Test repository list validate."""
if valid:
processed = repositories(repo_list)
assert len(processed) == 4
assert len(processed) == 5
assert set(repositories(repo_list)) == {
"core",
"local",
"https://github.com/hassio-addons/repository",
"https://github.com/esphome/home-assistant-addon",
"https://github.com/music-assistant/home-assistant-addon",
}
else:
with pytest.raises(Invalid):