mirror of
https://github.com/home-assistant/core.git
synced 2025-08-02 20:25:07 +02:00
Make Idasen Desk react to bluetooth changes (#115939)
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from attr import dataclass
|
from attr import dataclass
|
||||||
@@ -10,6 +11,7 @@ from idasen_ha import Desk
|
|||||||
from idasen_ha.errors import AuthFailedError
|
from idasen_ha.errors import AuthFailedError
|
||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
|
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
@@ -46,6 +48,7 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): # pylint: disab
|
|||||||
self._address = address
|
self._address = address
|
||||||
self._expected_connected = False
|
self._expected_connected = False
|
||||||
self._connection_lost = False
|
self._connection_lost = False
|
||||||
|
self._disconnect_lock = asyncio.Lock()
|
||||||
|
|
||||||
self.desk = Desk(self.async_set_updated_data)
|
self.desk = Desk(self.async_set_updated_data)
|
||||||
|
|
||||||
@@ -56,6 +59,7 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): # pylint: disab
|
|||||||
self.hass, self._address, connectable=True
|
self.hass, self._address, connectable=True
|
||||||
)
|
)
|
||||||
if ble_device is None:
|
if ble_device is None:
|
||||||
|
_LOGGER.debug("No BLEDevice for %s", self._address)
|
||||||
return False
|
return False
|
||||||
self._expected_connected = True
|
self._expected_connected = True
|
||||||
await self.desk.connect(ble_device)
|
await self.desk.connect(ble_device)
|
||||||
@@ -68,20 +72,28 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]): # pylint: disab
|
|||||||
self._connection_lost = False
|
self._connection_lost = False
|
||||||
await self.desk.disconnect()
|
await self.desk.disconnect()
|
||||||
|
|
||||||
@callback
|
async def async_ensure_connection_state(self) -> None:
|
||||||
def async_set_updated_data(self, data: int | None) -> None:
|
"""Check if the expected connection state matches the current state and correct it if needed."""
|
||||||
"""Handle data update."""
|
|
||||||
if self._expected_connected:
|
if self._expected_connected:
|
||||||
if not self.desk.is_connected:
|
if not self.desk.is_connected:
|
||||||
_LOGGER.debug("Desk disconnected. Reconnecting")
|
_LOGGER.debug("Desk disconnected. Reconnecting")
|
||||||
self._connection_lost = True
|
self._connection_lost = True
|
||||||
self.hass.async_create_task(self.async_connect(), eager_start=False)
|
await self.async_connect()
|
||||||
elif self._connection_lost:
|
elif self._connection_lost:
|
||||||
_LOGGER.info("Reconnected to desk")
|
_LOGGER.info("Reconnected to desk")
|
||||||
self._connection_lost = False
|
self._connection_lost = False
|
||||||
elif self.desk.is_connected:
|
elif self.desk.is_connected:
|
||||||
_LOGGER.warning("Desk is connected but should not be. Disconnecting")
|
if self._disconnect_lock.locked():
|
||||||
self.hass.async_create_task(self.desk.disconnect())
|
_LOGGER.debug("Already disconnecting")
|
||||||
|
return
|
||||||
|
async with self._disconnect_lock:
|
||||||
|
_LOGGER.debug("Desk is connected but should not be. Disconnecting")
|
||||||
|
await self.desk.disconnect()
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_updated_data(self, data: int | None) -> None:
|
||||||
|
"""Handle data update."""
|
||||||
|
self.hass.async_create_task(self.async_ensure_connection_state())
|
||||||
return super().async_set_updated_data(data)
|
return super().async_set_updated_data(data)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,6 +128,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_bluetooth_callback(
|
||||||
|
service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||||
|
change: bluetooth.BluetoothChange,
|
||||||
|
) -> None:
|
||||||
|
"""Update from a Bluetooth callback to ensure that a new BLEDevice is fetched."""
|
||||||
|
_LOGGER.debug("Bluetooth callback triggered")
|
||||||
|
hass.async_create_task(coordinator.async_ensure_connection_state())
|
||||||
|
|
||||||
|
entry.async_on_unload(
|
||||||
|
bluetooth.async_register_callback(
|
||||||
|
hass,
|
||||||
|
_async_bluetooth_callback,
|
||||||
|
BluetoothCallbackMatcher({ADDRESS: address}),
|
||||||
|
bluetooth.BluetoothScanningMode.ACTIVE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_stop(event: Event) -> None:
|
async def _async_stop(event: Event) -> None:
|
||||||
"""Close the connection."""
|
"""Close the connection."""
|
||||||
await coordinator.async_disconnect()
|
await coordinator.async_disconnect()
|
||||||
|
@@ -12,5 +12,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["idasen-ha==2.5.1"]
|
"requirements": ["idasen-ha==2.5.3"]
|
||||||
}
|
}
|
||||||
|
@@ -1122,7 +1122,7 @@ ical==8.0.0
|
|||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
|
||||||
# homeassistant.components.idasen_desk
|
# homeassistant.components.idasen_desk
|
||||||
idasen-ha==2.5.1
|
idasen-ha==2.5.3
|
||||||
|
|
||||||
# homeassistant.components.network
|
# homeassistant.components.network
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@@ -915,7 +915,7 @@ ical==8.0.0
|
|||||||
icmplib==3.0
|
icmplib==3.0
|
||||||
|
|
||||||
# homeassistant.components.idasen_desk
|
# homeassistant.components.idasen_desk
|
||||||
idasen-ha==2.5.1
|
idasen-ha==2.5.3
|
||||||
|
|
||||||
# homeassistant.components.network
|
# homeassistant.components.network
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
"""Test the IKEA Idasen Desk cover."""
|
"""Test the IKEA Idasen Desk cover."""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from bleak.exc import BleakError
|
from bleak.exc import BleakError
|
||||||
import pytest
|
import pytest
|
||||||
@@ -39,6 +39,7 @@ async def test_cover_available(
|
|||||||
assert state.state == STATE_OPEN
|
assert state.state == STATE_OPEN
|
||||||
assert state.attributes[ATTR_CURRENT_POSITION] == 60
|
assert state.attributes[ATTR_CURRENT_POSITION] == 60
|
||||||
|
|
||||||
|
mock_desk_api.connect = AsyncMock()
|
||||||
mock_desk_api.is_connected = False
|
mock_desk_api.is_connected = False
|
||||||
mock_desk_api.trigger_update_callback(None)
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
"""Test the IKEA Idasen Desk init."""
|
"""Test the IKEA Idasen Desk init."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
@@ -53,6 +54,77 @@ async def test_no_ble_device(hass: HomeAssistant, mock_desk_api: MagicMock) -> N
|
|||||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_reconnect_on_bluetooth_callback(
|
||||||
|
hass: HomeAssistant, mock_desk_api: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that a reconnet is made after the bluetooth callback is triggered."""
|
||||||
|
with mock.patch(
|
||||||
|
"homeassistant.components.idasen_desk.bluetooth.async_register_callback"
|
||||||
|
) as mock_register_callback:
|
||||||
|
await init_integration(hass)
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
mock_desk_api.connect.assert_called_once()
|
||||||
|
mock_register_callback.assert_called_once()
|
||||||
|
|
||||||
|
mock_desk_api.is_connected = False
|
||||||
|
_, register_callback_args, _ = mock_register_callback.mock_calls[0]
|
||||||
|
bt_callback = register_callback_args[1]
|
||||||
|
bt_callback(None, None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert mock_desk_api.connect.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicated_disconnect_is_no_op(
|
||||||
|
hass: HomeAssistant, mock_desk_api: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that calling disconnect while disconnecting is a no-op."""
|
||||||
|
await init_integration(hass)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"button", "press", {"entity_id": "button.test_disconnect"}, blocking=True
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async def mock_disconnect():
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
mock_desk_api.disconnect.reset_mock()
|
||||||
|
mock_desk_api.disconnect.side_effect = mock_disconnect
|
||||||
|
|
||||||
|
# Since the disconnect button was pressed but the desk indicates "connected",
|
||||||
|
# any update event will call disconnect()
|
||||||
|
mock_desk_api.is_connected = True
|
||||||
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_desk_api.disconnect.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ensure_connection_state(
|
||||||
|
hass: HomeAssistant, mock_desk_api: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test that the connection state is ensured."""
|
||||||
|
await init_integration(hass)
|
||||||
|
|
||||||
|
mock_desk_api.connect.reset_mock()
|
||||||
|
mock_desk_api.is_connected = False
|
||||||
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_desk_api.connect.assert_called_once()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"button", "press", {"entity_id": "button.test_disconnect"}, blocking=True
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
mock_desk_api.disconnect.reset_mock()
|
||||||
|
mock_desk_api.is_connected = True
|
||||||
|
mock_desk_api.trigger_update_callback(None)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock_desk_api.disconnect.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_unload_entry(hass: HomeAssistant, mock_desk_api: MagicMock) -> None:
|
async def test_unload_entry(hass: HomeAssistant, mock_desk_api: MagicMock) -> None:
|
||||||
"""Test successful unload of entry."""
|
"""Test successful unload of entry."""
|
||||||
entry = await init_integration(hass)
|
entry = await init_integration(hass)
|
||||||
|
Reference in New Issue
Block a user