1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Improve availability in Fluss (#168154)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Marcello
2026-05-04 14:59:05 +02:00
committed by GitHub
parent db2dfbbc41
commit fa7ecddb66
6 changed files with 124 additions and 10 deletions
+5
View File
@@ -29,6 +29,11 @@ class FlussButton(FlussEntity, ButtonEntity):
_attr_name = None
@property
def available(self) -> bool:
"""Return True only when the device is online."""
return super().available and self.device["internetConnected"]
async def async_press(self) -> None:
"""Handle the button press."""
try:
+1 -2
View File
@@ -5,5 +5,4 @@ import logging
DOMAIN = "fluss"
LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = 60 # seconds
UPDATE_INTERVAL_TIMEDELTA = timedelta(seconds=UPDATE_INTERVAL)
UPDATE_INTERVAL = timedelta(minutes=30)
+27 -5
View File
@@ -1,5 +1,8 @@
"""DataUpdateCoordinator for Fluss+ integration."""
from __future__ import annotations
import asyncio
from typing import Any
from fluss_api import (
@@ -15,12 +18,12 @@ 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
from .const import LOGGER, UPDATE_INTERVAL
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
"""Manages fetching Fluss device data on a schedule."""
def __init__(
@@ -33,11 +36,19 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
LOGGER,
name=f"Fluss+ ({slugify(api_key[:8])})",
config_entry=config_entry,
update_interval=UPDATE_INTERVAL_TIMEDELTA,
update_interval=UPDATE_INTERVAL,
)
async def _async_get_connectivity(self, device_id: str) -> bool:
"""Return connectivity for a device; False if the status call fails."""
try:
status = await self.api.async_get_device_status(device_id)
except FlussApiClientError:
return False
return status["status"]["internetConnected"]
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."""
"""Fetch Fluss+ devices and merge per-device connectivity status."""
try:
devices = await self.api.async_get_devices()
except FlussApiClientAuthenticationError as err:
@@ -45,4 +56,15 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching Fluss devices: {err}") from err
return {device["deviceId"]: device for device in devices.get("devices", [])}
device_list = [
device
for device in devices["devices"]
if device["userPermissions"]["canUseWiFi"]
]
connectivity = await asyncio.gather(
*(self._async_get_connectivity(d["deviceId"]) for d in device_list)
)
return {
device["deviceId"]: {**device, "internetConnected": connected}
for device, connected in zip(device_list, connectivity, strict=False)
}
+13 -2
View File
@@ -46,8 +46,19 @@ def mock_api_client() -> Generator[AsyncMock]:
client = mock_client.return_value
client.async_get_devices.return_value = {
"devices": [
{"deviceId": "2a303030sdj1", "deviceName": "Device 1"},
{"deviceId": "ape93k9302j2", "deviceName": "Device 2"},
{
"deviceId": "2a303030sdj1",
"deviceName": "Device 1",
"userPermissions": {"canUseWiFi": True},
},
{
"deviceId": "ape93k9302j2",
"deviceName": "Device 2",
"userPermissions": {"canUseWiFi": True},
},
]
}
client.async_get_device_status.return_value = {
"status": {"internetConnected": True}
}
yield client
+61 -1
View File
@@ -7,7 +7,7 @@ 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.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
@@ -48,6 +48,66 @@ async def test_button_press(
mock_api_client.async_trigger_device.assert_called_once_with("2a303030sdj1")
async def test_devices_without_wifi_permission_are_filtered(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Devices whose userPermissions.canUseWiFi is false must not be surfaced."""
mock_api_client.async_get_devices.return_value = {
"devices": [
{
"deviceId": "allowed",
"deviceName": "Allowed",
"userPermissions": {"canUseWiFi": True},
},
{
"deviceId": "blocked",
"deviceName": "Blocked",
"userPermissions": {"canUseWiFi": False},
},
]
}
await setup_integration(hass, mock_config_entry)
assert hass.states.get("button.allowed") is not None
assert hass.states.get("button.blocked") is None
mock_api_client.async_get_device_status.assert_called_once_with("allowed")
async def test_button_unavailable_on_status_error(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Buttons become unavailable when the status call errors."""
mock_api_client.async_get_device_status.side_effect = FlussApiClientError(
"device offline"
)
await setup_integration(hass, mock_config_entry)
assert hass.states.get("button.device_1").state == STATE_UNAVAILABLE
assert hass.states.get("button.device_2").state == STATE_UNAVAILABLE
async def test_button_unavailable_when_internet_disconnected(
hass: HomeAssistant,
mock_api_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Buttons become unavailable when the device reports no internet."""
mock_api_client.async_get_device_status.return_value = {
"status": {"internetConnected": False}
}
await setup_integration(hass, mock_config_entry)
assert hass.states.get("button.device_1").state == STATE_UNAVAILABLE
assert hass.states.get("button.device_2").state == STATE_UNAVAILABLE
async def test_button_press_error(
hass: HomeAssistant,
mock_api_client: FlussApiClient,
+17
View File
@@ -10,6 +10,7 @@ from fluss_api import (
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from . import setup_integration
@@ -54,3 +55,19 @@ async def test_async_setup_entry_authentication_error(
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is state
async def test_status_authentication_error_marks_device_offline(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_api_client: AsyncMock,
) -> None:
"""Test that an auth error from a per-device status call marks the device offline."""
mock_api_client.async_get_device_status.side_effect = (
FlussApiClientAuthenticationError("permission revoked")
)
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("button.device_1").state == STATE_UNAVAILABLE
assert hass.states.get("button.device_2").state == STATE_UNAVAILABLE