mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 08:26:41 +01:00
329 lines
9.8 KiB
Python
329 lines
9.8 KiB
Python
"""Test the switchbot init."""
|
|
|
|
from collections.abc import Callable
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
from homeassistant.components.bluetooth import BluetoothServiceInfoBleak
|
|
from homeassistant.components.switchbot.const import (
|
|
CONF_CURTAIN_SPEED,
|
|
CONF_RETRY_COUNT,
|
|
DEFAULT_CURTAIN_SPEED,
|
|
DEFAULT_RETRY_COUNT,
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
|
|
DOMAIN,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_SENSOR_TYPE
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from . import (
|
|
AIR_PURIFIER_PM25_SERVICE_INFO,
|
|
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO,
|
|
AIR_PURIFIER_TABLE_VOC_SERVICE_INFO,
|
|
AIR_PURIFIER_VOC_SERVICE_INFO,
|
|
HUBMINI_MATTER_SERVICE_INFO,
|
|
LOCK_SERVICE_INFO,
|
|
WOCURTAIN_SERVICE_INFO,
|
|
WOMETERTHPC_SERVICE_INFO,
|
|
WOSENSORTH_SERVICE_INFO,
|
|
patch_async_ble_device_from_address,
|
|
)
|
|
|
|
from tests.common import MockConfigEntry
|
|
from tests.components.bluetooth import inject_bluetooth_service_info
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "error_message"),
|
|
[
|
|
(
|
|
ValueError("wrong model"),
|
|
"Switchbot device initialization failed because of incorrect configuration parameters: wrong model",
|
|
),
|
|
],
|
|
)
|
|
async def test_exception_handling_for_device_initialization(
|
|
hass: HomeAssistant,
|
|
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
|
|
exception: Exception,
|
|
error_message: str,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test exception handling for lock initialization."""
|
|
inject_bluetooth_service_info(hass, LOCK_SERVICE_INFO)
|
|
|
|
entry = mock_entry_encrypted_factory(sensor_type="lock")
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch(
|
|
"homeassistant.components.switchbot.lock.switchbot.SwitchbotLock.__init__",
|
|
side_effect=exception,
|
|
):
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
assert error_message in caplog.text
|
|
|
|
|
|
async def test_setup_entry_without_ble_device(
|
|
hass: HomeAssistant,
|
|
mock_entry_factory: Callable[[str], MockConfigEntry],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test setup entry without ble device."""
|
|
|
|
entry = mock_entry_factory("hygrometer_co2")
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch_async_ble_device_from_address(None):
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert (
|
|
"Could not find Switchbot hygrometer_co2 with address aa:bb:cc:dd:ee:ff"
|
|
in caplog.text
|
|
)
|
|
|
|
|
|
async def test_setup_entry_meter_pro_co2_uses_non_connectable(
|
|
hass: HomeAssistant,
|
|
mock_entry_factory: Callable[[str], MockConfigEntry],
|
|
) -> None:
|
|
"""Test that Meter Pro CO2 setup uses connectable=False for BLE lookup.
|
|
|
|
Meter Pro CO2 is in both CONNECTABLE and NON_CONNECTABLE model types,
|
|
so async_ble_device_from_address should be called with connectable=False
|
|
to support passive BT proxies.
|
|
"""
|
|
inject_bluetooth_service_info(hass, WOMETERTHPC_SERVICE_INFO)
|
|
|
|
entry = mock_entry_factory("hygrometer_co2")
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch(
|
|
"homeassistant.components.bluetooth.async_ble_device_from_address",
|
|
) as mock_ble:
|
|
mock_ble.return_value = WOMETERTHPC_SERVICE_INFO.device
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# connectable should be False since METER_PRO_C is in both lists
|
|
assert mock_ble.call_args_list[0][0][2] is False
|
|
|
|
|
|
async def test_coordinator_wait_ready_timeout(
|
|
hass: HomeAssistant,
|
|
mock_entry_factory: Callable[[str], MockConfigEntry],
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""Test the coordinator async_wait_ready timeout by calling it directly."""
|
|
|
|
inject_bluetooth_service_info(hass, HUBMINI_MATTER_SERVICE_INFO)
|
|
|
|
entry = mock_entry_factory("hubmini_matter")
|
|
entry.add_to_hass(hass)
|
|
|
|
timeout_mock = AsyncMock()
|
|
timeout_mock.__aenter__.side_effect = TimeoutError
|
|
timeout_mock.__aexit__.return_value = None
|
|
|
|
with patch(
|
|
"homeassistant.components.switchbot.coordinator.asyncio.timeout",
|
|
return_value=timeout_mock,
|
|
):
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert "aa:bb:cc:dd:ee:ff is not advertising state" in caplog.text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("sensor_type", "service_info", "expected_options"),
|
|
[
|
|
(
|
|
"curtain",
|
|
WOCURTAIN_SERVICE_INFO,
|
|
{
|
|
CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT,
|
|
CONF_CURTAIN_SPEED: DEFAULT_CURTAIN_SPEED,
|
|
},
|
|
),
|
|
(
|
|
"hygrometer",
|
|
WOSENSORTH_SERVICE_INFO,
|
|
{
|
|
CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
async def test_migrate_entry_from_v1_1_to_v1_2(
|
|
hass: HomeAssistant,
|
|
sensor_type: str,
|
|
service_info,
|
|
expected_options: dict,
|
|
) -> None:
|
|
"""Test migration from version 1.1 to 1.2 adds default options."""
|
|
inject_bluetooth_service_info(hass, service_info)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
CONF_NAME: "test-name",
|
|
CONF_SENSOR_TYPE: sensor_type,
|
|
},
|
|
unique_id="aabbccddeeff",
|
|
version=1,
|
|
minor_version=1,
|
|
options={},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.version == 1
|
|
assert entry.minor_version == 2
|
|
assert entry.options == expected_options
|
|
|
|
|
|
async def test_migrate_entry_preserves_existing_options(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test migration preserves existing options."""
|
|
inject_bluetooth_service_info(hass, WOCURTAIN_SERVICE_INFO)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
CONF_NAME: "test-name",
|
|
CONF_SENSOR_TYPE: "curtain",
|
|
},
|
|
unique_id="aabbccddeeff",
|
|
version=1,
|
|
minor_version=1,
|
|
options={CONF_RETRY_COUNT: 5},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.version == 1
|
|
assert entry.minor_version == 2
|
|
# Existing retry_count should be preserved, curtain_speed added
|
|
assert entry.options[CONF_RETRY_COUNT] == 5
|
|
assert entry.options[CONF_CURTAIN_SPEED] == DEFAULT_CURTAIN_SPEED
|
|
|
|
|
|
async def test_migrate_entry_fails_for_future_version(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test migration fails for future versions."""
|
|
inject_bluetooth_service_info(hass, WOCURTAIN_SERVICE_INFO)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
CONF_NAME: "test-name",
|
|
CONF_SENSOR_TYPE: "curtain",
|
|
},
|
|
unique_id="aabbccddeeff",
|
|
version=2,
|
|
minor_version=1,
|
|
options={},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# Entry should not be loaded due to failed migration
|
|
assert entry.version == 2
|
|
assert entry.minor_version == 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("old_sensor_type", "service_info", "expected_sensor_type"),
|
|
[
|
|
(
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
|
|
AIR_PURIFIER_VOC_SERVICE_INFO,
|
|
"air_purifier_jp",
|
|
),
|
|
(
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
|
|
AIR_PURIFIER_PM25_SERVICE_INFO,
|
|
"air_purifier_us",
|
|
),
|
|
(
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
|
|
AIR_PURIFIER_TABLE_VOC_SERVICE_INFO,
|
|
"air_purifier_table_jp",
|
|
),
|
|
(
|
|
DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE,
|
|
AIR_PURIFIER_TABLE_PM25_SERVICE_INFO,
|
|
"air_purifier_table_us",
|
|
),
|
|
],
|
|
)
|
|
async def test_migrate_deprecated_air_purifier_sensor_type(
|
|
hass: HomeAssistant,
|
|
old_sensor_type: str,
|
|
service_info: BluetoothServiceInfoBleak,
|
|
expected_sensor_type: str,
|
|
) -> None:
|
|
"""Test that deprecated air_purifier sensor types are migrated via BLE advertisement."""
|
|
inject_bluetooth_service_info(hass, service_info)
|
|
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
CONF_NAME: "test-name",
|
|
CONF_SENSOR_TYPE: old_sensor_type,
|
|
},
|
|
unique_id="aabbccddeeff",
|
|
version=1,
|
|
minor_version=2,
|
|
options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.data[CONF_SENSOR_TYPE] == expected_sensor_type
|
|
|
|
|
|
async def test_migrate_deprecated_air_purifier_sensor_type_device_not_in_range(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test deprecated air_purifier type entry is not loaded when device is out of range."""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
|
CONF_NAME: "test-name",
|
|
CONF_SENSOR_TYPE: DEPRECATED_SENSOR_TYPE_AIR_PURIFIER,
|
|
},
|
|
unique_id="aabbccddeeff",
|
|
version=1,
|
|
minor_version=2,
|
|
options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
# sensor_type unchanged and entry not loaded; will retry when device advertises
|
|
assert entry.data[CONF_SENSOR_TYPE] == DEPRECATED_SENSOR_TYPE_AIR_PURIFIER
|
|
assert entry.state is ConfigEntryState.SETUP_RETRY
|