Fire issue events on HA's bus (#3837)

* Fire issue events on HA's bus

* Convey event type with event field

* Message for humans
This commit is contained in:
Mike Degatano 2022-09-02 11:40:10 -04:00 committed by GitHub
parent 2dab39bf90
commit 1f28e6ad93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 44 deletions

View File

@ -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"

View File

@ -32,4 +32,6 @@ class WSEvent(str, Enum):
"""Websocket events."""
ADDON = "addon"
ISSUE_CHANGED = "issue_changed"
ISSUE_REMOVED = "issue_removed"
SUPERVISOR_UPDATE = "supervisor_update"

View File

@ -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))

View File

@ -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."""

View File

@ -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):