Add preset support to WLED (#52170)

This commit is contained in:
Franck Nijhof 2021-06-24 23:16:07 +02:00 committed by GitHub
parent 0730b375f3
commit f9d65b9196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 317 additions and 15 deletions

View File

@ -5,7 +5,6 @@ from functools import partial
from typing import Any, Tuple, cast
import voluptuous as vol
from wled import Preset
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@ -218,18 +217,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
if playlist == -1:
playlist = None
preset: int | None = None
if isinstance(self.coordinator.data.state.preset, Preset):
preset = self.coordinator.data.state.preset.preset_id
elif self.coordinator.data.state.preset != -1:
preset = self.coordinator.data.state.preset
segment = self.coordinator.data.state.segments[self._segment]
return {
ATTR_INTENSITY: segment.intensity,
ATTR_PALETTE: segment.palette.name,
ATTR_PLAYLIST: playlist,
ATTR_PRESET: preset,
ATTR_REVERSE: segment.reverse,
ATTR_SPEED: segment.speed,
}

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from functools import partial
from wled import Preset
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@ -23,6 +25,9 @@ async def async_setup_entry(
) -> None:
"""Set up WLED select based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([WLEDPresetSelect(coordinator)])
update_segments = partial(
async_update_segments,
coordinator,
@ -33,6 +38,37 @@ async def async_setup_entry(
update_segments()
class WLEDPresetSelect(WLEDEntity, SelectEntity):
"""Defined a WLED Preset select."""
_attr_icon = "mdi:playlist-play"
def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None:
"""Initialize WLED ."""
super().__init__(coordinator=coordinator)
self._attr_name = f"{coordinator.data.info.name} Preset"
self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset"
self._attr_options = [preset.name for preset in self.coordinator.data.presets]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return len(self.coordinator.data.presets) > 0 and super().available
@property
def current_option(self) -> str | None:
"""Return the current selected preset."""
if not isinstance(self.coordinator.data.state.preset, Preset):
return None
return self.coordinator.data.state.preset.name
@wled_exception_handler
async def async_select_option(self, option: str) -> None:
"""Set WLED segment to the selected preset."""
await self.coordinator.wled.preset(preset=option)
class WLEDPaletteSelect(WLEDEntity, SelectEntity):
"""Defines a WLED Palette select."""

View File

@ -13,6 +13,7 @@ from homeassistant.const import (
ATTR_ICON,
SERVICE_SELECT_OPTION,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -44,7 +45,7 @@ async def enable_all(hass: HomeAssistant) -> None:
)
async def test_select_state(
async def test_color_palette_state(
hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry
) -> None:
"""Test the creation and values of the WLED selects."""
@ -113,7 +114,7 @@ async def test_select_state(
assert entry.unique_id == "aabbccddeeff_palette_1"
async def test_segment_change_state(
async def test_color_palette_segment_change_state(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
@ -138,7 +139,7 @@ async def test_segment_change_state(
@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
async def test_dynamically_handle_segments(
async def test_color_palette_dynamically_handle_segments(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
@ -179,7 +180,7 @@ async def test_dynamically_handle_segments(
assert segment1.state == STATE_UNAVAILABLE
async def test_select_error(
async def test_color_palette_select_error(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
@ -208,7 +209,7 @@ async def test_select_error(
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
async def test_select_connection_error(
async def test_color_palette_select_connection_error(
hass: HomeAssistant,
enable_all: None,
init_integration: MockConfigEntry,
@ -237,6 +238,126 @@ async def test_select_connection_error(
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever")
async def test_preset_unavailable_without_presets(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test WLED preset entity is unavailable when presets are not available."""
state = hass.states.get("select.wled_rgb_light_preset")
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_preset_state(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
) -> None:
"""Test the creation and values of the WLED selects."""
entity_registry = er.async_get(hass)
state = hass.states.get("select.wled_rgbw_light_preset")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:playlist-play"
assert state.attributes.get(ATTR_OPTIONS) == ["Preset 1", "Preset 2"]
assert state.state == "Preset 1"
entry = entity_registry.async_get("select.wled_rgbw_light_preset")
assert entry
assert entry.unique_id == "aabbccddee11_preset"
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
ATTR_OPTION: "Preset 2",
},
blocking=True,
)
await hass.async_block_till_done()
assert mock_wled.preset.call_count == 1
mock_wled.preset.assert_called_with(preset="Preset 2")
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_old_style_preset_active(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test unknown preset returned (when old style/unknown) preset is active."""
# Set device preset state to a random number
mock_wled.update.return_value.state.preset = 99
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_preset")
assert state
assert state.state == STATE_UNKNOWN
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_preset_select_error(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.preset.side_effect = WLEDError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
ATTR_OPTION: "Preset 2",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_preset")
assert state
assert state.state == "Preset 1"
assert "Invalid response from API" in caplog.text
assert mock_wled.preset.call_count == 1
mock_wled.preset.assert_called_with(preset="Preset 2")
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_preset_select_connection_error(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.preset.side_effect = WLEDConnectionError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_preset",
ATTR_OPTION: "Preset 2",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_preset")
assert state
assert state.state == STATE_UNAVAILABLE
assert "Error communicating with API" in caplog.text
assert mock_wled.preset.call_count == 1
mock_wled.preset.assert_called_with(preset="Preset 2")
@pytest.mark.parametrize(
"entity_id",
(

View File

@ -3,7 +3,7 @@
"on": true,
"bri": 140,
"transition": 7,
"ps": -1,
"ps": 1,
"pl": -1,
"nl": {
"on": false,
@ -200,5 +200,158 @@
"Orangery",
"C9",
"Sakura"
],
"presets": {
"0": {},
"1": {
"on": false,
"bri": 255,
"transition": 7,
"mainseg": 0,
"seg": [
{
"id": 0,
"start": 0,
"stop": 13,
"grp": 1,
"spc": 0,
"on": true,
"bri": 255,
"col": [
[
97,
144,
255
],
[
0,
0,
0
],
[
0,
0,
0
]
],
"fx": 9,
"sx": 183,
"ix": 255,
"pal": 1,
"sel": true,
"rev": false,
"mi": false
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
}
],
"n": "Preset 1"
},
"2": {
"on": false,
"bri": 255,
"transition": 7,
"mainseg": 0,
"seg": [
{
"id": 0,
"start": 0,
"stop": 13,
"grp": 1,
"spc": 0,
"on": true,
"bri": 255,
"col": [
[
97,
144,
255
],
[
0,
0,
0
],
[
0,
0,
0
]
],
"fx": 9,
"sx": 183,
"ix": 255,
"pal": 1,
"sel": true,
"rev": false,
"mi": false
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
},
{
"stop": 0
}
],
"n": "Preset 2"
}
}
}