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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user