Skip to content

Use aiohasupervisor for all Supervisor service calls#166558

Merged
MartinHjelmare merged 5 commits into
devfrom
replace-service-calls-aiohasupervisor
Mar 31, 2026
Merged

Use aiohasupervisor for all Supervisor service calls#166558
MartinHjelmare merged 5 commits into
devfrom
replace-service-calls-aiohasupervisor

Conversation

@mdegat01
Copy link
Copy Markdown
Contributor

@mdegat01 mdegat01 commented Mar 26, 2026

Breaking change

Previously all actions registered by the supervisor integration (hassio.app_start, hassio.backup_partial, hassio.host_reboot, etc.) simply logged an error on failure. The script/automation proceeded regardless of whether that action succeeded. Now they properly raise on failure which means the automation/script will stop unless continue_on_error is set to True.

Proposed change

Final task in the long-running aiohasupervisor migration - the service calls. These were implemented in a dynamic way but they are static and known at design time so re-implemented them to use aiohasupervisor for all communication with Supervisor.

As the service calls had not been updated in a while, made two small changes. Let me know if one or both of these belong in a different PR and I can break them out:

  1. The partial and full backup APIs of Supervisor have always returned the ID of the new backup. As service calls now support responses I updated these services to return the backup ID if users want to use that in their automation.
  2. Supervisor's partial backup and partial restore APIs have always been set to reject calls as invalid if the list of addons or folders contained duplicates. This was not captured in the schema for these service calls. I updated them to raise invalid in this case. I'm not sure if this qualifies as a breaking change though? Since the call would've failed I figured no but it would've at least saved before and now it won't.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI review requested due to automatic review settings March 26, 2026 03:47
@home-assistant home-assistant Bot added cla-signed code-quality core has-tests integration: hassio Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage Top 50 Integration is ranked within the top 50 by usage by-code-owner Quality Scale: internal labels Mar 26, 2026
@home-assistant
Copy link
Copy Markdown
Contributor

Hey there @home-assistant/supervisor, mind taking a look at this pull request as it has been labeled with an integration (hassio) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of hassio can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign hassio Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

@mdegat01 mdegat01 changed the title Replace service calls aiohasupervisor Use aiohasupervisor for all Supervisor service calls Mar 26, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the aiohasupervisor migration for the hassio integration by moving Supervisor service calls to use SupervisorClient methods instead of dynamically constructed HTTP endpoints.

Changes:

  • Introduces a dedicated homeassistant/components/hassio/services.py to register and implement Supervisor services via aiohasupervisor.
  • Updates backup services to optionally return the newly created backup slug via service responses.
  • Tightens partial backup/restore validation to reject duplicate addons/apps and folders, and updates tests accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
homeassistant/components/hassio/services.py New module implementing Supervisor services using SupervisorClient, including optional service responses for backup creation.
homeassistant/components/hassio/__init__.py Removes the legacy dynamic service→endpoint mapping and delegates registration to the new services module.
tests/components/hassio/test_init.py Updates service-call tests to assert SupervisorClient method usage and adds tests for duplicate validation.
homeassistant/components/hassio/websocket_api.py Adjusts HassioAPIError import location.
homeassistant/components/hassio/addon_manager.py Simplifies error handling to only catch SupervisorError now that calls go through aiohasupervisor.

Comment thread homeassistant/components/hassio/services.py
Comment thread homeassistant/components/hassio/services.py
Comment thread homeassistant/components/hassio/services.py
Comment thread homeassistant/components/hassio/services.py Outdated
Comment thread tests/components/hassio/test_init.py Outdated
Comment on lines +206 to +227
async def async_app_stdin_service_handler(service: ServiceCall) -> None:
"""Handles app stdin service."""
app_slug = service.data[ATTR_APP]
data: dict | str = service.data[ATTR_INPUT]

if isinstance(data, dict):
data = json.dumps(data)
payload = data.encode(encoding="utf-8")

try:
await supervisor_client.addons.write_addon_stdin(app_slug, payload)
except SupervisorError as err:
raise HomeAssistantError(
f"Failed to write stdin to app {app_slug}: {err}"
) from err

hass.services.async_register(
DOMAIN,
SERVICE_APP_STDIN,
async_app_stdin_service_handler,
schema=SCHEMA_APP_STDIN,
)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this one is a bit different from before. Here's the old code to compare:

SCHEMA_APP_STDIN = SCHEMA_APP.extend(
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
)

async def async_service_handler(service: ServiceCall) -> None:
"""Handle service calls for Hass.io."""
api_endpoint = MAP_SERVICE_API[service.service]
data = service.data.copy()
addon = data.pop(ATTR_APP, None) or data.pop(ATTR_ADDON, None)
slug = data.pop(ATTR_SLUG, None)
if addons := data.pop(ATTR_APPS, None) or data.pop(ATTR_ADDONS, None):
data[ATTR_ADDONS] = addons
payload = None
# Pass data to Hass.io API
if service.service in (SERVICE_ADDON_STDIN, SERVICE_APP_STDIN):
payload = data[ATTR_INPUT]
elif api_endpoint.pass_data:
payload = data
# Call API
# The exceptions are logged properly in hassio.send_command
with suppress(HassioAPIError):
await hassio.send_command(
api_endpoint.command.format(addon=addon, slug=slug),
payload=payload,
timeout=api_endpoint.timeout,
)

async def send_command(
self,
command: str,
method: str = "post",
payload: Any | None = None,
timeout: int | None = 10,
return_text: bool = False,
*,
params: dict[str, Any] | None = None,
source: str = "core.handler",
) -> Any:
"""Send API command to Hass.io.
This method is a coroutine.
"""
joined_url = self._base_url.with_path(command)
# This check is to make sure the normalized URL string
# is the same as the URL string that was passed in. If
# they are different, then the passed in command URL
# contained characters that were removed by the normalization
# such as ../../../../etc/passwd
if joined_url.raw_path != command:
_LOGGER.error("Invalid request %s", command)
raise HassioAPIError
try:
response = await self.websession.request(
method,
joined_url,
params=params,
json=payload,
headers={
aiohttp.hdrs.AUTHORIZATION: (
f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}"
),
X_HASS_SOURCE: source,
},
timeout=aiohttp.ClientTimeout(total=timeout),
)

It appears before we allowed input to be a dictionary or a string. And then we took that dictionary or string and handed it off to aiohttp as json to make a request?

The problem is Supervisor is looking for binary input for this API, it's going to simply read the request as bytes and pipe it directly to the container. So I'm not entirely sure how this was working but it seems like a mess of type and encoding issues.

I adjusted it to encode whatever we're given as input so I can then hand bytes to the library like it (and Supervisor) expects. I'm worried this will create a change in behavior though because I really don't know how Supervisor handles it when an API that is expecting an application/octet-stream is called with an application/json request body. It must've worked a fair amount of the time but there's probably a lot of edge cases that aren't accounted for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we were passing string values provided as input as a json payload to aiohttp I bet it was coming through in quotes to supervisor 🤔

I'll have to test that. I might have to add quoted around it before encoding it for backwards compatibility 🙈

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I made a dumb addon that just accepts the stdin service input, reads it and logs it. I sent it a few tests. Here's the entirety of the addon's runtime logic:

#!/usr/bin/with-contenv bashio
set -e

# Read from STDIN aliases to send shutdown
while read -r input; do
    bashio::log.info "Input: $input"
done
  1. Example input 1:
action: hassio.addon_stdin
data:
  addon: local_example_input
  input: "test"

Result:

[11:33:55] INFO: Input: "test"
  1. Example input 2:
action: hassio.addon_stdin
data:
  addon: local_example_input
  input:
    a: a
    b: b

Result:

[11:34:16] INFO: Input: {"a":"a","b":"b"}
  1. Example input 3:
action: hassio.addon_stdin
data:
  addon: local_example_input
  input: "🥹"

Result:

[11:44:46] INFO: Input: "🥹"

So this poses a bit of an issue. I do have to wrap the strings provided in quotes for backwards compatibility as expected. But the encoding bit is even more of a challenge. I can't encode to bytes without knowing the encoding and it appears non-utf-8 worked fine.

I'm not entirely sure what to do with this service given this. Maybe I add new raw_input and encoding config options and deprecate input? And then have any usage of input continuing working exactly as it does today going through HassIO.send_command and the new things go through the library until the deprecation period ends. Unless we can come up with a way to make this behave sensibly and be backwards compatible?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A solution that I thought of was to do the JSON encoding ourselves first and then encode that JSON string to bytes. Would that work?

Otherwise it also sounds ok to change the service action parameters as suggested and a deprecation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you already did my suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MartinHjelmare yea I guess this works. For some reason I thought we'd have an encoding problem but I guess we don't really. We just add a note that its utf-8 encoded, which was already true since its the default and we didn't specify.

It does feel like they should have a way to provide text as input that gets used as is without being wrapped in quotes though. But perhaps that's just a separate enhancement.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it's fine to keep it working as it did, for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, preserving how things worked today make sense. We probably should add input_raw or something down the line.

FWIW, I checked how the rpc_shutdown app handles the quoted input: The app uses bashio::jq on the stdin. bashio::jq uses --raw-output, which strips the quotes, converting the json string to an unquoted string. If we were to send a unquoted string, rpc_shutdown would stop working since jq expects valid json (and unquoted string is not valid json).

It probably make sense to document the behvior in the hassio.app_stdin action docs.

Comment thread homeassistant/components/hassio/services.py
@mdegat01 mdegat01 requested a review from agners March 26, 2026 21:09
return value


SCHEMA_NO_DATA = vol.Schema({})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming all these schemas are just moved and not changed otherwise?

Copy link
Copy Markdown
Contributor Author

@mdegat01 mdegat01 Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MartinHjelmare mostly. As mentioned at the top there's changes to the partial backup and restore schemas, I changed the schema for the addons, apps and folders fields to require each item in the list be unique. Because the client library wants a set not a list. And also because supervisor requires those have unique values or it rejects the request with a 400 response (not new behavior, this is how it has always worked).

That was why I asked about that at the top in the PR description. If a user had a service call like these in their automations:

- action: hassio.partial_backup
  data:
    folders:
    - ssl
    - share
    - ssl
- action: hassio.partial_restore
  data:
    slug: my_backup
    apps:
    - core_ssh
    - core_samba
    - core_ssh

Then they would've failed at runtime. Supervisor would've responded with a 400 and unless they had continue_on_error: true the automation/script would've ended. But it would've saved and been considered valid config. With my change to the schema this will be marked as invalid config.

That's why I asked at the top if this is a considered a breaking change or not. And if so, if it should be separated into a different PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for reminding me. I had read that earlier. I think those changes to the schemas are fine since it wouldn't have worked anyway. We're just failing earlier which is good.

@MartinHjelmare MartinHjelmare marked this pull request as draft March 27, 2026 11:29
@MartinHjelmare
Copy link
Copy Markdown
Member

Besides the topic of discussion above, the code looks ok. 👍

Copilot AI review requested due to automatic review settings March 27, 2026 21:06
@mdegat01 mdegat01 force-pushed the replace-service-calls-aiohasupervisor branch from 4821e85 to db56157 Compare March 27, 2026 21:06
@mdegat01 mdegat01 marked this pull request as ready for review March 27, 2026 21:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread tests/components/hassio/test_init.py
Comment thread homeassistant/components/hassio/__init__.py
Copy link
Copy Markdown
Member

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Supervisor's partial backup and partial restore APIs have always been set to reject calls as invalid if the list of addons or folders contained duplicates. This was not captured in the schema for these service calls. I updated them to raise invalid in this case. I'm not sure if this qualifies as a breaking change though? Since the call would've failed I figured no but it would've at least saved before and now it won't.

So I've tested that, before you really didn't get any feedback at all it seems, not even in logs. it just didn't work. With this change, we get a proper error, but only on execution (it seems that validation only happens at action execution time, not safe!)

2026-03-30 15:25:31.692 INFO (MainThread) [homeassistant.components.automation.neue_automation] Neue Automation: Running automation actions
2026-03-30 15:25:31.692 INFO (MainThread) [homeassistant.components.automation.neue_automation] Neue Automation: Executing step call service
2026-03-30 15:25:31.693 ERROR (MainThread) [homeassistant.components.automation.neue_automation] Neue Automation: Error executing script. Invalid data for call_service at pos 1: two or more values in the same group of exclusion 'apps_or_addons' @ data[<apps_or_addons>]
2026-03-30 15:25:31.693 ERROR (MainThread) [homeassistant.components.automation.neue_automation] Error while executing automation automation.neue_automation: two or more values in the same group of exclusion 'apps_or_addons' @ data[<apps_or_addons>]

So the new behavior is much better. It also isn't a problem if existing automations have that error, they will still safe, but now have better logs.

Comment thread homeassistant/components/hassio/services.py Outdated
Copilot AI review requested due to automatic review settings March 30, 2026 19:21
@mdegat01 mdegat01 force-pushed the replace-service-calls-aiohasupervisor branch from 9732324 to f74256d Compare March 30, 2026 19:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@mdegat01 mdegat01 requested a review from agners March 30, 2026 19:32
@MartinHjelmare MartinHjelmare merged commit e164e65 into dev Mar 31, 2026
51 checks passed
@MartinHjelmare MartinHjelmare deleted the replace-service-calls-aiohasupervisor branch March 31, 2026 05:35
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

breaking-change by-code-owner cla-signed code-quality core has-tests integration: hassio Quality Scale: internal Top 50 Integration is ranked within the top 50 by usage Top 100 Integration is ranked within the top 100 by usage Top 200 Integration is ranked within the top 200 by usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants