Convert flux_led to use asyncio (#57440)

This commit is contained in:
J. Nick Koston 2021-10-10 15:12:54 -10:00 committed by GitHub
parent 5c91d8d379
commit e148939b78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 399 additions and 235 deletions

View File

@ -5,13 +5,17 @@ from datetime import timedelta
import logging import logging
from typing import Any, Final from typing import Any, Final
from flux_led import BulbScanner, WifiLedBulb from flux_led.aio import AIOWifiLedBulb
from flux_led.aioscanner import AIOBulbScanner
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -19,8 +23,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import ( from .const import (
DISCOVER_SCAN_TIMEOUT, DISCOVER_SCAN_TIMEOUT,
DOMAIN, DOMAIN,
FLUX_HOST,
FLUX_LED_DISCOVERY, FLUX_LED_DISCOVERY,
FLUX_LED_EXCEPTIONS, FLUX_LED_EXCEPTIONS,
FLUX_MAC,
FLUX_MODEL,
SIGNAL_STATE_UPDATED,
STARTUP_SCAN_TIMEOUT, STARTUP_SCAN_TIMEOUT,
) )
@ -31,22 +39,52 @@ DISCOVERY_INTERVAL: Final = timedelta(minutes=15)
REQUEST_REFRESH_DELAY: Final = 1.5 REQUEST_REFRESH_DELAY: Final = 1.5
async def async_wifi_bulb_for_host(hass: HomeAssistant, host: str) -> WifiLedBulb: @callback
"""Create a WifiLedBulb from a host.""" def async_wifi_bulb_for_host(host: str) -> AIOWifiLedBulb:
return await hass.async_add_executor_job(WifiLedBulb, host) """Create a AIOWifiLedBulb from a host."""
return AIOWifiLedBulb(host)
@callback
def async_update_entry_from_discovery(
hass: HomeAssistant, entry: config_entries.ConfigEntry, device: dict[str, Any]
) -> None:
"""Update a config entry from a flux_led discovery."""
name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}"
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_NAME: name},
title=name,
unique_id=dr.format_mac(device[FLUX_MAC]),
)
async def async_discover_devices( async def async_discover_devices(
hass: HomeAssistant, timeout: int hass: HomeAssistant, timeout: int, address: str | None = None
) -> list[dict[str, str]]: ) -> list[dict[str, str]]:
"""Discover flux led devices.""" """Discover flux led devices."""
scanner = AIOBulbScanner()
def _scan_with_timeout() -> list[dict[str, str]]: try:
scanner = BulbScanner() discovered: list[dict[str, str]] = await scanner.async_scan(
discovered: list[dict[str, str]] = scanner.scan(timeout=timeout) timeout=timeout, address=address
)
except OSError as ex:
_LOGGER.debug("Scanning failed with error: %s", ex)
return []
else:
return discovered return discovered
return await hass.async_add_executor_job(_scan_with_timeout)
async def async_discover_device(
hass: HomeAssistant, host: str
) -> dict[str, str] | None:
"""Direct discovery at a single ip instead of broadcast."""
# If we are missing the unique_id we should be able to fetch it
# from the device by doing a directed discovery at the host only
for device in await async_discover_devices(hass, DISCOVER_SCAN_TIMEOUT, host):
if device[FLUX_HOST] == host:
return device
return None
@callback @callback
@ -90,9 +128,26 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flux LED/MagicLight from a config entry.""" """Set up Flux LED/MagicLight from a config entry."""
host = entry.data[CONF_HOST]
if not entry.unique_id:
if discovery := await async_discover_device(hass, host):
async_update_entry_from_discovery(hass, entry, discovery)
coordinator = FluxLedUpdateCoordinator(hass, entry.data[CONF_HOST]) device: AIOWifiLedBulb = async_wifi_bulb_for_host(host)
await coordinator.async_config_entry_first_refresh() signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)
@callback
def _async_state_changed(*_: Any) -> None:
_LOGGER.debug("%s: Device state updated: %s", device.ipaddr, device.raw_state)
async_dispatcher_send(hass, signal)
try:
await device.async_setup(_async_state_changed)
except FLUX_LED_EXCEPTIONS as ex:
raise ConfigEntryNotReady(
str(ex) or f"Timed out trying to connect to {device.ipaddr}"
) from ex
coordinator = FluxLedUpdateCoordinator(hass, device)
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS) hass.config_entries.async_setup_platforms(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_listener)) entry.async_on_unload(entry.add_update_listener(async_update_listener))
@ -103,7 +158,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id) coordinator = hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.device.async_stop()
return unload_ok return unload_ok
@ -113,17 +169,15 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator):
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
host: str, device: AIOWifiLedBulb,
) -> None: ) -> None:
"""Initialize DataUpdateCoordinator to gather data for specific device.""" """Initialize DataUpdateCoordinator to gather data for specific device."""
self.host = host self.device = device
self.device: WifiLedBulb | None = None
update_interval = timedelta(seconds=5)
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name=host, name=self.device.ipaddr,
update_interval=update_interval, update_interval=timedelta(seconds=5),
# We don't want an immediate refresh since the device # We don't want an immediate refresh since the device
# takes a moment to reflect the state change # takes a moment to reflect the state change
request_refresh_debouncer=Debouncer( request_refresh_debouncer=Debouncer(
@ -134,12 +188,6 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Fetch all device and sensor data from api.""" """Fetch all device and sensor data from api."""
try: try:
if not self.device: await self.device.async_update()
self.device = await async_wifi_bulb_for_host(self.hass, self.host)
else:
await self.hass.async_add_executor_job(self.device.update_state)
except FLUX_LED_EXCEPTIONS as ex: except FLUX_LED_EXCEPTIONS as ex:
raise UpdateFailed(ex) from ex raise UpdateFailed(ex) from ex
if not self.device.raw_state:
raise UpdateFailed("The device failed to update")

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import logging import logging
from typing import Any, Final from typing import Any, Final
from flux_led import WifiLedBulb
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -15,7 +14,12 @@ from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.helpers.typing import DiscoveryInfoType
from . import async_discover_devices, async_wifi_bulb_for_host from . import (
async_discover_device,
async_discover_devices,
async_update_entry_from_discovery,
async_wifi_bulb_for_host,
)
from .const import ( from .const import (
CONF_CUSTOM_EFFECT_COLORS, CONF_CUSTOM_EFFECT_COLORS,
CONF_CUSTOM_EFFECT_SPEED_PCT, CONF_CUSTOM_EFFECT_SPEED_PCT,
@ -34,7 +38,6 @@ from .const import (
CONF_DEVICE: Final = "device" CONF_DEVICE: Final = "device"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -104,13 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(updates={CONF_HOST: host}) self._abort_if_unique_id_configured(updates={CONF_HOST: host})
for entry in self._async_current_entries(include_ignore=False): for entry in self._async_current_entries(include_ignore=False):
if entry.data[CONF_HOST] == host and not entry.unique_id: if entry.data[CONF_HOST] == host and not entry.unique_id:
name = f"{device[FLUX_MODEL]} {device[FLUX_MAC]}" async_update_entry_from_discovery(self.hass, entry, device)
self.hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_NAME: name},
title=name,
unique_id=mac,
)
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
self.context[CONF_HOST] = host self.context[CONF_HOST] = host
for progress in self._async_in_progress(): for progress in self._async_in_progress():
@ -157,13 +154,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not (host := user_input[CONF_HOST]): if not (host := user_input[CONF_HOST]):
return await self.async_step_pick_device() return await self.async_step_pick_device()
try: try:
await self._async_try_connect(host) device = await self._async_try_connect(host)
except FLUX_LED_EXCEPTIONS: except FLUX_LED_EXCEPTIONS:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else: else:
return self._async_create_entry_from_device( if device[FLUX_MAC]:
{FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host} await self.async_set_unique_id(
dr.format_mac(device[FLUX_MAC]), raise_on_progress=False
) )
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
return self._async_create_entry_from_device(device)
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
@ -204,10 +204,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
) )
async def _async_try_connect(self, host: str) -> WifiLedBulb: async def _async_try_connect(self, host: str) -> dict[str, Any]:
"""Try to connect.""" """Try to connect."""
self._async_abort_entries_match({CONF_HOST: host}) self._async_abort_entries_match({CONF_HOST: host})
return await async_wifi_bulb_for_host(self.hass, host) if device := await async_discover_device(self.hass, host):
return device
bulb = async_wifi_bulb_for_host(host)
try:
await bulb.async_setup(lambda: None)
finally:
await bulb.async_stop()
return {FLUX_MAC: None, FLUX_MODEL: None, FLUX_HOST: host}
class OptionsFlow(config_entries.OptionsFlow): class OptionsFlow(config_entries.OptionsFlow):

View File

@ -1,5 +1,6 @@
"""Constants of the FluxLed/MagicHome Integration.""" """Constants of the FluxLed/MagicHome Integration."""
import asyncio
import socket import socket
from typing import Final from typing import Final
@ -7,6 +8,7 @@ DOMAIN: Final = "flux_led"
API: Final = "flux_api" API: Final = "flux_api"
SIGNAL_STATE_UPDATED = "flux_led_{}_state_updated"
CONF_AUTOMATIC_ADD: Final = "automatic_add" CONF_AUTOMATIC_ADD: Final = "automatic_add"
DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120 DEFAULT_NETWORK_SCAN_INTERVAL: Final = 120
@ -15,7 +17,12 @@ DEFAULT_EFFECT_SPEED: Final = 50
FLUX_LED_DISCOVERY: Final = "flux_led_discovery" FLUX_LED_DISCOVERY: Final = "flux_led_discovery"
FLUX_LED_EXCEPTIONS: Final = (socket.timeout, BrokenPipeError) FLUX_LED_EXCEPTIONS: Final = (
asyncio.TimeoutError,
socket.error,
RuntimeError,
BrokenPipeError,
)
STARTUP_SCAN_TIMEOUT: Final = 5 STARTUP_SCAN_TIMEOUT: Final = 5
DISCOVER_SCAN_TIMEOUT: Final = 10 DISCOVER_SCAN_TIMEOUT: Final = 10

View File

@ -2,12 +2,11 @@
from __future__ import annotations from __future__ import annotations
import ast import ast
from functools import partial
import logging import logging
import random import random
from typing import Any, Final, cast from typing import Any, Final, cast
from flux_led import WifiLedBulb from flux_led.aiodevice import AIOWifiLedBulb
from flux_led.const import ( from flux_led.const import (
COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT,
COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM, COLOR_MODE_DIM as FLUX_COLOR_MODE_DIM,
@ -15,7 +14,6 @@ from flux_led.const import (
COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW, COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW,
COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW, COLOR_MODE_RGBWW as FLUX_COLOR_MODE_RGBWW,
) )
from flux_led.device import MAX_TEMP, MIN_TEMP
from flux_led.utils import ( from flux_led.utils import (
color_temp_to_white_levels, color_temp_to_white_levels,
rgbcw_brightness, rgbcw_brightness,
@ -61,13 +59,16 @@ from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PROTOCOL, CONF_PROTOCOL,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_platform from homeassistant.helpers import device_registry as dr, entity_platform
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.color import ( from homeassistant.util.color import (
color_hs_to_RGB,
color_RGB_to_hs,
color_temperature_kelvin_to_mired, color_temperature_kelvin_to_mired,
color_temperature_mired_to_kelvin, color_temperature_mired_to_kelvin,
) )
@ -91,6 +92,7 @@ from .const import (
MODE_RGB, MODE_RGB,
MODE_RGBW, MODE_RGBW,
MODE_WHITE, MODE_WHITE,
SIGNAL_STATE_UPDATED,
TRANSITION_GRADUAL, TRANSITION_GRADUAL,
TRANSITION_JUMP, TRANSITION_JUMP,
TRANSITION_STROBE, TRANSITION_STROBE,
@ -254,7 +256,7 @@ async def async_setup_entry(
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_CUSTOM_EFFECT, SERVICE_CUSTOM_EFFECT,
CUSTOM_EFFECT_DICT, CUSTOM_EFFECT_DICT,
"set_custom_effect", "async_set_custom_effect",
) )
options = entry.options options = entry.options
@ -298,17 +300,18 @@ class FluxLight(CoordinatorEntity, LightEntity):
) -> None: ) -> None:
"""Initialize the light.""" """Initialize the light."""
super().__init__(coordinator) super().__init__(coordinator)
self._bulb: WifiLedBulb = coordinator.device self._device: AIOWifiLedBulb = coordinator.device
self._responding = True
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._attr_supported_features = SUPPORT_FLUX_LED self._attr_supported_features = SUPPORT_FLUX_LED
self._attr_min_mireds = ( self._attr_min_mireds = (
color_temperature_kelvin_to_mired(MAX_TEMP) + 1 color_temperature_kelvin_to_mired(self._device.max_temp) + 1
) # for rounding ) # for rounding
self._attr_max_mireds = color_temperature_kelvin_to_mired(MIN_TEMP) self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp)
self._attr_supported_color_modes = { self._attr_supported_color_modes = {
FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF) FLUX_COLOR_MODE_TO_HASS.get(mode, COLOR_MODE_ONOFF)
for mode in self._bulb.color_modes for mode in self._device.color_modes
} }
self._attr_effect_list = FLUX_EFFECT_LIST self._attr_effect_list = FLUX_EFFECT_LIST
if custom_effect_colors: if custom_effect_colors:
@ -317,82 +320,83 @@ class FluxLight(CoordinatorEntity, LightEntity):
self._custom_effect_speed_pct = custom_effect_speed_pct self._custom_effect_speed_pct = custom_effect_speed_pct
self._custom_effect_transition = custom_effect_transition self._custom_effect_transition = custom_effect_transition
if self.unique_id: if self.unique_id:
old_protocol = self._bulb.protocol == "LEDENET_ORIGINAL"
raw_state = self._bulb.raw_state
self._attr_device_info = { self._attr_device_info = {
"connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)}, "connections": {(dr.CONNECTION_NETWORK_MAC, self.unique_id)},
ATTR_MODEL: f"0x{self._bulb.model_num:02X}", ATTR_MODEL: f"0x{self._device.model_num:02X}",
ATTR_NAME: self.name, ATTR_NAME: self.name,
ATTR_SW_VERSION: "1" if old_protocol else str(raw_state.version_number), ATTR_SW_VERSION: str(self._device.version_num),
ATTR_MANUFACTURER: "FluxLED/Magic Home", ATTR_MANUFACTURER: "FluxLED/Magic Home",
} }
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if device is on.""" """Return true if device is on."""
return cast(bool, self._bulb.is_on) return cast(bool, self._device.is_on)
@property @property
def brightness(self) -> int: def brightness(self) -> int:
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return cast(int, self._bulb.brightness) return cast(int, self._device.brightness)
@property @property
def color_temp(self) -> int: def color_temp(self) -> int:
"""Return the kelvin value of this light in mired.""" """Return the kelvin value of this light in mired."""
return color_temperature_kelvin_to_mired(self._bulb.getWhiteTemperature()[0]) return color_temperature_kelvin_to_mired(self._device.color_temp)
@property @property
def rgb_color(self) -> tuple[int, int, int]: def rgb_color(self) -> tuple[int, int, int]:
"""Return the rgb color value.""" """Return the rgb color value."""
rgb: tuple[int, int, int] = self._bulb.rgb # Note that we call color_RGB_to_hs and not color_RGB_to_hsv
# to get the unscaled value since this is what the frontend wants
# https://github.com/home-assistant/frontend/blob/e797c017614797bb11671496d6bd65863de22063/src/dialogs/more-info/controls/more-info-light.ts#L263
rgb: tuple[int, int, int] = color_hs_to_RGB(*color_RGB_to_hs(*self._device.rgb))
return rgb return rgb
@property @property
def rgbw_color(self) -> tuple[int, int, int, int]: def rgbw_color(self) -> tuple[int, int, int, int]:
"""Return the rgbw color value.""" """Return the rgbw color value."""
rgbw: tuple[int, int, int, int] = self._bulb.rgbw rgbw: tuple[int, int, int, int] = self._device.rgbw
return rgbw return rgbw
@property @property
def rgbww_color(self) -> tuple[int, int, int, int, int]: def rgbww_color(self) -> tuple[int, int, int, int, int]:
"""Return the rgbww aka rgbcw color value.""" """Return the rgbww aka rgbcw color value."""
rgbcw: tuple[int, int, int, int, int] = self._bulb.rgbcw rgbcw: tuple[int, int, int, int, int] = self._device.rgbcw
return rgbcw return rgbcw
@property @property
def rgbwc_color(self) -> tuple[int, int, int, int, int]: def rgbwc_color(self) -> tuple[int, int, int, int, int]:
"""Return the rgbwc color value.""" """Return the rgbwc color value."""
rgbwc: tuple[int, int, int, int, int] = self._bulb.rgbww rgbwc: tuple[int, int, int, int, int] = self._device.rgbww
return rgbwc return rgbwc
@property @property
def color_mode(self) -> str: def color_mode(self) -> str:
"""Return the color mode of the light.""" """Return the color mode of the light."""
return FLUX_COLOR_MODE_TO_HASS.get(self._bulb.color_mode, COLOR_MODE_ONOFF) return FLUX_COLOR_MODE_TO_HASS.get(self._device.color_mode, COLOR_MODE_ONOFF)
@property @property
def effect(self) -> str | None: def effect(self) -> str | None:
"""Return the current effect.""" """Return the current effect."""
if (current_mode := self._bulb.raw_state.preset_pattern) == EFFECT_CUSTOM_CODE: if (current_mode := self._device.preset_pattern_num) == EFFECT_CUSTOM_CODE:
return EFFECT_CUSTOM return EFFECT_CUSTOM
return EFFECT_ID_NAME.get(current_mode) return EFFECT_ID_NAME.get(current_mode)
@property @property
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return the attributes.""" """Return the attributes."""
return {"ip_address": self._bulb.ipaddr} return {"ip_address": self._device.ipaddr}
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified or all lights on.""" """Turn the specified or all lights on."""
await self.hass.async_add_executor_job(partial(self._turn_on, **kwargs)) await self._async_turn_on(**kwargs)
self.async_write_ha_state() self.async_write_ha_state()
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
def _turn_on(self, **kwargs: Any) -> None: async def _async_turn_on(self, **kwargs: Any) -> None:
"""Turn the specified or all lights on.""" """Turn the specified or all lights on."""
if not self.is_on: if not self.is_on:
self._bulb.turnOn() await self._device.async_turn_on()
if not kwargs: if not kwargs:
return return
@ -404,21 +408,23 @@ class FluxLight(CoordinatorEntity, LightEntity):
color_temp_mired = kwargs[ATTR_COLOR_TEMP] color_temp_mired = kwargs[ATTR_COLOR_TEMP]
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired) color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
if self.color_mode != COLOR_MODE_RGBWW: if self.color_mode != COLOR_MODE_RGBWW:
self._bulb.setWhiteTemperature(color_temp_kelvin, brightness) await self._device.async_set_white_temp(color_temp_kelvin, brightness)
return return
# When switching to color temp from RGBWW mode, # When switching to color temp from RGBWW mode,
# we do not want the overall brightness, we only # we do not want the overall brightness, we only
# want the brightness of the white channels # want the brightness of the white channels
brightness = kwargs.get( brightness = kwargs.get(
ATTR_BRIGHTNESS, self._bulb.getWhiteTemperature()[1] ATTR_BRIGHTNESS, self._device.getWhiteTemperature()[1]
) )
cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness) cold, warm = color_temp_to_white_levels(color_temp_kelvin, brightness)
self._bulb.set_levels(r=0, b=0, g=0, w=warm, w2=cold) await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold)
return return
# Handle switch to HS Color Mode # Handle switch to RGB Color Mode
if ATTR_RGB_COLOR in kwargs: if ATTR_RGB_COLOR in kwargs:
self._bulb.set_levels(*kwargs[ATTR_RGB_COLOR], brightness=brightness) await self._device.async_set_levels(
*kwargs[ATTR_RGB_COLOR], brightness=brightness
)
return return
# Handle switch to RGBW Color Mode # Handle switch to RGBW Color Mode
if ATTR_RGBW_COLOR in kwargs: if ATTR_RGBW_COLOR in kwargs:
@ -426,7 +432,7 @@ class FluxLight(CoordinatorEntity, LightEntity):
rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness) rgbw = rgbw_brightness(kwargs[ATTR_RGBW_COLOR], brightness)
else: else:
rgbw = kwargs[ATTR_RGBW_COLOR] rgbw = kwargs[ATTR_RGBW_COLOR]
self._bulb.set_levels(*rgbw) await self._device.async_set_levels(*rgbw)
return return
# Handle switch to RGBWW Color Mode # Handle switch to RGBWW Color Mode
if ATTR_RGBWW_COLOR in kwargs: if ATTR_RGBWW_COLOR in kwargs:
@ -434,17 +440,17 @@ class FluxLight(CoordinatorEntity, LightEntity):
rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness) rgbcw = rgbcw_brightness(kwargs[ATTR_RGBWW_COLOR], brightness)
else: else:
rgbcw = kwargs[ATTR_RGBWW_COLOR] rgbcw = kwargs[ATTR_RGBWW_COLOR]
self._bulb.set_levels(*rgbcw_to_rgbwc(rgbcw)) await self._device.async_set_levels(*rgbcw_to_rgbwc(rgbcw))
return return
# Handle switch to White Color Mode # Handle switch to White Color Mode
if ATTR_WHITE in kwargs: if ATTR_WHITE in kwargs:
self._bulb.set_levels(w=kwargs[ATTR_WHITE]) await self._device.async_set_levels(w=kwargs[ATTR_WHITE])
return return
if ATTR_EFFECT in kwargs: if ATTR_EFFECT in kwargs:
effect = kwargs[ATTR_EFFECT] effect = kwargs[ATTR_EFFECT]
# Random color effect # Random color effect
if effect == EFFECT_RANDOM: if effect == EFFECT_RANDOM:
self._bulb.set_levels( await self._device.async_set_levels(
random.randint(0, 255), random.randint(0, 255),
random.randint(0, 255), random.randint(0, 255),
random.randint(0, 255), random.randint(0, 255),
@ -453,7 +459,7 @@ class FluxLight(CoordinatorEntity, LightEntity):
# Custom effect # Custom effect
if effect == EFFECT_CUSTOM: if effect == EFFECT_CUSTOM:
if self._custom_effect_colors: if self._custom_effect_colors:
self._bulb.setCustomPattern( await self._device.async_set_custom_pattern(
self._custom_effect_colors, self._custom_effect_colors,
self._custom_effect_speed_pct, self._custom_effect_speed_pct,
self._custom_effect_transition, self._custom_effect_transition,
@ -461,39 +467,41 @@ class FluxLight(CoordinatorEntity, LightEntity):
return return
# Effect selection # Effect selection
if effect in EFFECT_MAP: if effect in EFFECT_MAP:
self._bulb.setPresetPattern(EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED) await self._device.async_set_preset_pattern(
EFFECT_MAP[effect], DEFAULT_EFFECT_SPEED
)
return return
raise ValueError(f"Unknown effect {effect}") raise ValueError(f"Unknown effect {effect}")
# Handle brightness adjustment in CCT Color Mode # Handle brightness adjustment in CCT Color Mode
if self.color_mode == COLOR_MODE_COLOR_TEMP: if self.color_mode == COLOR_MODE_COLOR_TEMP:
self._bulb.setWhiteTemperature( await self._device.async_set_white_temp(self._device.color_temp, brightness)
self._bulb.getWhiteTemperature()[0], brightness
)
return return
# Handle brightness adjustment in RGB Color Mode # Handle brightness adjustment in RGB Color Mode
if self.color_mode == COLOR_MODE_RGB: if self.color_mode == COLOR_MODE_RGB:
self._bulb.set_levels(*self.rgb_color, brightness=brightness) await self._device.async_set_levels(*self.rgb_color, brightness=brightness)
return return
# Handle brightness adjustment in RGBW Color Mode # Handle brightness adjustment in RGBW Color Mode
if self.color_mode == COLOR_MODE_RGBW: if self.color_mode == COLOR_MODE_RGBW:
self._bulb.set_levels(*rgbw_brightness(self.rgbw_color, brightness)) await self._device.async_set_levels(
*rgbw_brightness(self.rgbw_color, brightness)
)
return return
# Handle brightness adjustment in RGBWW Color Mode # Handle brightness adjustment in RGBWW Color Mode
if self.color_mode == COLOR_MODE_RGBWW: if self.color_mode == COLOR_MODE_RGBWW:
rgbwc = self.rgbwc_color rgbwc = self.rgbwc_color
self._bulb.set_levels(*rgbww_brightness(rgbwc, brightness)) await self._device.async_set_levels(*rgbww_brightness(rgbwc, brightness))
return return
# Handle White Color Mode and Brightness Only Color Mode # Handle White Color Mode and Brightness Only Color Mode
if self.color_mode in (COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS): if self.color_mode in (COLOR_MODE_WHITE, COLOR_MODE_BRIGHTNESS):
self._bulb.set_levels(w=brightness) await self._device.async_set_levels(w=brightness)
return return
raise ValueError(f"Unsupported color mode {self.color_mode}") raise ValueError(f"Unsupported color mode {self.color_mode}")
def set_custom_effect( async def async_set_custom_effect(
self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str self, colors: list[tuple[int, int, int]], speed_pct: int, transition: str
) -> None: ) -> None:
"""Set a custom effect on the bulb.""" """Set a custom effect on the bulb."""
self._bulb.setCustomPattern( await self._device.async_set_custom_pattern(
colors, colors,
speed_pct, speed_pct,
transition, transition,
@ -501,6 +509,24 @@ class FluxLight(CoordinatorEntity, LightEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the specified or all lights off.""" """Turn the specified or all lights off."""
await self.hass.async_add_executor_job(self._bulb.turnOff) await self._device.async_turn_off()
self.async_write_ha_state() self.async_write_ha_state()
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
if self.coordinator.last_update_success != self._responding:
self.async_write_ha_state()
self._responding = self.coordinator.last_update_success
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_STATE_UPDATED.format(self._device.ipaddr),
self.async_write_ha_state,
)
)
await super().async_added_to_hass()

View File

@ -1 +0,0 @@
"""Tests for the flux_led integration."""

View File

@ -1,10 +1,11 @@
"""Tests for the flux_led integration.""" """Tests for the flux_led integration."""
from __future__ import annotations from __future__ import annotations
import socket import asyncio
from unittest.mock import MagicMock, patch from typing import Callable
from unittest.mock import AsyncMock, MagicMock, patch
from flux_led import WifiLedBulb from flux_led.aio import AIOWifiLedBulb
from flux_led.const import ( from flux_led.const import (
COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT, COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT,
COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB, COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB,
@ -17,6 +18,7 @@ from homeassistant.components.dhcp import (
MAC_ADDRESS as DHCP_MAC_ADDRESS, MAC_ADDRESS as DHCP_MAC_ADDRESS,
) )
from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL from homeassistant.components.flux_led.const import FLUX_HOST, FLUX_MAC, FLUX_MODEL
from homeassistant.core import HomeAssistant
MODULE = "homeassistant.components.flux_led" MODULE = "homeassistant.components.flux_led"
MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow" MODULE_CONFIG_FLOW = "homeassistant.components.flux_led.config_flow"
@ -35,8 +37,23 @@ DHCP_DISCOVERY = {
FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS} FLUX_DISCOVERY = {FLUX_HOST: IP_ADDRESS, FLUX_MODEL: MODEL, FLUX_MAC: FLUX_MAC_ADDRESS}
def _mocked_bulb() -> WifiLedBulb: def _mocked_bulb() -> AIOWifiLedBulb:
bulb = MagicMock(auto_spec=WifiLedBulb) bulb = MagicMock(auto_spec=AIOWifiLedBulb)
async def _save_setup_callback(callback: Callable) -> None:
bulb.data_receive_callback = callback
bulb.async_setup = AsyncMock(side_effect=_save_setup_callback)
bulb.async_set_custom_pattern = AsyncMock()
bulb.async_set_preset_pattern = AsyncMock()
bulb.async_set_white_temp = AsyncMock()
bulb.async_stop = AsyncMock()
bulb.async_update = AsyncMock()
bulb.async_turn_off = AsyncMock()
bulb.async_turn_on = AsyncMock()
bulb.async_set_levels = AsyncMock()
bulb.min_temp = 2700
bulb.max_temp = 6500
bulb.getRgb = MagicMock(return_value=[255, 0, 0]) bulb.getRgb = MagicMock(return_value=[255, 0, 0])
bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50]) bulb.getRgbw = MagicMock(return_value=[255, 0, 0, 50])
bulb.getRgbww = MagicMock(return_value=[255, 0, 0, 50, 0]) bulb.getRgbww = MagicMock(return_value=[255, 0, 0, 50, 0])
@ -45,9 +62,11 @@ def _mocked_bulb() -> WifiLedBulb:
bulb.rgbw = (255, 0, 0, 50) bulb.rgbw = (255, 0, 0, 50)
bulb.rgbww = (255, 0, 0, 50, 0) bulb.rgbww = (255, 0, 0, 50, 0)
bulb.rgbcw = (255, 0, 0, 0, 50) bulb.rgbcw = (255, 0, 0, 0, 50)
bulb.color_temp = 2700
bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128)) bulb.getWhiteTemperature = MagicMock(return_value=(2700, 128))
bulb.brightness = 128 bulb.brightness = 128
bulb.model_num = 0x35 bulb.model_num = 0x35
bulb.version_num = 8
bulb.rgbwcapable = True bulb.rgbwcapable = True
bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT} bulb.color_modes = {FLUX_COLOR_MODE_RGB, FLUX_COLOR_MODE_CCT}
bulb.color_mode = FLUX_COLOR_MODE_RGB bulb.color_mode = FLUX_COLOR_MODE_RGB
@ -57,19 +76,39 @@ def _mocked_bulb() -> WifiLedBulb:
return bulb return bulb
async def async_mock_bulb_turn_off(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None:
"""Mock the bulb being off."""
bulb.is_on = False
bulb.raw_state._replace(power_state=0x24)
bulb.data_receive_callback()
await hass.async_block_till_done()
async def async_mock_bulb_turn_on(hass: HomeAssistant, bulb: AIOWifiLedBulb) -> None:
"""Mock the bulb being on."""
bulb.is_on = True
bulb.raw_state._replace(power_state=0x23)
bulb.data_receive_callback()
await hass.async_block_till_done()
def _patch_discovery(device=None, no_device=False): def _patch_discovery(device=None, no_device=False):
def _discovery(*args, **kwargs): async def _discovery(*args, **kwargs):
if no_device: if no_device:
return [] raise OSError
return [FLUX_DISCOVERY] return [FLUX_DISCOVERY]
return patch("homeassistant.components.flux_led.BulbScanner.scan", new=_discovery) return patch(
"homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery
)
def _patch_wifibulb(device=None, no_device=False): def _patch_wifibulb(device=None, no_device=False):
def _wifi_led_bulb(*args, **kwargs): def _wifi_led_bulb(*args, **kwargs):
bulb = _mocked_bulb()
if no_device: if no_device:
raise socket.timeout bulb.async_setup = AsyncMock(side_effect=asyncio.TimeoutError)
return bulb
return device if device else _mocked_bulb() return device if device else _mocked_bulb()
return patch("homeassistant.components.flux_led.WifiLedBulb", new=_wifi_led_bulb) return patch("homeassistant.components.flux_led.AIOWifiLedBulb", new=_wifi_led_bulb)

View File

@ -247,7 +247,7 @@ async def test_import(hass: HomeAssistant):
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_manual(hass: HomeAssistant): async def test_manual_working_discovery(hass: HomeAssistant):
"""Test manually setup.""" """Test manually setup."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -276,8 +276,8 @@ async def test_manual(hass: HomeAssistant):
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert result4["type"] == "create_entry" assert result4["type"] == "create_entry"
assert result4["title"] == IP_ADDRESS assert result4["title"] == DEFAULT_ENTRY_TITLE
assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: IP_ADDRESS} assert result4["data"] == {CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE}
# Duplicate # Duplicate
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(

View File

@ -6,16 +6,16 @@ from unittest.mock import patch
from homeassistant.components import flux_led from homeassistant.components import flux_led
from homeassistant.components.flux_led.const import DOMAIN from homeassistant.components.flux_led.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import ( from . import (
DEFAULT_ENTRY_TITLE,
FLUX_DISCOVERY, FLUX_DISCOVERY,
IP_ADDRESS, IP_ADDRESS,
MAC_ADDRESS, MAC_ADDRESS,
_mocked_bulb,
_patch_discovery, _patch_discovery,
_patch_wifibulb, _patch_wifibulb,
) )
@ -25,7 +25,9 @@ from tests.common import MockConfigEntry, async_fire_time_changed
async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None: async def test_configuring_flux_led_causes_discovery(hass: HomeAssistant) -> None:
"""Test that specifying empty config does discovery.""" """Test that specifying empty config does discovery."""
with patch("homeassistant.components.flux_led.BulbScanner.scan") as discover: with patch(
"homeassistant.components.flux_led.AIOBulbScanner.async_scan"
) as discover:
discover.return_value = [FLUX_DISCOVERY] discover.return_value = [FLUX_DISCOVERY]
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -65,15 +67,26 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None:
assert config_entry.state == ConfigEntryState.SETUP_RETRY assert config_entry.state == ConfigEntryState.SETUP_RETRY
async def test_config_entry_retry_when_state_missing(hass: HomeAssistant) -> None: async def test_config_entry_fills_unique_id_with_directed_discovery(
"""Test that a config entry is retried when state is missing.""" hass: HomeAssistant,
) -> None:
"""Test that the unique id is added if its missing via directed (not broadcast) discovery."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=None
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
bulb = _mocked_bulb()
bulb.raw_state = None async def _discovery(self, *args, address=None, **kwargs):
with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb): # Only return discovery results when doing directed discovery
return [FLUX_DISCOVERY] if address == IP_ADDRESS else []
with patch(
"homeassistant.components.flux_led.AIOBulbScanner.async_scan", new=_discovery
), _patch_wifibulb():
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}}) await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.SETUP_RETRY assert config_entry.state == ConfigEntryState.LOADED
assert config_entry.unique_id == MAC_ADDRESS
assert config_entry.data[CONF_NAME] == DEFAULT_ENTRY_TITLE
assert config_entry.title == DEFAULT_ENTRY_TITLE

View File

@ -1,6 +1,6 @@
"""Tests for light platform.""" """Tests for light platform."""
from datetime import timedelta from datetime import timedelta
from unittest.mock import Mock from unittest.mock import AsyncMock, Mock
from flux_led.const import ( from flux_led.const import (
COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE, COLOR_MODE_ADDRESSABLE as FLUX_COLOR_MODE_ADDRESSABLE,
@ -50,6 +50,7 @@ from homeassistant.const import (
CONF_PROTOCOL, CONF_PROTOCOL,
STATE_OFF, STATE_OFF,
STATE_ON, STATE_ON,
STATE_UNAVAILABLE,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
@ -63,6 +64,8 @@ from . import (
_mocked_bulb, _mocked_bulb,
_patch_discovery, _patch_discovery,
_patch_wifibulb, _patch_wifibulb,
async_mock_bulb_turn_off,
async_mock_bulb_turn_on,
) )
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -88,6 +91,40 @@ async def test_light_unique_id(hass: HomeAssistant) -> None:
assert state.state == STATE_ON assert state.state == STATE_ON
async def test_light_goes_unavailable_and_recovers(hass: HomeAssistant) -> None:
"""Test a light goes unavailable and then recovers."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
unique_id=MAC_ADDRESS,
)
config_entry.add_to_hass(hass)
bulb = _mocked_bulb()
with _patch_discovery(device=bulb), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.az120444_aabbccddeeff"
entity_registry = er.async_get(hass)
assert entity_registry.async_get(entity_id).unique_id == MAC_ADDRESS
state = hass.states.get(entity_id)
assert state.state == STATE_ON
now = utcnow()
bulb.async_update = AsyncMock(side_effect=RuntimeError)
for i in range(10, 50, 10):
async_fire_time_changed(hass, now + timedelta(seconds=i))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_UNAVAILABLE
bulb.async_update = AsyncMock()
for i in range(60, 100, 10):
async_fire_time_changed(hass, now + timedelta(seconds=i))
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
async def test_light_no_unique_id(hass: HomeAssistant) -> None: async def test_light_no_unique_id(hass: HomeAssistant) -> None:
"""Test a light without a unique id.""" """Test a light without a unique id."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
@ -120,6 +157,7 @@ async def test_light_device_registry(
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
bulb = _mocked_bulb() bulb = _mocked_bulb()
bulb.version_num = sw_version
bulb.protocol = protocol bulb.protocol = protocol
bulb.raw_state = bulb.raw_state._replace(model_num=model, version_number=sw_version) bulb.raw_state = bulb.raw_state._replace(model_num=model, version_number=sw_version)
bulb.model_num = model bulb.model_num = model
@ -166,18 +204,16 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
bulb.is_on = False await async_mock_bulb_turn_off(hass, bulb)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -185,8 +221,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -194,8 +230,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -203,8 +239,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_once() bulb.async_set_levels.assert_called_once()
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -212,8 +248,8 @@ async def test_rgb_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
blocking=True, blocking=True,
) )
bulb.setPresetPattern.assert_called_with(43, 50) bulb.async_set_preset_pattern.assert_called_with(43, 50)
bulb.setPresetPattern.reset_mock() bulb.async_set_preset_pattern.reset_mock()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call( await hass.services.async_call(
@ -254,18 +290,16 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
await async_mock_bulb_turn_off(hass, bulb)
bulb.is_on = False
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -273,8 +307,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 0, 0, brightness=100) bulb.async_set_levels.assert_called_with(255, 0, 0, brightness=100)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -282,8 +316,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 191, 178, brightness=128) bulb.async_set_levels.assert_called_with(255, 191, 178, brightness=128)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -291,8 +325,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_once() bulb.async_set_levels.assert_called_once()
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -300,17 +334,16 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
blocking=True, blocking=True,
) )
bulb.setPresetPattern.assert_called_with(43, 50) bulb.async_set_preset_pattern.assert_called_with(43, 50)
bulb.setPresetPattern.reset_mock() bulb.async_set_preset_pattern.reset_mock()
bulb.is_on = True
bulb.color_mode = FLUX_COLOR_MODE_CCT bulb.color_mode = FLUX_COLOR_MODE_CCT
bulb.getWhiteTemperature = Mock(return_value=(5000, 128)) bulb.getWhiteTemperature = Mock(return_value=(5000, 128))
bulb.color_temp = 5000
bulb.raw_state = bulb.raw_state._replace( bulb.raw_state = bulb.raw_state._replace(
red=0, green=0, blue=0, warm_white=1, cool_white=2 red=0, green=0, blue=0, warm_white=1, cool_white=2
) )
async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) await async_mock_bulb_turn_on(hass, bulb)
await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
attributes = state.attributes attributes = state.attributes
@ -325,8 +358,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 370}, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 370},
blocking=True, blocking=True,
) )
bulb.setWhiteTemperature.assert_called_with(2702, 128) bulb.async_set_white_temp.assert_called_with(2702, 128)
bulb.setWhiteTemperature.reset_mock() bulb.async_set_white_temp.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -334,8 +367,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255},
blocking=True, blocking=True,
) )
bulb.setWhiteTemperature.assert_called_with(5000, 255) bulb.async_set_white_temp.assert_called_with(5000, 255)
bulb.setWhiteTemperature.reset_mock() bulb.async_set_white_temp.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -343,8 +376,8 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
blocking=True, blocking=True,
) )
bulb.setWhiteTemperature.assert_called_with(5000, 128) bulb.async_set_white_temp.assert_called_with(5000, 128)
bulb.setWhiteTemperature.reset_mock() bulb.async_set_white_temp.reset_mock()
async def test_rgbw_light(hass: HomeAssistant) -> None: async def test_rgbw_light(hass: HomeAssistant) -> None:
@ -376,18 +409,16 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
await async_mock_bulb_turn_off(hass, bulb)
bulb.is_on = False
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
bulb.is_on = True bulb.is_on = True
await hass.services.async_call( await hass.services.async_call(
@ -396,8 +427,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(168, 0, 0, 33) bulb.async_set_levels.assert_called_with(168, 0, 0, 33)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
@ -411,8 +442,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
}, },
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(128, 128, 128, 128) bulb.async_set_levels.assert_called_with(128, 128, 128, 128)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -420,8 +451,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 255, 255, 255)}, {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 255, 255, 255)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 255, 255, 255) bulb.async_set_levels.assert_called_with(255, 255, 255, 255)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -429,8 +460,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)}, {ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 191, 178, 0) bulb.async_set_levels.assert_called_with(255, 191, 178, 0)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -438,8 +469,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_once() bulb.async_set_levels.assert_called_once()
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -447,8 +478,8 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
blocking=True, blocking=True,
) )
bulb.setPresetPattern.assert_called_with(43, 50) bulb.async_set_preset_pattern.assert_called_with(43, 50)
bulb.setPresetPattern.reset_mock() bulb.async_set_preset_pattern.reset_mock()
async def test_rgbcw_light(hass: HomeAssistant) -> None: async def test_rgbcw_light(hass: HomeAssistant) -> None:
@ -481,18 +512,16 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
await async_mock_bulb_turn_off(hass, bulb)
bulb.is_on = False
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -500,8 +529,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(250, 0, 0, 49, 0) bulb.async_set_levels.assert_called_with(250, 0, 0, 49, 0)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
bulb.is_on = True bulb.is_on = True
await hass.services.async_call( await hass.services.async_call(
@ -514,8 +543,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
}, },
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(192, 192, 192, 192, 0) bulb.async_set_levels.assert_called_with(192, 192, 192, 192, 0)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -523,8 +552,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 255, 255, 255, 50)}, {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 255, 255, 255, 50)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 255, 255, 50, 255) bulb.async_set_levels.assert_called_with(255, 255, 255, 50, 255)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -532,8 +561,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154}, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127) bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -541,8 +570,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255}, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255) bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -550,8 +579,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290}, {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25) bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -559,8 +588,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 191, 178, 0, 0)}, {ATTR_ENTITY_ID: entity_id, ATTR_RGBWW_COLOR: (255, 191, 178, 0, 0)},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(255, 191, 178, 0, 0) bulb.async_set_levels.assert_called_with(255, 191, 178, 0, 0)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -568,8 +597,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_once() bulb.async_set_levels.assert_called_once()
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -577,8 +606,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade"},
blocking=True, blocking=True,
) )
bulb.setPresetPattern.assert_called_with(43, 50) bulb.async_set_preset_pattern.assert_called_with(43, 50)
bulb.setPresetPattern.reset_mock() bulb.async_set_preset_pattern.reset_mock()
async def test_white_light(hass: HomeAssistant) -> None: async def test_white_light(hass: HomeAssistant) -> None:
@ -610,18 +639,16 @@ async def test_white_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
await async_mock_bulb_turn_off(hass, bulb)
bulb.is_on = False
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -629,8 +656,8 @@ async def test_white_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(w=100) bulb.async_set_levels.assert_called_with(w=100)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, LIGHT_DOMAIN,
@ -638,8 +665,8 @@ async def test_white_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_WHITE: 100}, {ATTR_ENTITY_ID: entity_id, ATTR_WHITE: 100},
blocking=True, blocking=True,
) )
bulb.set_levels.assert_called_with(w=100) bulb.async_set_levels.assert_called_with(w=100)
bulb.set_levels.reset_mock() bulb.async_set_levels.reset_mock()
async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None: async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None:
@ -677,10 +704,8 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
await async_mock_bulb_turn_off(hass, bulb)
bulb.is_on = False
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
@ -690,12 +715,13 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "custom"}, {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "custom"},
blocking=True, blocking=True,
) )
bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.async_set_custom_pattern.assert_called_with(
bulb.setCustomPattern.reset_mock() [[0, 0, 255], [255, 0, 0]], 88, "jump"
bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) )
bulb.is_on = True bulb.async_set_custom_pattern.reset_mock()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) bulb.preset_pattern_num = EFFECT_CUSTOM_CODE
await hass.async_block_till_done() await async_mock_bulb_turn_on(hass, bulb)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
attributes = state.attributes attributes = state.attributes
@ -707,12 +733,13 @@ async def test_rgb_light_custom_effects(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 55, ATTR_EFFECT: "custom"}, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 55, ATTR_EFFECT: "custom"},
blocking=True, blocking=True,
) )
bulb.setCustomPattern.assert_called_with([[0, 0, 255], [255, 0, 0]], 88, "jump") bulb.async_set_custom_pattern.assert_called_with(
bulb.setCustomPattern.reset_mock() [[0, 0, 255], [255, 0, 0]], 88, "jump"
bulb.raw_state = bulb.raw_state._replace(preset_pattern=EFFECT_CUSTOM_CODE) )
bulb.is_on = True bulb.async_set_custom_pattern.reset_mock()
async_fire_time_changed(hass, utcnow() + timedelta(seconds=20)) bulb.preset_pattern_num = EFFECT_CUSTOM_CODE
await hass.async_block_till_done() await async_mock_bulb_turn_on(hass, bulb)
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
assert state.state == STATE_ON assert state.state == STATE_ON
attributes = state.attributes attributes = state.attributes
@ -783,11 +810,9 @@ async def test_rgb_light_custom_effect_via_service(
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
bulb.is_on = False await async_mock_bulb_turn_off(hass, bulb)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
@ -801,8 +826,10 @@ async def test_rgb_light_custom_effect_via_service(
}, },
blocking=True, blocking=True,
) )
bulb.setCustomPattern.assert_called_with([(0, 0, 255), (255, 0, 0)], 30, "jump") bulb.async_set_custom_pattern.assert_called_with(
bulb.setCustomPattern.reset_mock() [(0, 0, 255), (255, 0, 0)], 30, "jump"
)
bulb.async_set_custom_pattern.reset_mock()
async def test_migrate_from_yaml(hass: HomeAssistant) -> None: async def test_migrate_from_yaml(hass: HomeAssistant) -> None:
@ -882,19 +909,17 @@ async def test_addressable_light(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOff.assert_called_once() bulb.async_turn_off.assert_called_once()
bulb.is_on = False await async_mock_bulb_turn_off(hass, bulb)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call( await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
) )
bulb.turnOn.assert_called_once() bulb.async_turn_on.assert_called_once()
bulb.turnOn.reset_mock() bulb.async_turn_on.reset_mock()
bulb.is_on = True await async_mock_bulb_turn_on(hass, bulb)
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call( await hass.services.async_call(