diff --git a/homeassistant/components/hue_ble/__init__.py b/homeassistant/components/hue_ble/__init__.py index 06a4a738b46..25d20136e8f 100644 --- a/homeassistant/components/hue_ble/__init__.py +++ b/homeassistant/components/hue_ble/__init__.py @@ -2,7 +2,7 @@ import logging -from HueBLE import HueBleLight +from HueBLE import ConnectionError, HueBleError, HueBleLight from homeassistant.components.bluetooth import ( async_ble_device_from_address, @@ -38,8 +38,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bo light = HueBleLight(ble_device) - if not await light.connect() or not await light.poll_state(): - raise ConfigEntryNotReady("Device found but unable to connect.") + try: + await light.connect() + await light.poll_state() + except ConnectionError as e: + raise ConfigEntryNotReady("Device found but unable to connect.") from e + except HueBleError as e: + raise ConfigEntryNotReady( + "Device found and connected but unable to poll values from it." + ) from e entry.runtime_data = light diff --git a/homeassistant/components/hue_ble/config_flow.py b/homeassistant/components/hue_ble/config_flow.py index e7b4409c789..6d3df824b17 100644 --- a/homeassistant/components/hue_ble/config_flow.py +++ b/homeassistant/components/hue_ble/config_flow.py @@ -6,7 +6,7 @@ from enum import Enum import logging from typing import Any -from HueBLE import HueBleLight +from HueBLE import ConnectionError, HueBleError, HueBleLight, PairingError import voluptuous as vol from homeassistant.components import bluetooth @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr -from .const import DOMAIN, URL_PAIRING_MODE +from .const import DOMAIN, URL_FACTORY_RESET, URL_PAIRING_MODE from .light import get_available_color_modes _LOGGER = logging.getLogger(__name__) @@ -41,32 +41,22 @@ async def validate_input(hass: HomeAssistant, address: str) -> Error | None: try: light = HueBleLight(ble_device) - await light.connect() + get_available_color_modes(light) + await light.poll_state() - if light.authenticated is None: - _LOGGER.warning( - "Unable to determine if light authenticated, proceeding anyway" - ) - elif not light.authenticated: - return Error.INVALID_AUTH - - if not light.connected: - return Error.CANNOT_CONNECT - - try: - get_available_color_modes(light) - except HomeAssistantError: - return Error.NOT_SUPPORTED - - _, errors = await light.poll_state() - if len(errors) != 0: - _LOGGER.warning("Errors raised when connecting to light: %s", errors) - return Error.CANNOT_CONNECT - - except Exception: + except ConnectionError as e: + _LOGGER.exception("Error connecting to light") + return ( + Error.INVALID_AUTH + if type(e.__cause__) is PairingError + else Error.CANNOT_CONNECT + ) + except HueBleError: _LOGGER.exception("Unexpected error validating light connection") return Error.UNKNOWN + except HomeAssistantError: + return Error.NOT_SUPPORTED else: return None finally: @@ -129,6 +119,7 @@ class HueBleConfigFlow(ConfigFlow, domain=DOMAIN): CONF_NAME: self._discovery_info.name, CONF_MAC: self._discovery_info.address, "url_pairing_mode": URL_PAIRING_MODE, + "url_factory_reset": URL_FACTORY_RESET, }, ) diff --git a/homeassistant/components/hue_ble/const.py b/homeassistant/components/hue_ble/const.py index 741c8e31070..25edceb0683 100644 --- a/homeassistant/components/hue_ble/const.py +++ b/homeassistant/components/hue_ble/const.py @@ -2,3 +2,4 @@ DOMAIN = "hue_ble" URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup" +URL_FACTORY_RESET = "https://www.philips-hue.com/en-gb/support/article/how-to-factory-reset-philips-hue-lights/000004" diff --git a/homeassistant/components/hue_ble/light.py b/homeassistant/components/hue_ble/light.py index 434c5cb9092..18ab878acdf 100644 --- a/homeassistant/components/hue_ble/light.py +++ b/homeassistant/components/hue_ble/light.py @@ -113,7 +113,7 @@ class HueBLELight(LightEntity): async def async_update(self) -> None: """Fetch latest state from light and make available via properties.""" - await self._api.poll_state(run_callbacks=True) + await self._api.poll_state() async def async_turn_on(self, **kwargs: Any) -> None: """Set properties then turn the light on.""" diff --git a/homeassistant/components/hue_ble/manifest.json b/homeassistant/components/hue_ble/manifest.json index feb0c45cbbc..ce3b4b4e585 100644 --- a/homeassistant/components/hue_ble/manifest.json +++ b/homeassistant/components/hue_ble/manifest.json @@ -15,5 +15,5 @@ "iot_class": "local_push", "loggers": ["bleak", "HueBLE"], "quality_scale": "bronze", - "requirements": ["HueBLE==1.0.8"] + "requirements": ["HueBLE==2.1.0"] } diff --git a/homeassistant/components/hue_ble/strings.json b/homeassistant/components/hue_ble/strings.json index 72e6b2dab84..bbae80573f3 100644 --- a/homeassistant/components/hue_ble/strings.json +++ b/homeassistant/components/hue_ble/strings.json @@ -14,7 +14,7 @@ }, "step": { "confirm": { - "description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode})." + "description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode}) or has been [factory reset]({url_factory_reset})." } } } diff --git a/requirements_all.txt b/requirements_all.txt index f6cfd0160fd..305254022a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ HAP-python==5.0.0 HATasmota==0.10.1 # homeassistant.components.hue_ble -HueBLE==1.0.8 +HueBLE==2.1.0 # homeassistant.components.mastodon Mastodon.py==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebb7bb7ed41..c8b819688dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -22,7 +22,7 @@ HAP-python==5.0.0 HATasmota==0.10.1 # homeassistant.components.hue_ble -HueBLE==1.0.8 +HueBLE==2.1.0 # homeassistant.components.mastodon Mastodon.py==2.1.2 diff --git a/tests/components/hue_ble/test_config_flow.py b/tests/components/hue_ble/test_config_flow.py index ea08a3fa656..62a88b3fbdc 100644 --- a/tests/components/hue_ble/test_config_flow.py +++ b/tests/components/hue_ble/test_config_flow.py @@ -2,11 +2,16 @@ from unittest.mock import AsyncMock, PropertyMock, patch +from HueBLE import ConnectionError, HueBleError, PairingError import pytest from homeassistant import config_entries from homeassistant.components.hue_ble.config_flow import Error -from homeassistant.components.hue_ble.const import DOMAIN, URL_PAIRING_MODE +from homeassistant.components.hue_ble.const import ( + DOMAIN, + URL_FACTORY_RESET, + URL_PAIRING_MODE, +) from homeassistant.config_entries import SOURCE_BLUETOOTH from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant @@ -18,22 +23,13 @@ from . import HUE_BLE_SERVICE_INFO, TEST_DEVICE_MAC, TEST_DEVICE_NAME from tests.common import MockConfigEntry from tests.components.bluetooth import BLEDevice, generate_ble_device +AUTH_ERROR = ConnectionError() +AUTH_ERROR.__cause__ = PairingError() + -@pytest.mark.parametrize( - ("mock_authenticated"), - [ - (True,), - (None), - ], - ids=[ - "normal", - "unknown_auth", - ], -) async def test_bluetooth_form( hass: HomeAssistant, mock_setup_entry: AsyncMock, - mock_authenticated: bool | None, ) -> None: """Test bluetooth discovery form.""" @@ -48,6 +44,7 @@ async def test_bluetooth_form( CONF_NAME: TEST_DEVICE_NAME, CONF_MAC: TEST_DEVICE_MAC, "url_pairing_mode": URL_PAIRING_MODE, + "url_factory_reset": URL_FACTORY_RESET, } with ( @@ -65,17 +62,7 @@ async def test_bluetooth_form( ), patch( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", - side_effect=[(True, [])], - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=True, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=mock_authenticated, + side_effect=[True], ), ): result = await hass.config_entries.flow.async_configure( @@ -96,8 +83,6 @@ async def test_bluetooth_form( "mock_return_device", "mock_scanner_count", "mock_connect", - "mock_authenticated", - "mock_connected", "mock_support_on_off", "mock_poll_state", "error", @@ -106,71 +91,57 @@ async def test_bluetooth_form( ( None, 0, + None, True, - True, - True, - True, - (True, []), + None, Error.NO_SCANNERS, ), ( None, 1, + None, True, - True, - True, - True, - (True, []), + None, Error.NOT_FOUND, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + AUTH_ERROR, True, - False, - True, - True, - (True, []), + None, Error.INVALID_AUTH, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + ConnectionError, True, - True, - False, - True, - (True, []), + None, Error.CANNOT_CONNECT, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, - True, - True, - True, + None, False, - (True, []), + None, Error.NOT_SUPPORTED, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, + None, True, - True, - True, - True, - (True, ["ERROR!"]), - Error.CANNOT_CONNECT, + HueBleError, + Error.UNKNOWN, ), ( generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), 1, - Exception, + HueBleError, + None, None, - True, - True, - (True, []), Error.UNKNOWN, ), ], @@ -189,11 +160,9 @@ async def test_bluetooth_form_exception( mock_setup_entry: AsyncMock, mock_return_device: BLEDevice | None, mock_scanner_count: int, - mock_connect: Exception | bool, - mock_authenticated: bool | None, - mock_connected: bool, + mock_connect: Exception | None, mock_support_on_off: bool, - mock_poll_state: Exception | tuple[bool, list[Exception]], + mock_poll_state: Exception | None, error: Error, ) -> None: """Test bluetooth discovery form with errors.""" @@ -228,16 +197,6 @@ async def test_bluetooth_form_exception( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", side_effect=[mock_poll_state], ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=mock_connected, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=mock_authenticated, - ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -262,17 +221,7 @@ async def test_bluetooth_form_exception( ), patch( "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", - side_effect=[(True, [])], - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.connected", - new_callable=PropertyMock, - return_value=True, - ), - patch( - "homeassistant.components.hue_ble.config_flow.HueBleLight.authenticated", - new_callable=PropertyMock, - return_value=True, + side_effect=[True], ), ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/hue_ble/test_init.py b/tests/components/hue_ble/test_init.py index aa70ab68652..6265e9c7c61 100644 --- a/tests/components/hue_ble/test_init.py +++ b/tests/components/hue_ble/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch from bleak.backends.device import BLEDevice +from HueBLE import ConnectionError, HueBleError import pytest from homeassistant.components.hue_ble.const import DOMAIN @@ -29,29 +30,29 @@ from tests.components.bluetooth import generate_ble_device None, 2, True, - True, + None, "The light was not found.", ), ( None, 0, True, - True, + None, "No Bluetooth scanners are available to search for the light.", ), ( generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), 2, False, - True, + ConnectionError, "Device found but unable to connect.", ), ( generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), 2, True, - False, - "Device found but unable to connect.", + HueBleError, + "Device found and connected but unable to poll values from it.", ), ], ids=["no_device", "no_scanners", "error_connect", "error_poll"], @@ -61,8 +62,8 @@ async def test_setup_error( caplog: pytest.LogCaptureFixture, ble_device: BLEDevice | None, scanner_count: int, - connect_result: bool, - poll_state_result: bool, + connect_result: Exception | None, + poll_state_result: Exception | None, message: str, ) -> None: """Test that ConfigEntryNotReady is raised if there is an error condition.""" @@ -80,11 +81,11 @@ async def test_setup_error( ), patch( "homeassistant.components.hue_ble.HueBleLight.connect", - return_value=connect_result, + side_effect=[connect_result], ), patch( "homeassistant.components.hue_ble.HueBleLight.poll_state", - return_value=poll_state_result, + side_effect=[poll_state_result], ), ): assert await async_setup_component(hass, DOMAIN, {}) is True @@ -111,11 +112,11 @@ async def test_setup( ), patch( "homeassistant.components.hue_ble.HueBleLight.connect", - return_value=True, + return_value=None, ), patch( "homeassistant.components.hue_ble.HueBleLight.poll_state", - return_value=True, + return_value=None, ), ): assert await async_setup_component(hass, DOMAIN, {}) is True