Make repairs out of select supervisor issues (#90893)
* Make repairs out of select supervisor issues * Fix comment formatting * Add a test case for API error * Testing and type fix
This commit is contained in:
parent
6b5e82ed40
commit
a511e7d6bc
9 changed files with 1170 additions and 86 deletions
|
@ -72,6 +72,7 @@ from .const import (
|
|||
DATA_KEY_HOST,
|
||||
DATA_KEY_OS,
|
||||
DATA_KEY_SUPERVISOR,
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
DOMAIN,
|
||||
SupervisorEntityModel,
|
||||
)
|
||||
|
@ -126,7 +127,6 @@ DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
|
|||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_SUPERVISOR_ISSUES = "supervisor_issues"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
|
@ -611,7 +611,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||
)
|
||||
|
||||
# Start listening for problems with supervisor and making issues
|
||||
hass.data[DATA_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
|
||||
await issues.setup()
|
||||
|
||||
return True
|
||||
|
|
|
@ -16,10 +16,12 @@ ATTR_FOLDERS = "folders"
|
|||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_HOMEASSISTANT = "homeassistant"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_ISSUES = "issues"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_PANELS = "panels"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_SUGGESTIONS = "suggestions"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_TITLE = "title"
|
||||
|
@ -49,6 +51,8 @@ EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
|||
EVENT_SUPERVISOR_UPDATE = "supervisor_update"
|
||||
EVENT_HEALTH_CHANGED = "health_changed"
|
||||
EVENT_SUPPORTED_CHANGED = "supported_changed"
|
||||
EVENT_ISSUE_CHANGED = "issue_changed"
|
||||
EVENT_ISSUE_REMOVED = "issue_removed"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
|
@ -69,6 +73,9 @@ DATA_KEY_OS = "os"
|
|||
DATA_KEY_SUPERVISOR = "supervisor"
|
||||
DATA_KEY_CORE = "core"
|
||||
DATA_KEY_HOST = "host"
|
||||
DATA_KEY_SUPERVISOR_ISSUES = "supervisor_issues"
|
||||
|
||||
PLACEHOLDER_KEY_REFERENCE = "reference"
|
||||
|
||||
|
||||
class SupervisorEntityModel(str, Enum):
|
||||
|
|
|
@ -5,6 +5,7 @@ import asyncio
|
|||
from http import HTTPStatus
|
||||
import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
@ -249,6 +250,18 @@ async def async_update_core(
|
|||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@_api_bool
|
||||
async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> bool:
|
||||
"""Apply a suggestion from supervisor's resolution center.
|
||||
|
||||
The caller of the function should handle HassioAPIError.
|
||||
"""
|
||||
hassio = hass.data[DOMAIN]
|
||||
command = f"/resolution/suggestion/{suggestion_uuid}"
|
||||
return await hassio.send_command(command, timeout=None)
|
||||
|
||||
|
||||
class HassIO:
|
||||
"""Small API wrapper for Hass.io."""
|
||||
|
||||
|
@ -416,6 +429,16 @@ class HassIO:
|
|||
"""
|
||||
return self.send_command("/resolution/info", method="get")
|
||||
|
||||
@api_data
|
||||
def get_suggestions_for_issue(self, issue_id: str) -> dict[str, Any]:
|
||||
"""Return suggestions for issue from Supervisor resolution center.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(
|
||||
f"/resolution/issue/{issue_id}/suggestions", method="get"
|
||||
)
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(self, http_config, refresh_token):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
|
@ -454,6 +477,14 @@ class HassIO:
|
|||
"/supervisor/options", payload={"diagnostics": diagnostics}
|
||||
)
|
||||
|
||||
@_api_bool
|
||||
def apply_suggestion(self, suggestion_uuid: str):
|
||||
"""Apply a suggestion from supervisor's resolution center.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(f"/resolution/suggestion/{suggestion_uuid}")
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command,
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
"""Supervisor events monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -14,6 +19,8 @@ from homeassistant.helpers.issue_registry import (
|
|||
from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_ISSUES,
|
||||
ATTR_SUGGESTIONS,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_UNHEALTHY,
|
||||
ATTR_UNHEALTHY_REASONS,
|
||||
|
@ -23,19 +30,26 @@ from .const import (
|
|||
ATTR_WS_EVENT,
|
||||
DOMAIN,
|
||||
EVENT_HEALTH_CHANGED,
|
||||
EVENT_ISSUE_CHANGED,
|
||||
EVENT_ISSUE_REMOVED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
PLACEHOLDER_KEY_REFERENCE,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .handler import HassIO
|
||||
from .handler import HassIO, HassioAPIError
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
ISSUE_ID_UNHEALTHY = "unhealthy_system"
|
||||
ISSUE_ID_UNSUPPORTED = "unsupported_system"
|
||||
|
||||
INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy"
|
||||
INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported"
|
||||
|
||||
PLACEHOLDER_KEY_REASON = "reason"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"connectivity_check",
|
||||
|
@ -69,6 +83,88 @@ UNHEALTHY_REASONS = {
|
|||
"untrusted",
|
||||
}
|
||||
|
||||
# Keys (type + context) of issues that when found should be made into a repair
|
||||
ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_system_multiple_data_disks",
|
||||
"issue_system_reboot_required",
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SuggestionDataType(TypedDict):
|
||||
"""Suggestion dictionary as received from supervisor."""
|
||||
|
||||
uuid: str
|
||||
type: str
|
||||
context: str
|
||||
reference: str | None
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Suggestion:
|
||||
"""Suggestion from Supervisor which resolves an issue."""
|
||||
|
||||
uuid: str
|
||||
type_: str
|
||||
context: str
|
||||
reference: str | None = None
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Get key for suggestion (combination of context and type)."""
|
||||
return f"{self.context}_{self.type_}"
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: SuggestionDataType) -> Suggestion:
|
||||
"""Convert from dictionary representation."""
|
||||
return Suggestion(
|
||||
uuid=data["uuid"],
|
||||
type_=data["type"],
|
||||
context=data["context"],
|
||||
reference=data["reference"],
|
||||
)
|
||||
|
||||
|
||||
class IssueDataType(TypedDict):
|
||||
"""Issue dictionary as received from supervisor."""
|
||||
|
||||
uuid: str
|
||||
type: str
|
||||
context: str
|
||||
reference: str | None
|
||||
suggestions: NotRequired[list[SuggestionDataType]]
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Issue:
|
||||
"""Issue from Supervisor."""
|
||||
|
||||
uuid: str
|
||||
type_: str
|
||||
context: str
|
||||
reference: str | None = None
|
||||
suggestions: list[Suggestion] = field(default_factory=list, compare=False)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Get key for issue (combination of context and type)."""
|
||||
return f"issue_{self.context}_{self.type_}"
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: IssueDataType) -> Issue:
|
||||
"""Convert from dictionary representation."""
|
||||
suggestions: list[SuggestionDataType] = data.get("suggestions", [])
|
||||
return Issue(
|
||||
uuid=data["uuid"],
|
||||
type_=data["type"],
|
||||
context=data["context"],
|
||||
reference=data["reference"],
|
||||
suggestions=[
|
||||
Suggestion.from_dict(suggestion) for suggestion in suggestions
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class SupervisorIssues:
|
||||
"""Create issues from supervisor events."""
|
||||
|
@ -79,6 +175,7 @@ class SupervisorIssues:
|
|||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[str, Issue] = {}
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
|
@ -87,14 +184,14 @@ class SupervisorIssues:
|
|||
|
||||
@unhealthy_reasons.setter
|
||||
def unhealthy_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unhealthy reasons. Create or delete issues as necessary."""
|
||||
"""Set unhealthy reasons. Create or delete repairs as necessary."""
|
||||
for unhealthy in reasons - self.unhealthy_reasons:
|
||||
if unhealthy in UNHEALTHY_REASONS:
|
||||
translation_key = f"unhealthy_{unhealthy}"
|
||||
translation_key = f"{ISSUE_KEY_UNHEALTHY}_{unhealthy}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unhealthy"
|
||||
translation_placeholders = {"reason": unhealthy}
|
||||
translation_key = ISSUE_KEY_UNHEALTHY
|
||||
translation_placeholders = {PLACEHOLDER_KEY_REASON: unhealthy}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
|
@ -119,14 +216,14 @@ class SupervisorIssues:
|
|||
|
||||
@unsupported_reasons.setter
|
||||
def unsupported_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unsupported reasons. Create or delete issues as necessary."""
|
||||
"""Set unsupported reasons. Create or delete repairs as necessary."""
|
||||
for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons:
|
||||
if unsupported in UNSUPPORTED_REASONS:
|
||||
translation_key = f"unsupported_{unsupported}"
|
||||
translation_key = f"{ISSUE_KEY_UNSUPPORTED}_{unsupported}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unsupported"
|
||||
translation_placeholders = {"reason": unsupported}
|
||||
translation_key = ISSUE_KEY_UNSUPPORTED
|
||||
translation_placeholders = {PLACEHOLDER_KEY_REASON: unsupported}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
|
@ -144,6 +241,60 @@ class SupervisorIssues:
|
|||
|
||||
self._unsupported_reasons = reasons
|
||||
|
||||
def add_issue(self, issue: Issue) -> None:
|
||||
"""Add or update an issue in the list. Create or update a repair if necessary."""
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
issue.uuid,
|
||||
is_fixable=bool(issue.suggestions),
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue.key,
|
||||
translation_placeholders={PLACEHOLDER_KEY_REFERENCE: issue.reference}
|
||||
if issue.reference
|
||||
else None,
|
||||
)
|
||||
|
||||
self._issues[issue.uuid] = issue
|
||||
|
||||
async def add_issue_from_data(self, data: IssueDataType) -> None:
|
||||
"""Add issue from data to list after getting latest suggestions."""
|
||||
try:
|
||||
suggestions = (await self._client.get_suggestions_for_issue(data["uuid"]))[
|
||||
ATTR_SUGGESTIONS
|
||||
]
|
||||
self.add_issue(
|
||||
Issue(
|
||||
uuid=data["uuid"],
|
||||
type_=data["type"],
|
||||
context=data["context"],
|
||||
reference=data["reference"],
|
||||
suggestions=[
|
||||
Suggestion.from_dict(suggestion) for suggestion in suggestions
|
||||
],
|
||||
)
|
||||
)
|
||||
except HassioAPIError:
|
||||
_LOGGER.error(
|
||||
"Could not get suggestions for supervisor issue %s, skipping it",
|
||||
data["uuid"],
|
||||
)
|
||||
|
||||
def remove_issue(self, issue: Issue) -> None:
|
||||
"""Remove an issue from the list. Delete a repair if necessary."""
|
||||
if issue.uuid not in self._issues:
|
||||
return
|
||||
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
async_delete_issue(self._hass, DOMAIN, issue.uuid)
|
||||
|
||||
del self._issues[issue.uuid]
|
||||
|
||||
def get_issue(self, issue_id: str) -> Issue | None:
|
||||
"""Get issue from key."""
|
||||
return self._issues.get(issue_id)
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
await self.update()
|
||||
|
@ -153,11 +304,22 @@ class SupervisorIssues:
|
|||
)
|
||||
|
||||
async def update(self) -> None:
|
||||
"""Update issuess from Supervisor resolution center."""
|
||||
"""Update issues from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
# Remove any cached issues that weren't returned
|
||||
for issue_id in set(self._issues.keys()) - {
|
||||
issue["uuid"] for issue in data[ATTR_ISSUES]
|
||||
}:
|
||||
self.remove_issue(self._issues[issue_id])
|
||||
|
||||
# Add/update any issues that came back
|
||||
await asyncio.gather(
|
||||
*[self.add_issue_from_data(issue) for issue in data[ATTR_ISSUES]]
|
||||
)
|
||||
|
||||
@callback
|
||||
def _supervisor_events_to_issues(self, event: dict[str, Any]) -> None:
|
||||
"""Create issues from supervisor events."""
|
||||
|
@ -183,3 +345,9 @@ class SupervisorIssues:
|
|||
if event[ATTR_DATA][ATTR_SUPPORTED]
|
||||
else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
|
||||
)
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_CHANGED:
|
||||
self.add_issue(Issue.from_dict(event[ATTR_DATA]))
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_ISSUE_REMOVED:
|
||||
self.remove_issue(Issue.from_dict(event[ATTR_DATA]))
|
||||
|
|
121
homeassistant/components/hassio/repairs.py
Normal file
121
homeassistant/components/hassio/repairs.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
"""Repairs implementation for supervisor integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
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 .const import DATA_KEY_SUPERVISOR_ISSUES, PLACEHOLDER_KEY_REFERENCE
|
||||
from .handler import HassioAPIError, async_apply_suggestion
|
||||
from .issues import Issue, Suggestion, SupervisorIssues
|
||||
|
||||
SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"}
|
||||
|
||||
|
||||
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."""
|
||||
if not self._issue:
|
||||
supervisor_issues: SupervisorIssues = self.hass.data[
|
||||
DATA_KEY_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."""
|
||||
return (
|
||||
{PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
if self.issue and self.issue.reference
|
||||
else 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."""
|
||||
# Got out of sync with supervisor, issue is resolved or isn't fixable. Either way, resolve the repair
|
||||
if not self.issue or not self.issue.suggestions:
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
# All suggestions do the same thing: 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 self.async_show_menu(
|
||||
step_id="fix_menu",
|
||||
menu_options=[suggestion.key for suggestion in self.issue.suggestions],
|
||||
description_placeholders=self.description_placeholders,
|
||||
)
|
||||
|
||||
# Always show a form if there's only one suggestion so we can explain to the user what's happening
|
||||
return self._async_form_for_suggestion(self.issue.suggestions[0])
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
await async_apply_suggestion(self.hass, suggestion.uuid)
|
||||
except HassioAPIError:
|
||||
return self.async_abort(reason="apply_suggestion_fail")
|
||||
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
@staticmethod
|
||||
def _async_step(suggestion: Suggestion) -> Callable:
|
||||
"""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
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
return SupervisorIssueRepairFlow(issue_id)
|
|
@ -17,6 +17,32 @@
|
|||
}
|
||||
},
|
||||
"issues": {
|
||||
"issue_system_multiple_data_disks": {
|
||||
"title": "Multiple data disks detected",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"system_rename_data_disk": {
|
||||
"description": "'{reference}' is a filesystem with the name 'hassos-data' and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the fix option to rename the filesystem to prevent this. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not rename the filesystem. Check the supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"issue_system_reboot_required": {
|
||||
"title": "Reboot required",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"system_execute_reboot": {
|
||||
"description": "Settings were changed which require a system reboot to take effect.\n\nThis fix will initiate a system reboot which will make Home Assistant and all the Add-ons inaccessible for a brief period."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not reboot the system. Check the supervisor logs for more details."
|
||||
}
|
||||
}
|
||||
},
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue