diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 0df1d03858e..9bdc1096a75 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -128,6 +128,8 @@ ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned" ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space" ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon" +ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed" + CORE_CONTAINER = "homeassistant" SUPERVISOR_CONTAINER = "hassio_supervisor" diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 603a5fe8fcd..25b4db9c861 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -27,6 +27,7 @@ from homeassistant.helpers.issue_registry import ( ) from .const import ( + ADDONS_COORDINATOR, ATTR_DATA, ATTR_HEALTHY, ATTR_STARTUP, @@ -49,6 +50,7 @@ from .const import ( ISSUE_KEY_ADDON_PWNED, ISSUE_KEY_SYSTEM_DOCKER_CONFIG, ISSUE_KEY_SYSTEM_FREE_SPACE, + ISSUE_MOUNT_MOUNT_FAILED, PLACEHOLDER_KEY_ADDON, PLACEHOLDER_KEY_ADDON_URL, PLACEHOLDER_KEY_FREE_SPACE, @@ -57,7 +59,7 @@ from .const import ( STARTUP_COMPLETE, 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 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 ISSUE_KEYS_FOR_REPAIRS = { ISSUE_KEY_ADDON_BOOT_FAIL, - "issue_mount_mount_failed", + ISSUE_MOUNT_MOUNT_FAILED, "issue_system_multiple_data_disks", "issue_system_reboot_required", ISSUE_KEY_SYSTEM_DOCKER_CONFIG, @@ -284,6 +286,9 @@ class SupervisorIssues: else: placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2" + if issue.key == ISSUE_MOUNT_MOUNT_FAILED: + self._async_coordinator_refresh() + async_create_issue( self._hass, DOMAIN, @@ -336,6 +341,9 @@ class SupervisorIssues: if issue.key in ISSUE_KEYS_FOR_REPAIRS: 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] def get_issue(self, issue_id: str) -> Issue | None: @@ -406,3 +414,11 @@ class SupervisorIssues: elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED: 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() + ) diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index de733f490e7..dc7a4d961fc 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -3,9 +3,18 @@ from dataclasses import replace from datetime import timedelta import os +from pathlib import PurePath 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 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.test_util.aiohttp import AiohttpClientMocker +from tests.typing import WebSocketGenerator 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 # Add a mount. - mock_mounts = [ + mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [ CIFSMountResponse( share="files", server="1.2.3.4", name="NAS", - type="cifs", - usage="share", + type=MountType.CIFS, + usage=MountUsage.SHARE, read_only=False, state=MountState.ACTIVE, - user_path="/share/nas", + user_path=PurePath("/share/nas"), ) ] 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)) await hass.async_block_till_done(wait_background_tasks=True) 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"