mirror of https://github.com/home-assistant/core
205 lines
5.8 KiB
Python
205 lines
5.8 KiB
Python
"""Tests for the Yeelight integration."""
|
|
|
|
from datetime import timedelta
|
|
from ipaddress import ip_address
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from async_upnp_client.search import SsdpSearchListener
|
|
from async_upnp_client.utils import CaseInsensitiveDict
|
|
from yeelight import BulbException, BulbType
|
|
from yeelight.main import _MODEL_SPECS
|
|
|
|
from homeassistant.components import zeroconf
|
|
from homeassistant.components.yeelight import (
|
|
CONF_MODE_MUSIC,
|
|
CONF_NIGHTLIGHT_SWITCH_TYPE,
|
|
CONF_SAVE_ON_CHANGE,
|
|
DOMAIN,
|
|
NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
|
YeelightScanner,
|
|
scanner,
|
|
)
|
|
from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME
|
|
from homeassistant.core import callback
|
|
|
|
FAIL_TO_BIND_IP = "1.2.3.4"
|
|
|
|
IP_ADDRESS = "192.168.1.239"
|
|
MODEL = "color"
|
|
ID = "0x000000000015243f"
|
|
FW_VER = "18"
|
|
|
|
CAPABILITIES = {
|
|
"id": ID,
|
|
"model": MODEL,
|
|
"fw_ver": FW_VER,
|
|
"location": f"yeelight://{IP_ADDRESS}",
|
|
"support": (
|
|
"get_prop set_default set_power toggle set_bright start_cf stop_cf"
|
|
" set_scene cron_add cron_get cron_del set_ct_abx set_rgb"
|
|
),
|
|
"name": "",
|
|
}
|
|
|
|
ID_DECIMAL = f"{int(ID, 16):08d}"
|
|
|
|
ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo(
|
|
ip_address=ip_address(IP_ADDRESS),
|
|
ip_addresses=[ip_address(IP_ADDRESS)],
|
|
port=54321,
|
|
hostname=f"yeelink-light-strip1_miio{ID_DECIMAL}.local.",
|
|
type="_miio._udp.local.",
|
|
name=f"yeelink-light-strip1_miio{ID_DECIMAL}._miio._udp.local.",
|
|
properties={"epoch": "1", "mac": "000000000000"},
|
|
)
|
|
|
|
NAME = "name"
|
|
SHORT_ID = hex(int("0x000000000015243f", 16))
|
|
UNIQUE_NAME = f"yeelight_{MODEL}_{SHORT_ID}"
|
|
UNIQUE_FRIENDLY_NAME = f"Yeelight {MODEL.title()} {SHORT_ID}"
|
|
|
|
MODULE = "homeassistant.components.yeelight"
|
|
MODULE_CONFIG_FLOW = f"{MODULE}.config_flow"
|
|
|
|
PROPERTIES = {
|
|
"power": "on",
|
|
"main_power": "on",
|
|
"bright": "50",
|
|
"ct": "4000",
|
|
"rgb": "16711680",
|
|
"hue": "100",
|
|
"sat": "35",
|
|
"color_mode": "1",
|
|
"flowing": "0",
|
|
"bg_power": "on",
|
|
"bg_lmode": "1",
|
|
"bg_flowing": "0",
|
|
"bg_ct": "5000",
|
|
"bg_bright": "80",
|
|
"bg_rgb": "65280",
|
|
"bg_hue": "200",
|
|
"bg_sat": "70",
|
|
"nl_br": "23",
|
|
"active_mode": "0",
|
|
"current_brightness": "30",
|
|
}
|
|
|
|
ENTITY_BINARY_SENSOR_TEMPLATE = "binary_sensor.{}_nightlight"
|
|
ENTITY_BINARY_SENSOR = ENTITY_BINARY_SENSOR_TEMPLATE.format(UNIQUE_NAME)
|
|
ENTITY_LIGHT = f"light.{UNIQUE_NAME}"
|
|
ENTITY_NIGHTLIGHT = f"light.{UNIQUE_NAME}_nightlight"
|
|
ENTITY_AMBILIGHT = f"light.{UNIQUE_NAME}_ambilight"
|
|
|
|
YAML_CONFIGURATION = {
|
|
DOMAIN: {
|
|
CONF_DEVICES: {
|
|
IP_ADDRESS: {
|
|
CONF_NAME: NAME,
|
|
CONF_NIGHTLIGHT_SWITCH_TYPE: NIGHTLIGHT_SWITCH_TYPE_LIGHT,
|
|
CONF_MODE_MUSIC: True,
|
|
CONF_SAVE_ON_CHANGE: True,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CONFIG_ENTRY_DATA = {CONF_ID: ID}
|
|
|
|
|
|
class MockAsyncBulb:
|
|
"""A mock for yeelight.aio.AsyncBulb."""
|
|
|
|
def __init__(self, model, bulb_type, cannot_connect) -> None:
|
|
"""Init the mock."""
|
|
self.model = model
|
|
self.bulb_type = bulb_type
|
|
self._async_callback = None
|
|
self._cannot_connect = cannot_connect
|
|
self.capabilities = None
|
|
|
|
async def async_listen(self, callback):
|
|
"""Mock the listener."""
|
|
if self._cannot_connect:
|
|
raise BulbException
|
|
self._async_callback = callback
|
|
|
|
async def async_stop_listening(self):
|
|
"""Drop the listener."""
|
|
self._async_callback = None
|
|
|
|
def set_capabilities(self, capabilities):
|
|
"""Mock setting capabilities."""
|
|
self.capabilities = capabilities
|
|
|
|
|
|
def _mocked_bulb(cannot_connect=False):
|
|
# pylint: disable=attribute-defined-outside-init
|
|
bulb = MockAsyncBulb(MODEL, BulbType.Color, cannot_connect)
|
|
type(bulb).async_get_properties = AsyncMock(
|
|
side_effect=BulbException if cannot_connect else None
|
|
)
|
|
type(bulb).get_properties = MagicMock(
|
|
side_effect=BulbException if cannot_connect else None
|
|
)
|
|
type(bulb).get_model_specs = MagicMock(return_value=_MODEL_SPECS[MODEL])
|
|
bulb.capabilities = CAPABILITIES.copy()
|
|
bulb.available = True
|
|
bulb.last_properties = PROPERTIES.copy()
|
|
bulb.music_mode = False
|
|
bulb.async_get_properties = AsyncMock()
|
|
bulb.async_update = AsyncMock()
|
|
bulb.async_turn_on = AsyncMock()
|
|
bulb.async_turn_off = AsyncMock()
|
|
bulb.async_set_brightness = AsyncMock()
|
|
bulb.async_set_color_temp = AsyncMock()
|
|
bulb.async_set_hsv = AsyncMock()
|
|
bulb.async_set_rgb = AsyncMock()
|
|
bulb.async_start_flow = AsyncMock()
|
|
bulb.async_stop_flow = AsyncMock()
|
|
bulb.async_set_power_mode = AsyncMock()
|
|
bulb.async_set_scene = AsyncMock()
|
|
bulb.async_set_default = AsyncMock()
|
|
bulb.async_start_music = AsyncMock()
|
|
return bulb
|
|
|
|
|
|
def _patched_ssdp_listener(info: CaseInsensitiveDict, *args, **kwargs):
|
|
listener = SsdpSearchListener(*args, **kwargs)
|
|
|
|
async def _async_callback(*_):
|
|
if kwargs["source"][0] == FAIL_TO_BIND_IP:
|
|
raise OSError
|
|
listener.connect_callback()
|
|
|
|
@callback
|
|
def _async_search(*_):
|
|
if info:
|
|
listener.callback(info)
|
|
|
|
listener.async_start = _async_callback
|
|
listener.async_search = _async_search
|
|
return listener
|
|
|
|
|
|
def _patch_discovery(no_device=False, capabilities=None):
|
|
YeelightScanner._scanner = None # Clear class scanner to reset hass
|
|
|
|
def _generate_fake_ssdp_listener(*args, **kwargs):
|
|
info = None
|
|
if not no_device:
|
|
info = capabilities or CAPABILITIES
|
|
return _patched_ssdp_listener(info, *args, **kwargs)
|
|
|
|
return patch(
|
|
"homeassistant.components.yeelight.scanner.SsdpSearchListener",
|
|
new=_generate_fake_ssdp_listener,
|
|
)
|
|
|
|
|
|
def _patch_discovery_interval():
|
|
return patch.object(scanner, "DISCOVERY_SEARCH_INTERVAL", timedelta(seconds=0))
|
|
|
|
|
|
def _patch_discovery_timeout():
|
|
return patch.object(scanner, "DISCOVERY_TIMEOUT", 0.0001)
|