mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Fix/Refactor Hyperion Integration (#39738)
This commit is contained in:
parent
e06f2a89ea
commit
0a656f13eb
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
1
tests/components/hyperion/__init__.py
Normal file
1
tests/components/hyperion/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Hyperion component."""
|
430
tests/components/hyperion/test_light.py
Normal file
430
tests/components/hyperion/test_light.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user