"""BleBox cover entities tests.""" import logging from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi from blebox_uniapi.cover import UnifiedCoverType import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, CoverDeviceClass, CoverEntityFeature, CoverState, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, 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, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .conftest import async_setup_config_entry, async_setup_entity, mock_feature from tests.common import MockConfigEntry ALL_COVER_FIXTURES = ["gatecontroller", "shutterbox", "gatebox"] FIXTURES_SUPPORTING_STOP = ["gatecontroller", "shutterbox"] @pytest.fixture(name="shutterbox") def shutterbox_fixture(): """Return a shutterBox fixture.""" feature = mock_feature( "covers", blebox_uniapi.cover.Cover, unique_id="BleBox-shutterBox-2bee34e750b8-position", full_name="shutterBox-position", device_class="shutter", current=None, tilt_current=None, state=None, has_stop=True, has_tilt=True, is_slider=True, is_position_inverted=True, cover_type=None, tilt_only=False, ) product = feature.product type(product).name = PropertyMock(return_value="My shutter") type(product).model = PropertyMock(return_value="shutterBox") return (feature, "cover.my_shutter_shutterbox_position") @pytest.fixture(name="gatebox") def gatebox_fixture(): """Return a gateBox fixture.""" feature = mock_feature( "covers", blebox_uniapi.cover.Cover, unique_id="BleBox-gateBox-1afe34db9437-position", device_class="gatebox", full_name="gateBox-position", current=None, state=None, has_stop=False, is_slider=False, is_position_inverted=False, cover_type=None, tilt_only=False, ) product = feature.product type(product).name = PropertyMock(return_value="My gatebox") type(product).model = PropertyMock(return_value="gateBox") return (feature, "cover.my_gatebox_gatebox_position") @pytest.fixture(name="gatecontroller") def gate_fixture(): """Return a gateController fixture.""" feature = mock_feature( "covers", blebox_uniapi.cover.Cover, unique_id="BleBox-gateController-2bee34e750b8-position", full_name="gateController-position", device_class="gate", current=None, state=None, has_stop=True, is_slider=True, is_position_inverted=True, cover_type=None, tilt_only=False, ) product = feature.product type(product).name = PropertyMock(return_value="My gate controller") type(product).model = PropertyMock(return_value="gateController") return (feature, "cover.my_gate_controller_gatecontroller_position") async def test_init_gatecontroller( gatecontroller, hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test gateController default state.""" _, entity_id = gatecontroller entry = await async_setup_entity(hass, entity_id) assert entry.unique_id == "BleBox-gateController-2bee34e750b8-position" state = hass.states.get(entity_id) assert state.name == "My gate controller gateController-position" assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.GATE supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & CoverEntityFeature.OPEN assert supported_features & CoverEntityFeature.CLOSE assert supported_features & CoverEntityFeature.STOP assert supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN device = device_registry.async_get(entry.device_id) assert device.name == "My gate controller" assert device.identifiers == {("blebox", "abcd0123ef5678")} assert device.manufacturer == "BleBox" assert device.model == "gateController" assert device.sw_version == "1.23" async def test_init_shutterbox( shutterbox, hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test gateBox default state.""" _, entity_id = shutterbox entry = await async_setup_entity(hass, entity_id) assert entry.unique_id == "BleBox-shutterBox-2bee34e750b8-position" state = hass.states.get(entity_id) assert state.name == "My shutter shutterBox-position" assert entry.original_device_class == CoverDeviceClass.SHUTTER supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & CoverEntityFeature.OPEN assert supported_features & CoverEntityFeature.CLOSE assert supported_features & CoverEntityFeature.STOP assert supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN device = device_registry.async_get(entry.device_id) assert device.name == "My shutter" assert device.identifiers == {("blebox", "abcd0123ef5678")} assert device.manufacturer == "BleBox" assert device.model == "shutterBox" assert device.sw_version == "1.23" async def test_init_gatebox( gatebox, hass: HomeAssistant, device_registry: dr.DeviceRegistry ) -> None: """Test cover default state.""" _, entity_id = gatebox entry = await async_setup_entity(hass, entity_id) assert entry.unique_id == "BleBox-gateBox-1afe34db9437-position" state = hass.states.get(entity_id) assert state.name == "My gatebox gateBox-position" assert state.attributes[ATTR_DEVICE_CLASS] == CoverDeviceClass.DOOR supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & CoverEntityFeature.OPEN assert supported_features & CoverEntityFeature.CLOSE # Not available during init since requires fetching state to detect assert not supported_features & CoverEntityFeature.STOP assert not supported_features & CoverEntityFeature.SET_POSITION assert ATTR_CURRENT_POSITION not in state.attributes assert state.state == STATE_UNKNOWN device = device_registry.async_get(entry.device_id) assert device.name == "My gatebox" assert device.identifiers == {("blebox", "abcd0123ef5678")} assert device.manufacturer == "BleBox" assert device.model == "gateBox" assert device.sw_version == "1.23" @pytest.mark.parametrize( ("cover_type", "expected_device_class"), [ pytest.param(UnifiedCoverType.AWNING, CoverDeviceClass.AWNING, id="awning"), pytest.param(UnifiedCoverType.BLIND, CoverDeviceClass.BLIND, id="blind"), pytest.param(UnifiedCoverType.CURTAIN, CoverDeviceClass.CURTAIN, id="curtain"), pytest.param(UnifiedCoverType.DAMPER, CoverDeviceClass.DAMPER, id="damper"), pytest.param(UnifiedCoverType.DOOR, CoverDeviceClass.DOOR, id="door"), pytest.param(UnifiedCoverType.GARAGE, CoverDeviceClass.GARAGE, id="garage"), pytest.param(UnifiedCoverType.GATE, CoverDeviceClass.GATE, id="gate"), pytest.param(UnifiedCoverType.SHADE, CoverDeviceClass.SHADE, id="shade"), pytest.param(UnifiedCoverType.SHUTTER, CoverDeviceClass.SHUTTER, id="shutter"), pytest.param(UnifiedCoverType.WINDOW, CoverDeviceClass.WINDOW, id="window"), ], ) async def test_device_class_from_unified_cover_type( hass: HomeAssistant, cover_type: UnifiedCoverType, expected_device_class: CoverDeviceClass, ) -> None: """Test that device class is resolved from unified cover type when available.""" feature = mock_feature( "covers", blebox_uniapi.cover.Cover, unique_id="BleBox-shutterBox-2bee34e750b8-position", full_name="shutterBox-position", device_class="shutter", current=None, tilt_current=None, state=None, has_stop=True, has_tilt=False, tilt_only=False, is_slider=True, is_position_inverted=True, cover_type=cover_type, ) product = feature.product type(product).name = PropertyMock(return_value="My shutter") type(product).model = PropertyMock(return_value="shutterBox") entity_id = "cover.my_shutter_shutterbox_position" entry = await async_setup_entity(hass, entity_id) assert entry.original_device_class == expected_device_class @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_open(feature, hass: HomeAssistant) -> None: """Test cover opening.""" feature_mock, entity_id = feature feature_mock.state = 3 # manually stopped await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.CLOSED def open_gate(): feature_mock.state = 1 # opening feature_mock.async_open = AsyncMock(side_effect=open_gate) await hass.services.async_call( "cover", SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True, ) assert hass.states.get(entity_id).state == CoverState.OPENING @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_close(feature, hass: HomeAssistant) -> None: """Test cover closing.""" feature_mock, entity_id = feature feature_mock.state = 4 # open await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.OPEN def close(): feature_mock.state = 0 # closing feature_mock.async_close = AsyncMock(side_effect=close) await hass.services.async_call( "cover", SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == CoverState.CLOSING @pytest.mark.parametrize("feature", FIXTURES_SUPPORTING_STOP, indirect=["feature"]) async def test_stop(feature, hass: HomeAssistant) -> None: """Test cover stopping.""" feature_mock, entity_id = feature feature_mock.state = 1 # opening await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.OPENING def stop(): feature_mock.state = 2 # manually stopped feature_mock.async_stop = AsyncMock(side_effect=stop) await hass.services.async_call( "cover", SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == CoverState.OPEN @pytest.mark.parametrize( "feature", ["gatecontroller", "shutterbox"], indirect=["feature"] ) async def test_update_inverted(feature, hass: HomeAssistant) -> None: """Test cover position is inverted for shutterBox and gateController.""" feature_mock, entity_id = feature feature_mock.current = 29 # device: 29% closed = 71% open feature_mock.state = 2 # manually stopped await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_POSITION] == 71 # 100 - 29 assert state.state == CoverState.OPEN async def test_update_not_inverted(gatebox, hass: HomeAssistant) -> None: """Test cover position is not inverted for gateBox.""" feature_mock, entity_id = gatebox feature_mock.current = 100 # fully open feature_mock.state = 4 # open await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_POSITION] == 100 assert state.state == CoverState.OPEN @pytest.mark.parametrize( "feature", ["gatecontroller", "shutterbox"], indirect=["feature"] ) async def test_set_position(feature, hass: HomeAssistant) -> None: """Test cover position setting.""" feature_mock, entity_id = feature feature_mock.state = 3 # closed await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.CLOSED def set_position(position): assert position == 99 # inverted feature_mock.state = 1 # opening # feature_mock.current = position feature_mock.async_set_position = AsyncMock(side_effect=set_position) await hass.services.async_call( "cover", SERVICE_SET_COVER_POSITION, {"entity_id": entity_id, ATTR_POSITION: 1}, blocking=True, ) # almost closed assert hass.states.get(entity_id).state == CoverState.OPENING async def test_unknown_position(shutterbox, hass: HomeAssistant) -> None: """Test cover position setting.""" feature_mock, entity_id = shutterbox feature_mock.state = 4 # opening feature_mock.current = -1 await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) assert state.state == CoverState.OPEN assert ATTR_CURRENT_POSITION not in state.attributes async def test_with_stop(gatebox, hass: HomeAssistant) -> None: """Test stop capability is available.""" feature_mock, entity_id = gatebox feature_mock.state = 1 # opening feature_mock.has_stop = True await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert supported_features & CoverEntityFeature.STOP async def test_with_no_stop(gatebox, hass: HomeAssistant) -> None: """Test stop capability is not available.""" feature_mock, entity_id = gatebox feature_mock.state = 1 # opening feature_mock.has_stop = False await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] assert not supported_features & CoverEntityFeature.STOP async def test_tilt_only_supported_features(shutterbox, hass: HomeAssistant) -> None: """Test that tilt_only removes position/open/close/stop features.""" feature_mock, entity_id = shutterbox feature_mock.tilt_only = True await async_setup_entity(hass, entity_id) supported_features = hass.states.get(entity_id).attributes[ATTR_SUPPORTED_FEATURES] assert not supported_features & CoverEntityFeature.OPEN assert not supported_features & CoverEntityFeature.CLOSE assert not supported_features & CoverEntityFeature.SET_POSITION assert not supported_features & CoverEntityFeature.STOP assert supported_features & CoverEntityFeature.OPEN_TILT assert supported_features & CoverEntityFeature.CLOSE_TILT assert supported_features & CoverEntityFeature.SET_TILT_POSITION async def test_tilt_with_position_supported_features( shutterbox, hass: HomeAssistant ) -> None: """Test that has_tilt without tilt_only keeps both position and tilt features.""" await async_setup_entity(hass, shutterbox[1]) supported_features = hass.states.get(shutterbox[1]).attributes[ ATTR_SUPPORTED_FEATURES ] assert supported_features & CoverEntityFeature.OPEN assert supported_features & CoverEntityFeature.CLOSE assert supported_features & CoverEntityFeature.SET_POSITION assert supported_features & CoverEntityFeature.STOP assert supported_features & CoverEntityFeature.OPEN_TILT assert supported_features & CoverEntityFeature.CLOSE_TILT assert supported_features & CoverEntityFeature.SET_TILT_POSITION @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_update_failure( feature, hass: HomeAssistant, config_entry: MockConfigEntry, caplog: pytest.LogCaptureFixture, ) -> None: """Test that update failures cause config entry setup retry.""" caplog.set_level(logging.ERROR) feature_mock, _entity_id = feature feature_mock.product.async_update_data = AsyncMock( side_effect=blebox_uniapi.error.ClientError ) await async_setup_config_entry(hass, config_entry) feature_mock.product.async_update_data.assert_called() assert config_entry.state is ConfigEntryState.SETUP_RETRY @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_opening_state(feature, hass: HomeAssistant) -> None: """Test that entity properties work.""" feature_mock, entity_id = feature feature_mock.state = 1 # opening await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.OPENING @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_closing_state(feature, hass: HomeAssistant) -> None: """Test that entity properties work.""" feature_mock, entity_id = feature feature_mock.state = 0 # closing await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.CLOSING @pytest.mark.parametrize("feature", ALL_COVER_FIXTURES, indirect=["feature"]) async def test_closed_state(feature, hass: HomeAssistant) -> None: """Test that entity properties work.""" feature_mock, entity_id = feature feature_mock.state = 3 # closed await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.CLOSED async def test_tilt_position(shutterbox, hass: HomeAssistant) -> None: """Test tilt capability is available.""" feature_mock, entity_id = shutterbox feature_mock.tilt_current = 90 await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 10 async def test_set_tilt_position(shutterbox, hass: HomeAssistant) -> None: """Test tilt position setting.""" feature_mock, entity_id = shutterbox feature_mock.state = 3 await async_setup_entity(hass, entity_id) assert hass.states.get(entity_id).state == CoverState.CLOSED def set_tilt(tilt_position): assert tilt_position == 20 feature_mock.state = 1 feature_mock.async_set_tilt_position = AsyncMock(side_effect=set_tilt) await hass.services.async_call( "cover", SERVICE_SET_COVER_TILT_POSITION, {"entity_id": entity_id, ATTR_TILT_POSITION: 80}, blocking=True, ) assert hass.states.get(entity_id).state == CoverState.OPENING @pytest.mark.parametrize( ("is_tilt_180", "expected_tilt_position", "expected_tilt_reported"), [ pytest.param(False, 0, 100, id="tilt_90"), pytest.param(True, 50, 50, id="tilt_180"), ], ) async def test_open_tilt( shutterbox, hass: HomeAssistant, is_tilt_180: bool, expected_tilt_position: int, expected_tilt_reported: int, ) -> None: """Test opening tilt for 90-degree and 180-degree tilt shutters.""" feature_mock, entity_id = shutterbox feature_mock.is_tilt_180 = is_tilt_180 feature_mock.tilt_current = 100 await async_setup_entity(hass, entity_id) def set_tilt_position(tilt_position): assert tilt_position == expected_tilt_position feature_mock.tilt_current = tilt_position feature_mock.async_set_tilt_position = AsyncMock(side_effect=set_tilt_position) await hass.services.async_call( "cover", SERVICE_OPEN_COVER_TILT, {"entity_id": entity_id}, blocking=True, ) state = hass.states.get(entity_id) assert ( state.attributes[ATTR_CURRENT_TILT_POSITION] == expected_tilt_reported ) # inverted async def test_close_tilt(shutterbox, hass: HomeAssistant) -> None: """Test closing tilt.""" feature_mock, entity_id = shutterbox feature_mock.tilt_current = 0 await async_setup_entity(hass, entity_id) def set_tilt_position(tilt_position): assert tilt_position == 100 feature_mock.tilt_current = tilt_position feature_mock.async_set_tilt_position = AsyncMock(side_effect=set_tilt_position) await hass.services.async_call( "cover", SERVICE_CLOSE_COVER_TILT, {"entity_id": entity_id}, blocking=True, ) state = hass.states.get(entity_id) assert state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 # inverted