mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Fix yeelight state when controlled outside of Home Assistant (#56964)
This commit is contained in:
parent
d0827a9129
commit
1aeab65f56
@ -36,6 +36,9 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STATE_CHANGE_TIME = 0.25 # seconds
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = "yeelight"
|
DOMAIN = "yeelight"
|
||||||
DATA_YEELIGHT = DOMAIN
|
DATA_YEELIGHT = DOMAIN
|
||||||
DATA_UPDATED = "yeelight_{}_data_updated"
|
DATA_UPDATED = "yeelight_{}_data_updated"
|
||||||
@ -546,6 +549,17 @@ class YeelightScanner:
|
|||||||
self._async_stop_scan()
|
self._async_stop_scan()
|
||||||
|
|
||||||
|
|
||||||
|
def update_needs_bg_power_workaround(data):
|
||||||
|
"""Check if a push update needs the bg_power workaround.
|
||||||
|
|
||||||
|
Some devices will push the incorrect state for bg_power.
|
||||||
|
|
||||||
|
To work around this any time we are pushed an update
|
||||||
|
with bg_power, we force poll state which will be correct.
|
||||||
|
"""
|
||||||
|
return "bg_power" in data
|
||||||
|
|
||||||
|
|
||||||
class YeelightDevice:
|
class YeelightDevice:
|
||||||
"""Represents single Yeelight device."""
|
"""Represents single Yeelight device."""
|
||||||
|
|
||||||
@ -692,12 +706,18 @@ class YeelightDevice:
|
|||||||
await self._async_update_properties()
|
await self._async_update_properties()
|
||||||
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
||||||
|
|
||||||
|
async def _async_forced_update(self, _now):
|
||||||
|
"""Call a forced update."""
|
||||||
|
await self.async_update(True)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_update_callback(self, data):
|
def async_update_callback(self, data):
|
||||||
"""Update push from device."""
|
"""Update push from device."""
|
||||||
was_available = self._available
|
was_available = self._available
|
||||||
self._available = data.get(KEY_CONNECTED, True)
|
self._available = data.get(KEY_CONNECTED, True)
|
||||||
if self._did_first_update and not was_available and self._available:
|
if update_needs_bg_power_workaround(data) or (
|
||||||
|
self._did_first_update and not was_available and self._available
|
||||||
|
):
|
||||||
# On reconnect the properties may be out of sync
|
# On reconnect the properties may be out of sync
|
||||||
#
|
#
|
||||||
# We need to make sure the DEVICE_INITIALIZED dispatcher is setup
|
# We need to make sure the DEVICE_INITIALIZED dispatcher is setup
|
||||||
@ -708,7 +728,7 @@ class YeelightDevice:
|
|||||||
# to be called when async_setup_entry reaches the end of the
|
# to be called when async_setup_entry reaches the end of the
|
||||||
# function
|
# function
|
||||||
#
|
#
|
||||||
asyncio.create_task(self.async_update(True))
|
async_call_later(self._hass, STATE_CHANGE_TIME, self._async_forced_update)
|
||||||
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
async_dispatcher_send(self._hass, DATA_UPDATED.format(self._host))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Light platform support for yeelight."""
|
"""Light platform support for yeelight."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@ -210,9 +209,6 @@ SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
STATE_CHANGE_TIME = 0.25 # seconds
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _transitions_config_parser(transitions):
|
def _transitions_config_parser(transitions):
|
||||||
"""Parse transitions config into initialized objects."""
|
"""Parse transitions config into initialized objects."""
|
||||||
@ -252,13 +248,15 @@ def _async_cmd(func):
|
|||||||
# A network error happened, the bulb is likely offline now
|
# A network error happened, the bulb is likely offline now
|
||||||
self.device.async_mark_unavailable()
|
self.device.async_mark_unavailable()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
exc_message = str(ex) or type(ex)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
|
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
|
||||||
) from ex
|
) from ex
|
||||||
except BULB_EXCEPTIONS as ex:
|
except BULB_EXCEPTIONS as ex:
|
||||||
# The bulb likely responded but had an error
|
# The bulb likely responded but had an error
|
||||||
|
exc_message = str(ex) or type(ex)
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {ex}"
|
f"Error when calling {func.__name__} for bulb {self.device.name} at {self.device.host}: {exc_message}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
return _async_wrap
|
return _async_wrap
|
||||||
@ -762,11 +760,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
|
if self.config[CONF_SAVE_ON_CHANGE] and (brightness or colortemp or rgb):
|
||||||
await self.async_set_default()
|
await self.async_set_default()
|
||||||
|
|
||||||
# Some devices (mainly nightlights) will not send back the on state so we need to force a refresh
|
|
||||||
await asyncio.sleep(STATE_CHANGE_TIME)
|
|
||||||
if not self.is_on:
|
|
||||||
await self.device.async_update(True)
|
|
||||||
|
|
||||||
@_async_cmd
|
@_async_cmd
|
||||||
async def _async_turn_off(self, duration) -> None:
|
async def _async_turn_off(self, duration) -> None:
|
||||||
"""Turn off with a given transition duration wrapped with _async_cmd."""
|
"""Turn off with a given transition duration wrapped with _async_cmd."""
|
||||||
@ -782,10 +775,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity):
|
|||||||
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s
|
||||||
|
|
||||||
await self._async_turn_off(duration)
|
await self._async_turn_off(duration)
|
||||||
# Some devices will not send back the off state so we need to force a refresh
|
|
||||||
await asyncio.sleep(STATE_CHANGE_TIME)
|
|
||||||
if self.is_on:
|
|
||||||
await self.device.async_update(True)
|
|
||||||
|
|
||||||
@_async_cmd
|
@_async_cmd
|
||||||
async def async_set_mode(self, mode: str):
|
async def async_set_mode(self, mode: str):
|
||||||
@ -850,10 +839,8 @@ class YeelightNightLightSupport:
|
|||||||
return PowerMode.NORMAL
|
return PowerMode.NORMAL
|
||||||
|
|
||||||
|
|
||||||
class YeelightColorLightWithoutNightlightSwitch(
|
class YeelightWithoutNightlightSwitchMixIn:
|
||||||
YeelightColorLightSupport, YeelightGenericLight
|
"""A mix-in for yeelights without a nightlight switch."""
|
||||||
):
|
|
||||||
"""Representation of a Color Yeelight light."""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _brightness_property(self):
|
def _brightness_property(self):
|
||||||
@ -861,9 +848,25 @@ class YeelightColorLightWithoutNightlightSwitch(
|
|||||||
# want to "current_brightness" since it will check
|
# want to "current_brightness" since it will check
|
||||||
# "bg_power" and main light could still be on
|
# "bg_power" and main light could still be on
|
||||||
if self.device.is_nightlight_enabled:
|
if self.device.is_nightlight_enabled:
|
||||||
return "current_brightness"
|
return "nl_br"
|
||||||
return super()._brightness_property
|
return super()._brightness_property
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self) -> int:
|
||||||
|
"""Return the color temperature."""
|
||||||
|
if self.device.is_nightlight_enabled:
|
||||||
|
# Enabling the nightlight locks the colortemp to max
|
||||||
|
return self._max_mireds
|
||||||
|
return super().color_temp
|
||||||
|
|
||||||
|
|
||||||
|
class YeelightColorLightWithoutNightlightSwitch(
|
||||||
|
YeelightColorLightSupport,
|
||||||
|
YeelightWithoutNightlightSwitchMixIn,
|
||||||
|
YeelightGenericLight,
|
||||||
|
):
|
||||||
|
"""Representation of a Color Yeelight light."""
|
||||||
|
|
||||||
|
|
||||||
class YeelightColorLightWithNightlightSwitch(
|
class YeelightColorLightWithNightlightSwitch(
|
||||||
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
|
YeelightNightLightSupport, YeelightColorLightSupport, YeelightGenericLight
|
||||||
@ -880,19 +883,12 @@ class YeelightColorLightWithNightlightSwitch(
|
|||||||
|
|
||||||
|
|
||||||
class YeelightWhiteTempWithoutNightlightSwitch(
|
class YeelightWhiteTempWithoutNightlightSwitch(
|
||||||
YeelightWhiteTempLightSupport, YeelightGenericLight
|
YeelightWhiteTempLightSupport,
|
||||||
|
YeelightWithoutNightlightSwitchMixIn,
|
||||||
|
YeelightGenericLight,
|
||||||
):
|
):
|
||||||
"""White temp light, when nightlight switch is not set to light."""
|
"""White temp light, when nightlight switch is not set to light."""
|
||||||
|
|
||||||
@property
|
|
||||||
def _brightness_property(self):
|
|
||||||
# If the nightlight is not active, we do not
|
|
||||||
# want to "current_brightness" since it will check
|
|
||||||
# "bg_power" and main light could still be on
|
|
||||||
if self.device.is_nightlight_enabled:
|
|
||||||
return "current_brightness"
|
|
||||||
return super()._brightness_property
|
|
||||||
|
|
||||||
|
|
||||||
class YeelightWithNightLight(
|
class YeelightWithNightLight(
|
||||||
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight
|
YeelightNightLightSupport, YeelightWhiteTempLightSupport, YeelightGenericLight
|
||||||
@ -911,6 +907,9 @@ class YeelightWithNightLight(
|
|||||||
class YeelightNightLightMode(YeelightGenericLight):
|
class YeelightNightLightMode(YeelightGenericLight):
|
||||||
"""Representation of a Yeelight when in nightlight mode."""
|
"""Representation of a Yeelight when in nightlight mode."""
|
||||||
|
|
||||||
|
_attr_color_mode = COLOR_MODE_BRIGHTNESS
|
||||||
|
_attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
@ -941,8 +940,9 @@ class YeelightNightLightMode(YeelightGenericLight):
|
|||||||
return PowerMode.MOONLIGHT
|
return PowerMode.MOONLIGHT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _predefined_effects(self):
|
def supported_features(self):
|
||||||
return YEELIGHT_TEMP_ONLY_EFFECT_LIST
|
"""Flag no supported features."""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
|
class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode):
|
||||||
@ -962,11 +962,6 @@ class YeelightNightLightModeWithoutBrightnessControl(YeelightNightLightMode):
|
|||||||
_attr_color_mode = COLOR_MODE_ONOFF
|
_attr_color_mode = COLOR_MODE_ONOFF
|
||||||
_attr_supported_color_modes = {COLOR_MODE_ONOFF}
|
_attr_supported_color_modes = {COLOR_MODE_ONOFF}
|
||||||
|
|
||||||
@property
|
|
||||||
def supported_features(self):
|
|
||||||
"""Flag no supported features."""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
|
class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch):
|
||||||
"""Representation of a Yeelight which has ambilight support.
|
"""Representation of a Yeelight which has ambilight support.
|
||||||
|
@ -13,6 +13,7 @@ from homeassistant.components.yeelight import (
|
|||||||
DATA_DEVICE,
|
DATA_DEVICE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
||||||
|
STATE_CHANGE_TIME,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -458,6 +459,8 @@ async def test_connection_dropped_resyncs_properties(hass: HomeAssistant):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
|
assert len(mocked_bulb.async_get_properties.mock_calls) == 1
|
||||||
mocked_bulb._async_callback({KEY_CONNECTED: True})
|
mocked_bulb._async_callback({KEY_CONNECTED: True})
|
||||||
await hass.async_block_till_done()
|
async_fire_time_changed(
|
||||||
|
hass, dt_util.utcnow() + timedelta(seconds=STATE_CHANGE_TIME)
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
||||||
|
@ -545,7 +545,8 @@ async def test_update_errors(hass: HomeAssistant, caplog):
|
|||||||
|
|
||||||
# Timeout usually means the bulb is overloaded with commands
|
# Timeout usually means the bulb is overloaded with commands
|
||||||
# but will still respond eventually.
|
# but will still respond eventually.
|
||||||
mocked_bulb.async_get_properties = AsyncMock(side_effect=asyncio.TimeoutError)
|
mocked_bulb.async_turn_off = AsyncMock(side_effect=asyncio.TimeoutError)
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
@ -557,7 +558,8 @@ async def test_update_errors(hass: HomeAssistant, caplog):
|
|||||||
# socket.error usually means the bulb dropped the connection
|
# socket.error usually means the bulb dropped the connection
|
||||||
# or lost wifi, then came back online and forced the existing
|
# or lost wifi, then came back online and forced the existing
|
||||||
# connection closed with a TCP RST
|
# connection closed with a TCP RST
|
||||||
mocked_bulb.async_get_properties = AsyncMock(side_effect=socket.error)
|
mocked_bulb.async_turn_off = AsyncMock(side_effect=socket.error)
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"light",
|
"light",
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
@ -572,6 +574,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
|
|||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
properties = {**PROPERTIES}
|
properties = {**PROPERTIES}
|
||||||
properties.pop("active_mode")
|
properties.pop("active_mode")
|
||||||
|
properties.pop("nl_br")
|
||||||
properties["color_mode"] = "3" # HSV
|
properties["color_mode"] = "3" # HSV
|
||||||
mocked_bulb.last_properties = properties
|
mocked_bulb.last_properties = properties
|
||||||
mocked_bulb.bulb_type = BulbType.Color
|
mocked_bulb.bulb_type = BulbType.Color
|
||||||
@ -579,7 +582,9 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
|
|||||||
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
|
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
|
||||||
)
|
)
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
with _patch_discovery(), _patch_discovery_interval(), patch(
|
||||||
|
f"{MODULE}.AsyncBulb", return_value=mocked_bulb
|
||||||
|
):
|
||||||
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()
|
||||||
# We use asyncio.create_task now to avoid
|
# We use asyncio.create_task now to avoid
|
||||||
@ -623,7 +628,7 @@ async def test_state_already_set_avoid_ratelimit(hass: HomeAssistant):
|
|||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
{
|
{
|
||||||
ATTR_ENTITY_ID: ENTITY_LIGHT,
|
ATTR_ENTITY_ID: ENTITY_LIGHT,
|
||||||
ATTR_BRIGHTNESS_PCT: PROPERTIES["current_brightness"],
|
ATTR_BRIGHTNESS_PCT: PROPERTIES["bright"],
|
||||||
},
|
},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -696,9 +701,10 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
bulb_type,
|
bulb_type,
|
||||||
model,
|
model,
|
||||||
target_properties,
|
target_properties,
|
||||||
nightlight_properties=None,
|
nightlight_entity_properties=None,
|
||||||
name=UNIQUE_FRIENDLY_NAME,
|
name=UNIQUE_FRIENDLY_NAME,
|
||||||
entity_id=ENTITY_LIGHT,
|
entity_id=ENTITY_LIGHT,
|
||||||
|
nightlight_mode_properties=None,
|
||||||
):
|
):
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
|
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
|
||||||
@ -708,6 +714,9 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
mocked_bulb.bulb_type = bulb_type
|
mocked_bulb.bulb_type = bulb_type
|
||||||
model_specs = _MODEL_SPECS.get(model)
|
model_specs = _MODEL_SPECS.get(model)
|
||||||
type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs)
|
type(mocked_bulb).get_model_specs = MagicMock(return_value=model_specs)
|
||||||
|
original_nightlight_brightness = mocked_bulb.last_properties["nl_br"]
|
||||||
|
|
||||||
|
mocked_bulb.last_properties["nl_br"] = "0"
|
||||||
await _async_setup(config_entry)
|
await _async_setup(config_entry)
|
||||||
|
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
@ -715,18 +724,36 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
target_properties["friendly_name"] = name
|
target_properties["friendly_name"] = name
|
||||||
target_properties["flowing"] = False
|
target_properties["flowing"] = False
|
||||||
target_properties["night_light"] = True
|
target_properties["night_light"] = False
|
||||||
target_properties["music_mode"] = False
|
target_properties["music_mode"] = False
|
||||||
assert dict(state.attributes) == target_properties
|
assert dict(state.attributes) == target_properties
|
||||||
|
|
||||||
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 = er.async_get(hass)
|
registry = er.async_get(hass)
|
||||||
registry.async_clear_config_entry(config_entry.entry_id)
|
registry.async_clear_config_entry(config_entry.entry_id)
|
||||||
|
mocked_bulb.last_properties["nl_br"] = original_nightlight_brightness
|
||||||
|
|
||||||
# nightlight
|
# nightlight as a setting of the main entity
|
||||||
if nightlight_properties is None:
|
if nightlight_mode_properties is not None:
|
||||||
return
|
mocked_bulb.last_properties["active_mode"] = True
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await _async_setup(config_entry)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "on"
|
||||||
|
nightlight_mode_properties["friendly_name"] = name
|
||||||
|
nightlight_mode_properties["flowing"] = False
|
||||||
|
nightlight_mode_properties["night_light"] = True
|
||||||
|
nightlight_mode_properties["music_mode"] = False
|
||||||
|
assert dict(state.attributes) == nightlight_mode_properties
|
||||||
|
|
||||||
|
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)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mocked_bulb.last_properties.pop("active_mode")
|
||||||
|
|
||||||
|
# nightlight as a separate entity
|
||||||
|
if nightlight_entity_properties is not None:
|
||||||
config_entry = MockConfigEntry(
|
config_entry = MockConfigEntry(
|
||||||
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
|
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: True}
|
||||||
)
|
)
|
||||||
@ -736,12 +763,12 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
assert hass.states.get(entity_id).state == "off"
|
assert hass.states.get(entity_id).state == "off"
|
||||||
state = hass.states.get(f"{entity_id}_nightlight")
|
state = hass.states.get(f"{entity_id}_nightlight")
|
||||||
assert state.state == "on"
|
assert state.state == "on"
|
||||||
nightlight_properties["friendly_name"] = f"{name} Nightlight"
|
nightlight_entity_properties["friendly_name"] = f"{name} Nightlight"
|
||||||
nightlight_properties["icon"] = "mdi:weather-night"
|
nightlight_entity_properties["icon"] = "mdi:weather-night"
|
||||||
nightlight_properties["flowing"] = False
|
nightlight_entity_properties["flowing"] = False
|
||||||
nightlight_properties["night_light"] = True
|
nightlight_entity_properties["night_light"] = True
|
||||||
nightlight_properties["music_mode"] = False
|
nightlight_entity_properties["music_mode"] = False
|
||||||
assert dict(state.attributes) == nightlight_properties
|
assert dict(state.attributes) == nightlight_entity_properties
|
||||||
|
|
||||||
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)
|
||||||
@ -749,7 +776,6 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
bright = round(255 * int(PROPERTIES["bright"]) / 100)
|
bright = round(255 * int(PROPERTIES["bright"]) / 100)
|
||||||
current_brightness = round(255 * int(PROPERTIES["current_brightness"]) / 100)
|
|
||||||
ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"]))
|
ct = color_temperature_kelvin_to_mired(int(PROPERTIES["ct"]))
|
||||||
hue = int(PROPERTIES["hue"])
|
hue = int(PROPERTIES["hue"])
|
||||||
sat = int(PROPERTIES["sat"])
|
sat = int(PROPERTIES["sat"])
|
||||||
@ -806,7 +832,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"color_temp": ct,
|
"color_temp": ct,
|
||||||
"color_mode": "color_temp",
|
"color_mode": "color_temp",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
@ -814,11 +840,30 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"rgb_color": (255, 205, 166),
|
"rgb_color": (255, 205, 166),
|
||||||
"xy_color": (0.421, 0.364),
|
"xy_color": (0.421, 0.364),
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"supported_features": 0,
|
"supported_features": 0,
|
||||||
"color_mode": "onoff",
|
"color_mode": "onoff",
|
||||||
"supported_color_modes": ["onoff"],
|
"supported_color_modes": ["onoff"],
|
||||||
},
|
},
|
||||||
|
nightlight_mode_properties={
|
||||||
|
"effect_list": YEELIGHT_COLOR_EFFECT_LIST,
|
||||||
|
"supported_features": SUPPORT_YEELIGHT,
|
||||||
|
"hs_color": (28.401, 100.0),
|
||||||
|
"rgb_color": (255, 120, 0),
|
||||||
|
"xy_color": (0.621, 0.367),
|
||||||
|
"min_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["max"]
|
||||||
|
),
|
||||||
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
"brightness": nl_br,
|
||||||
|
"color_mode": "color_temp",
|
||||||
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
|
"color_temp": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Color - color mode HS
|
# Color - color mode HS
|
||||||
@ -836,14 +881,14 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"hs_color": hs_color,
|
"hs_color": hs_color,
|
||||||
"rgb_color": color_hs_to_RGB(*hs_color),
|
"rgb_color": color_hs_to_RGB(*hs_color),
|
||||||
"xy_color": color_hs_to_xy(*hs_color),
|
"xy_color": color_hs_to_xy(*hs_color),
|
||||||
"color_mode": "hs",
|
"color_mode": "hs",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"supported_features": 0,
|
"supported_features": 0,
|
||||||
"color_mode": "onoff",
|
"color_mode": "onoff",
|
||||||
"supported_color_modes": ["onoff"],
|
"supported_color_modes": ["onoff"],
|
||||||
@ -865,14 +910,14 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"hs_color": color_RGB_to_hs(*rgb_color),
|
"hs_color": color_RGB_to_hs(*rgb_color),
|
||||||
"rgb_color": rgb_color,
|
"rgb_color": rgb_color,
|
||||||
"xy_color": color_RGB_to_xy(*rgb_color),
|
"xy_color": color_RGB_to_xy(*rgb_color),
|
||||||
"color_mode": "rgb",
|
"color_mode": "rgb",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"supported_features": 0,
|
"supported_features": 0,
|
||||||
"color_mode": "onoff",
|
"color_mode": "onoff",
|
||||||
"supported_color_modes": ["onoff"],
|
"supported_color_modes": ["onoff"],
|
||||||
@ -895,11 +940,11 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"color_mode": "hs",
|
"color_mode": "hs",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"supported_features": 0,
|
"supported_features": 0,
|
||||||
"color_mode": "onoff",
|
"color_mode": "onoff",
|
||||||
"supported_color_modes": ["onoff"],
|
"supported_color_modes": ["onoff"],
|
||||||
@ -922,11 +967,11 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"color_mode": "rgb",
|
"color_mode": "rgb",
|
||||||
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
"supported_color_modes": ["color_temp", "hs", "rgb"],
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"supported_features": 0,
|
"supported_features": 0,
|
||||||
"color_mode": "onoff",
|
"color_mode": "onoff",
|
||||||
"supported_color_modes": ["onoff"],
|
"supported_color_modes": ["onoff"],
|
||||||
@ -973,7 +1018,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"color_temp": ct,
|
"color_temp": ct,
|
||||||
"color_mode": "color_temp",
|
"color_mode": "color_temp",
|
||||||
"supported_color_modes": ["color_temp"],
|
"supported_color_modes": ["color_temp"],
|
||||||
@ -981,13 +1026,31 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"rgb_color": (255, 205, 166),
|
"rgb_color": (255, 205, 166),
|
||||||
"xy_color": (0.421, 0.364),
|
"xy_color": (0.421, 0.364),
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
|
"supported_features": 0,
|
||||||
"supported_features": SUPPORT_YEELIGHT,
|
|
||||||
"brightness": nl_br,
|
"brightness": nl_br,
|
||||||
"color_mode": "brightness",
|
"color_mode": "brightness",
|
||||||
"supported_color_modes": ["brightness"],
|
"supported_color_modes": ["brightness"],
|
||||||
},
|
},
|
||||||
|
nightlight_mode_properties={
|
||||||
|
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
|
||||||
|
"supported_features": SUPPORT_YEELIGHT,
|
||||||
|
"min_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["max"]
|
||||||
|
),
|
||||||
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
"brightness": nl_br,
|
||||||
|
"color_temp": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
"color_mode": "color_temp",
|
||||||
|
"supported_color_modes": ["color_temp"],
|
||||||
|
"hs_color": (28.391, 65.659),
|
||||||
|
"rgb_color": (255, 166, 87),
|
||||||
|
"xy_color": (0.526, 0.387),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# WhiteTempMood
|
# WhiteTempMood
|
||||||
@ -1009,7 +1072,7 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"max_mireds": color_temperature_kelvin_to_mired(
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
model_specs["color_temp"]["min"]
|
model_specs["color_temp"]["min"]
|
||||||
),
|
),
|
||||||
"brightness": current_brightness,
|
"brightness": bright,
|
||||||
"color_temp": ct,
|
"color_temp": ct,
|
||||||
"color_mode": "color_temp",
|
"color_mode": "color_temp",
|
||||||
"supported_color_modes": ["color_temp"],
|
"supported_color_modes": ["color_temp"],
|
||||||
@ -1017,13 +1080,34 @@ async def test_device_types(hass: HomeAssistant, caplog):
|
|||||||
"rgb_color": (255, 205, 166),
|
"rgb_color": (255, 205, 166),
|
||||||
"xy_color": (0.421, 0.364),
|
"xy_color": (0.421, 0.364),
|
||||||
},
|
},
|
||||||
{
|
nightlight_entity_properties={
|
||||||
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
|
"supported_features": 0,
|
||||||
"supported_features": SUPPORT_YEELIGHT,
|
|
||||||
"brightness": nl_br,
|
"brightness": nl_br,
|
||||||
"color_mode": "brightness",
|
"color_mode": "brightness",
|
||||||
"supported_color_modes": ["brightness"],
|
"supported_color_modes": ["brightness"],
|
||||||
},
|
},
|
||||||
|
nightlight_mode_properties={
|
||||||
|
"friendly_name": NAME,
|
||||||
|
"effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST,
|
||||||
|
"flowing": False,
|
||||||
|
"night_light": True,
|
||||||
|
"supported_features": SUPPORT_YEELIGHT,
|
||||||
|
"min_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["max"]
|
||||||
|
),
|
||||||
|
"max_mireds": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
"brightness": nl_br,
|
||||||
|
"color_temp": color_temperature_kelvin_to_mired(
|
||||||
|
model_specs["color_temp"]["min"]
|
||||||
|
),
|
||||||
|
"color_mode": "color_temp",
|
||||||
|
"supported_color_modes": ["color_temp"],
|
||||||
|
"hs_color": (28.391, 65.659),
|
||||||
|
"rgb_color": (255, 166, 87),
|
||||||
|
"xy_color": (0.526, 0.387),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
# Background light - color mode CT
|
# Background light - color mode CT
|
||||||
mocked_bulb.last_properties["bg_lmode"] = "2" # CT
|
mocked_bulb.last_properties["bg_lmode"] = "2" # CT
|
||||||
@ -1261,62 +1345,6 @@ async def test_effects(hass: HomeAssistant):
|
|||||||
await _async_test_effect("not_existed", called=False)
|
await _async_test_effect("not_existed", called=False)
|
||||||
|
|
||||||
|
|
||||||
async def test_state_fails_to_update_triggers_update(hass: HomeAssistant):
|
|
||||||
"""Ensure we call async_get_properties if the turn on/off fails to update the state."""
|
|
||||||
mocked_bulb = _mocked_bulb()
|
|
||||||
properties = {**PROPERTIES}
|
|
||||||
properties.pop("active_mode")
|
|
||||||
properties["color_mode"] = "3" # HSV
|
|
||||||
mocked_bulb.last_properties = properties
|
|
||||||
mocked_bulb.bulb_type = BulbType.Color
|
|
||||||
config_entry = MockConfigEntry(
|
|
||||||
domain=DOMAIN, data={**CONFIG_ENTRY_DATA, CONF_NIGHTLIGHT_SWITCH: False}
|
|
||||||
)
|
|
||||||
config_entry.add_to_hass(hass)
|
|
||||||
with patch(f"{MODULE}.AsyncBulb", return_value=mocked_bulb):
|
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
# We use asyncio.create_task now to avoid
|
|
||||||
# blocking starting so we need to block again
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
mocked_bulb.last_properties["power"] = "off"
|
|
||||||
await hass.services.async_call(
|
|
||||||
"light",
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
{
|
|
||||||
ATTR_ENTITY_ID: ENTITY_LIGHT,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
|
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 2
|
|
||||||
|
|
||||||
mocked_bulb.last_properties["power"] = "on"
|
|
||||||
await hass.services.async_call(
|
|
||||||
"light",
|
|
||||||
SERVICE_TURN_OFF,
|
|
||||||
{
|
|
||||||
ATTR_ENTITY_ID: ENTITY_LIGHT,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert len(mocked_bulb.async_turn_off.mock_calls) == 1
|
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
|
|
||||||
|
|
||||||
# But if the state is correct no calls
|
|
||||||
await hass.services.async_call(
|
|
||||||
"light",
|
|
||||||
SERVICE_TURN_ON,
|
|
||||||
{
|
|
||||||
ATTR_ENTITY_ID: ENTITY_LIGHT,
|
|
||||||
},
|
|
||||||
blocking=True,
|
|
||||||
)
|
|
||||||
assert len(mocked_bulb.async_turn_on.mock_calls) == 1
|
|
||||||
assert len(mocked_bulb.async_get_properties.mock_calls) == 3
|
|
||||||
|
|
||||||
|
|
||||||
async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
|
async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
|
||||||
"""Test that main light on ambilights with the nightlight disabled shows the correct brightness."""
|
"""Test that main light on ambilights with the nightlight disabled shows the correct brightness."""
|
||||||
mocked_bulb = _mocked_bulb()
|
mocked_bulb = _mocked_bulb()
|
||||||
@ -1325,7 +1353,6 @@ async def test_ambilight_with_nightlight_disabled(hass: HomeAssistant):
|
|||||||
capabilities["model"] = "ceiling10"
|
capabilities["model"] = "ceiling10"
|
||||||
properties["color_mode"] = "3" # HSV
|
properties["color_mode"] = "3" # HSV
|
||||||
properties["bg_power"] = "off"
|
properties["bg_power"] = "off"
|
||||||
properties["current_brightness"] = 0
|
|
||||||
properties["bg_lmode"] = "2" # CT
|
properties["bg_lmode"] = "2" # CT
|
||||||
mocked_bulb.last_properties = properties
|
mocked_bulb.last_properties = properties
|
||||||
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
mocked_bulb.bulb_type = BulbType.WhiteTempMood
|
||||||
|
Loading…
x
Reference in New Issue
Block a user