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 01:44:35 +01:00 committed by GitHub
parent 6c095a963d
commit bd9bab000e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 851 additions and 1 deletions

View File

@ -180,6 +180,7 @@ homeassistant.components.huawei_lte.*
homeassistant.components.hydrawise.*
homeassistant.components.hyperion.*
homeassistant.components.ibeacon.*
homeassistant.components.idasen_desk.*
homeassistant.components.image.*
homeassistant.components.image_processing.*
homeassistant.components.image_upload.*

View File

@ -569,6 +569,8 @@ build.json @home-assistant/supervisor
/tests/components/ibeacon/ @bdraco
/homeassistant/components/icloud/ @Quentame @nzapponi
/tests/components/icloud/ @Quentame @nzapponi
/homeassistant/components/idasen_desk/ @abmantis
/tests/components/idasen_desk/ @abmantis
/homeassistant/components/ign_sismologia/ @exxamalte
/tests/components/ign_sismologia/ @exxamalte
/homeassistant/components/image/ @home-assistant/core

View File

@ -1,5 +1,5 @@
{
"domain": "ikea",
"name": "IKEA",
"integrations": ["symfonisk", "tradfri"]
"integrations": ["symfonisk", "tradfri", "idasen_desk"]
}

View File

@ -0,0 +1,94 @@
"""The IKEA Idasen Desk integration."""
from __future__ import annotations
import logging
from attr import dataclass
from bleak import BleakError
from idasen_ha import Desk
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_NAME,
CONF_ADDRESS,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.COVER]
_LOGGER = logging.getLogger(__name__)
@dataclass
class DeskData:
"""Data for the Idasen Desk integration."""
desk: Desk
address: str
device_info: DeviceInfo
coordinator: DataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IKEA Idasen from a config entry."""
address: str = entry.data[CONF_ADDRESS].upper()
coordinator: DataUpdateCoordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=entry.title,
)
desk = Desk(coordinator.async_set_updated_data)
device_info = DeviceInfo(
name=entry.title,
connections={(dr.CONNECTION_BLUETOOTH, address)},
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = DeskData(
desk, address, device_info, coordinator
)
ble_device = bluetooth.async_ble_device_from_address(
hass, address, connectable=True
)
try:
await desk.connect(ble_device)
except (TimeoutError, BleakError) as ex:
raise ConfigEntryNotReady(f"Unable to connect to desk {address}") from ex
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
async def _async_stop(event: Event) -> None:
"""Close the connection."""
await desk.disconnect()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
)
return True
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
data: DeskData = hass.data[DOMAIN][entry.entry_id]
if entry.title != data.device_info[ATTR_NAME]:
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
data: DeskData = hass.data[DOMAIN].pop(entry.entry_id)
await data.desk.disconnect()
return unload_ok

View File

@ -0,0 +1,115 @@
"""Config flow for Idasen Desk integration."""
from __future__ import annotations
import logging
from typing import Any
from bleak import BleakError
from bluetooth_data_tools import human_readable_name
from idasen_ha import Desk
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN, EXPECTED_SERVICE_UUID
_LOGGER = logging.getLogger(__name__)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Idasen Desk integration."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
self._discovery_info = discovery_info
self.context["title_placeholders"] = {
"name": human_readable_name(
None, discovery_info.name, discovery_info.address
)
}
return await self.async_step_user()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user step to pick discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
address = user_input[CONF_ADDRESS]
discovery_info = self._discovered_devices[address]
local_name = discovery_info.name
await self.async_set_unique_id(
discovery_info.address, raise_on_progress=False
)
self._abort_if_unique_id_configured()
desk = Desk(None)
try:
await desk.connect(discovery_info.device, monitor_height=False)
except TimeoutError as err:
_LOGGER.exception("TimeoutError", exc_info=err)
errors["base"] = "cannot_connect"
except BleakError as err:
_LOGGER.exception("BleakError", exc_info=err)
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
await desk.disconnect()
return self.async_create_entry(
title=local_name,
data={
CONF_ADDRESS: discovery_info.address,
},
)
if discovery := self._discovery_info:
self._discovered_devices[discovery.address] = discovery
else:
current_addresses = self._async_current_ids()
for discovery in async_discovered_service_info(self.hass):
if (
discovery.address in current_addresses
or discovery.address in self._discovered_devices
or EXPECTED_SERVICE_UUID not in discovery.service_uuids
):
continue
self._discovered_devices[discovery.address] = discovery
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
data_schema = vol.Schema(
{
vol.Required(CONF_ADDRESS): vol.In(
{
service_info.address: f"{service_info.name} ({service_info.address})"
for service_info in self._discovered_devices.values()
}
),
}
)
return self.async_show_form(
step_id="user",
data_schema=data_schema,
errors=errors,
)

View File

@ -0,0 +1,6 @@
"""Constants for the Idasen Desk integration."""
DOMAIN = "idasen_desk"
EXPECTED_SERVICE_UUID = "99fa0001-338a-1024-8a49-009c0215f78a"

View File

@ -0,0 +1,101 @@
"""Idasen Desk integration cover platform."""
from __future__ import annotations
import logging
from typing import Any
from idasen_ha import Desk
from homeassistant.components.cover import (
ATTR_POSITION,
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import DeskData
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the cover platform for Idasen Desk."""
data: DeskData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[IdasenDeskCover(data.desk, data.address, data.device_info, data.coordinator)]
)
class IdasenDeskCover(CoordinatorEntity, CoverEntity):
"""Representation of Idasen Desk device."""
_attr_device_class = CoverDeviceClass.DAMPER
_attr_icon = "mdi:desk"
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.STOP
| CoverEntityFeature.SET_POSITION
)
def __init__(
self,
desk: Desk,
address: str,
device_info: DeviceInfo,
coordinator: DataUpdateCoordinator,
) -> None:
"""Initialize an Idasen Desk cover."""
super().__init__(coordinator)
self._desk = desk
self._attr_name = device_info[ATTR_NAME]
self._attr_unique_id = address
self._attr_device_info = device_info
self._attr_current_cover_position = self._desk.height_percent
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._desk.is_connected is True
@property
def is_closed(self) -> bool:
"""Return if the cover is closed."""
return self.current_cover_position == 0
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self._desk.move_down()
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self._desk.move_up()
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
await self._desk.stop()
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover shutter to a specific position."""
await self._desk.move_to(int(kwargs[ATTR_POSITION]))
@callback
def _handle_coordinator_update(self, *args: Any) -> None:
"""Handle data update."""
self._attr_current_cover_position = self._desk.height_percent
self.async_write_ha_state()

View File

@ -0,0 +1,15 @@
{
"domain": "idasen_desk",
"name": "IKEA Idasen Desk",
"bluetooth": [
{
"service_uuid": "99fa0001-338a-1024-8a49-009c0215f78a"
}
],
"codeowners": ["@abmantis"],
"config_flow": true,
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
"iot_class": "local_push",
"requirements": ["idasen-ha==1.4"]
}

View File

@ -0,0 +1,22 @@
{
"config": {
"flow_title": "{name}",
"step": {
"user": {
"data": {
"address": "Bluetooth address"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"not_supported": "Device not supported",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}

View File

@ -213,6 +213,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [
],
"manufacturer_id": 76,
},
{
"domain": "idasen_desk",
"service_uuid": "99fa0001-338a-1024-8a49-009c0215f78a",
},
{
"connectable": False,
"domain": "inkbird",

View File

@ -210,6 +210,7 @@ FLOWS = {
"iaqualink",
"ibeacon",
"icloud",
"idasen_desk",
"ifttt",
"imap",
"inkbird",

View File

@ -2578,6 +2578,12 @@
"config_flow": true,
"iot_class": "local_polling",
"name": "IKEA TR\u00c5DFRI"
},
"idasen_desk": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "IKEA Idasen Desk"
}
}
},

View File

@ -1562,6 +1562,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.idasen_desk.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.image.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -1042,6 +1042,9 @@ ical==5.0.1
# homeassistant.components.ping
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==1.4
# homeassistant.components.network
ifaddr==0.2.0

View File

@ -819,6 +819,9 @@ ical==5.0.1
# homeassistant.components.ping
icmplib==3.0
# homeassistant.components.idasen_desk
idasen-ha==1.4
# homeassistant.components.network
ifaddr==0.2.0

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

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

View File

@ -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

View File

@ -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

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