mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add WLED integration (#28542)
* Add WLED integration * Use f-string for uniq id in sensor platform * Typing improvements * Removes sensor & light platform * Remove PARALLEL_UPDATES from integration level * Correct type in code comment 'themselves' * Use async_track_time_interval in async context * Remove stale code * Remove decorator from Flow handler * Remove unused __init__ from config flow * Move show form methods to sync * Only wrap lines that can raise in try except block * Remove domain and platform from uniq id * Wrap light state in bool object in is_on method * Use async_schedule_update_ha_state in async context * Return empty dict in device state attributes instead of None * Remove unneeded setdefault call in setup entry * Cancel update timer on entry unload * Restructure config flow code * Adjust tests for new uniq id * Correct typo AdGuard Home -> WLED in config flow file comment * Convert internal package imports to be relative * Reformat JSON files with Prettier * Improve tests based on review comments * Add test for zeroconf when no data is provided * Cleanup and extended tests
This commit is contained in:
parent
3d2ff841d3
commit
78b83c653a
@ -339,6 +339,7 @@ homeassistant/components/weblink/* @home-assistant/core
|
||||
homeassistant/components/websocket_api/* @home-assistant/core
|
||||
homeassistant/components/wemo/* @sqldiablo
|
||||
homeassistant/components/withings/* @vangorra
|
||||
homeassistant/components/wled/* @frenck
|
||||
homeassistant/components/worldclock/* @fabaff
|
||||
homeassistant/components/wwlln/* @bachya
|
||||
homeassistant/components/xbox_live/* @MartinHjelmare
|
||||
|
26
homeassistant/components/wled/.translations/en.json
Normal file
26
homeassistant/components/wled/.translations/en.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "WLED",
|
||||
"flow_title": "WLED: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your WLED",
|
||||
"description": "Set up your WLED to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "Host or IP address"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the WLED named `{name}` to Home Assistant?",
|
||||
"title": "Discovered WLED device"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to WLED device."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This WLED device is already configured.",
|
||||
"connection_error": "Failed to connect to WLED device."
|
||||
}
|
||||
}
|
||||
}
|
182
homeassistant/components/wled/__init__.py
Normal file
182
homeassistant/components/wled/__init__.py
Normal file
@ -0,0 +1,182 @@
|
||||
"""Support for WLED."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from wled import WLED, WLEDConnectionError, WLEDError
|
||||
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_NAME, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTR_IDENTIFIERS,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL,
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
DATA_WLED_CLIENT,
|
||||
DATA_WLED_TIMER,
|
||||
DATA_WLED_UPDATED,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the WLED components."""
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up WLED from a config entry."""
|
||||
|
||||
# Create WLED instance for this entry
|
||||
session = async_get_clientsession(hass)
|
||||
wled = WLED(entry.data[CONF_HOST], loop=hass.loop, session=session)
|
||||
|
||||
# Ensure we can connect and talk to it
|
||||
try:
|
||||
await wled.update()
|
||||
except WLEDConnectionError as exception:
|
||||
raise ConfigEntryNotReady from exception
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled}
|
||||
|
||||
# Set up all platforms for this device/entry.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN)
|
||||
)
|
||||
|
||||
async def interval_update(now: dt_util.dt.datetime = None) -> None:
|
||||
"""Poll WLED device function, dispatches event after update."""
|
||||
try:
|
||||
await wled.update()
|
||||
except WLEDError:
|
||||
_LOGGER.debug("An error occurred while updating WLED", exc_info=True)
|
||||
|
||||
# Even if the update failed, we still send out the event.
|
||||
# To allow entities to make themselves unavailable.
|
||||
async_dispatcher_send(hass, DATA_WLED_UPDATED, entry.entry_id)
|
||||
|
||||
# Schedule update interval
|
||||
hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] = async_track_time_interval(
|
||||
hass, interval_update, SCAN_INTERVAL
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload WLED config entry."""
|
||||
|
||||
# Cancel update timer for this entry/device.
|
||||
cancel_timer = hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER]
|
||||
cancel_timer()
|
||||
|
||||
# Unload entities for this entry/device.
|
||||
await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN)
|
||||
|
||||
# Cleanup
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
if not hass.data[DOMAIN]:
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class WLEDEntity(Entity):
|
||||
"""Defines a base WLED entity."""
|
||||
|
||||
def __init__(self, entry_id: str, wled: WLED, name: str, icon: str) -> None:
|
||||
"""Initialize the WLED entity."""
|
||||
self._attributes: Dict[str, Union[str, int, float]] = {}
|
||||
self._available = True
|
||||
self._entry_id = entry_id
|
||||
self._icon = icon
|
||||
self._name = name
|
||||
self._unsub_dispatcher = None
|
||||
self.wled = wled
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return the mdi icon of the entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return the polling requirement of the entity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> Optional[Dict[str, Any]]:
|
||||
"""Return the state attributes of the entity."""
|
||||
return self._attributes
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect to dispatcher listening for entity data notifications."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, DATA_WLED_UPDATED, self._schedule_immediate_update
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect from update signal."""
|
||||
self._unsub_dispatcher()
|
||||
|
||||
@callback
|
||||
def _schedule_immediate_update(self, entry_id: str) -> None:
|
||||
"""Schedule an immediate update of the entity."""
|
||||
if entry_id == self._entry_id:
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update WLED entity."""
|
||||
if self.wled.device is None:
|
||||
self._available = False
|
||||
return
|
||||
|
||||
self._available = True
|
||||
await self._wled_update()
|
||||
|
||||
async def _wled_update(self) -> None:
|
||||
"""Update WLED entity."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class WLEDDeviceEntity(WLEDEntity):
|
||||
"""Defines a WLED device entity."""
|
||||
|
||||
@property
|
||||
def device_info(self) -> Dict[str, Any]:
|
||||
"""Return device information about this WLED device."""
|
||||
return {
|
||||
ATTR_IDENTIFIERS: {(DOMAIN, self.wled.device.info.mac_address)},
|
||||
ATTR_NAME: self.wled.device.info.name,
|
||||
ATTR_MANUFACTURER: self.wled.device.info.brand,
|
||||
ATTR_MODEL: self.wled.device.info.product,
|
||||
ATTR_SOFTWARE_VERSION: self.wled.device.info.version,
|
||||
}
|
123
homeassistant/components/wled/config_flow.py
Normal file
123
homeassistant/components/wled/config_flow.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""Config flow to configure the WLED integration."""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
from wled import WLED, WLEDConnectionError
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
CONN_CLASS_LOCAL_POLL,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.helpers import ConfigType
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN # pylint: disable=W0611
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a WLED config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Optional[ConfigType] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initiated by the user."""
|
||||
return await self._handle_config_flow(user_input)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, user_input: Optional[ConfigType] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle zeroconf discovery."""
|
||||
if user_input is None:
|
||||
return self.async_abort(reason="connection_error")
|
||||
|
||||
# Hostname is format: wled-livingroom.local.
|
||||
host = user_input["hostname"].rstrip(".")
|
||||
name, _ = host.rsplit(".")
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update(
|
||||
{CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": name}}
|
||||
)
|
||||
|
||||
# Prepare configuration flow
|
||||
return await self._handle_config_flow(user_input, True)
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: ConfigType = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
return await self._handle_config_flow(user_input)
|
||||
|
||||
async def _handle_config_flow(
|
||||
self, user_input: Optional[ConfigType] = None, prepare: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""Config flow handler for WLED."""
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
source = self.context.get("source")
|
||||
|
||||
# Request user input, unless we are preparing discovery flow
|
||||
if user_input is None and not prepare:
|
||||
if source == SOURCE_ZEROCONF:
|
||||
return self._show_confirm_dialog()
|
||||
return self._show_setup_form()
|
||||
|
||||
if source == SOURCE_ZEROCONF:
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
user_input[CONF_HOST] = self.context.get(CONF_HOST)
|
||||
|
||||
errors = {}
|
||||
session = async_get_clientsession(self.hass)
|
||||
wled = WLED(user_input[CONF_HOST], loop=self.hass.loop, session=session)
|
||||
|
||||
try:
|
||||
device = await wled.update()
|
||||
except WLEDConnectionError:
|
||||
if source == SOURCE_ZEROCONF:
|
||||
return self.async_abort(reason="connection_error")
|
||||
errors["base"] = "connection_error"
|
||||
return self._show_setup_form(errors)
|
||||
|
||||
# Check if already configured
|
||||
mac_address = device.info.mac_address
|
||||
for entry in self._async_current_entries():
|
||||
if entry.data[CONF_MAC] == mac_address:
|
||||
# This mac address is already configured
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
title = user_input[CONF_HOST]
|
||||
if source == SOURCE_ZEROCONF:
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
title = self.context.get(CONF_NAME)
|
||||
|
||||
if prepare:
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: mac_address}
|
||||
)
|
||||
|
||||
def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||
"""Show the confirm dialog to the user."""
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
name = self.context.get(CONF_NAME)
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={"name": name},
|
||||
errors=errors or {},
|
||||
)
|
25
homeassistant/components/wled/const.py
Normal file
25
homeassistant/components/wled/const.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""Constants for the WLED integration."""
|
||||
|
||||
# Integration domain
|
||||
DOMAIN = "wled"
|
||||
|
||||
# Hass data keys
|
||||
DATA_WLED_CLIENT = "wled_client"
|
||||
DATA_WLED_TIMER = "wled_timer"
|
||||
DATA_WLED_UPDATED = "wled_updated"
|
||||
|
||||
# Attributes
|
||||
ATTR_COLOR_PRIMARY = "color_primary"
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_IDENTIFIERS = "identifiers"
|
||||
ATTR_INTENSITY = "intensity"
|
||||
ATTR_MANUFACTURER = "manufacturer"
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_ON = "on"
|
||||
ATTR_PALETTE = "palette"
|
||||
ATTR_PLAYLIST = "playlist"
|
||||
ATTR_PRESET = "preset"
|
||||
ATTR_SEGMENT_ID = "segment_id"
|
||||
ATTR_SOFTWARE_VERSION = "sw_version"
|
||||
ATTR_SPEED = "speed"
|
||||
ATTR_TARGET_BRIGHTNESS = "target_brightness"
|
219
homeassistant/components/wled/light.py
Normal file
219
homeassistant/components/wled/light.py
Normal file
@ -0,0 +1,219 @@
|
||||
"""Support for LED lights."""
|
||||
import logging
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
|
||||
from wled import WLED, Effect, WLEDError
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR,
|
||||
SUPPORT_COLOR_TEMP,
|
||||
SUPPORT_EFFECT,
|
||||
SUPPORT_TRANSITION,
|
||||
Light,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from . import WLEDDeviceEntity
|
||||
from .const import (
|
||||
ATTR_COLOR_PRIMARY,
|
||||
ATTR_INTENSITY,
|
||||
ATTR_ON,
|
||||
ATTR_PALETTE,
|
||||
ATTR_PLAYLIST,
|
||||
ATTR_PRESET,
|
||||
ATTR_SEGMENT_ID,
|
||||
ATTR_SPEED,
|
||||
DATA_WLED_CLIENT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: Callable[[List[Entity], bool], None],
|
||||
) -> None:
|
||||
"""Set up WLED light based on a config entry."""
|
||||
wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT]
|
||||
|
||||
# Does the WLED device support RGBW
|
||||
rgbw = wled.device.info.leds.rgbw
|
||||
|
||||
# List of supported effects
|
||||
effects = wled.device.effects
|
||||
|
||||
# WLED supports splitting a strip in multiple segments
|
||||
# Each segment will be a separate light in Home Assistant
|
||||
lights = []
|
||||
for light in wled.device.state.segments:
|
||||
lights.append(WLEDLight(entry.entry_id, wled, light.segment_id, rgbw, effects))
|
||||
|
||||
async_add_entities(lights, True)
|
||||
|
||||
|
||||
class WLEDLight(Light, WLEDDeviceEntity):
|
||||
"""Defines a WLED light."""
|
||||
|
||||
def __init__(
|
||||
self, entry_id: str, wled: WLED, segment: int, rgbw: bool, effects: List[Effect]
|
||||
):
|
||||
"""Initialize WLED light."""
|
||||
self._effects = effects
|
||||
self._rgbw = rgbw
|
||||
self._segment = segment
|
||||
|
||||
self._brightness: Optional[int] = None
|
||||
self._color: Optional[Tuple[float, float]] = None
|
||||
self._effect: Optional[str] = None
|
||||
self._state: Optional[bool] = None
|
||||
|
||||
# Only apply the segment ID if it is not the first segment
|
||||
name = wled.device.info.name
|
||||
if segment != 0:
|
||||
name += f" {segment}"
|
||||
|
||||
super().__init__(entry_id, wled, name, "mdi:led-strip-variant")
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this sensor."""
|
||||
return f"{self.wled.device.info.mac_address}_{self._segment}"
|
||||
|
||||
@property
|
||||
def hs_color(self) -> Optional[Tuple[float, float]]:
|
||||
"""Return the hue and saturation color value [float, float]."""
|
||||
return self._color
|
||||
|
||||
@property
|
||||
def effect(self) -> Optional[str]:
|
||||
"""Return the current effect of the light."""
|
||||
return self._effect
|
||||
|
||||
@property
|
||||
def brightness(self) -> Optional[int]:
|
||||
"""Return the brightness of this light between 1..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Flag supported features."""
|
||||
return (
|
||||
SUPPORT_BRIGHTNESS
|
||||
| SUPPORT_COLOR
|
||||
| SUPPORT_COLOR_TEMP
|
||||
| SUPPORT_EFFECT
|
||||
| SUPPORT_TRANSITION
|
||||
)
|
||||
|
||||
@property
|
||||
def effect_list(self) -> List[str]:
|
||||
"""Return the list of supported effects."""
|
||||
return [effect.name for effect in self._effects]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the light."""
|
||||
return bool(self._state)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
try:
|
||||
await self.wled.light(on=False)
|
||||
self._state = False
|
||||
except WLEDError:
|
||||
_LOGGER.error("An error occurred while turning off WLED light.")
|
||||
self._available = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment}
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
mireds = color_util.color_temperature_kelvin_to_mired(
|
||||
kwargs[ATTR_COLOR_TEMP]
|
||||
)
|
||||
data[ATTR_COLOR_PRIMARY] = tuple(
|
||||
map(int, color_util.color_temperature_to_rgb(mireds))
|
||||
)
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
hue, sat = kwargs[ATTR_HS_COLOR]
|
||||
data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100)
|
||||
|
||||
if ATTR_TRANSITION in kwargs:
|
||||
data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION]
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
data[ATTR_EFFECT] = kwargs[ATTR_EFFECT]
|
||||
|
||||
# Support for RGBW strips
|
||||
if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs):
|
||||
data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw(
|
||||
*data[ATTR_COLOR_PRIMARY]
|
||||
)
|
||||
|
||||
try:
|
||||
await self.wled.light(**data)
|
||||
|
||||
self._state = True
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
|
||||
if ATTR_EFFECT in kwargs:
|
||||
self._effect = kwargs[ATTR_EFFECT]
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
self._color = kwargs[ATTR_HS_COLOR]
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
self._color = color_util.color_temperature_to_hs(mireds)
|
||||
|
||||
except WLEDError:
|
||||
_LOGGER.error("An error occurred while turning on WLED light.")
|
||||
self._available = False
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
async def _wled_update(self) -> None:
|
||||
"""Update WLED entity."""
|
||||
self._brightness = self.wled.device.state.brightness
|
||||
self._effect = self.wled.device.state.segments[self._segment].effect.name
|
||||
self._state = self.wled.device.state.on
|
||||
|
||||
color = self.wled.device.state.segments[self._segment].color_primary
|
||||
if self._rgbw:
|
||||
color = color_util.color_rgbw_to_rgb(*color)
|
||||
self._color = color_util.color_RGB_to_hs(*color)
|
||||
|
||||
playlist = self.wled.device.state.playlist
|
||||
if playlist == -1:
|
||||
playlist = None
|
||||
|
||||
preset = self.wled.device.state.preset
|
||||
if preset == -1:
|
||||
preset = None
|
||||
|
||||
self._attributes = {
|
||||
ATTR_INTENSITY: self.wled.device.state.segments[self._segment].intensity,
|
||||
ATTR_PALETTE: self.wled.device.state.segments[self._segment].palette.name,
|
||||
ATTR_PLAYLIST: playlist,
|
||||
ATTR_PRESET: preset,
|
||||
ATTR_SPEED: self.wled.device.state.segments[self._segment].speed,
|
||||
}
|
10
homeassistant/components/wled/manifest.json
Normal file
10
homeassistant/components/wled/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "wled",
|
||||
"name": "WLED",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/wled",
|
||||
"requirements": ["wled==0.1.0"],
|
||||
"dependencies": [],
|
||||
"zeroconf": ["_wled._tcp.local."],
|
||||
"codeowners": ["@frenck"]
|
||||
}
|
26
homeassistant/components/wled/strings.json
Normal file
26
homeassistant/components/wled/strings.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "WLED",
|
||||
"flow_title": "WLED: {name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Link your WLED",
|
||||
"description": "Set up your WLED to integrate with Home Assistant.",
|
||||
"data": {
|
||||
"host": "Host or IP address"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the WLED named `{name}` to Home Assistant?",
|
||||
"title": "Discovered WLED device"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"connection_error": "Failed to connect to WLED device."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "This WLED device is already configured.",
|
||||
"connection_error": "Failed to connect to WLED device."
|
||||
}
|
||||
}
|
||||
}
|
@ -81,6 +81,7 @@ FLOWS = [
|
||||
"vesync",
|
||||
"wemo",
|
||||
"withings",
|
||||
"wled",
|
||||
"wwlln",
|
||||
"zha",
|
||||
"zone",
|
||||
|
@ -20,6 +20,9 @@ ZEROCONF = {
|
||||
],
|
||||
"_hap._tcp.local.": [
|
||||
"homekit_controller"
|
||||
],
|
||||
"_wled._tcp.local.": [
|
||||
"wled"
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1995,6 +1995,9 @@ wirelesstagpy==0.4.0
|
||||
# homeassistant.components.withings
|
||||
withings-api==2.1.3
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.1.0
|
||||
|
||||
# homeassistant.components.wunderlist
|
||||
wunderpy2==0.1.6
|
||||
|
||||
|
@ -617,6 +617,9 @@ websockets==6.0
|
||||
# homeassistant.components.withings
|
||||
withings-api==2.1.3
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.1.0
|
||||
|
||||
# homeassistant.components.bluesound
|
||||
# homeassistant.components.startca
|
||||
# homeassistant.components.ted5000
|
||||
|
42
tests/components/wled/__init__.py
Normal file
42
tests/components/wled/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""Tests for the WLED integration."""
|
||||
|
||||
from homeassistant.components.wled.const import DOMAIN
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
rgbw: bool = False,
|
||||
skip_setup: bool = False,
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the WLED integration in Home Assistant."""
|
||||
|
||||
fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json"
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
text=load_fixture(fixture),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
aioclient_mock.post(
|
||||
"http://example.local:80/json/state",
|
||||
json={"success": True},
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={CONF_HOST: "example.local", CONF_MAC: "aabbccddeeff"}
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
207
tests/components/wled/test_config_flow.py
Normal file
207
tests/components/wled/test_config_flow.py
Normal file
@ -0,0 +1,207 @@
|
||||
"""Tests for the WLED config flow."""
|
||||
import aiohttp
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.wled import config_flow
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_show_user_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the user set up form is served."""
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
result = await flow.async_step_user(user_input=None)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF, CONF_NAME: "test"}
|
||||
result = await flow.async_step_zeroconf_confirm()
|
||||
|
||||
assert result["description_placeholders"] == {CONF_NAME: "test"}
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_show_zerconf_form(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that the zeroconf confirmation form is served."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf({"hostname": "example.local."})
|
||||
|
||||
assert flow.context[CONF_HOST] == "example.local"
|
||||
assert flow.context[CONF_NAME] == "example"
|
||||
assert result["description_placeholders"] == {CONF_NAME: "example"}
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we show user form on WLED connection error."""
|
||||
aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
result = await flow.async_step_user(user_input={CONF_HOST: "example.com"})
|
||||
|
||||
assert result["errors"] == {"base": "connection_error"}
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
|
||||
async def test_zeroconf_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on WLED connection error."""
|
||||
aioclient_mock.get("http://example.local/json/", exc=aiohttp.ClientError)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf(user_input={"hostname": "example.local."})
|
||||
|
||||
assert result["reason"] == "connection_error"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_zeroconf_confirm_connection_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow on WLED connection error."""
|
||||
aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {
|
||||
"source": SOURCE_ZEROCONF,
|
||||
CONF_HOST: "example.com",
|
||||
CONF_NAME: "test",
|
||||
}
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "example.com"}
|
||||
)
|
||||
|
||||
assert result["reason"] == "connection_error"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_zeroconf_no_data(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort if zeroconf provides no data."""
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
result = await flow.async_step_zeroconf()
|
||||
|
||||
assert result["reason"] == "connection_error"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_user_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if WLED device already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
result = await flow.async_step_user({CONF_HOST: "example.local"})
|
||||
|
||||
assert result["reason"] == "already_configured"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_zeroconf_device_exists_abort(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test we abort zeroconf flow if WLED device already configured."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf({"hostname": "example.local."})
|
||||
|
||||
assert result["reason"] == "already_configured"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
|
||||
|
||||
async def test_full_user_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
result = await flow.async_step_user(user_input=None)
|
||||
|
||||
assert result["step_id"] == "user"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await flow.async_step_user(user_input={CONF_HOST: "example.local"})
|
||||
assert result["data"][CONF_HOST] == "example.local"
|
||||
assert result["data"][CONF_MAC] == "aabbccddeeff"
|
||||
assert result["title"] == "example.local"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_full_zeroconf_flow_implementation(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the full manual user flow from start to finish."""
|
||||
aioclient_mock.get(
|
||||
"http://example.local:80/json/",
|
||||
text=load_fixture("wled/rgb.json"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
flow = config_flow.WLEDFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_ZEROCONF}
|
||||
result = await flow.async_step_zeroconf({"hostname": "example.local."})
|
||||
|
||||
assert flow.context[CONF_HOST] == "example.local"
|
||||
assert flow.context[CONF_NAME] == "example"
|
||||
assert result["description_placeholders"] == {CONF_NAME: "example"}
|
||||
assert result["step_id"] == "zeroconf_confirm"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await flow.async_step_zeroconf_confirm(
|
||||
user_input={CONF_HOST: "example.local"}
|
||||
)
|
||||
assert result["data"][CONF_HOST] == "example.local"
|
||||
assert result["data"][CONF_MAC] == "aabbccddeeff"
|
||||
assert result["title"] == "example"
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
60
tests/components/wled/test_init.py
Normal file
60
tests/components/wled/test_init.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Tests for the WLED integration."""
|
||||
import aiohttp
|
||||
from asynctest import patch
|
||||
|
||||
from homeassistant.components.wled.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
|
||||
from homeassistant.const import STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.wled import init_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the WLED configuration entry not ready."""
|
||||
aioclient_mock.get("http://example.local:80/json/", exc=aiohttp.ClientError)
|
||||
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
assert entry.state == ENTRY_STATE_SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_config_entry(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the WLED configuration entry unloading."""
|
||||
entry = await init_integration(hass, aioclient_mock)
|
||||
assert hass.data[DOMAIN]
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_interval_update(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the WLED configuration entry unloading."""
|
||||
entry = await init_integration(hass, aioclient_mock, skip_setup=True)
|
||||
|
||||
interval_action = False
|
||||
|
||||
def async_track_time_interval(hass, action, interval):
|
||||
nonlocal interval_action
|
||||
interval_action = action
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.wled.async_track_time_interval",
|
||||
new=async_track_time_interval,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert interval_action
|
||||
await interval_action() # pylint: disable=not-callable
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_ON
|
179
tests/components/wled/test_light.py
Normal file
179
tests/components/wled/test_light.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""Tests for the WLED light platform."""
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_EFFECT,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_TRANSITION,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.wled.const import (
|
||||
ATTR_INTENSITY,
|
||||
ATTR_PALETTE,
|
||||
ATTR_PLAYLIST,
|
||||
ATTR_PRESET,
|
||||
ATTR_SPEED,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_ICON,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.wled import init_integration
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
async def test_rgb_light_state(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the creation and values of the WLED lights."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
entity_registry = await hass.helpers.entity_registry.async_get_registry()
|
||||
|
||||
# First segment of the strip
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 127
|
||||
assert state.attributes.get(ATTR_EFFECT) == "Solid"
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (37.412, 100.0)
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant"
|
||||
assert state.attributes.get(ATTR_INTENSITY) == 128
|
||||
assert state.attributes.get(ATTR_PALETTE) == "Default"
|
||||
assert state.attributes.get(ATTR_PLAYLIST) is None
|
||||
assert state.attributes.get(ATTR_PRESET) is None
|
||||
assert state.attributes.get(ATTR_SPEED) == 32
|
||||
assert state.state == STATE_ON
|
||||
|
||||
entry = entity_registry.async_get("light.wled_rgb_light")
|
||||
assert entry
|
||||
assert entry.unique_id == "aabbccddeeff_0"
|
||||
|
||||
# Second segment of the strip
|
||||
state = hass.states.get("light.wled_rgb_light_1")
|
||||
assert state
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 127
|
||||
assert state.attributes.get(ATTR_EFFECT) == "Blink"
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (148.941, 100.0)
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant"
|
||||
assert state.attributes.get(ATTR_INTENSITY) == 64
|
||||
assert state.attributes.get(ATTR_PALETTE) == "Random Cycle"
|
||||
assert state.attributes.get(ATTR_PLAYLIST) is None
|
||||
assert state.attributes.get(ATTR_PRESET) is None
|
||||
assert state.attributes.get(ATTR_SPEED) == 16
|
||||
assert state.state == STATE_ON
|
||||
|
||||
entry = entity_registry.async_get("light.wled_rgb_light_1")
|
||||
assert entry
|
||||
assert entry.unique_id == "aabbccddeeff_1"
|
||||
|
||||
|
||||
async def test_switch_change_state(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test the change of state of the WLED switches."""
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.wled_rgb_light"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_BRIGHTNESS: 42,
|
||||
ATTR_EFFECT: "Chase",
|
||||
ATTR_ENTITY_ID: "light.wled_rgb_light",
|
||||
ATTR_RGB_COLOR: [255, 0, 0],
|
||||
ATTR_TRANSITION: 5,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == 42
|
||||
assert state.attributes.get(ATTR_EFFECT) == "Chase"
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522)
|
||||
|
||||
|
||||
async def test_light_error(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test error handling of the WLED switches."""
|
||||
aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError)
|
||||
await init_integration(hass, aioclient_mock)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: "light.wled_rgb_light"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.wled_rgb_light")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.wled_rgb_light_1"},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("light.wled_rgb_light_1")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_rgbw_light(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test RGBW support for WLED."""
|
||||
await init_integration(hass, aioclient_mock, rgbw=True)
|
||||
|
||||
state = hass.states.get("light.wled_rgbw_light")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.wled_rgbw_light")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522)
|
210
tests/fixtures/wled/rgb.json
vendored
Normal file
210
tests/fixtures/wled/rgb.json
vendored
Normal file
@ -0,0 +1,210 @@
|
||||
{
|
||||
"state": {
|
||||
"on": true,
|
||||
"bri": 127,
|
||||
"transition": 7,
|
||||
"ps": -1,
|
||||
"pl": -1,
|
||||
"nl": {
|
||||
"on": false,
|
||||
"dur": 60,
|
||||
"fade": true,
|
||||
"tbri": 0
|
||||
},
|
||||
"udpn": {
|
||||
"send": false,
|
||||
"recv": true
|
||||
},
|
||||
"seg": [
|
||||
{
|
||||
"id": 0,
|
||||
"start": 0,
|
||||
"stop": 19,
|
||||
"len": 20,
|
||||
"col": [[255, 159, 0], [0, 0, 0], [0, 0, 0]],
|
||||
"fx": 0,
|
||||
"sx": 32,
|
||||
"ix": 128,
|
||||
"pal": 0,
|
||||
"sel": true,
|
||||
"rev": false,
|
||||
"cln": -1
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"start": 20,
|
||||
"stop": 30,
|
||||
"len": 10,
|
||||
"col": [[0, 255, 123], [0, 0, 0], [0, 0, 0]],
|
||||
"fx": 1,
|
||||
"sx": 16,
|
||||
"ix": 64,
|
||||
"pal": 1,
|
||||
"sel": true,
|
||||
"rev": false,
|
||||
"cln": -1
|
||||
}
|
||||
]
|
||||
},
|
||||
"info": {
|
||||
"ver": "0.8.5",
|
||||
"vid": 1909122,
|
||||
"leds": {
|
||||
"count": 30,
|
||||
"rgbw": false,
|
||||
"pin": [2],
|
||||
"pwr": 470,
|
||||
"maxpwr": 850,
|
||||
"maxseg": 10
|
||||
},
|
||||
"name": "WLED RGB Light",
|
||||
"udpport": 21324,
|
||||
"live": false,
|
||||
"fxcount": 81,
|
||||
"palcount": 50,
|
||||
"arch": "esp8266",
|
||||
"core": "2_4_2",
|
||||
"freeheap": 14600,
|
||||
"uptime": 32,
|
||||
"opt": 119,
|
||||
"brand": "WLED",
|
||||
"product": "DIY light",
|
||||
"btype": "bin",
|
||||
"mac": "aabbccddeeff"
|
||||
},
|
||||
"effects": [
|
||||
"Solid",
|
||||
"Blink",
|
||||
"Breathe",
|
||||
"Wipe",
|
||||
"Wipe Random",
|
||||
"Random Colors",
|
||||
"Sweep",
|
||||
"Dynamic",
|
||||
"Colorloop",
|
||||
"Rainbow",
|
||||
"Scan",
|
||||
"Dual Scan",
|
||||
"Fade",
|
||||
"Chase",
|
||||
"Chase Rainbow",
|
||||
"Running",
|
||||
"Saw",
|
||||
"Twinkle",
|
||||
"Dissolve",
|
||||
"Dissolve Rnd",
|
||||
"Sparkle",
|
||||
"Dark Sparkle",
|
||||
"Sparkle+",
|
||||
"Strobe",
|
||||
"Strobe Rainbow",
|
||||
"Mega Strobe",
|
||||
"Blink Rainbow",
|
||||
"Android",
|
||||
"Chase",
|
||||
"Chase Random",
|
||||
"Chase Rainbow",
|
||||
"Chase Flash",
|
||||
"Chase Flash Rnd",
|
||||
"Rainbow Runner",
|
||||
"Colorful",
|
||||
"Traffic Light",
|
||||
"Sweep Random",
|
||||
"Running 2",
|
||||
"Red & Blue",
|
||||
"Stream",
|
||||
"Scanner",
|
||||
"Lighthouse",
|
||||
"Fireworks",
|
||||
"Rain",
|
||||
"Merry Christmas",
|
||||
"Fire Flicker",
|
||||
"Gradient",
|
||||
"Loading",
|
||||
"In Out",
|
||||
"In In",
|
||||
"Out Out",
|
||||
"Out In",
|
||||
"Circus",
|
||||
"Halloween",
|
||||
"Tri Chase",
|
||||
"Tri Wipe",
|
||||
"Tri Fade",
|
||||
"Lightning",
|
||||
"ICU",
|
||||
"Multi Comet",
|
||||
"Dual Scanner",
|
||||
"Stream 2",
|
||||
"Oscillate",
|
||||
"Pride 2015",
|
||||
"Juggle",
|
||||
"Palette",
|
||||
"Fire 2012",
|
||||
"Colorwaves",
|
||||
"BPM",
|
||||
"Fill Noise",
|
||||
"Noise 1",
|
||||
"Noise 2",
|
||||
"Noise 3",
|
||||
"Noise 4",
|
||||
"Colortwinkle",
|
||||
"Lake",
|
||||
"Meteor",
|
||||
"Smooth Meteor",
|
||||
"Railway",
|
||||
"Ripple",
|
||||
"Twinklefox"
|
||||
],
|
||||
"palettes": [
|
||||
"Default",
|
||||
"Random Cycle",
|
||||
"Primary Color",
|
||||
"Based on Primary",
|
||||
"Set Colors",
|
||||
"Based on Set",
|
||||
"Party",
|
||||
"Cloud",
|
||||
"Lava",
|
||||
"Ocean",
|
||||
"Forest",
|
||||
"Rainbow",
|
||||
"Rainbow Bands",
|
||||
"Sunset",
|
||||
"Rivendell",
|
||||
"Breeze",
|
||||
"Red & Blue",
|
||||
"Yellowout",
|
||||
"Analogous",
|
||||
"Splash",
|
||||
"Pastel",
|
||||
"Sunset 2",
|
||||
"Beech",
|
||||
"Vintage",
|
||||
"Departure",
|
||||
"Landscape",
|
||||
"Beach",
|
||||
"Sherbet",
|
||||
"Hult",
|
||||
"Hult 64",
|
||||
"Drywet",
|
||||
"Jul",
|
||||
"Grintage",
|
||||
"Rewhi",
|
||||
"Tertiary",
|
||||
"Fire",
|
||||
"Icefire",
|
||||
"Cyane",
|
||||
"Light Pink",
|
||||
"Autumn",
|
||||
"Magenta",
|
||||
"Magred",
|
||||
"Yelmag",
|
||||
"Yelblu",
|
||||
"Orange & Teal",
|
||||
"Tiamat",
|
||||
"April Night",
|
||||
"Orangery",
|
||||
"C9",
|
||||
"Sakura"
|
||||
]
|
||||
}
|
198
tests/fixtures/wled/rgbw.json
vendored
Normal file
198
tests/fixtures/wled/rgbw.json
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
{
|
||||
"state": {
|
||||
"on": true,
|
||||
"bri": 140,
|
||||
"transition": 7,
|
||||
"ps": -1,
|
||||
"pl": -1,
|
||||
"nl": {
|
||||
"on": false,
|
||||
"dur": 60,
|
||||
"fade": true,
|
||||
"tbri": 0
|
||||
},
|
||||
"udpn": {
|
||||
"send": false,
|
||||
"recv": true
|
||||
},
|
||||
"seg": [
|
||||
{
|
||||
"id": 0,
|
||||
"start": 0,
|
||||
"stop": 13,
|
||||
"len": 13,
|
||||
"col": [[255, 0, 0, 139], [0, 0, 0, 0], [0, 0, 0, 0]],
|
||||
"fx": 9,
|
||||
"sx": 165,
|
||||
"ix": 128,
|
||||
"pal": 0,
|
||||
"sel": true,
|
||||
"rev": false,
|
||||
"cln": -1
|
||||
}
|
||||
]
|
||||
},
|
||||
"info": {
|
||||
"ver": "0.8.6",
|
||||
"vid": 1910255,
|
||||
"leds": {
|
||||
"count": 13,
|
||||
"rgbw": true,
|
||||
"pin": [2],
|
||||
"pwr": 208,
|
||||
"maxpwr": 850,
|
||||
"maxseg": 10
|
||||
},
|
||||
"name": "WLED RGBW Light",
|
||||
"udpport": 21324,
|
||||
"live": false,
|
||||
"fxcount": 83,
|
||||
"palcount": 50,
|
||||
"arch": "esp8266",
|
||||
"core": "2_5_2",
|
||||
"freeheap": 20136,
|
||||
"uptime": 5591,
|
||||
"opt": 119,
|
||||
"brand": "WLED",
|
||||
"product": "DIY light",
|
||||
"btype": "bin",
|
||||
"mac": "aabbccddee11"
|
||||
},
|
||||
"effects": [
|
||||
"Solid",
|
||||
"Blink",
|
||||
"Breathe",
|
||||
"Wipe",
|
||||
"Wipe Random",
|
||||
"Random Colors",
|
||||
"Sweep",
|
||||
"Dynamic",
|
||||
"Colorloop",
|
||||
"Rainbow",
|
||||
"Scan",
|
||||
"Dual Scan",
|
||||
"Fade",
|
||||
"Chase",
|
||||
"Chase Rainbow",
|
||||
"Running",
|
||||
"Saw",
|
||||
"Twinkle",
|
||||
"Dissolve",
|
||||
"Dissolve Rnd",
|
||||
"Sparkle",
|
||||
"Dark Sparkle",
|
||||
"Sparkle+",
|
||||
"Strobe",
|
||||
"Strobe Rainbow",
|
||||
"Mega Strobe",
|
||||
"Blink Rainbow",
|
||||
"Android",
|
||||
"Chase",
|
||||
"Chase Random",
|
||||
"Chase Rainbow",
|
||||
"Chase Flash",
|
||||
"Chase Flash Rnd",
|
||||
"Rainbow Runner",
|
||||
"Colorful",
|
||||
"Traffic Light",
|
||||
"Sweep Random",
|
||||
"Running 2",
|
||||
"Red & Blue",
|
||||
"Stream",
|
||||
"Scanner",
|
||||
"Lighthouse",
|
||||
"Fireworks",
|
||||
"Rain",
|
||||
"Merry Christmas",
|
||||
"Fire Flicker",
|
||||
"Gradient",
|
||||
"Loading",
|
||||
"In Out",
|
||||
"In In",
|
||||
"Out Out",
|
||||
"Out In",
|
||||
"Circus",
|
||||
"Halloween",
|
||||
"Tri Chase",
|
||||
"Tri Wipe",
|
||||
"Tri Fade",
|
||||
"Lightning",
|
||||
"ICU",
|
||||
"Multi Comet",
|
||||
"Dual Scanner",
|
||||
"Stream 2",
|
||||
"Oscillate",
|
||||
"Pride 2015",
|
||||
"Juggle",
|
||||
"Palette",
|
||||
"Fire 2012",
|
||||
"Colorwaves",
|
||||
"BPM",
|
||||
"Fill Noise",
|
||||
"Noise 1",
|
||||
"Noise 2",
|
||||
"Noise 3",
|
||||
"Noise 4",
|
||||
"Colortwinkles",
|
||||
"Lake",
|
||||
"Meteor",
|
||||
"Smooth Meteor",
|
||||
"Railway",
|
||||
"Ripple",
|
||||
"Twinklefox",
|
||||
"Twinklecat",
|
||||
"Halloween Eyes"
|
||||
],
|
||||
"palettes": [
|
||||
"Default",
|
||||
"Random Cycle",
|
||||
"Primary Color",
|
||||
"Based on Primary",
|
||||
"Set Colors",
|
||||
"Based on Set",
|
||||
"Party",
|
||||
"Cloud",
|
||||
"Lava",
|
||||
"Ocean",
|
||||
"Forest",
|
||||
"Rainbow",
|
||||
"Rainbow Bands",
|
||||
"Sunset",
|
||||
"Rivendell",
|
||||
"Breeze",
|
||||
"Red & Blue",
|
||||
"Yellowout",
|
||||
"Analogous",
|
||||
"Splash",
|
||||
"Pastel",
|
||||
"Sunset 2",
|
||||
"Beech",
|
||||
"Vintage",
|
||||
"Departure",
|
||||
"Landscape",
|
||||
"Beach",
|
||||
"Sherbet",
|
||||
"Hult",
|
||||
"Hult 64",
|
||||
"Drywet",
|
||||
"Jul",
|
||||
"Grintage",
|
||||
"Rewhi",
|
||||
"Tertiary",
|
||||
"Fire",
|
||||
"Icefire",
|
||||
"Cyane",
|
||||
"Light Pink",
|
||||
"Autumn",
|
||||
"Magenta",
|
||||
"Magred",
|
||||
"Yelmag",
|
||||
"Yelblu",
|
||||
"Orange & Teal",
|
||||
"Tiamat",
|
||||
"April Night",
|
||||
"Orangery",
|
||||
"C9",
|
||||
"Sakura"
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user