mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
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:
parent
39cad5f1ee
commit
2599252600
@ -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
|
||||
|
@ -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"])
|
||||
|
@ -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",
|
||||
|
29
homeassistant/components/system_bridge/data.py
Normal file
29
homeassistant/components/system_bridge/data.py
Normal 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
|
@ -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
|
||||
|
||||
|
@ -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],
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
83
tests/components/system_bridge/test_init.py
Normal file
83
tests/components/system_bridge/test_init.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user