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

View File

@ -3,6 +3,8 @@ from __future__ import annotations
from functools import partial from functools import partial
from wled import Preset
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -23,6 +25,9 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up WLED select based on a config entry.""" """Set up WLED select based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([WLEDPresetSelect(coordinator)])
update_segments = partial( update_segments = partial(
async_update_segments, async_update_segments,
coordinator, coordinator,
@ -33,6 +38,37 @@ async def async_setup_entry(
update_segments() 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): class WLEDPaletteSelect(WLEDEntity, SelectEntity):
"""Defines a WLED Palette select.""" """Defines a WLED Palette select."""

View File

@ -13,6 +13,7 @@ from homeassistant.const import (
ATTR_ICON, ATTR_ICON,
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er 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 hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry
) -> None: ) -> None:
"""Test the creation and values of the WLED selects.""" """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" assert entry.unique_id == "aabbccddeeff_palette_1"
async def test_segment_change_state( async def test_color_palette_segment_change_state(
hass: HomeAssistant, hass: HomeAssistant,
enable_all: None, enable_all: None,
init_integration: MockConfigEntry, 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) @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, hass: HomeAssistant,
enable_all: None, enable_all: None,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,
@ -179,7 +180,7 @@ async def test_dynamically_handle_segments(
assert segment1.state == STATE_UNAVAILABLE assert segment1.state == STATE_UNAVAILABLE
async def test_select_error( async def test_color_palette_select_error(
hass: HomeAssistant, hass: HomeAssistant,
enable_all: None, enable_all: None,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,
@ -208,7 +209,7 @@ async def test_select_error(
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") 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, hass: HomeAssistant,
enable_all: None, enable_all: None,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,
@ -237,6 +238,126 @@ async def test_select_connection_error(
mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") 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( @pytest.mark.parametrize(
"entity_id", "entity_id",
( (

View File

@ -3,7 +3,7 @@
"on": true, "on": true,
"bri": 140, "bri": 140,
"transition": 7, "transition": 7,
"ps": -1, "ps": 1,
"pl": -1, "pl": -1,
"nl": { "nl": {
"on": false, "on": false,
@ -200,5 +200,158 @@
"Orangery", "Orangery",
"C9", "C9",
"Sakura" "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"
}
}
} }