Deprecate modbus get_hub in favour of async_get_unit (#179933)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Balloob Bot
2026-09-02 13:57:03 -04:00
committed by GitHub
co-authored by Paulus Schoutsen Martin Hjelmare Copilot Autofix powered by AI
parent d0ebb31684
commit f04652b324
2 changed files with 111 additions and 1 deletions
+20 -1
View File
@@ -5,6 +5,7 @@ import logging
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import Event, HomeAssistant, ServiceCall
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.typing import ConfigType
@@ -26,7 +27,25 @@ _LOGGER = logging.getLogger(__name__)
def get_hub(hass: HomeAssistant, name: str) -> ModbusHub:
"""Return modbus hub with name."""
"""Return modbus hub with name.
Deprecated. Use `async_get_unit` instead, which builds a connection from
credentials the integration holds rather than attaching to a hub the user
configured in YAML under a name the integration has to be told.
"""
report_usage(
"calls `modbus.get_hub`, which is deprecated in favour of "
"`modbus.async_get_unit`. Collect the connection details in your own "
"config flow and ask for a unit on them",
breaks_in_ha_version="2027.10",
core_behavior=ReportBehavior.IGNORE,
core_integration_behavior=ReportBehavior.IGNORE,
custom_integration_behavior=ReportBehavior.LOG,
# get_hub is defined here, so its own frame is the first one the stack
# walk meets. Without this it reports modbus every time, and the core
# behavior above then silences the caller it was meant to name.
exclude_integrations={DOMAIN},
)
return hass.data[DATA_MODBUS_HUBS][name]
@@ -0,0 +1,91 @@
"""Test the deprecation of get_hub."""
import importlib.util
from pathlib import Path
import sys
from types import ModuleType
import pytest
from homeassistant.components.modbus import get_hub
from homeassistant.components.modbus.modbus import DATA_MODBUS_HUBS
from homeassistant.core import HomeAssistant
def _caller_in(root: Path, path: str) -> ModuleType:
"""Load a module that calls get_hub, from a file at *path*.
The report names an integration by walking the stack for a file under
`custom_components/` or `homeassistant/components/`, so a test of who gets
named has to call from a file that actually sits there.
"""
source = root / path / "caller.py"
source.parent.mkdir(parents=True, exist_ok=True)
source.write_text(
"from homeassistant.components.modbus import get_hub\n"
"\n"
"def call(hass):\n"
' return get_hub(hass, "hub")\n'
)
spec = importlib.util.spec_from_file_location(
f"caller_{source.parent.name}", source
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
@pytest.fixture(name="hub")
def hub_fixture(hass: HomeAssistant) -> object:
"""Put one hub in place for get_hub to return."""
hub = object()
hass.data[DATA_MODBUS_HUBS] = {"hub": hub}
return hub
async def test_a_custom_integration_is_warned(
hass: HomeAssistant,
hub: object,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A custom integration has its own config flow to collect details in.
Called through the real stack, so this also covers the report reaching
past modbus's own frame to the caller that asked.
"""
caller = _caller_in(tmp_path, "custom_components/my_integration")
assert caller.call(hass) is hub
assert "deprecated" in caplog.text
assert "async_get_unit" in caplog.text
assert "my_integration" in caplog.text
assert "2027.10" in caplog.text
assert "modbus" not in caplog.text.split("my_integration")[0]
async def test_a_core_integration_is_not_warned(
hass: HomeAssistant,
hub: object,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Flexit is the one caller in core, and the user cannot act on it."""
caller = _caller_in(tmp_path, "homeassistant/components/flexit")
assert caller.call(hass) is hub
assert "deprecated" not in caplog.text
async def test_a_caller_outside_any_integration_is_not_warned(
hass: HomeAssistant, hub: object, caplog: pytest.LogCaptureFixture
) -> None:
"""Nothing to name and nobody to tell, so it stays quiet."""
assert get_hub(hass, "hub") is hub
assert "deprecated" not in caplog.text