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 <marhje52@gmail.com>

* Refactor system bridge migration tests

* Remove minor downgrade code

* Update tests/components/system_bridge/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/system_bridge/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* 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 <marhje52@gmail.com>
This commit is contained in:
Aidan Timson 2024-03-06 09:47:21 +00:00 committed by GitHub
parent 39cad5f1ee
commit 2599252600
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 302 additions and 292 deletions

View File

@ -45,6 +45,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from .config_flow import SystemBridgeConfigFlow
from .const import DOMAIN, MODULES from .const import DOMAIN, MODULES
from .coordinator import SystemBridgeDataUpdateCoordinator 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: async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry.""" """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 # Migrate to CONF_TOKEN, which was added in 1.2
new_data = dict(config_entry.data) new_data = dict(config_entry.data)
new_data.setdefault(CONF_TOKEN, config_entry.data.get(CONF_API_KEY)) 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( hass.config_entries.async_update_entry(
config_entry, config_entry,
@ -378,5 +385,4 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry.minor_version, config_entry.minor_version,
) )
# User is trying to downgrade from a future version return True
return False

View File

@ -12,18 +12,19 @@ from systembridgeconnector.exceptions import (
ConnectionErrorException, ConnectionErrorException,
) )
from systembridgeconnector.websocket_client import WebSocketClient from systembridgeconnector.websocket_client import WebSocketClient
from systembridgemodels.modules import GetData, System from systembridgemodels.modules import GetData
import voluptuous as vol import voluptuous as vol
from homeassistant.components import zeroconf from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult 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.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
from .data import SystemBridgeData
_LOGGER = logging.getLogger(__name__) _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. 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( websocket_client = WebSocketClient(
host, data[CONF_HOST],
data[CONF_PORT], data[CONF_PORT],
data[CONF_API_KEY], data[CONF_TOKEN],
) )
try: try:
async with asyncio.timeout(15): async with asyncio.timeout(15):
await websocket_client.connect(session=async_get_clientsession(hass)) 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"])) response = await websocket_client.get_data(GetData(modules=["system"]))
_LOGGER.debug("Got response: %s", response.json()) _LOGGER.debug("Got response: %s", response)
if response.data is None or not isinstance(response.data, System): if response is None:
raise CannotConnect("No data received") 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: except AuthenticationException as exception:
_LOGGER.warning( _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 raise InvalidAuth from exception
except ( except (
@ -80,9 +96,9 @@ async def _validate_input(
except ValueError as exception: except ValueError as exception:
raise CannotConnect from 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( async def _async_get_info(
@ -120,93 +136,6 @@ class SystemBridgeConfigFlow(
self._name: str | None = None self._name: str | None = None
self._input: dict[str, Any] = {} self._input: dict[str, Any] = {}
self._reauth = False 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( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -217,7 +146,7 @@ class SystemBridgeConfigFlow(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA 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: if not errors and info is not None:
# Check if already configured # Check if already configured
await self.async_set_unique_id(info["uuid"], raise_on_progress=False) await self.async_set_unique_id(info["uuid"], raise_on_progress=False)
@ -237,7 +166,7 @@ class SystemBridgeConfigFlow(
if user_input is not None: if user_input is not None:
user_input = {**self._input, **user_input} 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: if not errors and info is not None:
# Check if already configured # Check if already configured
existing_entry = await self.async_set_unique_id(info["uuid"]) existing_entry = await self.async_set_unique_id(info["uuid"])

View File

@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
@ -18,19 +17,7 @@ from systembridgemodels.media_directories import MediaDirectory
from systembridgemodels.media_files import MediaFile, MediaFiles from systembridgemodels.media_files import MediaFile, MediaFiles
from systembridgemodels.media_get_file import MediaGetFile from systembridgemodels.media_get_file import MediaGetFile
from systembridgemodels.media_get_files import MediaGetFiles from systembridgemodels.media_get_files import MediaGetFiles
from systembridgemodels.modules import ( from systembridgemodels.modules import GetData, RegisterDataListener
CPU,
GPU,
Battery,
Disks,
Display,
GetData,
Media,
Memory,
Process,
RegisterDataListener,
System,
)
from systembridgemodels.response import Response from systembridgemodels.response import Response
from homeassistant.config_entries import ConfigEntry 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, MODULES from .const import DOMAIN, MODULES
from .data import SystemBridgeData
@dataclass class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[SystemBridgeData]):
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 to manage fetching System Bridge data from single endpoint.""" """Class to manage fetching System Bridge data from single endpoint."""
def __init__( def __init__(
@ -79,7 +50,7 @@ class SystemBridgeDataUpdateCoordinator(
self.title = entry.title self.title = entry.title
self.unsub: Callable | None = None self.unsub: Callable | None = None
self.systembridge_data = SystemBridgeCoordinatorData() self.systembridge_data = SystemBridgeData()
self.websocket_client = WebSocketClient( self.websocket_client = WebSocketClient(
entry.data[CONF_HOST], entry.data[CONF_HOST],
entry.data[CONF_PORT], entry.data[CONF_PORT],
@ -247,7 +218,7 @@ class SystemBridgeDataUpdateCoordinator(
EVENT_HOMEASSISTANT_STOP, close_websocket 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.""" """Update System Bridge data from WebSocket."""
self.logger.debug( self.logger.debug(
"_async_update_data - WebSocket Connected: %s", "_async_update_data - WebSocket Connected: %s",

View File

@ -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

View File

@ -20,7 +20,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator from .coordinator import SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity from .entity import SystemBridgeEntity
STATUS_CHANGING: Final[str] = "CHANGING" STATUS_CHANGING: Final[str] = "CHANGING"
@ -126,7 +127,7 @@ class SystemBridgeMediaPlayer(SystemBridgeEntity, MediaPlayerEntity):
return features return features
@property @property
def _systembridge_data(self) -> SystemBridgeCoordinatorData: def _systembridge_data(self) -> SystemBridgeData:
"""Return data for the entity.""" """Return data for the entity."""
return self.coordinator.data return self.coordinator.data

View File

@ -33,7 +33,8 @@ from homeassistant.helpers.typing import UNDEFINED, StateType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator from .coordinator import SystemBridgeDataUpdateCoordinator
from .data import SystemBridgeData
from .entity import SystemBridgeEntity from .entity import SystemBridgeEntity
ATTR_AVAILABLE: Final = "available" ATTR_AVAILABLE: Final = "available"
@ -53,14 +54,14 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription):
value: Callable = round value: Callable = round
def battery_time_remaining(data: SystemBridgeCoordinatorData) -> datetime | None: def battery_time_remaining(data: SystemBridgeData) -> datetime | None:
"""Return the battery time remaining.""" """Return the battery time remaining."""
if (battery_time := data.battery.time_remaining) is not None: if (battery_time := data.battery.time_remaining) is not None:
return dt_util.utcnow() + timedelta(seconds=battery_time) return dt_util.utcnow() + timedelta(seconds=battery_time)
return None return None
def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None: def cpu_speed(data: SystemBridgeData) -> float | None:
"""Return the CPU speed.""" """Return the CPU speed."""
if (cpu_frequency := data.cpu.frequency) is not None and ( if (cpu_frequency := data.cpu.frequency) is not None and (
cpu_frequency.current cpu_frequency.current
@ -72,7 +73,7 @@ def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None:
def with_per_cpu(func) -> Callable: def with_per_cpu(func) -> Callable:
"""Wrap a function to ensure per CPU data is available.""" """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.""" """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): if data.cpu.per_cpu is not None and index < len(data.cpu.per_cpu):
return func(data.cpu.per_cpu[index]) 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: def with_display(func) -> Callable:
"""Wrap a function to ensure a Display is available.""" """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.""" """Wrap a function to ensure a Display is available."""
if index < len(data.displays): if index < len(data.displays):
return func(data.displays[index]) return func(data.displays[index])
@ -126,7 +127,7 @@ def display_refresh_rate(display: Display) -> float | None:
def with_gpu(func) -> Callable: def with_gpu(func) -> Callable:
"""Wrap a function to ensure a GPU is available.""" """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.""" """Wrap a function to ensure a GPU is available."""
if index < len(data.gpus): if index < len(data.gpus):
return func(data.gpus[index]) return func(data.gpus[index])
@ -191,7 +192,7 @@ def gpu_usage_percentage(gpu: GPU) -> float | None:
return gpu.core_load return gpu.core_load
def memory_free(data: SystemBridgeCoordinatorData) -> float | None: def memory_free(data: SystemBridgeData) -> float | None:
"""Return the free memory.""" """Return the free memory."""
if (virtual := data.memory.virtual) is not None and ( if (virtual := data.memory.virtual) is not None and (
free := virtual.free free := virtual.free
@ -200,7 +201,7 @@ def memory_free(data: SystemBridgeCoordinatorData) -> float | None:
return None return None
def memory_used(data: SystemBridgeCoordinatorData) -> float | None: def memory_used(data: SystemBridgeData) -> float | None:
"""Return the used memory.""" """Return the used memory."""
if (virtual := data.memory.virtual) is not None and ( if (virtual := data.memory.virtual) is not None and (
used := virtual.used used := virtual.used
@ -210,7 +211,7 @@ def memory_used(data: SystemBridgeCoordinatorData) -> float | None:
def partition_usage( def partition_usage(
data: SystemBridgeCoordinatorData, data: SystemBridgeData,
device_index: int, device_index: int,
partition_index: int, partition_index: int,
) -> float | None: ) -> float | None:
@ -382,9 +383,11 @@ async def async_setup_entry(
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk", icon="mdi:harddisk",
value=lambda data, value=(
dk=index_device, lambda data,
pk=index_partition: partition_usage(data, dk, pk), dk=index_device,
pk=index_partition: partition_usage(data, dk, pk)
),
), ),
entry.data[CONF_PORT], entry.data[CONF_PORT],
) )

View File

@ -1 +1,116 @@
"""Tests for the System Bridge integration.""" """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)

View File

@ -1,131 +1,29 @@
"""Test the System Bridge config flow.""" """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 unittest.mock import patch
from systembridgeconnector.const import TYPE_DATA_UPDATE
from systembridgeconnector.exceptions import ( from systembridgeconnector.exceptions import (
AuthenticationException, AuthenticationException,
ConnectionClosedException, ConnectionClosedException,
ConnectionErrorException, 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 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.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 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 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: async def test_show_user_form(hass: HomeAssistant) -> None:
"""Test that the setup form is served.""" """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["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "unknown" 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

View File

@ -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