mirror of
https://github.com/home-assistant/core.git
synced 2025-10-04 09:19:28 +00:00
Compare commits
9 Commits
2025.10.1
...
async_curr
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8dc9fa7bba | ||
![]() |
7d2cdf8024 | ||
![]() |
d9091c3d52 | ||
![]() |
1553933c6c | ||
![]() |
d21d708efd | ||
![]() |
3827ba64a9 | ||
![]() |
1336095e95 | ||
![]() |
f75bebb532 | ||
![]() |
bd1862538b |
@@ -57,6 +57,7 @@ from .api import (
|
||||
_get_manager,
|
||||
async_address_present,
|
||||
async_ble_device_from_address,
|
||||
async_current_scanners,
|
||||
async_discovered_service_info,
|
||||
async_get_advertisement_callback,
|
||||
async_get_fallback_availability_interval,
|
||||
@@ -114,6 +115,7 @@ __all__ = [
|
||||
"HomeAssistantRemoteScanner",
|
||||
"async_address_present",
|
||||
"async_ble_device_from_address",
|
||||
"async_current_scanners",
|
||||
"async_discovered_service_info",
|
||||
"async_get_advertisement_callback",
|
||||
"async_get_fallback_availability_interval",
|
||||
|
@@ -66,6 +66,22 @@ def async_scanner_count(hass: HomeAssistant, connectable: bool = True) -> int:
|
||||
return _get_manager(hass).async_scanner_count(connectable)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_current_scanners(hass: HomeAssistant) -> list[BaseHaScanner]:
|
||||
"""Return the list of currently active scanners.
|
||||
|
||||
This method returns a list of all active Bluetooth scanners registered
|
||||
with Home Assistant, including both connectable and non-connectable scanners.
|
||||
|
||||
Args:
|
||||
hass: Home Assistant instance
|
||||
|
||||
Returns:
|
||||
List of all active scanner instances
|
||||
"""
|
||||
return _get_manager(hass).async_current_scanners()
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_discovered_service_info(
|
||||
hass: HomeAssistant, connectable: bool = True
|
||||
|
@@ -5,18 +5,21 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from habluetooth import BluetoothScanningMode
|
||||
from switchbot import (
|
||||
SwitchbotAccountConnectionError,
|
||||
SwitchBotAdvertisement,
|
||||
SwitchbotApiError,
|
||||
SwitchbotAuthenticationError,
|
||||
SwitchbotModel,
|
||||
fetch_cloud_devices,
|
||||
parse_advertisement_data,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_current_scanners,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
@@ -87,6 +90,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
self._discovered_adv: SwitchBotAdvertisement | None = None
|
||||
self._discovered_advs: dict[str, SwitchBotAdvertisement] = {}
|
||||
self._cloud_username: str | None = None
|
||||
self._cloud_password: str | None = None
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
@@ -176,9 +181,17 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the SwitchBot API auth step."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
assert self._discovered_adv is not None
|
||||
description_placeholders = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
# If we have saved credentials from cloud login, try them first
|
||||
if user_input is None and self._cloud_username and self._cloud_password:
|
||||
user_input = {
|
||||
CONF_USERNAME: self._cloud_username,
|
||||
CONF_PASSWORD: self._cloud_password,
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
cls = ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS[model]
|
||||
@@ -200,6 +213,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
# Clear saved credentials if auth failed
|
||||
self._cloud_username = None
|
||||
self._cloud_password = None
|
||||
else:
|
||||
return await self.async_step_encrypted_key(key_details)
|
||||
|
||||
@@ -239,7 +255,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the encryption key step."""
|
||||
errors = {}
|
||||
errors: dict[str, str] = {}
|
||||
assert self._discovered_adv is not None
|
||||
if user_input is not None:
|
||||
model: SwitchbotModel = self._discovered_adv.data["modelName"]
|
||||
@@ -308,7 +324,73 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
"""Handle the user step to choose cloud login or direct discovery."""
|
||||
# Check if all scanners are in active mode
|
||||
# If so, skip the menu and go directly to device selection
|
||||
scanners = async_current_scanners(self.hass)
|
||||
if scanners and all(
|
||||
scanner.current_mode == BluetoothScanningMode.ACTIVE for scanner in scanners
|
||||
):
|
||||
# All scanners are active, skip the menu
|
||||
return await self.async_step_select_device()
|
||||
|
||||
return self.async_show_menu(
|
||||
step_id="user",
|
||||
menu_options=["cloud_login", "select_device"],
|
||||
)
|
||||
|
||||
async def async_step_cloud_login(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the cloud login step."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await fetch_cloud_devices(
|
||||
async_get_clientsession(self.hass),
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except (SwitchbotApiError, SwitchbotAccountConnectionError) as ex:
|
||||
_LOGGER.debug(
|
||||
"Failed to connect to SwitchBot API: %s", ex, exc_info=True
|
||||
)
|
||||
raise AbortFlow(
|
||||
"api_error", description_placeholders={"error_detail": str(ex)}
|
||||
) from ex
|
||||
except SwitchbotAuthenticationError as ex:
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
else:
|
||||
# Save credentials temporarily for the duration of this flow
|
||||
# to avoid re-prompting if encrypted device auth is needed
|
||||
# These will be discarded when the flow completes
|
||||
self._cloud_username = user_input[CONF_USERNAME]
|
||||
self._cloud_password = user_input[CONF_PASSWORD]
|
||||
return await self.async_step_select_device()
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="cloud_login",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
|
||||
): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async def async_step_select_device(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the step to pick discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
device_adv: SwitchBotAdvertisement | None = None
|
||||
if user_input is not None:
|
||||
@@ -333,7 +415,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_confirm()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
step_id="select_device",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
|
@@ -3,6 +3,24 @@
|
||||
"flow_title": "{name} ({address})",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Would you like to sign in to your SwitchBot account to download device model information? This improves device discovery, especially when using passive Bluetooth scanning. If your Bluetooth adapter uses active scanning, you can skip this step.",
|
||||
"menu_options": {
|
||||
"cloud_login": "Sign in to SwitchBot account",
|
||||
"select_device": "Continue without signing in"
|
||||
}
|
||||
},
|
||||
"cloud_login": {
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and is only used to retrieve device model information for better discovery. Usernames and passwords are case-sensitive.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::switchbot::config::step::encrypted_auth::data_description::username%]",
|
||||
"password": "[%key:component::switchbot::config::step::encrypted_auth::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"select_device": {
|
||||
"data": {
|
||||
"address": "MAC address"
|
||||
},
|
||||
|
@@ -9,6 +9,7 @@ from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth import (
|
||||
MONOTONIC_TIME,
|
||||
BaseHaRemoteScanner,
|
||||
BluetoothScanningMode,
|
||||
HaBluetoothConnector,
|
||||
async_scanner_by_source,
|
||||
async_scanner_devices_by_address,
|
||||
@@ -16,6 +17,7 @@ from homeassistant.components.bluetooth import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import (
|
||||
FakeRemoteScanner,
|
||||
FakeScanner,
|
||||
MockBleakClient,
|
||||
_get_manager,
|
||||
@@ -161,3 +163,68 @@ async def test_async_scanner_devices_by_address_non_connectable(
|
||||
assert devices[0].ble_device.name == switchbot_device.name
|
||||
assert devices[0].advertisement.local_name == switchbot_device_adv.local_name
|
||||
cancel()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_bluetooth")
|
||||
async def test_async_current_scanners(hass: HomeAssistant) -> None:
|
||||
"""Test getting the list of current scanners."""
|
||||
# The enable_bluetooth fixture registers one scanner
|
||||
initial_scanners = bluetooth.async_current_scanners(hass)
|
||||
assert len(initial_scanners) == 1
|
||||
initial_scanner_count = len(initial_scanners)
|
||||
|
||||
# Verify current_mode is accessible on the initial scanner
|
||||
for scanner in initial_scanners:
|
||||
assert hasattr(scanner, "current_mode")
|
||||
# The mode might be None or a BluetoothScanningMode enum value
|
||||
|
||||
# Register additional connectable scanners
|
||||
hci0_scanner = FakeScanner("hci0", "hci0")
|
||||
hci1_scanner = FakeScanner("hci1", "hci1")
|
||||
cancel_hci0 = bluetooth.async_register_scanner(hass, hci0_scanner)
|
||||
cancel_hci1 = bluetooth.async_register_scanner(hass, hci1_scanner)
|
||||
|
||||
# Test that the new scanners are added
|
||||
scanners = bluetooth.async_current_scanners(hass)
|
||||
assert len(scanners) == initial_scanner_count + 2
|
||||
assert hci0_scanner in scanners
|
||||
assert hci1_scanner in scanners
|
||||
|
||||
# Verify current_mode is accessible on all scanners
|
||||
for scanner in scanners:
|
||||
assert hasattr(scanner, "current_mode")
|
||||
# Verify it's None or the correct type (BluetoothScanningMode)
|
||||
assert scanner.current_mode is None or isinstance(
|
||||
scanner.current_mode, BluetoothScanningMode
|
||||
)
|
||||
|
||||
# Register non-connectable scanner
|
||||
connector = HaBluetoothConnector(
|
||||
MockBleakClient, "mock_bleak_client", lambda: False
|
||||
)
|
||||
hci2_scanner = FakeRemoteScanner("hci2", "hci2", connector, False)
|
||||
cancel_hci2 = bluetooth.async_register_scanner(hass, hci2_scanner)
|
||||
|
||||
# Test that all scanners are returned (both connectable and non-connectable)
|
||||
all_scanners = bluetooth.async_current_scanners(hass)
|
||||
assert len(all_scanners) == initial_scanner_count + 3
|
||||
assert hci0_scanner in all_scanners
|
||||
assert hci1_scanner in all_scanners
|
||||
assert hci2_scanner in all_scanners
|
||||
|
||||
# Verify current_mode is accessible on all scanners including non-connectable
|
||||
for scanner in all_scanners:
|
||||
assert hasattr(scanner, "current_mode")
|
||||
# The mode should be None or a BluetoothScanningMode instance
|
||||
assert scanner.current_mode is None or isinstance(
|
||||
scanner.current_mode, BluetoothScanningMode
|
||||
)
|
||||
|
||||
# Clean up our scanners
|
||||
cancel_hci0()
|
||||
cancel_hci1()
|
||||
cancel_hci2()
|
||||
|
||||
# Verify we're back to the initial scanner
|
||||
final_scanners = bluetooth.async_current_scanners(hass)
|
||||
assert len(final_scanners) == initial_scanner_count
|
||||
|
@@ -251,12 +251,19 @@ async def test_async_step_bluetooth_not_connectable(hass: HomeAssistant) -> None
|
||||
async def test_user_setup_wohand(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -292,12 +299,20 @@ async def test_user_setup_wohand_already_configured(hass: HomeAssistant) -> None
|
||||
unique_id="aabbccddeeff",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
@@ -309,12 +324,20 @@ async def test_user_setup_wohand_replaces_ignored(hass: HomeAssistant) -> None:
|
||||
domain=DOMAIN, data={}, unique_id="aabbccddeeff", source=SOURCE_IGNORE
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -339,12 +362,19 @@ async def test_user_setup_wohand_replaces_ignored(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_wocurtain(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -370,6 +400,12 @@ async def test_user_setup_wocurtain(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with valid address."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -379,11 +415,12 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
WOHAND_SERVICE_INFO_NOT_CONNECTABLE,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry:
|
||||
@@ -406,6 +443,12 @@ async def test_user_setup_wocurtain_or_bot(hass: HomeAssistant) -> None:
|
||||
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."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -414,11 +457,12 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) ->
|
||||
WOHAND_SERVICE_INFO_NOT_CONNECTABLE,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@@ -450,12 +494,19 @@ async def test_user_setup_wocurtain_or_bot_with_password(hass: HomeAssistant) ->
|
||||
async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a bot with a password."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "password"
|
||||
@@ -482,12 +533,19 @@ async def test_user_setup_single_bot_with_password(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_woencrypted_key(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -548,12 +606,19 @@ async def test_user_setup_woencrypted_key(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_woencrypted_auth(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -623,12 +688,19 @@ async def test_user_setup_woencrypted_auth_switchbot_api_down(
|
||||
) -> None:
|
||||
"""Test the user initiated form for a lock when the switchbot api is down."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -661,6 +733,12 @@ async def test_user_setup_woencrypted_auth_switchbot_api_down(
|
||||
async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a lock."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[
|
||||
@@ -668,11 +746,12 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
WOHAND_SERVICE_ALT_ADDRESS_INFO,
|
||||
],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["step_id"] == "select_device"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
@@ -721,12 +800,19 @@ async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None:
|
||||
|
||||
async def test_user_setup_wosensor(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
@@ -749,14 +835,225 @@ async def test_user_setup_wosensor(hass: HomeAssistant) -> None:
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_cloud_login(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test successful cloud login
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOHAND_SERVICE_INFO],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
|
||||
# Should proceed to device selection with single device, so go to confirm
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
# Confirm device setup
|
||||
with patch_async_setup_entry():
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Bot EEFF"
|
||||
assert result["data"] == {
|
||||
CONF_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
||||
CONF_SENSOR_TYPE: "bot",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_cloud_login_auth_failed(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow with authentication failure."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test authentication failure
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
side_effect=SwitchbotAuthenticationError("Invalid credentials"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "wrongpass",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
assert result["errors"] == {"base": "auth_failed"}
|
||||
assert "Invalid credentials" in result["description_placeholders"]["error_detail"]
|
||||
|
||||
|
||||
async def test_user_cloud_login_api_error(hass: HomeAssistant) -> None:
|
||||
"""Test the cloud login flow with API error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
# Test API connection error
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
side_effect=SwitchbotAccountConnectionError("API is down"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "api_error"
|
||||
assert result["description_placeholders"] == {"error_detail": "API is down"}
|
||||
|
||||
|
||||
async def test_user_cloud_login_then_encrypted_device(hass: HomeAssistant) -> None:
|
||||
"""Test cloud login followed by encrypted device setup using saved credentials."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "cloud_login"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "cloud_login"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.fetch_cloud_devices",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WOLOCK_SERVICE_INFO],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
|
||||
# Should go to encrypted device choice menu
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
||||
# Choose encrypted auth
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"next_step_id": "encrypted_auth"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
None,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "encrypted_auth"
|
||||
|
||||
with (
|
||||
patch_async_setup_entry() as mock_setup_entry,
|
||||
patch(
|
||||
"switchbot.SwitchbotLock.async_retrieve_encryption_key",
|
||||
return_value={
|
||||
CONF_KEY_ID: "ff",
|
||||
CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff",
|
||||
},
|
||||
),
|
||||
patch("switchbot.SwitchbotLock.verify_encryption_key", return_value=True),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: "test@example.com",
|
||||
CONF_PASSWORD: "testpass",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is 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_no_devices(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form with password and valid mac."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "no_devices_found"
|
||||
@@ -774,13 +1071,20 @@ async def test_async_step_user_takes_precedence_over_discovery(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
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},
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
@@ -931,12 +1235,19 @@ async def test_options_flow_lock_pro(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_worelay_switch_1pm_key(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -979,12 +1290,19 @@ async def test_user_setup_worelay_switch_1pm_key(hass: HomeAssistant) -> None:
|
||||
async def test_user_setup_worelay_switch_1pm_auth(hass: HomeAssistant) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
@@ -1053,12 +1371,19 @@ async def test_user_setup_worelay_switch_1pm_auth_switchbot_api_down(
|
||||
) -> None:
|
||||
"""Test the user initiated form for a relay switch 1pm when the switchbot api is down."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.async_discovered_service_info",
|
||||
return_value=[WORELAY_SWITCH_1PM_SERVICE_INFO],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "select_device"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "encrypted_choose_method"
|
||||
|
Reference in New Issue
Block a user