Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
781ec62
add next-flow to repairs
iluvdata Feb 24, 2026
c657339
add tests repairs flow fix next flow
iluvdata Feb 24, 2026
287c921
add next_flow to async_create_entry
iluvdata Mar 7, 2026
70ce27c
Merge remote-tracking branch 'upstream/dev' into repair-flow-next-flow
iluvdata Mar 7, 2026
9bd1c31
Merge branch 'home-assistant:dev' into repair-flow-next-flow
iluvdata Mar 7, 2026
2d05ad8
Merge branch 'repair-flow-next-flow' of https://github.com/iluvdata/h…
iluvdata Mar 7, 2026
649b3ed
comment grammar error
iluvdata Mar 7, 2026
9de1d4c
Update tests/components/repairs/test_websocket_api.py
iluvdata Mar 7, 2026
65d1b0c
Update tests/components/repairs/test_websocket_api.py
iluvdata Mar 7, 2026
453c862
suggested edits
iluvdata Mar 7, 2026
9239847
suggested fixes
iluvdata Mar 8, 2026
aba15cb
Move FlowType to NextFlowType
iluvdata Mar 9, 2026
dd23f75
Move FlowType to NextFlowType
iluvdata Mar 9, 2026
55a1edd
Merge branch 'home-assistant:dev' into repair-flow-next-flow
iluvdata Mar 9, 2026
4af986d
Merge branch 'home-assistant:dev' into repair-flow-next-flow
iluvdata Apr 1, 2026
5ddcb7c
Address mypy error, don't delete issues with next_flow
iluvdata Apr 3, 2026
0eeb6ab
change tests for repairs with next_flows (issues remain in registry)
iluvdata Apr 3, 2026
8454a60
Merge remote-tracking branch 'upstream/dev' into repair-flow-next-flow
iluvdata Apr 23, 2026
4f1da34
Add repair flows that trigger other different repair flows via next_flow
iluvdata Apr 27, 2026
0d65b9a
Add next_flow specificying repair_flow as the next_flow in repair flows
iluvdata Apr 29, 2026
2cb8163
Move all relevant methods to issue_handler.py and various grammar upd…
iluvdata Apr 29, 2026
ed18a35
Minor copilot review code changes
iluvdata Apr 29, 2026
3441f50
Merge branch 'dev' of https://github.com/home-assistant/core into rep…
iluvdata May 6, 2026
41cf6a4
Resolve merge conflicts
iluvdata May 6, 2026
2d43119
add copilot review changes
iluvdata May 6, 2026
72744d0
minor updates (revised tests)
iluvdata May 6, 2026
1a7d273
Merge branch 'dev' into repair-flow-next-flow
iluvdata May 11, 2026
7e45d2e
Merge branch 'home-assistant:dev' into repair-flow-next-flow
iluvdata May 18, 2026
67496ea
Add UnknownIssue Error, Fix Generic FlowResult Type in _BaseFlowManag…
iluvdata May 18, 2026
88258be
Remove setting flow.handler as overwritten anyway in super().async_in…
iluvdata May 18, 2026
2e03884
Add copilot edits
iluvdata May 18, 2026
b6100bf
Merge branch 'dev' of https://github.com/home-assistant/core into rep…
iluvdata May 19, 2026
fc1e169
Revert anthropic patch
iluvdata May 19, 2026
dba8ba4
comment clarification based on copilot review
iluvdata May 19, 2026
66657a2
Potential fix for pull request finding
iluvdata May 19, 2026
35be68a
ruff format error fix
iluvdata May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions homeassistant/components/repairs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,40 @@
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType

from . import issue_handler, websocket_api
from .const import DOMAIN
from .issue_handler import ConfirmRepairFlow, RepairsFlowManager
from .models import RepairsFlow, RepairsFlowResult
from . import websocket_api
from .const import DOMAIN, FlowType
from .issue_handler import (
ConfirmRepairFlow,
RepairsFlow,
RepairsFlowContext,
RepairsFlowManager,
RepairsFlowResult,
UnknownIssue,
async_get,
async_setup as async_setup_issue_handler,
repairs_flow_manager,
)

__all__ = [
"DOMAIN",
"ConfirmRepairFlow",
"FlowType",
"RepairsFlow",
"RepairsFlowContext",
"RepairsFlowManager",
"RepairsFlowResult",
"UnknownIssue",
"async_get",
"repairs_flow_manager",
]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)


def repairs_flow_manager(hass: HomeAssistant) -> RepairsFlowManager | None:
"""Return the repairs flow manager."""
if (domain_data := hass.data.get(DOMAIN)) is None:
return None

flow_manager: RepairsFlowManager | None = domain_data.get("flow_manager")
return flow_manager


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Repairs."""
hass.data[DOMAIN] = {}

issue_handler.async_setup(hass)
async_setup_issue_handler(hass)
websocket_api.async_setup(hass)

return True
11 changes: 11 additions & 0 deletions homeassistant/components/repairs/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
"""Constants for the Repairs integration."""

from enum import StrEnum

DOMAIN = "repairs"


class FlowType(StrEnum):
"""FlowType to support additional next flow types for repairs."""

CONFIG_FLOW = "config_flow"
OPTIONS_FLOW = "options_flow"
CONFIG_SUBENTRIES_FLOW = "config_subentries_flow"
Comment thread
iluvdata marked this conversation as resolved.
REPAIRS_FLOW = "repairs_flow"
214 changes: 198 additions & 16 deletions homeassistant/components/repairs/issue_handler.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,149 @@
"""The repairs integration."""

from typing import Any
from collections.abc import Mapping
from typing import Any, Protocol

import voluptuous as vol

from homeassistant import data_entry_flow
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlowResult,
SubentryFlowResult,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.integration_platform import (
async_process_integration_platforms,
)

from .const import DOMAIN
from .models import RepairsFlow, RepairsFlowResult, RepairsProtocol
from .const import DOMAIN, FlowType


class UnknownIssue(data_entry_flow.FlowError):
"""Error for issue not found in registry or issue_id not provided."""


class RepairsFlowContext(data_entry_flow.FlowContext, total=False):
"""Typed context dict for repair flow."""

issue_id: str


class RepairsFlowResult(
data_entry_flow.FlowResult[RepairsFlowContext, str],
total=False,
):
"""Typed context dict for repairs flow."""

next_flow: tuple[FlowType, str]
# Frontend needs these to render the dialog:
result: ConfigEntry | ir.IssueEntry


class RepairsFlow(
data_entry_flow.FlowHandler[RepairsFlowContext, RepairsFlowResult, str],
):
"""Handle a flow for fixing an issue."""

data: dict[str, str | int | float | None] | None

@property
def issue_id(self) -> str:
"""Return the issue_id."""
return self.context["issue_id"]

@issue_id.setter
def issue_id(self, issue_id: str) -> None:
"""Warn that setting the issue_id directly is useless.

Even prior to changing the platform to pass issue_id via the RepairFlowContext,
the RepairFlowManager would overwrite any attempt of any integration implementing RepairFlow
setting issue_id in __init__().
"""
report_usage(
"sets issue_id directly in a RepairFlow using self.issue_id which is ignored by the repairs "
"platform and overridden by the RepairsFlowManager",
core_behavior=ReportBehavior.LOG,
integration_domain=self.handler,
Comment thread
iluvdata marked this conversation as resolved.
Comment thread
iluvdata marked this conversation as resolved.
)

@callback
def async_create_entry(
self,
*,
title: str | None = None,
data: Mapping[str, Any],
description: str | None = None,
description_placeholders: Mapping[str, str] | None = None,
next_flow: tuple[FlowType, str] | None = None,
) -> RepairsFlowResult:
"""Create an entry (fix a flow)."""
result: RepairsFlowResult = super().async_create_entry(
title=title,
data=data,
description=description,
description_placeholders=description_placeholders,
)

self._async_set_next_flow_if_valid(result, next_flow)

return result

@callback
def async_abort(
self,
*,
reason: str,
description_placeholders: Mapping[str, str] | None = None,
next_flow: tuple[FlowType, str] | None = None,
) -> RepairsFlowResult:
"""Abort the flow (leave the issue unrepaired)."""
result: RepairsFlowResult = super().async_abort(
reason=reason, description_placeholders=description_placeholders
)

self._async_set_next_flow_if_valid(result, next_flow)

return result

def _async_set_next_flow_if_valid(
self,
result: RepairsFlowResult,
next_flow: tuple[FlowType, str] | None,
) -> None:
"""Validate and set next_flow in result if provided."""
if next_flow is None:
return
flow_type, flow_id = next_flow
if flow_type not in FlowType:
raise data_entry_flow.UnknownFlow("Invalid next_flow type")
Comment thread
iluvdata marked this conversation as resolved.
entry_id: str | None = None
if flow_type == FlowType.CONFIG_FLOW:
config_flow: ConfigFlowResult = self.hass.config_entries.flow.async_get(
flow_id
)
entry_id = config_flow["context"].get("entry_id")
elif flow_type == FlowType.CONFIG_SUBENTRIES_FLOW:
subentry_flow: SubentryFlowResult = (
self.hass.config_entries.subentries.async_get(flow_id)
)
entry_id, _ = subentry_flow["handler"]
elif flow_type == FlowType.OPTIONS_FLOW:
config_flow = self.hass.config_entries.options.async_get(flow_id)
entry_id = config_flow["handler"]
else: # FlowType.REPAIRS_FLOW
repair_flow: RepairsFlowResult = async_get(self.hass).async_get(flow_id)
issue_registry: ir.IssueRegistry = ir.async_get(self.hass)
if issue := issue_registry.async_get_issue(
repair_flow["handler"], repair_flow["context"]["issue_id"]
):
result["result"] = issue
if entry_id is not None:
result["result"] = self.hass.config_entries.async_get_known_entry(entry_id)
result["next_flow"] = next_flow
Comment thread
iluvdata marked this conversation as resolved.
Comment thread
iluvdata marked this conversation as resolved.


class ConfirmRepairFlow(RepairsFlow):
Expand Down Expand Up @@ -44,26 +174,59 @@ async def async_step_confirm(
)


class RepairsProtocol(Protocol):
"""Define the format of repairs platforms."""

async def async_create_fix_flow(
self,
hass: HomeAssistant,
issue_id: str,
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create a flow to fix a fixable issue."""


class RepairsFlowManager(
data_entry_flow.FlowManager[data_entry_flow.FlowContext, RepairsFlowResult, str]
data_entry_flow.FlowManager[RepairsFlowContext, RepairsFlowResult, str]
):
"""Manage repairs flows."""

async def async_init(
self,
handler: str,
*,
context: RepairsFlowContext | None = None,
data: dict[str, Any] | None = None,
) -> RepairsFlowResult:
"""Start a RepairFlow."""
context = context or {}
if "issue_id" not in context:
if data is None or "issue_id" not in data:
raise UnknownIssue("issue_id missing, cannot create flow")
context["issue_id"] = data["issue_id"]
report_usage(
'created a repair flow using data={"issue_id": <issue_id>} '
"instead of context=RepairsFlowContext(issue_id=<issue_id>)"
'or context={"issue_id": <issue_id>}',
integration_domain=handler,
core_behavior=ReportBehavior.LOG,
)
Comment thread
iluvdata marked this conversation as resolved.
return await super().async_init(handler, context=context)

async def async_create_flow(
self,
handler_key: str,
*,
context: data_entry_flow.FlowContext | None = None,
context: RepairsFlowContext | None = None,
data: dict[str, Any] | None = None,
) -> RepairsFlow:
"""Create a flow. platform is a repairs module."""
assert data and "issue_id" in data
issue_id = data["issue_id"]
assert context and "issue_id" in context

issue_registry = ir.async_get(self.hass)
issue = issue_registry.async_get_issue(handler_key, issue_id)
issue = issue_registry.async_get_issue(handler_key, context["issue_id"])
if issue is None or not issue.is_fixable:
raise data_entry_flow.UnknownStep
raise UnknownIssue("Fixable issue not found in registry")

if "platforms" not in self.hass.data[DOMAIN]:
await async_process_repairs_platforms(self.hass)
Expand All @@ -73,17 +236,16 @@ async def async_create_flow(
flow: RepairsFlow = ConfirmRepairFlow()
else:
platform = platforms[handler_key]
flow = await platform.async_create_fix_flow(self.hass, issue_id, issue.data)
flow = await platform.async_create_fix_flow(
self.hass, context["issue_id"], issue.data
)

flow.issue_id = issue_id
flow.data = issue.data
return flow

async def async_finish_flow(
self,
flow: data_entry_flow.FlowHandler[
data_entry_flow.FlowContext, RepairsFlowResult, str
],
flow: data_entry_flow.FlowHandler[RepairsFlowContext, RepairsFlowResult, str],
result: RepairsFlowResult,
) -> RepairsFlowResult:
"""Complete a fix flow.
Expand All @@ -92,10 +254,30 @@ async def async_finish_flow(
FlowResultType.CREATE_ENTRY.
"""
if result.get("type") != data_entry_flow.FlowResultType.ABORT:
ir.async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"])
ir.async_delete_issue(self.hass, flow.handler, flow.context["issue_id"])
return result


@callback
def repairs_flow_manager(hass: HomeAssistant) -> RepairsFlowManager | None:
"""Get the repairs flow manager."""
if (domain_data := hass.data.get(DOMAIN)) is None:
return None
flow_manager: RepairsFlowManager | None = domain_data.get("flow_manager")
return flow_manager


@callback
def async_get(hass: HomeAssistant) -> RepairsFlowManager:
"""Get the repairs flow manager.

Preferred over repairs_flow_manager.
"""
if (flow_manager := repairs_flow_manager(hass)) is None:
raise PlatformNotReady("Repairs platform not loaded")
return flow_manager


@callback
def async_setup(hass: HomeAssistant) -> None:
"""Initialize repairs."""
Expand Down
30 changes: 0 additions & 30 deletions homeassistant/components/repairs/models.py

This file was deleted.

Loading
Loading