Allow keeping master light in WLED (#51759)

This commit is contained in:
Franck Nijhof 2021-06-12 13:33:23 +02:00 committed by GitHub
parent 779ef3c8e1
commit cfce71d7df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 12 deletions

View file

@ -31,6 +31,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Set up all platforms for this device/entry. # Set up all platforms for this device/entry.
hass.config_entries.async_setup_platforms(entry, PLATFORMS) 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 return True
@ -48,3 +51,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
del hass.data[DOMAIN][entry.entry_id] del hass.data[DOMAIN][entry.entry_id]
return unload_ok 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)

View file

@ -6,13 +6,19 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from wled import WLED, WLEDConnectionError 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.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType 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): class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
@ -20,6 +26,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
VERSION = 1 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( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
@ -115,3 +127,32 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN):
description_placeholders={"name": name}, description_placeholders={"name": name},
errors=errors or {}, 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,
}
),
)

View file

@ -8,6 +8,10 @@ DOMAIN = "wled"
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=10)
# Options
CONF_KEEP_MASTER_LIGHT = "keep_master_light"
DEFAULT_KEEP_MASTER_LIGHT = False
# Attributes # Attributes
ATTR_COLOR_PRIMARY = "color_primary" ATTR_COLOR_PRIMARY = "color_primary"
ATTR_DURATION = "duration" ATTR_DURATION = "duration"

View file

@ -37,6 +37,8 @@ from .const import (
ATTR_REVERSE, ATTR_REVERSE,
ATTR_SEGMENT_ID, ATTR_SEGMENT_ID,
ATTR_SPEED, ATTR_SPEED,
CONF_KEEP_MASTER_LIGHT,
DEFAULT_KEEP_MASTER_LIGHT,
DOMAIN, DOMAIN,
SERVICE_EFFECT, SERVICE_EFFECT,
SERVICE_PRESET, SERVICE_PRESET,
@ -84,8 +86,19 @@ async def async_setup_entry(
"async_preset", "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( 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) coordinator.async_add_listener(update_segments)
@ -169,9 +182,15 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
_attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION
_attr_icon = "mdi:led-strip-variant" _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.""" """Initialize WLED segment light."""
super().__init__(coordinator=coordinator) super().__init__(coordinator=coordinator)
self._keep_master_light = keep_master_light
self._rgbw = coordinator.data.info.leds.rgbw self._rgbw = coordinator.data.info.leds.rgbw
self._wv = coordinator.data.info.leds.wv self._wv = coordinator.data.info.leds.wv
self._segment = segment self._segment = segment
@ -247,7 +266,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
# If this is the one and only segment, calculate brightness based # If this is the one and only segment, calculate brightness based
# on the master and segment brightness # on the master and segment brightness
if len(state.segments) == 1: if not self._keep_master_light and len(state.segments) == 1:
return int( return int(
(state.segments[self._segment].brightness * state.brightness) / 255 (state.segments[self._segment].brightness * state.brightness) / 255
) )
@ -280,7 +299,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10)
# If there is a single segment, control via the master # 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] await self.coordinator.wled.master(**data) # type: ignore[arg-type]
return return
@ -313,7 +335,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
# When only 1 segment is present, switch along the master, and use # When only 1 segment is present, switch along the master, and use
# the master for power/brightness control. # 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} master_data = {ATTR_ON: True}
if ATTR_BRIGHTNESS in data: if ATTR_BRIGHTNESS in data:
master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS] master_data[ATTR_BRIGHTNESS] = data[ATTR_BRIGHTNESS]
@ -373,6 +398,7 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
def async_update_segments( def async_update_segments(
entry: ConfigEntry, entry: ConfigEntry,
coordinator: WLEDDataUpdateCoordinator, coordinator: WLEDDataUpdateCoordinator,
keep_master_light: bool,
current: dict[int, WLEDSegmentLight | WLEDMasterLight], current: dict[int, WLEDSegmentLight | WLEDMasterLight],
async_add_entities, async_add_entities,
) -> None: ) -> None:
@ -383,14 +409,17 @@ def async_update_segments(
# Discard master (if present) # Discard master (if present)
current_ids.discard(-1) current_ids.discard(-1)
# Process new segments, add them to Home Assistant
new_entities = [] new_entities = []
# Process new segments, add them to Home Assistant
for segment_id in segment_ids - current_ids: 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]) new_entities.append(current[segment_id])
# More than 1 segment now? Add master controls # 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) current[-1] = WLEDMasterLight(coordinator)
new_entities.append(current[-1]) new_entities.append(current[-1])
@ -404,7 +433,7 @@ def async_update_segments(
) )
# Remove master if there is only 1 segment left # 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( coordinator.hass.async_create_task(
async_remove_entity(-1, coordinator, current) async_remove_entity(-1, coordinator, current)
) )

View file

@ -20,5 +20,14 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
} }
},
"options": {
"step": {
"init": {
"data": {
"keep_master_light": "Keep master light, even with 1 LED segment."
}
}
}
} }
} }

View file

@ -20,5 +20,14 @@
"title": "Discovered WLED device" "title": "Discovered WLED device"
} }
} }
},
"options": {
"step": {
"init": {
"data": {
"keep_master_light": "Keep master light, even with 1 LED segment."
}
}
}
} }
} }

View file

@ -3,7 +3,7 @@ from unittest.mock import MagicMock
from wled import WLEDConnectionError 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.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import HomeAssistant 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("type") == RESULT_TYPE_ABORT
assert result.get("reason") == "already_configured" 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,
}

View file

@ -21,6 +21,7 @@ from homeassistant.components.wled.const import (
ATTR_PRESET, ATTR_PRESET,
ATTR_REVERSE, ATTR_REVERSE,
ATTR_SPEED, ATTR_SPEED,
CONF_KEEP_MASTER_LIGHT,
DOMAIN, DOMAIN,
SCAN_INTERVAL, SCAN_INTERVAL,
SERVICE_EFFECT, SERVICE_EFFECT,
@ -588,3 +589,22 @@ async def test_preset_service_error(
assert "Invalid response from API" in caplog.text assert "Invalid response from API" in caplog.text
assert mock_wled.preset.call_count == 1 assert mock_wled.preset.call_count == 1
mock_wled.preset.assert_called_with(preset=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