diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 56931fe289dac6..58d9c9c728f32a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2887,18 +2887,12 @@ def async_create_entry( # type: ignore[override] ) -> ConfigFlowResult: """Finish config flow and create a config entry.""" if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}: - report_issue = async_suggest_report_issue( - self.hass, integration_domain=self.handler - ) - _LOGGER.warning( - ( - "Detected %s config flow creating a new entry, " - "when it is expected to update an existing entry and abort. " - "This will stop working in %s, please %s" - ), - self.source, - "2025.11", - report_issue, + report_usage( + f"creates a new entry in a '{self.source}' flow, " + "when it is expected to update an existing entry and abort", + core_behavior=ReportBehavior.LOG, + breaks_in_ha_version="2025.11", + integration_domain=self.handler, ) result = super().async_create_entry( title=title, diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 3ebe6fdba295c4..6d03ae4ffd2755 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -15,9 +15,13 @@ from propcache import cached_property -from homeassistant.core import async_get_hass_or_none +from homeassistant.core import HomeAssistant, async_get_hass_or_none from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_suggest_report_issue +from homeassistant.loader import ( + Integration, + async_get_issue_integration, + async_suggest_report_issue, +) _LOGGER = logging.getLogger(__name__) @@ -186,6 +190,7 @@ def report_usage( core_integration_behavior: ReportBehavior = ReportBehavior.LOG, custom_integration_behavior: ReportBehavior = ReportBehavior.LOG, exclude_integrations: set[str] | None = None, + integration_domain: str | None = None, level: int = logging.WARNING, ) -> None: """Report incorrect code usage. @@ -194,12 +199,29 @@ def report_usage( Please create a bug report at https://..." :param breaks_in_ha_version: if set, the report will be adjusted to specify the breaking version + :param exclude_integrations: skip specified integration when reviewing the stack. + If no integration is found, the core behavior will be applied + :param integration_domain: fallback for identifying the integration if the + frame is not found """ try: integration_frame = get_integration_frame( exclude_integrations=exclude_integrations ) except MissingIntegrationFrame as err: + if integration := async_get_issue_integration( + hass := async_get_hass_or_none(), integration_domain + ): + _report_integration_domain( + hass, + what, + breaks_in_ha_version, + integration, + core_integration_behavior, + custom_integration_behavior, + level, + ) + return msg = f"Detected code that {what}. Please report this issue" if core_behavior is ReportBehavior.ERROR: raise RuntimeError(msg) from err @@ -217,7 +239,7 @@ def report_usage( integration_behavior = custom_integration_behavior if integration_behavior is not ReportBehavior.IGNORE: - _report_integration( + _report_integration_frame( what, breaks_in_ha_version, integration_frame, @@ -226,14 +248,64 @@ def report_usage( ) -def _report_integration( +def _report_integration_domain( + hass: HomeAssistant | None, + what: str, + breaks_in_ha_version: str | None, + integration: Integration, + core_integration_behavior: ReportBehavior, + custom_integration_behavior: ReportBehavior, + level: int, +) -> None: + """Report incorrect usage in an integration (identified via domain). + + Async friendly. + """ + integration_behavior = core_integration_behavior + if not integration.is_built_in: + integration_behavior = custom_integration_behavior + + if integration_behavior is ReportBehavior.IGNORE: + return + + # Keep track of integrations already reported to prevent flooding + key = f"{integration.domain}:{what}" + if ( + integration_behavior is not ReportBehavior.ERROR + and key in _REPORTED_INTEGRATIONS + ): + return + _REPORTED_INTEGRATIONS.add(key) + + report_issue = async_suggest_report_issue(hass, integration=integration) + integration_type = "" if integration.is_built_in else "custom " + _LOGGER.log( + level, + "Detected that %sintegration '%s' %s. %s %s", + integration_type, + integration.domain, + what, + f"This will stop working in Home Assistant {breaks_in_ha_version}, please" + if breaks_in_ha_version + else "Please", + report_issue, + ) + + if integration_behavior is ReportBehavior.ERROR: + raise RuntimeError( + f"Detected that {integration_type}integration " + f"'{integration.domain}' {what}. Please {report_issue}" + ) + + +def _report_integration_frame( what: str, breaks_in_ha_version: str | None, integration_frame: IntegrationFrame, level: int = logging.WARNING, error: bool = False, ) -> None: - """Report incorrect usage in an integration. + """Report incorrect usage in an integration (identified via frame). Async friendly. """ diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 1c1dc8eb71eec0..fb98111fd42412 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import frame +from homeassistant.loader import async_get_integration from tests.common import extract_stack_to_frame @@ -445,3 +446,89 @@ async def test_report( assert errored == expected_error assert caplog.text.count(what) == expected_log + + +@pytest.mark.parametrize( + ("behavior", "integration_domain", "source", "logs_again"), + [ + pytest.param( + "core_behavior", + None, + "code that", + True, + id="core", + ), + pytest.param( + "core_behavior", + "unknown_integration", + "code that", + True, + id="unknown integration", + ), + pytest.param( + "core_integration_behavior", + "sensor", + "that integration 'sensor'", + False, + id="core integration", + ), + pytest.param( + "custom_integration_behavior", + "test_package", + "that custom integration 'test_package'", + False, + id="custom integration", + ), + ], +) +@pytest.mark.usefixtures("enable_custom_integrations") +async def test_report_integration_domain( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + behavior: str, + integration_domain: str | None, + source: str, + logs_again: bool, +) -> None: + """Test report.""" + await async_get_integration(hass, "sensor") + await async_get_integration(hass, "test_package") + + what = "test_report_string" + lookup_text = f"Detected {source} {what}" + + caplog.clear() + frame.report_usage( + what, + **{behavior: frame.ReportBehavior.IGNORE}, + integration_domain=integration_domain, + ) + + assert lookup_text not in caplog.text + + with patch.object(frame, "_REPORTED_INTEGRATIONS", set()): + frame.report_usage( + what, + **{behavior: frame.ReportBehavior.LOG}, + integration_domain=integration_domain, + ) + + assert lookup_text in caplog.text + + # Check that it does not log again + caplog.clear() + frame.report_usage( + what, + **{behavior: frame.ReportBehavior.LOG}, + integration_domain=integration_domain, + ) + + assert (lookup_text in caplog.text) == logs_again + + # Check that it raises + with pytest.raises(RuntimeError, match=lookup_text): + frame.report_usage( + what, + **{behavior: frame.ReportBehavior.ERROR}, + integration_domain=integration_domain, + ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 44f741242aa834..4fad1a32b43d4d 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7157,7 +7157,10 @@ async def _async_step_confirm(self): assert len(hass.config_entries.async_entries("test")) == 1 - with mock_config_flow("test", TestFlow): + with ( + mock_config_flow("test", TestFlow), + patch.object(frame, "_REPORTED_INTEGRATIONS", set()), + ): result = await getattr(entry, f"start_{source}_flow")(hass) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY @@ -7169,10 +7172,10 @@ async def _async_step_confirm(self): assert entries[0].entry_id != entry.entry_id assert ( - f"Detected {source} config flow creating a new entry, when it is expected " - "to update an existing entry and abort. This will stop working in " - "2025.11, please create a bug report at https://github.com/home" - "-assistant/core/issues?q=is%3Aopen+is%3Aissue+" + f"Detected that integration 'test' creates a new entry in a '{source}' flow, " + "when it is expected to update an existing entry and abort. This will stop " + "working in Home Assistant 2025.11, please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+" "label%3A%22integration%3A+test%22" ) in caplog.text