From 458b11ee9b260a7e63ab0fd365f41e9f261ebcea Mon Sep 17 00:00:00 2001 From: Sab44 <64696149+Sab44@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:26:22 +0200 Subject: [PATCH] Raise error on deprecated Libre Hardware Monitor version (#181928) --- .../libre_hardware_monitor/__init__.py | 21 +------ .../libre_hardware_monitor/config_flow.py | 47 +++++++++------- .../libre_hardware_monitor/coordinator.py | 21 +++---- .../libre_hardware_monitor/strings.json | 11 ++-- .../libre_hardware_monitor/conftest.py | 10 ++++ .../test_config_flow.py | 56 +++++++++++++++++++ .../libre_hardware_monitor/test_init.py | 49 +++++++++++++++- .../libre_hardware_monitor/test_sensor.py | 56 +------------------ 8 files changed, 161 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/libre_hardware_monitor/__init__.py b/homeassistant/components/libre_hardware_monitor/__init__.py index dae6a54a6951..f3270a6ec5ab 100644 --- a/homeassistant/components/libre_hardware_monitor/__init__.py +++ b/homeassistant/components/libre_hardware_monitor/__init__.py @@ -4,11 +4,7 @@ import logging from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - issue_registry as ir, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .coordinator import ( @@ -82,21 +78,6 @@ async def async_setup_entry( lhm_coordinator = LibreHardwareMonitorCoordinator(hass, config_entry) await lhm_coordinator.async_config_entry_first_refresh() - if lhm_coordinator.data.is_deprecated_version: - issue_id = f"deprecated_api_{config_entry.entry_id}" - ir.async_create_issue( - hass, - DOMAIN, - issue_id, - breaks_in_ha_version="2026.9.0", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="deprecated_api", - translation_placeholders={ - "lhm_releases_url": "https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases" - }, - ) - config_entry.runtime_data = lhm_coordinator await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/homeassistant/components/libre_hardware_monitor/config_flow.py b/homeassistant/components/libre_hardware_monitor/config_flow.py index f9f525e706d2..e59762d1b1e6 100644 --- a/homeassistant/components/libre_hardware_monitor/config_flow.py +++ b/homeassistant/components/libre_hardware_monitor/config_flow.py @@ -10,6 +10,7 @@ from librehardwaremonitor_api import ( LibreHardwareMonitorNoDevicesError, LibreHardwareMonitorUnauthorizedError, ) +from librehardwaremonitor_api.model import LibreHardwareMonitorData import voluptuous as vol from homeassistant.config_entries import ( @@ -39,7 +40,9 @@ REAUTH_SCHEMA = vol.Schema( ) -async def _validate_connection(user_input: dict[str, Any]) -> str: +async def _validate_connection( + user_input: dict[str, Any], +) -> LibreHardwareMonitorData: """Ensure a connection can be established.""" api = LibreHardwareMonitorClient( host=user_input[CONF_HOST], @@ -48,7 +51,7 @@ async def _validate_connection(user_input: dict[str, Any]) -> str: password=user_input.get(CONF_PASSWORD), ) - return (await api.get_data()).computer_name + return await api.get_data() class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): @@ -73,7 +76,7 @@ class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match(user_input) try: - computer_name = await _validate_connection(user_input) + lhm_data = await _validate_connection(user_input) except LibreHardwareMonitorConnectionError as exception: _LOGGER.error(exception) errors["base"] = "cannot_connect" @@ -84,14 +87,17 @@ class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): except LibreHardwareMonitorNoDevicesError: errors["base"] = "no_devices" else: - return self.async_create_entry( - title=( - f"{computer_name}" - f" ({user_input[CONF_HOST]}" - f":{user_input[CONF_PORT]})" - ), - data=user_input, - ) + if lhm_data.is_deprecated_version: + errors["base"] = "deprecated_version" + else: + return self.async_create_entry( + title=( + f"{lhm_data.computer_name}" + f" ({user_input[CONF_HOST]}" + f":{user_input[CONF_PORT]})" + ), + data=user_input, + ) return self.async_show_form( step_id="user", @@ -123,7 +129,7 @@ class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, } try: - computer_name = await _validate_connection(data) + lhm_data = await _validate_connection(data) except LibreHardwareMonitorConnectionError as exception: _LOGGER.error(exception) errors["base"] = "cannot_connect" @@ -132,17 +138,20 @@ class LibreHardwareMonitorConfigFlow(ConfigFlow, domain=DOMAIN): except LibreHardwareMonitorNoDevicesError: errors["base"] = "no_devices" else: - if self.source == SOURCE_REAUTH: + if lhm_data.is_deprecated_version: + errors["base"] = "deprecated_version" + elif self.source == SOURCE_REAUTH: return self.async_update_reload_and_abort( entry=reauth_entry, # type: ignore[arg-type] data_updates=user_input, ) - # the initial connection was unauthorized, - # now we can create the config entry - return self.async_create_entry( - title=f"{computer_name} ({self._host}:{self._port})", - data=data, - ) + else: + # the initial connection was unauthorized, + # now we can create the config entry + return self.async_create_entry( + title=f"{lhm_data.computer_name} ({self._host}:{self._port})", + data=data, + ) return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/libre_hardware_monitor/coordinator.py b/homeassistant/components/libre_hardware_monitor/coordinator.py index 4f74e94138ed..f024d0413ad2 100644 --- a/homeassistant/components/libre_hardware_monitor/coordinator.py +++ b/homeassistant/components/libre_hardware_monitor/coordinator.py @@ -16,11 +16,11 @@ from librehardwaremonitor_api.model import ( LibreHardwareMonitorData, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers import device_registry as dr, issue_registry as ir +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -67,7 +67,6 @@ class LibreHardwareMonitorCoordinator(DataUpdateCoordinator[LibreHardwareMonitor for device in device_entries if device.identifiers and device.name } - self._is_deprecated_version: bool | None = None @override async def _async_update_data(self) -> LibreHardwareMonitorData: @@ -83,12 +82,14 @@ class LibreHardwareMonitorCoordinator(DataUpdateCoordinator[LibreHardwareMonitor except LibreHardwareMonitorNoDevicesError as err: raise UpdateFailed("No sensor data available, will retry") from err - # Check whether user has upgraded LHM from a deprecated - # version while the integration is running - if self._is_deprecated_version and not lhm_data.is_deprecated_version: - # Clear deprecation issue - ir.async_delete_issue(self.hass, DOMAIN, f"deprecated_api_{self._entry_id}") - self._is_deprecated_version = lhm_data.is_deprecated_version + if lhm_data.is_deprecated_version: + if self.config_entry.state is ConfigEntryState.LOADED: + # if user downgrades while HA is running, reload integration to surface ConfigEntryError + self.hass.config_entries.async_schedule_reload(self._entry_id) + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="deprecated_version", + ) await self._async_handle_changes_in_devices( dict(lhm_data.main_device_ids_and_names) diff --git a/homeassistant/components/libre_hardware_monitor/strings.json b/homeassistant/components/libre_hardware_monitor/strings.json index b6a35f6d3c42..760a9dce86ba 100644 --- a/homeassistant/components/libre_hardware_monitor/strings.json +++ b/homeassistant/components/libre_hardware_monitor/strings.json @@ -1,10 +1,14 @@ { + "common": { + "deprecated_version": "Your version of Libre Hardware Monitor is no longer supported. Please update to version 0.9.5 or later." + }, "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "deprecated_version": "[%key:component::libre_hardware_monitor::common::deprecated_version%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "no_devices": "[%key:common::config_flow::abort::no_devices_found%]" }, @@ -33,10 +37,9 @@ } } }, - "issues": { - "deprecated_api": { - "description": "Your version of Libre Hardware Monitor is deprecated and may not provide stable sensor data. To fix this issue:\n\n1. Download version 0.9.5 or later from {lhm_releases_url}\n2. Close Libre Hardware Monitor on your computer\n3. Install or extract the new version and start Libre Hardware Monitor again (you might have to re-enable the remote web server)\n4. Home Assistant will detect the new version and this issue will clear automatically", - "title": "Deprecated Libre Hardware Monitor version" + "exceptions": { + "deprecated_version": { + "message": "[%key:component::libre_hardware_monitor::common::deprecated_version%]" } } } diff --git a/tests/components/libre_hardware_monitor/conftest.py b/tests/components/libre_hardware_monitor/conftest.py index 882595e525b2..e168123f5a20 100644 --- a/tests/components/libre_hardware_monitor/conftest.py +++ b/tests/components/libre_hardware_monitor/conftest.py @@ -1,6 +1,7 @@ """Common fixtures for the LibreHardwareMonitor tests.""" from collections.abc import Generator +from dataclasses import replace from unittest.mock import AsyncMock, patch from librehardwaremonitor_api.parser import LibreHardwareMonitorParser @@ -84,3 +85,12 @@ def mock_lhm_client() -> Generator[AsyncMock]: client.get_data.return_value = test_data yield client + + +@pytest.fixture +def mock_deprecated_lhm_client(mock_lhm_client: AsyncMock) -> AsyncMock: + """Mock a LibreHardwareMonitor client reporting a deprecated version.""" + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, is_deprecated_version=True + ) + return mock_lhm_client diff --git a/tests/components/libre_hardware_monitor/test_config_flow.py b/tests/components/libre_hardware_monitor/test_config_flow.py index fd575deb2d6a..11e357eb4d0f 100644 --- a/tests/components/libre_hardware_monitor/test_config_flow.py +++ b/tests/components/libre_hardware_monitor/test_config_flow.py @@ -1,5 +1,6 @@ """Test the LibreHardwareMonitor config flow.""" +from dataclasses import replace from unittest.mock import AsyncMock from librehardwaremonitor_api import ( @@ -266,3 +267,58 @@ async def test_reauth_errors( assert result["reason"] == "reauth_successful" assert mock_config_entry.data == {**VALID_CONFIG, **REAUTH_INPUT} assert len(hass.config_entries.async_entries()) == 1 + + +async def test_deprecated_version_is_rejected_and_flow_recovery( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_lhm_client: AsyncMock, +) -> None: + """Test that a deprecated LHM version cannot be configured.""" + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, is_deprecated_version=True + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "deprecated_version"} + assert mock_setup_entry.call_count == 0 + + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, is_deprecated_version=False + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=VALID_CONFIG + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert mock_setup_entry.call_count == 1 + + +@pytest.mark.usefixtures("mock_deprecated_lhm_client") +async def test_reauth_deprecated_version_is_rejected( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that reauth does not complete for a deprecated LHM version.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], REAUTH_INPUT + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "deprecated_version"} + assert mock_config_entry.data == VALID_CONFIG diff --git a/tests/components/libre_hardware_monitor/test_init.py b/tests/components/libre_hardware_monitor/test_init.py index 3a83d6098c0b..385bb109acdc 100644 --- a/tests/components/libre_hardware_monitor/test_init.py +++ b/tests/components/libre_hardware_monitor/test_init.py @@ -1,15 +1,24 @@ """Tests for the LibreHardwareMonitor init.""" +from dataclasses import replace +from datetime import timedelta +from unittest.mock import AsyncMock + +from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.libre_hardware_monitor.const import DOMAIN +from homeassistant.components.libre_hardware_monitor.const import ( + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from . import init_integration from .conftest import VALID_CONFIG -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.mark.usefixtures("mock_lhm_client") @@ -93,3 +102,39 @@ async def test_migration_to_unique_ids( legacy_config_entry_v1.entry_id ) assert updated_config_entry.version == 2 + + +@pytest.mark.usefixtures("mock_deprecated_lhm_client") +async def test_deprecated_version_blocks_setup( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test that a deprecated LHM version prevents setup with an error.""" + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.error_reason_translation_domain == DOMAIN + assert mock_config_entry.error_reason_translation_key == "deprecated_version" + + +async def test_downgrade_to_deprecated_version_fails_entry( + hass: HomeAssistant, + mock_lhm_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that downgrading to a deprecated LHM version while running fails the entry.""" + await init_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_lhm_client.get_data.return_value = replace( + mock_lhm_client.get_data.return_value, is_deprecated_version=True + ) + + freezer.tick(timedelta(seconds=DEFAULT_SCAN_INTERVAL)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + assert mock_config_entry.error_reason_translation_domain == DOMAIN + assert mock_config_entry.error_reason_translation_key == "deprecated_version" diff --git a/tests/components/libre_hardware_monitor/test_sensor.py b/tests/components/libre_hardware_monitor/test_sensor.py index 17b43d461212..11ba0abe669c 100644 --- a/tests/components/libre_hardware_monitor/test_sensor.py +++ b/tests/components/libre_hardware_monitor/test_sensor.py @@ -27,11 +27,7 @@ from homeassistant.components.libre_hardware_monitor.const import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - issue_registry as ir, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry from . import init_integration @@ -346,53 +342,3 @@ async def test_integration_dynamically_adds_new_devices( assert "sensor.gaming_pc_generic_memory_test_sensor" in [ entry.entity_id for entry in entity_entries ] - - -async def test_non_deprecated_version_does_not_raise_issue( - hass: HomeAssistant, - mock_lhm_client: AsyncMock, - mock_config_entry: MockConfigEntry, - issue_registry: ir.IssueRegistry, -) -> None: - """Test non-deprecated LHM version does not raise an issue.""" - await init_integration(hass, mock_config_entry) - - assert ( - DOMAIN, - f"deprecated_api_{mock_config_entry.entry_id}", - ) not in issue_registry.issues - - -async def test_deprecated_version_raises_issue_and_is_removed_after_update( - hass: HomeAssistant, - mock_lhm_client: AsyncMock, - mock_config_entry: MockConfigEntry, - freezer: FrozenDateTimeFactory, - issue_registry: ir.IssueRegistry, -) -> None: - """Test deprecated LHM version raises issue removed after update.""" - mock_lhm_client.get_data.return_value = replace( - mock_lhm_client.get_data.return_value, - is_deprecated_version=True, - ) - - await init_integration(hass, mock_config_entry) - - assert ( - DOMAIN, - f"deprecated_api_{mock_config_entry.entry_id}", - ) in issue_registry.issues - - mock_lhm_client.get_data.return_value = replace( - mock_lhm_client.get_data.return_value, - is_deprecated_version=False, - ) - - freezer.tick(timedelta(DEFAULT_SCAN_INTERVAL)) - async_fire_time_changed(hass) - await hass.async_block_till_done() - - assert ( - DOMAIN, - f"deprecated_api_{mock_config_entry.entry_id}", - ) not in issue_registry.issues