diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 3223f24fa027..519825582b33 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -197,6 +197,11 @@ PLATFORMS_BY_TYPE = { Platform.LOCK, Platform.SENSOR, ], + SupportedModels.LOCK_ULTRA_MAX.value: [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + ], SupportedModels.WEATHER_STATION.value: [Platform.SENSOR], SupportedModels.CANDLE_WARMER_LAMP.value: [Platform.LIGHT, Platform.SENSOR], SupportedModels.RGBIC_NEON_ROPE_LIGHT.value: [Platform.LIGHT, Platform.SENSOR], @@ -257,6 +262,7 @@ CLASS_BY_DEVICE = { SupportedModels.LOCK_VISION_PRO.value: switchbot.SwitchbotLock, SupportedModels.LOCK_VISION.value: switchbot.SwitchbotLock, SupportedModels.LOCK_PRO_WIFI.value: switchbot.SwitchbotLock, + SupportedModels.LOCK_ULTRA_MAX.value: switchbot.SwitchbotLock, SupportedModels.CANDLE_WARMER_LAMP.value: switchbot.SwitchbotCandleWarmerLamp, SupportedModels.RGBIC_NEON_ROPE_LIGHT.value: switchbot.SwitchbotRgbicNeonLight, SupportedModels.RGBIC_NEON_WIRE_ROPE_LIGHT.value: switchbot.SwitchbotRgbicNeonLight, diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 765b2385e11b..ff637aa9bb30 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -482,6 +482,7 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): SupportedModels.LOCK, SupportedModels.LOCK_PRO, SupportedModels.LOCK_ULTRA, + SupportedModels.LOCK_ULTRA_MAX, SupportedModels.LOCK_PRO_WIFI, SupportedModels.LOCK_VISION, SupportedModels.LOCK_VISION_PRO, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 465974cd12ff..cf0d8e2364b3 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -47,6 +47,7 @@ class SupportedModels(StrEnum): HUB3 = "hub3" LOCK_LITE = "lock_lite" LOCK_ULTRA = "lock_ultra" + LOCK_ULTRA_MAX = "lock_ultra_max" AIR_PURIFIER_JP = "air_purifier_jp" AIR_PURIFIER_US = "air_purifier_us" AIR_PURIFIER_TABLE_JP = "air_purifier_table_jp" @@ -101,6 +102,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = { SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM, SwitchbotModel.LOCK_LITE: SupportedModels.LOCK_LITE, SwitchbotModel.LOCK_ULTRA: SupportedModels.LOCK_ULTRA, + SwitchbotModel.LOCK_ULTRA_MAX: SupportedModels.LOCK_ULTRA_MAX, SwitchbotModel.AIR_PURIFIER_JP: SupportedModels.AIR_PURIFIER_JP, SwitchbotModel.AIR_PURIFIER_US: SupportedModels.AIR_PURIFIER_US, SwitchbotModel.AIR_PURIFIER_TABLE_JP: SupportedModels.AIR_PURIFIER_TABLE_JP, @@ -159,6 +161,7 @@ ENCRYPTED_MODELS = { SwitchbotModel.LOCK_PRO, SwitchbotModel.LOCK_LITE, SwitchbotModel.LOCK_ULTRA, + SwitchbotModel.LOCK_ULTRA_MAX, SwitchbotModel.AIR_PURIFIER_JP, SwitchbotModel.AIR_PURIFIER_US, SwitchbotModel.AIR_PURIFIER_TABLE_JP, @@ -193,6 +196,7 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[ SwitchbotModel.RELAY_SWITCH_1: switchbot.SwitchbotRelaySwitch, SwitchbotModel.LOCK_LITE: switchbot.SwitchbotLock, SwitchbotModel.LOCK_ULTRA: switchbot.SwitchbotLock, + SwitchbotModel.LOCK_ULTRA_MAX: switchbot.SwitchbotLock, SwitchbotModel.AIR_PURIFIER_JP: switchbot.SwitchbotAirPurifier, SwitchbotModel.AIR_PURIFIER_US: switchbot.SwitchbotAirPurifier, SwitchbotModel.AIR_PURIFIER_TABLE_JP: switchbot.SwitchbotAirPurifier, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 97e039a3ed68..d6518d5b82e0 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -860,6 +860,31 @@ LOCK_PRO_WIFI_SERVICE_INFO = BluetoothServiceInfoBleak( ) +LOCK_ULTRA_MAX_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Lock Ultra Max", + manufacturer_data={2409: b"\xb0\xe9\xfe\x11\x22\x33\x2a\x88\x18\x64\x00\x91"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x80\x64\x00\x11\x9f\xb8" + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + source="local", + advertisement=generate_advertisement_data( + local_name="Lock Ultra Max", + manufacturer_data={2409: b"\xb0\xe9\xfe\x11\x22\x33\x2a\x88\x18\x64\x00\x91"}, + service_data={ + "0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x80\x64\x00\x11\x9f\xb8" + }, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Lock Ultra Max"), + time=0, + connectable=True, + tx_power=-127, +) + + AIR_PURIFIER_TABLE_US_SERVICE_INFO = BluetoothServiceInfoBleak( name="Air Purifier Table US", manufacturer_data={ diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index a111fbd8fd10..286c223e4a6e 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -26,6 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( + LOCK_ULTRA_MAX_SERVICE_INFO, NOT_SWITCHBOT_INFO, USER_INPUT, WOCURTAIN_SERVICE_INFO, @@ -176,6 +177,40 @@ async def test_bluetooth_discovery_encrypted_key(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_lock_ultra_max(hass: HomeAssistant) -> None: + """Test discovery of an encrypted Lock Ultra Max.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=LOCK_ULTRA_MAX_SERVICE_INFO, + ) + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "encrypted_choose_method" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"next_step_id": "encrypted_key"} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "encrypted_key" + + with ( + patch_async_setup_entry() as mock_setup_entry, + patch("switchbot.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"] is FlowResultType.CREATE_ENTRY + assert result["data"][CONF_SENSOR_TYPE] == "lock_ultra_max" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_key(hass: HomeAssistant) -> None: """Test discovery via bluetooth with a encrypted device.""" result = await hass.config_entries.flow.async_init( @@ -1883,15 +1918,18 @@ async def test_user_show_menu_when_no_scanners(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_options_flow_lock_pro_wifi(hass: HomeAssistant) -> None: - """Test updating options for lock_pro_wifi.""" +@pytest.mark.parametrize("sensor_type", ["lock_pro_wifi", "lock_ultra_max"]) +async def test_options_flow_lock_with_night_latch( + hass: HomeAssistant, sensor_type: str +) -> None: + """Test updating options for locks with night latch support.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_SENSOR_TYPE: "lock_pro_wifi", + CONF_SENSOR_TYPE: sensor_type, }, options={CONF_RETRY_COUNT: 10}, unique_id="aabbccddeeff", diff --git a/tests/components/switchbot/test_lock.py b/tests/components/switchbot/test_lock.py index 46f58fa210a3..c61ad5f29109 100644 --- a/tests/components/switchbot/test_lock.py +++ b/tests/components/switchbot/test_lock.py @@ -24,6 +24,7 @@ from . import ( LOCK_LITE_SERVICE_INFO, LOCK_PRO_WIFI_SERVICE_INFO, LOCK_SERVICE_INFO, + LOCK_ULTRA_MAX_SERVICE_INFO, LOCK_ULTRA_SERVICE_INFO, LOCK_VISION_PRO_SERVICE_INFO, LOCK_VISION_SERVICE_INFO, @@ -41,6 +42,7 @@ from tests.components.bluetooth import inject_bluetooth_service_info ("lock", LOCK_SERVICE_INFO), ("lock_lite", LOCK_LITE_SERVICE_INFO), ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), + ("lock_ultra_max", LOCK_ULTRA_MAX_SERVICE_INFO), ("lock_vision", LOCK_VISION_SERVICE_INFO), ("lock_vision_pro", LOCK_VISION_PRO_SERVICE_INFO), ("lock_pro_wifi", LOCK_PRO_WIFI_SERVICE_INFO), @@ -92,6 +94,7 @@ async def test_lock_services( ("lock", LOCK_SERVICE_INFO), ("lock_lite", LOCK_LITE_SERVICE_INFO), ("lock_ultra", LOCK_ULTRA_SERVICE_INFO), + ("lock_ultra_max", LOCK_ULTRA_MAX_SERVICE_INFO), ("lock_vision", LOCK_VISION_SERVICE_INFO), ("lock_vision_pro", LOCK_VISION_PRO_SERVICE_INFO), ("lock_pro_wifi", LOCK_PRO_WIFI_SERVICE_INFO), @@ -137,6 +140,28 @@ async def test_lock_services_with_night_latch_enabled( mocked_instance.assert_awaited_once() +async def test_lock_ultra_max_entities( + hass: HomeAssistant, + mock_entry_encrypted_factory: Callable[[str], MockConfigEntry], +) -> None: + """Test Lock Ultra Max creates lock, door, and battery entities.""" + inject_bluetooth_service_info(hass, LOCK_ULTRA_MAX_SERVICE_INFO) + + entry = mock_entry_encrypted_factory(sensor_type="lock_ultra_max") + entry.add_to_hass(hass) + + with patch.multiple( + "homeassistant.components.switchbot.lock.switchbot.SwitchbotLock", + update=AsyncMock(return_value=None), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("lock.test_name") is not None + assert hass.states.get("binary_sensor.test_name") is not None + assert hass.states.get("sensor.test_name_battery") is not None + + @pytest.mark.parametrize( ("exception", "error_message"), [