mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Raise error on deprecated Libre Hardware Monitor version (#181928)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user