diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 2082f45c6c3..32782338fe5 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -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) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index bb9d4c0cfe5..7f4d006d122 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -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, + } + ), + ) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 77e404cdd4c..d80dbf16a60 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -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" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index ed1ec22a65c..c9a074bbdce 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -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) ) diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index c42a6cdffb1..9717637fdbb 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -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." + } + } + } } } diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 8ebf6f4d91b..a114d0218ca 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -20,5 +20,14 @@ "title": "Discovered WLED device" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Keep master light, even with 1 LED segment." + } + } + } } } \ No newline at end of file diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index ea0167ed6d5..842e7e332e0 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -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, + } diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index c37f28a882e..3ce0e167cb5 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -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