Skip to content

Commit ddb74c5

Browse files
authored
Refresh HassOS coordinator when mount repair is received (home-assistant#155969)
1 parent 9aec7b1 commit ddb74c5

File tree

3 files changed

+147
-7
lines changed

3 files changed

+147
-7
lines changed

homeassistant/components/hassio/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@
128128
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
129129
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
130130

131+
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
132+
131133
CORE_CONTAINER = "homeassistant"
132134
SUPERVISOR_CONTAINER = "hassio_supervisor"
133135

homeassistant/components/hassio/issues.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
)
2828

2929
from .const import (
30+
ADDONS_COORDINATOR,
3031
ATTR_DATA,
3132
ATTR_HEALTHY,
3233
ATTR_STARTUP,
@@ -49,6 +50,7 @@
4950
ISSUE_KEY_ADDON_PWNED,
5051
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
5152
ISSUE_KEY_SYSTEM_FREE_SPACE,
53+
ISSUE_MOUNT_MOUNT_FAILED,
5254
PLACEHOLDER_KEY_ADDON,
5355
PLACEHOLDER_KEY_ADDON_URL,
5456
PLACEHOLDER_KEY_FREE_SPACE,
@@ -57,7 +59,7 @@
5759
STARTUP_COMPLETE,
5860
UPDATE_KEY_SUPERVISOR,
5961
)
60-
from .coordinator import get_addons_info, get_host_info
62+
from .coordinator import HassioDataUpdateCoordinator, get_addons_info, get_host_info
6163
from .handler import HassIO, get_supervisor_client
6264

6365
ISSUE_KEY_UNHEALTHY = "unhealthy"
@@ -77,7 +79,7 @@
7779
# Keys (type + context) of issues that when found should be made into a repair
7880
ISSUE_KEYS_FOR_REPAIRS = {
7981
ISSUE_KEY_ADDON_BOOT_FAIL,
80-
"issue_mount_mount_failed",
82+
ISSUE_MOUNT_MOUNT_FAILED,
8183
"issue_system_multiple_data_disks",
8284
"issue_system_reboot_required",
8385
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
@@ -284,6 +286,9 @@ def add_issue(self, issue: Issue) -> None:
284286
else:
285287
placeholders[PLACEHOLDER_KEY_FREE_SPACE] = "<2"
286288

289+
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
290+
self._async_coordinator_refresh()
291+
287292
async_create_issue(
288293
self._hass,
289294
DOMAIN,
@@ -336,6 +341,9 @@ def remove_issue(self, issue: Issue) -> None:
336341
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
337342
async_delete_issue(self._hass, DOMAIN, issue.uuid.hex)
338343

344+
if issue.key == ISSUE_MOUNT_MOUNT_FAILED:
345+
self._async_coordinator_refresh()
346+
339347
del self._issues[issue.uuid]
340348

341349
def get_issue(self, issue_id: str) -> Issue | None:
@@ -406,3 +414,11 @@ def _supervisor_events_to_issues(self, event: dict[str, Any]) -> None:
406414

407415
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
408416
self.remove_issue(Issue.from_dict(event[ATTR_DATA]))
417+
418+
def _async_coordinator_refresh(self) -> None:
419+
"""Refresh coordinator to update latest data in entities."""
420+
coordinator: HassioDataUpdateCoordinator | None
421+
if coordinator := self._hass.data.get(ADDONS_COORDINATOR):
422+
coordinator.config_entry.async_create_task(
423+
self._hass, coordinator.async_refresh()
424+
)

tests/components/hassio/test_binary_sensor.py

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33
from dataclasses import replace
44
from datetime import timedelta
55
import os
6+
from pathlib import PurePath
67
from unittest.mock import AsyncMock, patch
8+
from uuid import uuid4
79

8-
from aiohasupervisor.models.mounts import CIFSMountResponse, MountsInfo, MountState
10+
from aiohasupervisor.models.mounts import (
11+
CIFSMountResponse,
12+
MountsInfo,
13+
MountState,
14+
MountType,
15+
MountUsage,
16+
NFSMountResponse,
17+
)
918
import pytest
1019

1120
from homeassistant.components.hassio import DOMAIN
@@ -18,6 +27,7 @@
1827

1928
from tests.common import MockConfigEntry, async_fire_time_changed
2029
from tests.test_util.aiohttp import AiohttpClientMocker
30+
from tests.typing import WebSocketGenerator
2131

2232
MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"}
2333

@@ -230,16 +240,16 @@ async def test_mount_binary_sensor(
230240
assert hass.states.get(entity_id) is None
231241

232242
# Add a mount.
233-
mock_mounts = [
243+
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
234244
CIFSMountResponse(
235245
share="files",
236246
server="1.2.3.4",
237247
name="NAS",
238-
type="cifs",
239-
usage="share",
248+
type=MountType.CIFS,
249+
usage=MountUsage.SHARE,
240250
read_only=False,
241251
state=MountState.ACTIVE,
242-
user_path="/share/nas",
252+
user_path=PurePath("/share/nas"),
243253
)
244254
]
245255
supervisor_client.mounts.info = AsyncMock(
@@ -282,3 +292,115 @@ async def test_mount_binary_sensor(
282292
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1000))
283293
await hass.async_block_till_done(wait_background_tasks=True)
284294
assert hass.states.get(entity_id) is not None
295+
296+
297+
async def test_mount_refresh_after_issue(
298+
hass: HomeAssistant,
299+
entity_registry: er.EntityRegistry,
300+
supervisor_client: AsyncMock,
301+
hass_ws_client: WebSocketGenerator,
302+
) -> None:
303+
"""Test hassio mount state is refreshed after an issue was send by the supervisor."""
304+
# Add a mount.
305+
mock_mounts: list[CIFSMountResponse | NFSMountResponse] = [
306+
CIFSMountResponse(
307+
share="files",
308+
server="1.2.3.4",
309+
name="NAS",
310+
type=MountType.CIFS,
311+
usage=MountUsage.SHARE,
312+
read_only=False,
313+
state=MountState.ACTIVE,
314+
user_path=PurePath("/share/nas"),
315+
)
316+
]
317+
supervisor_client.mounts.info = AsyncMock(
318+
return_value=MountsInfo(default_backup_mount=None, mounts=mock_mounts)
319+
)
320+
321+
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
322+
config_entry.add_to_hass(hass)
323+
324+
with patch.dict(os.environ, MOCK_ENVIRON):
325+
result = await async_setup_component(
326+
hass,
327+
"hassio",
328+
{"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}},
329+
)
330+
assert result
331+
await hass.async_block_till_done()
332+
333+
# Enable the entity.
334+
entity_id = "binary_sensor.nas_connected"
335+
entity_registry.async_update_entity(entity_id, disabled_by=None)
336+
await hass.config_entries.async_reload(config_entry.entry_id)
337+
await hass.async_block_till_done()
338+
339+
# Test new entity.
340+
entity = hass.states.get(entity_id)
341+
assert entity is not None
342+
assert entity.state == "on"
343+
344+
# Change mount state to failed, issue a repair, and verify entity's state.
345+
mock_mounts[0] = replace(mock_mounts[0], state=MountState.FAILED)
346+
client = await hass_ws_client(hass)
347+
issue_uuid = uuid4().hex
348+
await client.send_json(
349+
{
350+
"id": 1,
351+
"type": "supervisor/event",
352+
"data": {
353+
"event": "issue_changed",
354+
"data": {
355+
"uuid": issue_uuid,
356+
"type": "mount_failed",
357+
"context": "mount",
358+
"reference": "nas",
359+
"suggestions": [
360+
{
361+
"uuid": uuid4().hex,
362+
"type": "execute_reload",
363+
"context": "mount",
364+
"reference": "nas",
365+
},
366+
{
367+
"uuid": uuid4().hex,
368+
"type": "execute_remove",
369+
"context": "mount",
370+
"reference": "nas",
371+
},
372+
],
373+
},
374+
},
375+
}
376+
)
377+
msg = await client.receive_json()
378+
assert msg["success"]
379+
await hass.async_block_till_done(wait_background_tasks=True)
380+
entity = hass.states.get(entity_id)
381+
assert entity is not None
382+
assert entity.state == "off"
383+
384+
# Change mount state to active, issue a repair, and verify entity's state.
385+
mock_mounts[0] = replace(mock_mounts[0], state=MountState.ACTIVE)
386+
await client.send_json(
387+
{
388+
"id": 2,
389+
"type": "supervisor/event",
390+
"data": {
391+
"event": "issue_removed",
392+
"data": {
393+
"uuid": issue_uuid,
394+
"type": "mount_failed",
395+
"context": "mount",
396+
"reference": "nas",
397+
},
398+
},
399+
}
400+
)
401+
msg = await client.receive_json()
402+
assert msg["success"]
403+
await hass.async_block_till_done(wait_background_tasks=True)
404+
entity = hass.states.get(entity_id)
405+
assert entity is not None
406+
assert entity.state == "on"

0 commit comments

Comments
 (0)