mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add advanced Hyperion entities (#45410)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
06ade6129c
commit
890eaf840c
@ -2,29 +2,40 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast
|
||||||
|
|
||||||
from hyperion import client, const as hyperion_const
|
from hyperion import client, const as hyperion_const
|
||||||
from pkg_resources import parse_version
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN
|
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_registry import (
|
||||||
|
async_entries_for_config_entry,
|
||||||
|
async_get_registry,
|
||||||
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_INSTANCE_CLIENTS,
|
||||||
CONF_ON_UNLOAD,
|
CONF_ON_UNLOAD,
|
||||||
CONF_ROOT_CLIENT,
|
CONF_ROOT_CLIENT,
|
||||||
|
DEFAULT_NAME,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
HYPERION_RELEASES_URL,
|
HYPERION_RELEASES_URL,
|
||||||
HYPERION_VERSION_WARN_CUTOFF,
|
HYPERION_VERSION_WARN_CUTOFF,
|
||||||
SIGNAL_INSTANCES_UPDATED,
|
SIGNAL_INSTANCE_ADD,
|
||||||
|
SIGNAL_INSTANCE_REMOVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [LIGHT_DOMAIN]
|
PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -59,6 +70,17 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str:
|
|||||||
return f"{server_id}_{instance}_{name}"
|
return f"{server_id}_{instance}_{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def split_hyperion_unique_id(unique_id: str) -> Optional[Tuple[str, int, str]]:
|
||||||
|
"""Split a unique_id into a (server_id, instance, type) tuple."""
|
||||||
|
data = tuple(unique_id.split("_", 2))
|
||||||
|
if len(data) != 3:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return (data[0], int(data[1]), data[2])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_hyperion_client(
|
def create_hyperion_client(
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
@ -96,6 +118,31 @@ async def _create_reauth_flow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def listen_for_instance_updates(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
add_func: Callable,
|
||||||
|
remove_func: Callable,
|
||||||
|
) -> None:
|
||||||
|
"""Listen for instance additions/removals."""
|
||||||
|
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].extend(
|
||||||
|
[
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
SIGNAL_INSTANCE_ADD.format(config_entry.entry_id),
|
||||||
|
add_func,
|
||||||
|
),
|
||||||
|
async_dispatcher_connect(
|
||||||
|
hass,
|
||||||
|
SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id),
|
||||||
|
remove_func,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
"""Set up Hyperion from a config entry."""
|
"""Set up Hyperion from a config entry."""
|
||||||
host = config_entry.data[CONF_HOST]
|
host = config_entry.data[CONF_HOST]
|
||||||
@ -151,23 +198,86 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
await hyperion_client.async_client_disconnect()
|
await hyperion_client.async_client_disconnect()
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
hyperion_client.set_callbacks(
|
# We need 1 root client (to manage instances being removed/added) and then 1 client
|
||||||
{
|
# per Hyperion server instance which is shared for all entities associated with
|
||||||
f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: (
|
# that instance.
|
||||||
async_dispatcher_send(
|
|
||||||
hass,
|
|
||||||
SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id),
|
|
||||||
json,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||||
CONF_ROOT_CLIENT: hyperion_client,
|
CONF_ROOT_CLIENT: hyperion_client,
|
||||||
|
CONF_INSTANCE_CLIENTS: {},
|
||||||
CONF_ON_UNLOAD: [],
|
CONF_ON_UNLOAD: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_instances_to_clients(response: Dict[str, Any]) -> None:
|
||||||
|
"""Convert instances to Hyperion clients."""
|
||||||
|
if not response or hyperion_const.KEY_DATA not in response:
|
||||||
|
return
|
||||||
|
await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA])
|
||||||
|
|
||||||
|
async def async_instances_to_clients_raw(instances: List[Dict[str, Any]]) -> None:
|
||||||
|
"""Convert instances to Hyperion clients."""
|
||||||
|
registry = await async_get_registry(hass)
|
||||||
|
running_instances: Set[int] = set()
|
||||||
|
stopped_instances: Set[int] = set()
|
||||||
|
existing_instances = hass.data[DOMAIN][config_entry.entry_id][
|
||||||
|
CONF_INSTANCE_CLIENTS
|
||||||
|
]
|
||||||
|
server_id = cast(str, config_entry.unique_id)
|
||||||
|
|
||||||
|
# In practice, an instance can be in 3 states as seen by this function:
|
||||||
|
#
|
||||||
|
# * Exists, and is running: Should be present in HASS/registry.
|
||||||
|
# * Exists, but is not running: Cannot add it yet, but entity may have be
|
||||||
|
# registered from a previous time it was running.
|
||||||
|
# * No longer exists at all: Should not be present in HASS/registry.
|
||||||
|
|
||||||
|
# Add instances that are missing.
|
||||||
|
for instance in instances:
|
||||||
|
instance_num = instance.get(hyperion_const.KEY_INSTANCE)
|
||||||
|
if instance_num is None:
|
||||||
|
continue
|
||||||
|
if not instance.get(hyperion_const.KEY_RUNNING, False):
|
||||||
|
stopped_instances.add(instance_num)
|
||||||
|
continue
|
||||||
|
running_instances.add(instance_num)
|
||||||
|
if instance_num in existing_instances:
|
||||||
|
continue
|
||||||
|
hyperion_client = await async_create_connect_hyperion_client(
|
||||||
|
host, port, instance=instance_num, token=token
|
||||||
|
)
|
||||||
|
if not hyperion_client:
|
||||||
|
continue
|
||||||
|
existing_instances[instance_num] = hyperion_client
|
||||||
|
instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME)
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass,
|
||||||
|
SIGNAL_INSTANCE_ADD.format(config_entry.entry_id),
|
||||||
|
instance_num,
|
||||||
|
instance_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove entities that are are not running instances on Hyperion.
|
||||||
|
for instance_num in set(existing_instances) - running_instances:
|
||||||
|
del existing_instances[instance_num]
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deregister entities that belong to removed instances.
|
||||||
|
for entry in async_entries_for_config_entry(registry, config_entry.entry_id):
|
||||||
|
data = split_hyperion_unique_id(entry.unique_id)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
if data[0] == server_id and (
|
||||||
|
data[1] not in running_instances and data[1] not in stopped_instances
|
||||||
|
):
|
||||||
|
registry.async_remove(entry.entity_id)
|
||||||
|
|
||||||
|
hyperion_client.set_callbacks(
|
||||||
|
{
|
||||||
|
f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": async_instances_to_clients,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Must only listen for option updates after the setup is complete, as otherwise
|
# Must only listen for option updates after the setup is complete, as otherwise
|
||||||
# the YAML->ConfigEntry migration code triggers an options update, which causes a
|
# the YAML->ConfigEntry migration code triggers an options update, which causes a
|
||||||
# reload -- which clashes with the initial load (causing entity_id / unique_id
|
# reload -- which clashes with the initial load (causing entity_id / unique_id
|
||||||
@ -179,6 +289,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
for component in PLATFORMS
|
for component in PLATFORMS
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
assert hyperion_client
|
||||||
|
await async_instances_to_clients_raw(hyperion_client.instances)
|
||||||
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
|
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
|
||||||
config_entry.add_update_listener(_async_entry_updated)
|
config_entry.add_update_listener(_async_entry_updated)
|
||||||
)
|
)
|
||||||
@ -210,6 +322,18 @@ async def async_unload_entry(
|
|||||||
config_data = hass.data[DOMAIN].pop(config_entry.entry_id)
|
config_data = hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||||
for func in config_data[CONF_ON_UNLOAD]:
|
for func in config_data[CONF_ON_UNLOAD]:
|
||||||
func()
|
func()
|
||||||
|
|
||||||
|
# Disconnect the shared instance clients.
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
config_data[CONF_INSTANCE_CLIENTS][
|
||||||
|
instance_num
|
||||||
|
].async_client_disconnect()
|
||||||
|
for instance_num in config_data[CONF_INSTANCE_CLIENTS]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Disconnect the root client.
|
||||||
root_client = config_data[CONF_ROOT_CLIENT]
|
root_client = config_data[CONF_ROOT_CLIENT]
|
||||||
await root_client.async_client_disconnect()
|
await root_client.async_client_disconnect()
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
@ -1,8 +1,33 @@
|
|||||||
"""Constants for Hyperion integration."""
|
"""Constants for Hyperion integration."""
|
||||||
|
|
||||||
|
from hyperion.const import (
|
||||||
|
KEY_COMPONENTID_ALL,
|
||||||
|
KEY_COMPONENTID_BLACKBORDER,
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||||
|
KEY_COMPONENTID_FORWARDER,
|
||||||
|
KEY_COMPONENTID_GRABBER,
|
||||||
|
KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
KEY_COMPONENTID_SMOOTHING,
|
||||||
|
KEY_COMPONENTID_V4L,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Maps between Hyperion API component names to Hyperion UI names. This allows Home
|
||||||
|
# Assistant to use names that match what Hyperion users may expect from the Hyperion UI.
|
||||||
|
COMPONENT_TO_NAME = {
|
||||||
|
KEY_COMPONENTID_ALL: "All",
|
||||||
|
KEY_COMPONENTID_SMOOTHING: "Smoothing",
|
||||||
|
KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection",
|
||||||
|
KEY_COMPONENTID_FORWARDER: "Forwarder",
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server",
|
||||||
|
KEY_COMPONENTID_GRABBER: "Platform Capture",
|
||||||
|
KEY_COMPONENTID_LEDDEVICE: "LED Device",
|
||||||
|
KEY_COMPONENTID_V4L: "USB Capture",
|
||||||
|
}
|
||||||
|
|
||||||
CONF_AUTH_ID = "auth_id"
|
CONF_AUTH_ID = "auth_id"
|
||||||
CONF_CREATE_TOKEN = "create_token"
|
CONF_CREATE_TOKEN = "create_token"
|
||||||
CONF_INSTANCE = "instance"
|
CONF_INSTANCE = "instance"
|
||||||
|
CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS"
|
||||||
CONF_ON_UNLOAD = "ON_UNLOAD"
|
CONF_ON_UNLOAD = "ON_UNLOAD"
|
||||||
CONF_PRIORITY = "priority"
|
CONF_PRIORITY = "priority"
|
||||||
CONF_ROOT_CLIENT = "ROOT_CLIENT"
|
CONF_ROOT_CLIENT = "ROOT_CLIENT"
|
||||||
@ -16,7 +41,14 @@ DOMAIN = "hyperion"
|
|||||||
HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases"
|
HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases"
|
||||||
HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"
|
HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9"
|
||||||
|
|
||||||
SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}"
|
NAME_SUFFIX_HYPERION_LIGHT = ""
|
||||||
SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}"
|
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT = "Priority"
|
||||||
|
NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component"
|
||||||
|
|
||||||
|
SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}"
|
||||||
|
SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}"
|
||||||
|
SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}"
|
||||||
|
|
||||||
TYPE_HYPERION_LIGHT = "hyperion_light"
|
TYPE_HYPERION_LIGHT = "hyperion_light"
|
||||||
|
TYPE_HYPERION_PRIORITY_LIGHT = "hyperion_priority_light"
|
||||||
|
TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch"
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
from hyperion import client, const
|
from hyperion import client, const
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -22,17 +22,15 @@ from homeassistant.components.light import (
|
|||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||||
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import PlatformNotReady
|
from homeassistant.exceptions import PlatformNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect,
|
async_dispatcher_connect,
|
||||||
async_dispatcher_send,
|
async_dispatcher_send,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_registry import (
|
from homeassistant.helpers.entity_registry import async_get_registry
|
||||||
async_entries_for_config_entry,
|
|
||||||
async_get_registry,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import (
|
from homeassistant.helpers.typing import (
|
||||||
ConfigType,
|
ConfigType,
|
||||||
DiscoveryInfoType,
|
DiscoveryInfoType,
|
||||||
@ -41,24 +39,27 @@ from homeassistant.helpers.typing import (
|
|||||||
import homeassistant.util.color as color_util
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
async_create_connect_hyperion_client,
|
|
||||||
create_hyperion_client,
|
create_hyperion_client,
|
||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
|
listen_for_instance_updates,
|
||||||
)
|
)
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ON_UNLOAD,
|
CONF_INSTANCE_CLIENTS,
|
||||||
CONF_PRIORITY,
|
CONF_PRIORITY,
|
||||||
CONF_ROOT_CLIENT,
|
|
||||||
DEFAULT_ORIGIN,
|
DEFAULT_ORIGIN,
|
||||||
DEFAULT_PRIORITY,
|
DEFAULT_PRIORITY,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SIGNAL_INSTANCE_REMOVED,
|
NAME_SUFFIX_HYPERION_LIGHT,
|
||||||
SIGNAL_INSTANCES_UPDATED,
|
NAME_SUFFIX_HYPERION_PRIORITY_LIGHT,
|
||||||
|
SIGNAL_ENTITY_REMOVE,
|
||||||
TYPE_HYPERION_LIGHT,
|
TYPE_HYPERION_LIGHT,
|
||||||
|
TYPE_HYPERION_PRIORITY_LIGHT,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
COLOR_BLACK = color_util.COLORS["black"]
|
||||||
|
|
||||||
CONF_DEFAULT_COLOR = "default_color"
|
CONF_DEFAULT_COLOR = "default_color"
|
||||||
CONF_HDMI_PRIORITY = "hdmi_priority"
|
CONF_HDMI_PRIORITY = "hdmi_priority"
|
||||||
CONF_EFFECT_LIST = "effect_list"
|
CONF_EFFECT_LIST = "effect_list"
|
||||||
@ -226,90 +227,53 @@ async def async_setup_entry(
|
|||||||
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
|
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Set up a Hyperion platform from config entry."""
|
"""Set up a Hyperion platform from config entry."""
|
||||||
host = config_entry.data[CONF_HOST]
|
|
||||||
port = config_entry.data[CONF_PORT]
|
|
||||||
token = config_entry.data.get(CONF_TOKEN)
|
|
||||||
|
|
||||||
async def async_instances_to_entities(response: Dict[str, Any]) -> None:
|
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
if not response or const.KEY_DATA not in response:
|
server_id = config_entry.unique_id
|
||||||
return
|
|
||||||
await async_instances_to_entities_raw(response[const.KEY_DATA])
|
|
||||||
|
|
||||||
async def async_instances_to_entities_raw(instances: List[Dict[str, Any]]) -> None:
|
@callback
|
||||||
registry = await async_get_registry(hass)
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
||||||
entities_to_add: List[HyperionLight] = []
|
"""Add entities for a new Hyperion instance."""
|
||||||
running_unique_ids: Set[str] = set()
|
assert server_id
|
||||||
stopped_unique_ids: Set[str] = set()
|
async_add_entities(
|
||||||
server_id = cast(str, config_entry.unique_id)
|
[
|
||||||
|
|
||||||
# In practice, an instance can be in 3 states as seen by this function:
|
|
||||||
#
|
|
||||||
# * Exists, and is running: Should be present in HASS/registry.
|
|
||||||
# * Exists, but is not running: Cannot add it yet, but entity may have be
|
|
||||||
# registered from a previous time it was running.
|
|
||||||
# * No longer exists at all: Should not be present in HASS/registry.
|
|
||||||
|
|
||||||
# Add instances that are missing.
|
|
||||||
for instance in instances:
|
|
||||||
instance_id = instance.get(const.KEY_INSTANCE)
|
|
||||||
if instance_id is None:
|
|
||||||
continue
|
|
||||||
unique_id = get_hyperion_unique_id(
|
|
||||||
server_id, instance_id, TYPE_HYPERION_LIGHT
|
|
||||||
)
|
|
||||||
if not instance.get(const.KEY_RUNNING, False):
|
|
||||||
stopped_unique_ids.add(unique_id)
|
|
||||||
continue
|
|
||||||
running_unique_ids.add(unique_id)
|
|
||||||
|
|
||||||
if unique_id in live_entities:
|
|
||||||
continue
|
|
||||||
hyperion_client = await async_create_connect_hyperion_client(
|
|
||||||
host, port, instance=instance_id, token=token
|
|
||||||
)
|
|
||||||
if not hyperion_client:
|
|
||||||
continue
|
|
||||||
live_entities.add(unique_id)
|
|
||||||
entities_to_add.append(
|
|
||||||
HyperionLight(
|
HyperionLight(
|
||||||
unique_id,
|
get_hyperion_unique_id(
|
||||||
instance.get(const.KEY_FRIENDLY_NAME, DEFAULT_NAME),
|
server_id, instance_num, TYPE_HYPERION_LIGHT
|
||||||
|
),
|
||||||
|
f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}",
|
||||||
config_entry.options,
|
config_entry.options,
|
||||||
hyperion_client,
|
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
||||||
)
|
),
|
||||||
|
HyperionPriorityLight(
|
||||||
|
get_hyperion_unique_id(
|
||||||
|
server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT
|
||||||
|
),
|
||||||
|
f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}",
|
||||||
|
config_entry.options,
|
||||||
|
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def instance_remove(instance_num: int) -> None:
|
||||||
|
"""Remove entities for an old Hyperion instance."""
|
||||||
|
assert server_id
|
||||||
|
for light_type in LIGHT_TYPES:
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass,
|
||||||
|
SIGNAL_ENTITY_REMOVE.format(
|
||||||
|
get_hyperion_unique_id(server_id, instance_num, light_type)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove entities that are are not running instances on Hyperion:
|
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
||||||
for unique_id in live_entities - running_unique_ids:
|
|
||||||
live_entities.remove(unique_id)
|
|
||||||
async_dispatcher_send(hass, SIGNAL_INSTANCE_REMOVED.format(unique_id))
|
|
||||||
|
|
||||||
# Deregister instances that are no longer present on this server.
|
|
||||||
for entry in async_entries_for_config_entry(registry, config_entry.entry_id):
|
|
||||||
if entry.unique_id not in running_unique_ids.union(stopped_unique_ids):
|
|
||||||
registry.async_remove(entry.entity_id)
|
|
||||||
|
|
||||||
async_add_entities(entities_to_add)
|
|
||||||
|
|
||||||
# Readability note: This variable is kept alive in the context of the callback to
|
|
||||||
# async_instances_to_entities below.
|
|
||||||
live_entities: Set[str] = set()
|
|
||||||
|
|
||||||
await async_instances_to_entities_raw(
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id][CONF_ROOT_CLIENT].instances,
|
|
||||||
)
|
|
||||||
hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append(
|
|
||||||
async_dispatcher_connect(
|
|
||||||
hass,
|
|
||||||
SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id),
|
|
||||||
async_instances_to_entities,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class HyperionLight(LightEntity):
|
class HyperionBaseLight(LightEntity):
|
||||||
"""Representation of a Hyperion remote."""
|
"""A Hyperion light base class."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -329,7 +293,23 @@ class HyperionLight(LightEntity):
|
|||||||
self._rgb_color: Sequence[int] = DEFAULT_COLOR
|
self._rgb_color: Sequence[int] = DEFAULT_COLOR
|
||||||
self._effect: str = KEY_EFFECT_SOLID
|
self._effect: str = KEY_EFFECT_SOLID
|
||||||
|
|
||||||
self._effect_list: List[str] = []
|
self._static_effect_list: List[str] = [KEY_EFFECT_SOLID]
|
||||||
|
if self._support_external_effects:
|
||||||
|
self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES)
|
||||||
|
self._effect_list: List[str] = self._static_effect_list[:]
|
||||||
|
|
||||||
|
self._client_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,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Whether or not the entity is enabled by default."""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self) -> bool:
|
def should_poll(self) -> bool:
|
||||||
@ -351,11 +331,6 @@ class HyperionLight(LightEntity):
|
|||||||
"""Return last color value set."""
|
"""Return last color value set."""
|
||||||
return color_util.color_RGB_to_hs(*self._rgb_color)
|
return color_util.color_RGB_to_hs(*self._rgb_color)
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if not black."""
|
|
||||||
return bool(self._client.is_on()) and self._client.visible_priority is not None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str:
|
def icon(self) -> str:
|
||||||
"""Return state specific icon."""
|
"""Return state specific icon."""
|
||||||
@ -374,11 +349,7 @@ class HyperionLight(LightEntity):
|
|||||||
@property
|
@property
|
||||||
def effect_list(self) -> List[str]:
|
def effect_list(self) -> List[str]:
|
||||||
"""Return the list of supported effects."""
|
"""Return the list of supported effects."""
|
||||||
return (
|
return self._effect_list
|
||||||
self._effect_list
|
|
||||||
+ list(const.KEY_COMPONENTID_EXTERNAL_SOURCES)
|
|
||||||
+ [KEY_EFFECT_SOLID]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self) -> int:
|
def supported_features(self) -> int:
|
||||||
@ -401,33 +372,7 @@ class HyperionLight(LightEntity):
|
|||||||
return self._options.get(key, defaults[key])
|
return self._options.get(key, defaults[key])
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
"""Turn the lights on."""
|
"""Turn on the light."""
|
||||||
# == 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 ==
|
# == Get key parameters ==
|
||||||
if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs:
|
if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs:
|
||||||
effect = KEY_EFFECT_SOLID
|
effect = KEY_EFFECT_SOLID
|
||||||
@ -457,7 +402,11 @@ class HyperionLight(LightEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# == Set an external source
|
# == Set an external source
|
||||||
if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
|
if (
|
||||||
|
effect
|
||||||
|
and self._support_external_effects
|
||||||
|
and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
||||||
|
):
|
||||||
|
|
||||||
# Clear any color/effect.
|
# Clear any color/effect.
|
||||||
if not await self._client.async_send_clear(
|
if not await self._client.async_send_clear(
|
||||||
@ -505,18 +454,6 @@ class HyperionLight(LightEntity):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""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(
|
def _set_internal_state(
|
||||||
self,
|
self,
|
||||||
brightness: Optional[int] = None,
|
brightness: Optional[int] = None,
|
||||||
@ -531,10 +468,12 @@ class HyperionLight(LightEntity):
|
|||||||
if effect is not None:
|
if effect is not None:
|
||||||
self._effect = effect
|
self._effect = effect
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
|
def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Update Hyperion components."""
|
"""Update Hyperion components."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None:
|
def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Update Hyperion adjustments."""
|
"""Update Hyperion adjustments."""
|
||||||
if self._client.adjustment:
|
if self._client.adjustment:
|
||||||
@ -548,26 +487,31 @@ class HyperionLight(LightEntity):
|
|||||||
)
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None:
|
def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Update Hyperion priorities."""
|
"""Update Hyperion priorities."""
|
||||||
visible_priority = self._client.visible_priority
|
priority = self._get_priority_entry_that_dictates_state()
|
||||||
if visible_priority:
|
if priority and self._allow_priority_update(priority):
|
||||||
componentid = visible_priority.get(const.KEY_COMPONENTID)
|
componentid = priority.get(const.KEY_COMPONENTID)
|
||||||
if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES:
|
if (
|
||||||
|
self._support_external_effects
|
||||||
|
and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES
|
||||||
|
):
|
||||||
self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid)
|
self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid)
|
||||||
elif componentid == const.KEY_COMPONENTID_EFFECT:
|
elif componentid == const.KEY_COMPONENTID_EFFECT:
|
||||||
# Owner is the effect name.
|
# Owner is the effect name.
|
||||||
# See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
|
# See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities
|
||||||
self._set_internal_state(
|
self._set_internal_state(
|
||||||
rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER]
|
rgb_color=DEFAULT_COLOR, effect=priority[const.KEY_OWNER]
|
||||||
)
|
)
|
||||||
elif componentid == const.KEY_COMPONENTID_COLOR:
|
elif componentid == const.KEY_COMPONENTID_COLOR:
|
||||||
self._set_internal_state(
|
self._set_internal_state(
|
||||||
rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB],
|
rgb_color=priority[const.KEY_VALUE][const.KEY_RGB],
|
||||||
effect=KEY_EFFECT_SOLID,
|
effect=KEY_EFFECT_SOLID,
|
||||||
)
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None:
|
def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Update Hyperion effects."""
|
"""Update Hyperion effects."""
|
||||||
if not self._client.effects:
|
if not self._client.effects:
|
||||||
@ -577,9 +521,10 @@ class HyperionLight(LightEntity):
|
|||||||
if const.KEY_NAME in effect:
|
if const.KEY_NAME in effect:
|
||||||
effect_list.append(effect[const.KEY_NAME])
|
effect_list.append(effect[const.KEY_NAME])
|
||||||
if effect_list:
|
if effect_list:
|
||||||
self._effect_list = effect_list
|
self._effect_list = self._static_effect_list + effect_list
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_full_state(self) -> None:
|
def _update_full_state(self) -> None:
|
||||||
"""Update full Hyperion state."""
|
"""Update full Hyperion state."""
|
||||||
self._update_adjustment()
|
self._update_adjustment()
|
||||||
@ -596,6 +541,7 @@ class HyperionLight(LightEntity):
|
|||||||
self._rgb_color,
|
self._rgb_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None:
|
def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Update client connection state."""
|
"""Update client connection state."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@ -606,24 +552,160 @@ class HyperionLight(LightEntity):
|
|||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass,
|
self.hass,
|
||||||
SIGNAL_INSTANCE_REMOVED.format(self._unique_id),
|
SIGNAL_ENTITY_REMOVE.format(self._unique_id),
|
||||||
self.async_remove,
|
self.async_remove,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._client.set_callbacks(
|
self._client.add_callbacks(self._client_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.
|
# Load initial state.
|
||||||
self._update_full_state()
|
self._update_full_state()
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Disconnect from server."""
|
"""Cleanup prior to hass removal."""
|
||||||
await self._client.async_client_disconnect()
|
self._client.remove_callbacks(self._client_callbacks)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _support_external_effects(self) -> bool:
|
||||||
|
"""Whether or not to support setting external effects from the light entity."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get the relevant Hyperion priority entry to consider."""
|
||||||
|
# Return the visible priority (whether or not it is the HA priority).
|
||||||
|
return self._client.visible_priority # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Determine whether to allow a priority to update internal state."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HyperionLight(HyperionBaseLight):
|
||||||
|
"""A Hyperion light that acts in absolute (vs priority) manner.
|
||||||
|
|
||||||
|
Light state is the absolute Hyperion component state (e.g. LED device on/off) rather
|
||||||
|
than color based at a particular priority, and the 'winning' priority determines
|
||||||
|
shown state rather than exclusively the HA priority.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
return (
|
||||||
|
bool(self._client.is_on())
|
||||||
|
and self._get_priority_entry_that_dictates_state() is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the light."""
|
||||||
|
# == 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 bool(self._client.is_on()):
|
||||||
|
for component in [
|
||||||
|
const.KEY_COMPONENTID_ALL,
|
||||||
|
const.KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
]:
|
||||||
|
if not await self._client.async_send_set_component(
|
||||||
|
**{
|
||||||
|
const.KEY_COMPONENTSTATE: {
|
||||||
|
const.KEY_COMPONENT: component,
|
||||||
|
const.KEY_STATE: True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Turn on the relevant Hyperion priority as usual.
|
||||||
|
await super().async_turn_on(**kwargs)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
if not await self._client.async_send_set_component(
|
||||||
|
**{
|
||||||
|
const.KEY_COMPONENTSTATE: {
|
||||||
|
const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
const.KEY_STATE: False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class HyperionPriorityLight(HyperionBaseLight):
|
||||||
|
"""A Hyperion light that only acts on a single Hyperion priority."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Whether or not the entity is enabled by default."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if light is on."""
|
||||||
|
priority = self._get_priority_entry_that_dictates_state()
|
||||||
|
return (
|
||||||
|
priority is not None
|
||||||
|
and not HyperionPriorityLight._is_priority_entry_black(priority)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the light."""
|
||||||
|
if not await self._client.async_send_clear(
|
||||||
|
**{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)}
|
||||||
|
):
|
||||||
|
return
|
||||||
|
await self._client.async_send_set_color(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: self._get_option(CONF_PRIORITY),
|
||||||
|
const.KEY_COLOR: COLOR_BLACK,
|
||||||
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _support_external_effects(self) -> bool:
|
||||||
|
"""Whether or not to support setting external effects from the light entity."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get the relevant Hyperion priority entry to consider."""
|
||||||
|
# Return the active priority (if any) at the configured HA priority.
|
||||||
|
for candidate in self._client.priorities or []:
|
||||||
|
if const.KEY_PRIORITY not in candidate:
|
||||||
|
continue
|
||||||
|
if candidate[const.KEY_PRIORITY] == self._get_option(
|
||||||
|
CONF_PRIORITY
|
||||||
|
) and candidate.get(const.KEY_ACTIVE, False):
|
||||||
|
return candidate # type: ignore[no-any-return]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_priority_entry_black(cls, priority: Optional[Dict[str, Any]]) -> bool:
|
||||||
|
"""Determine if a given priority entry is the color black."""
|
||||||
|
if not priority:
|
||||||
|
return False
|
||||||
|
if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR:
|
||||||
|
rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB)
|
||||||
|
if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# pylint: disable=no-self-use
|
||||||
|
def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool:
|
||||||
|
"""Determine whether to allow a Hyperion priority to update entity attributes."""
|
||||||
|
# Black is treated as 'off' (and Home Assistant does not support selecting black
|
||||||
|
# from the color selector). Do not set our internal attributes if the priority is
|
||||||
|
# 'off' (i.e. if black is active). Do this to ensure it seamlessly turns back on
|
||||||
|
# at the correct prior color on the next 'on' call.
|
||||||
|
return not HyperionPriorityLight._is_priority_entry_black(priority)
|
||||||
|
|
||||||
|
|
||||||
|
LIGHT_TYPES = {
|
||||||
|
TYPE_HYPERION_LIGHT: HyperionLight,
|
||||||
|
TYPE_HYPERION_PRIORITY_LIGHT: HyperionPriorityLight,
|
||||||
|
}
|
||||||
|
210
homeassistant/components/hyperion/switch.py
Normal file
210
homeassistant/components/hyperion/switch.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
"""Switch platform for Hyperion."""
|
||||||
|
|
||||||
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
|
||||||
|
from hyperion import client
|
||||||
|
from hyperion.const import (
|
||||||
|
KEY_COMPONENT,
|
||||||
|
KEY_COMPONENTID_ALL,
|
||||||
|
KEY_COMPONENTID_BLACKBORDER,
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||||
|
KEY_COMPONENTID_FORWARDER,
|
||||||
|
KEY_COMPONENTID_GRABBER,
|
||||||
|
KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
KEY_COMPONENTID_SMOOTHING,
|
||||||
|
KEY_COMPONENTID_V4L,
|
||||||
|
KEY_COMPONENTS,
|
||||||
|
KEY_COMPONENTSTATE,
|
||||||
|
KEY_ENABLED,
|
||||||
|
KEY_NAME,
|
||||||
|
KEY_STATE,
|
||||||
|
KEY_UPDATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import get_hyperion_unique_id, listen_for_instance_updates
|
||||||
|
from .const import (
|
||||||
|
COMPONENT_TO_NAME,
|
||||||
|
CONF_INSTANCE_CLIENTS,
|
||||||
|
DOMAIN,
|
||||||
|
NAME_SUFFIX_HYPERION_COMPONENT_SWITCH,
|
||||||
|
SIGNAL_ENTITY_REMOVE,
|
||||||
|
TYPE_HYPERION_COMPONENT_SWITCH_BASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
COMPONENT_SWITCHES = [
|
||||||
|
KEY_COMPONENTID_ALL,
|
||||||
|
KEY_COMPONENTID_SMOOTHING,
|
||||||
|
KEY_COMPONENTID_BLACKBORDER,
|
||||||
|
KEY_COMPONENTID_FORWARDER,
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||||
|
KEY_COMPONENTID_GRABBER,
|
||||||
|
KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
KEY_COMPONENTID_V4L,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable
|
||||||
|
) -> bool:
|
||||||
|
"""Set up a Hyperion platform from config entry."""
|
||||||
|
entry_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
server_id = config_entry.unique_id
|
||||||
|
|
||||||
|
def component_to_switch_type(component: str) -> str:
|
||||||
|
"""Convert a component to a switch type string."""
|
||||||
|
return slugify(
|
||||||
|
f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def component_to_unique_id(component: str, instance_num: int) -> str:
|
||||||
|
"""Convert a component to a unique_id."""
|
||||||
|
assert server_id
|
||||||
|
return get_hyperion_unique_id(
|
||||||
|
server_id, instance_num, component_to_switch_type(component)
|
||||||
|
)
|
||||||
|
|
||||||
|
def component_to_switch_name(component: str, instance_name: str) -> str:
|
||||||
|
"""Convert a component to a switch name."""
|
||||||
|
return (
|
||||||
|
f"{instance_name} "
|
||||||
|
f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} "
|
||||||
|
f"{COMPONENT_TO_NAME.get(component, component.capitalize())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def instance_add(instance_num: int, instance_name: str) -> None:
|
||||||
|
"""Add entities for a new Hyperion instance."""
|
||||||
|
assert server_id
|
||||||
|
switches = []
|
||||||
|
for component in COMPONENT_SWITCHES:
|
||||||
|
switches.append(
|
||||||
|
HyperionComponentSwitch(
|
||||||
|
component_to_unique_id(component, instance_num),
|
||||||
|
component_to_switch_name(component, instance_name),
|
||||||
|
component,
|
||||||
|
entry_data[CONF_INSTANCE_CLIENTS][instance_num],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async_add_entities(switches)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def instance_remove(instance_num: int) -> None:
|
||||||
|
"""Remove entities for an old Hyperion instance."""
|
||||||
|
assert server_id
|
||||||
|
for component in COMPONENT_SWITCHES:
|
||||||
|
async_dispatcher_send(
|
||||||
|
hass,
|
||||||
|
SIGNAL_ENTITY_REMOVE.format(
|
||||||
|
component_to_unique_id(component, instance_num),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
listen_for_instance_updates(hass, config_entry, instance_add, instance_remove)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HyperionComponentSwitch(SwitchEntity):
|
||||||
|
"""ComponentBinarySwitch switch class."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unique_id: str,
|
||||||
|
name: str,
|
||||||
|
component_name: str,
|
||||||
|
hyperion_client: client.HyperionClient,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the switch."""
|
||||||
|
self._unique_id = unique_id
|
||||||
|
self._name = name
|
||||||
|
self._component_name = component_name
|
||||||
|
self._client = hyperion_client
|
||||||
|
self._client_callbacks = {
|
||||||
|
f"{KEY_COMPONENTS}-{KEY_UPDATE}": self._update_components
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return whether or not this entity should be polled."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
"""Whether or not the entity is enabled by default."""
|
||||||
|
# These component controls are for advanced users and are disabled by default.
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self) -> str:
|
||||||
|
"""Return a unique id for this instance."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the switch."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return true if the switch is on."""
|
||||||
|
for component in self._client.components:
|
||||||
|
if component[KEY_NAME] == self._component_name:
|
||||||
|
return bool(component.setdefault(KEY_ENABLED, False))
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return server availability."""
|
||||||
|
return bool(self._client.has_loaded_state)
|
||||||
|
|
||||||
|
async def _async_send_set_component(self, value: bool) -> None:
|
||||||
|
"""Send a component control request."""
|
||||||
|
await self._client.async_send_set_component(
|
||||||
|
**{
|
||||||
|
KEY_COMPONENTSTATE: {
|
||||||
|
KEY_COMPONENT: self._component_name,
|
||||||
|
KEY_STATE: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on the switch."""
|
||||||
|
await self._async_send_set_component(True)
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the switch."""
|
||||||
|
await self._async_send_set_component(False)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""Update Hyperion components."""
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Register callbacks when entity added to hass."""
|
||||||
|
assert self.hass
|
||||||
|
self.async_on_remove(
|
||||||
|
async_dispatcher_connect(
|
||||||
|
self.hass,
|
||||||
|
SIGNAL_ENTITY_REMOVE.format(self._unique_id),
|
||||||
|
self.async_remove,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._client.add_callbacks(self._client_callbacks)
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
"""Cleanup prior to hass removal."""
|
||||||
|
self._client.remove_callbacks(self._client_callbacks)
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any, Dict, Optional, Type
|
from typing import Any, Dict, Optional, Type
|
||||||
from unittest.mock import AsyncMock, Mock, patch # type: ignore[attr-defined]
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
from hyperion import const
|
from hyperion import const
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}"
|
|||||||
TEST_ENTITY_ID_1 = "light.test_instance_1"
|
TEST_ENTITY_ID_1 = "light.test_instance_1"
|
||||||
TEST_ENTITY_ID_2 = "light.test_instance_2"
|
TEST_ENTITY_ID_2 = "light.test_instance_2"
|
||||||
TEST_ENTITY_ID_3 = "light.test_instance_3"
|
TEST_ENTITY_ID_3 = "light.test_instance_3"
|
||||||
|
TEST_PRIORITY_LIGHT_ENTITY_ID_1 = "light.test_instance_1_priority"
|
||||||
TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}"
|
TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}"
|
||||||
|
|
||||||
TEST_TOKEN = "sekr1t"
|
TEST_TOKEN = "sekr1t"
|
||||||
@ -68,7 +69,7 @@ TEST_AUTH_NOT_REQUIRED_RESP = {
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AsyncContextManagerMock(Mock): # type: ignore[misc]
|
class AsyncContextManagerMock(Mock):
|
||||||
"""An async context manager mock for Hyperion."""
|
"""An async context manager mock for Hyperion."""
|
||||||
|
|
||||||
async def __aenter__(self) -> Optional[AsyncContextManagerMock]:
|
async def __aenter__(self) -> Optional[AsyncContextManagerMock]:
|
||||||
@ -112,6 +113,7 @@ def create_mock_client() -> Mock:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mock_client.priorities = []
|
||||||
mock_client.adjustment = None
|
mock_client.adjustment = None
|
||||||
mock_client.effects = None
|
mock_client.effects = None
|
||||||
mock_client.instances = [
|
mock_client.instances = [
|
||||||
@ -160,3 +162,12 @@ async def setup_test_config_entry(
|
|||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
return config_entry
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
|
def call_registered_callback(
|
||||||
|
client: AsyncMock, key: str, *args: Any, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""Call Hyperion entity callbacks that were registered with the client."""
|
||||||
|
for call in client.add_callbacks.call_args_list:
|
||||||
|
if key in call[0][0]:
|
||||||
|
call[0][0][key](*args, **kwargs)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from unittest.mock import AsyncMock, patch # type: ignore[attr-defined]
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from hyperion import const
|
from hyperion import const
|
||||||
|
|
||||||
@ -689,6 +689,7 @@ async def test_options(hass: HomeAssistantType) -> None:
|
|||||||
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
|
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
# pylint: disable=unsubscriptable-object
|
||||||
assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority
|
assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""Tests for the Hyperion integration."""
|
"""Tests for the Hyperion integration."""
|
||||||
import logging
|
import logging
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
from unittest.mock import AsyncMock, call, patch # type: ignore[attr-defined]
|
from unittest.mock import AsyncMock, Mock, call, patch
|
||||||
|
|
||||||
from hyperion import const
|
from hyperion import const
|
||||||
|
|
||||||
@ -11,7 +11,11 @@ from homeassistant.components.hyperion import (
|
|||||||
get_hyperion_unique_id,
|
get_hyperion_unique_id,
|
||||||
light as hyperion_light,
|
light as hyperion_light,
|
||||||
)
|
)
|
||||||
from homeassistant.components.hyperion.const import DOMAIN, TYPE_HYPERION_LIGHT
|
from homeassistant.components.hyperion.const import (
|
||||||
|
DEFAULT_ORIGIN,
|
||||||
|
DOMAIN,
|
||||||
|
TYPE_HYPERION_LIGHT,
|
||||||
|
)
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
ATTR_BRIGHTNESS,
|
ATTR_BRIGHTNESS,
|
||||||
ATTR_EFFECT,
|
ATTR_EFFECT,
|
||||||
@ -34,6 +38,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_registry import async_get_registry
|
from homeassistant.helpers.entity_registry import async_get_registry
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
import homeassistant.util.color as color_util
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
TEST_AUTH_NOT_REQUIRED_RESP,
|
TEST_AUTH_NOT_REQUIRED_RESP,
|
||||||
@ -49,22 +54,19 @@ from . import (
|
|||||||
TEST_INSTANCE_3,
|
TEST_INSTANCE_3,
|
||||||
TEST_PORT,
|
TEST_PORT,
|
||||||
TEST_PRIORITY,
|
TEST_PRIORITY,
|
||||||
|
TEST_PRIORITY_LIGHT_ENTITY_ID_1,
|
||||||
TEST_SYSINFO_ID,
|
TEST_SYSINFO_ID,
|
||||||
TEST_YAML_ENTITY_ID,
|
TEST_YAML_ENTITY_ID,
|
||||||
TEST_YAML_NAME,
|
TEST_YAML_NAME,
|
||||||
add_test_config_entry,
|
add_test_config_entry,
|
||||||
|
call_registered_callback,
|
||||||
create_mock_client,
|
create_mock_client,
|
||||||
setup_test_config_entry,
|
setup_test_config_entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
COLOR_BLACK = color_util.COLORS["black"]
|
||||||
def _call_registered_callback(
|
|
||||||
client: AsyncMock, key: str, *args: Any, **kwargs: Any
|
|
||||||
) -> None:
|
|
||||||
"""Call a Hyperion entity callback that was registered with the client."""
|
|
||||||
client.set_callbacks.call_args[0][0][key](*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None:
|
async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None:
|
||||||
@ -264,7 +266,7 @@ async def test_setup_config_entry_not_ready_load_state_fail(
|
|||||||
|
|
||||||
|
|
||||||
async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None:
|
async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None:
|
||||||
"""Test dynamic changes in the omstamce configuration."""
|
"""Test dynamic changes in the instance configuration."""
|
||||||
registry = await async_get_registry(hass)
|
registry = await async_get_registry(hass)
|
||||||
|
|
||||||
config_entry = add_test_config_entry(hass)
|
config_entry = add_test_config_entry(hass)
|
||||||
@ -291,11 +293,12 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) ->
|
|||||||
instance_callback = master_client.set_callbacks.call_args[0][0][
|
instance_callback = master_client.set_callbacks.call_args[0][0][
|
||||||
f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}"
|
f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}"
|
||||||
]
|
]
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.hyperion.client.HyperionClient",
|
"homeassistant.components.hyperion.client.HyperionClient",
|
||||||
return_value=entity_client,
|
return_value=entity_client,
|
||||||
):
|
):
|
||||||
instance_callback(
|
await instance_callback(
|
||||||
{
|
{
|
||||||
const.KEY_SUCCESS: True,
|
const.KEY_SUCCESS: True,
|
||||||
const.KEY_DATA: [
|
const.KEY_DATA: [
|
||||||
@ -323,7 +326,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) ->
|
|||||||
"homeassistant.components.hyperion.client.HyperionClient",
|
"homeassistant.components.hyperion.client.HyperionClient",
|
||||||
return_value=entity_client,
|
return_value=entity_client,
|
||||||
):
|
):
|
||||||
instance_callback(
|
await instance_callback(
|
||||||
{
|
{
|
||||||
const.KEY_SUCCESS: True,
|
const.KEY_SUCCESS: True,
|
||||||
const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3],
|
const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3],
|
||||||
@ -343,7 +346,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) ->
|
|||||||
"homeassistant.components.hyperion.client.HyperionClient",
|
"homeassistant.components.hyperion.client.HyperionClient",
|
||||||
return_value=entity_client,
|
return_value=entity_client,
|
||||||
):
|
):
|
||||||
instance_callback(
|
await instance_callback(
|
||||||
{
|
{
|
||||||
const.KEY_SUCCESS: True,
|
const.KEY_SUCCESS: True,
|
||||||
const.KEY_DATA: [
|
const.KEY_DATA: [
|
||||||
@ -364,7 +367,7 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) ->
|
|||||||
"homeassistant.components.hyperion.client.HyperionClient",
|
"homeassistant.components.hyperion.client.HyperionClient",
|
||||||
return_value=entity_client,
|
return_value=entity_client,
|
||||||
):
|
):
|
||||||
instance_callback(
|
await instance_callback(
|
||||||
{
|
{
|
||||||
const.KEY_SUCCESS: True,
|
const.KEY_SUCCESS: True,
|
||||||
const.KEY_DATA: [TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3],
|
const.KEY_DATA: [TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3],
|
||||||
@ -413,7 +416,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_COLOR: [255, 255, 255],
|
const.KEY_COLOR: [255, 255, 255],
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -437,7 +440,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_COLOR: [255, 255, 255],
|
const.KEY_COLOR: [255, 255, 255],
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -453,7 +456,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
|
|
||||||
# Simulate a state callback from Hyperion.
|
# Simulate a state callback from Hyperion.
|
||||||
client.adjustment = [{const.KEY_BRIGHTNESS: 50}]
|
client.adjustment = [{const.KEY_BRIGHTNESS: 50}]
|
||||||
_call_registered_callback(client, "adjustment-update")
|
call_registered_callback(client, "adjustment-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "on"
|
assert entity_state.state == "on"
|
||||||
@ -473,7 +476,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_COLOR: (0, 255, 255),
|
const.KEY_COLOR: (0, 255, 255),
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -483,7 +486,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
|
const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)},
|
||||||
}
|
}
|
||||||
|
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["hs_color"] == hs_color
|
assert entity_state.attributes["hs_color"] == hs_color
|
||||||
@ -509,11 +512,11 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_COLOR: (0, 255, 255),
|
const.KEY_COLOR: (0, 255, 255),
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
client.adjustment = [{const.KEY_BRIGHTNESS: 100}]
|
client.adjustment = [{const.KEY_BRIGHTNESS: 100}]
|
||||||
_call_registered_callback(client, "adjustment-update")
|
call_registered_callback(client, "adjustment-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["brightness"] == brightness
|
assert entity_state.attributes["brightness"] == brightness
|
||||||
@ -559,7 +562,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
client.visible_priority = {const.KEY_COMPONENTID: effect}
|
client.visible_priority = {const.KEY_COMPONENTID: effect}
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
||||||
@ -584,14 +587,14 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_EFFECT: {const.KEY_NAME: effect},
|
const.KEY_EFFECT: {const.KEY_NAME: effect},
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
client.visible_priority = {
|
client.visible_priority = {
|
||||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT,
|
||||||
const.KEY_OWNER: effect,
|
const.KEY_OWNER: effect,
|
||||||
}
|
}
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
|
assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT
|
||||||
@ -612,7 +615,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
**{
|
**{
|
||||||
const.KEY_PRIORITY: TEST_PRIORITY,
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
const.KEY_COLOR: (0, 0, 255),
|
const.KEY_COLOR: (0, 0, 255),
|
||||||
const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN,
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Simulate a state callback from Hyperion.
|
# Simulate a state callback from Hyperion.
|
||||||
@ -620,7 +623,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)},
|
const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)},
|
||||||
}
|
}
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["hs_color"] == hs_color
|
assert entity_state.attributes["hs_color"] == hs_color
|
||||||
@ -629,7 +632,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
|
|
||||||
# No calls if disconnected.
|
# No calls if disconnected.
|
||||||
client.has_loaded_state = False
|
client.has_loaded_state = False
|
||||||
_call_registered_callback(client, "client-update", {"loaded-state": False})
|
call_registered_callback(client, "client-update", {"loaded-state": False})
|
||||||
client.async_send_clear = AsyncMock(return_value=True)
|
client.async_send_clear = AsyncMock(return_value=True)
|
||||||
client.async_send_set_effect = AsyncMock(return_value=True)
|
client.async_send_set_effect = AsyncMock(return_value=True)
|
||||||
|
|
||||||
@ -641,6 +644,51 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None:
|
|||||||
assert not client.async_send_set_effect.called
|
assert not client.async_send_set_effect.called
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_async_turn_on_error_conditions(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test error conditions when turning the light on."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.async_send_set_component = AsyncMock(return_value=False)
|
||||||
|
client.is_on = Mock(return_value=False)
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
# On (=), 100% (=), solid (=), [255,255,255] (=)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_set_component.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_COMPONENTSTATE: {
|
||||||
|
const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL,
|
||||||
|
const.KEY_STATE: True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test error conditions when turning the light off."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.async_send_set_component = AsyncMock(return_value=False)
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: TEST_ENTITY_ID_1},
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
|
async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
|
||||||
"""Test turning the light off."""
|
"""Test turning the light off."""
|
||||||
client = create_mock_client()
|
client = create_mock_client()
|
||||||
@ -663,7 +711,7 @@ async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_call_registered_callback(client, "components-update")
|
call_registered_callback(client, "components-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
|
assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB
|
||||||
@ -671,7 +719,7 @@ async def test_light_async_turn_off(hass: HomeAssistantType) -> None:
|
|||||||
# No calls if no state loaded.
|
# No calls if no state loaded.
|
||||||
client.has_loaded_state = False
|
client.has_loaded_state = False
|
||||||
client.async_send_set_component = AsyncMock(return_value=True)
|
client.async_send_set_component = AsyncMock(return_value=True)
|
||||||
_call_registered_callback(client, "client-update", {"loaded-state": False})
|
call_registered_callback(client, "client-update", {"loaded-state": False})
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
LIGHT_DOMAIN,
|
LIGHT_DOMAIN,
|
||||||
@ -693,7 +741,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
# Bright change gets accepted.
|
# Bright change gets accepted.
|
||||||
brightness = 10
|
brightness = 10
|
||||||
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
|
client.adjustment = [{const.KEY_BRIGHTNESS: brightness}]
|
||||||
_call_registered_callback(client, "adjustment-update")
|
call_registered_callback(client, "adjustment-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
||||||
@ -701,20 +749,20 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
# Broken brightness value is ignored.
|
# Broken brightness value is ignored.
|
||||||
bad_brightness = -200
|
bad_brightness = -200
|
||||||
client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}]
|
client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}]
|
||||||
_call_registered_callback(client, "adjustment-update")
|
call_registered_callback(client, "adjustment-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0))
|
||||||
|
|
||||||
# Update components.
|
# Update components.
|
||||||
client.is_on.return_value = True
|
client.is_on.return_value = True
|
||||||
_call_registered_callback(client, "components-update")
|
call_registered_callback(client, "components-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "on"
|
assert entity_state.state == "on"
|
||||||
|
|
||||||
client.is_on.return_value = False
|
client.is_on.return_value = False
|
||||||
_call_registered_callback(client, "components-update")
|
call_registered_callback(client, "components-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "off"
|
assert entity_state.state == "off"
|
||||||
@ -722,7 +770,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
# Update priorities (V4L)
|
# Update priorities (V4L)
|
||||||
client.is_on.return_value = True
|
client.is_on.return_value = True
|
||||||
client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
|
client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L}
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE
|
||||||
@ -736,7 +784,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
const.KEY_OWNER: effect,
|
const.KEY_OWNER: effect,
|
||||||
}
|
}
|
||||||
|
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["effect"] == effect
|
assert entity_state.attributes["effect"] == effect
|
||||||
@ -750,7 +798,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||||
}
|
}
|
||||||
|
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
|
assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID
|
||||||
@ -760,7 +808,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
# Update priorities (None)
|
# Update priorities (None)
|
||||||
client.visible_priority = None
|
client.visible_priority = None
|
||||||
|
|
||||||
_call_registered_callback(client, "priorities-update")
|
call_registered_callback(client, "priorities-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "off"
|
assert entity_state.state == "off"
|
||||||
@ -768,18 +816,20 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
# Update effect list
|
# Update effect list
|
||||||
effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
|
effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}]
|
||||||
client.effects = effects
|
client.effects = effects
|
||||||
_call_registered_callback(client, "effects-update")
|
call_registered_callback(client, "effects-update")
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.attributes["effect_list"] == [
|
assert entity_state.attributes["effect_list"] == [
|
||||||
|
hyperion_light.KEY_EFFECT_SOLID
|
||||||
|
] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [
|
||||||
effect[const.KEY_NAME] for effect in effects
|
effect[const.KEY_NAME] for effect in effects
|
||||||
] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID]
|
]
|
||||||
|
|
||||||
# Update connection status (e.g. disconnection).
|
# Update connection status (e.g. disconnection).
|
||||||
|
|
||||||
# Turn on late, check state, disconnect, ensure it cannot be turned off.
|
# Turn on late, check state, disconnect, ensure it cannot be turned off.
|
||||||
client.has_loaded_state = False
|
client.has_loaded_state = False
|
||||||
_call_registered_callback(client, "client-update", {"loaded-state": False})
|
call_registered_callback(client, "client-update", {"loaded-state": False})
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "unavailable"
|
assert entity_state.state == "unavailable"
|
||||||
@ -790,7 +840,7 @@ async def test_light_async_updates_from_hyperion_client(
|
|||||||
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
const.KEY_VALUE: {const.KEY_RGB: rgb},
|
||||||
}
|
}
|
||||||
_call_registered_callback(client, "client-update", {"loaded-state": True})
|
call_registered_callback(client, "client-update", {"loaded-state": True})
|
||||||
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
entity_state = hass.states.get(TEST_ENTITY_ID_1)
|
||||||
assert entity_state
|
assert entity_state
|
||||||
assert entity_state.state == "on"
|
assert entity_state.state == "on"
|
||||||
@ -892,3 +942,347 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None:
|
|||||||
data=config_entry.data,
|
data=config_entry.data,
|
||||||
)
|
)
|
||||||
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
|
||||||
|
async def test_priority_light_async_updates(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
) -> None:
|
||||||
|
"""Test receiving a variety of Hyperion client callbacks to a HyperionPriorityLight."""
|
||||||
|
priority_template = {
|
||||||
|
const.KEY_ACTIVE: True,
|
||||||
|
const.KEY_VISIBLE: True,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||||
|
}
|
||||||
|
|
||||||
|
client = create_mock_client()
|
||||||
|
client.priorities = [{**priority_template}]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
# == Scenario: Color at HA priority will show light as on.
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
assert entity_state.attributes["hs_color"] == (0.0, 0.0)
|
||||||
|
|
||||||
|
# == Scenario: Color going to black shows the light as off.
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# == Scenario: Lower priority than HA priority should have no impact on what HA
|
||||||
|
# shows when the HA priority is present.
|
||||||
|
client.priorities = [
|
||||||
|
{**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1},
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# == Scenario: Fresh color at HA priority should turn HA entity on (even though
|
||||||
|
# there's a lower priority enabled/visible in Hyperion).
|
||||||
|
client.priorities = [
|
||||||
|
{**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1},
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
assert entity_state.attributes["hs_color"] == (240.0, 33.333)
|
||||||
|
|
||||||
|
# == Scenario: V4L at a higher priority, with no other HA priority at all, should
|
||||||
|
# have no effect.
|
||||||
|
|
||||||
|
# Emulate HA turning the light off with black at the HA priority.
|
||||||
|
client.priorities = []
|
||||||
|
client.visible_priority = None
|
||||||
|
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# Emulate V4L turning on.
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_PRIORITY: 240,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# == Scenario: A lower priority input (lower priority than HA) should have no effect.
|
||||||
|
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VISIBLE: True,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY - 1,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (255, 0, 0)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_PRIORITY: 240,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)},
|
||||||
|
const.KEY_VISIBLE: False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# == Scenario: A non-active priority is ignored.
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
const.KEY_ACTIVE: False,
|
||||||
|
const.KEY_VISIBLE: False,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = None
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# == Scenario: A priority with no ... priority ... is ignored.
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
const.KEY_ACTIVE: True,
|
||||||
|
const.KEY_VISIBLE: True,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = None
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_priority_light_async_updates_off_sets_black(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
) -> None:
|
||||||
|
"""Test turning the HyperionPriorityLight off."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
const.KEY_ACTIVE: True,
|
||||||
|
const.KEY_VISIBLE: True,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
client.async_send_clear = AsyncMock(return_value=True)
|
||||||
|
client.async_send_set_color = AsyncMock(return_value=True)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_clear.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_set_color.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COLOR: COLOR_BLACK,
|
||||||
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_priority_light_prior_color_preserved_after_black(
|
||||||
|
hass: HomeAssistantType,
|
||||||
|
) -> None:
|
||||||
|
"""Test that color is preserved in an on->off->on cycle for a HyperionPriorityLight.
|
||||||
|
|
||||||
|
For a HyperionPriorityLight the color black is used to indicate off. This test
|
||||||
|
ensures that a cycle through 'off' will preserve the original color.
|
||||||
|
"""
|
||||||
|
priority_template = {
|
||||||
|
const.KEY_ACTIVE: True,
|
||||||
|
const.KEY_VISIBLE: True,
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR,
|
||||||
|
}
|
||||||
|
|
||||||
|
client = create_mock_client()
|
||||||
|
client.async_send_set_color = AsyncMock(return_value=True)
|
||||||
|
client.async_send_clear = AsyncMock(return_value=True)
|
||||||
|
client.priorities = []
|
||||||
|
client.visible_priority = None
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
# Turn the light on full green...
|
||||||
|
# On (=), 100% (=), solid (=), [0,0,255] (=)
|
||||||
|
hs_color = (240.0, 100.0)
|
||||||
|
rgb_color = (0, 0, 255)
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1, ATTR_HS_COLOR: hs_color},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_set_color.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COLOR: rgb_color,
|
||||||
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: rgb_color},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
assert entity_state.attributes["hs_color"] == hs_color
|
||||||
|
|
||||||
|
# Then turn it off.
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_set_color.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COLOR: COLOR_BLACK,
|
||||||
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# Then turn it back on and ensure it's still green.
|
||||||
|
# On (=), 100% (=), solid (=), [0,0,255] (=)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert client.async_send_set_color.call_args == call(
|
||||||
|
**{
|
||||||
|
const.KEY_PRIORITY: TEST_PRIORITY,
|
||||||
|
const.KEY_COLOR: rgb_color,
|
||||||
|
const.KEY_ORIGIN: DEFAULT_ORIGIN,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.priorities = [
|
||||||
|
{
|
||||||
|
**priority_template,
|
||||||
|
const.KEY_VALUE: {const.KEY_RGB: rgb_color},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
client.visible_priority = client.priorities[0]
|
||||||
|
call_registered_callback(client, "priorities-update")
|
||||||
|
|
||||||
|
entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
assert entity_state.attributes["hs_color"] == hs_color
|
||||||
|
|
||||||
|
|
||||||
|
async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -> None:
|
||||||
|
"""Ensure a HyperionPriorityLight does not list external sources."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.priorities = []
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
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]
|
||||||
|
140
tests/components/hyperion/test_switch.py
Normal file
140
tests/components/hyperion/test_switch.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
"""Tests for the Hyperion integration."""
|
||||||
|
import logging
|
||||||
|
from unittest.mock import AsyncMock, call, patch
|
||||||
|
|
||||||
|
from hyperion.const import (
|
||||||
|
KEY_COMPONENT,
|
||||||
|
KEY_COMPONENTID_ALL,
|
||||||
|
KEY_COMPONENTID_BLACKBORDER,
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||||
|
KEY_COMPONENTID_FORWARDER,
|
||||||
|
KEY_COMPONENTID_GRABBER,
|
||||||
|
KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
KEY_COMPONENTID_SMOOTHING,
|
||||||
|
KEY_COMPONENTID_V4L,
|
||||||
|
KEY_COMPONENTSTATE,
|
||||||
|
KEY_STATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.hyperion.const import COMPONENT_TO_NAME
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from . import call_registered_callback, create_mock_client, setup_test_config_entry
|
||||||
|
|
||||||
|
TEST_COMPONENTS = [
|
||||||
|
{"enabled": True, "name": "ALL"},
|
||||||
|
{"enabled": True, "name": "SMOOTHING"},
|
||||||
|
{"enabled": True, "name": "BLACKBORDER"},
|
||||||
|
{"enabled": False, "name": "FORWARDER"},
|
||||||
|
{"enabled": False, "name": "BOBLIGHTSERVER"},
|
||||||
|
{"enabled": False, "name": "GRABBER"},
|
||||||
|
{"enabled": False, "name": "V4L"},
|
||||||
|
{"enabled": True, "name": "LEDDEVICE"},
|
||||||
|
]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
TEST_SWITCH_COMPONENT_BASE_ENTITY_ID = "switch.test_instance_1_component"
|
||||||
|
TEST_SWITCH_COMPONENT_ALL_ENTITY_ID = f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_all"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_turn_on_off(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test turning the light on."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.async_send_set_component = AsyncMock(return_value=True)
|
||||||
|
client.components = TEST_COMPONENTS
|
||||||
|
|
||||||
|
# Setup component switch.
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
# Verify switch is on (as per TEST_COMPONENTS above).
|
||||||
|
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
|
||||||
|
# Turn switch off.
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: TEST_SWITCH_COMPONENT_ALL_ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify correct parameters are passed to the library.
|
||||||
|
assert client.async_send_set_component.call_args == call(
|
||||||
|
**{KEY_COMPONENTSTATE: {KEY_COMPONENT: KEY_COMPONENTID_ALL, KEY_STATE: False}}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.components[0] = {
|
||||||
|
"enabled": False,
|
||||||
|
"name": "ALL",
|
||||||
|
}
|
||||||
|
call_registered_callback(client, "components-update")
|
||||||
|
|
||||||
|
# Verify the switch turns off.
|
||||||
|
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "off"
|
||||||
|
|
||||||
|
# Turn switch on.
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: TEST_SWITCH_COMPONENT_ALL_ENTITY_ID},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify correct parameters are passed to the library.
|
||||||
|
assert client.async_send_set_component.call_args == call(
|
||||||
|
**{KEY_COMPONENTSTATE: {KEY_COMPONENT: KEY_COMPONENTID_ALL, KEY_STATE: True}}
|
||||||
|
)
|
||||||
|
|
||||||
|
client.components[0] = {
|
||||||
|
"enabled": True,
|
||||||
|
"name": "ALL",
|
||||||
|
}
|
||||||
|
call_registered_callback(client, "components-update")
|
||||||
|
|
||||||
|
# Verify the switch turns on.
|
||||||
|
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID)
|
||||||
|
assert entity_state
|
||||||
|
assert entity_state.state == "on"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None:
|
||||||
|
"""Test that the correct switch entities are created."""
|
||||||
|
client = create_mock_client()
|
||||||
|
client.components = TEST_COMPONENTS
|
||||||
|
|
||||||
|
# Setup component switch.
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default"
|
||||||
|
) as enabled_by_default_mock:
|
||||||
|
enabled_by_default_mock.return_value = True
|
||||||
|
await setup_test_config_entry(hass, hyperion_client=client)
|
||||||
|
|
||||||
|
entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID)
|
||||||
|
|
||||||
|
for component in (
|
||||||
|
KEY_COMPONENTID_ALL,
|
||||||
|
KEY_COMPONENTID_SMOOTHING,
|
||||||
|
KEY_COMPONENTID_BLACKBORDER,
|
||||||
|
KEY_COMPONENTID_FORWARDER,
|
||||||
|
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||||
|
KEY_COMPONENTID_GRABBER,
|
||||||
|
KEY_COMPONENTID_LEDDEVICE,
|
||||||
|
KEY_COMPONENTID_V4L,
|
||||||
|
):
|
||||||
|
entity_id = (
|
||||||
|
TEST_SWITCH_COMPONENT_BASE_ENTITY_ID
|
||||||
|
+ "_"
|
||||||
|
+ slugify(COMPONENT_TO_NAME[component])
|
||||||
|
)
|
||||||
|
entity_state = hass.states.get(entity_id)
|
||||||
|
assert entity_state, f"Couldn't find entity: {entity_id}"
|
Loading…
x
Reference in New Issue
Block a user