1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add Fluss+ Button integration (#139925)

Co-authored-by: NjDaGreat <1754227@students.wits.ac.za>
Co-authored-by: NjeruFluss <161302608+NjeruFluss@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Marcello
2025-12-23 19:48:22 +02:00
committed by GitHub
parent 5f1e6f3633
commit 5933c09a1d
20 changed files with 829 additions and 0 deletions

2
CODEOWNERS generated
View File

@@ -530,6 +530,8 @@ build.json @home-assistant/supervisor
/tests/components/flo/ @dmulcahey
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
/homeassistant/components/fluss/ @fluss
/tests/components/fluss/ @fluss
/homeassistant/components/flux_led/ @icemanch
/tests/components/flux_led/ @icemanch
/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck

View File

@@ -0,0 +1,31 @@
"""The Fluss+ integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from .coordinator import FlussDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BUTTON]
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
async def async_setup_entry(
hass: HomeAssistant,
entry: FlussConfigEntry,
) -> bool:
"""Set up Fluss+ from a config entry."""
coordinator = FlussDataUpdateCoordinator(hass, entry, entry.data[CONF_API_KEY])
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: FlussConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,40 @@
"""Support for Fluss Devices."""
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import FlussApiClientError, FlussDataUpdateCoordinator
from .entity import FlussEntity
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
async def async_setup_entry(
hass: HomeAssistant,
entry: FlussConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Fluss Devices, filtering out any invalid payloads."""
coordinator = entry.runtime_data
devices = coordinator.data
async_add_entities(
FlussButton(coordinator, device_id, device)
for device_id, device in devices.items()
)
class FlussButton(FlussEntity, ButtonEntity):
"""Representation of a Fluss button device."""
_attr_name = None
async def async_press(self) -> None:
"""Handle the button press."""
try:
await self.coordinator.api.async_trigger_device(self.device_id)
except FlussApiClientError as err:
raise HomeAssistantError(f"Failed to trigger device: {err}") from err

View File

@@ -0,0 +1,55 @@
"""Config flow for Fluss+ integration."""
from __future__ import annotations
from typing import Any
from fluss_api import (
FlussApiClient,
FlussApiClientAuthenticationError,
FlussApiClientCommunicationError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, LOGGER
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fluss+."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
client = FlussApiClient(
user_input[CONF_API_KEY], session=async_get_clientsession(self.hass)
)
try:
await client.async_get_devices()
except FlussApiClientCommunicationError:
errors["base"] = "cannot_connect"
except FlussApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception occurred")
errors["base"] = "unknown"
if not errors:
return self.async_create_entry(
title="My Fluss+ Devices", data=user_input
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -0,0 +1,9 @@
"""Constants for the Fluss+ integration."""
from datetime import timedelta
import logging
DOMAIN = "fluss"
LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = 60 # seconds
UPDATE_INTERVAL_TIMEDELTA = timedelta(seconds=UPDATE_INTERVAL)

View File

@@ -0,0 +1,50 @@
"""DataUpdateCoordinator for Fluss+ integration."""
from __future__ import annotations
from typing import Any
from fluss_api import (
FlussApiClient,
FlussApiClientAuthenticationError,
FlussApiClientError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from .const import LOGGER, UPDATE_INTERVAL_TIMEDELTA
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages fetching Fluss device data on a schedule."""
def __init__(
self, hass: HomeAssistant, config_entry: FlussConfigEntry, api_key: str
) -> None:
"""Initialize the coordinator."""
self.api = FlussApiClient(api_key, session=async_get_clientsession(hass))
super().__init__(
hass,
LOGGER,
name=f"Fluss+ ({slugify(api_key[:8])})",
config_entry=config_entry,
update_interval=UPDATE_INTERVAL_TIMEDELTA,
)
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch data from the Fluss API and return as a dictionary keyed by deviceId."""
try:
devices = await self.api.async_get_devices()
except FlussApiClientAuthenticationError as err:
raise ConfigEntryError(f"Authentication failed: {err}") from err
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching Fluss devices: {err}") from err
return {device["deviceId"]: device for device in devices.get("devices", [])}

View File

@@ -0,0 +1,39 @@
"""Base entities for the Fluss+ integration."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import FlussDataUpdateCoordinator
class FlussEntity(CoordinatorEntity[FlussDataUpdateCoordinator]):
"""Base class for Fluss entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: FlussDataUpdateCoordinator,
device_id: str,
device: dict,
) -> None:
"""Initialize the entity with a device ID and device data."""
super().__init__(coordinator)
self.device_id = device_id
self._attr_unique_id = device_id
self._attr_device_info = DeviceInfo(
identifiers={("fluss", device_id)},
name=device.get("deviceName"),
manufacturer="Fluss",
model="Fluss+ Device",
)
@property
def available(self) -> bool:
"""Return if the device is available."""
return super().available and self.device_id in self.coordinator.data
@property
def device(self) -> dict:
"""Return the stored device data."""
return self.coordinator.data[self.device_id]

View File

@@ -0,0 +1,11 @@
{
"domain": "fluss",
"name": "Fluss+",
"codeowners": ["@fluss"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fluss",
"iot_class": "cloud_polling",
"loggers": ["fluss-api"],
"quality_scale": "bronze",
"requirements": ["fluss-api==0.1.9.20"]
}

View File

@@ -0,0 +1,69 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No actions present
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
entity-translations: done
entity-device-class: done
devices: done
entity-category: done
entity-disabled-by-default:
status: exempt
comment: |
Not needed
discovery: todo
stale-devices: todo
diagnostics: todo
exception-translations: todo
icon-translations:
status: exempt
comment: |
No icons used
reconfiguration-flow: todo
dynamic-devices: todo
discovery-update-info: todo
repair-issues:
status: exempt
comment: |
No issues to repair
docs-use-cases: done
docs-supported-devices: todo
docs-supported-functions: done
docs-data-update: todo
docs-known-limitations: done
docs-troubleshooting: todo
docs-examples: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "The API key found in the profile page of the Fluss+ app."
},
"description": "Your Fluss API key, available in the profile page of the Fluss+ app"
}
}
}
}

View File

@@ -220,6 +220,7 @@ FLOWS = {
"flipr",
"flo",
"flume",
"fluss",
"flux_led",
"folder_watcher",
"forecast_solar",

View File

@@ -2092,6 +2092,12 @@
"config_flow": true,
"iot_class": "cloud_polling"
},
"fluss": {
"name": "Fluss+",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"flux": {
"name": "Flux",
"integration_type": "hub",

3
requirements_all.txt generated
View File

@@ -985,6 +985,9 @@ flexit_bacnet==2.2.3
# homeassistant.components.flipr
flipr-api==1.6.1
# homeassistant.components.fluss
fluss-api==0.1.9.20
# homeassistant.components.flux_led
flux-led==1.2.0

View File

@@ -870,6 +870,9 @@ flexit_bacnet==2.2.3
# homeassistant.components.flipr
flipr-api==1.6.1
# homeassistant.components.fluss
fluss-api==0.1.9.20
# homeassistant.components.flux_led
flux-led==1.2.0

View File

@@ -0,0 +1,102 @@
"""Test Script for Fluss+ Initialisation."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from fluss_api import (
FlussApiClient,
FlussApiClientAuthenticationError,
FlussApiClientCommunicationError,
FlussApiClientError,
)
import pytest
from homeassistant.components.fluss import PLATFORMS
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@pytest.mark.parametrize(
("side_effect", "expected_exception"),
[
(FlussApiClientAuthenticationError, ConfigEntryAuthFailed),
(FlussApiClientCommunicationError, ConfigEntryNotReady),
(FlussApiClientError, ConfigEntryNotReady),
],
)
async def test_async_setup_entry_errors(
hass: HomeAssistant,
mock_config_entry: MagicMock,
side_effect: Exception,
expected_exception: type[Exception],
) -> None:
"""Test setup errors."""
with (
patch("fluss_api.FlussApiClient", side_effect=side_effect),
pytest.raises(expected_exception),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.asyncio
async def test_async_setup_entry_success(
hass: HomeAssistant,
mock_config_entry: MagicMock,
mock_api_client: FlussApiClient,
) -> None:
"""Test successful setup."""
with patch("fluss_api.FlussApiClient", return_value=mock_api_client):
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
hass.config_entries.async_forward_entry_setups.assert_called_once_with(
mock_config_entry, PLATFORMS
)
@pytest.mark.asyncio
async def test_async_unload_entry(
hass: HomeAssistant,
mock_config_entry: MagicMock,
mock_api_client: FlussApiClient,
) -> None:
"""Test unloading entry."""
# Set up the config entry first to ensure it's in LOADED state
with patch("fluss_api.FlussApiClient", return_value=mock_api_client):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
# Test unloading
with patch(
"homeassistant.components.fluss.async_unload_platforms", return_value=True
):
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.asyncio
async def test_platforms_forwarded(
hass: HomeAssistant,
mock_config_entry: MagicMock,
mock_api_client: FlussApiClient,
) -> None:
"""Test platforms are forwarded correctly."""
with patch("fluss_api.FlussApiClient", return_value=mock_api_client):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
hass.config_entries.async_forward_entry_setups.assert_called_with(
mock_config_entry, [Platform.BUTTON]
)

View File

@@ -0,0 +1,55 @@
"""Shared test fixtures for Fluss+ integration."""
from __future__ import annotations
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.fluss.const import DOMAIN
from homeassistant.const import CONF_API_KEY
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="My Fluss+ Devices",
data={CONF_API_KEY: "test_api_key"},
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.fluss.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_api_client() -> Generator[AsyncMock]:
"""Mock Fluss API client with single device."""
with (
patch(
"homeassistant.components.fluss.coordinator.FlussApiClient",
autospec=True,
) as mock_client,
patch(
"homeassistant.components.fluss.config_flow.FlussApiClient",
new=mock_client,
),
):
client = mock_client.return_value
client.async_get_devices.return_value = {
"devices": [
{"deviceId": "2a303030sdj1", "deviceName": "Device 1"},
{"deviceId": "ape93k9302j2", "deviceName": "Device 2"},
]
}
yield client

View File

@@ -0,0 +1,97 @@
# serializer version: 1
# name: test_buttons[button.device_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.device_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'fluss',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '2a303030sdj1',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[button.device_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Device 1',
}),
'context': <ANY>,
'entity_id': 'button.device_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_buttons[button.device_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': None,
'entity_id': 'button.device_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'fluss',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'ape93k9302j2',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[button.device_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Device 2',
}),
'context': <ANY>,
'entity_id': 'button.device_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -0,0 +1,69 @@
"""Tests for the Fluss Buttons."""
from __future__ import annotations
from unittest.mock import AsyncMock
from fluss_api import FlussApiClient, FlussApiClientError
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_buttons(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test setup with multiple devices."""
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_button_press(
hass: HomeAssistant,
mock_api_client: FlussApiClient,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test successful button press."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.device_1"},
blocking=True,
)
mock_api_client.async_trigger_device.assert_called_once_with("2a303030sdj1")
async def test_button_press_error(
hass: HomeAssistant,
mock_api_client: FlussApiClient,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test button press with API error."""
await setup_integration(hass, mock_config_entry)
mock_api_client.async_trigger_device.side_effect = FlussApiClientError("API Boom")
with pytest.raises(HomeAssistantError, match="Failed to trigger device: API Boom"):
await hass.services.async_call(
BUTTON_DOMAIN,
SERVICE_PRESS,
{ATTR_ENTITY_ID: "button.device_1"},
blocking=True,
)

View File

@@ -0,0 +1,108 @@
"""Tests for the Fluss+ config flow."""
from unittest.mock import AsyncMock
from fluss_api import (
FlussApiClientAuthenticationError,
FlussApiClientCommunicationError,
)
import pytest
from homeassistant.components.fluss.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry
async def test_full_flow(
hass: HomeAssistant, mock_api_client: AsyncMock, mock_setup_entry: AsyncMock
) -> None:
"""Test full config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "valid_api_key"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "My Fluss+ Devices"
assert result["data"] == {CONF_API_KEY: "valid_api_key"}
@pytest.mark.parametrize(
("exception", "expected_error"),
[
(FlussApiClientAuthenticationError, "invalid_auth"),
(FlussApiClientCommunicationError, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_step_user_errors(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_setup_entry: AsyncMock,
exception: Exception,
expected_error: str,
) -> None:
"""Test error cases for user step with recovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
user_input = {CONF_API_KEY: "some_api_key"}
mock_api_client.async_get_devices.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_API_KEY: "valid_api_key"}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": expected_error}
mock_api_client.async_get_devices.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_duplicate_entry(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test error cases for user step with recovery."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_API_KEY: "test_api_key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@@ -0,0 +1,56 @@
"""Test script for Fluss+ integration initialization."""
from unittest.mock import AsyncMock
from fluss_api import (
FlussApiClientAuthenticationError,
FlussApiClientCommunicationError,
FlussApiClientError,
)
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_api_client: AsyncMock,
) -> None:
"""Test the Fluss configuration entry loading/unloading."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
assert len(mock_api_client.async_get_devices.mock_calls) == 1
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
("exception", "state"),
[
(FlussApiClientAuthenticationError, ConfigEntryState.SETUP_ERROR),
(FlussApiClientCommunicationError, ConfigEntryState.SETUP_RETRY),
(FlussApiClientError, ConfigEntryState.SETUP_RETRY),
],
)
async def test_async_setup_entry_authentication_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_api_client: AsyncMock,
exception: Exception,
state: ConfigEntryState,
) -> None:
"""Test that an authentication error during setup leads to SETUP_ERROR state."""
mock_api_client.async_get_devices.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is state