"""Test the switchbot config flow.""" from unittest.mock import patch from switchbot import SwitchbotAccountConnectionError, SwitchbotAuthenticationError from homeassistant.components.switchbot.const import ( CONF_ENCRYPTION_KEY, CONF_KEY_ID, CONF_RETRY_COUNT, ) from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER from homeassistant.const import ( CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE, CONF_USERNAME, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( NOT_SWITCHBOT_INFO, USER_INPUT, WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO, WOHAND_SERVICE_INFO, WOHAND_SERVICE_INFO_NOT_CONNECTABLE, WOLOCK_SERVICE_INFO, WOSENSORTH_SERVICE_INFO, init_integration, patch_async_setup_entry, ) from tests.common import MockConfigEntry DOMAIN = "switchbot" async def test_bluetooth_discovery(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 async def test_bluetooth_discovery_requires_password(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device that needs a password.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOHAND_ENCRYPTED_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "password" with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: "abc123"}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Bot 923B" assert result["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", CONF_SENSOR_TYPE: "bot", CONF_PASSWORD: "abc123", } assert len(mock_setup_entry.mock_calls) == 1 async def test_bluetooth_discovery_lock_key(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a lock.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOLOCK_SERVICE_INFO, ) assert result["type"] == FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=False, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "", CONF_ENCRYPTION_KEY: "", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", CONF_SENSOR_TYPE: "lock", } assert len(mock_setup_entry.mock_calls) == 1 async def test_bluetooth_discovery_already_setup(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", }, unique_id="aabbccddeeff", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_async_step_bluetooth_not_switchbot(hass: HomeAssistant) -> None: """Test discovery via bluetooth not switchbot.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=NOT_SWITCHBOT_INFO, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported" async def test_async_step_bluetooth_not_connectable(hass: HomeAssistant) -> None: """Test discovery via bluetooth and its not connectable switchbot.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOHAND_SERVICE_INFO_NOT_CONNECTABLE, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported" async def test_user_setup_wohand(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOHAND_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "AA:BB:CC:DD:EE:FF", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", }, unique_id="aabbccddeeff", ) entry.add_to_hass(hass) with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOHAND_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" async def test_user_setup_wocurtain(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOCURTAIN_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_SENSOR_TYPE: "curtain", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None: """Test the user initiated form with valid address.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[ NOT_SWITCHBOT_INFO, WOCURTAIN_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO, WOHAND_SERVICE_INFO_NOT_CONNECTABLE, ], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_SENSOR_TYPE: "curtain", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) -> None: """Test the user initiated form and valid address and a bot with a password.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[ WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO, WOHAND_SERVICE_INFO_NOT_CONNECTABLE, ], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"}, ) assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "password" assert result2["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {CONF_PASSWORD: "abc123"}, ) await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Bot 923B" assert result3["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", CONF_PASSWORD: "abc123", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None: """Test the user initiated form for a bot with a password.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOHAND_ENCRYPTED_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "password" assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: "abc123"}, ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Bot 923B" assert result2["data"] == { CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", CONF_PASSWORD: "abc123", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wolock_key(hass: HomeAssistant) -> None: """Test the user initiated form for a lock.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOLOCK_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=False, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "", CONF_ENCRYPTION_KEY: "", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {"base": "encryption_key_invalid"} with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", CONF_SENSOR_TYPE: "lock", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: """Test the user initiated form for a lock.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOLOCK_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "", CONF_PASSWORD: "", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {"base": "auth_failed"} assert "error from api" in result["description_placeholders"]["error_detail"] with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=True, ), patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", return_value={ CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", }, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "username", CONF_PASSWORD: "password", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", CONF_SENSOR_TYPE: "lock", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> None: """Test the user initiated form for a lock when the switchbot api is down.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOLOCK_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_auth"} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_auth" assert result["errors"] == {} with patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", side_effect=SwitchbotAccountConnectionError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "", CONF_PASSWORD: "", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: """Test the user initiated form for a lock.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[ WOLOCK_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO, ], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.MENU assert result["step_id"] == "lock_choose_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"next_step_id": "lock_key"} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "lock_key" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry, patch( "homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key", return_value=True, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Lock EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", CONF_SENSOR_TYPE: "lock", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_setup_wosensor(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOSENSORTH_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Meter EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_SENSOR_TYPE: "hygrometer", } assert len(mock_setup_entry.mock_calls) == 1 async def test_user_no_devices(hass: HomeAssistant) -> None: """Test the user initiated form with password and valid mac.""" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" async def test_async_step_user_takes_precedence_over_discovery( hass: HomeAssistant, ) -> None: """Test manual setup takes precedence over discovery.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=WOCURTAIN_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", return_value=[WOCURTAIN_SERVICE_INFO], ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, ) assert result["type"] == FlowResultType.FORM with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Curtain EEFF" assert result2["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_SENSOR_TYPE: "curtain", } assert len(mock_setup_entry.mock_calls) == 1 # Verify the original one was aborted assert not hass.config_entries.flow.async_progress(DOMAIN) async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", }, options={ CONF_RETRY_COUNT: 10, }, unique_id="aabbccddeeff", ) entry.add_to_hass(hass) with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ CONF_RETRY_COUNT: 3, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 assert len(mock_setup_entry.mock_calls) == 2 # Test changing of entry options. with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ CONF_RETRY_COUNT: 6, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 assert len(mock_setup_entry.mock_calls) == 1 assert entry.options[CONF_RETRY_COUNT] == 6