diff --git a/tests/components/motionblinds_ble/__init__.py b/tests/components/motionblinds_ble/__init__.py index c2385555dbf..e1caef9f51f 100644 --- a/tests/components/motionblinds_ble/__init__.py +++ b/tests/components/motionblinds_ble/__init__.py @@ -1 +1,16 @@ """Tests for the Motionblinds Bluetooth integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Mock a fully setup config entry.""" + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index 00db23734dd..f89cf4f305d 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -3,21 +3,140 @@ from collections.abc import Generator from unittest.mock import AsyncMock, Mock, patch +from motionblindsble.const import MotionBlindType import pytest -TEST_MAC = "abcd" -TEST_NAME = f"MOTION_{TEST_MAC.upper()}" -TEST_ADDRESS = "test_adress" +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.motionblinds_ble.const import ( + CONF_BLIND_TYPE, + CONF_LOCAL_NAME, + CONF_MAC_CODE, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS + +from tests.common import MockConfigEntry +from tests.components.bluetooth import generate_advertisement_data, generate_ble_device -@pytest.fixture(name="motionblinds_ble_connect", autouse=True) -def motion_blinds_connect_fixture( - enable_bluetooth: None, +@pytest.fixture +def address() -> str: + """Address fixture.""" + return "cc:cc:cc:cc:cc:cc" + + +@pytest.fixture +def mac_code(address: str) -> str: + """MAC code fixture.""" + return "".join(address.split(":")[-3:-1]).upper() + + +@pytest.fixture +def display_name(mac_code: str) -> str: + """Display name fixture.""" + return f"Motionblind {mac_code.upper()}" + + +@pytest.fixture +def name(display_name: str) -> str: + """Name fixture.""" + return display_name.lower().replace(" ", "_") + + +@pytest.fixture +def local_name(mac_code: str) -> str: + """Local name fixture.""" + return f"MOTION_{mac_code.upper()}" + + +@pytest.fixture +def blind_type() -> MotionBlindType: + """Blind type fixture.""" + return MotionBlindType.ROLLER + + +@pytest.fixture +def service_info(local_name: str, address: str) -> BluetoothServiceInfoBleak: + """Service info fixture.""" + return BluetoothServiceInfoBleak( + name=local_name, + address=address, + device=generate_ble_device( + address=address, + name=local_name, + ), + rssi=-61, + manufacturer_data={000: b"test"}, + service_data={ + "test": bytearray(b"0000"), + }, + service_uuids=[ + "test", + ], + source="local", + advertisement=generate_advertisement_data( + manufacturer_data={000: b"test"}, + service_uuids=["test"], + ), + connectable=True, + time=0, + tx_power=-127, + ) + + +@pytest.fixture +def mock_motion_device( + blind_type: MotionBlindType, display_name: str +) -> Generator[AsyncMock]: + """Mock a MotionDevice.""" + + with patch( + "homeassistant.components.motionblinds_ble.MotionDevice", + autospec=True, + ) as mock_device: + device = mock_device.return_value + device.ble_device = Mock() + device.display_name = display_name + device.blind_type = blind_type + yield device + + +@pytest.fixture +def mock_config_entry( + blind_type: MotionBlindType, address: str, display_name: str, mac_code: str +) -> MockConfigEntry: + """Config entry fixture.""" + return MockConfigEntry( + title="mock_title", + domain=DOMAIN, + unique_id=address, + data={ + CONF_ADDRESS: address, + CONF_LOCAL_NAME: display_name, + CONF_MAC_CODE: mac_code, + CONF_BLIND_TYPE: blind_type.name.lower(), + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.motionblinds_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def motionblinds_ble_connect( + enable_bluetooth: None, local_name: str, address: str ) -> Generator[tuple[AsyncMock, Mock]]: """Mock motion blinds ble connection and entry setup.""" device = Mock() - device.name = TEST_NAME - device.address = TEST_ADDRESS + device.name = local_name + device.address = address bleak_scanner = AsyncMock() bleak_scanner.discover.return_value = [device] @@ -31,9 +150,5 @@ def motion_blinds_connect_fixture( "homeassistant.components.motionblinds_ble.config_flow.bluetooth.async_get_scanner", return_value=bleak_scanner, ), - patch( - "homeassistant.components.motionblinds_ble.async_setup_entry", - return_value=True, - ), ): yield bleak_scanner, device diff --git a/tests/components/motionblinds_ble/test_button.py b/tests/components/motionblinds_ble/test_button.py new file mode 100644 index 00000000000..f0f80762759 --- /dev/null +++ b/tests/components/motionblinds_ble/test_button.py @@ -0,0 +1,47 @@ +"""Tests for Motionblinds BLE buttons.""" + +from unittest.mock import Mock + +import pytest + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.components.motionblinds_ble.const import ( + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("motionblinds_ble_connect") +@pytest.mark.parametrize( + ("button"), + [ + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, + ], +) +async def test_button( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + button: str, +) -> None: + """Test states of the button.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: f"button.{name}_{button}"}, + blocking=True, + ) + getattr(mock_motion_device, button).assert_called_once() diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 4cab12269dd..05d3077ceb1 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -12,41 +12,19 @@ from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import TEST_ADDRESS, TEST_MAC, TEST_NAME - from tests.common import MockConfigEntry -from tests.components.bluetooth import generate_advertisement_data, generate_ble_device - -TEST_BLIND_TYPE = MotionBlindType.ROLLER.name.lower() - -BLIND_SERVICE_INFO = BluetoothServiceInfoBleak( - name=TEST_NAME, - address=TEST_ADDRESS, - device=generate_ble_device( - address="cc:cc:cc:cc:cc:cc", - name=TEST_NAME, - ), - rssi=-61, - manufacturer_data={000: b"test"}, - service_data={ - "test": bytearray(b"0000"), - }, - service_uuids=[ - "test", - ], - source="local", - advertisement=generate_advertisement_data( - manufacturer_data={000: b"test"}, - service_uuids=["test"], - ), - connectable=True, - time=0, - tx_power=-127, -) @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_manual_success(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_config_flow_manual_success( + hass: HomeAssistant, + blind_type: MotionBlindType, + mac_code: str, + address: str, + local_name: str, + display_name: str, +) -> None: """Successful flow manually initialized by the user.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -57,28 +35,36 @@ async def test_config_flow_manual_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_config_flow_manual_error_invalid_mac( + hass: HomeAssistant, + mac_code: str, + address: str, + local_name: str, + display_name: str, + blind_type: MotionBlindType, +) -> None: """Invalid MAC code error flow manually initialized by the user.""" # Initialize @@ -101,7 +87,7 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None # Recover result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -109,15 +95,15 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None # Finish flow result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} @@ -125,6 +111,7 @@ async def test_config_flow_manual_error_invalid_mac(hass: HomeAssistant) -> None @pytest.mark.usefixtures("motionblinds_ble_connect") async def test_config_flow_manual_error_no_bluetooth_adapter( hass: HomeAssistant, + mac_code: str, ) -> None: """No Bluetooth adapter error flow manually initialized by the user.""" @@ -153,14 +140,21 @@ async def test_config_flow_manual_error_no_bluetooth_adapter( ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_BLUETOOTH_ADAPTER +@pytest.mark.usefixtures("mock_setup_entry") async def test_config_flow_manual_error_could_not_find_motor( - hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] + hass: HomeAssistant, + motionblinds_ble_connect: tuple[AsyncMock, Mock], + mac_code: str, + local_name: str, + display_name: str, + address: str, + blind_type: MotionBlindType, ) -> None: """Could not find motor error flow manually initialized by the user.""" @@ -176,17 +170,17 @@ async def test_config_flow_manual_error_could_not_find_motor( motionblinds_ble_connect[1].name = "WRONG_NAME" result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": const.ERROR_COULD_NOT_FIND_MOTOR} # Recover - motionblinds_ble_connect[1].name = TEST_NAME + motionblinds_ble_connect[1].name = local_name result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm" @@ -194,21 +188,23 @@ async def test_config_flow_manual_error_could_not_find_motor( # Finish flow result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} async def test_config_flow_manual_error_no_devices_found( - hass: HomeAssistant, motionblinds_ble_connect: tuple[AsyncMock, Mock] + hass: HomeAssistant, + motionblinds_ble_connect: tuple[AsyncMock, Mock], + mac_code: str, ) -> None: """No devices found error flow manually initialized by the user.""" @@ -224,19 +220,27 @@ async def test_config_flow_manual_error_no_devices_found( motionblinds_ble_connect[0].discover.return_value = [] result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_MAC_CODE: TEST_MAC}, + {const.CONF_MAC_CODE: mac_code}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == const.ERROR_NO_DEVICES_FOUND @pytest.mark.usefixtures("motionblinds_ble_connect") -async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: +async def test_config_flow_bluetooth_success( + hass: HomeAssistant, + mac_code: str, + service_info: BluetoothServiceInfoBleak, + address: str, + local_name: str, + display_name: str, + blind_type: MotionBlindType, +) -> None: """Successful bluetooth discovery flow.""" result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, - data=BLIND_SERVICE_INFO, + data=service_info, ) assert result["type"] is FlowResultType.FORM @@ -244,36 +248,32 @@ async def test_config_flow_bluetooth_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {const.CONF_BLIND_TYPE: MotionBlindType.ROLLER.name.lower()}, + {const.CONF_BLIND_TYPE: blind_type.name.lower()}, ) assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == f"Motionblind {TEST_MAC.upper()}" + assert result["title"] == display_name assert result["data"] == { - CONF_ADDRESS: TEST_ADDRESS, - const.CONF_LOCAL_NAME: TEST_NAME, - const.CONF_MAC_CODE: TEST_MAC.upper(), - const.CONF_BLIND_TYPE: TEST_BLIND_TYPE, + CONF_ADDRESS: address, + const.CONF_LOCAL_NAME: local_name, + const.CONF_MAC_CODE: mac_code, + const.CONF_BLIND_TYPE: blind_type.name.lower(), } assert result["options"] == {} -async def test_options_flow(hass: HomeAssistant) -> None: +@pytest.mark.usefixtures("mock_setup_entry") +async def test_options_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: """Test the options flow.""" - entry = MockConfigEntry( - domain=const.DOMAIN, - unique_id="0123456789", - data={ - const.CONF_BLIND_TYPE: MotionBlindType.ROLLER, - }, - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" diff --git a/tests/components/motionblinds_ble/test_cover.py b/tests/components/motionblinds_ble/test_cover.py new file mode 100644 index 00000000000..2e6f1ad587a --- /dev/null +++ b/tests/components/motionblinds_ble/test_cover.py @@ -0,0 +1,124 @@ +"""Tests for Motionblinds BLE covers.""" + +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import MotionBlindType, MotionRunningType +import pytest + +from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("blind_type", [MotionBlindType.VENETIAN]) +@pytest.mark.parametrize( + ("service", "method", "kwargs"), + [ + (SERVICE_OPEN_COVER, "open", {}), + (SERVICE_CLOSE_COVER, "close", {}), + (SERVICE_OPEN_COVER_TILT, "open_tilt", {}), + (SERVICE_CLOSE_COVER_TILT, "close_tilt", {}), + (SERVICE_SET_COVER_POSITION, "position", {ATTR_POSITION: 5}), + (SERVICE_SET_COVER_TILT_POSITION, "tilt", {ATTR_TILT_POSITION: 10}), + (SERVICE_STOP_COVER, "stop", {}), + (SERVICE_STOP_COVER_TILT, "stop", {}), + ], +) +async def test_cover_service( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + service: str, + method: str, + kwargs: dict[str, Any], +) -> None: + """Test cover service.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + COVER_DOMAIN, + service, + {ATTR_ENTITY_ID: f"cover.{name}", **kwargs}, + blocking=True, + ) + getattr(mock_motion_device, method).assert_called_once() + + +@pytest.mark.parametrize( + ("running_type", "state"), + [ + (None, "unknown"), + (MotionRunningType.STILL, "unknown"), + (MotionRunningType.OPENING, STATE_OPENING), + (MotionRunningType.CLOSING, STATE_CLOSING), + ], +) +async def test_cover_update_running( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + running_type: str | None, + state: str, +) -> None: + """Test updating running status.""" + + await setup_integration(hass, mock_config_entry) + + async_update_running = mock_motion_device.register_running_callback.call_args[0][0] + + async_update_running(running_type) + assert hass.states.get(f"cover.{name}").state == state + + +@pytest.mark.parametrize( + ("position", "tilt", "state"), + [ + (None, None, "unknown"), + (0, 0, STATE_OPEN), + (50, 90, STATE_OPEN), + (100, 180, STATE_CLOSED), + ], +) +async def test_cover_update_position( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + position: int, + tilt: int, + state: str, +) -> None: + """Test updating cover position and tilt.""" + + await setup_integration(hass, mock_config_entry) + + async_update_position = mock_motion_device.register_position_callback.call_args[0][ + 0 + ] + + async_update_position(position, tilt) + assert hass.states.get(f"cover.{name}").state == state diff --git a/tests/components/motionblinds_ble/test_entity.py b/tests/components/motionblinds_ble/test_entity.py new file mode 100644 index 00000000000..d5927e438a5 --- /dev/null +++ b/tests/components/motionblinds_ble/test_entity.py @@ -0,0 +1,54 @@ +"""Tests for Motionblinds BLE entities.""" + +from unittest.mock import Mock + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.motionblinds_ble.const import ( + ATTR_CONNECT, + ATTR_DISCONNECT, + ATTR_FAVORITE, + ATTR_SPEED, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + ("platform", "entity"), + [ + (Platform.BUTTON, ATTR_CONNECT), + (Platform.BUTTON, ATTR_DISCONNECT), + (Platform.BUTTON, ATTR_FAVORITE), + (Platform.SELECT, ATTR_SPEED), + ], +) +async def test_entity_update( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + platform: Platform, + entity: str, +) -> None: + """Test updating entity using homeassistant.update_entity.""" + + await async_setup_component(hass, HA_DOMAIN, {}) + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: f"{platform.name.lower()}.{name}_{entity}"}, + blocking=True, + ) + getattr(mock_motion_device, "status_query").assert_called_once_with() diff --git a/tests/components/motionblinds_ble/test_init.py b/tests/components/motionblinds_ble/test_init.py new file mode 100644 index 00000000000..706dfdc2f01 --- /dev/null +++ b/tests/components/motionblinds_ble/test_init.py @@ -0,0 +1,48 @@ +"""Tests for Motionblinds BLE init.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.motionblinds_ble import options_update_listener +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_options_update_listener( + mock_config_entry: MockConfigEntry, hass: HomeAssistant +) -> None: + """Test options_update_listener.""" + + await setup_integration(hass, mock_config_entry) + + with ( + patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_custom_disconnect_time" + ) as mock_set_custom_disconnect_time, + patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_permanent_connection" + ) as set_permanent_connection, + ): + await options_update_listener(hass, mock_config_entry) + mock_set_custom_disconnect_time.assert_called_once() + set_permanent_connection.assert_called_once() + + +async def test_update_ble_device( + mock_config_entry: MockConfigEntry, + hass: HomeAssistant, + service_info: BluetoothServiceInfoBleak, +) -> None: + """Test async_update_ble_device.""" + + await setup_integration(hass, mock_config_entry) + + with patch( + "homeassistant.components.motionblinds_ble.MotionDevice.set_ble_device" + ) as mock_set_ble_device: + inject_bluetooth_service_info(hass, service_info) + mock_set_ble_device.assert_called_once() diff --git a/tests/components/motionblinds_ble/test_select.py b/tests/components/motionblinds_ble/test_select.py new file mode 100644 index 00000000000..813df89e387 --- /dev/null +++ b/tests/components/motionblinds_ble/test_select.py @@ -0,0 +1,76 @@ +"""Tests for Motionblinds BLE selects.""" + +from collections.abc import Callable +from enum import Enum +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import MotionSpeedLevel +from motionblindsble.device import MotionDevice +import pytest + +from homeassistant.components.motionblinds_ble.const import ATTR_SPEED +from homeassistant.components.select import ( + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize(("select", "args"), [(ATTR_SPEED, MotionSpeedLevel.HIGH)]) +async def test_select( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + select: str, + args: Any, +) -> None: + """Test select.""" + + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: f"select.{name}_{select}", + ATTR_OPTION: MotionSpeedLevel.HIGH.value, + }, + blocking=True, + ) + getattr(mock_motion_device, select).assert_called_once_with(args) + + +@pytest.mark.parametrize( + ("select", "register_callback", "value"), + [ + ( + ATTR_SPEED, + lambda device: device.register_speed_callback, + MotionSpeedLevel.HIGH, + ) + ], +) +async def test_select_update( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + select: str, + register_callback: Callable[[MotionDevice], Callable[..., None]], + value: type[Enum], +) -> None: + """Test select state update.""" + + await setup_integration(hass, mock_config_entry) + + update_func = register_callback(mock_motion_device).call_args[0][0] + + update_func(value) + assert hass.states.get(f"select.{name}_{select}").state == str(value.value) diff --git a/tests/components/motionblinds_ble/test_sensor.py b/tests/components/motionblinds_ble/test_sensor.py new file mode 100644 index 00000000000..4859fc643c9 --- /dev/null +++ b/tests/components/motionblinds_ble/test_sensor.py @@ -0,0 +1,107 @@ +"""Tests for Motionblinds BLE sensors.""" + +from collections.abc import Callable +from typing import Any +from unittest.mock import Mock + +from motionblindsble.const import ( + MotionBlindType, + MotionCalibrationType, + MotionConnectionType, +) +from motionblindsble.device import MotionDevice +import pytest + +from homeassistant.components.motionblinds_ble.const import ( + ATTR_BATTERY, + ATTR_SIGNAL_STRENGTH, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("blind_type", [MotionBlindType.CURTAIN]) +@pytest.mark.parametrize( + ("sensor", "register_callback", "initial_value", "args", "expected_value"), + [ + ( + "connection_status", + lambda device: device.register_connection_callback, + MotionConnectionType.DISCONNECTED.value, + [MotionConnectionType.CONNECTING], + MotionConnectionType.CONNECTING.value, + ), + ( + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [25, True, False], + "25", + ), + ( # Battery unknown + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [None, False, False], + "unknown", + ), + ( # Wired + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [255, False, True], + "255", + ), + ( # Almost full + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [99, False, False], + "99", + ), + ( # Almost empty + ATTR_BATTERY, + lambda device: device.register_battery_callback, + "unknown", + [1, False, False], + "1", + ), + ( + "calibration_status", + lambda device: device.register_calibration_callback, + "unknown", + [MotionCalibrationType.CALIBRATING], + MotionCalibrationType.CALIBRATING.value, + ), + ( + ATTR_SIGNAL_STRENGTH, + lambda device: device.register_signal_strength_callback, + "unknown", + [-50], + "-50", + ), + ], +) +async def test_sensor( + mock_config_entry: MockConfigEntry, + mock_motion_device: Mock, + name: str, + hass: HomeAssistant, + sensor: str, + register_callback: Callable[[MotionDevice], Callable[..., None]], + initial_value: str, + args: list[Any], + expected_value: str, +) -> None: + """Test sensors.""" + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get(f"{SENSOR_DOMAIN}.{name}_{sensor}").state == initial_value + update_func = register_callback(mock_motion_device).call_args[0][0] + update_func(*args) + assert hass.states.get(f"{SENSOR_DOMAIN}.{name}_{sensor}").state == expected_value