mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
New integration: Hue BLE (#118635)
Co-authored-by: Mr. Bubbles <manni@zapto.de> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -704,6 +704,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/huawei_lte/ @scop @fphammerle
|
||||
/homeassistant/components/hue/ @marcelveldt
|
||||
/tests/components/hue/ @marcelveldt
|
||||
/homeassistant/components/hue_ble/ @flip-dots
|
||||
/tests/components/hue_ble/ @flip-dots
|
||||
/homeassistant/components/huisbaasje/ @dennisschroer
|
||||
/tests/components/huisbaasje/ @dennisschroer
|
||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "philips",
|
||||
"name": "Philips",
|
||||
"integrations": ["dynalite", "hue", "philips_js"]
|
||||
"integrations": ["dynalite", "hue", "hue_ble", "philips_js"]
|
||||
}
|
||||
|
||||
54
homeassistant/components/hue_ble/__init__.py
Normal file
54
homeassistant/components/hue_ble/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Hue BLE integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from HueBLE import HueBleLight
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
async_ble_device_from_address,
|
||||
async_scanner_count,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HueBLEConfigEntry = ConfigEntry[HueBleLight]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bool:
|
||||
"""Set up the integration from a config entry."""
|
||||
|
||||
assert entry.unique_id is not None
|
||||
address = entry.unique_id.upper()
|
||||
|
||||
ble_device = async_ble_device_from_address(hass, address, connectable=True)
|
||||
|
||||
if ble_device is None:
|
||||
count_scanners = async_scanner_count(hass, connectable=True)
|
||||
_LOGGER.debug("Count of BLE scanners: %i", count_scanners)
|
||||
|
||||
if count_scanners < 1:
|
||||
raise ConfigEntryNotReady(
|
||||
"No Bluetooth scanners are available to search for the light."
|
||||
)
|
||||
raise ConfigEntryNotReady("The light was not found.")
|
||||
|
||||
light = HueBleLight(ble_device)
|
||||
|
||||
if not await light.connect() or not await light.poll_state():
|
||||
raise ConfigEntryNotReady("Device found but unable to connect.")
|
||||
|
||||
entry.runtime_data = light
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, [Platform.LIGHT])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HueBLEConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, [Platform.LIGHT])
|
||||
155
homeassistant/components/hue_ble/config_flow.py
Normal file
155
homeassistant/components/hue_ble/config_flow.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Config flow for Hue BLE integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from HueBLE import HueBleLight
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.components.bluetooth.api import (
|
||||
async_ble_device_from_address,
|
||||
async_scanner_count,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME
|
||||
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 .light import get_available_color_modes
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, address: str) -> Error | None:
|
||||
"""Return error if cannot connect and validate."""
|
||||
|
||||
ble_device = async_ble_device_from_address(hass, address.upper(), connectable=True)
|
||||
|
||||
if ble_device is None:
|
||||
count_scanners = async_scanner_count(hass, connectable=True)
|
||||
_LOGGER.debug("Count of BLE scanners in HA bt: %i", count_scanners)
|
||||
|
||||
if count_scanners < 1:
|
||||
return Error.NO_SCANNERS
|
||||
return Error.NOT_FOUND
|
||||
|
||||
try:
|
||||
light = HueBleLight(ble_device)
|
||||
|
||||
await light.connect()
|
||||
|
||||
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:
|
||||
_LOGGER.exception("Unexpected error validating light connection")
|
||||
return Error.UNKNOWN
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
await light.disconnect()
|
||||
|
||||
|
||||
class HueBleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Hue BLE."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: bluetooth.BluetoothServiceInfoBleak | None = None
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the home assistant scanner."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"HA found light %s. Will show in UI but not auto connect",
|
||||
discovery_info.name,
|
||||
)
|
||||
|
||||
unique_id = dr.format_mac(discovery_info.address)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
name = f"{discovery_info.name} ({discovery_info.address})"
|
||||
self.context.update({"title_placeholders": {CONF_NAME: name}})
|
||||
|
||||
self._discovery_info = discovery_info
|
||||
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a single device."""
|
||||
|
||||
assert self._discovery_info is not None
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
unique_id = dr.format_mac(self._discovery_info.address)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
error = await validate_input(self.hass, unique_id)
|
||||
if error:
|
||||
errors["base"] = error.value
|
||||
else:
|
||||
return self.async_create_entry(title=self._discovery_info.name, data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
CONF_NAME: self._discovery_info.name,
|
||||
CONF_MAC: self._discovery_info.address,
|
||||
"url_pairing_mode": URL_PAIRING_MODE,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class Error(Enum):
|
||||
"""Potential validation errors when attempting to connect."""
|
||||
|
||||
CANNOT_CONNECT = "cannot_connect"
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
INVALID_AUTH = "invalid_auth"
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
NO_SCANNERS = "no_scanners"
|
||||
"""Error to indicate no bluetooth scanners are available."""
|
||||
|
||||
NOT_FOUND = "not_found"
|
||||
"""Error to indicate the light could not be found."""
|
||||
|
||||
NOT_SUPPORTED = "not_supported"
|
||||
"""Error to indicate that the light is not a supported model."""
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
"""Error to indicate that the issue is unknown."""
|
||||
4
homeassistant/components/hue_ble/const.py
Normal file
4
homeassistant/components/hue_ble/const.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Constants for the Hue BLE integration."""
|
||||
|
||||
DOMAIN = "hue_ble"
|
||||
URL_PAIRING_MODE = "https://www.home-assistant.io/integrations/hue_ble#initial-setup"
|
||||
160
homeassistant/components/hue_ble/light.py
Normal file
160
homeassistant/components/hue_ble/light.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Hue BLE light platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from HueBLE import HueBleLight
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_XY_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
filter_supported_color_modes,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HueBLEConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HueBLEConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add light for passed config_entry in HA."""
|
||||
|
||||
light = config_entry.runtime_data
|
||||
async_add_entities([HueBLELight(light)])
|
||||
|
||||
|
||||
def get_available_color_modes(api: HueBleLight) -> set[ColorMode]:
|
||||
"""Return a set of available color modes."""
|
||||
color_modes = set()
|
||||
if api.supports_colour_xy:
|
||||
color_modes.add(ColorMode.XY)
|
||||
if api.supports_colour_temp:
|
||||
color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if api.supports_brightness:
|
||||
color_modes.add(ColorMode.BRIGHTNESS)
|
||||
if api.supports_on_off:
|
||||
color_modes.add(ColorMode.ONOFF)
|
||||
return filter_supported_color_modes(color_modes)
|
||||
|
||||
|
||||
class HueBLELight(LightEntity):
|
||||
"""Representation of a light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, light: HueBleLight) -> None:
|
||||
"""Initialize the light object. Does not connect."""
|
||||
|
||||
self._api = light
|
||||
self._attr_unique_id = light.address
|
||||
self._attr_min_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(light.maximum_mireds)
|
||||
if light.maximum_mireds
|
||||
else None
|
||||
)
|
||||
self._attr_max_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(light.minimum_mireds)
|
||||
if light.minimum_mireds
|
||||
else None
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=light.name,
|
||||
connections={(CONNECTION_BLUETOOTH, light.address)},
|
||||
manufacturer=light.manufacturer,
|
||||
model_id=light.model,
|
||||
sw_version=light.firmware,
|
||||
)
|
||||
self._attr_supported_color_modes = get_available_color_modes(self._api)
|
||||
self._update_updatable_attributes()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
|
||||
self._api.add_callback_on_state_changed(self._state_change_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from HA."""
|
||||
|
||||
self._api.remove_callback(self._state_change_callback)
|
||||
|
||||
def _update_updatable_attributes(self) -> None:
|
||||
"""Update this entities updatable attrs from the lights state."""
|
||||
self._attr_available = self._api.available
|
||||
self._attr_is_on = self._api.power_state
|
||||
self._attr_brightness = self._api.brightness
|
||||
self._attr_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(self._api.colour_temp)
|
||||
if self._api.colour_temp is not None and self._api.colour_temp != 0
|
||||
else None
|
||||
)
|
||||
self._attr_xy_color = self._api.colour_xy
|
||||
|
||||
def _state_change_callback(self) -> None:
|
||||
"""Run when light informs of state update. Updates local properties."""
|
||||
_LOGGER.debug("Received state notification from light %s", self.name)
|
||||
self._update_updatable_attributes()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Fetch latest state from light and make available via properties."""
|
||||
await self._api.poll_state(run_callbacks=True)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set properties then turn the light on."""
|
||||
|
||||
_LOGGER.debug("Turning light %s on with args %s", self.name, kwargs)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
_LOGGER.debug("Setting brightness of %s to %s", self.name, brightness)
|
||||
await self._api.set_brightness(brightness)
|
||||
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
color_temp_kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
mireds = color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)
|
||||
_LOGGER.debug("Setting color temp of %s to %s", self.name, mireds)
|
||||
await self._api.set_colour_temp(mireds)
|
||||
|
||||
if ATTR_XY_COLOR in kwargs:
|
||||
xy_color = kwargs[ATTR_XY_COLOR]
|
||||
_LOGGER.debug("Setting XY color of %s to %s", self.name, xy_color)
|
||||
await self._api.set_colour_xy(xy_color[0], xy_color[1])
|
||||
|
||||
await self._api.set_power(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn light off then set properties."""
|
||||
|
||||
_LOGGER.debug("Turning light %s off with args %s", self.name, kwargs)
|
||||
await self._api.set_power(False)
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode:
|
||||
"""Color mode of the light."""
|
||||
|
||||
if self._api.supports_colour_xy and not self._api.colour_temp_mode:
|
||||
return ColorMode.XY
|
||||
|
||||
if self._api.colour_temp_mode:
|
||||
return ColorMode.COLOR_TEMP
|
||||
|
||||
if self._api.supports_brightness:
|
||||
return ColorMode.BRIGHTNESS
|
||||
|
||||
return ColorMode.ONOFF
|
||||
19
homeassistant/components/hue_ble/manifest.json
Normal file
19
homeassistant/components/hue_ble/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "hue_ble",
|
||||
"name": "Philips Hue BLE",
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": true,
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@flip-dots"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/hue_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["bleak", "HueBLE"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["HueBLE==1.0.8"]
|
||||
}
|
||||
60
homeassistant/components/hue_ble/quality_scale.yaml
Normal file
60
homeassistant/components/hue_ble/quality_scale.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
21
homeassistant/components/hue_ble/strings.json
Normal file
21
homeassistant/components/hue_ble/strings.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"not_implemented": "This integration can only be setup via discovery."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "Invalid authentication. Ensure the light is [made discoverable to voice assistants]({url_pairing_mode}).",
|
||||
"no_scanners": "No Bluetooth scanners are available to search for the light.",
|
||||
"not_found": "The light was not found.",
|
||||
"not_supported": "The light is not a supported model.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up {name} ({mac})?. Make sure the light is [made discoverable to voice assistants]({url_pairing_mode})."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
homeassistant/generated/bluetooth.py
generated
5
homeassistant/generated/bluetooth.py
generated
@@ -319,6 +319,11 @@ BLUETOOTH: Final[list[dict[str, bool | str | int | list[int]]]] = [
|
||||
],
|
||||
"manufacturer_id": 76,
|
||||
},
|
||||
{
|
||||
"connectable": True,
|
||||
"domain": "hue_ble",
|
||||
"service_data_uuid": "0000fe0f-0000-1000-8000-00805f9b34fb",
|
||||
},
|
||||
{
|
||||
"connectable": True,
|
||||
"domain": "husqvarna_automower_ble",
|
||||
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -290,6 +290,7 @@ FLOWS = {
|
||||
"html5",
|
||||
"huawei_lte",
|
||||
"hue",
|
||||
"hue_ble",
|
||||
"huisbaasje",
|
||||
"hunterdouglas_powerview",
|
||||
"husqvarna_automower",
|
||||
|
||||
@@ -4992,6 +4992,12 @@
|
||||
"iot_class": "local_push",
|
||||
"name": "Philips Hue"
|
||||
},
|
||||
"hue_ble": {
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Philips Hue BLE"
|
||||
},
|
||||
"philips_js": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
|
||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -21,6 +21,9 @@ HAP-python==5.0.0
|
||||
# homeassistant.components.tasmota
|
||||
HATasmota==0.10.1
|
||||
|
||||
# homeassistant.components.hue_ble
|
||||
HueBLE==1.0.8
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
|
||||
|
||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -21,6 +21,9 @@ HAP-python==5.0.0
|
||||
# homeassistant.components.tasmota
|
||||
HATasmota==0.10.1
|
||||
|
||||
# homeassistant.components.hue_ble
|
||||
HueBLE==1.0.8
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==2.1.2
|
||||
|
||||
|
||||
44
tests/components/hue_ble/__init__.py
Normal file
44
tests/components/hue_ble/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Tests for the HueBLE Bluetooth integration."""
|
||||
|
||||
from habluetooth import BluetoothServiceInfoBleak
|
||||
|
||||
from tests.components.bluetooth import generate_advertisement_data, generate_ble_device
|
||||
|
||||
TEST_DEVICE_NAME = "Hue Light"
|
||||
TEST_DEVICE_MAC = "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
HUE_BLE_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name=TEST_DEVICE_NAME,
|
||||
manufacturer_data={89: b"\x12\x02\x00\x02"},
|
||||
service_data={"0000fe0f-0000-1000-8000-00805f9b34fb": b"\x02\x10\x0e\xbe\x00"},
|
||||
service_uuids=[
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
"00001801-0000-1000-8000-00805f9b34fb",
|
||||
"0000180a-0000-1000-8000-00805f9b34fb",
|
||||
"0000fe0f-0000-1000-8000-00805f9b34fb",
|
||||
"932c32bd-0000-47a2-835a-a8d455b859dd",
|
||||
"9da2ddf1-0000-44d0-909c-3f3d3cb34a7b",
|
||||
"b8843add-0000-4aa1-8794-c3f462030bda",
|
||||
],
|
||||
address=TEST_DEVICE_MAC,
|
||||
rssi=-60,
|
||||
source="local",
|
||||
advertisement=generate_advertisement_data(
|
||||
local_name=TEST_DEVICE_NAME,
|
||||
manufacturer_data={89: b"\xfd`0U\x92W"},
|
||||
service_data={"0000fe0f-0000-1000-8000-00805f9b34fb": b"\x02\x10\x0e\xbe\x00"},
|
||||
service_uuids=[
|
||||
"00001800-0000-1000-8000-00805f9b34fb",
|
||||
"00001801-0000-1000-8000-00805f9b34fb",
|
||||
"0000180a-0000-1000-8000-00805f9b34fb",
|
||||
"0000fe0f-0000-1000-8000-00805f9b34fb",
|
||||
"932c32bd-0000-47a2-835a-a8d455b859dd",
|
||||
"9da2ddf1-0000-44d0-909c-3f3d3cb34a7b",
|
||||
"b8843add-0000-4aa1-8794-c3f462030bda",
|
||||
],
|
||||
),
|
||||
device=generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
83
tests/components/hue_ble/conftest.py
Normal file
83
tests/components/hue_ble/conftest.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Common fixtures for the Hue BLE tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hue_ble.const import DOMAIN
|
||||
|
||||
from . import TEST_DEVICE_MAC, TEST_DEVICE_NAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import generate_ble_device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.hue_ble.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_scanner_count() -> Generator[AsyncMock]:
|
||||
"""Override async_scanner_count."""
|
||||
with patch(
|
||||
"homeassistant.components.hue_ble.async_scanner_count", return_value=1
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_ble_device() -> Generator[AsyncMock]:
|
||||
"""Override async_scanner_count."""
|
||||
with patch(
|
||||
"homeassistant.components.hue_ble.async_ble_device_from_address",
|
||||
return_value=generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth(enable_bluetooth: None):
|
||||
"""Auto mock bluetooth."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Create a mock config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title=TEST_DEVICE_NAME,
|
||||
unique_id=TEST_DEVICE_MAC.lower(),
|
||||
data={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_light() -> Generator[AsyncMock]:
|
||||
"""Mock a Hue BLE light."""
|
||||
with patch(
|
||||
"homeassistant.components.hue_ble.HueBleLight", autospec=True
|
||||
) as mock_client:
|
||||
client = mock_client.return_value
|
||||
client.address = TEST_DEVICE_MAC
|
||||
client.maximum_mireds = 454
|
||||
client.minimum_mireds = 153
|
||||
client.name = TEST_DEVICE_NAME
|
||||
client.manufacturer = "Signify Netherlands B.V."
|
||||
client.model = "LTC004"
|
||||
client.firmware = "1.104.2"
|
||||
client.supports_colour_xy = True
|
||||
client.supports_colour_temp = True
|
||||
client.supports_brightness = True
|
||||
client.supports_on_off = True
|
||||
client.available = True
|
||||
client.power_state = True
|
||||
client.brightness = 100
|
||||
client.colour_temp = 250
|
||||
client.colour_xy = (0.5, 0.5)
|
||||
yield client
|
||||
84
tests/components/hue_ble/snapshots/test_light.ambr
Normal file
84
tests/components/hue_ble/snapshots/test_light.ambr
Normal file
@@ -0,0 +1,84 @@
|
||||
# serializer version: 1
|
||||
# name: test_light[light.hue_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max_color_temp_kelvin': 6535,
|
||||
'max_mireds': 454,
|
||||
'min_color_temp_kelvin': 2202,
|
||||
'min_mireds': 153,
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||
<ColorMode.XY: 'xy'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.hue_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'hue_ble',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'AA:BB:CC:DD:EE:FF',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_light[light.hue_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': 100,
|
||||
'color_mode': <ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||
'color_temp': 250,
|
||||
'color_temp_kelvin': 4000,
|
||||
'friendly_name': 'Hue Light',
|
||||
'hs_color': tuple(
|
||||
26.812,
|
||||
34.87,
|
||||
),
|
||||
'max_color_temp_kelvin': 6535,
|
||||
'max_mireds': 454,
|
||||
'min_color_temp_kelvin': 2202,
|
||||
'min_mireds': 153,
|
||||
'rgb_color': tuple(
|
||||
255,
|
||||
206,
|
||||
166,
|
||||
),
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.COLOR_TEMP: 'color_temp'>,
|
||||
<ColorMode.XY: 'xy'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
'xy_color': tuple(
|
||||
0.42,
|
||||
0.365,
|
||||
),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.hue_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
314
tests/components/hue_ble/test_config_flow.py
Normal file
314
tests/components/hue_ble/test_config_flow.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Test the Hue BLE config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
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.config_entries import SOURCE_BLUETOOTH
|
||||
from homeassistant.const import CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=HUE_BLE_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
assert result["description_placeholders"] == {
|
||||
CONF_NAME: TEST_DEVICE_NAME,
|
||||
CONF_MAC: TEST_DEVICE_MAC,
|
||||
"url_pairing_mode": URL_PAIRING_MODE,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_ble_device_from_address",
|
||||
return_value=generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_scanner_count",
|
||||
return_value=1,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.HueBleLight.connect",
|
||||
side_effect=[True],
|
||||
),
|
||||
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,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == TEST_DEVICE_NAME
|
||||
assert result["result"].unique_id == dr.format_mac(TEST_DEVICE_MAC)
|
||||
assert result["result"].data == {}
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"mock_return_device",
|
||||
"mock_scanner_count",
|
||||
"mock_connect",
|
||||
"mock_authenticated",
|
||||
"mock_connected",
|
||||
"mock_support_on_off",
|
||||
"mock_poll_state",
|
||||
"error",
|
||||
),
|
||||
[
|
||||
(
|
||||
None,
|
||||
0,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
(True, []),
|
||||
Error.NO_SCANNERS,
|
||||
),
|
||||
(
|
||||
None,
|
||||
1,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
(True, []),
|
||||
Error.NOT_FOUND,
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
1,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
True,
|
||||
(True, []),
|
||||
Error.INVALID_AUTH,
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
1,
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
True,
|
||||
(True, []),
|
||||
Error.CANNOT_CONNECT,
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
1,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
False,
|
||||
(True, []),
|
||||
Error.NOT_SUPPORTED,
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
1,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
True,
|
||||
(True, ["ERROR!"]),
|
||||
Error.CANNOT_CONNECT,
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
1,
|
||||
Exception,
|
||||
None,
|
||||
True,
|
||||
True,
|
||||
(True, []),
|
||||
Error.UNKNOWN,
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"no_scanners",
|
||||
"not_found",
|
||||
"invalid_auth",
|
||||
"cannot_connect",
|
||||
"not_supported",
|
||||
"cannot_poll",
|
||||
"unknown",
|
||||
],
|
||||
)
|
||||
async def test_bluetooth_form_exception(
|
||||
hass: HomeAssistant,
|
||||
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_support_on_off: bool,
|
||||
mock_poll_state: Exception | tuple[bool, list[Exception]],
|
||||
error: Error,
|
||||
) -> None:
|
||||
"""Test bluetooth discovery form with errors."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=HUE_BLE_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_ble_device_from_address",
|
||||
return_value=mock_return_device,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_scanner_count",
|
||||
return_value=mock_scanner_count,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.HueBleLight.connect",
|
||||
side_effect=[mock_connect],
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.HueBleLight.supports_on_off",
|
||||
new_callable=PropertyMock,
|
||||
return_value=mock_support_on_off,
|
||||
),
|
||||
patch(
|
||||
"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"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error.value}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_ble_device_from_address",
|
||||
return_value=generate_ble_device(TEST_DEVICE_NAME, TEST_DEVICE_MAC),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.async_scanner_count",
|
||||
return_value=1,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.config_flow.HueBleLight.connect",
|
||||
side_effect=[True],
|
||||
),
|
||||
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,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_form_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the user form raises a discovery only error."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "not_implemented"
|
||||
|
||||
|
||||
async def test_bluetooth_form_exception_already_set_up(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test bluetooth discovery form when device is already set up."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_BLUETOOTH},
|
||||
data=HUE_BLE_SERVICE_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
123
tests/components/hue_ble/test_init.py
Normal file
123
tests/components/hue_ble/test_init.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Test hue_ble setup process."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from bleak.backends.device import BLEDevice
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.hue_ble.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import TEST_DEVICE_MAC, TEST_DEVICE_NAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.bluetooth import generate_ble_device
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"ble_device",
|
||||
"scanner_count",
|
||||
"connect_result",
|
||||
"poll_state_result",
|
||||
"message",
|
||||
),
|
||||
[
|
||||
(
|
||||
None,
|
||||
2,
|
||||
True,
|
||||
True,
|
||||
"The light was not found.",
|
||||
),
|
||||
(
|
||||
None,
|
||||
0,
|
||||
True,
|
||||
True,
|
||||
"No Bluetooth scanners are available to search for the light.",
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
|
||||
2,
|
||||
False,
|
||||
True,
|
||||
"Device found but unable to connect.",
|
||||
),
|
||||
(
|
||||
generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
|
||||
2,
|
||||
True,
|
||||
False,
|
||||
"Device found but unable to connect.",
|
||||
),
|
||||
],
|
||||
ids=["no_device", "no_scanners", "error_connect", "error_poll"],
|
||||
)
|
||||
async def test_setup_error(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
ble_device: BLEDevice | None,
|
||||
scanner_count: int,
|
||||
connect_result: bool,
|
||||
poll_state_result: bool,
|
||||
message: str,
|
||||
) -> None:
|
||||
"""Test that ConfigEntryNotReady is raised if there is an error condition."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, unique_id="abcd", data={})
|
||||
entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.async_ble_device_from_address",
|
||||
return_value=ble_device,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.async_scanner_count",
|
||||
return_value=scanner_count,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.HueBleLight.connect",
|
||||
return_value=connect_result,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.HueBleLight.poll_state",
|
||||
return_value=poll_state_result,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {}) is True
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
assert message in caplog.text
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that the config is loaded if there are no errors."""
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, unique_id="abcd", data={})
|
||||
entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.async_ble_device_from_address",
|
||||
return_value=generate_ble_device(TEST_DEVICE_MAC, TEST_DEVICE_NAME),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.async_scanner_count",
|
||||
return_value=1,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.HueBleLight.connect",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hue_ble.HueBleLight.poll_state",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {}) is True
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
29
tests/components/hue_ble/test_light.py
Normal file
29
tests/components/hue_ble/test_light.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Hue BLE light tests."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_light(
|
||||
hass: HomeAssistant,
|
||||
mock_scanner_count: AsyncMock,
|
||||
mock_light: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test light entity setup."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
Reference in New Issue
Block a user