mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Add integration for IKEA Idasen Desk (#99173)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
6c095a963d
commit
bd9bab000e
@ -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.*
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "ikea",
|
||||
"name": "IKEA",
|
||||
"integrations": ["symfonisk", "tradfri"]
|
||||
"integrations": ["symfonisk", "tradfri", "idasen_desk"]
|
||||
}
|
||||
|
94
homeassistant/components/idasen_desk/__init__.py
Normal file
94
homeassistant/components/idasen_desk/__init__.py
Normal 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
|
115
homeassistant/components/idasen_desk/config_flow.py
Normal file
115
homeassistant/components/idasen_desk/config_flow.py
Normal 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,
|
||||
)
|
6
homeassistant/components/idasen_desk/const.py
Normal file
6
homeassistant/components/idasen_desk/const.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Constants for the Idasen Desk integration."""
|
||||
|
||||
|
||||
DOMAIN = "idasen_desk"
|
||||
|
||||
EXPECTED_SERVICE_UUID = "99fa0001-338a-1024-8a49-009c0215f78a"
|
101
homeassistant/components/idasen_desk/cover.py
Normal file
101
homeassistant/components/idasen_desk/cover.py
Normal 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()
|
15
homeassistant/components/idasen_desk/manifest.json
Normal file
15
homeassistant/components/idasen_desk/manifest.json
Normal 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"]
|
||||
}
|
22
homeassistant/components/idasen_desk/strings.json
Normal file
22
homeassistant/components/idasen_desk/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -210,6 +210,7 @@ FLOWS = {
|
||||
"iaqualink",
|
||||
"ibeacon",
|
||||
"icloud",
|
||||
"idasen_desk",
|
||||
"ifttt",
|
||||
"imap",
|
||||
"inkbird",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
10
mypy.ini
10
mypy.ini
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
51
tests/components/idasen_desk/__init__.py
Normal file
51
tests/components/idasen_desk/__init__.py
Normal 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
tests/components/idasen_desk/conftest.py
Normal file
49
tests/components/idasen_desk/conftest.py
Normal 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
|
230
tests/components/idasen_desk/test_config_flow.py
Normal file
230
tests/components/idasen_desk/test_config_flow.py
Normal 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
|
82
tests/components/idasen_desk/test_cover.py
Normal file
82
tests/components/idasen_desk/test_cover.py
Normal 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
|
55
tests/components/idasen_desk/test_init.py
Normal file
55
tests/components/idasen_desk/test_init.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user