mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Add HassOS "mount_reload" action (#155996)
Co-authored-by: Shay Levy <levyshay1@gmail.com> Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
@@ -21,6 +21,7 @@ from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_NAME,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
HASSIO_USER_NAME,
|
||||
@@ -34,11 +35,13 @@ from homeassistant.core import (
|
||||
async_get_hass_or_none,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery_flow,
|
||||
issue_registry as ir,
|
||||
selector,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
@@ -92,6 +95,7 @@ from .const import (
|
||||
DATA_SUPERVISOR_INFO,
|
||||
DOMAIN,
|
||||
HASSIO_UPDATE_INTERVAL,
|
||||
SupervisorEntityModel,
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioDataUpdateCoordinator,
|
||||
@@ -147,6 +151,7 @@ SERVICE_BACKUP_FULL = "backup_full"
|
||||
SERVICE_BACKUP_PARTIAL = "backup_partial"
|
||||
SERVICE_RESTORE_FULL = "restore_full"
|
||||
SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||
SERVICE_MOUNT_RELOAD = "mount_reload"
|
||||
|
||||
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||
|
||||
@@ -229,6 +234,19 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
}
|
||||
)
|
||||
|
||||
SCHEMA_MOUNT_RELOAD = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): selector.DeviceSelector(
|
||||
selector.DeviceSelectorConfig(
|
||||
filter=selector.DeviceFilterSelectorConfig(
|
||||
integration=DOMAIN,
|
||||
model=SupervisorEntityModel.MOUNT,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
@@ -444,6 +462,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
DOMAIN, service, async_service_handler, schema=settings.schema
|
||||
)
|
||||
|
||||
dev_reg = dr.async_get(hass)
|
||||
|
||||
async def async_mount_reload(service: ServiceCall) -> None:
|
||||
"""Handle service calls for Hass.io."""
|
||||
coordinator: HassioDataUpdateCoordinator | None = None
|
||||
|
||||
if (device := dev_reg.async_get(service.data[ATTR_DEVICE_ID])) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mount_reload_unknown_device_id",
|
||||
)
|
||||
|
||||
if (
|
||||
device.name is None
|
||||
or device.model != SupervisorEntityModel.MOUNT
|
||||
or (coordinator := hass.data.get(ADDONS_COORDINATOR)) is None
|
||||
or coordinator.entry_id not in device.config_entries
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mount_reload_invalid_device",
|
||||
)
|
||||
|
||||
try:
|
||||
await supervisor_client.mounts.reload_mount(device.name)
|
||||
except SupervisorError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="mount_reload_error",
|
||||
translation_placeholders={"name": device.name, "error": str(error)},
|
||||
) from error
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_MOUNT_RELOAD, async_mount_reload, SCHEMA_MOUNT_RELOAD
|
||||
)
|
||||
|
||||
async def update_info_data(_: datetime | None = None) -> None:
|
||||
"""Update last available supervisor information."""
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"host_shutdown": {
|
||||
"service": "mdi:power"
|
||||
},
|
||||
"mount_reload": {
|
||||
"service": "mdi:reload"
|
||||
},
|
||||
"restore_full": {
|
||||
"service": "mdi:backup-restore"
|
||||
},
|
||||
|
||||
@@ -165,3 +165,13 @@ restore_partial:
|
||||
example: "password"
|
||||
selector:
|
||||
text:
|
||||
|
||||
mount_reload:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
filter:
|
||||
integration: hassio
|
||||
model: Home Assistant Mount
|
||||
|
||||
@@ -43,6 +43,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"mount_reload_error": {
|
||||
"message": "Failed to reload mount {name}: {error}"
|
||||
},
|
||||
"mount_reload_invalid_device": {
|
||||
"message": "Device is not a supervisor mount point"
|
||||
},
|
||||
"mount_reload_unknown_device_id": {
|
||||
"message": "Device ID not found"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_addon_boot_fail": {
|
||||
"fix_flow": {
|
||||
@@ -456,6 +467,16 @@
|
||||
"description": "Powers off the host system.",
|
||||
"name": "Power off the host system"
|
||||
},
|
||||
"mount_reload": {
|
||||
"description": "Reloads a network storage mount.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "The device ID of the network storage mount to reload.",
|
||||
"name": "Device ID"
|
||||
}
|
||||
},
|
||||
"name": "Reload network storage mount"
|
||||
},
|
||||
"restore_full": {
|
||||
"description": "Restores from full backup.",
|
||||
"fields": {
|
||||
|
||||
@@ -2,17 +2,25 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from pathlib import PurePath
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import AddonsStats
|
||||
from aiohasupervisor.models.mounts import (
|
||||
CIFSMountResponse,
|
||||
MountsInfo,
|
||||
MountState,
|
||||
MountType,
|
||||
MountUsage,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components import frontend, hassio
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.hassio import (
|
||||
ADDONS_COORDINATOR,
|
||||
@@ -31,10 +39,12 @@ from homeassistant.components.homeassistant import (
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.yaml import load_yaml_dict
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
@@ -514,6 +524,7 @@ async def test_service_register(hass: HomeAssistant) -> None:
|
||||
assert hass.services.has_service("hassio", "backup_partial")
|
||||
assert hass.services.has_service("hassio", "restore_full")
|
||||
assert hass.services.has_service("hassio", "restore_partial")
|
||||
assert hass.services.has_service("hassio", "mount_reload")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1484,3 +1495,150 @@ async def test_deprecated_installation_issue_supported_board(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def mount_reload_test_setup(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> dr.DeviceEntry:
|
||||
"""Set up mount reload test and return the device entry."""
|
||||
supervisor_client.mounts.info = AsyncMock(
|
||||
return_value=MountsInfo(
|
||||
default_backup_mount=None,
|
||||
mounts=[
|
||||
CIFSMountResponse(
|
||||
share="files",
|
||||
server="1.2.3.4",
|
||||
name="NAS",
|
||||
type=MountType.CIFS,
|
||||
usage=MountUsage.SHARE,
|
||||
read_only=False,
|
||||
state=MountState.ACTIVE,
|
||||
user_path=PurePath("/share/nas"),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, "mount_NAS")})
|
||||
assert device is not None
|
||||
return device
|
||||
|
||||
|
||||
async def test_mount_reload_action(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount service call."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": device.id}, blocking=True
|
||||
)
|
||||
supervisor_client.mounts.reload_mount.assert_awaited_once_with("NAS")
|
||||
|
||||
|
||||
async def test_mount_reload_action_failure(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount service call failure."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
supervisor_client.mounts.reload_mount = AsyncMock(
|
||||
side_effect=SupervisorError("test failure")
|
||||
)
|
||||
with pytest.raises(HomeAssistantError) as exc:
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": device.id}, blocking=True
|
||||
)
|
||||
assert str(exc.value) == "Failed to reload mount NAS: test failure"
|
||||
|
||||
|
||||
async def test_mount_reload_unknown_device_id(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount with unknown device ID."""
|
||||
await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": "1234"}, blocking=True
|
||||
)
|
||||
assert str(exc.value) == "Device ID not found"
|
||||
|
||||
|
||||
async def test_mount_reload_no_name(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount with an unnamed device."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
device_registry.async_update_device(device.id, name=None)
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": device.id}, blocking=True
|
||||
)
|
||||
assert str(exc.value) == "Device is not a supervisor mount point"
|
||||
|
||||
|
||||
async def test_mount_reload_invalid_model(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount with an invalid model."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
device_registry.async_update_device(device.id, model=None)
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": device.id}, blocking=True
|
||||
)
|
||||
assert str(exc.value) == "Device is not a supervisor mount point"
|
||||
|
||||
|
||||
async def test_mount_reload_not_supervisor_device(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reload_mount with a device not belonging to the supervisor."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
config_entry = MockConfigEntry()
|
||||
config_entry.add_to_hass(hass)
|
||||
device2 = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
identifiers={("test", "test")},
|
||||
name=device.name,
|
||||
model=device.model,
|
||||
)
|
||||
with pytest.raises(ServiceValidationError) as exc:
|
||||
await hass.services.async_call(
|
||||
"hassio", "mount_reload", {"device_id": device2.id}, blocking=True
|
||||
)
|
||||
assert str(exc.value) == "Device is not a supervisor mount point"
|
||||
|
||||
|
||||
async def test_mount_reload_selector_matches_device_name(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
supervisor_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the model name in the selector of mount reload is valid."""
|
||||
device = await mount_reload_test_setup(hass, device_registry, supervisor_client)
|
||||
services = load_yaml_dict(f"{hassio.__path__[0]}/services.yaml")
|
||||
assert (
|
||||
services["mount_reload"]["fields"]["device_id"]["selector"]["device"]["filter"][
|
||||
"model"
|
||||
]
|
||||
== device.model
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user