mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add an option to hide selected Hyperion effects (#45689)
This commit is contained in:
parent
286217f771
commit
781084880b
@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
CONF_TOKEN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import create_hyperion_client
|
||||
@ -34,6 +35,8 @@ from . import create_hyperion_client
|
||||
from .const import (
|
||||
CONF_AUTH_ID,
|
||||
CONF_CREATE_TOKEN,
|
||||
CONF_EFFECT_HIDE_LIST,
|
||||
CONF_EFFECT_SHOW_LIST,
|
||||
CONF_PRIORITY,
|
||||
DEFAULT_ORIGIN,
|
||||
DEFAULT_PRIORITY,
|
||||
@ -439,13 +442,44 @@ class HyperionOptionsFlow(OptionsFlow):
|
||||
"""Initialize a Hyperion options flow."""
|
||||
self._config_entry = config_entry
|
||||
|
||||
def _create_client(self) -> client.HyperionClient:
|
||||
"""Create and connect a client instance."""
|
||||
return create_hyperion_client(
|
||||
self._config_entry.data[CONF_HOST],
|
||||
self._config_entry.data[CONF_PORT],
|
||||
token=self._config_entry.data.get(CONF_TOKEN),
|
||||
)
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Manage the options."""
|
||||
|
||||
effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES}
|
||||
async with self._create_client() as hyperion_client:
|
||||
if not hyperion_client:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
for effect in hyperion_client.effects or []:
|
||||
if const.KEY_NAME in effect:
|
||||
effects[effect[const.KEY_NAME]] = effect[const.KEY_NAME]
|
||||
|
||||
# If a new effect is added to Hyperion, we always want it to show by default. So
|
||||
# rather than store a 'show list' in the config entry, we store a 'hide list'.
|
||||
# However, it's more intuitive to ask the user to select which effects to show,
|
||||
# so we inverse the meaning prior to storage.
|
||||
|
||||
if user_input is not None:
|
||||
effect_show_list = user_input.pop(CONF_EFFECT_SHOW_LIST)
|
||||
user_input[CONF_EFFECT_HIDE_LIST] = sorted(
|
||||
set(effects) - set(effect_show_list)
|
||||
)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
default_effect_show_list = list(
|
||||
set(effects)
|
||||
- set(self._config_entry.options.get(CONF_EFFECT_HIDE_LIST, []))
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
@ -456,6 +490,10 @@ class HyperionOptionsFlow(OptionsFlow):
|
||||
CONF_PRIORITY, DEFAULT_PRIORITY
|
||||
),
|
||||
): vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
|
||||
vol.Optional(
|
||||
CONF_EFFECT_SHOW_LIST,
|
||||
default=default_effect_show_list,
|
||||
): cv.multi_select(effects),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@ -31,6 +31,8 @@ CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS"
|
||||
CONF_ON_UNLOAD = "ON_UNLOAD"
|
||||
CONF_PRIORITY = "priority"
|
||||
CONF_ROOT_CLIENT = "ROOT_CLIENT"
|
||||
CONF_EFFECT_HIDE_LIST = "effect_hide_list"
|
||||
CONF_EFFECT_SHOW_LIST = "effect_show_list"
|
||||
|
||||
DEFAULT_NAME = "Hyperion"
|
||||
DEFAULT_ORIGIN = "Home Assistant"
|
||||
|
@ -28,6 +28,7 @@ import homeassistant.util.color as color_util
|
||||
|
||||
from . import get_hyperion_unique_id, listen_for_instance_updates
|
||||
from .const import (
|
||||
CONF_EFFECT_HIDE_LIST,
|
||||
CONF_INSTANCE_CLIENTS,
|
||||
CONF_PRIORITY,
|
||||
DEFAULT_ORIGIN,
|
||||
@ -217,7 +218,10 @@ class HyperionBaseLight(LightEntity):
|
||||
|
||||
def _get_option(self, key: str) -> Any:
|
||||
"""Get a value from the provided options."""
|
||||
defaults = {CONF_PRIORITY: DEFAULT_PRIORITY}
|
||||
defaults = {
|
||||
CONF_PRIORITY: DEFAULT_PRIORITY,
|
||||
CONF_EFFECT_HIDE_LIST: [],
|
||||
}
|
||||
return self._options.get(key, defaults[key])
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
@ -366,11 +370,17 @@ class HyperionBaseLight(LightEntity):
|
||||
if not self._client.effects:
|
||||
return
|
||||
effect_list: list[str] = []
|
||||
hide_effects = self._get_option(CONF_EFFECT_HIDE_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 = self._static_effect_list + effect_list
|
||||
effect_name = effect[const.KEY_NAME]
|
||||
if effect_name not in hide_effects:
|
||||
effect_list.append(effect_name)
|
||||
|
||||
self._effect_list = [
|
||||
effect for effect in self._static_effect_list if effect not in hide_effects
|
||||
] + effect_list
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
|
@ -45,7 +45,8 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"priority": "Hyperion priority to use for colors and effects"
|
||||
"priority": "Hyperion priority to use for colors and effects",
|
||||
"effect_show_list": "Hyperion effects to show"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +118,9 @@ def create_mock_client() -> Mock:
|
||||
|
||||
|
||||
def add_test_config_entry(
|
||||
hass: HomeAssistantType, data: dict[str, Any] | None = None
|
||||
hass: HomeAssistantType,
|
||||
data: dict[str, Any] | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> ConfigEntry:
|
||||
"""Add a test config entry."""
|
||||
config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call]
|
||||
@ -131,7 +133,7 @@ def add_test_config_entry(
|
||||
},
|
||||
title=f"Hyperion {TEST_SYSINFO_ID}",
|
||||
unique_id=TEST_SYSINFO_ID,
|
||||
options=TEST_CONFIG_ENTRY_OPTIONS,
|
||||
options=options or TEST_CONFIG_ENTRY_OPTIONS,
|
||||
)
|
||||
config_entry.add_to_hass(hass) # type: ignore[no-untyped-call]
|
||||
return config_entry
|
||||
@ -141,9 +143,10 @@ async def setup_test_config_entry(
|
||||
hass: HomeAssistantType,
|
||||
config_entry: ConfigEntry | None = None,
|
||||
hyperion_client: Mock | None = None,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> ConfigEntry:
|
||||
"""Add a test Hyperion entity to hass."""
|
||||
config_entry = config_entry or add_test_config_entry(hass)
|
||||
config_entry = config_entry or add_test_config_entry(hass, options=options)
|
||||
|
||||
hyperion_client = hyperion_client or create_mock_client()
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Tests for the Hyperion config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from hyperion import const
|
||||
|
||||
@ -10,6 +11,8 @@ from homeassistant import data_entry_flow
|
||||
from homeassistant.components.hyperion.const import (
|
||||
CONF_AUTH_ID,
|
||||
CONF_CREATE_TOKEN,
|
||||
CONF_EFFECT_HIDE_LIST,
|
||||
CONF_EFFECT_SHOW_LIST,
|
||||
CONF_PRIORITY,
|
||||
DOMAIN,
|
||||
)
|
||||
@ -309,23 +312,42 @@ async def test_auth_static_token_success(hass: HomeAssistantType) -> None:
|
||||
}
|
||||
|
||||
|
||||
async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None:
|
||||
"""Test correct behavior with a bad static token."""
|
||||
async def test_auth_static_token_login_connect_fail(hass: HomeAssistantType) -> None:
|
||||
"""Test correct behavior with a static token that cannot connect."""
|
||||
result = await _init_flow(hass)
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
client = create_mock_client()
|
||||
client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
|
||||
|
||||
# Fail the login call.
|
||||
client.async_login = AsyncMock(
|
||||
return_value={"command": "authorize-login", "success": False, "tan": 0}
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
|
||||
client.async_client_connect = AsyncMock(return_value=False)
|
||||
result = await _configure_flow(
|
||||
hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_auth_static_token_login_fail(hass: HomeAssistantType) -> None:
|
||||
"""Test correct behavior with a static token that cannot login."""
|
||||
result = await _init_flow(hass)
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
client = create_mock_client()
|
||||
client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
|
||||
client.async_login = AsyncMock(
|
||||
return_value={"command": "authorize-login", "success": False, "tan": 0}
|
||||
)
|
||||
result = await _configure_flow(
|
||||
hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN}
|
||||
)
|
||||
@ -377,6 +399,66 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistantType) -> N
|
||||
assert result["reason"] == "auth_new_token_not_granted_error"
|
||||
|
||||
|
||||
async def test_auth_create_token_approval_declined_task_canceled(
|
||||
hass: HomeAssistantType,
|
||||
) -> None:
|
||||
"""Verify correct behaviour when a token request is declined."""
|
||||
result = await _init_flow(hass)
|
||||
|
||||
client = create_mock_client()
|
||||
client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
|
||||
assert result["step_id"] == "auth"
|
||||
|
||||
client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL)
|
||||
|
||||
class CanceledAwaitableMock(AsyncMock):
|
||||
"""A canceled awaitable mock."""
|
||||
|
||||
def __await__(self):
|
||||
raise asyncio.CancelledError
|
||||
|
||||
mock_task = CanceledAwaitableMock()
|
||||
task_coro = None
|
||||
|
||||
def create_task(arg):
|
||||
nonlocal task_coro
|
||||
task_coro = arg
|
||||
return mock_task
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
), patch(
|
||||
"homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
|
||||
return_value=TEST_AUTH_ID,
|
||||
), patch.object(
|
||||
hass, "async_create_task", side_effect=create_task
|
||||
):
|
||||
result = await _configure_flow(
|
||||
hass, result, user_input={CONF_CREATE_TOKEN: True}
|
||||
)
|
||||
assert result["step_id"] == "create_token"
|
||||
|
||||
result = await _configure_flow(hass, result)
|
||||
assert result["step_id"] == "create_token_external"
|
||||
|
||||
# Leave the task running, to ensure it is canceled.
|
||||
mock_task.done = Mock(return_value=False)
|
||||
mock_task.cancel = Mock()
|
||||
|
||||
result = await _configure_flow(hass, result)
|
||||
|
||||
# This await will advance to the next step.
|
||||
await task_coro
|
||||
|
||||
# Assert that cancel is called on the task.
|
||||
assert mock_task.cancel.called
|
||||
|
||||
|
||||
async def test_auth_create_token_when_issued_token_fails(
|
||||
hass: HomeAssistantType,
|
||||
) -> None:
|
||||
@ -468,6 +550,47 @@ async def test_auth_create_token_success(hass: HomeAssistantType) -> None:
|
||||
}
|
||||
|
||||
|
||||
async def test_auth_create_token_success_but_login_fail(
|
||||
hass: HomeAssistantType,
|
||||
) -> None:
|
||||
"""Verify correct behaviour when a token is successfully created but the login fails."""
|
||||
result = await _init_flow(hass)
|
||||
|
||||
client = create_mock_client()
|
||||
client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT)
|
||||
assert result["step_id"] == "auth"
|
||||
|
||||
client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS)
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
), patch(
|
||||
"homeassistant.components.hyperion.config_flow.client.generate_random_auth_id",
|
||||
return_value=TEST_AUTH_ID,
|
||||
):
|
||||
result = await _configure_flow(
|
||||
hass, result, user_input={CONF_CREATE_TOKEN: True}
|
||||
)
|
||||
assert result["step_id"] == "create_token"
|
||||
|
||||
result = await _configure_flow(hass, result)
|
||||
assert result["step_id"] == "create_token_external"
|
||||
|
||||
client.async_login = AsyncMock(
|
||||
return_value={"command": "authorize-login", "success": False, "tan": 0}
|
||||
)
|
||||
|
||||
# The flow will be automatically advanced by the auth token response.
|
||||
result = await _configure_flow(hass, result)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "auth_new_token_not_work_error"
|
||||
|
||||
|
||||
async def test_ssdp_success(hass: HomeAssistantType) -> None:
|
||||
"""Check an SSDP flow."""
|
||||
|
||||
@ -599,8 +722,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistantType) -> None:
|
||||
assert result_2["reason"] == "already_in_progress"
|
||||
|
||||
|
||||
async def test_options(hass: HomeAssistantType) -> None:
|
||||
"""Check an options flow."""
|
||||
async def test_options_priority(hass: HomeAssistantType) -> None:
|
||||
"""Check an options flow priority option."""
|
||||
|
||||
config_entry = add_test_config_entry(hass)
|
||||
|
||||
@ -618,11 +741,12 @@ async def test_options(hass: HomeAssistantType) -> None:
|
||||
|
||||
new_priority = 1
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_PRIORITY: new_priority}
|
||||
result["flow_id"],
|
||||
user_input={CONF_PRIORITY: new_priority},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"] == {CONF_PRIORITY: new_priority}
|
||||
assert result["data"][CONF_PRIORITY] == new_priority
|
||||
|
||||
# Turn the light on and ensure the new priority is used.
|
||||
client.async_send_set_color = AsyncMock(return_value=True)
|
||||
@ -636,6 +760,59 @@ async def test_options(hass: HomeAssistantType) -> None:
|
||||
assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority
|
||||
|
||||
|
||||
async def test_options_effect_show_list(hass: HomeAssistantType) -> None:
|
||||
"""Check an options flow effect show list."""
|
||||
|
||||
config_entry = add_test_config_entry(hass)
|
||||
|
||||
client = create_mock_client()
|
||||
client.effects = [
|
||||
{const.KEY_NAME: "effect1"},
|
||||
{const.KEY_NAME: "effect2"},
|
||||
{const.KEY_NAME: "effect3"},
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
# effect1 and effect3 only, so effect2 & external sources are hidden.
|
||||
assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted(
|
||||
["effect2"] + const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
||||
)
|
||||
|
||||
|
||||
async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistantType) -> None:
|
||||
"""Check an options flow effect hide list with a failed connection."""
|
||||
|
||||
config_entry = add_test_config_entry(hass)
|
||||
client = create_mock_client()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.hyperion.client.HyperionClient", return_value=client
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
client.async_client_connect = AsyncMock(return_value=False)
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_reauth_success(hass: HomeAssistantType) -> None:
|
||||
"""Check a reauth flow that succeeds."""
|
||||
|
||||
|
@ -6,7 +6,11 @@ from unittest.mock import AsyncMock, Mock, call, patch
|
||||
from hyperion import const
|
||||
|
||||
from homeassistant.components.hyperion import light as hyperion_light
|
||||
from homeassistant.components.hyperion.const import DEFAULT_ORIGIN, DOMAIN
|
||||
from homeassistant.components.hyperion.const import (
|
||||
CONF_EFFECT_HIDE_LIST,
|
||||
DEFAULT_ORIGIN,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_EFFECT,
|
||||
@ -1129,3 +1133,21 @@ async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -
|
||||
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||
assert entity_state
|
||||
assert entity_state.attributes["effect_list"] == [hyperion_light.KEY_EFFECT_SOLID]
|
||||
|
||||
|
||||
async def test_light_option_effect_hide_list(hass: HomeAssistantType) -> None:
|
||||
"""Test the effect_hide_list option."""
|
||||
client = create_mock_client()
|
||||
client.effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
|
||||
|
||||
await setup_test_config_entry(
|
||||
hass, hyperion_client=client, options={CONF_EFFECT_HIDE_LIST: ["Two", "V4L"]}
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||
assert entity_state.attributes["effect_list"] == [
|
||||
"Solid",
|
||||
"BOBLIGHTSERVER",
|
||||
"GRABBER",
|
||||
"One",
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user