Skip to content

Commit 806bd9f

Browse files
mdegat01Copilot
andauthored
Apply store reload suggestion automatically on connectivity change (#6004)
* Apply store reload suggestion automatically on connectivity change * Use sys_bus not coresys.bus Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 953f7d0 commit 806bd9f

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed

supervisor/resolution/fixups/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from abc import ABC, abstractmethod
44
import logging
55

6+
from ...const import BusEvent
67
from ...coresys import CoreSys, CoreSysAttributes
78
from ...exceptions import ResolutionFixupError
89
from ..const import ContextType, IssueType, SuggestionType
@@ -66,6 +67,11 @@ def auto(self) -> bool:
6667
"""Return if a fixup can be apply as auto fix."""
6768
return False
6869

70+
@property
71+
def bus_event(self) -> BusEvent | None:
72+
"""Return the BusEvent that triggers this fixup, or None if not event-based."""
73+
return None
74+
6975
@property
7076
def all_suggestions(self) -> list[Suggestion]:
7177
"""List of all suggestions which when applied run this fixup."""

supervisor/resolution/fixups/store_execute_reload.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44

5+
from ...const import BusEvent
56
from ...coresys import CoreSys
67
from ...exceptions import (
78
ResolutionFixupError,
@@ -68,3 +69,8 @@ def issues(self) -> list[IssueType]:
6869
def auto(self) -> bool:
6970
"""Return if a fixup can be apply as auto fix."""
7071
return True
72+
73+
@property
74+
def bus_event(self) -> BusEvent | None:
75+
"""Return the BusEvent that triggers this fixup, or None if not event-based."""
76+
return BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE

supervisor/resolution/module.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import attr
77

8+
from ..bus import EventListener
89
from ..coresys import CoreSys, CoreSysAttributes
910
from ..exceptions import ResolutionError, ResolutionNotFound
1011
from ..homeassistant.const import WSEvent
@@ -46,6 +47,9 @@ def __init__(self, coresys: CoreSys):
4647
self._unsupported: list[UnsupportedReason] = []
4748
self._unhealthy: list[UnhealthyReason] = []
4849

50+
# Map suggestion UUID to event listeners (list)
51+
self._suggestion_listeners: dict[str, list[EventListener]] = {}
52+
4953
async def load_modules(self):
5054
"""Load resolution evaluation, check and fixup modules."""
5155

@@ -105,6 +109,19 @@ def add_suggestion(self, suggestion: Suggestion) -> None:
105109
)
106110
self._suggestions.append(suggestion)
107111

112+
# Register event listeners if fixups have a bus_event
113+
listeners: list[EventListener] = []
114+
for fixup in self.fixup.fixes_for_suggestion(suggestion):
115+
if fixup.auto and fixup.bus_event:
116+
117+
def event_callback(reference, fixup=fixup):
118+
return fixup(suggestion)
119+
120+
listener = self.sys_bus.register_event(fixup.bus_event, event_callback)
121+
listeners.append(listener)
122+
if listeners:
123+
self._suggestion_listeners[suggestion.uuid] = listeners
124+
108125
# Event on suggestion added to issue
109126
for issue in self.issues_for_suggestion(suggestion):
110127
self.sys_homeassistant.websocket.supervisor_event(
@@ -233,6 +250,11 @@ def dismiss_suggestion(self, suggestion: Suggestion) -> None:
233250
)
234251
self._suggestions.remove(suggestion)
235252

253+
# Remove event listeners if present
254+
listeners = self._suggestion_listeners.pop(suggestion.uuid, [])
255+
for listener in listeners:
256+
self.sys_bus.remove_listener(listener)
257+
236258
# Event on suggestion removed from issues
237259
for issue in self.issues_for_suggestion(suggestion):
238260
self.sys_homeassistant.websocket.supervisor_event(

tests/resolution/fixup/test_store_execute_reload.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Test evaluation base."""
22

33
# pylint: disable=import-error,protected-access
4+
import asyncio
45
from unittest.mock import AsyncMock, patch
56

7+
import pytest
8+
9+
from supervisor.const import BusEvent
610
from supervisor.coresys import CoreSys
11+
from supervisor.exceptions import ResolutionFixupError
712
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
813
from supervisor.resolution.data import Issue, Suggestion
914
from supervisor.resolution.fixups.store_execute_reload import FixupStoreExecuteReload
@@ -32,3 +37,94 @@ async def test_fixup(coresys: CoreSys, supervisor_internet):
3237
assert mock_repositorie.update.called
3338
assert len(coresys.resolution.suggestions) == 0
3439
assert len(coresys.resolution.issues) == 0
40+
41+
42+
@pytest.mark.usefixtures("supervisor_internet")
43+
async def test_store_execute_reload_runs_on_connectivity_true(coresys: CoreSys):
44+
"""Test fixup runs when connectivity goes from false to true."""
45+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
46+
coresys.supervisor.connectivity = False
47+
await asyncio.sleep(0)
48+
49+
mock_repository = AsyncMock()
50+
coresys.store.repositories["test_store"] = mock_repository
51+
coresys.resolution.add_issue(
52+
Issue(
53+
IssueType.FATAL_ERROR,
54+
ContextType.STORE,
55+
reference="test_store",
56+
),
57+
suggestions=[SuggestionType.EXECUTE_RELOAD],
58+
)
59+
60+
with patch.object(coresys.store, "reload") as mock_reload:
61+
# Fire event with connectivity True
62+
coresys.supervisor.connectivity = True
63+
await asyncio.sleep(0.1)
64+
65+
mock_repository.load.assert_called_once()
66+
mock_reload.assert_awaited_once_with(mock_repository)
67+
68+
69+
@pytest.mark.usefixtures("supervisor_internet")
70+
async def test_store_execute_reload_does_not_run_on_connectivity_false(
71+
coresys: CoreSys,
72+
):
73+
"""Test fixup does not run when connectivity goes from true to false."""
74+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
75+
coresys.supervisor.connectivity = True
76+
await asyncio.sleep(0)
77+
78+
mock_repository = AsyncMock()
79+
coresys.store.repositories["test_store"] = mock_repository
80+
coresys.resolution.add_issue(
81+
Issue(
82+
IssueType.FATAL_ERROR,
83+
ContextType.STORE,
84+
reference="test_store",
85+
),
86+
suggestions=[SuggestionType.EXECUTE_RELOAD],
87+
)
88+
89+
# Fire event with connectivity True
90+
coresys.supervisor.connectivity = False
91+
await asyncio.sleep(0.1)
92+
93+
mock_repository.load.assert_not_called()
94+
95+
96+
@pytest.mark.usefixtures("supervisor_internet")
97+
async def test_store_execute_reload_dismiss_suggestion_removes_listener(
98+
coresys: CoreSys,
99+
):
100+
"""Test fixup does not run on event if suggestion has been dismissed."""
101+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
102+
coresys.supervisor.connectivity = True
103+
await asyncio.sleep(0)
104+
105+
mock_repository = AsyncMock()
106+
coresys.store.repositories["test_store"] = mock_repository
107+
coresys.resolution.add_issue(
108+
issue := Issue(
109+
IssueType.FATAL_ERROR,
110+
ContextType.STORE,
111+
reference="test_store",
112+
),
113+
suggestions=[SuggestionType.EXECUTE_RELOAD],
114+
)
115+
116+
with patch.object(
117+
FixupStoreExecuteReload, "process_fixup", side_effect=ResolutionFixupError
118+
) as mock_fixup:
119+
# Fire event with issue there to trigger fixup
120+
coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True)
121+
await asyncio.sleep(0.1)
122+
mock_fixup.assert_called_once()
123+
124+
# Remove issue and suggestion and re-fire to see listener is gone
125+
mock_fixup.reset_mock()
126+
coresys.resolution.dismiss_issue(issue)
127+
128+
coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True)
129+
await asyncio.sleep(0.1)
130+
mock_fixup.assert_not_called()

0 commit comments

Comments
 (0)