1 """Supervisor events monitor."""
3 from __future__
import annotations
6 from dataclasses
import dataclass, field
7 from datetime
import datetime
9 from typing
import Any, NotRequired, TypedDict
12 from aiohasupervisor
import SupervisorError
13 from aiohasupervisor.models
import ContextType, Issue
as SupervisorIssue
28 ATTR_UNHEALTHY_REASONS,
29 ATTR_UNSUPPORTED_REASONS,
36 EVENT_SUPERVISOR_EVENT,
37 EVENT_SUPERVISOR_UPDATE,
38 EVENT_SUPPORTED_CHANGED,
39 ISSUE_KEY_ADDON_BOOT_FAIL,
40 ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
41 ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
42 ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
43 PLACEHOLDER_KEY_ADDON,
44 PLACEHOLDER_KEY_ADDON_URL,
45 PLACEHOLDER_KEY_REFERENCE,
46 REQUEST_REFRESH_DELAY,
47 UPDATE_KEY_SUPERVISOR,
49 from .coordinator
import get_addons_info
50 from .handler
import HassIO, get_supervisor_client
52 ISSUE_KEY_UNHEALTHY =
"unhealthy"
53 ISSUE_KEY_UNSUPPORTED =
"unsupported"
54 ISSUE_ID_UNHEALTHY =
"unhealthy_system"
55 ISSUE_ID_UNSUPPORTED =
"unsupported_system"
57 INFO_URL_UNHEALTHY =
"https://www.home-assistant.io/more-info/unhealthy"
58 INFO_URL_UNSUPPORTED =
"https://www.home-assistant.io/more-info/unsupported"
60 PLACEHOLDER_KEY_REASON =
"reason"
62 UNSUPPORTED_REASONS = {
68 "docker_configuration",
86 UNSUPPORTED_SKIP_REPAIR = {
"privileged"}
96 ISSUE_KEYS_FOR_REPAIRS = {
97 ISSUE_KEY_ADDON_BOOT_FAIL,
98 "issue_mount_mount_failed",
99 "issue_system_multiple_data_disks",
100 "issue_system_reboot_required",
101 ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
102 ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
103 ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
106 _LOGGER = logging.getLogger(__name__)
110 """Suggestion dictionary as received from supervisor."""
115 reference: str |
None
118 @dataclass(slots=True, frozen=True)
120 """Suggestion from Supervisor which resolves an issue."""
125 reference: str |
None =
None
129 """Get key for suggestion (combination of context and type)."""
130 return f
"{self.context}_{self.type}"
133 def from_dict(cls, data: SuggestionDataType) -> Suggestion:
134 """Convert from dictionary representation."""
136 uuid=
UUID(data[
"uuid"]),
138 context=ContextType(data[
"context"]),
139 reference=data[
"reference"],
144 """Issue dictionary as received from supervisor."""
149 reference: str |
None
150 suggestions: NotRequired[list[SuggestionDataType]]
153 @dataclass(slots=True, frozen=True)
155 """Issue from Supervisor."""
160 reference: str |
None =
None
161 suggestions: list[Suggestion] = field(default_factory=list, compare=
False)
165 """Get key for issue (combination of context and type)."""
166 return f
"issue_{self.context}_{self.type}"
170 """Convert from dictionary representation."""
171 suggestions: list[SuggestionDataType] = data.get(
"suggestions", [])
173 uuid=
UUID(data[
"uuid"]),
175 context=ContextType(data[
"context"]),
176 reference=data[
"reference"],
178 Suggestion.from_dict(suggestion)
for suggestion
in suggestions
184 """Create issues from supervisor events."""
186 def __init__(self, hass: HomeAssistant, client: HassIO) ->
None:
187 """Initialize supervisor issues."""
192 self._issues: dict[UUID, Issue] = {}
197 """Get unhealthy reasons. Returns empty set if system is healthy."""
200 @unhealthy_reasons.setter
202 """Set unhealthy reasons. Create or delete repairs as necessary."""
204 if unhealthy
in UNHEALTHY_REASONS:
205 translation_key = f
"{ISSUE_KEY_UNHEALTHY}_{unhealthy}"
206 translation_placeholders =
None
208 translation_key = ISSUE_KEY_UNHEALTHY
209 translation_placeholders = {PLACEHOLDER_KEY_REASON: unhealthy}
214 f
"{ISSUE_ID_UNHEALTHY}_{unhealthy}",
216 learn_more_url=f
"{INFO_URL_UNHEALTHY}/{unhealthy}",
217 severity=IssueSeverity.CRITICAL,
218 translation_key=translation_key,
219 translation_placeholders=translation_placeholders,
229 """Get unsupported reasons. Returns empty set if system is supported."""
232 @unsupported_reasons.setter
234 """Set unsupported reasons. Create or delete repairs as necessary."""
236 if unsupported
in UNSUPPORTED_REASONS:
237 translation_key = f
"{ISSUE_KEY_UNSUPPORTED}_{unsupported}"
238 translation_placeholders =
None
240 translation_key = ISSUE_KEY_UNSUPPORTED
241 translation_placeholders = {PLACEHOLDER_KEY_REASON: unsupported}
246 f
"{ISSUE_ID_UNSUPPORTED}_{unsupported}",
248 learn_more_url=f
"{INFO_URL_UNSUPPORTED}/{unsupported}",
249 severity=IssueSeverity.WARNING,
250 translation_key=translation_key,
251 translation_placeholders=translation_placeholders,
262 return set(self._issues.values())
265 """Add or update an issue in the list. Create or update a repair if necessary."""
266 if issue.key
in ISSUE_KEYS_FOR_REPAIRS:
267 placeholders: dict[str, str] |
None =
None
269 placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference}
271 if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING:
272 placeholders[PLACEHOLDER_KEY_ADDON_URL] = (
273 f
"/hassio/addon/{issue.reference}"
276 if addons
and issue.reference
in addons:
277 placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][
281 placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference
287 is_fixable=bool(issue.suggestions),
288 severity=IssueSeverity.WARNING,
289 translation_key=issue.key,
290 translation_placeholders=placeholders,
293 self._issues[issue.uuid] = issue
296 """Add issue from data to list after getting latest suggestions."""
303 except SupervisorError:
305 "Could not get suggestions for supervisor issue %s, skipping it",
313 context=data.context,
314 reference=data.reference,
317 uuid=suggestion.uuid,
318 type=
str(suggestion.type),
319 context=suggestion.context,
320 reference=suggestion.reference,
322 for suggestion
in suggestions
328 """Remove an issue from the list. Delete a repair if necessary."""
329 if issue.uuid
not in self._issues:
332 if issue.key
in ISSUE_KEYS_FOR_REPAIRS:
335 del self._issues[issue.uuid]
338 """Get issue from key."""
339 return self._issues.
get(
UUID(issue_id))
342 """Create supervisor events listener."""
349 async
def _update(self, _: datetime |
None =
None) ->
None:
350 """Update issues from Supervisor resolution center."""
353 except SupervisorError
as err:
354 _LOGGER.error(
"Failed to update supervisor issues: %r", err)
357 REQUEST_REFRESH_DELAY,
365 for issue_id
in set(self._issues) - {issue.uuid
for issue
in data.issues}:
369 await asyncio.gather(
375 """Create issues from supervisor events."""
376 if ATTR_WS_EVENT
not in event:
380 event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
381 and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
383 self.
_hass_hass.async_create_task(self.
_update_update())
385 elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED:
388 if event[ATTR_DATA][ATTR_HEALTHY]
389 else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS])
392 elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED:
395 if event[ATTR_DATA][ATTR_SUPPORTED]
396 else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
399 elif event[ATTR_WS_EVENT] == EVENT_ISSUE_CHANGED:
400 self.
add_issueadd_issue(Issue.from_dict(event[ATTR_DATA]))
402 elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
403 self.
remove_issueremove_issue(Issue.from_dict(event[ATTR_DATA]))
Issue from_dict(cls, IssueDataType data)
Suggestion from_dict(cls, SuggestionDataType data)
None add_issue_from_data(self, SupervisorIssue data)
None unhealthy_reasons(self, set[str] reasons)
None __init__(self, HomeAssistant hass, HassIO client)
set[str] unsupported_reasons(self)
None unsupported_reasons(self, set[str] reasons)
None add_issue(self, Issue issue)
None _supervisor_events_to_issues(self, dict[str, Any] event)
None _update(self, datetime|None _=None)
Issue|None get_issue(self, str issue_id)
set[str] unhealthy_reasons(self)
None remove_issue(self, Issue issue)
web.Response get(self, web.Request request, str config_key)
dict[str, dict[str, Any]]|None get_addons_info(HomeAssistant hass)
SupervisorClient get_supervisor_client(HomeAssistant hass)
None async_create_issue(HomeAssistant hass, str entry_id)
None async_delete_issue(HomeAssistant hass, str entry_id)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)