184 lines
6.5 KiB
Python
184 lines
6.5 KiB
Python
"""Repairs implementation for supervisor integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable, Coroutine
|
|
from types import MethodType
|
|
from typing import Any
|
|
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.repairs import RepairsFlow
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResult
|
|
|
|
from . import get_addons_info, get_issues_info
|
|
from .const import (
|
|
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
|
PLACEHOLDER_KEY_COMPONENTS,
|
|
PLACEHOLDER_KEY_REFERENCE,
|
|
SupervisorIssueContext,
|
|
)
|
|
from .handler import async_apply_suggestion
|
|
from .issues import Issue, Suggestion
|
|
|
|
SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"}
|
|
|
|
EXTRA_PLACEHOLDERS = {
|
|
"issue_mount_mount_failed": {
|
|
"storage_url": "/config/storage",
|
|
}
|
|
}
|
|
|
|
|
|
class SupervisorIssueRepairFlow(RepairsFlow):
|
|
"""Handler for an issue fixing flow."""
|
|
|
|
_data: dict[str, Any] | None = None
|
|
_issue: Issue | None = None
|
|
|
|
def __init__(self, issue_id: str) -> None:
|
|
"""Initialize repair flow."""
|
|
self._issue_id = issue_id
|
|
super().__init__()
|
|
|
|
@property
|
|
def issue(self) -> Issue | None:
|
|
"""Get associated issue."""
|
|
supervisor_issues = get_issues_info(self.hass)
|
|
if not self._issue and supervisor_issues:
|
|
self._issue = supervisor_issues.get_issue(self._issue_id)
|
|
|
|
return self._issue
|
|
|
|
@property
|
|
def description_placeholders(self) -> dict[str, str] | None:
|
|
"""Get description placeholders for steps."""
|
|
placeholders = {}
|
|
if self.issue:
|
|
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
|
|
if self.issue.reference:
|
|
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
|
|
|
return placeholders or None
|
|
|
|
def _async_form_for_suggestion(self, suggestion: Suggestion) -> FlowResult:
|
|
"""Return form for suggestion."""
|
|
return self.async_show_form(
|
|
step_id=suggestion.key,
|
|
data_schema=vol.Schema({}),
|
|
description_placeholders=self.description_placeholders,
|
|
last_step=True,
|
|
)
|
|
|
|
async def async_step_init(self, _: None = None) -> FlowResult:
|
|
"""Handle the first step of a fix flow."""
|
|
# Out of sync with supervisor, issue is resolved or not fixable. Remove it
|
|
if not self.issue or not self.issue.suggestions:
|
|
return self.async_create_entry(data={})
|
|
|
|
# All suggestions have the same logic: Apply them in supervisor,
|
|
# optionally with a confirmation step. Generating the required handler for each
|
|
# allows for shared logic but screens can still be translated per step id.
|
|
for suggestion in self.issue.suggestions:
|
|
setattr(
|
|
self,
|
|
f"async_step_{suggestion.key}",
|
|
MethodType(self._async_step(suggestion), self),
|
|
)
|
|
|
|
if len(self.issue.suggestions) > 1:
|
|
return await self.async_step_fix_menu()
|
|
|
|
# Always show a form for one suggestion to explain to user what's happening
|
|
return self._async_form_for_suggestion(self.issue.suggestions[0])
|
|
|
|
async def async_step_fix_menu(self, _: None = None) -> FlowResult:
|
|
"""Show the fix menu."""
|
|
assert self.issue
|
|
|
|
return self.async_show_menu(
|
|
step_id="fix_menu",
|
|
menu_options=[suggestion.key for suggestion in self.issue.suggestions],
|
|
description_placeholders=self.description_placeholders,
|
|
)
|
|
|
|
async def _async_step_apply_suggestion(
|
|
self, suggestion: Suggestion, confirmed: bool = False
|
|
) -> FlowResult:
|
|
"""Handle applying a suggestion as a flow step. Optionally request confirmation."""
|
|
if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED:
|
|
return self._async_form_for_suggestion(suggestion)
|
|
|
|
if await async_apply_suggestion(self.hass, suggestion.uuid):
|
|
return self.async_create_entry(data={})
|
|
return self.async_abort(reason="apply_suggestion_fail")
|
|
|
|
@staticmethod
|
|
def _async_step(
|
|
suggestion: Suggestion,
|
|
) -> Callable[
|
|
[SupervisorIssueRepairFlow, dict[str, str] | None],
|
|
Coroutine[Any, Any, FlowResult],
|
|
]:
|
|
"""Generate a step handler for a suggestion."""
|
|
|
|
async def _async_step(
|
|
self: SupervisorIssueRepairFlow, user_input: dict[str, str] | None = None
|
|
) -> FlowResult:
|
|
"""Handle a flow step for a suggestion."""
|
|
# pylint: disable-next=protected-access
|
|
return await self._async_step_apply_suggestion(
|
|
suggestion, confirmed=user_input is not None
|
|
)
|
|
|
|
return _async_step
|
|
|
|
|
|
class DockerConfigIssueRepairFlow(SupervisorIssueRepairFlow):
|
|
"""Handler for docker config issue fixing flow."""
|
|
|
|
@property
|
|
def description_placeholders(self) -> dict[str, str] | None:
|
|
"""Get description placeholders for steps."""
|
|
placeholders = {PLACEHOLDER_KEY_COMPONENTS: ""}
|
|
supervisor_issues = get_issues_info(self.hass)
|
|
if supervisor_issues and self.issue:
|
|
addons = get_addons_info(self.hass) or {}
|
|
components: list[str] = []
|
|
for issue in supervisor_issues.issues:
|
|
if issue.key == self.issue.key or issue.type != self.issue.type:
|
|
continue
|
|
|
|
if issue.context == SupervisorIssueContext.CORE:
|
|
components.insert(0, "Home Assistant")
|
|
elif issue.context == SupervisorIssueContext.ADDON:
|
|
components.append(
|
|
next(
|
|
(
|
|
info["name"]
|
|
for slug, info in addons.items()
|
|
if slug == issue.reference
|
|
),
|
|
issue.reference or "",
|
|
)
|
|
)
|
|
|
|
placeholders[PLACEHOLDER_KEY_COMPONENTS] = "\n- ".join(components)
|
|
|
|
return placeholders
|
|
|
|
|
|
async def async_create_fix_flow(
|
|
hass: HomeAssistant,
|
|
issue_id: str,
|
|
data: dict[str, str | int | float | None] | None,
|
|
) -> RepairsFlow:
|
|
"""Create flow."""
|
|
supervisor_issues = get_issues_info(hass)
|
|
issue = supervisor_issues and supervisor_issues.get_issue(issue_id)
|
|
if issue and issue.key == ISSUE_KEY_SYSTEM_DOCKER_CONFIG:
|
|
return DockerConfigIssueRepairFlow(issue_id)
|
|
|
|
return SupervisorIssueRepairFlow(issue_id)
|