Add support for changing the white strip type to flux_led (#63943)

This commit is contained in:
J. Nick Koston 2022-01-12 13:03:09 -10:00 committed by GitHub
parent 5622db10b1
commit 1c6ca908d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 304 additions and 54 deletions

View File

@ -7,7 +7,7 @@ from typing import Any, Final, cast
from flux_led import DeviceType
from flux_led.aio import AIOWifiLedBulb
from flux_led.const import ATTR_ID
from flux_led.const import ATTR_ID, WhiteChannelType
from flux_led.scanner import FluxLEDDiscovery
from homeassistant.config_entries import ConfigEntry
@ -23,6 +23,7 @@ from homeassistant.helpers.event import (
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_WHITE_CHANNEL_TYPE,
DISCOVER_SCAN_TIMEOUT,
DOMAIN,
FLUX_LED_DISCOVERY,
@ -56,6 +57,9 @@ PLATFORMS_BY_TYPE: Final = {
}
DISCOVERY_INTERVAL: Final = timedelta(minutes=15)
REQUEST_REFRESH_DELAY: Final = 1.5
NAME_TO_WHITE_CHANNEL_TYPE: Final = {
option.name.lower(): option for option in WhiteChannelType
}
@callback
@ -95,6 +99,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
device: AIOWifiLedBulb = async_wifi_bulb_for_host(host, discovery=discovery)
signal = SIGNAL_STATE_UPDATED.format(device.ipaddr)
device.discovery = discovery
if white_channel_type := entry.data.get(CONF_WHITE_CHANNEL_TYPE):
device.white_channel_channel_type = NAME_TO_WHITE_CHANNEL_TYPE[
white_channel_type
]
@callback
def _async_state_changed(*_: Any) -> None:

View File

@ -28,6 +28,7 @@ FLUX_COLOR_MODE_TO_HASS: Final = {
FLUX_COLOR_MODE_CCT: COLOR_MODE_COLOR_TEMP,
}
MULTI_BRIGHTNESS_COLOR_MODES: Final = {COLOR_MODE_RGBWW, COLOR_MODE_RGBW}
API: Final = "flux_api"
@ -57,6 +58,8 @@ CONF_MINOR_VERSION: Final = "minor_version"
CONF_REMOTE_ACCESS_ENABLED: Final = "remote_access_enabled"
CONF_REMOTE_ACCESS_HOST: Final = "remote_access_host"
CONF_REMOTE_ACCESS_PORT: Final = "remote_access_port"
CONF_WHITE_CHANNEL_TYPE: Final = "white_channel_type"
TRANSITION_GRADUAL: Final = "gradual"
TRANSITION_JUMP: Final = "jump"

View File

@ -7,12 +7,7 @@ from typing import Any, Final
from flux_led.const import MultiColorEffects
from flux_led.protocol import MusicMode
from flux_led.utils import (
color_temp_to_white_levels,
rgbcw_brightness,
rgbcw_to_rgbwc,
rgbw_brightness,
)
from flux_led.utils import rgbcw_brightness, rgbcw_to_rgbwc, rgbw_brightness
import voluptuous as vol
from homeassistant import config_entries
@ -24,7 +19,6 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_WHITE,
COLOR_MODE_RGBWW,
SUPPORT_EFFECT,
SUPPORT_TRANSITION,
LightEntity,
@ -50,6 +44,7 @@ from .const import (
CONF_TRANSITION,
DEFAULT_EFFECT_SPEED,
DOMAIN,
MULTI_BRIGHTNESS_COLOR_MODES,
TRANSITION_GRADUAL,
TRANSITION_JUMP,
TRANSITION_STROBE,
@ -203,9 +198,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
) -> None:
"""Initialize the light."""
super().__init__(coordinator, unique_id, name, None)
self._attr_min_mireds = (
color_temperature_kelvin_to_mired(self._device.max_temp) + 1
) # for rounding
self._attr_min_mireds = color_temperature_kelvin_to_mired(self._device.max_temp)
self._attr_max_mireds = color_temperature_kelvin_to_mired(self._device.min_temp)
self._attr_supported_color_modes = _hass_color_modes(self._device)
custom_effects: list[str] = []
@ -306,20 +299,14 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
# Handle switch to CCT Color Mode
if color_temp_mired := kwargs.get(ATTR_COLOR_TEMP):
color_temp_kelvin = color_temperature_mired_to_kelvin(color_temp_mired)
if self.color_mode != COLOR_MODE_RGBWW:
await self._device.async_set_white_temp(color_temp_kelvin, brightness)
return
# When switching to color temp from RGBWW mode,
# we do not want the overall brightness, we only
# want the brightness of the white channels
brightness = kwargs.get(
ATTR_BRIGHTNESS, self._device.getWhiteTemperature()[1]
)
channels = color_temp_to_white_levels(color_temp_kelvin, brightness)
warm = channels.warm_white
cold = channels.cool_white
await self._device.async_set_levels(r=0, b=0, g=0, w=warm, w2=cold)
if (
ATTR_BRIGHTNESS not in kwargs
and self.color_mode in MULTI_BRIGHTNESS_COLOR_MODES
):
# When switching to color temp from RGBWW or RGB&W mode,
# we do not want the overall brightness of the RGB channels
brightness = max(self._device.rgb)
await self._device.async_set_white_temp(color_temp_kelvin, brightness)
return
# Handle switch to RGB Color Mode
if rgb := kwargs.get(ATTR_RGB_COLOR):

View File

@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.27.45"],
"requirements": ["flux_led==0.28.1"],
"quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push",

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from flux_led.aio import AIOWifiLedBulb
from flux_led.base_device import DeviceType
from flux_led.const import DEFAULT_WHITE_CHANNEL_TYPE, WhiteChannelType
from flux_led.protocol import PowerRestoreState, RemoteConfig
from homeassistant import config_entries
@ -12,9 +13,14 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .const import CONF_WHITE_CHANNEL_TYPE, DOMAIN, FLUX_COLOR_MODE_RGBW
from .coordinator import FluxLedUpdateCoordinator
from .entity import FluxBaseEntity, FluxEntity
from .util import _human_readable_option
NAME_TO_POWER_RESTORE_STATE = {
_human_readable_option(option.name): option for option in PowerRestoreState
}
async def async_setup_entry(
@ -31,6 +37,7 @@ async def async_setup_entry(
| FluxWiringsSelect
| FluxICTypeSelect
| FluxRemoteConfigSelect
| FluxWhiteChannelSelect
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
@ -57,20 +64,30 @@ async def async_setup_entry(
coordinator, unique_id, f"{name} Remote Config", "remote_config"
)
)
if FLUX_COLOR_MODE_RGBW in device.color_modes:
entities.append(FluxWhiteChannelSelect(coordinator.device, entry))
if entities:
async_add_entities(entities)
def _human_readable_option(const_option: str) -> str:
return const_option.replace("_", " ").title()
class FluxConfigAtStartSelect(FluxBaseEntity, SelectEntity):
"""Representation of a flux config entity that only updates at start or change."""
_attr_entity_category = EntityCategory.CONFIG
class FluxPowerStateSelect(FluxBaseEntity, SelectEntity):
class FluxConfigSelect(FluxEntity, SelectEntity):
"""Representation of a flux config entity that updates."""
_attr_entity_category = EntityCategory.CONFIG
class FluxPowerStateSelect(FluxConfigAtStartSelect, SelectEntity):
"""Representation of a Flux power restore state option."""
_attr_icon = "mdi:transmission-tower-off"
_attr_entity_category = EntityCategory.CONFIG
_attr_options = list(NAME_TO_POWER_RESTORE_STATE)
def __init__(
self,
@ -82,10 +99,6 @@ class FluxPowerStateSelect(FluxBaseEntity, SelectEntity):
self._attr_name = f"{entry.data[CONF_NAME]} Power Restored"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_power_restored"
self._name_to_state = {
_human_readable_option(option.name): option for option in PowerRestoreState
}
self._attr_options = list(self._name_to_state)
self._async_set_current_option_from_device()
@callback
@ -98,17 +111,13 @@ class FluxPowerStateSelect(FluxBaseEntity, SelectEntity):
async def async_select_option(self, option: str) -> None:
"""Change the power state."""
await self._device.async_set_power_restore(channel1=self._name_to_state[option])
await self._device.async_set_power_restore(
channel1=NAME_TO_POWER_RESTORE_STATE[option]
)
self._async_set_current_option_from_device()
self.async_write_ha_state()
class FluxConfigSelect(FluxEntity, SelectEntity):
"""Representation of a flux config entity that updates."""
_attr_entity_category = EntityCategory.CONFIG
class FluxICTypeSelect(FluxConfigSelect):
"""Representation of Flux ic type."""
@ -202,3 +211,39 @@ class FluxRemoteConfigSelect(FluxConfigSelect):
"""Change the remote config setting."""
remote_config: RemoteConfig = self._name_to_state[option]
await self._device.async_config_remotes(remote_config)
class FluxWhiteChannelSelect(FluxConfigAtStartSelect):
"""Representation of Flux white channel."""
_attr_options = [_human_readable_option(option.name) for option in WhiteChannelType]
def __init__(
self,
device: AIOWifiLedBulb,
entry: config_entries.ConfigEntry,
) -> None:
"""Initialize the white channel select."""
super().__init__(device, entry)
self._attr_name = f"{entry.data[CONF_NAME]} White Channel"
if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_white_channel"
@property
def current_option(self) -> str | None:
"""Return the current white channel type."""
return _human_readable_option(
self.entry.data.get(
CONF_WHITE_CHANNEL_TYPE, DEFAULT_WHITE_CHANNEL_TYPE.name
)
)
async def async_select_option(self, option: str) -> None:
"""Change the white channel type."""
entry = self.entry
hass = self.hass
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_WHITE_CHANNEL_TYPE: option.lower()}
)
# reload since we need to reinit the device
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))

View File

@ -23,6 +23,10 @@ def format_as_flux_mac(mac: str | None) -> str | None:
return None if mac is None else mac.replace(":", "").upper()
def _human_readable_option(const_option: str) -> str:
return const_option.replace("_", " ").title()
def _flux_color_mode_to_hass(
flux_color_mode: str | None, flux_color_modes: set[str]
) -> str:

View File

@ -681,7 +681,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.45
flux_led==0.28.1
# homeassistant.components.homekit
fnvhash==0.1.0

View File

@ -424,7 +424,7 @@ fjaraskupan==1.0.2
flipr-api==1.4.1
# homeassistant.components.flux_led
flux_led==0.27.45
flux_led==0.28.1
# homeassistant.components.homekit
fnvhash==0.1.0

View File

@ -12,6 +12,7 @@ from flux_led.aio import AIOWifiLedBulb
from flux_led.const import (
COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT,
COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB,
WhiteChannelType,
)
from flux_led.models_db import MODEL_MAP
from flux_led.protocol import (
@ -105,6 +106,7 @@ def _mocked_bulb() -> AIOWifiLedBulb:
bulb.async_set_brightness = AsyncMock()
bulb.async_set_device_config = AsyncMock()
bulb.async_config_remotes = AsyncMock()
bulb.white_channel_channel_type = WhiteChannelType.WARM
bulb.paired_remotes = 2
bulb.pixels_per_segment = 300
bulb.segments = 2

View File

@ -12,6 +12,7 @@ from flux_led.const import (
COLOR_MODES_RGB_W as FLUX_COLOR_MODES_RGB_W,
MODE_MUSIC,
MultiColorEffects,
WhiteChannelType,
)
from flux_led.protocol import MusicMode
import pytest
@ -25,6 +26,7 @@ from homeassistant.components.flux_led.const import (
CONF_EFFECT,
CONF_SPEED_PCT,
CONF_TRANSITION,
CONF_WHITE_CHANNEL_TYPE,
DOMAIN,
TRANSITION_JUMP,
)
@ -520,11 +522,15 @@ async def test_rgb_cct_light(hass: HomeAssistant) -> None:
bulb.async_set_brightness.reset_mock()
async def test_rgbw_light(hass: HomeAssistant) -> None:
"""Test an rgbw light."""
async def test_rgbw_light_cold_white(hass: HomeAssistant) -> None:
"""Test an rgbw light with a cold white channel."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
data={
CONF_HOST: IP_ADDRESS,
CONF_NAME: DEFAULT_ENTRY_TITLE,
CONF_WHITE_CHANNEL_TYPE: WhiteChannelType.COLD.name.lower(),
},
unique_id=MAC_ADDRESS,
)
config_entry.add_to_hass(hass)
@ -622,6 +628,148 @@ async def test_rgbw_light(hass: HomeAssistant) -> None:
bulb.async_set_effect.reset_mock()
async def test_rgbw_light_warm_white(hass: HomeAssistant) -> None:
"""Test an rgbw light with a warm white channel."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: IP_ADDRESS,
CONF_NAME: DEFAULT_ENTRY_TITLE,
CONF_WHITE_CHANNEL_TYPE: WhiteChannelType.WARM.name.lower(),
},
unique_id=MAC_ADDRESS,
)
config_entry.add_to_hass(hass)
bulb = _mocked_bulb()
bulb.color_modes = {FLUX_COLOR_MODE_RGBW, FLUX_COLOR_MODE_CCT}
bulb.color_mode = FLUX_COLOR_MODE_RGBW
with _patch_discovery(), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
entity_id = "light.bulb_rgbcw_ddeeff"
state = hass.states.get(entity_id)
assert state.state == STATE_ON
attributes = state.attributes
assert attributes[ATTR_BRIGHTNESS] == 128
assert attributes[ATTR_COLOR_MODE] == "rgbw"
assert attributes[ATTR_EFFECT_LIST] == bulb.effect_list
assert attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "rgbw"]
assert attributes[ATTR_RGB_COLOR] == (255, 42, 42)
await hass.services.async_call(
LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
bulb.async_turn_off.assert_called_once()
await async_mock_device_turn_off(hass, bulb)
assert hass.states.get(entity_id).state == STATE_OFF
await hass.services.async_call(
LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True
)
bulb.async_turn_on.assert_called_once()
bulb.async_turn_on.reset_mock()
bulb.is_on = True
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100},
blocking=True,
)
bulb.async_set_brightness.assert_called_with(100)
bulb.async_set_brightness.reset_mock()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{
ATTR_ENTITY_ID: entity_id,
ATTR_RGBW_COLOR: (255, 255, 255, 255),
ATTR_BRIGHTNESS: 128,
},
blocking=True,
)
bulb.async_set_levels.assert_called_with(128, 128, 128, 128)
bulb.async_set_levels.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 255, 255, 255)},
blocking=True,
)
bulb.async_set_levels.assert_called_with(255, 255, 255, 255)
bulb.async_set_levels.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)},
blocking=True,
)
bulb.async_set_levels.assert_called_with(255, 191, 178, 0)
bulb.async_set_levels.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154},
blocking=True,
)
bulb.async_set_white_temp.assert_called_with(6493, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255},
blocking=True,
)
bulb.async_set_white_temp.assert_called_with(6493, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290},
blocking=True,
)
bulb.async_set_white_temp.assert_called_with(3448, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_RGBW_COLOR: (255, 191, 178, 0)},
blocking=True,
)
bulb.async_set_levels.assert_called_with(255, 191, 178, 0)
bulb.async_set_levels.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "random"},
blocking=True,
)
bulb.async_set_effect.assert_called_once()
bulb.async_set_effect.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
"turn_on",
{ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "purple_fade", ATTR_BRIGHTNESS: 255},
blocking=True,
)
bulb.async_set_effect.assert_called_with("purple_fade", 50, 100)
bulb.async_set_effect.reset_mock()
async def test_rgb_or_w_light(hass: HomeAssistant) -> None:
"""Test an rgb or w light."""
config_entry = MockConfigEntry(
@ -811,8 +959,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154},
blocking=True,
)
bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=127)
bulb.async_set_levels.reset_mock()
bulb.async_set_white_temp.assert_called_with(6493, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
@ -820,8 +968,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 154, ATTR_BRIGHTNESS: 255},
blocking=True,
)
bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=0, w2=255)
bulb.async_set_levels.reset_mock()
bulb.async_set_white_temp.assert_called_with(6493, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,
@ -829,8 +977,8 @@ async def test_rgbcw_light(hass: HomeAssistant) -> None:
{ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 290},
blocking=True,
)
bulb.async_set_levels.assert_called_with(r=0, b=0, g=0, w=102, w2=25)
bulb.async_set_levels.reset_mock()
bulb.async_set_white_temp.assert_called_with(3448, 255)
bulb.async_set_white_temp.reset_mock()
await hass.services.async_call(
LIGHT_DOMAIN,

View File

@ -1,11 +1,16 @@
"""Tests for select platform."""
from unittest.mock import patch
from flux_led.const import (
COLOR_MODE_CCT as FLUX_COLOR_MODE_CCT,
COLOR_MODE_RGBW as FLUX_COLOR_MODE_RGBW,
WhiteChannelType,
)
from flux_led.protocol import PowerRestoreState, RemoteConfig
import pytest
from homeassistant.components import flux_led
from homeassistant.components.flux_led.const import DOMAIN
from homeassistant.components.flux_led.const import CONF_WHITE_CHANNEL_TYPE, DOMAIN
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, CONF_HOST, CONF_NAME
from homeassistant.core import HomeAssistant
@ -191,3 +196,51 @@ async def test_select_24ghz_remote_config(hass: HomeAssistant) -> None:
)
bulb.async_config_remotes.assert_called_once_with(RemoteConfig.PAIRED_ONLY)
bulb.async_config_remotes.reset_mock()
async def test_select_white_channel_type(hass: HomeAssistant) -> None:
"""Test selecting the white channel type."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: IP_ADDRESS, CONF_NAME: DEFAULT_ENTRY_TITLE},
unique_id=MAC_ADDRESS,
)
config_entry.add_to_hass(hass)
bulb = _mocked_bulb()
bulb.color_modes = {FLUX_COLOR_MODE_RGBW, FLUX_COLOR_MODE_CCT}
bulb.color_mode = FLUX_COLOR_MODE_RGBW
bulb.raw_state = bulb.raw_state._replace(model_num=0x06) # rgbw
with _patch_discovery(), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
operating_mode_entity_id = "select.bulb_rgbcw_ddeeff_white_channel"
state = hass.states.get(operating_mode_entity_id)
assert state.state == WhiteChannelType.WARM.name.title()
with pytest.raises(ValueError):
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: operating_mode_entity_id, ATTR_OPTION: "INVALID"},
blocking=True,
)
with patch(
"homeassistant.components.flux_led.async_setup_entry"
) as mock_setup_entry:
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{
ATTR_ENTITY_ID: operating_mode_entity_id,
ATTR_OPTION: WhiteChannelType.NATURAL.name.title(),
},
blocking=True,
)
await hass.async_block_till_done()
assert (
config_entry.data[CONF_WHITE_CHANNEL_TYPE]
== WhiteChannelType.NATURAL.name.lower()
)
assert len(mock_setup_entry.mock_calls) == 1