remove_delta_apps checked membership against the app_list property for
every installed app. That property rebuilds a list on each access, so
the check rebuilt and linearly scanned it once per installed app.
Resolve the slugs into a set once before the loop for constant-time
membership checks. Behavior is unchanged.
The per-file exclusion filter in folder backups looped over
sys_mounts.bound_mounts for every file. That property rebuilds a list
from a dict on each call, so backing up a folder with many files meant
rebuilding the list and scanning it linearly once per file.
Bind mounts don't change during a backup, so resolve the set of paths to
skip once up front and turn the per-file check into a single set
membership test.
Add-on repositories can pin a branch with the "url#branch" syntax. The
branch part of the repository regex only accepted word characters,
hyphens and dots, so a branch with a slash like "feature/hot-new-stuff"
failed to match and the repository was rejected as invalid.
Git branch names commonly use prefixes such as "feature/" or "fix/", so
allow slashes in the captured branch name.
The `/addons/{slug}/info` endpoint returned the target app's user options,
which can contain secrets such as passwords and API keys. The security
middleware grants every role (including the default role) access to any
`/.+/info` path, so an installed app with `hassio_api: true` and the default
role could read another app's options simply by requesting its info.
Redact the options field in info_data() unless the caller is entitled to see
it: Home Assistant Core (and other non-app internals), the app reading its
own info, or an app with the manager or admin role. Other apps reading a
different app's info now receive an empty options dict while all non-secret
metadata stays available for discovery. This mirrors the existing self-only
restriction on the dedicated /options/config endpoint.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Make Core.shutdown idempotent and safe to call concurrently
After #6887, Core.shutdown() now runs in the SIGTERM path during host
shutdown in addition to the existing host.control reboot/shutdown and
backup restore paths. Multiple concurrent callers were possible (e.g.
SIGTERM arriving while a reboot API call is mid-flight), so __main__.py
debounced the signal handler by stashing the in-flight task in a single-
element list and bailing out on the second SIGTERM.
Move the idempotency into Core.shutdown() itself, where it belongs:
- A second call while shutdown is in progress awaits the in-flight
shutdown via an asyncio.Event rather than re-running the sequence.
- Calls during STOPPING/CLOSE return early (Supervisor is already going
away; the work is moot).
- Calls during STARTING_STATES (INITIALIZE/STARTUP/SETUP) return early
too. There is nothing coherent to gracefully stop before startup
completes, and on the SIGTERM-during-startup path the caller cancels
startup_task first, so waiting for it to complete would deadlock.
- The sequence is wrapped in try/finally so the completion event is set
even when an inner step raises.
With that in place the closure workaround in __main__.py collapses to a
plain coresys.create_task(stop_supervisor()): repeat SIGTERMs spawn
extra tasks but each just observes the in-flight shutdown and waits.
Tests cover the four state branches and confirm the event is reset
between repeated shutdown cycles (backup restore re-enters RUNNING).
* Split Core.shutdown() into teardown_services + shutdown
PR feedback (@mdegat01) flagged the "supports repeated use" comment on
_shutdown_event.clear() as describing a use case that does not exist.
Investigating, the real source of confusion is that the old shutdown()
did two different things stitched together:
- Stop user-facing containers (add-ons + Home Assistant Core), which
backup restore uses while leaving the host alone.
- Run the full shutdown ceremony (state transition to SHUTDOWN, stop
plugins), which only the SIGTERM signal handler and the host
reboot/power-off API want.
That dispatch was implemented with an asymmetric state transition
("only set SHUTDOWN if state == RUNNING") and a plugin-shutdown gate
("only stop plugins if state in (STOPPING, SHUTDOWN)"). It worked but
made the intent of each branch hard to read, broke the reentrancy
guard on the restore path (state never reaches SHUTDOWN, so concurrent
callers fall through every early return), and forced the misleading
"repeated cycles" framing on the event handling.
Split into two methods with one job each:
- teardown_services(): stop add-ons + Home Assistant Core. Does not
change Core state and does not stop plugins. Backup restore calls
this directly so HA Core's watchdog stays registered (it only
disables on transitions into CLOSING_STATES) and plugins keep
running for the restore body to use.
- shutdown(): real shutdown ceremony. Unconditionally transitions to
SHUTDOWN, calls teardown_services(), then stops plugins. The
reentrancy guard (state == SHUTDOWN -> await event) now works
correctly because every caller transitions state on entry. One-shot
per process lifetime; no clear() needed.
Update backups/manager.py:867 to call teardown_services() instead.
remove_homeassistant_container moves to teardown_services() since
restore is the only caller that passes it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* Release shutdown waiters when set_state() is cancelled
PR feedback from Copilot: set_state() updates Core._state synchronously
(line 84 in core.py) before awaiting _write_run_state(). If the
shutdown task is cancelled while awaiting that write, in-memory state
is already SHUTDOWN but the function exits before entering the
try/finally that sets _shutdown_event. Any concurrent or later
shutdown() caller then sees state == SHUTDOWN and blocks forever on
_shutdown_event.wait().
Move the set_state(SHUTDOWN) call inside the try so finally always
runs and releases waiters. CancelledError still propagates to the
caller after finally as expected; we just no longer leak the lock.
Add a regression test that simulates cancellation inside
_write_run_state() and asserts both that state has moved to SHUTDOWN
and that _shutdown_event is set.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* Fix typos in comments, docstrings and log messages
Correct 39 spelling mistakes across comments, docstrings and log/error
message strings throughout the package (e.g. "conection" -> "connection",
"Incomming" -> "Incoming", "Rasie" -> "Raise"). All changes are confined
to human-readable text; no identifiers, attributes or D-Bus contracts are
touched, so there is no behavior change. Found with codespell.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix typos in tests and CI workflow
Correct spelling mistakes in test comments, docstrings and data, plus one
in the builder workflow, so the whole tree is clean for the codespell hook
added next. The assertion in test_network_manager.py is updated to match
the corrected "Unknown error while processing" log message in the source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add codespell pre-commit hook
Wire up codespell so spelling mistakes in comments, docstrings and strings
are caught automatically. The vendored frontend panel is excluded, and
"hass" and "astroid" are added to the ignore list as known false positives
(the Home Assistant abbreviation and the pylint dependency package).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address review feedback
Improve grammar in several of the touched comments and docstrings: use the
plural "ignore conditions" for the list-returning property, add the missing
auxiliary verb and fix agreement in the timezone-filter comment, fix
"backups ... use" agreement, and reword "underlay" to "underlying" in the
arch module docstring.
Also drop the "*.json" skip from the codespell hook. It was carried over
from another project but is unnecessary here (all tracked JSON is clean),
and skipping it would needlessly leave translation and data JSON unchecked.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Reword onboarding comment
"overflight" was a literal calque of the German "überflogen"; use the
idiomatic "skimmed through" instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mount failures generally reflect user configuration or host conditions
(unreachable server, wrong credentials, ...) rather than a Supervisor
bug. As a plain HassioError, MountError reached the generic error branch
of api_process, which logged a full traceback as an "Unexpected error
during API call" and captured the exception to Sentry. The mount reload
path already encoded the opposite intent by explicitly skipping Sentry
for MountError.
Make MountError an APIError so mount failures are surfaced as client-side
errors with their explicit message, without a traceback or Sentry noise,
matching the existing JobException handling. MountNotFound additionally
inherits from APINotFound so it returns a 404 instead of a 400.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(docker): register hw listener and match by-id paths for options-based devices
Two bugs caused a crash loop when a USB device re-enumerates to a different
minor number (e.g. ttyACM0→ttyACM1) after a HAOS reboot:
1. _hw_listener was only registered when addon.static_devices was non-empty.
Addons that expose a device via the options schema (e.g. Z-Wave JS `device:`
option) never had the listener registered, so add_devices_allowed was never
called when the device reappeared at a new minor.
2. _hardware_events matched only device.path and device.sysfs against
static_devices. When static_devices (or the new options path) contains a
by-id symlink, the match always failed because by-id paths live in
device.links.
Fix: extend the listener registration condition to also cover addon.devices
(options-based), and expand the path-matching set to include device.links so
by-id paths resolve correctly. For options-based devices, compare the incoming
Device against addon.devices (which re-evaluates options.json against the live
hardware list, picking up the new minor number automatically).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(docker): use option_device_paths for cheap by-id hw event matching
Refactor _hardware_events to avoid per-event full options validation
(including pwnd hashing). Introduce AppOptions.extract_device_paths and
AppModel.option_device_paths to extract raw device paths from options
without resolving against live hardware. Use set-intersection against
{device.path, device.sysfs, *device.links} so by-id symlinks match
correctly after re-enumeration for both static and options-based devices.
Update test to use real schema/options setup and simulate a minor-number
change (ttyACM0→ttyACM1) with a stable by-id symlink.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: improve hw listener test coverage and add policy check
Address PR review feedback:
- Add hardware policy check in _hardware_events to prevent bypassing access
restrictions on hotplug events (follows same pattern as startup cgroup setup)
- Fix test_app_options_device_hw_listener to properly simulate USB re-enumeration
with different minor numbers (166:0 → 166:1)
- Add test_app_options_device_policy_check to verify policy enforcement for
options-based devices
- Update TEST_HW_DEVICE with realistic major/minor attributes (166:0 for tty)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: mock HostFeature.OS_AGENT in hardware event tests
The _hardware_events method has a @Job decorator with conditions=[JobCondition.OS_AGENT],
which checks if HostFeature.OS_AGENT is in sys_host.features. Without mocking this,
the job conditions fail and the hardware event handler is never invoked, causing
add_devices_allowed to not be called and tests to fail.
Add patch.object(type(coresys.host), "features", ...) to all four hardware event tests
to ensure the OS_AGENT job condition is met.
Fixes test failures:
- test_app_new_device (all 6 parametrized cases)
- test_app_new_device_no_haos
- test_app_options_device_hw_listener
- test_app_options_device_policy_check
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: fix TEST_DEV_PATH to match TEST_HW_DEVICE.path
TEST_DEV_PATH was set to /dev/ttyAMA0 but TEST_HW_DEVICE.path is /dev/ttyACM0.
This mismatch would cause the dev_path=TEST_DEV_PATH parametrized test cases
to fail because the hardware event handler checks if the device path intersects
with the app's allowed devices, and "/dev/ttyAMA0" != "/dev/ttyACM0".
Update TEST_DEV_PATH from /dev/ttyAMA0 to /dev/ttyACM0 to match TEST_HW_DEVICE.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: mock option_device_paths in test_app_options_device_hw_listener
The test sets up schema and options but option_device_paths property may not
be working as expected in the test environment. Add an explicit mock for
option_device_paths to ensure it returns the by-id path, guaranteeing that:
1. The hardware listener is registered (checks option_device_paths at registration)
2. The device path matching works correctly in _hardware_events
This ensures the test properly validates that hardware events are processed
for options-based devices after re-enumeration.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: make policy check test actually exercise the policy guard
test_app_options_device_policy_check set the device option via
persist["options"], but option_device_paths reads the merged options and
does not pick that override up during the test, so it returned an empty
set. The hardware event therefore failed the path-match guard and returned
before reaching the allowed_for_access check. The assert_not_called()
assertion then passed regardless of the policy outcome -- it would still
pass if the policy guard were removed entirely.
Mock option_device_paths to return the configured by-id path (mirroring
test_app_options_device_hw_listener) so the event device matches and
execution actually reaches the policy guard the test is meant to verify.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add unit test for AppOptions.extract_device_paths
The integration tests exercise extract_device_paths only through a mocked
option_device_paths property, so the schema-walking logic introduced for
the hardware-event matching had no direct coverage.
Add a unit test that drives every schema shape the recursion handles --
flat, optional, filtered, list, nested dict and list of dicts -- and
asserts that non-device options, unset keys and empty values are skipped,
without requiring the devices to exist in hardware.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
The REST API handler class for the resolution center was misspelled as
"APIResoulution", which also leaked into its docstring and the related
comment in core.py. Rename it to "APIResolution" and correct the
surrounding spelling. The class is internal to the Supervisor package,
so no public API path is affected.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The blacklist in the security middleware didn't take the v2 prefix into
account, allowing to call routes that are supposed to be blacklisted for
apps with hassio and homeassistant APIs enabled in app config.
Add test checking that these routes are always blacklisted, and
parametrize other tests using v2 endpoints in the test_security module.
This is a corner case: a locally-built app whose container image went missing
for some external reason and which is also detached from its store, e.g. its
local source folder was removed. There is no normal Supervisor command that
produces this combination -- rebuild removes the image before rebuilding, but
it is rejected for detached apps, so it cannot leave a detached app without an
image. The image loss here came from outside Supervisor's normal flow.
Such an app cannot be rebuilt: there is no source. App.load still surfaced a
MISSING_IMAGE repair with an EXECUTE_REPAIR suggestion for it, and the
resolution autofix loop then tried to rebuild it. That dead-ends in
App.path_location, which raises for a detached app, crashing the autofix with
an unhandled exception.
Don't create the rebuild repair when the app is detached. The detached-app
check already surfaces a DETACHED_ADDON_REMOVED issue with an EXECUTE_REMOVE
suggestion for these apps (the built-in local repository is always loaded), so
the user is offered removal instead of a repair that can never succeed.
Also guard the repair fixup itself against a detached build app. With the
App.load change this is only reachable via a race: the repair is created while
the app is attached and the app detaches before the autofix runs (or between
its retries). Skip gracefully there too rather than throwing, mirroring the
is_detached guards already in AppManager.update/rebuild.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The hassos-config service was renamed to haos-config in
home-assistant/operating-system#4740. This change preserved the old name
as an alias, however, Supervisor uses ListUnits that doesn't report the
aliases and then thinks such unit doesn't exist. To fix that, simply
call the unit by its primary name which now varies by the OS version.
Fixes#6942
Persisting the timezone to the host requires Home Assistant OS 16.2 or
newer, since /etc/localtime is not writable on older versions. While 16.2
was still unreleased this was logged at info level, as running an older OS
was expected. Now that 16.2 has shipped, an OS that still cannot persist
the timezone is outdated, so raise the message to a warning to make the
limitation visible to the user. Drop the now-resolved TODO and its fixme
suppression.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The landingpage image now stamps a real Core version into its
io.hass.version label rather than the sentinel "landingpage" string.
Supervisor decided whether Core was still just a landingpage by
comparing that version against the LANDINGPAGE constant, so with the
new label it mistook the landingpage for an installed Core. Restarting
the Supervisor mid-install then stranded the system on the landingpage
until the next OS reboot, with no install job scheduled and the
watchdog hot-looping on the missing Core auth API.
Override the version resolution for the Home Assistant container so a
landingpage image (io.hass.type == "landingpage") always reports
LANDINGPAGE, keeping every existing version check working unchanged
while leaving io.hass.version free to carry the real version.
Fixes#6934
* Derive installed-app source location from the store
The installed app's source location was persisted as an absolute string in
apps.json, captured at install time. The addons->apps directory migration
renamed the source directories but left that stored string pointing at the old
addons path, so locally-built apps failed to build with a misleading
"dockerfile is missing" error on install/update/rebuild (#6917).
The location is not really app state: it is the source directory discovered
during the store scan and recomputed on every store reload. Derive it from the
store data (App.path_location -> data_store) instead of persisting it, and drop
ATTR_LOCATION from the system schema. REMOVE_EXTRA strips the stale value from
existing apps.json files, so no migration is needed and already-migrated
instances are fixed as well.
location only exists for a store-backed app. The store is loaded before apps
during setup (Core.setup ordering), so an installed, attached app can always
resolve it; reaching path_location while detached is a programming error and
now raises. Detached apps have no source, so their asset accessors
(with_icon/logo/changelog/documentation, long_description) report absence
rather than reading a path, and the path cache is no longer refreshed for them.
Build, apparmor install and backup/restore never touch path_location on a
detached app (build/update are blocked when detached; apparmor and the built
image are captured from the host and restored from the backup), so those paths
are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Guard path_location on app_store instead of is_detached
Address review feedback: path_location guarded on is_detached while the asset
accessors guarded on app_store, two ways of expressing the same condition.
Key path_location off app_store as well, matching install()/update() and the
store API, so the check is consistent and is_detached is no longer needed here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Return 404 for raspberrypi endpoints on boards without firmware
Return "not found" when board doesn't have the firmware update
available. Any other error may indicate it's worth retrying later (as
discussed in [1]), so 404 is more appropriate here.
[1] https://github.com/home-assistant/core/pull/172929#discussion_r3363706261
* Consistently return APINotFound with debug log
* Sync docker pull progress to Core install job
The home_assistant_core_install job had no child_job_syncs, so the
download/extract progress tracked on the internal docker_interface_install
job never propagated to the user-visible install job. On a fresh system,
/jobs reporting therefore jumped from 0 to 100 with nothing in between
during the initial Core install.
Mirror what home_assistant_core_update already does and sync the docker
pull progress up to the install job, allocating it the full progress
range. As with the update path, this treats the image pull as 100% of the
task even though image cleanup and Home Assistant start are not accounted
for.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Test Core install job is exposed with progress for landing page
The landing page frontend (home-assistant/frontend#52359) polls /jobs/info
during the initial Core install and reads the progress of a root job named
home_assistant_core_install to render a download progress bar. Add a test
that guards this contract: the install must expose a non-internal
home_assistant_core_install job whose progress is driven by the docker image
pull, reporting intermediate values rather than a bare 0 to 100 jump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Exclude Supervisor self-update pull from Core install progress
The home_assistant_core_install (and _update) jobs sync their progress from
the docker image pull via child_job_syncs. The install job may first trigger
a Supervisor self-update, which also pulls an image through a
docker_interface_install job. With an unscoped filter that pull incorrectly
counted towards Core install progress, driving the user-visible job to 100%
before the actual Core image download even started.
Scope the child sync to the Home Assistant container reference so only the
Core image pull contributes. Promote the container name constant to public
(HASS_DOCKER_NAME) so it can be reused for the filter reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Reset synced progress when a child job is re-triggered
A parent job syncs its progress from a child via child_job_syncs, capturing
the parent's progress at the time the child starts as the band the child
fills. When the same child ran more than once - e.g. the Core image pull,
which runs in a retry loop and which Docker resumes at layer granularity -
the second child stacked on top of the first attempt's leftover progress,
pushing the user-visible job straight to 100% instead of restarting.
Record a per-sync baseline on the parent: the first child to match a sync
captures the baseline, and children matching the same sync afterwards reuse
it and reset the parent back to it. This implements the behavior the prior
HACK comment anticipated (reset progress instead of skipping the second
sync) and removes the now-unneeded progress >= 100 skip.
Add tests covering an install retry (progress resets rather than overshoots)
and a Supervisor self-update done first (its pull does not count towards the
Core install job).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Updating Core to a non-existent version (e.g. a mistyped beta tag) reported
success on the CLI: the image pull failed with a 404, but update() swallowed
the error via "with suppress(HomeAssistantError)" and then ran its post-update
health check against the still-running old Core, which passed.
Split the update routine into image install and start phases. A failed image
install leaves the running Core untouched, so there is nothing to health-check
or roll back; let it bubble out instead of masking it. Only failures after the
image is in place (e.g. the new container starting unhealthy) fall through to
the health check and rollback logic, where the error is now captured at debug
level rather than silently suppressed.
Model the update errors as client-facing APIError subclasses carrying an
error_key so the frontend can translate them and they no longer reach Sentry as
unexpected errors:
- HomeAssistantUpdateError: generic update failure
- HomeAssistantUpdateImageError: image download failed (includes the version)
- HomeAssistantUpdateAlreadyInstalledError: requested version already installed
Unix socket communication between Supervisor and Home Assistant Core was
initially gated behind the UNIX_SOCKET_CORE_API feature flag, then
enabled by default for Core 2026.5.1+ while older supported versions
still required the flag. Now that the transport has settled and is on by
default, the flag no longer serves a purpose.
Remove the feature flag and the CORE_UNIX_SOCKET_DEFAULT_VERSION split,
collapsing supports_unix_socket back to a single check: the Unix socket
is used for any non-landingpage Core version at or above
CORE_UNIX_SOCKET_MIN_VERSION.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a transport validity check before the WebSocket upgrade to handle
clients that disconnect during the handshake.
The connection can be lost between the Home Assistant API state check and
the server.prepare() call, leaving request.transport as None. aiohttp's
_pre_start() then raises ConnectionResetError (an AssertionError prior to
aiohttp 3.14.0), which propagates out of the handler as an unhandled
exception. The result is a 500 response and a Sentry report for what is
really just a client disconnect.
The fix detects the closed connection early and raises HTTPBadRequest
with a clear reason, turning the race into a clean 4xx response with a
warning log instead of error noise.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Bump aiohttp from 3.13.5 to 3.14.0
---
updated-dependencies:
- dependency-name: aiohttp
dependency-version: 3.14.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* Adjust API layer for aiohttp 3.14
aiohttp 3.14 raises NotAppKeyWarning when a plain string is used as a
request storage key. Convert REQUEST_FROM to a web.RequestKey instance so
request[REQUEST_FROM] no longer triggers the warning (which the test suite
escalates to an error, failing every authenticated endpoint). It is typed
as RequestKey[Any] to preserve the current access semantics: handlers store
different origins (App, Home Assistant, host, observer) and narrow the value
to the concrete type they expect.
aiohttp 3.14 also widened the request.post() return type to include
bytearray. Update the _process_dict annotation accordingly to satisfy mypy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Use encode_basic_auth for registry token requests
aiohttp 3.14 deprecates the BasicAuth constructor and the client auth=
request parameter (both removed in aiohttp 4.0), each emitting a
DeprecationWarning at runtime. The registry manifest fetcher hit both when
requesting a token with stored credentials. Switch to aiohttp.encode_basic_auth()
and pass the result via an Authorization header instead.
Add a test covering the credentials path, which the existing tests skipped by
mocking _get_auth_token. It asserts the Authorization header is sent and, since
the suite escalates warnings to errors, guards against the deprecations
returning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
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>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add Raspberry Pi firmware update API
Expose `io.hass.os.Boards.RaspberryPi.Firmware` via a D-Bus proxy and
`GET/POST /os/boards/raspberrypi/firmware[/update]` REST endpoints for
Raspberry Pi 4 / 5 / CM4 (Yellow). Gated on OS Agent >= 1.9.0. The
update job raises a `REBOOT_REQUIRED` resolution issue on success and
rejects up front when the agent reports `update_blocked`.
The `blocked_reason` field currently returns only
`unsupported_boot_device` regardless of the underlying cause (CM4
without self-update, USB/NVMe boot, etc.), more reasons may be added
later if we need to make distinction.
Refs home-assistant/operating-system#4631
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix pylint issue in tests
* Return blocked_reason=None instead of empty string when update is not blocked
* Fix typo in update_raspberrypi_firmware docstring
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Reject API call for update early if blocked
* Flip API availablility check in _check_rpi_firmware_available
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Remove extra newline in docstring
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Home Assistant Core now triggers a versionless Supervisor update during
onboarding to ensure Supervisor is current before it continues setup. It
treats a "no update available" response as the signal to proceed.
In DEV mode the update endpoint bypassed the need_update check entirely
and resolved a versionless request to the latest published version. So
Core's onboarding call made a freshly-built DEV Supervisor install the
latest published build and restart, which breaks the run_supervisor CI
job (the API disappears mid-test with "connection refused").
Tie the bypass to an explicit version instead: specifying a version is
still DEV-only, but a versionless request now always respects
need_update. Since need_update is always False in DEV, Core's onboarding
call becomes a no-op there, avoiding the update to the latest published
Supervisor on the dev channel.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Derive App state from container state
The App.state setter mixed two responsibilities: it both mutated a
private `_state` field and dispatched side effects (WebSocket events,
issue dismissal, startup_event signaling). On top of that, an installed
but never-started app stayed in AppState.UNKNOWN forever, because the
attach() image-only fallback never fires a container state-change event
and the AppState therefore kept its constructor default. Conceptually,
ContainerState.UNKNOWN ("container does not exist") and AppState.UNKNOWN
("nothing observed yet") happened to share a name but meant different
things, which made the distinction easy to lose.
Make App.state a pure derived property. The source of truth is the last
observed ContainerState (cached on the App), plus a sticky operation-
error flag for start/stop failures that the docker event stream cannot
reflect. When no container has been observed yet, the derivation falls
back to install signals: an attached instance (image present) is
STOPPED, otherwise UNKNOWN. As a side effect, an installed-but-never-
started app now correctly reports STOPPED instead of UNKNOWN.
container_state_changed updates the cached container state and routes
all side effects through a single _emit_state_change helper that diffs
old vs new derived state. The two start/stop failure paths route
through _set_operation_error. Uninstall resets the cached signals so
the derivation naturally returns UNKNOWN.
Tests use a new tests/common.force_app_state helper that pokes the
underlying signals directly; the production class no longer carries
test-only setters.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix App state drive to AppState.UNKNOWN
* Unify state mutation through _update_state
Previously, state-driving signal changes were spread across two helpers
(_set_operation_error, _emit_state_change(old_state)) and required each
caller to capture self.state before mutating a private field — leaking
implementation details to call sites and raising the "why am I emitting
the old state?" question pointed out in code review.
Replace both helpers with a single _update_state(*, container_state=,
operation_error=) entry point. Callers describe what changed via
keyword arguments (None leaves a signal untouched); the helper captures
the previous state, applies the updates, recomputes the derived state
and emits side effects if anything changed.
Diff against a tracked _last_state instead of a freshly derived
"current" state, so that an out-of-band mutation between updates does
not silently shift the comparison baseline. The concrete case is
App.uninstall: instance.remove() clears the docker meta mid-flow, which
would otherwise reshape the derivation (RUNNING with no healthcheck
becomes STARTED instead of STARTUP) and suppress the STARTUP transition
that resolves the start-wait task. As a side effect, the initial
UNKNOWN -> STOPPED transition on attach is also now reliably emitted.
Switch the uninstall path to ContainerState.UNKNOWN ("we know there is
no container") rather than the constructor sentinel None.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Cache app state instead of deriving on every read
Building on the previous commit, make App.state a plain read of a
cached _state field rather than re-deriving on every property access.
The derivation moves to _derive_state(), and _update_state() is the
sole place that recomputes and assigns _state, so the value consumers
read always matches what was last emitted to listeners.
This removes the _last_state bookkeeping introduced previously: with a
single cached value there is no longer a separate "derived now" vs
"last emitted" distinction to reconcile, and out-of-band mutations
(e.g. instance.remove() clearing _meta during uninstall) can no longer
silently shift what state returns between updates.
Call _update_state() at the end of load() so the cached state settles
once attach() has run. Image-only attaches do not fire a docker event,
so without this an installed app would stay in the constructor-default
UNKNOWN until first start; this also makes the initial transition on
attach observable to listeners.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Pass operation error to _derive_state instead of storing it
The two state-driving signals were not symmetric. _container_state is
genuinely persisted state ("the last thing docker told us") that
re-derivation legitimately reads across calls. _operation_error, on the
other hand, is a momentary "force ERROR for this transition" signal; the
persistence of an error condition already lives in the cached _state.
Storing it as an instance attribute implied a sticky cross-call behavior
that no call path actually exercised: every caller either set it
explicitly right before deriving (start/stop failures, container events)
or ran argless only at load time, where no failure has occurred.
Drop the _operation_error field and pass operation_error as a parameter
to _derive_state(), defaulting to False in _update_state(). A container
observation now supersedes a prior error implicitly via the default,
which lets the container-event and uninstall call sites drop their
explicit operation_error=False.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Settle load state synchronously from current_state
The argless _update_state() settle at the end of load() raced attach()'s
container-state event. attach() fires DOCKER_CONTAINER_STATE_CHANGE via
the bus, which schedules the container_state_changed listener as a task
rather than running it inline. In the deprecated-arch early-return path
there is no await between attach() and the settle, so the listener had
not run yet: _container_state was still None and the settle derived
STOPPED (instance attached) — emitting a transient UNKNOWN->STOPPED even
for a running container before the listener corrected it. The main path
only avoided this incidentally, by having awaits (check_image,
save_persist) in between for the listener to run.
Derive the load-time state synchronously from instance.current_state()
instead of relying on the asynchronously delivered event. current_state()
returns the real container state, or UNKNOWN when only an image is
present (which derives to STOPPED), so both paths settle correctly
without racing the event.
Add a regression test that loading a running container settles to
STARTED, and mock current_state() in the state-listener test which
relies on a clean UNKNOWN baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>