Add support for Elgato Light Strip (#49988)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
pull/50141/head
Franck Nijhof 2021-05-06 01:41:32 +02:00 committed by GitHub
parent e4ef06d6b1
commit ae692a003f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 65 deletions

View File

@ -1,4 +1,4 @@
"""Support for Elgato Key Lights."""
"""Support for Elgato Lights."""
import logging
from elgato import Elgato, ElgatoConnectionError
@ -16,7 +16,7 @@ PLATFORMS = [LIGHT_DOMAIN]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Elgato Key Light from a config entry."""
"""Set up Elgato Light from a config entry."""
session = async_get_clientsession(hass)
elgato = Elgato(
entry.data[CONF_HOST],
@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Elgato Key Light config entry."""
"""Unload Elgato Light config entry."""
# Unload entities for this entry/device.
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:

View File

@ -1,4 +1,4 @@
"""Config flow to configure the Elgato Key Light integration."""
"""Config flow to configure the Elgato Light integration."""
from __future__ import annotations
from typing import Any
@ -16,7 +16,7 @@ from .const import CONF_SERIAL_NUMBER, DOMAIN
class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Elgato Key Light config flow."""
"""Handle a Elgato Light config flow."""
VERSION = 1
@ -91,7 +91,7 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def _get_elgato_serial_number(self, raise_on_progress: bool = True) -> None:
"""Get device information from an Elgato Key Light device."""
"""Get device information from an Elgato Light device."""
session = async_get_clientsession(self.hass)
elgato = Elgato(
host=self.host,

View File

@ -1,4 +1,4 @@
"""Constants for the Elgato Key Light integration."""
"""Constants for the Elgato Light integration."""
# Integration domain
DOMAIN = "elgato"

View File

@ -1,17 +1,18 @@
"""Support for LED lights."""
"""Support for Elgato lights."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from elgato import Elgato, ElgatoError, Info, State
from elgato import Elgato, ElgatoError, Info, Settings, State
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
ATTR_HS_COLOR,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
@ -42,10 +43,11 @@ async def async_setup_entry(
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Elgato Key Light based on a config entry."""
"""Set up Elgato Light based on a config entry."""
elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT]
info = await elgato.info()
async_add_entities([ElgatoLight(elgato, info)], True)
settings = await elgato.settings()
async_add_entities([ElgatoLight(elgato, info, settings)], True)
platform = async_get_current_platform()
platform.async_register_entity_service(
@ -56,18 +58,25 @@ async def async_setup_entry(
class ElgatoLight(LightEntity):
"""Defines a Elgato Key Light."""
"""Defines an Elgato Light."""
def __init__(
self,
elgato: Elgato,
info: Info,
) -> None:
"""Initialize Elgato Key Light."""
self._info: Info = info
def __init__(self, elgato: Elgato, info: Info, settings: Settings) -> None:
"""Initialize Elgato Light."""
self._info = info
self._settings = settings
self._state: State | None = None
self.elgato = elgato
self._min_mired = 143
self._max_mired = 344
self._supported_color_modes = {COLOR_MODE_COLOR_TEMP}
# Elgato Light supporting color, have a different temperature range
if settings.power_on_hue is not None:
self._supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS}
self._min_mired = 153
self._max_mired = 285
@property
def name(self) -> str:
"""Return the name of the entity."""
@ -99,17 +108,38 @@ class ElgatoLight(LightEntity):
@property
def min_mireds(self) -> int:
"""Return the coldest color_temp that this light supports."""
return 143
return self._min_mired
@property
def max_mireds(self) -> int:
"""Return the warmest color_temp that this light supports."""
return 344
# Elgato lights with color capabilities have a different highest value
return self._max_mired
@property
def supported_features(self) -> int:
"""Flag supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
def supported_color_modes(self) -> set[str]:
"""Flag supported color modes."""
return self._supported_color_modes
@property
def color_mode(self) -> str | None:
"""Return the color mode of the light."""
if self._state and self._state.hue is not None:
return COLOR_MODE_HS
return COLOR_MODE_COLOR_TEMP
@property
def hs_color(self) -> tuple[float, float] | None:
"""Return the hue and saturation color value [float, float]."""
if (
self._state is None
or self._state.hue is None
or self._state.saturation is None
):
return None
return (self._state.hue, self._state.saturation)
@property
def is_on(self) -> bool:
@ -122,22 +152,44 @@ class ElgatoLight(LightEntity):
try:
await self.elgato.light(on=False)
except ElgatoError:
_LOGGER.error("An error occurred while updating the Elgato Key Light")
_LOGGER.error("An error occurred while updating the Elgato Light")
self._state = None
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the light."""
temperature = kwargs.get(ATTR_COLOR_TEMP)
hue = None
saturation = None
if ATTR_HS_COLOR in kwargs:
hue, saturation = kwargs[ATTR_HS_COLOR]
brightness = None
if ATTR_BRIGHTNESS in kwargs:
brightness = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100)
# For Elgato lights supporting color mode, but in temperature mode;
# adjusting only brightness make them jump back to color mode.
# Resending temperature prevents that.
if (
brightness
and ATTR_HS_COLOR not in kwargs
and ATTR_COLOR_TEMP not in kwargs
and COLOR_MODE_HS in self.supported_color_modes
and self.color_mode == COLOR_MODE_COLOR_TEMP
):
temperature = self.color_temp
try:
await self.elgato.light(
on=True, brightness=brightness, temperature=temperature
on=True,
brightness=brightness,
hue=hue,
saturation=saturation,
temperature=temperature,
)
except ElgatoError:
_LOGGER.error("An error occurred while updating the Elgato Key Light")
_LOGGER.error("An error occurred while updating the Elgato Light")
self._state = None
async def async_update(self) -> None:
@ -149,12 +201,12 @@ class ElgatoLight(LightEntity):
_LOGGER.info("Connection restored")
except ElgatoError as err:
meth = _LOGGER.error if self._state else _LOGGER.debug
meth("An error occurred while updating the Elgato Key Light: %s", err)
meth("An error occurred while updating the Elgato Light: %s", err)
self._state = None
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Elgato Key Light."""
"""Return device information about this Elgato Light."""
return {
ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)},
ATTR_NAME: self._info.product_name,

View File

@ -1,6 +1,6 @@
{
"domain": "elgato",
"name": "Elgato Key Light",
"name": "Elgato Light",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/elgato",
"requirements": ["elgato==2.1.0"],

View File

@ -1,17 +1,17 @@
{
"config": {
"flow_title": "Elgato Key Light: {serial_number}",
"flow_title": "Elgato Light: {serial_number}",
"step": {
"user": {
"description": "Set up your Elgato Key Light to integrate with Home Assistant.",
"description": "Set up your Elgato Light to integrate with Home Assistant.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?",
"title": "Discovered Elgato Key Light device"
"description": "Do you want to add the Elgato Light with serial number `{serial_number}` to Home Assistant?",
"title": "Discovered Elgato Light device"
}
},
"error": {

View File

@ -7,18 +7,18 @@
"error": {
"cannot_connect": "Failed to connect"
},
"flow_title": "Elgato Key Light: {serial_number}",
"flow_title": "Elgato Light: {serial_number}",
"step": {
"user": {
"data": {
"host": "Host",
"port": "Port"
},
"description": "Set up your Elgato Key Light to integrate with Home Assistant."
"description": "Set up your Elgato Light to integrate with Home Assistant."
},
"zeroconf_confirm": {
"description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?",
"title": "Discovered Elgato Key Light device"
"description": "Do you want to add the Elgato Light with serial number `{serial_number}` to Home Assistant?",
"title": "Discovered Elgato Light device"
}
}
}

View File

@ -12,6 +12,8 @@ async def init_integration(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
skip_setup: bool = False,
color: bool = False,
mode_color: bool = False,
) -> MockConfigEntry:
"""Set up the Elgato Key Light integration in Home Assistant."""
aioclient_mock.get(
@ -20,24 +22,38 @@ async def init_integration(
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.put(
"http://127.0.0.1:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://127.0.0.1:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.get(
"http://127.0.0.2:9123/elgato/accessory-info",
text=load_fixture("elgato/info.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
settings = "elgato/settings.json"
if color:
settings = "elgato/settings-color.json"
aioclient_mock.get(
"http://127.0.0.1:9123/elgato/lights/settings",
text=load_fixture(settings),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
state = "elgato/state.json"
if mode_color:
state = "elgato/state-color.json"
aioclient_mock.get(
"http://127.0.0.1:9123/elgato/lights",
text=load_fixture(state),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
aioclient_mock.put(
"http://127.0.0.1:9123/elgato/lights",
text=load_fixture("elgato/state.json"),
headers={"Content-Type": CONTENT_TYPE_JSON},
)
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="CN11A1A00001",

View File

@ -2,11 +2,19 @@
from unittest.mock import patch
from elgato import ElgatoError
import pytest
from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
ATTR_MAX_MIREDS,
ATTR_MIN_MIREDS,
ATTR_SUPPORTED_COLOR_MODES,
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
DOMAIN as LIGHT_DOMAIN,
)
from homeassistant.const import (
@ -24,10 +32,10 @@ from tests.components.elgato import init_integration
from tests.test_util.aiohttp import AiohttpClientMocker
async def test_light_state(
async def test_light_state_temperature(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the creation and values of the Elgato Key Lights."""
"""Test the creation and values of the Elgato Lights in temperature mode."""
await init_integration(hass, aioclient_mock)
entity_registry = er.async_get(hass)
@ -37,6 +45,11 @@ async def test_light_state(
assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 54
assert state.attributes.get(ATTR_COLOR_TEMP) == 297
assert state.attributes.get(ATTR_HS_COLOR) is None
assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_COLOR_TEMP
assert state.attributes.get(ATTR_MIN_MIREDS) == 143
assert state.attributes.get(ATTR_MAX_MIREDS) == 344
assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_COLOR_TEMP]
assert state.state == STATE_ON
entry = entity_registry.async_get("light.frenck")
@ -44,13 +57,42 @@ async def test_light_state(
assert entry.unique_id == "CN11A1A00001"
async def test_light_change_state(
async def test_light_state_color(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the creation and values of the Elgato Lights in temperature mode."""
await init_integration(hass, aioclient_mock, color=True, mode_color=True)
entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("light.frenck")
assert state
assert state.attributes.get(ATTR_BRIGHTNESS) == 128
assert state.attributes.get(ATTR_COLOR_TEMP) is None
assert state.attributes.get(ATTR_HS_COLOR) == (358.0, 6.0)
assert state.attributes.get(ATTR_MIN_MIREDS) == 153
assert state.attributes.get(ATTR_MAX_MIREDS) == 285
assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_HS
assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [
COLOR_MODE_COLOR_TEMP,
COLOR_MODE_HS,
]
assert state.state == STATE_ON
entry = entity_registry.async_get("light.frenck")
assert entry
assert entry.unique_id == "CN11A1A00001"
async def test_light_change_state_temperature(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test the change of state of a Elgato Key Light device."""
await init_integration(hass, aioclient_mock)
await init_integration(hass, aioclient_mock, color=True, mode_color=False)
state = hass.states.get("light.frenck")
assert state
assert state.state == STATE_ON
with patch(
@ -69,12 +111,25 @@ async def test_light_change_state(
)
await hass.async_block_till_done()
assert len(mock_light.mock_calls) == 1
mock_light.assert_called_with(on=True, brightness=100, temperature=100)
mock_light.assert_called_with(
on=True, brightness=100, temperature=100, hue=None, saturation=None
)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.frenck",
ATTR_BRIGHTNESS: 255,
},
blocking=True,
)
await hass.async_block_till_done()
assert len(mock_light.mock_calls) == 2
mock_light.assert_called_with(
on=True, brightness=100, temperature=297, hue=None, saturation=None
)
with patch(
"homeassistant.components.elgato.light.Elgato.light",
return_value=mock_coro(),
) as mock_light:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
@ -82,14 +137,46 @@ async def test_light_change_state(
blocking=True,
)
await hass.async_block_till_done()
assert len(mock_light.mock_calls) == 1
assert len(mock_light.mock_calls) == 3
mock_light.assert_called_with(on=False)
async def test_light_unavailable(
async def test_light_change_state_color(
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test error/unavailable handling of an Elgato Key Light."""
"""Test the color state state of a Elgato Light device."""
await init_integration(hass, aioclient_mock, color=True)
state = hass.states.get("light.frenck")
assert state
assert state.state == STATE_ON
with patch(
"homeassistant.components.elgato.light.Elgato.light",
return_value=mock_coro(),
) as mock_light:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "light.frenck",
ATTR_BRIGHTNESS: 255,
ATTR_HS_COLOR: (10.1, 20.2),
},
blocking=True,
)
await hass.async_block_till_done()
assert len(mock_light.mock_calls) == 1
mock_light.assert_called_with(
on=True, brightness=100, temperature=None, hue=10.1, saturation=20.2
)
@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF])
async def test_light_unavailable(
service: str, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
) -> None:
"""Test error/unavailable handling of an Elgato Light."""
await init_integration(hass, aioclient_mock)
with patch(
"homeassistant.components.elgato.light.Elgato.light",
@ -100,7 +187,7 @@ async def test_light_unavailable(
):
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
service,
{ATTR_ENTITY_ID: "light.frenck"},
blocking=True,
)

View File

@ -0,0 +1,10 @@
{
"powerOnBehavior": 2,
"powerOnHue": 40.0,
"powerOnSaturation": 15.0,
"powerOnBrightness": 40,
"powerOnTemperature": 0,
"switchOnDurationMs": 150,
"switchOffDurationMs": 400,
"colorChangeDurationMs": 150
}

8
tests/fixtures/elgato/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"powerOnBehavior": 1,
"powerOnBrightness": 20,
"powerOnTemperature": 213,
"switchOnDurationMs": 100,
"switchOffDurationMs": 300,
"colorChangeDurationMs": 100
}

11
tests/fixtures/elgato/state-color.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"numberOfLights": 1,
"lights": [
{
"on": 1,
"hue": 358.0,
"saturation": 6.0,
"brightness": 50
}
]
}