Compare commits

..

1 Commits

Author SHA1 Message Date
Jan Čermák
351ac662c5 Add issue and repair for NTP sync failure
Notify user about NTP sync failures and create repair for issue/suggestion
added in home-assistant/supervisor#6625.
2026-03-13 15:42:48 +01:00
7 changed files with 222 additions and 4 deletions

View File

@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.02.0"
BASE_IMAGE_VERSION: "2026.01.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}

View File

@@ -1 +1 @@
3.14.3
3.14.2

View File

@@ -88,6 +88,7 @@ ISSUE_KEYS_FOR_REPAIRS = {
"issue_system_disk_lifetime",
ISSUE_KEY_SYSTEM_FREE_SPACE,
ISSUE_KEY_ADDON_PWNED,
"issue_system_ntp_sync_failed",
}
_LOGGER = logging.getLogger(__name__)

View File

@@ -164,6 +164,19 @@
},
"title": "Multiple data disks detected"
},
"issue_system_ntp_sync_failed": {
"fix_flow": {
"abort": {
"apply_suggestion_fail": "Could not re-enable NTP. Check the Supervisor logs for more details."
},
"step": {
"system_enable_ntp": {
"description": "NTP time servers were unreachable and the system clock was found to be more than 1 hour off. The time has been corrected and the NTP service was temporarily disabled to allow this adjustment.\n\nCheck the **Host logs** to investigate why NTP servers could not be reached. Once resolved, select **Submit** to re-enable the NTP service."
}
}
},
"title": "NTP sync failed - system time was corrected"
},
"issue_system_reboot_required": {
"fix_flow": {
"abort": {

View File

@@ -961,8 +961,7 @@ class HomeAssistant:
async def async_block_till_done(self, wait_background_tasks: bool = False) -> None:
"""Block until all pending work is done."""
# Sleep twice to flush out any call_soon_threadsafe
await asyncio.sleep(0)
# To flush out any call_soon_threadsafe
await asyncio.sleep(0)
start_time: float | None = None
current_task = asyncio.current_task()

View File

@@ -949,6 +949,61 @@ async def test_supervisor_issues_detached_addon_missing(
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_ntp_sync_failed(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test supervisor issue for NTP sync failed."""
mock_resolution_info(supervisor_client)
result = await async_setup_component(hass, "hassio", {})
assert result
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "supervisor/event",
"data": {
"event": "issue_changed",
"data": {
"uuid": (issue_uuid := uuid4().hex),
"type": "ntp_sync_failed",
"context": "system",
"reference": None,
"suggestions": [
{
"uuid": uuid4().hex,
"type": "enable_ntp",
"context": "system",
"reference": None,
}
],
},
},
}
)
msg = await client.receive_json()
assert msg["success"]
await hass.async_block_till_done()
await client.send_json({"id": 2, "type": "repairs/list_issues"})
msg = await client.receive_json()
assert msg["success"]
assert len(msg["result"]["issues"]) == 1
assert_issue_repair_in_list(
msg["result"]["issues"],
uuid=issue_uuid,
context="system",
type_="ntp_sync_failed",
fixable=True,
placeholders=None,
)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issues_disk_lifetime(
hass: HomeAssistant,

View File

@@ -402,6 +402,156 @@ async def test_supervisor_issue_repair_flow_skip_confirmation(
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_ntp_sync_failed_repair_flow(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow for NTP sync failed supervisor issue."""
mock_resolution_info(
supervisor_client,
issues=[
Issue(
# IssueType.NTP_SYNC_FAILED once aiohasupervisor >0.3.3 is released
type="ntp_sync_failed",
context=ContextType.SYSTEM,
reference=None,
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
# SuggestionType.ENABLE_NTP once aiohasupervisor >0.3.3 is released
type="enable_ntp",
context=ContextType.SYSTEM,
reference=None,
uuid=(sugg_uuid := uuid4()),
auto=False,
),
]
},
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
resp = await client.post(
"/api/repairs/issues/fix",
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"type": "form",
"flow_id": flow_id,
"handler": "hassio",
"step_id": "system_enable_ntp",
"data_schema": [],
"errors": None,
"description_placeholders": None,
"last_step": True,
"preview": None,
}
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"type": "create_entry",
"flow_id": flow_id,
"handler": "hassio",
"description": None,
"description_placeholders": None,
}
assert not issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
supervisor_client.resolution.apply_suggestion.assert_called_once_with(sugg_uuid)
@pytest.mark.usefixtures("all_setup_requests")
async def test_supervisor_issue_ntp_sync_failed_repair_flow_error(
hass: HomeAssistant,
supervisor_client: AsyncMock,
hass_client: ClientSessionGenerator,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test fix flow aborts when NTP re-enable fails."""
mock_resolution_info(
supervisor_client,
issues=[
Issue(
# IssueType.NTP_SYNC_FAILED once aiohasupervisor >0.3.3 is released
type="ntp_sync_failed",
context=ContextType.SYSTEM,
reference=None,
uuid=(issue_uuid := uuid4()),
),
],
suggestions_by_issue={
issue_uuid: [
Suggestion(
# SuggestionType.ENABLE_NTP once aiohasupervisor >0.3.3 is released
type="enable_ntp",
context=ContextType.SYSTEM,
reference=None,
uuid=uuid4(),
auto=False,
),
]
},
suggestion_result=SupervisorError("boom"),
)
assert await async_setup_component(hass, "hassio", {})
repair_issue = issue_registry.async_get_issue(
domain="hassio", issue_id=issue_uuid.hex
)
assert repair_issue
client = await hass_client()
resp = await client.post(
"/api/repairs/issues/fix",
json={"handler": "hassio", "issue_id": repair_issue.issue_id},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
resp = await client.post(f"/api/repairs/issues/fix/{flow_id}")
assert resp.status == HTTPStatus.OK
data = await resp.json()
flow_id = data["flow_id"]
assert data == {
"type": "abort",
"flow_id": flow_id,
"handler": "hassio",
"reason": "apply_suggestion_fail",
"description_placeholders": None,
}
assert issue_registry.async_get_issue(domain="hassio", issue_id=issue_uuid.hex)
@pytest.mark.usefixtures("all_setup_requests")
async def test_mount_failed_repair_flow_error(
hass: HomeAssistant,