Fix/Refactor Hyperion Integration (#39738)

This commit is contained in:
Dermot Duffy 2020-09-24 12:37:34 -07:00 committed by GitHub
parent e06f2a89ea
commit 0a656f13eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 711 additions and 209 deletions

View File

@ -193,6 +193,7 @@ homeassistant/components/humidifier/* @home-assistant/core @Shulyaka
homeassistant/components/hunterdouglas_powerview/* @bdraco
homeassistant/components/hvv_departures/* @vigonotion
homeassistant/components/hydrawise/* @ptcryan
homeassistant/components/hyperion/* @dermotduffy
homeassistant/components/iammeter/* @lewei50
homeassistant/components/iaqualink/* @flz
homeassistant/components/icloud/* @Quentame

View File

@ -1,8 +1,7 @@
"""Support for Hyperion remotes."""
import json
"""Support for Hyperion-NG remotes."""
import logging
import socket
from hyperion import client, const
import voluptuous as vol
from homeassistant.components.light import (
@ -16,6 +15,7 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
@ -26,103 +26,91 @@ CONF_PRIORITY = "priority"
CONF_HDMI_PRIORITY = "hdmi_priority"
CONF_EFFECT_LIST = "effect_list"
# As we want to preserve brightness control for effects (e.g. to reduce the
# brightness for V4L), we need to persist the effect that is in flight, so
# subsequent calls to turn_on will know the keep the effect enabled.
# Unfortunately the Home Assistant UI does not easily expose a way to remove a
# selected effect (there is no 'No Effect' option by default). Instead, we
# create a new fake effect ("Solid") that is always selected by default for
# showing a solid color. This is the same method used by WLED.
KEY_EFFECT_SOLID = "Solid"
DEFAULT_COLOR = [255, 255, 255]
DEFAULT_BRIGHTNESS = 255
DEFAULT_EFFECT = KEY_EFFECT_SOLID
DEFAULT_NAME = "Hyperion"
DEFAULT_ORIGIN = "Home Assistant"
DEFAULT_PORT = 19444
DEFAULT_PRIORITY = 128
DEFAULT_HDMI_PRIORITY = 880
DEFAULT_EFFECT_LIST = [
"HDMI",
"Cinema brighten lights",
"Cinema dim lights",
"Knight rider",
"Blue mood blobs",
"Cold mood blobs",
"Full color mood blobs",
"Green mood blobs",
"Red mood blobs",
"Warm mood blobs",
"Police Lights Single",
"Police Lights Solid",
"Rainbow mood",
"Rainbow swirl fast",
"Rainbow swirl",
"Random",
"Running dots",
"System Shutdown",
"Snake",
"Sparks Color",
"Sparks",
"Strobe blue",
"Strobe Raspbmc",
"Strobe white",
"Color traces",
"UDP multicast listener",
"UDP listener",
"X-Mas",
]
DEFAULT_EFFECT_LIST = []
SUPPORT_HYPERION = SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
list,
vol.Length(min=3, max=3),
[vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
vol.Optional(
CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
): cv.positive_int,
vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
cv.ensure_list, [cv.string]
),
}
PLATFORM_SCHEMA = vol.All(
cv.deprecated(CONF_HDMI_PRIORITY, invalidation_version="0.118"),
cv.deprecated(CONF_DEFAULT_COLOR, invalidation_version="0.118"),
cv.deprecated(CONF_EFFECT_LIST, invalidation_version="0.118"),
PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_DEFAULT_COLOR, default=DEFAULT_COLOR): vol.All(
list,
vol.Length(min=3, max=3),
[vol.All(vol.Coerce(int), vol.Range(min=0, max=255))],
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PRIORITY, default=DEFAULT_PRIORITY): cv.positive_int,
vol.Optional(
CONF_HDMI_PRIORITY, default=DEFAULT_HDMI_PRIORITY
): cv.positive_int,
vol.Optional(CONF_EFFECT_LIST, default=DEFAULT_EFFECT_LIST): vol.All(
cv.ensure_list, [cv.string]
),
}
),
)
ICON_LIGHTBULB = "mdi:lightbulb"
ICON_EFFECT = "mdi:lava-lamp"
ICON_EXTERNAL_SOURCE = "mdi:video-input-hdmi"
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Hyperion server remote."""
name = config[CONF_NAME]
host = config[CONF_HOST]
port = config[CONF_PORT]
priority = config[CONF_PRIORITY]
hdmi_priority = config[CONF_HDMI_PRIORITY]
default_color = config[CONF_DEFAULT_COLOR]
effect_list = config[CONF_EFFECT_LIST]
device = Hyperion(
name, host, port, priority, default_color, hdmi_priority, effect_list
)
hyperion_client = client.HyperionClient(host, port)
if device.setup():
add_entities([device])
if not await hyperion_client.async_client_connect():
raise PlatformNotReady
async_add_entities([Hyperion(name, priority, hyperion_client)])
class Hyperion(LightEntity):
"""Representation of a Hyperion remote."""
def __init__(
self, name, host, port, priority, default_color, hdmi_priority, effect_list
):
def __init__(self, name, priority, hyperion_client):
"""Initialize the light."""
self._host = host
self._port = port
self._name = name
self._priority = priority
self._hdmi_priority = hdmi_priority
self._default_color = default_color
self._rgb_color = [0, 0, 0]
self._rgb_mem = [0, 0, 0]
self._brightness = 255
self._icon = "mdi:lightbulb"
self._effect_list = effect_list
self._effect = None
self._skip_update = False
self._client = hyperion_client
# Active state representing the Hyperion instance.
self._set_internal_state(
brightness=255, rgb_color=DEFAULT_COLOR, effect=KEY_EFFECT_SOLID
)
self._effect_list = []
@property
def should_poll(self):
"""Return whether or not this entity should be polled."""
return False
@property
def name(self):
@ -142,7 +130,7 @@ class Hyperion(LightEntity):
@property
def is_on(self):
"""Return true if not black."""
return self._rgb_color != [0, 0, 0]
return self._client.is_on()
@property
def icon(self):
@ -157,158 +145,233 @@ class Hyperion(LightEntity):
@property
def effect_list(self):
"""Return the list of supported effects."""
return self._effect_list
return (
self._effect_list
+ const.KEY_COMPONENTID_EXTERNAL_SOURCES
+ [KEY_EFFECT_SOLID]
)
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_HYPERION
def turn_on(self, **kwargs):
@property
def available(self):
"""Return server availability."""
return self._client.has_loaded_state
@property
def unique_id(self):
"""Return a unique id for this instance."""
return self._client.id
async def async_turn_on(self, **kwargs):
"""Turn the lights on."""
# == Turn device on ==
# Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be
# preferable to enable LEDDEVICE after the settings (e.g. brightness,
# color, effect), but this is not possible due to:
# https://github.com/hyperion-project/hyperion.ng/issues/967
if not self.is_on:
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL,
const.KEY_STATE: True,
}
}
):
return
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
const.KEY_STATE: True,
}
}
):
return
# == Get key parameters ==
brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
effect = kwargs.get(ATTR_EFFECT, self._effect)
if ATTR_HS_COLOR in kwargs:
rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR])
elif self._rgb_mem == [0, 0, 0]:
rgb_color = self._default_color
else:
rgb_color = self._rgb_mem
rgb_color = self._rgb_color
brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness)
if ATTR_EFFECT in kwargs:
self._skip_update = True
self._effect = kwargs[ATTR_EFFECT]
if self._effect == "HDMI":
self.json_request({"command": "clearall"})
self._icon = "mdi:video-input-hdmi"
self._brightness = 255
self._rgb_color = [125, 125, 125]
else:
self.json_request(
{
"command": "effect",
"priority": self._priority,
"effect": {"name": self._effect},
# == Set brightness ==
if self._brightness != brightness:
if not await self._client.async_send_set_adjustment(
**{
const.KEY_ADJUSTMENT: {
const.KEY_BRIGHTNESS: int(
round((float(brightness) * 100) / 255)
)
}
)
self._icon = "mdi:lava-lamp"
self._rgb_color = [175, 0, 255]
return
cal_color = [int(round(x * float(brightness) / 255)) for x in rgb_color]
self.json_request(
{"command": "color", "priority": self._priority, "color": cal_color}
)
def turn_off(self, **kwargs):
"""Disconnect all remotes."""
self.json_request({"command": "clearall"})
self.json_request(
{"command": "color", "priority": self._priority, "color": [0, 0, 0]}
)
def update(self):
"""Get the lights status."""
# postpone the immediate state check for changes that take time
if self._skip_update:
self._skip_update = False
return
response = self.json_request({"command": "serverinfo"})
if response:
# workaround for outdated Hyperion
if "activeLedColor" not in response["info"]:
self._rgb_color = self._default_color
self._rgb_mem = self._default_color
self._brightness = 255
self._icon = "mdi:lightbulb"
self._effect = None
}
):
return
# Check if Hyperion is in ambilight mode trough an HDMI grabber
try:
active_priority = response["info"]["priorities"][0]["priority"]
if active_priority == self._hdmi_priority:
self._brightness = 255
self._rgb_color = [125, 125, 125]
self._icon = "mdi:video-input-hdmi"
self._effect = "HDMI"
# == Set an external source
if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
# Clear any color/effect.
if not await self._client.async_send_clear(
**{const.KEY_PRIORITY: self._priority}
):
return
# Turn off all external sources, except the intended.
for key in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: key,
const.KEY_STATE: effect == key,
}
}
):
return
except (KeyError, IndexError):
pass
led_color = response["info"]["activeLedColor"]
if not led_color or led_color[0]["RGB Value"] == [0, 0, 0]:
# Get the active effect
if response["info"].get("activeEffects"):
self._rgb_color = [175, 0, 255]
self._icon = "mdi:lava-lamp"
try:
s_name = response["info"]["activeEffects"][0]["script"]
s_name = s_name.split("/")[-1][:-3].split("-")[0]
self._effect = [
x for x in self._effect_list if s_name.lower() in x.lower()
][0]
except (KeyError, IndexError):
self._effect = None
# Bulb off state
else:
self._rgb_color = [0, 0, 0]
self._icon = "mdi:lightbulb"
self._effect = None
# == Set an effect
elif effect and effect != KEY_EFFECT_SOLID:
# This call should not be necessary, but without it there is no priorities-update issued:
# https://github.com/hyperion-project/hyperion.ng/issues/992
if not await self._client.async_send_clear(
**{const.KEY_PRIORITY: self._priority}
):
return
if not await self._client.async_send_set_effect(
**{
const.KEY_PRIORITY: self._priority,
const.KEY_EFFECT: {const.KEY_NAME: effect},
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
):
return
# == Set a color
else:
if not await self._client.async_send_set_color(
**{
const.KEY_PRIORITY: self._priority,
const.KEY_COLOR: rgb_color,
const.KEY_ORIGIN: DEFAULT_ORIGIN,
}
):
return
async def async_turn_off(self, **kwargs):
"""Disable the LED output component."""
if not await self._client.async_send_set_component(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
const.KEY_STATE: False,
}
}
):
return
def _set_internal_state(self, brightness=None, rgb_color=None, effect=None):
"""Set the internal state."""
if brightness is not None:
self._brightness = brightness
if rgb_color is not None:
self._rgb_color = rgb_color
if effect is not None:
self._effect = effect
if effect == KEY_EFFECT_SOLID:
self._icon = ICON_LIGHTBULB
elif effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
self._icon = ICON_EXTERNAL_SOURCE
else:
# Get the RGB color
self._rgb_color = led_color[0]["RGB Value"]
self._brightness = max(self._rgb_color)
self._rgb_mem = [
int(round(float(x) * 255 / self._brightness))
for x in self._rgb_color
]
self._icon = "mdi:lightbulb"
self._effect = None
self._icon = ICON_EFFECT
def setup(self):
"""Get the hostname of the remote."""
response = self.json_request({"command": "serverinfo"})
if response:
if self._name == self._host:
self._name = response["info"]["hostname"]
return True
return False
def _update_components(self, _=None):
"""Update Hyperion components."""
self.async_write_ha_state()
def json_request(self, request, wait_for_response=False):
"""Communicate with the JSON server."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
def _update_adjustment(self, _=None):
"""Update Hyperion adjustments."""
if self._client.adjustment:
brightness_pct = self._client.adjustment[0].get(
const.KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS
)
if brightness_pct < 0 or brightness_pct > 100:
return
self._set_internal_state(
brightness=int(round((brightness_pct * 255) / float(100)))
)
self.async_write_ha_state()
try:
sock.connect((self._host, self._port))
except OSError:
sock.close()
return False
def _update_priorities(self, _=None):
"""Update Hyperion priorities."""
visible_priority = self._client.visible_priority
if visible_priority:
componentid = visible_priority.get(const.KEY_COMPONENTID)
if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid)
elif componentid == const.KEY_COMPONENTID_EFFECT:
# Owner is the effect name.
# See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
self._set_internal_state(
rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER]
)
elif componentid == const.KEY_COMPONENTID_COLOR:
self._set_internal_state(
rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB],
effect=KEY_EFFECT_SOLID,
)
self.async_write_ha_state()
sock.send(bytearray(f"{json.dumps(request)}\n", "utf-8"))
try:
buf = sock.recv(4096)
except socket.timeout:
# Something is wrong, assume it's offline
sock.close()
return False
def _update_effect_list(self, _=None):
"""Update Hyperion effects."""
if not self._client.effects:
return
effect_list = []
for effect in self._client.effects or []:
if const.KEY_NAME in effect:
effect_list.append(effect[const.KEY_NAME])
if effect_list:
self._effect_list = effect_list
self.async_write_ha_state()
# Read until a newline or timeout
buffering = True
while buffering:
if "\n" in str(buf, "utf-8"):
response = str(buf, "utf-8").split("\n")[0]
buffering = False
else:
try:
more = sock.recv(4096)
except socket.timeout:
more = None
if not more:
buffering = False
response = str(buf, "utf-8")
else:
buf += more
def _update_full_state(self):
"""Update full Hyperion state."""
self._update_adjustment()
self._update_priorities()
self._update_effect_list()
sock.close()
return json.loads(response)
_LOGGER.debug(
"Hyperion full state update: On=%s,Brightness=%i,Effect=%s "
"(%i effects total),Color=%s",
self.is_on,
self._brightness,
self._effect,
len(self._effect_list),
self._rgb_color,
)
def _update_client(self, json):
"""Update client connection state."""
self.async_write_ha_state()
async def async_added_to_hass(self):
"""Register callbacks when entity added to hass."""
self._client.set_callbacks(
{
f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment,
f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components,
f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list,
f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities,
f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client,
}
)
# Load initial state.
self._update_full_state()
return True

View File

@ -2,5 +2,6 @@
"domain": "hyperion",
"name": "Hyperion",
"documentation": "https://www.home-assistant.io/integrations/hyperion",
"codeowners": []
"requirements": ["hyperion-py==0.3.0"],
"codeowners": ["@dermotduffy"]
}

View File

@ -774,6 +774,9 @@ huawei-lte-api==1.4.12
# homeassistant.components.hydrawise
hydrawiser==0.2
# homeassistant.components.hyperion
hyperion-py==0.3.0
# homeassistant.components.bh1750
# homeassistant.components.bme280
# homeassistant.components.htu21d

View File

@ -391,6 +391,9 @@ httplib2==0.10.3
# homeassistant.components.huawei_lte
huawei-lte-api==1.4.12
# homeassistant.components.hyperion
hyperion-py==0.3.0
# homeassistant.components.iaqualink
iaqualink==0.3.4

View File

@ -0,0 +1 @@
"""Tests for the Hyperion component."""

View File

@ -0,0 +1,430 @@
"""Tests for the Hyperion integration."""
# from tests.async_mock import AsyncMock, MagicMock, patch
from asynctest import CoroutineMock, Mock, call, patch
from hyperion import const
from homeassistant.components.hyperion import light as hyperion_light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_EFFECT,
ATTR_HS_COLOR,
DOMAIN,
)
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.setup import async_setup_component
TEST_HOST = "test-hyperion-host"
TEST_PORT = const.DEFAULT_PORT
TEST_NAME = "test_hyperion_name"
TEST_PRIORITY = 128
TEST_ENTITY_ID = f"{DOMAIN}.{TEST_NAME}"
def create_mock_client():
"""Create a mock Hyperion client."""
mock_client = Mock()
mock_client.async_client_connect = CoroutineMock(return_value=True)
mock_client.adjustment = None
mock_client.effects = None
mock_client.id = "%s:%i" % (TEST_HOST, TEST_PORT)
return mock_client
def call_registered_callback(client, key, *args, **kwargs):
"""Call a Hyperion entity callback that was registered with the client."""
return client.set_callbacks.call_args[0][0][key](*args, **kwargs)
async def setup_entity(hass, client=None):
"""Add a test Hyperion entity to hass."""
client = client or create_mock_client()
with patch("hyperion.client.HyperionClient", return_value=client):
assert await async_setup_component(
hass,
DOMAIN,
{
DOMAIN: {
"platform": "hyperion",
"name": TEST_NAME,
"host": TEST_HOST,
"port": const.DEFAULT_PORT,
"priority": TEST_PRIORITY,
}
},
)
await hass.async_block_till_done()
async def test_setup_platform(hass):
"""Test setting up the platform."""
client = create_mock_client()
await setup_entity(hass, client=client)
assert hass.states.get(TEST_ENTITY_ID) is not None
async def test_setup_platform_not_ready(hass):
"""Test the platform not being ready."""
client = create_mock_client()
client.async_client_connect = CoroutineMock(return_value=False)
await setup_entity(hass, client=client)
assert hass.states.get(TEST_ENTITY_ID) is None
async def test_light_basic_properies(hass):
"""Test the basic properties."""
client = create_mock_client()
await setup_entity(hass, client=client)
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "on"
assert entity_state.attributes["brightness"] == 255
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
# By default the effect list is the 3 external sources + 'Solid'.
assert len(entity_state.attributes["effect_list"]) == 4
assert (
entity_state.attributes["supported_features"] == hyperion_light.SUPPORT_HYPERION
)
async def test_light_async_turn_on(hass):
"""Test turning the light on."""
client = create_mock_client()
await setup_entity(hass, client=client)
# On (=), 100% (=), solid (=), [255,255,255] (=)
client.async_send_set_color = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
)
assert client.async_send_set_color.call_args == call(
**{
const.KEY_PRIORITY: TEST_PRIORITY,
const.KEY_COLOR: [255, 255, 255],
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
}
)
# On (=), 50% (!), solid (=), [255,255,255] (=)
# ===
brightness = 128
client.async_send_set_color = CoroutineMock(return_value=True)
client.async_send_set_adjustment = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
blocking=True,
)
assert client.async_send_set_adjustment.call_args == call(
**{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 50}}
)
assert client.async_send_set_color.call_args == call(
**{
const.KEY_PRIORITY: TEST_PRIORITY,
const.KEY_COLOR: [255, 255, 255],
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
}
)
# Simulate a state callback from Hyperion.
client.adjustment = [{const.KEY_BRIGHTNESS: 50}]
call_registered_callback(client, "adjustment-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "on"
assert entity_state.attributes["brightness"] == brightness
# On (=), 50% (=), solid (=), [0,255,255] (!)
hs_color = (180.0, 100.0)
client.async_send_set_color = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_HS_COLOR: hs_color},
blocking=True,
)
assert client.async_send_set_color.call_args == call(
**{
const.KEY_PRIORITY: TEST_PRIORITY,
const.KEY_COLOR: (0, 255, 255),
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
}
)
# Simulate a state callback from Hyperion.
client.visible_priority = {
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["hs_color"] == hs_color
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
# On (=), 100% (!), solid, [0,255,255] (=)
brightness = 255
client.async_send_set_color = CoroutineMock(return_value=True)
client.async_send_set_adjustment = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_BRIGHTNESS: brightness},
blocking=True,
)
assert client.async_send_set_adjustment.call_args == call(
**{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 100}}
)
assert client.async_send_set_color.call_args == call(
**{
const.KEY_PRIORITY: TEST_PRIORITY,
const.KEY_COLOR: (0, 255, 255),
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
}
)
client.adjustment = [{const.KEY_BRIGHTNESS: 100}]
call_registered_callback(client, "adjustment-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["brightness"] == brightness
# On (=), 100% (=), V4L (!), [0,255,255] (=)
effect = const.KEY_COMPONENTID_EXTERNAL_SOURCES[2] # V4L
client.async_send_clear = CoroutineMock(return_value=True)
client.async_send_set_component = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
blocking=True,
)
assert client.async_send_clear.call_args == call(
**{const.KEY_PRIORITY: TEST_PRIORITY}
)
assert client.async_send_set_component.call_args_list == [
call(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[0],
const.KEY_STATE: False,
}
}
),
call(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[1],
const.KEY_STATE: False,
}
}
),
call(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_EXTERNAL_SOURCES[2],
const.KEY_STATE: True,
}
}
),
]
client.visible_priority = {const.KEY_COMPONENTID: effect}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
assert entity_state.attributes["effect"] == effect
# On (=), 100% (=), "Warm Blobs" (!), [0,255,255] (=)
effect = "Warm Blobs"
client.async_send_clear = CoroutineMock(return_value=True)
client.async_send_set_effect = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: TEST_ENTITY_ID, ATTR_EFFECT: effect},
blocking=True,
)
assert client.async_send_clear.call_args == call(
**{const.KEY_PRIORITY: TEST_PRIORITY}
)
assert client.async_send_set_effect.call_args == call(
**{
const.KEY_PRIORITY: TEST_PRIORITY,
const.KEY_EFFECT: {const.KEY_NAME: effect},
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
}
)
client.visible_priority = {
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
const.KEY_OWNER: effect,
}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
assert entity_state.attributes["effect"] == effect
# No calls if disconnected.
client.has_loaded_state = False
call_registered_callback(client, "client-update", {"loaded-state": False})
client.async_send_clear = CoroutineMock(return_value=True)
client.async_send_set_effect = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
)
assert not client.async_send_clear.called
assert not client.async_send_set_effect.called
async def test_light_async_turn_off(hass):
"""Test turning the light off."""
client = create_mock_client()
await setup_entity(hass, client=client)
client.async_send_set_component = CoroutineMock(return_value=True)
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
)
assert client.async_send_set_component.call_args == call(
**{
const.KEY_COMPONENTSTATE: {
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
const.KEY_STATE: False,
}
}
)
# No calls if no state loaded.
client.has_loaded_state = False
client.async_send_set_component = CoroutineMock(return_value=True)
call_registered_callback(client, "client-update", {"loaded-state": False})
await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TEST_ENTITY_ID}, blocking=True
)
assert not client.async_send_set_component.called
async def test_light_async_updates_from_hyperion_client(hass):
"""Test receiving a variety of Hyperion client callbacks."""
client = create_mock_client()
await setup_entity(hass, client=client)
# Bright change gets accepted.
brightness = 10
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
call_registered_callback(client, "adjustment-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
# Broken brightness value is ignored.
bad_brightness = -200
client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}]
call_registered_callback(client, "adjustment-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
# Update components.
client.is_on.return_value = True
call_registered_callback(client, "components-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "on"
client.is_on.return_value = False
call_registered_callback(client, "components-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "off"
# Update priorities (V4L)
client.is_on.return_value = True
client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
assert entity_state.attributes["effect"] == const.KEY_COMPONENTID_V4L
# Update priorities (Effect)
effect = "foo"
client.visible_priority = {
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
const.KEY_OWNER: effect,
}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["effect"] == effect
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
# Update priorities (Color)
rgb = (0, 100, 100)
client.visible_priority = {
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
const.KEY_VALUE: {const.KEY_RGB: rgb},
}
call_registered_callback(client, "priorities-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
assert entity_state.attributes["hs_color"] == (180.0, 100.0)
# Update effect list
effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
client.effects = effects
call_registered_callback(client, "effects-update")
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["effect_list"] == [
effect[const.KEY_NAME] for effect in effects
] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID]
# Update connection status (e.g. disconnection).
# Turn on late, check state, disconnect, ensure it cannot be turned off.
client.has_loaded_state = False
call_registered_callback(client, "client-update", {"loaded-state": False})
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "unavailable"
# Update connection status (e.g. re-connection)
client.has_loaded_state = True
call_registered_callback(client, "client-update", {"loaded-state": True})
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.state == "on"
async def test_full_state_loaded_on_start(hass):
"""Test receiving a variety of Hyperion client callbacks."""
client = create_mock_client()
# Update full state (should call all update methods).
brightness = 25
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
client.visible_priority = {
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
const.KEY_VALUE: {const.KEY_RGB: (0, 100, 100)},
}
client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
await setup_entity(hass, client=client)
entity_state = hass.states.get(TEST_ENTITY_ID)
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
assert entity_state.attributes["hs_color"] == (180.0, 100.0)