Add Color Palette Select entities to WLED (#51994)

* Add Color Palette Select entities to WLED

* Update with dev changes, disable by default
This commit is contained in:
Franck Nijhof 2021-06-24 20:25:21 +02:00 committed by GitHub
parent 5695710463
commit fba7118d44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 361 additions and 1 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import WLEDDataUpdateCoordinator
PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
PLATFORMS = (LIGHT_DOMAIN, SELECT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@ -0,0 +1,100 @@
"""Support for LED selects."""
from __future__ import annotations
from functools import partial
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import WLEDDataUpdateCoordinator
from .helpers import wled_exception_handler
from .models import WLEDEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up WLED select based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
update_segments = partial(
async_update_segments,
coordinator,
{},
async_add_entities,
)
coordinator.async_add_listener(update_segments)
update_segments()
class WLEDPaletteSelect(WLEDEntity, SelectEntity):
"""Defines a WLED Palette select."""
_attr_icon = "mdi:palette-outline"
_segment: int
_attr_entity_registry_enabled_default = False
def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None:
"""Initialize WLED ."""
super().__init__(coordinator=coordinator)
# Segment 0 uses a simpler name, which is more natural for when using
# a single segment / using WLED with one big LED strip.
self._attr_name = (
f"{coordinator.data.info.name} Segment {segment} Color Palette"
)
if segment == 0:
self._attr_name = f"{coordinator.data.info.name} Color Palette"
self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}"
self._attr_options = [
palette.name for palette in self.coordinator.data.palettes
]
self._segment = segment
@property
def available(self) -> bool:
"""Return True if entity is available."""
try:
self.coordinator.data.state.segments[self._segment]
except IndexError:
return False
return super().available
@property
def current_option(self) -> str | None:
"""Return the current selected color palette."""
return self.coordinator.data.state.segments[self._segment].palette.name
@wled_exception_handler
async def async_select_option(self, option: str) -> None:
"""Set WLED segment to the selected color palette."""
await self.coordinator.wled.segment(segment_id=self._segment, palette=option)
@callback
def async_update_segments(
coordinator: WLEDDataUpdateCoordinator,
current: dict[int, WLEDPaletteSelect],
async_add_entities,
) -> None:
"""Update segments."""
segment_ids = {segment.segment_id for segment in coordinator.data.state.segments}
current_ids = set(current)
new_entities = []
# Process new segments, add them to Home Assistant
for segment_id in segment_ids - current_ids:
current[segment_id] = WLEDPaletteSelect(coordinator, segment_id)
new_entities.append(current[segment_id])
if new_entities:
async_add_entities(new_entities)

View File

@ -0,0 +1,259 @@
"""Tests for the WLED select platform."""
import json
from unittest.mock import MagicMock
import pytest
from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS
from homeassistant.components.wled.const import DOMAIN, SCAN_INTERVAL
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_ICON,
SERVICE_SELECT_OPTION,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
import homeassistant.util.dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture
@pytest.fixture
async def enable_all(hass: HomeAssistant) -> None:
"""Enable all disabled by default select entities."""
registry = er.async_get(hass)
# Pre-create registry entries for disabled by default sensors
registry.async_get_or_create(
SELECT_DOMAIN,
DOMAIN,
"aabbccddeeff_palette_0",
suggested_object_id="wled_rgb_light_color_palette",
disabled_by=None,
)
registry.async_get_or_create(
SELECT_DOMAIN,
DOMAIN,
"aabbccddeeff_palette_1",
suggested_object_id="wled_rgb_light_segment_1_color_palette",
disabled_by=None,
)
async def test_select_state(
hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry
) -> None:
"""Test the creation and values of the WLED selects."""
entity_registry = er.async_get(hass)
# First segment of the strip
state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:palette-outline"
assert state.attributes.get(ATTR_OPTIONS) == [
"Analogous",
"April Night",
"Autumn",
"Based on Primary",
"Based on Set",
"Beach",
"Beech",
"Breeze",
"C9",
"Cloud",
"Cyane",
"Default",
"Departure",
"Drywet",
"Fire",
"Forest",
"Grintage",
"Hult",
"Hult 64",
"Icefire",
"Jul",
"Landscape",
"Lava",
"Light Pink",
"Magenta",
"Magred",
"Ocean",
"Orange & Teal",
"Orangery",
"Party",
"Pastel",
"Primary Color",
"Rainbow",
"Rainbow Bands",
"Random Cycle",
"Red & Blue",
"Rewhi",
"Rivendell",
"Sakura",
"Set Colors",
"Sherbet",
"Splash",
"Sunset",
"Sunset 2",
"Tertiary",
"Tiamat",
"Vintage",
"Yelblu",
"Yellowout",
"Yelmag",
]
assert state.state == "Random Cycle"
entry = entity_registry.async_get("select.wled_rgb_light_segment_1_color_palette")
assert entry
assert entry.unique_id == "aabbccddeeff_palette_1"
async def test_segment_change_state(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
) -> None:
"""Test the option change of state of the WLED segments."""
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
ATTR_OPTION: "Some Other Palette",
},
blocking=True,
)
await hass.async_block_till_done()
assert mock_wled.segment.call_count == 1
mock_wled.segment.assert_called_with(
segment_id=1,
palette="Some Other Palette",
)
@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
async def test_dynamically_handle_segments(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
) -> None:
"""Test if a new/deleted segment is dynamically added/removed."""
segment0 = hass.states.get("select.wled_rgb_light_color_palette")
segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert segment0
assert segment0.state == "Default"
assert not segment1
return_value = mock_wled.update.return_value
mock_wled.update.return_value = WLEDDevice(
json.loads(load_fixture("wled/rgb.json"))
)
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
segment0 = hass.states.get("select.wled_rgb_light_color_palette")
segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert segment0
assert segment0.state == "Default"
assert segment1
assert segment1.state == "Random Cycle"
# Test adding if segment shows up again, including the master entity
mock_wled.update.return_value = return_value
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
segment0 = hass.states.get("select.wled_rgb_light_color_palette")
segment1 = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert segment0
assert segment0.state == "Default"
assert segment1
assert segment1.state == STATE_UNAVAILABLE
async def test_select_error(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.segment.side_effect = WLEDError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
ATTR_OPTION: "Whatever",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert state
assert state.state == "Random Cycle"
assert "Invalid response from API" in caplog.text
assert mock_wled.segment.call_count == 1
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
async def test_select_connection_error(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.segment.side_effect = WLEDConnectionError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette",
ATTR_OPTION: "Whatever",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgb_light_segment_1_color_palette")
assert state
assert state.state == STATE_UNAVAILABLE
assert "Error communicating with API" in caplog.text
assert mock_wled.segment.call_count == 1
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
@pytest.mark.parametrize(
"entity_id",
(
"select.wled_rgb_light_color_palette",
"select.wled_rgb_light_segment_1_color_palette",
),
)
async def test_disabled_by_default_selects(
hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str
) -> None:
"""Test the disabled by default WLED selects."""
registry = er.async_get(hass)
state = hass.states.get(entity_id)
assert state is None
entry = registry.async_get(entity_id)
assert entry
assert entry.disabled
assert entry.disabled_by == er.DISABLED_INTEGRATION