mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-23 09:06:29 +00:00
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 <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
953f7d01d7
commit
806bd9f52c
@ -3,6 +3,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ...const import BusEvent
|
||||||
from ...coresys import CoreSys, CoreSysAttributes
|
from ...coresys import CoreSys, CoreSysAttributes
|
||||||
from ...exceptions import ResolutionFixupError
|
from ...exceptions import ResolutionFixupError
|
||||||
from ..const import ContextType, IssueType, SuggestionType
|
from ..const import ContextType, IssueType, SuggestionType
|
||||||
@ -66,6 +67,11 @@ class FixupBase(ABC, CoreSysAttributes):
|
|||||||
"""Return if a fixup can be apply as auto fix."""
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus_event(self) -> BusEvent | None:
|
||||||
|
"""Return the BusEvent that triggers this fixup, or None if not event-based."""
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_suggestions(self) -> list[Suggestion]:
|
def all_suggestions(self) -> list[Suggestion]:
|
||||||
"""List of all suggestions which when applied run this fixup."""
|
"""List of all suggestions which when applied run this fixup."""
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ...const import BusEvent
|
||||||
from ...coresys import CoreSys
|
from ...coresys import CoreSys
|
||||||
from ...exceptions import (
|
from ...exceptions import (
|
||||||
ResolutionFixupError,
|
ResolutionFixupError,
|
||||||
@ -68,3 +69,8 @@ class FixupStoreExecuteReload(FixupBase):
|
|||||||
def auto(self) -> bool:
|
def auto(self) -> bool:
|
||||||
"""Return if a fixup can be apply as auto fix."""
|
"""Return if a fixup can be apply as auto fix."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus_event(self) -> BusEvent | None:
|
||||||
|
"""Return the BusEvent that triggers this fixup, or None if not event-based."""
|
||||||
|
return BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE
|
||||||
|
@ -5,6 +5,7 @@ from typing import Any
|
|||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
|
from ..bus import EventListener
|
||||||
from ..coresys import CoreSys, CoreSysAttributes
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import ResolutionError, ResolutionNotFound
|
from ..exceptions import ResolutionError, ResolutionNotFound
|
||||||
from ..homeassistant.const import WSEvent
|
from ..homeassistant.const import WSEvent
|
||||||
@ -46,6 +47,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
self._unsupported: list[UnsupportedReason] = []
|
self._unsupported: list[UnsupportedReason] = []
|
||||||
self._unhealthy: list[UnhealthyReason] = []
|
self._unhealthy: list[UnhealthyReason] = []
|
||||||
|
|
||||||
|
# Map suggestion UUID to event listeners (list)
|
||||||
|
self._suggestion_listeners: dict[str, list[EventListener]] = {}
|
||||||
|
|
||||||
async def load_modules(self):
|
async def load_modules(self):
|
||||||
"""Load resolution evaluation, check and fixup modules."""
|
"""Load resolution evaluation, check and fixup modules."""
|
||||||
|
|
||||||
@ -105,6 +109,19 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._suggestions.append(suggestion)
|
self._suggestions.append(suggestion)
|
||||||
|
|
||||||
|
# Register event listeners if fixups have a bus_event
|
||||||
|
listeners: list[EventListener] = []
|
||||||
|
for fixup in self.fixup.fixes_for_suggestion(suggestion):
|
||||||
|
if fixup.auto and fixup.bus_event:
|
||||||
|
|
||||||
|
def event_callback(reference, fixup=fixup):
|
||||||
|
return fixup(suggestion)
|
||||||
|
|
||||||
|
listener = self.sys_bus.register_event(fixup.bus_event, event_callback)
|
||||||
|
listeners.append(listener)
|
||||||
|
if listeners:
|
||||||
|
self._suggestion_listeners[suggestion.uuid] = listeners
|
||||||
|
|
||||||
# Event on suggestion added to issue
|
# Event on suggestion added to issue
|
||||||
for issue in self.issues_for_suggestion(suggestion):
|
for issue in self.issues_for_suggestion(suggestion):
|
||||||
self.sys_homeassistant.websocket.supervisor_event(
|
self.sys_homeassistant.websocket.supervisor_event(
|
||||||
@ -233,6 +250,11 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
self._suggestions.remove(suggestion)
|
self._suggestions.remove(suggestion)
|
||||||
|
|
||||||
|
# Remove event listeners if present
|
||||||
|
listeners = self._suggestion_listeners.pop(suggestion.uuid, [])
|
||||||
|
for listener in listeners:
|
||||||
|
self.sys_bus.remove_listener(listener)
|
||||||
|
|
||||||
# Event on suggestion removed from issues
|
# Event on suggestion removed from issues
|
||||||
for issue in self.issues_for_suggestion(suggestion):
|
for issue in self.issues_for_suggestion(suggestion):
|
||||||
self.sys_homeassistant.websocket.supervisor_event(
|
self.sys_homeassistant.websocket.supervisor_event(
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
"""Test evaluation base."""
|
"""Test evaluation base."""
|
||||||
|
|
||||||
# pylint: disable=import-error,protected-access
|
# pylint: disable=import-error,protected-access
|
||||||
|
import asyncio
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from supervisor.const import BusEvent
|
||||||
from supervisor.coresys import CoreSys
|
from supervisor.coresys import CoreSys
|
||||||
|
from supervisor.exceptions import ResolutionFixupError
|
||||||
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
from supervisor.resolution.const import ContextType, IssueType, SuggestionType
|
||||||
from supervisor.resolution.data import Issue, Suggestion
|
from supervisor.resolution.data import Issue, Suggestion
|
||||||
from supervisor.resolution.fixups.store_execute_reload import FixupStoreExecuteReload
|
from supervisor.resolution.fixups.store_execute_reload import FixupStoreExecuteReload
|
||||||
@ -32,3 +37,94 @@ async def test_fixup(coresys: CoreSys, supervisor_internet):
|
|||||||
assert mock_repositorie.update.called
|
assert mock_repositorie.update.called
|
||||||
assert len(coresys.resolution.suggestions) == 0
|
assert len(coresys.resolution.suggestions) == 0
|
||||||
assert len(coresys.resolution.issues) == 0
|
assert len(coresys.resolution.issues) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("supervisor_internet")
|
||||||
|
async def test_store_execute_reload_runs_on_connectivity_true(coresys: CoreSys):
|
||||||
|
"""Test fixup runs when connectivity goes from false to true."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.supervisor.connectivity = False
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
mock_repository = AsyncMock()
|
||||||
|
coresys.store.repositories["test_store"] = mock_repository
|
||||||
|
coresys.resolution.add_issue(
|
||||||
|
Issue(
|
||||||
|
IssueType.FATAL_ERROR,
|
||||||
|
ContextType.STORE,
|
||||||
|
reference="test_store",
|
||||||
|
),
|
||||||
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(coresys.store, "reload") as mock_reload:
|
||||||
|
# Fire event with connectivity True
|
||||||
|
coresys.supervisor.connectivity = True
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
mock_repository.load.assert_called_once()
|
||||||
|
mock_reload.assert_awaited_once_with(mock_repository)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("supervisor_internet")
|
||||||
|
async def test_store_execute_reload_does_not_run_on_connectivity_false(
|
||||||
|
coresys: CoreSys,
|
||||||
|
):
|
||||||
|
"""Test fixup does not run when connectivity goes from true to false."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.supervisor.connectivity = True
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
mock_repository = AsyncMock()
|
||||||
|
coresys.store.repositories["test_store"] = mock_repository
|
||||||
|
coresys.resolution.add_issue(
|
||||||
|
Issue(
|
||||||
|
IssueType.FATAL_ERROR,
|
||||||
|
ContextType.STORE,
|
||||||
|
reference="test_store",
|
||||||
|
),
|
||||||
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fire event with connectivity True
|
||||||
|
coresys.supervisor.connectivity = False
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
mock_repository.load.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("supervisor_internet")
|
||||||
|
async def test_store_execute_reload_dismiss_suggestion_removes_listener(
|
||||||
|
coresys: CoreSys,
|
||||||
|
):
|
||||||
|
"""Test fixup does not run on event if suggestion has been dismissed."""
|
||||||
|
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
|
||||||
|
coresys.supervisor.connectivity = True
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
mock_repository = AsyncMock()
|
||||||
|
coresys.store.repositories["test_store"] = mock_repository
|
||||||
|
coresys.resolution.add_issue(
|
||||||
|
issue := Issue(
|
||||||
|
IssueType.FATAL_ERROR,
|
||||||
|
ContextType.STORE,
|
||||||
|
reference="test_store",
|
||||||
|
),
|
||||||
|
suggestions=[SuggestionType.EXECUTE_RELOAD],
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
FixupStoreExecuteReload, "process_fixup", side_effect=ResolutionFixupError
|
||||||
|
) as mock_fixup:
|
||||||
|
# Fire event with issue there to trigger fixup
|
||||||
|
coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
mock_fixup.assert_called_once()
|
||||||
|
|
||||||
|
# Remove issue and suggestion and re-fire to see listener is gone
|
||||||
|
mock_fixup.reset_mock()
|
||||||
|
coresys.resolution.dismiss_issue(issue)
|
||||||
|
|
||||||
|
coresys.bus.fire_event(BusEvent.SUPERVISOR_CONNECTIVITY_CHANGE, True)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
mock_fixup.assert_not_called()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user