mirror of
https://github.com/home-assistant/core.git
synced 2026-09-28 02:18:10 -04:00
Add integration for IKEA Idasen Desk (#99173)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
6c095a963d
commit
bd9bab000e
@@ -0,0 +1,51 @@
|
||||
"""Tests for the IKEA Idasen Desk integration."""
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.components.idasen_desk.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
|
||||
|
||||
IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
name="Desk 1234",
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
rssi=-60,
|
||||
manufacturer_data={},
|
||||
service_uuids=["99fa0001-338a-1024-8a49-009c0215f78a"],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=generate_ble_device(address="AA:BB:CC:DD:EE:FF", name="Desk 1234"),
|
||||
advertisement=generate_advertisement_data(),
|
||||
time=0,
|
||||
connectable=True,
|
||||
)
|
||||
|
||||
NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak(
|
||||
name="Not Desk",
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
rssi=-60,
|
||||
manufacturer_data={},
|
||||
service_uuids=[],
|
||||
service_data={},
|
||||
source="local",
|
||||
device=generate_ble_device(address="AA:BB:CC:DD:EE:FF", name="Not Desk"),
|
||||
advertisement=generate_advertisement_data(),
|
||||
time=0,
|
||||
connectable=True,
|
||||
)
|
||||
|
||||
|
||||
async def init_integration(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Set up the IKEA Idasen Desk integration in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
title="Test",
|
||||
domain=DOMAIN,
|
||||
data={CONF_ADDRESS: "AA:BB:CC:DD:EE:FF"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
@@ -0,0 +1,49 @@
|
||||
"""IKEA Idasen Desk fixtures."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest import mock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth):
|
||||
"""Auto mock bluetooth."""
|
||||
|
||||
|
||||
@pytest.fixture(autouse=False)
|
||||
def mock_desk_api():
|
||||
"""Set up idasen desk API fixture."""
|
||||
with mock.patch("homeassistant.components.idasen_desk.Desk") as desk_patched:
|
||||
mock_desk = MagicMock()
|
||||
|
||||
def mock_init(update_callback: Callable[[int | None], None] | None):
|
||||
mock_desk.trigger_update_callback = update_callback
|
||||
return mock_desk
|
||||
|
||||
desk_patched.side_effect = mock_init
|
||||
|
||||
async def mock_connect(ble_device, monitor_height: bool = True):
|
||||
mock_desk.is_connected = True
|
||||
|
||||
async def mock_move_to(height: float):
|
||||
mock_desk.height_percent = height
|
||||
mock_desk.trigger_update_callback(height)
|
||||
|
||||
async def mock_move_up():
|
||||
await mock_move_to(100)
|
||||
|
||||
async def mock_move_down():
|
||||
await mock_move_to(0)
|
||||
|
||||
mock_desk.connect = AsyncMock(side_effect=mock_connect)
|
||||
mock_desk.disconnect = AsyncMock()
|
||||
mock_desk.move_to = AsyncMock(side_effect=mock_move_to)
|
||||
mock_desk.move_up = AsyncMock(side_effect=mock_move_up)
|
||||
mock_desk.move_down = AsyncMock(side_effect=mock_move_down)
|
||||
mock_desk.stop = AsyncMock()
|
||||
mock_desk.height_percent = 60
|
||||
mock_desk.is_moving = False
|
||||
|
||||
yield mock_desk
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Test the IKEA Idasen Desk config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from bleak import BleakError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.idasen_desk.const import DOMAIN
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import IDASEN_DISCOVERY_INFO, NOT_IDASEN_DISCOVERY_INFO
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_user_step_success(hass: HomeAssistant) -> None:
|
||||
"""Test user step success path."""
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||
return_value=[NOT_IDASEN_DISCOVERY_INFO, IDASEN_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect"
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == IDASEN_DISCOVERY_INFO.name
|
||||
assert result2["data"] == {
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
}
|
||||
assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_step_no_devices_found(hass: HomeAssistant) -> None:
|
||||
"""Test user step with no devices found."""
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||
return_value=[NOT_IDASEN_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None:
|
||||
"""Test user step with only existing devices found."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
unique_id=IDASEN_DISCOVERY_INFO.address,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||
return_value=[IDASEN_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [TimeoutError(), BleakError()])
|
||||
async def test_user_step_cannot_connect(
|
||||
hass: HomeAssistant, exception: Exception
|
||||
) -> None:
|
||||
"""Test user step and we cannot connect."""
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||
return_value=[IDASEN_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.connect",
|
||||
side_effect=exception,
|
||||
), patch("homeassistant.components.idasen_desk.config_flow.Desk.disconnect"):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect"
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == IDASEN_DISCOVERY_INFO.name
|
||||
assert result3["data"] == {
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
}
|
||||
assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_step_unknown_exception(hass: HomeAssistant) -> None:
|
||||
"""Test user step with an unknown exception."""
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.async_discovered_service_info",
|
||||
return_value=[NOT_IDASEN_DISCOVERY_INFO, IDASEN_DISCOVERY_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.connect",
|
||||
side_effect=RuntimeError,
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect",
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["step_id"] == "user"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.connect",
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect",
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result2["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result3["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == IDASEN_DISCOVERY_INFO.name
|
||||
assert result3["data"] == {
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
}
|
||||
assert result3["result"].unique_id == IDASEN_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_bluetooth_step_success(hass: HomeAssistant) -> None:
|
||||
"""Test bluetooth step success path."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_BLUETOOTH},
|
||||
data=IDASEN_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch("homeassistant.components.idasen_desk.config_flow.Desk.connect"), patch(
|
||||
"homeassistant.components.idasen_desk.config_flow.Desk.disconnect"
|
||||
), patch(
|
||||
"homeassistant.components.idasen_desk.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == IDASEN_DISCOVERY_INFO.name
|
||||
assert result2["data"] == {
|
||||
CONF_ADDRESS: IDASEN_DISCOVERY_INFO.address,
|
||||
}
|
||||
assert result2["result"].unique_id == IDASEN_DISCOVERY_INFO.address
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Test the IKEA Idasen Desk cover."""
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_POSITION,
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
SERVICE_CLOSE_COVER,
|
||||
SERVICE_OPEN_COVER,
|
||||
SERVICE_SET_COVER_POSITION,
|
||||
SERVICE_STOP_COVER,
|
||||
STATE_CLOSED,
|
||||
STATE_OPEN,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
|
||||
|
||||
async def test_cover_available(
|
||||
hass: HomeAssistant,
|
||||
mock_desk_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test cover available property."""
|
||||
entity_id = "cover.test"
|
||||
await init_integration(hass)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OPEN
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 60
|
||||
|
||||
mock_desk_api.is_connected = False
|
||||
mock_desk_api.trigger_update_callback(None)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "expected_state", "expected_position"),
|
||||
[
|
||||
(SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 100}, STATE_OPEN, 100),
|
||||
(SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 0}, STATE_CLOSED, 0),
|
||||
(SERVICE_OPEN_COVER, {}, STATE_OPEN, 100),
|
||||
(SERVICE_CLOSE_COVER, {}, STATE_CLOSED, 0),
|
||||
(SERVICE_STOP_COVER, {}, STATE_OPEN, 60),
|
||||
],
|
||||
)
|
||||
async def test_cover_services(
|
||||
hass: HomeAssistant,
|
||||
mock_desk_api: MagicMock,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
expected_state: str,
|
||||
expected_position: int,
|
||||
) -> None:
|
||||
"""Test cover services."""
|
||||
entity_id = "cover.test"
|
||||
await init_integration(hass)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_OPEN
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 60
|
||||
await hass.services.async_call(
|
||||
COVER_DOMAIN,
|
||||
service,
|
||||
{"entity_id": entity_id, **service_data},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == expected_state
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == expected_position
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Test the IKEA Idasen Desk init."""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from bleak import BleakError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.idasen_desk.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
|
||||
|
||||
async def test_setup_and_shutdown(
|
||||
hass: HomeAssistant,
|
||||
mock_desk_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup."""
|
||||
entry = await init_integration(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
mock_desk_api.connect.assert_called_once()
|
||||
mock_desk_api.is_connected = True
|
||||
|
||||
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
|
||||
await hass.async_block_till_done()
|
||||
mock_desk_api.disconnect.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception", [TimeoutError(), BleakError()])
|
||||
async def test_setup_connect_exception(
|
||||
hass: HomeAssistant, mock_desk_api: MagicMock, exception: Exception
|
||||
) -> None:
|
||||
"""Test setup with an connection exception."""
|
||||
mock_desk_api.connect = AsyncMock(side_effect=exception)
|
||||
entry = await init_integration(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_entry(hass: HomeAssistant, mock_desk_api: MagicMock) -> None:
|
||||
"""Test successful unload of entry."""
|
||||
entry = await init_integration(hass)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
mock_desk_api.connect.assert_called_once()
|
||||
mock_desk_api.is_connected = True
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
mock_desk_api.disconnect.assert_called_once()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
Reference in New Issue
Block a user