From 25992526002edefdca8efdf18690fdc5b435a31a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 6 Mar 2024 09:47:21 +0000 Subject: [PATCH] Post System Bridge 4.x.x integration improvements (#112189) * Dont remove api key during migration * Fix return * Fix test * Make lambda more readable * Move fixtures to init, move migration test to test_init.py * Refactor config_entry data assignment * Refactor system_bridge migration tests * Fix type for debug message * Fix type for debug message * Remove duplicated unused code (rebase error) * Refactor test_migration_minor_2_to_1 to test_migration_minor_future_to_2 * Fix version check in async_migrate_entry * Update migration logic to handle future minor version * Add ConfigEntryState assertion in test_init.py * Change condition to minor_version < 2 Co-authored-by: Martin Hjelmare * Refactor system bridge migration tests * Remove minor downgrade code * Update tests/components/system_bridge/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/system_bridge/test_init.py Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Move dataclass to store requested data to data.py * Use dataclass in config flow * Move media player and sensor onto data.py dataclass * Move data and handler inside validate --------- Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 16 +- .../components/system_bridge/config_flow.py | 131 ++++----------- .../components/system_bridge/coordinator.py | 39 +---- .../components/system_bridge/data.py | 29 ++++ .../components/system_bridge/media_player.py | 5 +- .../components/system_bridge/sensor.py | 27 ++-- tests/components/system_bridge/__init__.py | 115 ++++++++++++++ .../system_bridge/test_config_flow.py | 149 ++---------------- tests/components/system_bridge/test_init.py | 83 ++++++++++ 9 files changed, 302 insertions(+), 292 deletions(-) create mode 100644 homeassistant/components/system_bridge/data.py create mode 100644 tests/components/system_bridge/test_init.py diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index a7541021e0d..3683834f184 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -45,6 +45,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from .config_flow import SystemBridgeConfigFlow from .const import DOMAIN, MODULES from .coordinator import SystemBridgeDataUpdateCoordinator @@ -358,13 +359,19 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" - _LOGGER.debug("Migrating from version %s", config_entry.version) + _LOGGER.debug( + "Migrating from version %s.%s", + config_entry.version, + config_entry.minor_version, + ) - if config_entry.version == 1 and config_entry.minor_version == 1: + if config_entry.version > SystemBridgeConfigFlow.VERSION: + return False + + if config_entry.minor_version < 2: # Migrate to CONF_TOKEN, which was added in 1.2 new_data = dict(config_entry.data) new_data.setdefault(CONF_TOKEN, config_entry.data.get(CONF_API_KEY)) - new_data.pop(CONF_API_KEY, None) hass.config_entries.async_update_entry( config_entry, @@ -378,5 +385,4 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry.minor_version, ) - # User is trying to downgrade from a future version - return False + return True diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 1d4f73799a1..0042d9c647e 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -12,18 +12,19 @@ from systembridgeconnector.exceptions import ( ConnectionErrorException, ) from systembridgeconnector.websocket_client import WebSocketClient -from systembridgemodels.modules import GetData, System +from systembridgemodels.modules import GetData import voluptuous as vol from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .data import SystemBridgeData _LOGGER = logging.getLogger(__name__) @@ -45,25 +46,40 @@ async def _validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - host = data[CONF_HOST] + + system_bridge_data = SystemBridgeData() + + async def _async_handle_module( + module_name: str, + module: Any, + ) -> None: + """Handle data from the WebSocket client.""" + _LOGGER.debug("Set new data for: %s", module_name) + setattr(system_bridge_data, module_name, module) websocket_client = WebSocketClient( - host, + data[CONF_HOST], data[CONF_PORT], - data[CONF_API_KEY], + data[CONF_TOKEN], ) + try: async with asyncio.timeout(15): await websocket_client.connect(session=async_get_clientsession(hass)) - hass.async_create_task(websocket_client.listen()) + hass.async_create_task( + websocket_client.listen(callback=_async_handle_module) + ) response = await websocket_client.get_data(GetData(modules=["system"])) - _LOGGER.debug("Got response: %s", response.json()) - if response.data is None or not isinstance(response.data, System): + _LOGGER.debug("Got response: %s", response) + if response is None: raise CannotConnect("No data received") - system: System = response.data + while system_bridge_data.system is None: + await asyncio.sleep(0.2) except AuthenticationException as exception: _LOGGER.warning( - "Authentication error when connecting to %s: %s", data[CONF_HOST], exception + "Authentication error when connecting to %s: %s", + data[CONF_HOST], + exception, ) raise InvalidAuth from exception except ( @@ -80,9 +96,9 @@ async def _validate_input( except ValueError as exception: raise CannotConnect from exception - _LOGGER.debug("Got System data: %s", system.json()) + _LOGGER.debug("Got System data: %s", system_bridge_data.system) - return {"hostname": host, "uuid": system.uuid} + return {"hostname": data[CONF_HOST], "uuid": system_bridge_data.system.uuid} async def _async_get_info( @@ -120,93 +136,6 @@ class SystemBridgeConfigFlow( self._name: str | None = None self._input: dict[str, Any] = {} self._reauth = False - self._system_data: System | None = None - - async def _validate_input( - self, - data: dict[str, Any], - ) -> dict[str, str]: - """Validate the user input allows us to connect. - - Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. - """ - host = data[CONF_HOST] - - websocket_client = WebSocketClient( - host, - data[CONF_PORT], - data[CONF_TOKEN], - ) - - async def async_handle_module( - module_name: str, - module: Any, - ) -> None: - """Handle data from the WebSocket client.""" - _LOGGER.debug("Set new data for: %s", module_name) - if module_name == "system": - self._system_data = module - - try: - async with asyncio.timeout(15): - await websocket_client.connect( - session=async_get_clientsession(self.hass) - ) - self.hass.async_create_task( - websocket_client.listen(callback=async_handle_module) - ) - response = await websocket_client.get_data(GetData(modules=["system"])) - _LOGGER.debug("Got response: %s", response) - if response is None: - raise CannotConnect("No data received") - while self._system_data is None: - await asyncio.sleep(0.2) - except AuthenticationException as exception: - _LOGGER.warning( - "Authentication error when connecting to %s: %s", - data[CONF_HOST], - exception, - ) - raise InvalidAuth from exception - except ( - ConnectionClosedException, - ConnectionErrorException, - ) as exception: - _LOGGER.warning( - "Connection error when connecting to %s: %s", data[CONF_HOST], exception - ) - raise CannotConnect from exception - except TimeoutError as exception: - _LOGGER.warning( - "Timed out connecting to %s: %s", data[CONF_HOST], exception - ) - raise CannotConnect from exception - except ValueError as exception: - raise CannotConnect from exception - - _LOGGER.debug("Got System data: %s", self._system_data) - - return {"hostname": host, "uuid": self._system_data.uuid} - - async def _async_get_info( - self, - user_input: dict[str, Any], - ) -> tuple[dict[str, str], dict[str, str] | None]: - errors = {} - - try: - info = await self._validate_input(user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - else: - return errors, info - - return errors, None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -217,7 +146,7 @@ class SystemBridgeConfigFlow( step_id="user", data_schema=STEP_USER_DATA_SCHEMA ) - errors, info = await self._async_get_info(user_input) + errors, info = await _async_get_info(self.hass, user_input) if not errors and info is not None: # Check if already configured await self.async_set_unique_id(info["uuid"], raise_on_progress=False) @@ -237,7 +166,7 @@ class SystemBridgeConfigFlow( if user_input is not None: user_input = {**self._input, **user_input} - errors, info = await self._async_get_info(user_input) + errors, info = await _async_get_info(self.hass, user_input) if not errors and info is not None: # Check if already configured existing_entry = await self.async_set_unique_id(info["uuid"]) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 459caa975cc..ca475dc5863 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from dataclasses import dataclass, field from datetime import timedelta import logging from typing import Any @@ -18,19 +17,7 @@ from systembridgemodels.media_directories import MediaDirectory from systembridgemodels.media_files import MediaFile, MediaFiles from systembridgemodels.media_get_file import MediaGetFile from systembridgemodels.media_get_files import MediaGetFiles -from systembridgemodels.modules import ( - CPU, - GPU, - Battery, - Disks, - Display, - GetData, - Media, - Memory, - Process, - RegisterDataListener, - System, -) +from systembridgemodels.modules import GetData, RegisterDataListener from systembridgemodels.response import Response from homeassistant.config_entries import ConfigEntry @@ -46,26 +33,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, MODULES +from .data import SystemBridgeData -@dataclass -class SystemBridgeCoordinatorData: - """System Bridge Coordianator Data.""" - - battery: Battery = field(default_factory=Battery) - cpu: CPU = field(default_factory=CPU) - disks: Disks = None - displays: list[Display] = field(default_factory=list[Display]) - gpus: list[GPU] = field(default_factory=list[GPU]) - media: Media = field(default_factory=Media) - memory: Memory = None - processes: list[Process] = field(default_factory=list[Process]) - system: System = None - - -class SystemBridgeDataUpdateCoordinator( - DataUpdateCoordinator[SystemBridgeCoordinatorData] -): +class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]): """Class to manage fetching System Bridge data from single endpoint.""" def __init__( @@ -79,7 +50,7 @@ class SystemBridgeDataUpdateCoordinator( self.title = entry.title self.unsub: Callable | None = None - self.systembridge_data = SystemBridgeCoordinatorData() + self.systembridge_data = SystemBridgeData() self.websocket_client = WebSocketClient( entry.data[CONF_HOST], entry.data[CONF_PORT], @@ -247,7 +218,7 @@ class SystemBridgeDataUpdateCoordinator( EVENT_HOMEASSISTANT_STOP, close_websocket ) - async def _async_update_data(self) -> SystemBridgeCoordinatorData: + async def _async_update_data(self) -> SystemBridgeData: """Update System Bridge data from WebSocket.""" self.logger.debug( "_async_update_data - WebSocket Connected: %s", diff --git a/homeassistant/components/system_bridge/data.py b/homeassistant/components/system_bridge/data.py new file mode 100644 index 00000000000..fc7d119a324 --- /dev/null +++ b/homeassistant/components/system_bridge/data.py @@ -0,0 +1,29 @@ +"""System Bridge integration data.""" +from dataclasses import dataclass, field + +from systembridgemodels.modules import ( + CPU, + GPU, + Battery, + Disks, + Display, + Media, + Memory, + Process, + System, +) + + +@dataclass +class SystemBridgeData: + """System Bridge Data.""" + + battery: Battery = field(default_factory=Battery) + cpu: CPU = field(default_factory=CPU) + disks: Disks = None + displays: list[Display] = field(default_factory=list[Display]) + gpus: list[GPU] = field(default_factory=list[GPU]) + media: Media = field(default_factory=Media) + memory: Memory = None + processes: list[Process] = field(default_factory=list[Process]) + system: System = None diff --git a/homeassistant/components/system_bridge/media_player.py b/homeassistant/components/system_bridge/media_player.py index a9252a739c9..79fa3dc8c93 100644 --- a/homeassistant/components/system_bridge/media_player.py +++ b/homeassistant/components/system_bridge/media_player.py @@ -20,7 +20,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator +from .coordinator import SystemBridgeDataUpdateCoordinator +from .data import SystemBridgeData from .entity import SystemBridgeEntity STATUS_CHANGING: Final[str] = "CHANGING" @@ -126,7 +127,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity): return features @property - def _systembridge_data(self) -> SystemBridgeCoordinatorData: + def _systembridge_data(self) -> SystemBridgeData: """Return data for the entity.""" return self.coordinator.data diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index b4e840643f1..c7a5eac391c 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -33,7 +33,8 @@ from homeassistant.helpers.typing import UNDEFINED, StateType from homeassistant.util import dt as dt_util from .const import DOMAIN -from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator +from .coordinator import SystemBridgeDataUpdateCoordinator +from .data import SystemBridgeData from .entity import SystemBridgeEntity ATTR_AVAILABLE: Final = "available" @@ -53,14 +54,14 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription): value: Callable = round -def battery_time_remaining(data: SystemBridgeCoordinatorData) -> datetime | None: +def battery_time_remaining(data: SystemBridgeData) -> datetime | None: """Return the battery time remaining.""" if (battery_time := data.battery.time_remaining) is not None: return dt_util.utcnow() + timedelta(seconds=battery_time) return None -def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None: +def cpu_speed(data: SystemBridgeData) -> float | None: """Return the CPU speed.""" if (cpu_frequency := data.cpu.frequency) is not None and ( cpu_frequency.current @@ -72,7 +73,7 @@ def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None: def with_per_cpu(func) -> Callable: """Wrap a function to ensure per CPU data is available.""" - def wrapper(data: SystemBridgeCoordinatorData, index: int) -> float | None: + def wrapper(data: SystemBridgeData, index: int) -> float | None: """Wrap a function to ensure per CPU data is available.""" if data.cpu.per_cpu is not None and index < len(data.cpu.per_cpu): return func(data.cpu.per_cpu[index]) @@ -96,7 +97,7 @@ def cpu_usage_per_cpu(per_cpu: PerCPU) -> float | None: def with_display(func) -> Callable: """Wrap a function to ensure a Display is available.""" - def wrapper(data: SystemBridgeCoordinatorData, index: int) -> Display | None: + def wrapper(data: SystemBridgeData, index: int) -> Display | None: """Wrap a function to ensure a Display is available.""" if index < len(data.displays): return func(data.displays[index]) @@ -126,7 +127,7 @@ def display_refresh_rate(display: Display) -> float | None: def with_gpu(func) -> Callable: """Wrap a function to ensure a GPU is available.""" - def wrapper(data: SystemBridgeCoordinatorData, index: int) -> GPU | None: + def wrapper(data: SystemBridgeData, index: int) -> GPU | None: """Wrap a function to ensure a GPU is available.""" if index < len(data.gpus): return func(data.gpus[index]) @@ -191,7 +192,7 @@ def gpu_usage_percentage(gpu: GPU) -> float | None: return gpu.core_load -def memory_free(data: SystemBridgeCoordinatorData) -> float | None: +def memory_free(data: SystemBridgeData) -> float | None: """Return the free memory.""" if (virtual := data.memory.virtual) is not None and ( free := virtual.free @@ -200,7 +201,7 @@ def memory_free(data: SystemBridgeCoordinatorData) -> float | None: return None -def memory_used(data: SystemBridgeCoordinatorData) -> float | None: +def memory_used(data: SystemBridgeData) -> float | None: """Return the used memory.""" if (virtual := data.memory.virtual) is not None and ( used := virtual.used @@ -210,7 +211,7 @@ def memory_used(data: SystemBridgeCoordinatorData) -> float | None: def partition_usage( - data: SystemBridgeCoordinatorData, + data: SystemBridgeData, device_index: int, partition_index: int, ) -> float | None: @@ -382,9 +383,11 @@ async def async_setup_entry( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - value=lambda data, - dk=index_device, - pk=index_partition: partition_usage(data, dk, pk), + value=( + lambda data, + dk=index_device, + pk=index_partition: partition_usage(data, dk, pk) + ), ), entry.data[CONF_PORT], ) diff --git a/tests/components/system_bridge/__init__.py b/tests/components/system_bridge/__init__.py index f049f887584..edbe5469705 100644 --- a/tests/components/system_bridge/__init__.py +++ b/tests/components/system_bridge/__init__.py @@ -1 +1,116 @@ """Tests for the System Bridge integration.""" + +from collections.abc import Awaitable, Callable +from dataclasses import asdict +from ipaddress import ip_address +from typing import Any + +from systembridgeconnector.const import TYPE_DATA_UPDATE +from systembridgemodels.const import MODEL_SYSTEM +from systembridgemodels.modules import System +from systembridgemodels.response import Response + +from homeassistant.components import zeroconf +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN + +FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" + +FIXTURE_AUTH_INPUT = {CONF_TOKEN: "abc-123-def-456-ghi"} + +FIXTURE_USER_INPUT = { + CONF_TOKEN: "abc-123-def-456-ghi", + CONF_HOST: "test-bridge", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF_INPUT = { + CONF_TOKEN: "abc-123-def-456-ghi", + CONF_HOST: "1.1.1.1", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", + properties={ + "address": "http://test-bridge:9170", + "fqdn": "test-bridge", + "host": "test-bridge", + "ip": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + "port": "9170", + "uuid": FIXTURE_UUID, + }, +) + +FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("1.1.1.1"), + ip_addresses=[ip_address("1.1.1.1")], + port=9170, + hostname="test-bridge.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", + properties={ + "something": "bad", + }, +) + + +FIXTURE_SYSTEM = System( + boot_time=1, + fqdn="", + hostname="1.1.1.1", + ip_address_4="1.1.1.1", + mac_address=FIXTURE_MAC_ADDRESS, + platform="", + platform_version="", + uptime=1, + uuid=FIXTURE_UUID, + version="", + version_latest="", + version_newer_available=False, + users=[], +) + +FIXTURE_DATA_RESPONSE = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data=asdict(FIXTURE_SYSTEM), +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + + +async def mock_data_listener( + self, + callback: Callable[[str, Any], Awaitable[None]] | None = None, + _: bool = False, +): + """Mock websocket data listener.""" + if callback is not None: + # Simulate data received from the websocket + await callback(MODEL_SYSTEM, FIXTURE_SYSTEM) diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 21194db42fa..e30d389cc1b 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -1,131 +1,29 @@ """Test the System Bridge config flow.""" -from collections.abc import Awaitable, Callable -from dataclasses import asdict -from ipaddress import ip_address -from typing import Any from unittest.mock import patch -from systembridgeconnector.const import TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) -from systembridgemodels.const import MODEL_SYSTEM -from systembridgemodels.modules.system import System -from systembridgemodels.response import Response from homeassistant import config_entries, data_entry_flow -from homeassistant.components import zeroconf -from homeassistant.components.system_bridge.config_flow import SystemBridgeConfigFlow from homeassistant.components.system_bridge.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant +from . import ( + FIXTURE_AUTH_INPUT, + FIXTURE_DATA_RESPONSE, + FIXTURE_USER_INPUT, + FIXTURE_UUID, + FIXTURE_ZEROCONF, + FIXTURE_ZEROCONF_BAD, + FIXTURE_ZEROCONF_INPUT, + mock_data_listener, +) + from tests.common import MockConfigEntry -FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" -FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" - -FIXTURE_AUTH_INPUT = {CONF_TOKEN: "abc-123-def-456-ghi"} - -FIXTURE_USER_INPUT = { - CONF_TOKEN: "abc-123-def-456-ghi", - CONF_HOST: "test-bridge", - CONF_PORT: "9170", -} - -FIXTURE_ZEROCONF_INPUT = { - CONF_TOKEN: "abc-123-def-456-ghi", - CONF_HOST: "1.1.1.1", - CONF_PORT: "9170", -} - -FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("1.1.1.1"), - ip_addresses=[ip_address("1.1.1.1")], - port=9170, - hostname="test-bridge.local.", - type="_system-bridge._tcp.local.", - name="System Bridge - test-bridge._system-bridge._tcp.local.", - properties={ - "address": "http://test-bridge:9170", - "fqdn": "test-bridge", - "host": "test-bridge", - "ip": "1.1.1.1", - "mac": FIXTURE_MAC_ADDRESS, - "port": "9170", - "uuid": FIXTURE_UUID, - }, -) - -FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("1.1.1.1"), - ip_addresses=[ip_address("1.1.1.1")], - port=9170, - hostname="test-bridge.local.", - type="_system-bridge._tcp.local.", - name="System Bridge - test-bridge._system-bridge._tcp.local.", - properties={ - "something": "bad", - }, -) - - -FIXTURE_SYSTEM = System( - boot_time=1, - fqdn="", - hostname="1.1.1.1", - ip_address_4="1.1.1.1", - mac_address=FIXTURE_MAC_ADDRESS, - platform="", - platform_version="", - uptime=1, - uuid=FIXTURE_UUID, - version="", - version_latest="", - version_newer_available=False, - users=[], -) - -FIXTURE_DATA_RESPONSE = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data=asdict(FIXTURE_SYSTEM), -) - -FIXTURE_DATA_RESPONSE_BAD = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data={}, -) - -FIXTURE_DATA_RESPONSE_BAD = Response( - id="1234", - type=TYPE_DATA_UPDATE, - subtype=None, - message="Data received", - module=MODEL_SYSTEM, - data={}, -) - - -async def mock_data_listener( - self, - callback: Callable[[str, Any], Awaitable[None]] | None = None, - _: bool = False, -): - """Mock websocket data listener.""" - if callback is not None: - # Simulate data received from the websocket - await callback(MODEL_SYSTEM, FIXTURE_SYSTEM) - async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -536,28 +434,3 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" - - -async def test_migration(hass: HomeAssistant) -> None: - """Test migration from system_bridge to system_bridge.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=FIXTURE_UUID, - data={ - CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], - CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], - CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], - }, - version=1, - minor_version=1, - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - # Check that the version has been updated and the api_key has been moved to token - assert config_entry.version == SystemBridgeConfigFlow.VERSION - assert config_entry.minor_version == SystemBridgeConfigFlow.MINOR_VERSION - assert CONF_API_KEY not in config_entry.data - assert config_entry.data[CONF_TOKEN] == FIXTURE_USER_INPUT[CONF_TOKEN] - assert config_entry.data == FIXTURE_USER_INPUT diff --git a/tests/components/system_bridge/test_init.py b/tests/components/system_bridge/test_init.py new file mode 100644 index 00000000000..67d8595ba4c --- /dev/null +++ b/tests/components/system_bridge/test_init.py @@ -0,0 +1,83 @@ +"""Test the System Bridge integration.""" + +from unittest.mock import patch + +from homeassistant.components.system_bridge.config_flow import SystemBridgeConfigFlow +from homeassistant.components.system_bridge.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.core import HomeAssistant + +from . import FIXTURE_USER_INPUT, FIXTURE_UUID + +from tests.common import MockConfigEntry + + +async def test_migration_minor_1_to_2(hass: HomeAssistant) -> None: + """Test migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data={ + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + }, + version=SystemBridgeConfigFlow.VERSION, + minor_version=1, + ) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + # Check that the version has been updated and the api_key has been moved to token + assert config_entry.version == SystemBridgeConfigFlow.VERSION + assert config_entry.minor_version == SystemBridgeConfigFlow.MINOR_VERSION + assert config_entry.data == { + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], + } + assert config_entry.state == ConfigEntryState.LOADED + + +async def test_migration_minor_future_version(hass: HomeAssistant) -> None: + """Test migration.""" + config_entry_data = { + CONF_API_KEY: FIXTURE_USER_INPUT[CONF_TOKEN], + CONF_HOST: FIXTURE_USER_INPUT[CONF_HOST], + CONF_PORT: FIXTURE_USER_INPUT[CONF_PORT], + CONF_TOKEN: FIXTURE_USER_INPUT[CONF_TOKEN], + } + config_entry_version = SystemBridgeConfigFlow.VERSION + config_entry_minor_version = SystemBridgeConfigFlow.MINOR_VERSION + 1 + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FIXTURE_UUID, + data=config_entry_data, + version=config_entry_version, + minor_version=config_entry_minor_version, + ) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + + assert config_entry.version == config_entry_version + assert config_entry.minor_version == config_entry_minor_version + assert config_entry.data == config_entry_data + assert config_entry.state == ConfigEntryState.LOADED