Fix yeelight connection issue (#40251)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
08342a1e05
commit
f23fcfcd9b
10 changed files with 230 additions and 126 deletions
|
@ -7,7 +7,7 @@ from typing import Optional
|
|||
import voluptuous as vol
|
||||
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 (
|
||||
CONF_DEVICES,
|
||||
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:
|
||||
"""Set up Yeelight from a config entry."""
|
||||
|
||||
async def _initialize(host: str) -> None:
|
||||
device = await _async_setup_device(hass, host, entry.options)
|
||||
async def _initialize(host: str, capabilities: Optional[dict] = None) -> None:
|
||||
device = await _async_setup_device(hass, host, entry, capabilities)
|
||||
hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
|
@ -252,20 +252,33 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
async def _async_setup_device(
|
||||
hass: HomeAssistant,
|
||||
host: str,
|
||||
config: dict,
|
||||
entry: ConfigEntry,
|
||||
capabilities: Optional[dict],
|
||||
) -> 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
|
||||
bulb = Bulb(host, model=config.get(CONF_MODEL) or None)
|
||||
capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
|
||||
if capabilities is None: # timeout
|
||||
_LOGGER.error("Failed to get capabilities from %s", host)
|
||||
raise ConfigEntryNotReady
|
||||
device = YeelightDevice(hass, host, config, bulb)
|
||||
bulb = Bulb(host, model=model or None)
|
||||
if capabilities is None:
|
||||
capabilities = await hass.async_add_executor_job(bulb.get_capabilities)
|
||||
|
||||
device = YeelightDevice(hass, host, entry.options, bulb, capabilities)
|
||||
await hass.async_add_executor_job(device.update)
|
||||
await device.async_setup()
|
||||
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):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
@ -332,7 +345,7 @@ class YeelightScanner:
|
|||
"""Register callback function."""
|
||||
host = self._seen.get(unique_id)
|
||||
if host is not None:
|
||||
self._hass.async_add_job(callback_func(host))
|
||||
self._hass.async_create_task(callback_func(host))
|
||||
else:
|
||||
self._callbacks[unique_id] = callback_func
|
||||
if len(self._callbacks) == 1:
|
||||
|
@ -351,18 +364,25 @@ class YeelightScanner:
|
|||
class YeelightDevice:
|
||||
"""Represents single Yeelight device."""
|
||||
|
||||
def __init__(self, hass, host, config, bulb):
|
||||
def __init__(self, hass, host, config, bulb, capabilities):
|
||||
"""Initialize device."""
|
||||
self._hass = hass
|
||||
self._config = config
|
||||
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._capabilities = capabilities or {}
|
||||
self._device_type = None
|
||||
self._available = False
|
||||
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
|
||||
def bulb(self):
|
||||
"""Return bulb device."""
|
||||
|
@ -396,7 +416,7 @@ class YeelightDevice:
|
|||
@property
|
||||
def fw_version(self):
|
||||
"""Return the firmware version."""
|
||||
return self._bulb_device.capabilities.get("fw_ver")
|
||||
return self._capabilities.get("fw_ver")
|
||||
|
||||
@property
|
||||
def is_nightlight_supported(self) -> bool:
|
||||
|
@ -449,11 +469,6 @@ class YeelightDevice:
|
|||
|
||||
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):
|
||||
"""Turn on device."""
|
||||
try:
|
||||
|
@ -532,15 +547,24 @@ class YeelightDevice:
|
|||
class YeelightEntity(Entity):
|
||||
"""Represents single Yeelight entity."""
|
||||
|
||||
def __init__(self, device: YeelightDevice):
|
||||
def __init__(self, device: YeelightDevice, entry: ConfigEntry):
|
||||
"""Initialize the entity."""
|
||||
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
|
||||
def device_info(self) -> dict:
|
||||
"""Return the device info."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self._device.unique_id)},
|
||||
"identifiers": {(DOMAIN, self._unique_id)},
|
||||
"name": self._device.name,
|
||||
"manufacturer": "Yeelight",
|
||||
"model": self._device.model,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
"""Sensor platform support for yeelight."""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
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]
|
||||
if device.is_nightlight_supported:
|
||||
_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):
|
||||
|
@ -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
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
|
|
|
@ -18,6 +18,7 @@ from . import (
|
|||
CONF_SAVE_ON_CHANGE,
|
||||
CONF_TRANSITION,
|
||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
||||
_async_unique_name,
|
||||
)
|
||||
from . import DOMAIN # pylint:disable=unused-import
|
||||
|
||||
|
@ -38,7 +39,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self._capabilities = None
|
||||
self._discovered_devices = {}
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
|
@ -49,7 +49,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
try:
|
||||
await self._async_try_connect(user_input[CONF_HOST])
|
||||
return self.async_create_entry(
|
||||
title=self._async_default_name(),
|
||||
title=user_input[CONF_HOST],
|
||||
data=user_input,
|
||||
)
|
||||
except CannotConnect:
|
||||
|
@ -59,9 +59,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
else:
|
||||
return await self.async_step_pick_device()
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
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,
|
||||
)
|
||||
|
||||
|
@ -69,9 +72,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Handle the step to pick discovered device."""
|
||||
if user_input is not None:
|
||||
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(
|
||||
title=self._async_default_name(),
|
||||
title=_async_unique_name(capabilities),
|
||||
data={CONF_ID: unique_id},
|
||||
)
|
||||
|
||||
|
@ -122,25 +127,32 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
|
||||
async def _async_try_connect(self, host):
|
||||
"""Set up with options."""
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data.get(CONF_HOST) == host:
|
||||
raise AlreadyConfigured
|
||||
|
||||
bulb = yeelight.Bulb(host)
|
||||
try:
|
||||
capabilities = await self.hass.async_add_executor_job(bulb.get_capabilities)
|
||||
if capabilities is None: # timeout
|
||||
_LOGGER.error("Failed to get capabilities from %s: timeout", host)
|
||||
raise CannotConnect
|
||||
_LOGGER.debug("Failed to get capabilities from %s: timeout", host)
|
||||
else:
|
||||
_LOGGER.debug("Get capabilities: %s", capabilities)
|
||||
await self.async_set_unique_id(capabilities["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
return
|
||||
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)
|
||||
self._capabilities = capabilities
|
||||
await self.async_set_unique_id(capabilities["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
_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
|
||||
def _async_default_name(self):
|
||||
model = self._capabilities["model"]
|
||||
unique_id = self._capabilities["id"]
|
||||
return f"yeelight_{model}_{unique_id}"
|
||||
# Fallback to get properties
|
||||
try:
|
||||
await self.hass.async_add_executor_job(bulb.get_properties)
|
||||
except yeelight.BulbException as err:
|
||||
_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):
|
||||
|
@ -153,11 +165,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||
async def async_step_init(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
if user_input is not None:
|
||||
# keep the name from imported entries
|
||||
options = {
|
||||
CONF_NAME: self._config_entry.options.get(CONF_NAME),
|
||||
**user_input,
|
||||
}
|
||||
options = {**self._config_entry.options}
|
||||
options.update(user_input)
|
||||
return self.async_create_entry(title="", data=options)
|
||||
|
||||
options = self._config_entry.options
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""Light platform support for yeelight."""
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import voluptuous as vol
|
||||
import yeelight
|
||||
|
@ -241,7 +240,7 @@ async def async_setup_entry(
|
|||
device_type = device.type
|
||||
|
||||
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:
|
||||
_lights_setup_helper(YeelightGenericLight)
|
||||
|
@ -382,9 +381,9 @@ def _async_setup_services(hass: HomeAssistant):
|
|||
class YeelightGenericLight(YeelightEntity, LightEntity):
|
||||
"""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."""
|
||||
super().__init__(device)
|
||||
super().__init__(device, entry)
|
||||
|
||||
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
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
|
@ -852,14 +845,10 @@ class YeelightNightLightMode(YeelightGenericLight):
|
|||
"""Representation of a Yeelight when in nightlight mode."""
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
unique = super().unique_id
|
||||
|
||||
if unique:
|
||||
return unique + "-nightlight"
|
||||
|
||||
return None
|
||||
return f"{unique}-nightlight"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
@ -945,12 +934,10 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch):
|
|||
self._light_type = LightType.Ambient
|
||||
|
||||
@property
|
||||
def unique_id(self) -> Optional[str]:
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
unique = super().unique_id
|
||||
|
||||
if unique:
|
||||
return unique + "-ambilight"
|
||||
return f"{unique}-ambilight"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
|
|
@ -36,4 +36,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Tests for the Yeelight integration."""
|
||||
from yeelight import BulbType
|
||||
from yeelight import BulbException, BulbType
|
||||
from yeelight.main import _MODEL_SPECS
|
||||
|
||||
from homeassistant.components.yeelight import (
|
||||
|
@ -8,6 +8,7 @@ from homeassistant.components.yeelight import (
|
|||
CONF_SAVE_ON_CHANGE,
|
||||
DOMAIN,
|
||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
||||
YeelightScanner,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME
|
||||
|
||||
|
@ -27,7 +28,8 @@ CAPABILITIES = {
|
|||
"name": "",
|
||||
}
|
||||
|
||||
NAME = f"yeelight_{MODEL}_{ID}"
|
||||
NAME = "name"
|
||||
UNIQUE_NAME = f"yeelight_{MODEL}_{ID}"
|
||||
|
||||
MODULE = "homeassistant.components.yeelight"
|
||||
MODULE_CONFIG_FLOW = f"{MODULE}.config_flow"
|
||||
|
@ -53,9 +55,10 @@ PROPERTIES = {
|
|||
"current_brightness": "30",
|
||||
}
|
||||
|
||||
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
|
||||
ENTITY_LIGHT = f"light.{NAME}"
|
||||
ENTITY_NIGHTLIGHT = f"light.{NAME}_nightlight"
|
||||
ENTITY_BINARY_SENSOR = f"binary_sensor.{UNIQUE_NAME}_nightlight"
|
||||
ENTITY_LIGHT = f"light.{UNIQUE_NAME}"
|
||||
ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight"
|
||||
ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight"
|
||||
|
||||
YAML_CONFIGURATION = {
|
||||
DOMAIN: {
|
||||
|
@ -80,6 +83,9 @@ def _mocked_bulb(cannot_connect=False):
|
|||
type(bulb).get_capabilities = MagicMock(
|
||||
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])
|
||||
|
||||
bulb.capabilities = CAPABILITIES
|
||||
|
@ -92,6 +98,8 @@ def _mocked_bulb(cannot_connect=False):
|
|||
|
||||
|
||||
def _patch_discovery(prefix, no_device=False):
|
||||
YeelightScanner._scanner = None # Clear class scanner to reset hass
|
||||
|
||||
def _mocked_discovery(timeout=2, interface=False):
|
||||
if no_device:
|
||||
return []
|
||||
|
|
|
@ -4,10 +4,12 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.helpers import entity_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
|
||||
|
||||
ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight"
|
||||
|
||||
|
||||
async def test_nightlight(hass: HomeAssistant):
|
||||
"""Test nightlight sensor."""
|
||||
|
|
|
@ -25,6 +25,7 @@ from . import (
|
|||
MODULE,
|
||||
MODULE_CONFIG_FLOW,
|
||||
NAME,
|
||||
UNIQUE_NAME,
|
||||
_mocked_bulb,
|
||||
_patch_discovery,
|
||||
)
|
||||
|
@ -33,7 +34,6 @@ from tests.async_mock import MagicMock, patch
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
CONF_NAME: NAME,
|
||||
CONF_MODEL: "",
|
||||
CONF_TRANSITION: DEFAULT_TRANSITION,
|
||||
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
|
||||
|
@ -67,9 +67,8 @@ async def test_discovery(hass: HomeAssistant):
|
|||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_DEVICE: ID}
|
||||
)
|
||||
|
||||
assert result3["type"] == "create_entry"
|
||||
assert result3["title"] == NAME
|
||||
assert result3["title"] == UNIQUE_NAME
|
||||
assert result3["data"] == {CONF_ID: ID}
|
||||
await hass.async_block_till_done()
|
||||
mock_setup.assert_called_once()
|
||||
|
@ -126,6 +125,7 @@ async def test_import(hass: HomeAssistant):
|
|||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config
|
||||
)
|
||||
type(mocked_bulb).get_capabilities.assert_called_once()
|
||||
type(mocked_bulb).get_properties.assert_called_once()
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
@ -203,7 +203,9 @@ async def test_manual(hass: HomeAssistant):
|
|||
result4 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: IP_ADDRESS}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result4["type"] == "create_entry"
|
||||
assert result4["title"] == IP_ADDRESS
|
||||
assert result4["data"] == {CONF_HOST: IP_ADDRESS}
|
||||
|
||||
# Duplicate
|
||||
|
@ -221,7 +223,9 @@ async def test_manual(hass: HomeAssistant):
|
|||
|
||||
async def test_options(hass: HomeAssistant):
|
||||
"""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)
|
||||
|
||||
mocked_bulb = _mocked_bulb()
|
||||
|
@ -230,16 +234,14 @@ async def test_options(hass: HomeAssistant):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
config = {
|
||||
CONF_NAME: NAME,
|
||||
CONF_MODEL: "",
|
||||
CONF_TRANSITION: DEFAULT_TRANSITION,
|
||||
CONF_MODE_MUSIC: DEFAULT_MODE_MUSIC,
|
||||
CONF_SAVE_ON_CHANGE: DEFAULT_SAVE_ON_CHANGE,
|
||||
CONF_NIGHTLIGHT_SWITCH: DEFAULT_NIGHTLIGHT_SWITCH,
|
||||
}
|
||||
assert config_entry.options == {
|
||||
CONF_NAME: "",
|
||||
**config,
|
||||
}
|
||||
assert config_entry.options == config
|
||||
assert hass.states.get(f"light.{NAME}_nightlight") is None
|
||||
|
||||
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"
|
||||
|
||||
config[CONF_NIGHTLIGHT_SWITCH] = True
|
||||
user_input = {**config}
|
||||
user_input.pop(CONF_NAME)
|
||||
with patch(f"{MODULE}.Bulb", return_value=mocked_bulb):
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], config
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["data"] == {
|
||||
CONF_NAME: "",
|
||||
**config,
|
||||
}
|
||||
assert result2["data"] == config
|
||||
assert result2["data"] == config_entry.options
|
||||
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}
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
"""Test Yeelight."""
|
||||
from yeelight import BulbType
|
||||
|
||||
from homeassistant.components.yeelight import (
|
||||
CONF_NIGHTLIGHT_SWITCH,
|
||||
CONF_NIGHTLIGHT_SWITCH_TYPE,
|
||||
DOMAIN,
|
||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
CONFIG_ENTRY_DATA,
|
||||
ENTITY_AMBILIGHT,
|
||||
ENTITY_BINARY_SENSOR,
|
||||
ENTITY_LIGHT,
|
||||
ENTITY_NIGHTLIGHT,
|
||||
ID,
|
||||
IP_ADDRESS,
|
||||
MODULE,
|
||||
MODULE_CONFIG_FLOW,
|
||||
NAME,
|
||||
_mocked_bulb,
|
||||
_patch_discovery,
|
||||
)
|
||||
|
@ -32,13 +40,13 @@ async def test_setup_discovery(hass: HomeAssistant):
|
|||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
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(ENTITY_BINARY_SENSOR) is not None
|
||||
assert hass.states.get(ENTITY_LIGHT) is not None
|
||||
|
||||
# Unload
|
||||
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(f"light.{NAME}") is None
|
||||
assert hass.states.get(ENTITY_BINARY_SENSOR) is None
|
||||
assert hass.states.get(ENTITY_LIGHT) is None
|
||||
|
||||
|
||||
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"light.{name}") 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"
|
||||
)
|
||||
|
|
|
@ -71,8 +71,9 @@ from homeassistant.components.yeelight.light import (
|
|||
YEELIGHT_MONO_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.helpers import entity_registry
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.color import (
|
||||
color_hs_to_RGB,
|
||||
|
@ -90,6 +91,7 @@ from . import (
|
|||
MODULE,
|
||||
NAME,
|
||||
PROPERTIES,
|
||||
UNIQUE_NAME,
|
||||
_mocked_bulb,
|
||||
_patch_discovery,
|
||||
)
|
||||
|
@ -97,15 +99,21 @@ from . import (
|
|||
from tests.async_mock import MagicMock, patch
|
||||
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):
|
||||
"""Test Yeelight services."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_ID: "",
|
||||
CONF_HOST: IP_ADDRESS,
|
||||
CONF_TRANSITION: DEFAULT_TRANSITION,
|
||||
**CONFIG_ENTRY_DATA,
|
||||
CONF_MODE_MUSIC: True,
|
||||
CONF_SAVE_ON_CHANGE: True,
|
||||
CONF_NIGHTLIGHT_SWITCH: True,
|
||||
|
@ -299,17 +307,13 @@ async def test_device_types(hass: HomeAssistant):
|
|||
model,
|
||||
target_properties,
|
||||
nightlight_properties=None,
|
||||
name=NAME,
|
||||
name=UNIQUE_NAME,
|
||||
entity_id=ENTITY_LIGHT,
|
||||
):
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
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,
|
||||
**CONFIG_ENTRY_DATA,
|
||||
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 config_entry.async_remove(hass)
|
||||
registry = await entity_registry.async_get_registry(hass)
|
||||
registry.async_clear_config_entry(config_entry.entry_id)
|
||||
|
||||
# nightlight
|
||||
if nightlight_properties is None:
|
||||
|
@ -336,11 +342,7 @@ async def test_device_types(hass: HomeAssistant):
|
|||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
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,
|
||||
**CONFIG_ENTRY_DATA,
|
||||
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 config_entry.async_remove(hass)
|
||||
registry.async_clear_config_entry(config_entry.entry_id)
|
||||
|
||||
bright = round(255 * int(PROPERTIES["bright"]) / 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,
|
||||
"xy_color": bg_xy_color,
|
||||
},
|
||||
name=f"{NAME} ambilight",
|
||||
name=f"{UNIQUE_NAME} ambilight",
|
||||
entity_id=f"{ENTITY_LIGHT}_ambilight",
|
||||
)
|
||||
|
||||
|
@ -518,14 +521,7 @@ async def test_effects(hass: HomeAssistant):
|
|||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
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,
|
||||
},
|
||||
data=CONFIG_ENTRY_DATA,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue