1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 10:59:24 +00:00

Bump HueBLE to 2.1.0 (#158197)

This commit is contained in:
Harvey
2025-12-08 18:55:55 +00:00
committed by GitHub
parent 6f5507670f
commit 33e09c4967
10 changed files with 72 additions and 123 deletions

View File

@@ -2,7 +2,7 @@
import logging import logging
from HueBLE import HueBleLight from HueBLE import ConnectionError, HueBleError, HueBleLight
from homeassistant.components.bluetooth import ( from homeassistant.components.bluetooth import (
async_ble_device_from_address, async_ble_device_from_address,
@@ -38,8 +38,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bo
light = HueBleLight(ble_device) light = HueBleLight(ble_device)
if not await light.connect() or not await light.poll_state(): try:
raise ConfigEntryNotReady("Device found but unable to connect.") 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 entry.runtime_data = light

View File

@@ -6,7 +6,7 @@ from enum import Enum
import logging import logging
from typing import Any from typing import Any
from HueBLE import HueBleLight from HueBLE import ConnectionError, HueBleError, HueBleLight, PairingError
import voluptuous as vol import voluptuous as vol
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
@@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr 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 from .light import get_available_color_modes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -41,32 +41,22 @@ async def validate_input(hass: HomeAssistant, address: str) -> Error | None:
try: try:
light = HueBleLight(ble_device) light = HueBleLight(ble_device)
await light.connect() await light.connect()
get_available_color_modes(light)
await light.poll_state()
if light.authenticated is None: except ConnectionError as e:
_LOGGER.warning( _LOGGER.exception("Error connecting to light")
"Unable to determine if light authenticated, proceeding anyway" return (
) Error.INVALID_AUTH
elif not light.authenticated: if type(e.__cause__) is PairingError
return Error.INVALID_AUTH else Error.CANNOT_CONNECT
)
if not light.connected: except HueBleError:
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:
_LOGGER.exception("Unexpected error validating light connection") _LOGGER.exception("Unexpected error validating light connection")
return Error.UNKNOWN return Error.UNKNOWN
except HomeAssistantError:
return Error.NOT_SUPPORTED
else: else:
return None return None
finally: finally:
@@ -129,6 +119,7 @@ class HueBleConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_NAME: self._discovery_info.name, CONF_NAME: self._discovery_info.name,
CONF_MAC: self._discovery_info.address, CONF_MAC: self._discovery_info.address,
"url_pairing_mode": URL_PAIRING_MODE, "url_pairing_mode": URL_PAIRING_MODE,
"url_factory_reset": URL_FACTORY_RESET,
}, },
) )

View File

@@ -2,3 +2,4 @@
DOMAIN = "hue_ble" DOMAIN = "hue_ble"
URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup" 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"

View File

@@ -113,7 +113,7 @@ class HueBLELight(LightEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Fetch latest state from light and make available via properties.""" """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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Set properties then turn the light on.""" """Set properties then turn the light on."""

View File

@@ -15,5 +15,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["bleak", "HueBLE"], "loggers": ["bleak", "HueBLE"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["HueBLE==1.0.8"] "requirements": ["HueBLE==2.1.0"]
} }

View File

@@ -14,7 +14,7 @@
}, },
"step": { "step": {
"confirm": { "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})."
} }
} }
} }

2
requirements_all.txt generated
View File

@@ -22,7 +22,7 @@ HAP-python==5.0.0
HATasmota==0.10.1 HATasmota==0.10.1
# homeassistant.components.hue_ble # homeassistant.components.hue_ble
HueBLE==1.0.8 HueBLE==2.1.0
# homeassistant.components.mastodon # homeassistant.components.mastodon
Mastodon.py==2.1.2 Mastodon.py==2.1.2

View File

@@ -22,7 +22,7 @@ HAP-python==5.0.0
HATasmota==0.10.1 HATasmota==0.10.1
# homeassistant.components.hue_ble # homeassistant.components.hue_ble
HueBLE==1.0.8 HueBLE==2.1.0
# homeassistant.components.mastodon # homeassistant.components.mastodon
Mastodon.py==2.1.2 Mastodon.py==2.1.2

View File

@@ -2,11 +2,16 @@
from unittest.mock import AsyncMock, PropertyMock, patch from unittest.mock import AsyncMock, PropertyMock, patch
from HueBLE import ConnectionError, HueBleError, PairingError
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.hue_ble.config_flow import Error 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.config_entries import SOURCE_BLUETOOTH
from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.const import CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant 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.common import MockConfigEntry
from tests.components.bluetooth import BLEDevice, generate_ble_device 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( async def test_bluetooth_form(
hass: HomeAssistant, hass: HomeAssistant,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_authenticated: bool | None,
) -> None: ) -> None:
"""Test bluetooth discovery form.""" """Test bluetooth discovery form."""
@@ -48,6 +44,7 @@ async def test_bluetooth_form(
CONF_NAME: TEST_DEVICE_NAME, CONF_NAME: TEST_DEVICE_NAME,
CONF_MAC: TEST_DEVICE_MAC, CONF_MAC: TEST_DEVICE_MAC,
"url_pairing_mode": URL_PAIRING_MODE, "url_pairing_mode": URL_PAIRING_MODE,
"url_factory_reset": URL_FACTORY_RESET,
} }
with ( with (
@@ -65,17 +62,7 @@ async def test_bluetooth_form(
), ),
patch( patch(
"homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state",
side_effect=[(True, [])], 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,
), ),
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@@ -96,8 +83,6 @@ async def test_bluetooth_form(
"mock_return_device", "mock_return_device",
"mock_scanner_count", "mock_scanner_count",
"mock_connect", "mock_connect",
"mock_authenticated",
"mock_connected",
"mock_support_on_off", "mock_support_on_off",
"mock_poll_state", "mock_poll_state",
"error", "error",
@@ -106,71 +91,57 @@ async def test_bluetooth_form(
( (
None, None,
0, 0,
None,
True, True,
True, None,
True,
True,
(True, []),
Error.NO_SCANNERS, Error.NO_SCANNERS,
), ),
( (
None, None,
1, 1,
None,
True, True,
True, None,
True,
True,
(True, []),
Error.NOT_FOUND, Error.NOT_FOUND,
), ),
( (
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
1, 1,
AUTH_ERROR,
True, True,
False, None,
True,
True,
(True, []),
Error.INVALID_AUTH, Error.INVALID_AUTH,
), ),
( (
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
1, 1,
ConnectionError,
True, True,
True, None,
False,
True,
(True, []),
Error.CANNOT_CONNECT, Error.CANNOT_CONNECT,
), ),
( (
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
1, 1,
True, None,
True,
True,
False, False,
(True, []), None,
Error.NOT_SUPPORTED, Error.NOT_SUPPORTED,
), ),
( (
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
1, 1,
None,
True, True,
True, HueBleError,
True, Error.UNKNOWN,
True,
(True, ["ERROR!"]),
Error.CANNOT_CONNECT,
), ),
( (
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC), generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
1, 1,
Exception, HueBleError,
None,
None, None,
True,
True,
(True, []),
Error.UNKNOWN, Error.UNKNOWN,
), ),
], ],
@@ -189,11 +160,9 @@ async def test_bluetooth_form_exception(
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_return_device: BLEDevice | None, mock_return_device: BLEDevice | None,
mock_scanner_count: int, mock_scanner_count: int,
mock_connect: Exception | bool, mock_connect: Exception | None,
mock_authenticated: bool | None,
mock_connected: bool,
mock_support_on_off: bool, mock_support_on_off: bool,
mock_poll_state: Exception | tuple[bool, list[Exception]], mock_poll_state: Exception | None,
error: Error, error: Error,
) -> None: ) -> None:
"""Test bluetooth discovery form with errors.""" """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", "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state",
side_effect=[mock_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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
@@ -262,17 +221,7 @@ async def test_bluetooth_form_exception(
), ),
patch( patch(
"homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state", "homeassistant.components.hue_ble.config_flow.HueBleLight.poll_state",
side_effect=[(True, [])], 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,
), ),
): ):
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(

View File

@@ -3,6 +3,7 @@
from unittest.mock import patch from unittest.mock import patch
from bleak.backends.device import BLEDevice from bleak.backends.device import BLEDevice
from HueBLE import ConnectionError, HueBleError
import pytest import pytest
from homeassistant.components.hue_ble.const import DOMAIN from homeassistant.components.hue_ble.const import DOMAIN
@@ -29,29 +30,29 @@ from tests.components.bluetooth import generate_ble_device
None, None,
2, 2,
True, True,
True, None,
"The light was not found.", "The light was not found.",
), ),
( (
None, None,
0, 0,
True, True,
True, None,
"No Bluetooth scanners are available to search for the light.", "No Bluetooth scanners are available to search for the light.",
), ),
( (
generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
2, 2,
False, False,
True, ConnectionError,
"Device found but unable to connect.", "Device found but unable to connect.",
), ),
( (
generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME), generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
2, 2,
True, True,
False, HueBleError,
"Device found but unable to connect.", "Device found and connected but unable to poll values from it.",
), ),
], ],
ids=["no_device", "no_scanners", "error_connect", "error_poll"], ids=["no_device", "no_scanners", "error_connect", "error_poll"],
@@ -61,8 +62,8 @@ async def test_setup_error(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
ble_device: BLEDevice | None, ble_device: BLEDevice | None,
scanner_count: int, scanner_count: int,
connect_result: bool, connect_result: Exception | None,
poll_state_result: bool, poll_state_result: Exception | None,
message: str, message: str,
) -> None: ) -> None:
"""Test that ConfigEntryNotReady is raised if there is an error condition.""" """Test that ConfigEntryNotReady is raised if there is an error condition."""
@@ -80,11 +81,11 @@ async def test_setup_error(
), ),
patch( patch(
"homeassistant.components.hue_ble.HueBleLight.connect", "homeassistant.components.hue_ble.HueBleLight.connect",
return_value=connect_result, side_effect=[connect_result],
), ),
patch( patch(
"homeassistant.components.hue_ble.HueBleLight.poll_state", "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 assert await async_setup_component(hass, DOMAIN, {}) is True
@@ -111,11 +112,11 @@ async def test_setup(
), ),
patch( patch(
"homeassistant.components.hue_ble.HueBleLight.connect", "homeassistant.components.hue_ble.HueBleLight.connect",
return_value=True, return_value=None,
), ),
patch( patch(
"homeassistant.components.hue_ble.HueBleLight.poll_state", "homeassistant.components.hue_ble.HueBleLight.poll_state",
return_value=True, return_value=None,
), ),
): ):
assert await async_setup_component(hass, DOMAIN, {}) is True assert await async_setup_component(hass, DOMAIN, {}) is True