mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 10:59:24 +00:00
Refresh HassOS coordinator when mount repair is received (#155969)
This commit is contained in:
@@ -128,6 +128,8 @@ ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
|
|||||||
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
|
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
|
||||||
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
|
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
|
||||||
|
|
||||||
|
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
|
||||||
|
|
||||||
CORE_CONTAINER = "homeassistant"
|
CORE_CONTAINER = "homeassistant"
|
||||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from homeassistant.helpers.issue_registry import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ADDONS_COORDINATOR,
|
||||||
ATTR_DATA,
|
ATTR_DATA,
|
||||||
ATTR_HEALTHY,
|
ATTR_HEALTHY,
|
||||||
ATTR_STARTUP,
|
ATTR_STARTUP,
|
||||||
@@ -49,6 +50,7 @@ from .const import (
|
|||||||
ISSUE_KEY_ADDON_PWNED,
|
ISSUE_KEY_ADDON_PWNED,
|
||||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||||
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
||||||
|
ISSUE_MOUNT_MOUNT_FAILED,
|
||||||
PLACEHOLDER_KEY_ADDON,
|
PLACEHOLDER_KEY_ADDON,
|
||||||
PLACEHOLDER_KEY_ADDON_URL,
|
PLACEHOLDER_KEY_ADDON_URL,
|
||||||
PLACEHOLDER_KEY_FREE_SPACE,
|
PLACEHOLDER_KEY_FREE_SPACE,
|
||||||
@@ -57,7 +59,7 @@ from .const import (
|
|||||||
STARTUP_COMPLETE,
|
STARTUP_COMPLETE,
|
||||||
UPDATE_KEY_SUPERVISOR,
|
UPDATE_KEY_SUPERVISOR,
|
||||||
)
|
)
|
||||||
from .coordinator import get_addons_info, get_host_info
|
from .coordinator import HassioDataUpdateCoordinator, get_addons_info, get_host_info
|
||||||
from .handler import HassIO, get_supervisor_client
|
from .handler import HassIO, get_supervisor_client
|
||||||
|
|
||||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||||
@@ -77,7 +79,7 @@ UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
|||||||
# Keys (type + context) of issues that when found should be made into a repair
|
# Keys (type + context) of issues that when found should be made into a repair
|
||||||
ISSUE_KEYS_FOR_REPAIRS = {
|
ISSUE_KEYS_FOR_REPAIRS = {
|
||||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||||
"issue_mount_mount_failed",
|
ISSUE_MOUNT_MOUNT_FAILED,
|
||||||
"issue_system_multiple_data_disks",
|
"issue_system_multiple_data_disks",
|
||||||
"issue_system_reboot_required",
|
"issue_system_reboot_required",
|
||||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||||
@@ -284,6 +286,9 @@ class SupervisorIssues:
|
|||||||
else:
|
else:
|
||||||
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2"
|
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2"
|
||||||
|
|
||||||
|
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
|
||||||
|
self._async_coordinator_refresh()
|
||||||
|
|
||||||
async_create_issue(
|
async_create_issue(
|
||||||
self._hass,
|
self._hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -336,6 +341,9 @@ class SupervisorIssues:
|
|||||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||||
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
|
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
|
||||||
|
|
||||||
|
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
|
||||||
|
self._async_coordinator_refresh()
|
||||||
|
|
||||||
del self._issues[issue.uuid]
|
del self._issues[issue.uuid]
|
||||||
|
|
||||||
def get_issue(self, issue_id: str) -> Issue | None:
|
def get_issue(self, issue_id: str) -> Issue | None:
|
||||||
@@ -406,3 +414,11 @@ class SupervisorIssues:
|
|||||||
|
|
||||||
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
|
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
|
||||||
self.remove_issue(Issue.from_dict(event[ATTR_DATA]))
|
self.remove_issue(Issue.from_dict(event[ATTR_DATA]))
|
||||||
|
|
||||||
|
def _async_coordinator_refresh(self) -> None:
|
||||||
|
"""Refresh coordinator to update latest data in entities."""
|
||||||
|
coordinator: HassioDataUpdateCoordinator | None
|
||||||
|
if coordinator := self._hass.data.get(ADDONS_COORDINATOR):
|
||||||
|
coordinator.config_entry.async_create_task(
|
||||||
|
self._hass, coordinator.async_refresh()
|
||||||
|
)
|
||||||
|
|||||||
@@ -3,9 +3,18 @@
|
|||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import os
|
import os
|
||||||
|
from pathlib import PurePath
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from aiohasupervisor.models.mounts import CIFSMountResponse, MountsInfo, MountState
|
from aiohasupervisor.models.mounts import (
|
||||||
|
CIFSMountResponse,
|
||||||
|
MountsInfo,
|
||||||
|
MountState,
|
||||||
|
MountType,
|
||||||
|
MountUsage,
|
||||||
|
NFSMountResponse,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.hassio import DOMAIN
|
from homeassistant.components.hassio import DOMAIN
|
||||||
@@ -18,6 +27,7 @@ from .common import MOCK_REPOSITORIES, MOCK_STORE_ADDONS
|
|||||||
|
|
||||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
|
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
|
||||||
|
|
||||||
@@ -230,16 +240,16 @@ async def test_mount_binary_sensor(
|
|||||||
assert hass.states.get(entity_id) is None
|
assert hass.states.get(entity_id) is None
|
||||||
|
|
||||||
# Add a mount.
|
# Add a mount.
|
||||||
mock_mounts = [
|
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
|
||||||
CIFSMountResponse(
|
CIFSMountResponse(
|
||||||
share="files",
|
share="files",
|
||||||
server="1.2.3.4",
|
server="1.2.3.4",
|
||||||
name="NAS",
|
name="NAS",
|
||||||
type="cifs",
|
type=MountType.CIFS,
|
||||||
usage="share",
|
usage=MountUsage.SHARE,
|
||||||
read_only=False,
|
read_only=False,
|
||||||
state=MountState.ACTIVE,
|
state=MountState.ACTIVE,
|
||||||
user_path="/share/nas",
|
user_path=PurePath("/share/nas"),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
supervisor_client.mounts.info = AsyncMock(
|
supervisor_client.mounts.info = AsyncMock(
|
||||||
@@ -282,3 +292,115 @@ async def test_mount_binary_sensor(
|
|||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1000))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1000))
|
||||||
await hass.async_block_till_done(wait_background_tasks=True)
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
assert hass.states.get(entity_id) is not None
|
assert hass.states.get(entity_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mount_refresh_after_issue(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
supervisor_client: AsyncMock,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test hassio mount state is refreshed after an issue was send by the supervisor."""
|
||||||
|
# Add a mount.
|
||||||
|
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
supervisor_client.mounts.info = AsyncMock(
|
||||||
|
return_value=MountsInfo(default_backup_mount=None, mounts=mock_mounts)
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch.dict(os.environ, MOCK_ENVIRON):
|
||||||
|
result = await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"hassio",
|
||||||
|
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
|
||||||
|
)
|
||||||
|
assert result
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Enable the entity.
|
||||||
|
entity_id = "binary_sensor.nas_connected"
|
||||||
|
entity_registry.async_update_entity(entity_id, disabled_by=None)
|
||||||
|
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Test new entity.
|
||||||
|
entity = hass.states.get(entity_id)
|
||||||
|
assert entity is not None
|
||||||
|
assert entity.state == "on"
|
||||||
|
|
||||||
|
# Change mount state to failed, issue a repair, and verify entity's state.
|
||||||
|
mock_mounts[0] = replace(mock_mounts[0], state=MountState.FAILED)
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
issue_uuid = uuid4().hex
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "supervisor/event",
|
||||||
|
"data": {
|
||||||
|
"event": "issue_changed",
|
||||||
|
"data": {
|
||||||
|
"uuid": issue_uuid,
|
||||||
|
"type": "mount_failed",
|
||||||
|
"context": "mount",
|
||||||
|
"reference": "nas",
|
||||||
|
"suggestions": [
|
||||||
|
{
|
||||||
|
"uuid": uuid4().hex,
|
||||||
|
"type": "execute_reload",
|
||||||
|
"context": "mount",
|
||||||
|
"reference": "nas",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": uuid4().hex,
|
||||||
|
"type": "execute_remove",
|
||||||
|
"context": "mount",
|
||||||
|
"reference": "nas",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
entity = hass.states.get(entity_id)
|
||||||
|
assert entity is not None
|
||||||
|
assert entity.state == "off"
|
||||||
|
|
||||||
|
# Change mount state to active, issue a repair, and verify entity's state.
|
||||||
|
mock_mounts[0] = replace(mock_mounts[0], state=MountState.ACTIVE)
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "supervisor/event",
|
||||||
|
"data": {
|
||||||
|
"event": "issue_removed",
|
||||||
|
"data": {
|
||||||
|
"uuid": issue_uuid,
|
||||||
|
"type": "mount_failed",
|
||||||
|
"context": "mount",
|
||||||
|
"reference": "nas",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
|
entity = hass.states.get(entity_id)
|
||||||
|
assert entity is not None
|
||||||
|
assert entity.state == "on"
|
||||||
|
|||||||
Reference in New Issue
Block a user