Fix yeelight connection issue (#40251)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Xiaonan Shen 2020-10-27 21:47:11 +08:00 committed by GitHub
parent 08342a1e05
commit f23fcfcd9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 126 deletions

View File

@ -7,7 +7,7 @@ from typing import Optional
import voluptuous as vol import voluptuous as vol
from yeelight import Bulb, BulbException, discover_bulbs from yeelight import Bulb, BulbException, discover_bulbs
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICES, CONF_DEVICES,
CONF_HOST, CONF_HOST,
@ -180,8 +180,8 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Yeelight from a config entry.""" """Set up Yeelight from a config entry."""
async def _initialize(host: str) -> None: async def _initialize(host: str, capabilities: Optional[dict] = None) -> None:
device = await _async_setup_device(hass, host, entry.options) device = await _async_setup_device(hass, host, entry, capabilities)
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device
for component in PLATFORMS: for component in PLATFORMS:
hass.async_create_task( hass.async_create_task(
@ -252,20 +252,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
async def _async_setup_device( async def _async_setup_device(
hass: HomeAssistant, hass: HomeAssistant,
host: str, host: str,
config: dict, entry: ConfigEntry,
capabilities: Optional[dict],
) -> None: ) -> None:
# Get model from config and capabilities
model = entry.options.get(CONF_MODEL)
if not model and capabilities is not None:
model = capabilities.get("model")
# Set up device # Set up device
bulb = Bulb(host, model=config.get(CONF_MODEL) or None) bulb = Bulb(host, model=model or None)
if capabilities is None:
capabilities = await hass.async_add_executor_job(bulb.get_capabilities) capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
if capabilities is None: # timeout
_LOGGER.error("Failed to get capabilities from %s", host) device = YeelightDevice(hass, host, entry.options, bulb, capabilities)
raise ConfigEntryNotReady
device = YeelightDevice(hass, host, config, bulb)
await hass.async_add_executor_job(device.update) await hass.async_add_executor_job(device.update)
await device.async_setup() await device.async_setup()
return device return device
@callback
def _async_unique_name(capabilities: dict) -> str:
"""Generate name from capabilities."""
model = capabilities["model"]
unique_id = capabilities["id"]
return f"yeelight_{model}_{unique_id}"
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(entry.entry_id) await hass.config_entries.async_reload(entry.entry_id)
@ -332,7 +345,7 @@ class YeelightScanner:
"""Register callback function.""" """Register callback function."""
host = self._seen.get(unique_id) host = self._seen.get(unique_id)
if host is not None: if host is not None:
self._hass.async_add_job(callback_func(host)) self._hass.async_create_task(callback_func(host))
else: else:
self._callbacks[unique_id] = callback_func self._callbacks[unique_id] = callback_func
if len(self._callbacks) == 1: if len(self._callbacks) == 1:
@ -351,18 +364,25 @@ class YeelightScanner:
class YeelightDevice: class YeelightDevice:
"""Represents single Yeelight device.""" """Represents single Yeelight device."""
def __init__(self, hass, host, config, bulb): def __init__(self, hass, host, config, bulb, capabilities):
"""Initialize device.""" """Initialize device."""
self._hass = hass self._hass = hass
self._config = config self._config = config
self._host = host self._host = host
unique_id = bulb.capabilities.get("id")
self._name = config.get(CONF_NAME) or f"yeelight_{bulb.model}_{unique_id}"
self._bulb_device = bulb self._bulb_device = bulb
self._capabilities = capabilities or {}
self._device_type = None self._device_type = None
self._available = False self._available = False
self._remove_time_tracker = None self._remove_time_tracker = None
self._name = host # Default name is host
if capabilities:
# Generate name from model and id when capabilities is available
self._name = _async_unique_name(capabilities)
if config.get(CONF_NAME):
# Override default name when name is set in config
self._name = config[CONF_NAME]
@property @property
def bulb(self): def bulb(self):
"""Return bulb device.""" """Return bulb device."""
@ -396,7 +416,7 @@ class YeelightDevice:
@property @property
def fw_version(self): def fw_version(self):
"""Return the firmware version.""" """Return the firmware version."""
return self._bulb_device.capabilities.get("fw_ver") return self._capabilities.get("fw_ver")
@property @property
def is_nightlight_supported(self) -> bool: def is_nightlight_supported(self) -> bool:
@ -449,11 +469,6 @@ class YeelightDevice:
return self._device_type return self._device_type
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self.bulb.capabilities.get("id")
def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None):
"""Turn on device.""" """Turn on device."""
try: try:
@ -532,15 +547,24 @@ class YeelightDevice:
class YeelightEntity(Entity): class YeelightEntity(Entity):
"""Represents single Yeelight entity.""" """Represents single Yeelight entity."""
def __init__(self, device: YeelightDevice): def __init__(self, device: YeelightDevice, entry: ConfigEntry):
"""Initialize the entity.""" """Initialize the entity."""
self._device = device self._device = device
self._unique_id = entry.entry_id
if entry.unique_id is not None:
# Use entry unique id (device id) whenever possible
self._unique_id = entry.unique_id
@property
def unique_id(self) -> str:
"""Return the unique ID."""
return self._unique_id
@property @property
def device_info(self) -> dict: def device_info(self) -> dict:
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": {(DOMAIN, self._device.unique_id)}, "identifiers": {(DOMAIN, self._unique_id)},
"name": self._device.name, "name": self._device.name,
"manufacturer": "Yeelight", "manufacturer": "Yeelight",
"model": self._device.model, "model": self._device.model,

View File

@ -1,6 +1,5 @@
"""Sensor platform support for yeelight.""" """Sensor platform support for yeelight."""
import logging import logging
from typing import Optional
from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -19,7 +18,7 @@ async def async_setup_entry(
device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE] device = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE]
if device.is_nightlight_supported: if device.is_nightlight_supported:
_LOGGER.debug("Adding nightlight mode sensor for %s", device.name) _LOGGER.debug("Adding nightlight mode sensor for %s", device.name)
async_add_entities([YeelightNightlightModeSensor(device)]) async_add_entities([YeelightNightlightModeSensor(device, config_entry)])
class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity): class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity):
@ -35,16 +34,6 @@ class YeelightNightlightModeSensor(YeelightEntity, BinarySensorEntity):
) )
) )
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
unique = self._device.unique_id
if unique:
return unique + "-nightlight_sensor"
return None
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""

View File

@ -18,6 +18,7 @@ from . import (
CONF_SAVE_ON_CHANGE, CONF_SAVE_ON_CHANGE,
CONF_TRANSITION, CONF_TRANSITION,
NIGHTLIGHT_SWITCH_TYPE_LIGHT, NIGHTLIGHT_SWITCH_TYPE_LIGHT,
_async_unique_name,
) )
from . import DOMAIN # pylint:disable=unused-import from . import DOMAIN # pylint:disable=unused-import
@ -38,7 +39,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize the config flow.""" """Initialize the config flow."""
self._capabilities = None
self._discovered_devices = {} self._discovered_devices = {}
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
@ -49,7 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try: try:
await self._async_try_connect(user_input[CONF_HOST]) await self._async_try_connect(user_input[CONF_HOST])
return self.async_create_entry( return self.async_create_entry(
title=self._async_default_name(), title=user_input[CONF_HOST],
data=user_input, data=user_input,
) )
except CannotConnect: except CannotConnect:
@ -59,9 +59,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
else: else:
return await self.async_step_pick_device() return await self.async_step_pick_device()
user_input = user_input or {}
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema({vol.Optional(CONF_HOST): str}), data_schema=vol.Schema(
{vol.Optional(CONF_HOST, default=user_input.get(CONF_HOST, "")): str}
),
errors=errors, errors=errors,
) )
@ -69,9 +72,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the step to pick discovered device.""" """Handle the step to pick discovered device."""
if user_input is not None: if user_input is not None:
unique_id = user_input[CONF_DEVICE] unique_id = user_input[CONF_DEVICE]
self._capabilities = self._discovered_devices[unique_id] capabilities = self._discovered_devices[unique_id]
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=self._async_default_name(), title=_async_unique_name(capabilities),
data={CONF_ID: unique_id}, data={CONF_ID: unique_id},
) )
@ -122,25 +127,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_try_connect(self, host): async def _async_try_connect(self, host):
"""Set up with options.""" """Set up with options."""
for entry in self._async_current_entries():
if entry.data.get(CONF_HOST) == host:
raise AlreadyConfigured
bulb = yeelight.Bulb(host) bulb = yeelight.Bulb(host)
try: try:
capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities) capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities)
if capabilities is None: # timeout if capabilities is None: # timeout
_LOGGER.error("Failed to get capabilities from %s: timeout", host) _LOGGER.debug("Failed to get capabilities from %s: timeout", host)
raise CannotConnect else:
except OSError as err:
_LOGGER.error("Failed to get capabilities from %s: %s", host, err)
raise CannotConnect from err
_LOGGER.debug("Get capabilities: %s", capabilities) _LOGGER.debug("Get capabilities: %s", capabilities)
self._capabilities = capabilities
await self.async_set_unique_id(capabilities["id"]) await self.async_set_unique_id(capabilities["id"])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return
except OSError as err:
_LOGGER.debug("Failed to get capabilities from %s: %s", host, err)
# Ignore the error since get_capabilities uses UDP discovery packet
# which does not work in all network environments
@callback # Fallback to get properties
def _async_default_name(self): try:
model = self._capabilities["model"] await self.hass.async_add_executor_job(bulb.get_properties)
unique_id = self._capabilities["id"] except yeelight.BulbException as err:
return f"yeelight_{model}_{unique_id}" _LOGGER.error("Failed to get properties from %s: %s", host, err)
raise CannotConnect from err
_LOGGER.debug("Get properties: %s", bulb.last_properties)
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
@ -153,11 +165,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):
"""Handle the initial step.""" """Handle the initial step."""
if user_input is not None: if user_input is not None:
# keep the name from imported entries options = {**self._config_entry.options}
options = { options.update(user_input)
CONF_NAME: self._config_entry.options.get(CONF_NAME),
**user_input,
}
return self.async_create_entry(title="", data=options) return self.async_create_entry(title="", data=options)
options = self._config_entry.options options = self._config_entry.options

View File

@ -1,7 +1,6 @@
"""Light platform support for yeelight.""" """Light platform support for yeelight."""
from functools import partial from functools import partial
import logging import logging
from typing import Optional
import voluptuous as vol import voluptuous as vol
import yeelight import yeelight
@ -241,7 +240,7 @@ async def async_setup_entry(
device_type = device.type device_type = device.type
def _lights_setup_helper(klass): def _lights_setup_helper(klass):
lights.append(klass(device, custom_effects=custom_effects)) lights.append(klass(device, config_entry, custom_effects=custom_effects))
if device_type == BulbType.White: if device_type == BulbType.White:
_lights_setup_helper(YeelightGenericLight) _lights_setup_helper(YeelightGenericLight)
@ -382,9 +381,9 @@ def _async_setup_services(hass: HomeAssistant):
class YeelightGenericLight(YeelightEntity, LightEntity): class YeelightGenericLight(YeelightEntity, LightEntity):
"""Representation of a Yeelight generic light.""" """Representation of a Yeelight generic light."""
def __init__(self, device, custom_effects=None): def __init__(self, device, entry, custom_effects=None):
"""Initialize the Yeelight light.""" """Initialize the Yeelight light."""
super().__init__(device) super().__init__(device, entry)
self.config = device.config self.config = device.config
@ -418,12 +417,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
) )
) )
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self.device.unique_id
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Flag supported features.""" """Flag supported features."""
@ -852,14 +845,10 @@ class YeelightNightLightMode(YeelightGenericLight):
"""Representation of a Yeelight when in nightlight mode.""" """Representation of a Yeelight when in nightlight mode."""
@property @property
def unique_id(self) -> Optional[str]: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
unique = super().unique_id unique = super().unique_id
return f"{unique}-nightlight"
if unique:
return unique + "-nightlight"
return None
@property @property
def name(self) -> str: def name(self) -> str:
@ -945,12 +934,10 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch):
self._light_type = LightType.Ambient self._light_type = LightType.Ambient
@property @property
def unique_id(self) -> Optional[str]: def unique_id(self) -> str:
"""Return a unique ID.""" """Return a unique ID."""
unique = super().unique_id unique = super().unique_id
return f"{unique}-ambilight"
if unique:
return unique + "-ambilight"
@property @property
def name(self) -> str: def name(self) -> str:

View File

@ -1,5 +1,5 @@
"""Tests for the Yeelight integration.""" """Tests for the Yeelight integration."""
from yeelight import BulbType from yeelight import BulbException, BulbType
from yeelight.main import _MODEL_SPECS from yeelight.main import _MODEL_SPECS
from homeassistant.components.yeelight import ( from homeassistant.components.yeelight import (
@ -8,6 +8,7 @@ from homeassistant.components.yeelight import (
CONF_SAVE_ON_CHANGE, CONF_SAVE_ON_CHANGE,
DOMAIN, DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT, NIGHTLIGHT_SWITCH_TYPE_LIGHT,
YeelightScanner,
) )
from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME
@ -27,7 +28,8 @@ CAPABILITIES = {
"name": "", "name": "",
} }
NAME = f"yeelight_{MODEL}_{ID}" NAME = "name"
UNIQUE_NAME = f"yeelight_{MODEL}_{ID}"
MODULE = "homeassistant.components.yeelight" MODULE = "homeassistant.components.yeelight"
MODULE_CONFIG_FLOW = f"{MODULE}.config_flow" MODULE_CONFIG_FLOW = f"{MODULE}.config_flow"
@ -53,9 +55,10 @@ PROPERTIES = {
"current_brightness": "30", "current_brightness": "30",
} }
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight"
ENTITY_LIGHT = f"light.{NAME}" ENTITY_LIGHT = f"light.{UNIQUE_NAME}"
ENTITY_NIGHTLIGHT = f"light.{NAME}_nightlight" ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight"
ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight"
YAML_CONFIGURATION = { YAML_CONFIGURATION = {
DOMAIN: { DOMAIN: {
@ -80,6 +83,9 @@ def _mocked_bulb(cannot_connect=False):
type(bulb).get_capabilities = MagicMock( type(bulb).get_capabilities = MagicMock(
return_value=None if cannot_connect else CAPABILITIES return_value=None if cannot_connect else CAPABILITIES
) )
type(bulb).get_properties = MagicMock(
side_effect=BulbException if cannot_connect else None
)
type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL]) type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL])
bulb.capabilities = CAPABILITIES bulb.capabilities = CAPABILITIES
@ -92,6 +98,8 @@ def _mocked_bulb(cannot_connect=False):
def _patch_discovery(prefix, no_device=False): def _patch_discovery(prefix, no_device=False):
YeelightScanner._scanner = None # Clear class scanner to reset hass
def _mocked_discovery(timeout=2, interface=False): def _mocked_discovery(timeout=2, interface=False):
if no_device: if no_device:
return [] return []

View File

@ -4,10 +4,12 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_component from homeassistant.helpers import entity_component
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import ENTITY_BINARY_SENSOR, MODULE, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb
from tests.async_mock import patch from tests.async_mock import patch
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
async def test_nightlight(hass: HomeAssistant): async def test_nightlight(hass: HomeAssistant):
"""Test nightlight sensor.""" """Test nightlight sensor."""

View File

@ -25,6 +25,7 @@ from . import (
MODULE, MODULE,
MODULE_CONFIG_FLOW, MODULE_CONFIG_FLOW,
NAME, NAME,
UNIQUE_NAME,
_mocked_bulb, _mocked_bulb,
_patch_discovery, _patch_discovery,
) )
@ -33,7 +34,6 @@ from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
CONF_NAME: NAME,
CONF_MODEL: "", CONF_MODEL: "",
CONF_TRANSITION: DEFAULT_TRANSITION, CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
@ -67,9 +67,8 @@ async def test_discovery(hass: HomeAssistant):
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_DEVICE: ID} result["flow_id"], {CONF_DEVICE: ID}
) )
assert result3["type"] == "create_entry" assert result3["type"] == "create_entry"
assert result3["title"] == NAME assert result3["title"] == UNIQUE_NAME
assert result3["data"] == {CONF_ID: ID} assert result3["data"] == {CONF_ID: ID}
await hass.async_block_till_done() await hass.async_block_till_done()
mock_setup.assert_called_once() mock_setup.assert_called_once()
@ -126,6 +125,7 @@ async def test_import(hass: HomeAssistant):
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
) )
type(mocked_bulb).get_capabilities.assert_called_once() type(mocked_bulb).get_capabilities.assert_called_once()
type(mocked_bulb).get_properties.assert_called_once()
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "cannot_connect" assert result["reason"] == "cannot_connect"
@ -203,7 +203,9 @@ async def test_manual(hass: HomeAssistant):
result4 = await hass.config_entries.flow.async_configure( result4 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS} result["flow_id"], {CONF_HOST: IP_ADDRESS}
) )
await hass.async_block_till_done()
assert result4["type"] == "create_entry" assert result4["type"] == "create_entry"
assert result4["title"] == IP_ADDRESS
assert result4["data"] == {CONF_HOST: IP_ADDRESS} assert result4["data"] == {CONF_HOST: IP_ADDRESS}
# Duplicate # Duplicate
@ -221,7 +223,9 @@ async def test_manual(hass: HomeAssistant):
async def test_options(hass: HomeAssistant): async def test_options(hass: HomeAssistant):
"""Test options flow.""" """Test options flow."""
config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}) config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: IP_ADDRESS, CONF_NAME: NAME}
)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb() mocked_bulb = _mocked_bulb()
@ -230,16 +234,14 @@ async def test_options(hass: HomeAssistant):
await hass.async_block_till_done() await hass.async_block_till_done()
config = { config = {
CONF_NAME: NAME,
CONF_MODEL: "", CONF_MODEL: "",
CONF_TRANSITION: DEFAULT_TRANSITION, CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC, CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE, CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH, CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH,
} }
assert config_entry.options == { assert config_entry.options == config
CONF_NAME: "",
**config,
}
assert hass.states.get(f"light.{NAME}_nightlight") is None assert hass.states.get(f"light.{NAME}_nightlight") is None
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
@ -247,15 +249,40 @@ async def test_options(hass: HomeAssistant):
assert result["step_id"] == "init" assert result["step_id"] == "init"
config[CONF_NIGHTLIGHT_SWITCH] = True config[CONF_NIGHTLIGHT_SWITCH] = True
user_input = {**config}
user_input.pop(CONF_NAME)
with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): with patch(f"{MODULE}.Bulb", return_value=mocked_bulb):
result2 = await hass.config_entries.options.async_configure( result2 = await hass.config_entries.options.async_configure(
result["flow_id"], config result["flow_id"], user_input
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result2["type"] == "create_entry" assert result2["type"] == "create_entry"
assert result2["data"] == { assert result2["data"] == config
CONF_NAME: "",
**config,
}
assert result2["data"] == config_entry.options assert result2["data"] == config_entry.options
assert hass.states.get(f"light.{NAME}_nightlight") is not None assert hass.states.get(f"light.{NAME}_nightlight") is not None
async def test_manual_no_capabilities(hass: HomeAssistant):
"""Test manually setup without successful get_capabilities."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert not result["errors"]
mocked_bulb = _mocked_bulb()
type(mocked_bulb).get_capabilities = MagicMock(return_value=None)
with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb), patch(
f"{MODULE}.async_setup", return_value=True
), patch(
f"{MODULE}.async_setup_entry",
return_value=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_HOST: IP_ADDRESS}
)
type(mocked_bulb).get_capabilities.assert_called_once()
type(mocked_bulb).get_properties.assert_called_once()
assert result["type"] == "create_entry"
assert result["data"] == {CONF_HOST: IP_ADDRESS}

View File

@ -1,19 +1,27 @@
"""Test Yeelight.""" """Test Yeelight."""
from yeelight import BulbType
from homeassistant.components.yeelight import ( from homeassistant.components.yeelight import (
CONF_NIGHTLIGHT_SWITCH,
CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_NIGHTLIGHT_SWITCH_TYPE,
DOMAIN, DOMAIN,
NIGHTLIGHT_SWITCH_TYPE_LIGHT, NIGHTLIGHT_SWITCH_TYPE_LIGHT,
) )
from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.const import CONF_DEVICES, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from . import ( from . import (
CONFIG_ENTRY_DATA, CONFIG_ENTRY_DATA,
ENTITY_AMBILIGHT,
ENTITY_BINARY_SENSOR,
ENTITY_LIGHT,
ENTITY_NIGHTLIGHT,
ID,
IP_ADDRESS, IP_ADDRESS,
MODULE, MODULE,
MODULE_CONFIG_FLOW, MODULE_CONFIG_FLOW,
NAME,
_mocked_bulb, _mocked_bulb,
_patch_discovery, _patch_discovery,
) )
@ -32,13 +40,13 @@ async def test_setup_discovery(hass: HomeAssistant):
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(f"binary_sensor.{NAME}_nightlight") is not None assert hass.states.get(ENTITY_BINARY_SENSOR) is not None
assert hass.states.get(f"light.{NAME}") is not None assert hass.states.get(ENTITY_LIGHT) is not None
# Unload # Unload
assert await hass.config_entries.async_unload(config_entry.entry_id) assert await hass.config_entries.async_unload(config_entry.entry_id)
assert hass.states.get(f"binary_sensor.{NAME}_nightlight") is None assert hass.states.get(ENTITY_BINARY_SENSOR) is None
assert hass.states.get(f"light.{NAME}") is None assert hass.states.get(ENTITY_LIGHT) is None
async def test_setup_import(hass: HomeAssistant): async def test_setup_import(hass: HomeAssistant):
@ -67,3 +75,57 @@ async def test_setup_import(hass: HomeAssistant):
assert hass.states.get(f"binary_sensor.{name}_nightlight") is not None assert hass.states.get(f"binary_sensor.{name}_nightlight") is not None
assert hass.states.get(f"light.{name}") is not None assert hass.states.get(f"light.{name}") is not None
assert hass.states.get(f"light.{name}_nightlight") is not None assert hass.states.get(f"light.{name}_nightlight") is not None
async def test_unique_ids_device(hass: HomeAssistant):
"""Test Yeelight unique IDs from yeelight device IDs."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: True,
},
unique_id=ID,
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
mocked_bulb.bulb_type = BulbType.WhiteTempMood
with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
er = await entity_registry.async_get_registry(hass)
assert er.async_get(ENTITY_BINARY_SENSOR).unique_id == ID
assert er.async_get(ENTITY_LIGHT).unique_id == ID
assert er.async_get(ENTITY_NIGHTLIGHT).unique_id == f"{ID}-nightlight"
assert er.async_get(ENTITY_AMBILIGHT).unique_id == f"{ID}-ambilight"
async def test_unique_ids_entry(hass: HomeAssistant):
"""Test Yeelight unique IDs from entry IDs."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
**CONFIG_ENTRY_DATA,
CONF_NIGHTLIGHT_SWITCH: True,
},
)
config_entry.add_to_hass(hass)
mocked_bulb = _mocked_bulb()
mocked_bulb.bulb_type = BulbType.WhiteTempMood
with _patch_discovery(MODULE), patch(f"{MODULE}.Bulb", return_value=mocked_bulb):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
er = await entity_registry.async_get_registry(hass)
assert er.async_get(ENTITY_BINARY_SENSOR).unique_id == config_entry.entry_id
assert er.async_get(ENTITY_LIGHT).unique_id == config_entry.entry_id
assert (
er.async_get(ENTITY_NIGHTLIGHT).unique_id
== f"{config_entry.entry_id}-nightlight"
)
assert (
er.async_get(ENTITY_AMBILIGHT).unique_id == f"{config_entry.entry_id}-ambilight"
)

View File

@ -71,8 +71,9 @@ from homeassistant.components.yeelight.light import (
YEELIGHT_MONO_EFFECT_LIST, YEELIGHT_MONO_EFFECT_LIST,
YEELIGHT_TEMP_ONLY_EFFECT_LIST, YEELIGHT_TEMP_ONLY_EFFECT_LIST,
) )
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.color import ( from homeassistant.util.color import (
color_hs_to_RGB, color_hs_to_RGB,
@ -90,6 +91,7 @@ from . import (
MODULE, MODULE,
NAME, NAME,
PROPERTIES, PROPERTIES,
UNIQUE_NAME,
_mocked_bulb, _mocked_bulb,
_patch_discovery, _patch_discovery,
) )
@ -97,15 +99,21 @@ from . import (
from tests.async_mock import MagicMock, patch from tests.async_mock import MagicMock, patch
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
CONFIG_ENTRY_DATA = {
CONF_HOST: IP_ADDRESS,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH,
}
async def test_services(hass: HomeAssistant, caplog): async def test_services(hass: HomeAssistant, caplog):
"""Test Yeelight services.""" """Test Yeelight services."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
CONF_ID: "", **CONFIG_ENTRY_DATA,
CONF_HOST: IP_ADDRESS,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: True, CONF_MODE_MUSIC: True,
CONF_SAVE_ON_CHANGE: True, CONF_SAVE_ON_CHANGE: True,
CONF_NIGHTLIGHT_SWITCH: True, CONF_NIGHTLIGHT_SWITCH: True,
@ -299,17 +307,13 @@ async def test_device_types(hass: HomeAssistant):
model, model,
target_properties, target_properties,
nightlight_properties=None, nightlight_properties=None,
name=NAME, name=UNIQUE_NAME,
entity_id=ENTITY_LIGHT, entity_id=ENTITY_LIGHT,
): ):
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
CONF_ID: "", **CONFIG_ENTRY_DATA,
CONF_HOST: IP_ADDRESS,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
CONF_NIGHTLIGHT_SWITCH: False, CONF_NIGHTLIGHT_SWITCH: False,
}, },
) )
@ -329,6 +333,8 @@ async def test_device_types(hass: HomeAssistant):
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await config_entry.async_remove(hass) await config_entry.async_remove(hass)
registry = await entity_registry.async_get_registry(hass)
registry.async_clear_config_entry(config_entry.entry_id)
# nightlight # nightlight
if nightlight_properties is None: if nightlight_properties is None:
@ -336,11 +342,7 @@ async def test_device_types(hass: HomeAssistant):
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data={
CONF_ID: "", **CONFIG_ENTRY_DATA,
CONF_HOST: IP_ADDRESS,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
CONF_NIGHTLIGHT_SWITCH: True, CONF_NIGHTLIGHT_SWITCH: True,
}, },
) )
@ -358,6 +360,7 @@ async def test_device_types(hass: HomeAssistant):
await hass.config_entries.async_unload(config_entry.entry_id) await hass.config_entries.async_unload(config_entry.entry_id)
await config_entry.async_remove(hass) await config_entry.async_remove(hass)
registry.async_clear_config_entry(config_entry.entry_id)
bright = round(255 * int(PROPERTIES["bright"]) / 100) bright = round(255 * int(PROPERTIES["bright"]) / 100)
current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100) current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100)
@ -486,7 +489,7 @@ async def test_device_types(hass: HomeAssistant):
"rgb_color": bg_rgb_color, "rgb_color": bg_rgb_color,
"xy_color": bg_xy_color, "xy_color": bg_xy_color,
}, },
name=f"{NAME} ambilight", name=f"{UNIQUE_NAME} ambilight",
entity_id=f"{ENTITY_LIGHT}_ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight",
) )
@ -518,14 +521,7 @@ async def test_effects(hass: HomeAssistant):
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={ data=CONFIG_ENTRY_DATA,
CONF_ID: "",
CONF_HOST: IP_ADDRESS,
CONF_TRANSITION: DEFAULT_TRANSITION,
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH,
},
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)