From 1f28e6ad9364fcee21a9b7b0136a11dda26352ba Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Fri, 2 Sep 2022 11:40:10 -0400 Subject: [PATCH] Fire issue events on HA's bus (#3837) * Fire issue events on HA's bus * Convey event type with event field * Message for humans --- supervisor/const.py | 2 - supervisor/homeassistant/const.py | 2 + supervisor/homeassistant/websocket.py | 27 +++++- supervisor/resolution/module.py | 20 +++-- tests/resolution/test_resolution_manager.py | 94 +++++++++++++-------- 5 files changed, 101 insertions(+), 44 deletions(-) diff --git a/supervisor/const.py b/supervisor/const.py index 20709777f..0f973c787 100644 --- a/supervisor/const.py +++ b/supervisor/const.py @@ -443,8 +443,6 @@ class BusEvent(str, Enum): HARDWARE_NEW_DEVICE = "hardware_new_device" HARDWARE_REMOVE_DEVICE = "hardware_remove_device" - ISSUE_CHANGED = "issue_changed" - ISSUE_REMOVED = "issue_removed" DOCKER_CONTAINER_STATE_CHANGE = "docker_container_state_change" diff --git a/supervisor/homeassistant/const.py b/supervisor/homeassistant/const.py index 5e6f33079..2e50e1386 100644 --- a/supervisor/homeassistant/const.py +++ b/supervisor/homeassistant/const.py @@ -32,4 +32,6 @@ class WSEvent(str, Enum): """Websocket events.""" ADDON = "addon" + ISSUE_CHANGED = "issue_changed" + ISSUE_REMOVED = "issue_removed" SUPERVISOR_UPDATE = "supervisor_update" diff --git a/supervisor/homeassistant/websocket.py b/supervisor/homeassistant/websocket.py index a0ab59774..79ff3af02 100644 --- a/supervisor/homeassistant/websocket.py +++ b/supervisor/homeassistant/websocket.py @@ -262,7 +262,7 @@ class HomeAssistantWebSocket(CoreSysAttributes): except HomeAssistantWSNotSupported: pass except HomeAssistantWSError as err: - _LOGGER.error(err) + _LOGGER.error("Could not send message to Home Assistant due to %s", err) def supervisor_update_event( self, @@ -279,3 +279,28 @@ class HomeAssistantWebSocket(CoreSysAttributes): if self.sys_core.state in CLOSING_STATES: return self.sys_create_task(self.async_send_message(message)) + + async def async_supervisor_event( + self, event: WSEvent, data: dict[str, Any] | None = None + ): + """Send a supervisor/event command to Home Assistant.""" + try: + await self.async_send_message( + { + ATTR_TYPE: WSType.SUPERVISOR_EVENT, + ATTR_DATA: { + ATTR_EVENT: event, + ATTR_DATA: data or {}, + }, + } + ) + except HomeAssistantWSNotSupported: + pass + except HomeAssistantWSError as err: + _LOGGER.error("Could not send message to Home Assistant due to %s", err) + + def supervisor_event(self, event: WSEvent, data: dict[str, Any] | None = None): + """Send a supervisor/event command to Home Assistant.""" + if self.sys_core.state in CLOSING_STATES: + return + self.sys_create_task(self.async_supervisor_event(event, data)) diff --git a/supervisor/resolution/module.py b/supervisor/resolution/module.py index dcf4334a9..82a818a7c 100644 --- a/supervisor/resolution/module.py +++ b/supervisor/resolution/module.py @@ -2,9 +2,11 @@ import logging from typing import Any -from ..const import BusEvent +import attr + from ..coresys import CoreSys, CoreSysAttributes from ..exceptions import ResolutionError, ResolutionNotFound +from ..homeassistant.const import WSEvent from ..utils.common import FileConfiguration from .check import ResolutionCheck from .const import ( @@ -84,7 +86,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): self._issues.append(issue) # Event on issue creation - self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue) + self.sys_homeassistant.websocket.supervisor_event( + WSEvent.ISSUE_CHANGED, attr.asdict(issue) + ) @property def suggestions(self) -> list[Suggestion]: @@ -107,7 +111,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): # Event on suggestion added to issue for issue in self.issues_for_suggestion(suggestion): - self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue) + self.sys_homeassistant.websocket.supervisor_event( + WSEvent.ISSUE_CHANGED, attr.asdict(issue) + ) @property def unsupported(self) -> list[UnsupportedReason]: @@ -200,7 +206,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): # Event on suggestion removed from issues for issue in self.issues_for_suggestion(suggestion): - self.sys_bus.fire_event(BusEvent.ISSUE_CHANGED, issue) + self.sys_homeassistant.websocket.supervisor_event( + WSEvent.ISSUE_CHANGED, attr.asdict(issue) + ) def dismiss_issue(self, issue: Issue) -> None: """Dismiss suggested action.""" @@ -211,7 +219,9 @@ class ResolutionManager(FileConfiguration, CoreSysAttributes): self._issues.remove(issue) # Event on issue removal - self.sys_bus.fire_event(BusEvent.ISSUE_REMOVED, issue) + self.sys_homeassistant.websocket.supervisor_event( + WSEvent.ISSUE_REMOVED, attr.asdict(issue) + ) def dismiss_unsupported(self, reason: Issue) -> None: """Dismiss a reason for unsupported.""" diff --git a/tests/resolution/test_resolution_manager.py b/tests/resolution/test_resolution_manager.py index 1d6e0bb05..0a80c8ff8 100644 --- a/tests/resolution/test_resolution_manager.py +++ b/tests/resolution/test_resolution_manager.py @@ -1,9 +1,11 @@ """Tests for resolution manager.""" +import asyncio +from typing import Any from unittest.mock import AsyncMock, patch +import attr import pytest -from supervisor.const import BusEvent from supervisor.coresys import CoreSys from supervisor.exceptions import ResolutionError from supervisor.resolution.const import ( @@ -18,7 +20,6 @@ from supervisor.resolution.data import Issue, Suggestion def test_properies_unsupported(coresys: CoreSys): """Test resolution manager properties unsupported.""" - assert coresys.core.supported coresys.resolution.unsupported = UnsupportedReason.OS @@ -27,7 +28,6 @@ def test_properies_unsupported(coresys: CoreSys): def test_properies_unhealthy(coresys: CoreSys): """Test resolution manager properties unhealthy.""" - assert coresys.core.healthy coresys.resolution.unhealthy = UnhealthyReason.SUPERVISOR @@ -180,46 +180,68 @@ async def test_issues_for_suggestion(coresys: CoreSys): } +def _supervisor_event_message(event: str, data: dict[str, Any]) -> dict[str, Any]: + """Make mock supervisor event message for ha websocket.""" + return { + "type": "supervisor/event", + "data": { + "event": event, + "data": data, + }, + } + + async def test_events_on_issue_changes(coresys: CoreSys): """Test events fired when an issue changes.""" - coresys.bus.register_event(BusEvent.ISSUE_CHANGED, change_handler := AsyncMock()) - coresys.bus.register_event(BusEvent.ISSUE_REMOVED, remove_handler := AsyncMock()) + with patch.object( + type(coresys.homeassistant.websocket), "async_send_message" + ) as send_message: + # Creating an issue with a suggestion should fire exactly one issue changed event + assert coresys.resolution.issues == [] + assert coresys.resolution.suggestions == [] + coresys.resolution.create_issue( + IssueType.CORRUPT_REPOSITORY, + ContextType.STORE, + "test_repo", + [SuggestionType.EXECUTE_RESET], + ) + await asyncio.sleep(0) - # Creating an issue with a suggestion should fire exactly one event - assert coresys.resolution.issues == [] - assert coresys.resolution.suggestions == [] - coresys.resolution.create_issue( - IssueType.CORRUPT_REPOSITORY, - ContextType.STORE, - "test_repo", - [SuggestionType.EXECUTE_RESET], - ) + assert len(coresys.resolution.issues) == 1 + assert len(coresys.resolution.suggestions) == 1 + issue = coresys.resolution.issues[0] + suggestion = coresys.resolution.suggestions[0] + send_message.assert_called_once_with( + _supervisor_event_message("issue_changed", attr.asdict(issue)) + ) - assert len(coresys.resolution.issues) == 1 - assert len(coresys.resolution.suggestions) == 1 - issue = coresys.resolution.issues[0] - suggestion = coresys.resolution.suggestions[0] - change_handler.assert_called_once_with(issue) + # Adding a suggestion that fixes the issue changes it + send_message.reset_mock() + coresys.resolution.suggestions = execute_remove = Suggestion( + SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "test_repo" + ) + await asyncio.sleep(0) + send_message.assert_called_once_with( + _supervisor_event_message("issue_changed", attr.asdict(issue)) + ) - # Adding and removing a suggestion that fixes the issue should fire another - change_handler.reset_mock() - coresys.resolution.suggestions = execute_remove = Suggestion( - SuggestionType.EXECUTE_REMOVE, ContextType.STORE, "test_repo" - ) - change_handler.assert_called_once_with(issue) + # Removing a suggestion that fixes the issue changes it again + send_message.reset_mock() + coresys.resolution.dismiss_suggestion(execute_remove) + await asyncio.sleep(0) + send_message.assert_called_once_with( + _supervisor_event_message("issue_changed", attr.asdict(issue)) + ) - change_handler.reset_mock() - coresys.resolution.dismiss_suggestion(execute_remove) - change_handler.assert_called_once_with(issue) - remove_handler.assert_not_called() + # Applying a suggestion should only fire an issue removed event + send_message.reset_mock() + with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))): + await coresys.resolution.apply_suggestion(suggestion) - # Applying a suggestion should only fire the issue removed event - change_handler.reset_mock() - with patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))): - await coresys.resolution.apply_suggestion(suggestion) - - change_handler.assert_not_called() - remove_handler.assert_called_once_with(issue) + await asyncio.sleep(0) + send_message.assert_called_once_with( + _supervisor_event_message("issue_removed", attr.asdict(issue)) + ) async def test_resolution_apply_suggestion_multiple_copies(coresys: CoreSys):