Add integration for IKEA Idasen Desk (#99173)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Abílio Costa
2023-09-20 02:44:35 +02:00
committed by GitHub
co-authored by J. Nick Koston
parent 6c095a963d
commit bd9bab000e
20 changed files with 851 additions and 1 deletions
+51
View File
@@ -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
+49
View File
@@ -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
+55
View File
@@ -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