Allow keeping master light in WLED (#51759)
parent
779ef3c8e1
commit
cfce71d7df
homeassistant/components/wled
tests/components/wled
|
@ -31,6 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
# Set up all platforms for this device/entry.
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
# Reload entry when its updated.
|
||||
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -48,3 +51,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
del hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Reload the config entry when it changed."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
|
|
@ -6,13 +6,19 @@ from typing import Any
|
|||
import voluptuous as vol
|
||||
from wled import WLED, WLEDConnectionError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT, DOMAIN
|
||||
|
||||
|
||||
class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
@ -20,6 +26,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> WLEDOptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return WLEDOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
@ -115,3 +127,32 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
description_placeholders={"name": name},
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
|
||||
class WLEDOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle WLED options."""
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize WLED options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage WLED options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_KEEP_MASTER_LIGHT,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT
|
||||
),
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -8,6 +8,10 @@ DOMAIN = "wled"
|
|||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
# Options
|
||||
CONF_KEEP_MASTER_LIGHT = "keep_master_light"
|
||||
DEFAULT_KEEP_MASTER_LIGHT = False
|
||||
|
||||
# Attributes
|
||||
ATTR_COLOR_PRIMARY = "color_primary"
|
||||
ATTR_DURATION = "duration"
|
||||
|
|
|
@ -37,6 +37,8 @@ from .const import (
|
|||
ATTR_REVERSE,
|
||||
ATTR_SEGMENT_ID,
|
||||
ATTR_SPEED,
|
||||
CONF_KEEP_MASTER_LIGHT,
|
||||
DEFAULT_KEEP_MASTER_LIGHT,
|
||||
DOMAIN,
|
||||
SERVICE_EFFECT,
|
||||
SERVICE_PRESET,
|
||||
|
@ -84,8 +86,19 @@ async def async_setup_entry(
|
|||
"async_preset",
|
||||
)
|
||||
|
||||
keep_master_light = entry.options.get(
|
||||
CONF_KEEP_MASTER_LIGHT, DEFAULT_KEEP_MASTER_LIGHT
|
||||
)
|
||||
if keep_master_light:
|
||||
async_add_entities([WLEDMasterLight(coordinator=coordinator)])
|
||||
|
||||
update_segments = partial(
|
||||
async_update_segments, entry, coordinator, {}, async_add_entities
|
||||
async_update_segments,
|
||||
entry,
|
||||
coordinator,
|
||||
keep_master_light,
|
||||
{},
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
coordinator.async_add_listener(update_segments)
|
||||
|
@ -169,9 +182,15 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
|||
_attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION
|
||||
_attr_icon = "mdi:led-strip-variant"
|
||||
|
||||
def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WLEDDataUpdateCoordinator,
|
||||
segment: int,
|
||||
keep_master_light: bool,
|
||||
) -> None:
|
||||
"""Initialize WLED segment light."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
self._keep_master_light = keep_master_light
|
||||
self._rgbw = coordinator.data.info.leds.rgbw
|
||||
self._wv = coordinator.data.info.leds.wv
|
||||
self._segment = segment
|
||||
|
@ -247,7 +266,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
|||
|
||||
# If this is the one and only segment, calculate brightness based
|
||||
# on the master and segment brightness
|
||||
if len(state.segments) == 1:
|
||||
if not self._keep_master_light and len(state.segments) == 1:
|
||||
return int(
|
||||
(state.segments[self._segment].brightness * state.brightness) / 255
|
||||
)
|
||||
|
@ -280,7 +299,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
|||
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
|
||||
|
||||
# If there is a single segment, control via the master
|
||||
if len(self.coordinator.data.state.segments) == 1:
|
||||
if (
|
||||
not self._keep_master_light
|
||||
and len(self.coordinator.data.state.segments) == 1
|
||||
):
|
||||
await self.coordinator.wled.master(**data) # type: ignore[arg-type]
|
||||
return
|
||||
|
||||
|
@ -313,7 +335,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
|||
|
||||
# When only 1 segment is present, switch along the master, and use
|
||||
# the master for power/brightness control.
|
||||
if len(self.coordinator.data.state.segments) == 1:
|
||||
if (
|
||||
not self._keep_master_light
|
||||
and len(self.coordinator.data.state.segments) == 1
|
||||
):
|
||||
master_data = {ATTR_ON: True}
|
||||
if ATTR_BRIGHTNESS in data:
|
||||
master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS]
|
||||
|
@ -373,6 +398,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
|||
def async_update_segments(
|
||||
entry: ConfigEntry,
|
||||
coordinator: WLEDDataUpdateCoordinator,
|
||||
keep_master_light: bool,
|
||||
current: dict[int, WLEDSegmentLight | WLEDMasterLight],
|
||||
async_add_entities,
|
||||
) -> None:
|
||||
|
@ -383,14 +409,17 @@ def async_update_segments(
|
|||
# Discard master (if present)
|
||||
current_ids.discard(-1)
|
||||
|
||||
# Process new segments, add them to Home Assistant
|
||||
new_entities = []
|
||||
|
||||
# Process new segments, add them to Home Assistant
|
||||
for segment_id in segment_ids - current_ids:
|
||||
current[segment_id] = WLEDSegmentLight(coordinator, segment_id)
|
||||
current[segment_id] = WLEDSegmentLight(
|
||||
coordinator, segment_id, keep_master_light
|
||||
)
|
||||
new_entities.append(current[segment_id])
|
||||
|
||||
# More than 1 segment now? Add master controls
|
||||
if len(current_ids) < 2 and len(segment_ids) > 1:
|
||||
if not keep_master_light and (len(current_ids) < 2 and len(segment_ids) > 1):
|
||||
current[-1] = WLEDMasterLight(coordinator)
|
||||
new_entities.append(current[-1])
|
||||
|
||||
|
@ -404,7 +433,7 @@ def async_update_segments(
|
|||
)
|
||||
|
||||
# Remove master if there is only 1 segment left
|
||||
if len(current_ids) > 1 and len(segment_ids) < 2:
|
||||
if not keep_master_light and len(current_ids) > 1 and len(segment_ids) < 2:
|
||||
coordinator.hass.async_create_task(
|
||||
async_remove_entity(-1, coordinator, current)
|
||||
)
|
||||
|
|
|
@ -20,5 +20,14 @@
|
|||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"keep_master_light": "Keep master light, even with 1 LED segment."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,5 +20,14 @@
|
|||
"title": "Discovered WLED device"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"keep_master_light": "Keep master light, even with 1 LED segment."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ from unittest.mock import MagicMock
|
|||
|
||||
from wled import WLEDConnectionError
|
||||
|
||||
from homeassistant.components.wled.const import DOMAIN
|
||||
from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@ -177,3 +177,26 @@ async def test_zeroconf_with_mac_device_exists_abort(
|
|||
|
||||
assert result.get("type") == RESULT_TYPE_ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test options config flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id)
|
||||
|
||||
assert result.get("type") == RESULT_TYPE_FORM
|
||||
assert result.get("step_id") == "init"
|
||||
assert "flow_id" in result
|
||||
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_KEEP_MASTER_LIGHT: True},
|
||||
)
|
||||
|
||||
assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY
|
||||
assert result2.get("data") == {
|
||||
CONF_KEEP_MASTER_LIGHT: True,
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ from homeassistant.components.wled.const import (
|
|||
ATTR_PRESET,
|
||||
ATTR_REVERSE,
|
||||
ATTR_SPEED,
|
||||
CONF_KEEP_MASTER_LIGHT,
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL,
|
||||
SERVICE_EFFECT,
|
||||
|
@ -588,3 +589,22 @@ async def test_preset_service_error(
|
|||
assert "Invalid response from API" in caplog.text
|
||||
assert mock_wled.preset.call_count == 1
|
||||
mock_wled.preset.assert_called_with(preset=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True)
|
||||
async def test_single_segment_with_keep_master_light(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_wled: MagicMock,
|
||||
) -> None:
|
||||
"""Test the behavior of the integration with a single segment."""
|
||||
assert not hass.states.get("light.wled_rgb_light_master")
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
init_integration, options={CONF_KEEP_MASTER_LIGHT: True}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.wled_rgb_light_master")
|
||||
assert state
|
||||
assert state.state == STATE_ON
|
||||
|
|
Loading…
Reference in New Issue