mirror of
https://github.com/home-assistant/core.git
synced 2026-02-03 22:05:35 +01:00
Add services using "apps" instead of "addons" to hassio integration (#161689)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
@@ -70,6 +70,8 @@ from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_ADDON,
|
||||
ATTR_ADDONS,
|
||||
ATTR_APP,
|
||||
ATTR_APPS,
|
||||
ATTR_COMPRESSED,
|
||||
ATTR_FOLDERS,
|
||||
ATTR_HOMEASSISTANT,
|
||||
@@ -135,6 +137,10 @@ SERVICE_ADDON_START = "addon_start"
|
||||
SERVICE_ADDON_STOP = "addon_stop"
|
||||
SERVICE_ADDON_RESTART = "addon_restart"
|
||||
SERVICE_ADDON_STDIN = "addon_stdin"
|
||||
SERVICE_APP_START = "app_start"
|
||||
SERVICE_APP_STOP = "app_stop"
|
||||
SERVICE_APP_RESTART = "app_restart"
|
||||
SERVICE_APP_STDIN = "app_stdin"
|
||||
SERVICE_HOST_SHUTDOWN = "host_shutdown"
|
||||
SERVICE_HOST_REBOOT = "host_reboot"
|
||||
SERVICE_BACKUP_FULL = "backup_full"
|
||||
@@ -156,7 +162,7 @@ def valid_addon(value: Any) -> str:
|
||||
hass = async_get_hass_or_none()
|
||||
|
||||
if hass and (addons := get_addons_info(hass)) is not None and value not in addons:
|
||||
raise vol.Invalid("Not a valid add-on slug")
|
||||
raise vol.Invalid("Not a valid app slug")
|
||||
return value
|
||||
|
||||
|
||||
@@ -168,6 +174,12 @@ SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend(
|
||||
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
|
||||
)
|
||||
|
||||
SCHEMA_APP = vol.Schema({vol.Required(ATTR_APP): valid_addon})
|
||||
|
||||
SCHEMA_APP_STDIN = SCHEMA_APP.extend(
|
||||
{vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)}
|
||||
)
|
||||
|
||||
SCHEMA_BACKUP_FULL = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
@@ -186,7 +198,13 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend(
|
||||
{
|
||||
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
||||
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]),
|
||||
vol.Exclusive(ATTR_APPS, "apps_or_addons"): vol.All(
|
||||
cv.ensure_list, [VALID_ADDON_SLUG]
|
||||
),
|
||||
# Legacy "addons", "apps" is preferred
|
||||
vol.Exclusive(ATTR_ADDONS, "apps_or_addons"): vol.All(
|
||||
cv.ensure_list, [VALID_ADDON_SLUG]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -201,7 +219,13 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
{
|
||||
vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
|
||||
vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]),
|
||||
vol.Exclusive(ATTR_APPS, "apps_or_addons"): vol.All(
|
||||
cv.ensure_list, [VALID_ADDON_SLUG]
|
||||
),
|
||||
# Legacy "addons", "apps" is preferred
|
||||
vol.Exclusive(ATTR_ADDONS, "apps_or_addons"): vol.All(
|
||||
cv.ensure_list, [VALID_ADDON_SLUG]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -221,12 +245,18 @@ class APIEndpointSettings(NamedTuple):
|
||||
|
||||
|
||||
MAP_SERVICE_API = {
|
||||
# Legacy addon services
|
||||
SERVICE_ADDON_START: APIEndpointSettings("/addons/{addon}/start", SCHEMA_ADDON),
|
||||
SERVICE_ADDON_STOP: APIEndpointSettings("/addons/{addon}/stop", SCHEMA_ADDON),
|
||||
SERVICE_ADDON_RESTART: APIEndpointSettings("/addons/{addon}/restart", SCHEMA_ADDON),
|
||||
SERVICE_ADDON_STDIN: APIEndpointSettings(
|
||||
"/addons/{addon}/stdin", SCHEMA_ADDON_STDIN
|
||||
),
|
||||
# New app services
|
||||
SERVICE_APP_START: APIEndpointSettings("/addons/{addon}/start", SCHEMA_APP),
|
||||
SERVICE_APP_STOP: APIEndpointSettings("/addons/{addon}/stop", SCHEMA_APP),
|
||||
SERVICE_APP_RESTART: APIEndpointSettings("/addons/{addon}/restart", SCHEMA_APP),
|
||||
SERVICE_APP_STDIN: APIEndpointSettings("/addons/{addon}/stdin", SCHEMA_APP_STDIN),
|
||||
SERVICE_HOST_SHUTDOWN: APIEndpointSettings("/host/shutdown", SCHEMA_NO_DATA),
|
||||
SERVICE_HOST_REBOOT: APIEndpointSettings("/host/reboot", SCHEMA_NO_DATA),
|
||||
SERVICE_BACKUP_FULL: APIEndpointSettings(
|
||||
@@ -386,12 +416,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
api_endpoint = MAP_SERVICE_API[service.service]
|
||||
|
||||
data = service.data.copy()
|
||||
addon = data.pop(ATTR_ADDON, None)
|
||||
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 == SERVICE_ADDON_STDIN:
|
||||
if service.service in (SERVICE_ADDON_STDIN, SERVICE_APP_STDIN):
|
||||
payload = data[ATTR_INPUT]
|
||||
elif api_endpoint.pass_data:
|
||||
payload = data
|
||||
|
||||
@@ -17,6 +17,8 @@ DOMAIN = "hassio"
|
||||
|
||||
ATTR_ADDON = "addon"
|
||||
ATTR_ADDONS = "addons"
|
||||
ATTR_APP = "app"
|
||||
ATTR_APPS = "apps"
|
||||
ATTR_ADMIN = "admin"
|
||||
ATTR_COMPRESSED = "compressed"
|
||||
ATTR_CONFIG = "config"
|
||||
|
||||
@@ -22,6 +22,18 @@
|
||||
"addon_stop": {
|
||||
"service": "mdi:stop"
|
||||
},
|
||||
"app_restart": {
|
||||
"service": "mdi:restart"
|
||||
},
|
||||
"app_start": {
|
||||
"service": "mdi:play"
|
||||
},
|
||||
"app_stdin": {
|
||||
"service": "mdi:console"
|
||||
},
|
||||
"app_stop": {
|
||||
"service": "mdi:stop"
|
||||
},
|
||||
"backup_full": {
|
||||
"service": "mdi:content-save"
|
||||
},
|
||||
|
||||
@@ -30,6 +30,42 @@ addon_stop:
|
||||
selector:
|
||||
addon:
|
||||
|
||||
app_start:
|
||||
fields:
|
||||
app:
|
||||
required: true
|
||||
example: core_ssh
|
||||
selector:
|
||||
app:
|
||||
|
||||
app_restart:
|
||||
fields:
|
||||
app:
|
||||
required: true
|
||||
example: core_ssh
|
||||
selector:
|
||||
app:
|
||||
|
||||
app_stdin:
|
||||
fields:
|
||||
app:
|
||||
required: true
|
||||
example: core_ssh
|
||||
selector:
|
||||
app:
|
||||
input:
|
||||
required: true
|
||||
selector:
|
||||
object:
|
||||
|
||||
app_stop:
|
||||
fields:
|
||||
app:
|
||||
required: true
|
||||
example: core_ssh
|
||||
selector:
|
||||
app:
|
||||
|
||||
host_reboot:
|
||||
host_shutdown:
|
||||
backup_full:
|
||||
@@ -64,6 +100,10 @@ backup_partial:
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
apps:
|
||||
example: ["core_ssh", "core_samba", "core_mosquitto"]
|
||||
selector:
|
||||
object:
|
||||
addons:
|
||||
example: ["core_ssh", "core_samba", "core_mosquitto"]
|
||||
selector:
|
||||
@@ -113,6 +153,10 @@ restore_partial:
|
||||
example: ["homeassistant", "share"]
|
||||
selector:
|
||||
object:
|
||||
apps:
|
||||
example: ["core_ssh", "core_samba", "core_mosquitto"]
|
||||
selector:
|
||||
object:
|
||||
addons:
|
||||
example: ["core_ssh", "core_samba", "core_mosquitto"]
|
||||
selector:
|
||||
|
||||
@@ -336,6 +336,50 @@
|
||||
},
|
||||
"name": "Stop add-on"
|
||||
},
|
||||
"app_restart": {
|
||||
"description": "Restarts a Home Assistant app.",
|
||||
"fields": {
|
||||
"app": {
|
||||
"description": "The app to restart.",
|
||||
"name": "[%key:component::hassio::services::app_start::fields::app::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Restart app"
|
||||
},
|
||||
"app_start": {
|
||||
"description": "Starts a Home Assistant app.",
|
||||
"fields": {
|
||||
"app": {
|
||||
"description": "The app to start.",
|
||||
"name": "App"
|
||||
}
|
||||
},
|
||||
"name": "Start app"
|
||||
},
|
||||
"app_stdin": {
|
||||
"description": "Writes data to the standard input of a Home Assistant app.",
|
||||
"fields": {
|
||||
"app": {
|
||||
"description": "The app to write to.",
|
||||
"name": "[%key:component::hassio::services::app_start::fields::app::name%]"
|
||||
},
|
||||
"input": {
|
||||
"description": "The data to write.",
|
||||
"name": "Input"
|
||||
}
|
||||
},
|
||||
"name": "Write data to app stdin"
|
||||
},
|
||||
"app_stop": {
|
||||
"description": "Stops a Home Assistant app.",
|
||||
"fields": {
|
||||
"app": {
|
||||
"description": "The app to stop.",
|
||||
"name": "[%key:component::hassio::services::app_start::fields::app::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Stop app"
|
||||
},
|
||||
"backup_full": {
|
||||
"description": "Creates a full backup.",
|
||||
"fields": {
|
||||
@@ -366,8 +410,12 @@
|
||||
"description": "Creates a partial backup.",
|
||||
"fields": {
|
||||
"addons": {
|
||||
"description": "List of add-ons to include in the backup. Use the name slug of each add-on.",
|
||||
"name": "Add-ons"
|
||||
"description": "List of apps (formerly add-ons) to include in the backup. Use the slug of each app. Legacy option - use apps instead.",
|
||||
"name": "Add-ons (legacy)"
|
||||
},
|
||||
"apps": {
|
||||
"description": "List of apps to include in the backup. Use the slug of each app.",
|
||||
"name": "Apps"
|
||||
},
|
||||
"compressed": {
|
||||
"description": "[%key:component::hassio::services::backup_full::fields::compressed::description%]",
|
||||
@@ -426,9 +474,13 @@
|
||||
"description": "Restores from a partial backup.",
|
||||
"fields": {
|
||||
"addons": {
|
||||
"description": "List of add-ons to restore from the backup. Use the name slug of each add-on.",
|
||||
"description": "List of apps (formerly add-ons) to restore from the backup. Use the slug of each app. Legacy option - use apps instead.",
|
||||
"name": "[%key:component::hassio::services::backup_partial::fields::addons::name%]"
|
||||
},
|
||||
"apps": {
|
||||
"description": "List of apps to restore from the backup. Use the slug of each app.",
|
||||
"name": "[%key:component::hassio::services::backup_partial::fields::apps::name%]"
|
||||
},
|
||||
"folders": {
|
||||
"description": "List of directories to restore from the backup.",
|
||||
"name": "[%key:component::hassio::services::backup_partial::fields::folders::name%]"
|
||||
|
||||
@@ -490,10 +490,17 @@ async def test_warn_when_cannot_connect(
|
||||
async def test_service_register(hass: HomeAssistant) -> None:
|
||||
"""Check if service will be setup."""
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
# New app services
|
||||
assert hass.services.has_service("hassio", "app_start")
|
||||
assert hass.services.has_service("hassio", "app_stop")
|
||||
assert hass.services.has_service("hassio", "app_restart")
|
||||
assert hass.services.has_service("hassio", "app_stdin")
|
||||
# Legacy addon services (deprecated)
|
||||
assert hass.services.has_service("hassio", "addon_start")
|
||||
assert hass.services.has_service("hassio", "addon_stop")
|
||||
assert hass.services.has_service("hassio", "addon_restart")
|
||||
assert hass.services.has_service("hassio", "addon_stdin")
|
||||
# Other services
|
||||
assert hass.services.has_service("hassio", "host_shutdown")
|
||||
assert hass.services.has_service("hassio", "host_reboot")
|
||||
assert hass.services.has_service("hassio", "host_reboot")
|
||||
@@ -503,14 +510,18 @@ async def test_service_register(hass: HomeAssistant) -> None:
|
||||
assert hass.services.has_service("hassio", "restore_partial")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"app_or_addon",
|
||||
["app", "addon"],
|
||||
)
|
||||
@pytest.mark.freeze_time("2021-11-13 11:48:00")
|
||||
async def test_service_calls(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
supervisor_client: AsyncMock,
|
||||
addon_installed: AsyncMock,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
app_or_addon: str,
|
||||
) -> None:
|
||||
"""Call service and check the API calls behind that."""
|
||||
supervisor_is_connected.side_effect = SupervisorError
|
||||
@@ -534,11 +545,17 @@ async def test_service_calls(
|
||||
"http://127.0.0.1/backups/test/restore/partial", json={"result": "ok"}
|
||||
)
|
||||
|
||||
await hass.services.async_call("hassio", "addon_start", {"addon": "test"})
|
||||
await hass.services.async_call("hassio", "addon_stop", {"addon": "test"})
|
||||
await hass.services.async_call("hassio", "addon_restart", {"addon": "test"})
|
||||
await hass.services.async_call(
|
||||
"hassio", "addon_stdin", {"addon": "test", "input": "test"}
|
||||
"hassio", f"{app_or_addon}_start", {app_or_addon: "test"}
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"hassio", f"{app_or_addon}_stop", {app_or_addon: "test"}
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"hassio", f"{app_or_addon}_restart", {app_or_addon: "test"}
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"hassio", f"{app_or_addon}_stdin", {app_or_addon: "test", "input": "test"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -557,7 +574,7 @@ async def test_service_calls(
|
||||
"backup_partial",
|
||||
{
|
||||
"homeassistant": True,
|
||||
"addons": ["test"],
|
||||
"apps": ["test"],
|
||||
"folders": ["ssl"],
|
||||
"password": "123456",
|
||||
},
|
||||
@@ -565,6 +582,7 @@ async def test_service_calls(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 28
|
||||
# API receives "addons" even when we pass "apps"
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"name": "2021-11-13 03:48:00",
|
||||
"homeassistant": True,
|
||||
@@ -582,7 +600,7 @@ async def test_service_calls(
|
||||
{
|
||||
"slug": "test",
|
||||
"homeassistant": False,
|
||||
"addons": ["test"],
|
||||
"apps": ["test"],
|
||||
"folders": ["ssl"],
|
||||
"password": "123456",
|
||||
},
|
||||
@@ -590,6 +608,7 @@ async def test_service_calls(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert aioclient_mock.call_count + len(supervisor_client.mock_calls) == 30
|
||||
# API receives "addons" even when we pass "apps"
|
||||
assert aioclient_mock.mock_calls[-1][2] == {
|
||||
"addons": ["test"],
|
||||
"folders": ["ssl"],
|
||||
@@ -650,10 +669,15 @@ async def test_service_calls(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"app_or_addon",
|
||||
["app", "addon"],
|
||||
)
|
||||
async def test_invalid_service_calls(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
app_or_addon: str,
|
||||
) -> None:
|
||||
"""Call service with invalid input and check that it raises."""
|
||||
supervisor_is_connected.side_effect = SupervisorError
|
||||
@@ -663,18 +687,57 @@ async def test_invalid_service_calls(
|
||||
|
||||
with pytest.raises(Invalid):
|
||||
await hass.services.async_call(
|
||||
"hassio", "addon_start", {"addon": "does_not_exist"}
|
||||
"hassio", f"{app_or_addon}_start", {app_or_addon: "does_not_exist"}
|
||||
)
|
||||
with pytest.raises(Invalid):
|
||||
await hass.services.async_call(
|
||||
"hassio", "addon_stdin", {"addon": "does_not_exist", "input": "test"}
|
||||
"hassio",
|
||||
f"{app_or_addon}_stdin",
|
||||
{app_or_addon: "does_not_exist", "input": "test"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data"),
|
||||
[
|
||||
(
|
||||
"backup_partial",
|
||||
{"apps": ["test"], "addons": ["test"]},
|
||||
),
|
||||
(
|
||||
"restore_partial",
|
||||
{"apps": ["test"], "addons": ["test"], "slug": "test"},
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("addon_installed")
|
||||
async def test_service_calls_apps_addons_exclusive(
|
||||
hass: HomeAssistant,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test that apps and addons parameters are mutually exclusive."""
|
||||
supervisor_is_connected.side_effect = SupervisorError
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(
|
||||
Invalid, match="two or more values in the same group of exclusion"
|
||||
):
|
||||
await hass.services.async_call("hassio", service, service_data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"app_or_addon",
|
||||
["app", "addon"],
|
||||
)
|
||||
async def test_addon_service_call_with_complex_slug(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
supervisor_is_connected: AsyncMock,
|
||||
app_or_addon: str,
|
||||
) -> None:
|
||||
"""Addon slugs can have ., - and _, confirm that passes validation."""
|
||||
supervisor_mock_data = {
|
||||
@@ -705,7 +768,9 @@ async def test_addon_service_call_with_complex_slug(
|
||||
assert await async_setup_component(hass, "hassio", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call("hassio", "addon_start", {"addon": "test.a_1-2"})
|
||||
await hass.services.async_call(
|
||||
"hassio", f"{app_or_addon}_start", {app_or_addon: "test.a_1-2"}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hassio_env")
|
||||
|
||||
Reference in New Issue
Block a user