diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index eda980997139d..766343aa4530c 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__) @@ -185,6 +189,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. @@ -196,6 +201,19 @@ def report_usage( 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, + integration, + core_integration_behavior=core_integration_behavior, + custom_integration_behavior=custom_integration_behavior, + exclude_integrations=exclude_integrations, + level=level, + ) + return msg = f"Detected code that {what}. Please report this issue." if core_behavior is ReportBehavior.ERROR: raise RuntimeError(msg) from err @@ -208,12 +226,57 @@ def report_usage( integration_behavior = custom_integration_behavior if integration_behavior is not ReportBehavior.IGNORE: - _report_integration( + _report_integration_frame( what, integration_frame, level, integration_behavior is ReportBehavior.ERROR ) -def _report_integration( +def _report_integration_domain( + hass: HomeAssistant | None, + what: str, + integration: Integration, + *, + core_integration_behavior: ReportBehavior, + custom_integration_behavior: ReportBehavior, + exclude_integrations: set[str] | None, + level: int, +) -> None: + integration_behavior = core_integration_behavior + if integration.is_built_in: + integration_behavior = custom_integration_behavior + + if integration_behavior is ReportBehavior.IGNORE or ( + exclude_integrations and integration.domain in exclude_integrations + ): + 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, please %s", + integration_type, + integration.domain, + what, + 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, integration_frame: IntegrationFrame, level: int = logging.WARNING, diff --git a/homeassistant/loader.py b/homeassistant/loader.py index d2e04df04c4b0..e63de0c80262a 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1685,6 +1685,29 @@ def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool: return module in hass.data[DATA_COMPONENTS] +@callback +def async_get_issue_integration( + hass: HomeAssistant | None, + integration_domain: str | None, +) -> Integration | None: + """Return details of an integration for issue reporting.""" + integration: Integration | None = None + if not hass or not integration_domain: + # We are unable to get the integration + return None + + if (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS)) and not isinstance( + comps_or_future, asyncio.Future + ): + integration = comps_or_future.get(integration_domain) + + if not integration: + with suppress(IntegrationNotLoaded): + integration = async_get_loaded_integration(hass, integration_domain) + + return integration + + @callback def async_get_issue_tracker( hass: HomeAssistant | None, @@ -1698,20 +1721,11 @@ def async_get_issue_tracker( "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" ) if not integration and not integration_domain and not module: - # If we know nothing about the entity, suggest opening an issue on HA core + # If we know nothing about the integration, suggest opening an issue on HA core return issue_tracker - if ( - not integration - and (hass and integration_domain) - and (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS)) - and not isinstance(comps_or_future, asyncio.Future) - ): - integration = comps_or_future.get(integration_domain) - - if not integration and (hass and integration_domain): - with suppress(IntegrationNotLoaded): - integration = async_get_loaded_integration(hass, integration_domain) + if not integration: + integration = async_get_issue_integration(hass, integration_domain) if integration and not integration.is_built_in: return integration.issue_tracker diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index a2a4890810b83..df47e24e2388d 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 Integration from tests.common import extract_stack_to_frame @@ -423,3 +424,49 @@ async def test_report( assert errored == expected_error assert caplog.text.count(what) == expected_log + + +@pytest.mark.parametrize( + ("integration", "integration_domain", "source"), + [ + pytest.param( + None, + None, + "code that", + id="core", + ), + pytest.param( + None, + "sensor", + "that integration 'sensor'", + id="core integration", + ), + pytest.param( + None, + "hue", + "that custom integration 'hue'", + id="custom integration", + ), + ], +) +async def test_report_integration_domain( + caplog: pytest.LogCaptureFixture, + integration: Integration | None, + integration_domain: str | None, + source: str, +) -> None: + """Test report.""" + what = "test_report_string" + + with ( + patch.object(frame, "_REPORTED_INTEGRATIONS", set()), + patch("homeassistant.loader.async_get_issue_integration", integration), + ): + frame.report_usage( + what, + core_behavior=frame.ReportBehavior.LOG, + exclude_integrations={"mobile_app"}, + integration_domain=integration_domain, + ) + + assert f"Detected {source} {what}" in caplog.text