mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Add Kuler Sky Bluetooth floor lamp integration (#42372)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
7cd17dd94f
commit
7c83092431
@ -242,6 +242,7 @@ homeassistant/components/keyboard_remote/* @bendavid
|
||||
homeassistant/components/knx/* @Julius2342 @farmio @marvin-w
|
||||
homeassistant/components/kodi/* @OnFreund @cgtobi
|
||||
homeassistant/components/konnected/* @heythisisnate @kit-klein
|
||||
homeassistant/components/kulersky/* @emlove
|
||||
homeassistant/components/lametric/* @robbiet480
|
||||
homeassistant/components/launch_library/* @ludeeus
|
||||
homeassistant/components/lcn/* @alengwenus
|
||||
|
44
homeassistant/components/kulersky/__init__.py
Normal file
44
homeassistant/components/kulersky/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Kuler Sky lights integration."""
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
PLATFORMS = ["light"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Kuler Sky component."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Kuler Sky from a config entry."""
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
29
homeassistant/components/kulersky/config_flow.py
Normal file
29
homeassistant/components/kulersky/config_flow.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Config flow for Kuler Sky."""
|
||||
import logging
|
||||
|
||||
import pykulersky
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_has_devices(hass) -> bool:
|
||||
"""Return if there are devices that can be discovered."""
|
||||
# Check if there are any devices that can be discovered in the network.
|
||||
try:
|
||||
devices = await hass.async_add_executor_job(
|
||||
pykulersky.discover_bluetooth_devices
|
||||
)
|
||||
except pykulersky.PykulerskyException as exc:
|
||||
_LOGGER.error("Unable to discover nearby Kuler Sky devices: %s", exc)
|
||||
return False
|
||||
return len(devices) > 0
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, "Kuler Sky", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN
|
||||
)
|
2
homeassistant/components/kulersky/const.py
Normal file
2
homeassistant/components/kulersky/const.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Constants for the Kuler Sky integration."""
|
||||
DOMAIN = "kulersky"
|
210
homeassistant/components/kulersky/light.py
Normal file
210
homeassistant/components/kulersky/light.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""Kuler Sky light platform."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Callable, List
|
||||
|
||||
import pykulersky
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_WHITE_VALUE,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_KULERSKY = SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE
|
||||
|
||||
DISCOVERY_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up Kuler sky light devices."""
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
if "devices" not in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN]["devices"] = set()
|
||||
if "discovery" not in hass.data[DOMAIN]:
|
||||
hass.data[DOMAIN]["discovery"] = asyncio.Lock()
|
||||
|
||||
async def discover(*args):
|
||||
"""Attempt to discover new lights."""
|
||||
# Since discovery needs to connect to all discovered bluetooth devices, and
|
||||
# only rules out devices after a timeout, it can potentially take a long
|
||||
# time. If there's already a discovery running, just skip this poll.
|
||||
if hass.data[DOMAIN]["discovery"].locked():
|
||||
return
|
||||
|
||||
async with hass.data[DOMAIN]["discovery"]:
|
||||
bluetooth_devices = await hass.async_add_executor_job(
|
||||
pykulersky.discover_bluetooth_devices
|
||||
)
|
||||
|
||||
# Filter out already connected lights
|
||||
new_devices = [
|
||||
device
|
||||
for device in bluetooth_devices
|
||||
if device["address"] not in hass.data[DOMAIN]["devices"]
|
||||
]
|
||||
|
||||
for device in new_devices:
|
||||
light = pykulersky.Light(device["address"], device["name"])
|
||||
try:
|
||||
# Attempt to connect to this light and read the color. If the
|
||||
# connection fails, either this is not a Kuler Sky light, or
|
||||
# it's bluetooth connection is currently locked by another
|
||||
# device. If the vendor's app is connected to the light when
|
||||
# home assistant tries to connect, this connection will fail.
|
||||
await hass.async_add_executor_job(light.connect)
|
||||
await hass.async_add_executor_job(light.get_color)
|
||||
except pykulersky.PykulerskyException:
|
||||
continue
|
||||
# The light has successfully connected
|
||||
hass.data[DOMAIN]["devices"].add(device["address"])
|
||||
async_add_entities([KulerskyLight(light)], update_before_add=True)
|
||||
|
||||
# Start initial discovery
|
||||
hass.async_add_job(discover)
|
||||
|
||||
# Perform recurring discovery of new devices
|
||||
async_track_time_interval(hass, discover, DISCOVERY_INTERVAL)
|
||||
|
||||
|
||||
class KulerskyLight(LightEntity):
|
||||
"""Representation of an Kuler Sky Light."""
|
||||
|
||||
def __init__(self, light: pykulersky.Light):
|
||||
"""Initialize a Kuler Sky light."""
|
||||
self._light = light
|
||||
self._hs_color = None
|
||||
self._brightness = None
|
||||
self._white_value = None
|
||||
self._available = True
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.disconnect)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass."""
|
||||
await self.hass.async_add_executor_job(self.disconnect)
|
||||
|
||||
def disconnect(self, *args) -> None:
|
||||
"""Disconnect the underlying device."""
|
||||
self._light.disconnect()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this light."""
|
||||
return self._light.name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the ID of this light."""
|
||||
return self._light.address
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info for this light."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, self.unique_id)},
|
||||
"name": self.name,
|
||||
"manufacturer": "Brightech",
|
||||
}
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return SUPPORT_KULERSKY
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of the light."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
"""Return the hs color."""
|
||||
return self._hs_color
|
||||
|
||||
@property
|
||||
def white_value(self):
|
||||
"""Return the white value of this light between 0..255."""
|
||||
return self._white_value
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._brightness > 0 or self._white_value > 0
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Instruct the light to turn on."""
|
||||
default_hs = (0, 0) if self._hs_color is None else self._hs_color
|
||||
hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs)
|
||||
|
||||
default_brightness = 0 if self._brightness is None else self._brightness
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness)
|
||||
|
||||
default_white_value = 255 if self._white_value is None else self._white_value
|
||||
white_value = kwargs.get(ATTR_WHITE_VALUE, default_white_value)
|
||||
|
||||
if brightness == 0 and white_value == 0 and not kwargs:
|
||||
# If the light would be off, and no additional parameters were
|
||||
# passed, just turn the light on full brightness.
|
||||
brightness = 255
|
||||
white_value = 255
|
||||
|
||||
rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100)
|
||||
|
||||
self._light.set_color(*rgb, white_value)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
self._light.set_color(0, 0, 0, 0)
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light."""
|
||||
try:
|
||||
if not self._light.connected:
|
||||
self._light.connect()
|
||||
# pylint: disable=invalid-name
|
||||
r, g, b, w = self._light.get_color()
|
||||
except pykulersky.PykulerskyException as exc:
|
||||
if self._available:
|
||||
_LOGGER.warning("Unable to connect to %s: %s", self._light.address, exc)
|
||||
self._available = False
|
||||
return
|
||||
if not self._available:
|
||||
_LOGGER.info("Reconnected to %s", self.entity_id)
|
||||
self._available = True
|
||||
|
||||
hsv = color_util.color_RGB_to_hsv(r, g, b)
|
||||
self._hs_color = hsv[:2]
|
||||
self._brightness = int(round((hsv[2] / 100) * 255))
|
||||
self._white_value = w
|
12
homeassistant/components/kulersky/manifest.json
Normal file
12
homeassistant/components/kulersky/manifest.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "kulersky",
|
||||
"name": "Kuler Sky",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/kulersky",
|
||||
"requirements": [
|
||||
"pykulersky==0.4.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@emlove"
|
||||
]
|
||||
}
|
13
homeassistant/components/kulersky/strings.json
Normal file
13
homeassistant/components/kulersky/strings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
}
|
||||
}
|
||||
}
|
13
homeassistant/components/kulersky/translations/en.json
Normal file
13
homeassistant/components/kulersky/translations/en.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to start set up?"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -108,6 +108,7 @@ FLOWS = [
|
||||
"juicenet",
|
||||
"kodi",
|
||||
"konnected",
|
||||
"kulersky",
|
||||
"life360",
|
||||
"lifx",
|
||||
"local_ip",
|
||||
|
@ -1475,6 +1475,9 @@ pykira==0.1.1
|
||||
# homeassistant.components.kodi
|
||||
pykodi==0.2.1
|
||||
|
||||
# homeassistant.components.kulersky
|
||||
pykulersky==0.4.0
|
||||
|
||||
# homeassistant.components.kwb
|
||||
pykwb==0.0.8
|
||||
|
||||
|
@ -745,6 +745,9 @@ pykira==0.1.1
|
||||
# homeassistant.components.kodi
|
||||
pykodi==0.2.1
|
||||
|
||||
# homeassistant.components.kulersky
|
||||
pykulersky==0.4.0
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==4.0.0
|
||||
|
||||
|
1
tests/components/kulersky/__init__.py
Normal file
1
tests/components/kulersky/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Kuler Sky integration."""
|
104
tests/components/kulersky/test_config_flow.py
Normal file
104
tests/components/kulersky/test_config_flow.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Test the Kuler Sky config flow."""
|
||||
import pykulersky
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.kulersky.config_flow import DOMAIN
|
||||
|
||||
from tests.async_mock import patch
|
||||
|
||||
|
||||
async def test_flow_success(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
|
||||
return_value=[
|
||||
{
|
||||
"address": "AA:BB:CC:11:22:33",
|
||||
"name": "Bedroom",
|
||||
}
|
||||
],
|
||||
), patch(
|
||||
"homeassistant.components.kulersky.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.kulersky.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Kuler Sky"
|
||||
assert result2["data"] == {}
|
||||
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_no_devices_found(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
|
||||
return_value=[],
|
||||
), patch(
|
||||
"homeassistant.components.kulersky.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.kulersky.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result2["type"] == "abort"
|
||||
assert result2["reason"] == "no_devices_found"
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 0
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
||||
|
||||
|
||||
async def test_flow_exceptions_caught(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.config_flow.pykulersky.discover_bluetooth_devices",
|
||||
side_effect=pykulersky.PykulerskyException("TEST"),
|
||||
), patch(
|
||||
"homeassistant.components.kulersky.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.kulersky.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result2["type"] == "abort"
|
||||
assert result2["reason"] == "no_devices_found"
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup.mock_calls) == 0
|
||||
assert len(mock_setup_entry.mock_calls) == 0
|
315
tests/components/kulersky/test_light.py
Normal file
315
tests/components/kulersky/test_light.py
Normal file
@ -0,0 +1,315 @@
|
||||
"""Test the Kuler Sky lights."""
|
||||
import asyncio
|
||||
|
||||
import pykulersky
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.kulersky.light import DOMAIN
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_WHITE_VALUE,
|
||||
ATTR_XY_COLOR,
|
||||
SCAN_INTERVAL,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_WHITE_VALUE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from tests.async_mock import MagicMock, patch
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_entry(hass):
|
||||
"""Create a mock light entity."""
|
||||
return MockConfigEntry(domain=DOMAIN)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_light(hass, mock_entry):
|
||||
"""Create a mock light entity."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
light = MagicMock(spec=pykulersky.Light)
|
||||
light.address = "AA:BB:CC:11:22:33"
|
||||
light.name = "Bedroom"
|
||||
light.connected = False
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
|
||||
return_value=[
|
||||
{
|
||||
"address": "AA:BB:CC:11:22:33",
|
||||
"name": "Bedroom",
|
||||
}
|
||||
],
|
||||
):
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.light.pykulersky.Light"
|
||||
) as mockdevice, patch.object(light, "connect") as mock_connect, patch.object(
|
||||
light, "get_color", return_value=(0, 0, 0, 0)
|
||||
):
|
||||
mockdevice.return_value = light
|
||||
mock_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_connect.called
|
||||
light.connected = True
|
||||
|
||||
yield light
|
||||
|
||||
|
||||
async def test_init(hass, mock_light):
|
||||
"""Test platform setup."""
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes == {
|
||||
ATTR_FRIENDLY_NAME: "Bedroom",
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR
|
||||
| SUPPORT_WHITE_VALUE,
|
||||
}
|
||||
|
||||
with patch.object(hass.loop, "stop"), patch.object(
|
||||
mock_light, "disconnect"
|
||||
) as mock_disconnect:
|
||||
await hass.async_stop()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_disconnect.called
|
||||
|
||||
|
||||
async def test_discovery_lock(hass, mock_entry):
|
||||
"""Test discovery lock."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
discovery_finished = None
|
||||
first_discovery_started = asyncio.Event()
|
||||
|
||||
async def mock_discovery(*args):
|
||||
"""Block to simulate multiple discovery calls while one still running."""
|
||||
nonlocal discovery_finished
|
||||
if discovery_finished:
|
||||
first_discovery_started.set()
|
||||
await discovery_finished.wait()
|
||||
return []
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
|
||||
return_value=[],
|
||||
), patch(
|
||||
"homeassistant.components.kulersky.light.async_track_time_interval",
|
||||
) as mock_track_time_interval:
|
||||
mock_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch.object(
|
||||
hass, "async_add_executor_job", side_effect=mock_discovery
|
||||
) as mock_run_discovery:
|
||||
discovery_coroutine = mock_track_time_interval.call_args[0][1]
|
||||
|
||||
discovery_finished = asyncio.Event()
|
||||
|
||||
# Schedule multiple discoveries
|
||||
hass.async_create_task(discovery_coroutine())
|
||||
hass.async_create_task(discovery_coroutine())
|
||||
hass.async_create_task(discovery_coroutine())
|
||||
|
||||
# Wait until the first discovery call is blocked
|
||||
await first_discovery_started.wait()
|
||||
|
||||
# Unblock the first discovery
|
||||
discovery_finished.set()
|
||||
|
||||
# Flush the remaining jobs
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The discovery method should only have been called once
|
||||
mock_run_discovery.assert_called_once()
|
||||
|
||||
|
||||
async def test_discovery_connection_error(hass, mock_entry):
|
||||
"""Test that invalid devices are skipped."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
light = MagicMock(spec=pykulersky.Light)
|
||||
light.address = "AA:BB:CC:11:22:33"
|
||||
light.name = "Bedroom"
|
||||
light.connected = False
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.light.pykulersky.discover_bluetooth_devices",
|
||||
return_value=[
|
||||
{
|
||||
"address": "AA:BB:CC:11:22:33",
|
||||
"name": "Bedroom",
|
||||
}
|
||||
],
|
||||
):
|
||||
with patch(
|
||||
"homeassistant.components.kulersky.light.pykulersky.Light"
|
||||
) as mockdevice, patch.object(
|
||||
light, "connect", side_effect=pykulersky.PykulerskyException
|
||||
):
|
||||
mockdevice.return_value = light
|
||||
mock_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Assert entity was not added
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state is None
|
||||
|
||||
|
||||
async def test_remove_entry(hass, mock_light, mock_entry):
|
||||
"""Test platform setup."""
|
||||
with patch.object(mock_light, "disconnect") as mock_disconnect:
|
||||
await hass.config_entries.async_remove(mock_entry.entry_id)
|
||||
|
||||
assert mock_disconnect.called
|
||||
|
||||
|
||||
async def test_update_exception(hass, mock_light):
|
||||
"""Test platform setup."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
|
||||
with patch.object(
|
||||
mock_light, "get_color", side_effect=pykulersky.PykulerskyException
|
||||
):
|
||||
await hass.helpers.entity_component.async_update_entity("light.bedroom")
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_light_turn_on(hass, mock_light):
|
||||
"""Test KulerSkyLight turn_on."""
|
||||
with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
|
||||
mock_light, "get_color", return_value=(255, 255, 255, 255)
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: "light.bedroom"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_color.assert_called_with(255, 255, 255, 255)
|
||||
|
||||
with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
|
||||
mock_light, "get_color", return_value=(50, 50, 50, 255)
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: "light.bedroom", ATTR_BRIGHTNESS: 50},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_color.assert_called_with(50, 50, 50, 255)
|
||||
|
||||
with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
|
||||
mock_light, "get_color", return_value=(50, 45, 25, 255)
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: "light.bedroom", ATTR_HS_COLOR: (50, 50)},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_set_color.assert_called_with(50, 45, 25, 255)
|
||||
|
||||
with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
|
||||
mock_light, "get_color", return_value=(220, 201, 110, 180)
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{ATTR_ENTITY_ID: "light.bedroom", ATTR_WHITE_VALUE: 180},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_color.assert_called_with(50, 45, 25, 180)
|
||||
|
||||
|
||||
async def test_light_turn_off(hass, mock_light):
|
||||
"""Test KulerSkyLight turn_on."""
|
||||
with patch.object(mock_light, "set_color") as mock_set_color, patch.object(
|
||||
mock_light, "get_color", return_value=(0, 0, 0, 0)
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{ATTR_ENTITY_ID: "light.bedroom"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_set_color.assert_called_with(0, 0, 0, 0)
|
||||
|
||||
|
||||
async def test_light_update(hass, mock_light):
|
||||
"""Test KulerSkyLight update."""
|
||||
utcnow = dt_util.utcnow()
|
||||
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes == {
|
||||
ATTR_FRIENDLY_NAME: "Bedroom",
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR
|
||||
| SUPPORT_WHITE_VALUE,
|
||||
}
|
||||
|
||||
# Test an exception during discovery
|
||||
with patch.object(
|
||||
mock_light, "get_color", side_effect=pykulersky.PykulerskyException("TEST")
|
||||
):
|
||||
utcnow = utcnow + SCAN_INTERVAL
|
||||
async_fire_time_changed(hass, utcnow)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert state.attributes == {
|
||||
ATTR_FRIENDLY_NAME: "Bedroom",
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR
|
||||
| SUPPORT_WHITE_VALUE,
|
||||
}
|
||||
|
||||
with patch.object(
|
||||
mock_light,
|
||||
"get_color",
|
||||
return_value=(80, 160, 200, 240),
|
||||
):
|
||||
utcnow = utcnow + SCAN_INTERVAL
|
||||
async_fire_time_changed(hass, utcnow)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.bedroom")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes == {
|
||||
ATTR_FRIENDLY_NAME: "Bedroom",
|
||||
ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR
|
||||
| SUPPORT_WHITE_VALUE,
|
||||
ATTR_BRIGHTNESS: 200,
|
||||
ATTR_HS_COLOR: (200, 60),
|
||||
ATTR_RGB_COLOR: (102, 203, 255),
|
||||
ATTR_WHITE_VALUE: 240,
|
||||
ATTR_XY_COLOR: (0.184, 0.261),
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user