Add device configuration entities to flux_led (#62786)

Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
J. Nick Koston 2022-01-06 21:02:19 -10:00 committed by GitHub
parent 250af90acb
commit e222e1b6f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 626 additions and 48 deletions

View file

@ -48,6 +48,7 @@ PLATFORMS_BY_TYPE: Final = {
Platform.BUTTON, Platform.BUTTON,
Platform.LIGHT, Platform.LIGHT,
Platform.NUMBER, Platform.NUMBER,
Platform.SELECT,
Platform.SWITCH, Platform.SWITCH,
], ],
DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH], DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH],

View file

@ -28,7 +28,6 @@ async def async_setup_entry(
class FluxRestartButton(FluxBaseEntity, ButtonEntity): class FluxRestartButton(FluxBaseEntity, ButtonEntity):
"""Representation of a Flux restart button.""" """Representation of a Flux restart button."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
def __init__( def __init__(

View file

@ -40,6 +40,8 @@ def _async_device_info(
class FluxBaseEntity(Entity): class FluxBaseEntity(Entity):
"""Representation of a Flux entity without a coordinator.""" """Representation of a Flux entity without a coordinator."""
_attr_should_poll = False
def __init__( def __init__(
self, self,
device: AIOWifiLedBulb, device: AIOWifiLedBulb,
@ -64,13 +66,17 @@ class FluxEntity(CoordinatorEntity):
coordinator: FluxLedUpdateCoordinator, coordinator: FluxLedUpdateCoordinator,
unique_id: str | None, unique_id: str | None,
name: str, name: str,
key: str | None,
) -> None: ) -> None:
"""Initialize the light.""" """Initialize the light."""
super().__init__(coordinator) super().__init__(coordinator)
self._device: AIOWifiLedBulb = coordinator.device self._device: AIOWifiLedBulb = coordinator.device
self._responding = True self._responding = True
self._attr_name = name self._attr_name = name
self._attr_unique_id = unique_id if key:
self._attr_unique_id = f"{unique_id}_{key}"
else:
self._attr_unique_id = unique_id
if unique_id: if unique_id:
self._attr_device_info = _async_device_info( self._attr_device_info = _async_device_info(
unique_id, self._device, coordinator.entry unique_id, self._device, coordinator.entry

View file

@ -202,7 +202,7 @@ class FluxLight(FluxOnOffEntity, CoordinatorEntity, LightEntity):
custom_effect_transition: str, custom_effect_transition: str,
) -> None: ) -> None:
"""Initialize the light.""" """Initialize the light."""
super().__init__(coordinator, unique_id, name) super().__init__(coordinator, unique_id, name, None)
self._attr_min_mireds = ( self._attr_min_mireds = (
color_temperature_kelvin_to_mired(self._device.max_temp) + 1 color_temperature_kelvin_to_mired(self._device.max_temp) + 1
) # for rounding ) # for rounding

View file

@ -1,20 +1,38 @@
"""Support for LED numbers.""" """Support for LED numbers."""
from __future__ import annotations from __future__ import annotations
from abc import abstractmethod
import logging
from typing import cast from typing import cast
from flux_led.protocol import (
MUSIC_PIXELS_MAX,
MUSIC_PIXELS_PER_SEGMENT_MAX,
MUSIC_SEGMENTS_MAX,
PIXELS_MAX,
PIXELS_PER_SEGMENT_MAX,
SEGMENTS_MAX,
)
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.light import EFFECT_RANDOM
from homeassistant.components.number import NumberEntity, NumberMode from homeassistant.components.number import NumberEntity, NumberMode
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, EFFECT_SPEED_SUPPORT_MODES from .const import DOMAIN
from .coordinator import FluxLedUpdateCoordinator from .coordinator import FluxLedUpdateCoordinator
from .entity import FluxEntity from .entity import FluxEntity
from .util import _effect_brightness, _hass_color_modes from .util import _effect_brightness
_LOGGER = logging.getLogger(__name__)
DEBOUNCE_TIME = 1
async def async_setup_entry( async def async_setup_entry(
@ -24,23 +42,55 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the Flux lights.""" """Set up the Flux lights."""
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
device = coordinator.device
entities: list[
FluxSpeedNumber
| FluxPixelsPerSegmentNumber
| FluxSegmentsNumber
| FluxMusicPixelsPerSegmentNumber
| FluxMusicSegmentsNumber
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
color_modes = _hass_color_modes(coordinator.device) if device.pixels_per_segment is not None:
if not color_modes.intersection(EFFECT_SPEED_SUPPORT_MODES): entities.append(
return FluxPixelsPerSegmentNumber(
async_add_entities(
[
FluxNumber(
coordinator, coordinator,
entry.unique_id, unique_id,
entry.data[CONF_NAME], f"{name} Pixels Per Segment",
"pixels_per_segment",
) )
] )
) if device.segments is not None:
entities.append(
FluxSegmentsNumber(coordinator, unique_id, f"{name} Segments", "segments")
)
if device.music_pixels_per_segment is not None:
entities.append(
FluxMusicPixelsPerSegmentNumber(
coordinator,
unique_id,
f"{name} Music Pixels Per Segment",
"music_pixels_per_segment",
)
)
if device.music_segments is not None:
entities.append(
FluxMusicSegmentsNumber(
coordinator, unique_id, f"{name} Music Segments", "music_segments"
)
)
if device.effect_list and device.effect_list != [EFFECT_RANDOM]:
entities.append(
FluxSpeedNumber(coordinator, unique_id, f"{name} Effect Speed", None)
)
if entities:
async_add_entities(entities)
class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity): class FluxSpeedNumber(FluxEntity, CoordinatorEntity, NumberEntity):
"""Defines a flux_led speed number.""" """Defines a flux_led speed number."""
_attr_min_value = 1 _attr_min_value = 1
@ -49,16 +99,6 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity):
_attr_mode = NumberMode.SLIDER _attr_mode = NumberMode.SLIDER
_attr_icon = "mdi:speedometer" _attr_icon = "mdi:speedometer"
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
name: str,
) -> None:
"""Initialize the flux number."""
super().__init__(coordinator, unique_id, name)
self._attr_name = f"{name} Effect Speed"
@property @property
def value(self) -> float: def value(self) -> float:
"""Return the effect speed.""" """Return the effect speed."""
@ -78,3 +118,174 @@ class FluxNumber(FluxEntity, CoordinatorEntity, NumberEntity):
current_effect, new_speed, _effect_brightness(self._device.brightness) current_effect, new_speed, _effect_brightness(self._device.brightness)
) )
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()
class FluxConfigNumber(FluxEntity, CoordinatorEntity, NumberEntity):
"""Base class for flux config numbers."""
_attr_entity_category = EntityCategory.CONFIG
_attr_min_value = 1
_attr_step = 1
_attr_mode = NumberMode.BOX
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
name: str,
key: str | None,
) -> None:
"""Initialize the flux number."""
super().__init__(coordinator, unique_id, name, key)
self._debouncer: Debouncer | None = None
self._pending_value: int | None = None
async def async_added_to_hass(self) -> None:
"""Set up the debouncer when adding to hass."""
self._debouncer = Debouncer(
hass=self.hass,
logger=_LOGGER,
cooldown=DEBOUNCE_TIME,
immediate=False,
function=self._async_set_value,
)
await super().async_added_to_hass()
async def async_set_value(self, value: float) -> None:
"""Set the value."""
self._pending_value = int(value)
assert self._debouncer is not None
await self._debouncer.async_call()
@abstractmethod
async def _async_set_value(self) -> None:
"""Call on debounce to set the value."""
def _pixels_and_segments_fit_in_music_mode(self) -> bool:
"""Check if the base pixel and segment settings will fit for music mode.
If they fit, they do not need to be configured.
"""
pixels_per_segment = self._device.pixels_per_segment
segments = self._device.segments
assert pixels_per_segment is not None
assert segments is not None
return bool(
pixels_per_segment <= MUSIC_PIXELS_PER_SEGMENT_MAX
and segments <= MUSIC_SEGMENTS_MAX
and pixels_per_segment * segments <= MUSIC_PIXELS_MAX
)
class FluxPixelsPerSegmentNumber(FluxConfigNumber):
"""Defines a flux_led pixels per segment number."""
_attr_icon = "mdi:dots-grid"
@property
def max_value(self) -> int:
"""Return the max value."""
return min(
PIXELS_PER_SEGMENT_MAX, int(PIXELS_MAX / (self._device.segments or 1))
)
@property
def value(self) -> int:
"""Return the pixels per segment."""
assert self._device.pixels_per_segment is not None
return self._device.pixels_per_segment
async def _async_set_value(self) -> None:
"""Set the pixels per segment."""
assert self._pending_value is not None
await self._device.async_set_device_config(
pixels_per_segment=self._pending_value
)
class FluxSegmentsNumber(FluxConfigNumber):
"""Defines a flux_led segments number."""
_attr_icon = "mdi:segment"
@property
def max_value(self) -> int:
"""Return the max value."""
assert self._device.pixels_per_segment is not None
return min(
SEGMENTS_MAX, int(PIXELS_MAX / (self._device.pixels_per_segment or 1))
)
@property
def value(self) -> int:
"""Return the segments."""
assert self._device.segments is not None
return self._device.segments
async def _async_set_value(self) -> None:
"""Set the segments."""
assert self._pending_value is not None
await self._device.async_set_device_config(segments=self._pending_value)
class FluxMusicNumber(FluxConfigNumber):
"""A number that is only available if the base pixels do not fit in music mode."""
@property
def available(self) -> bool:
"""Return if music pixels per segment can be set."""
return super().available and not self._pixels_and_segments_fit_in_music_mode()
class FluxMusicPixelsPerSegmentNumber(FluxMusicNumber):
"""Defines a flux_led music pixels per segment number."""
_attr_icon = "mdi:dots-grid"
@property
def max_value(self) -> int:
"""Return the max value."""
assert self._device.music_segments is not None
return min(
MUSIC_PIXELS_PER_SEGMENT_MAX,
int(MUSIC_PIXELS_MAX / (self._device.music_segments or 1)),
)
@property
def value(self) -> int:
"""Return the music pixels per segment."""
assert self._device.music_pixels_per_segment is not None
return self._device.music_pixels_per_segment
async def _async_set_value(self) -> None:
"""Set the music pixels per segment."""
assert self._pending_value is not None
await self._device.async_set_device_config(
music_pixels_per_segment=self._pending_value
)
class FluxMusicSegmentsNumber(FluxMusicNumber):
"""Defines a flux_led music segments number."""
_attr_icon = "mdi:segment"
@property
def max_value(self) -> int:
"""Return the max value."""
assert self._device.pixels_per_segment is not None
return min(
MUSIC_SEGMENTS_MAX,
int(MUSIC_PIXELS_MAX / (self._device.music_pixels_per_segment or 1)),
)
@property
def value(self) -> int:
"""Return the music segments."""
assert self._device.music_segments is not None
return self._device.music_segments
async def _async_set_value(self) -> None:
"""Set the music segments."""
assert self._pending_value is not None
await self._device.async_set_device_config(music_segments=self._pending_value)

View file

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from flux_led.aio import AIOWifiLedBulb from flux_led.aio import AIOWifiLedBulb
from flux_led.base_device import DeviceType
from flux_led.protocol import PowerRestoreState from flux_led.protocol import PowerRestoreState
from homeassistant import config_entries from homeassistant import config_entries
@ -13,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import FluxLedUpdateCoordinator from .coordinator import FluxLedUpdateCoordinator
from .entity import FluxBaseEntity from .entity import FluxBaseEntity, FluxEntity
async def async_setup_entry( async def async_setup_entry(
@ -23,17 +24,46 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the Flux selects.""" """Set up the Flux selects."""
coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: FluxLedUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([FluxPowerState(coordinator.device, entry)]) device = coordinator.device
entities: list[
FluxPowerStateSelect
| FluxOperatingModesSelect
| FluxWiringsSelect
| FluxICTypeSelect
] = []
name = entry.data[CONF_NAME]
unique_id = entry.unique_id
if device.device_type == DeviceType.Switch:
entities.append(FluxPowerStateSelect(coordinator.device, entry))
if device.operating_modes:
entities.append(
FluxOperatingModesSelect(
coordinator, unique_id, f"{name} Operating Mode", "operating_mode"
)
)
if device.wirings:
entities.append(
FluxWiringsSelect(coordinator, unique_id, f"{name} Wiring", "wiring")
)
if device.ic_types:
entities.append(
FluxICTypeSelect(coordinator, unique_id, f"{name} IC Type", "ic_type")
)
if entities:
async_add_entities(entities)
def _human_readable_option(const_option: str) -> str: def _human_readable_option(const_option: str) -> str:
return const_option.replace("_", " ").title() return const_option.replace("_", " ").title()
class FluxPowerState(FluxBaseEntity, SelectEntity): class FluxPowerStateSelect(FluxBaseEntity, SelectEntity):
"""Representation of a Flux power restore state option.""" """Representation of a Flux power restore state option."""
_attr_should_poll = False _attr_icon = "mdi:transmission-tower-off"
_attr_entity_category = EntityCategory.CONFIG
def __init__( def __init__(
self, self,
@ -42,7 +72,6 @@ class FluxPowerState(FluxBaseEntity, SelectEntity):
) -> None: ) -> None:
"""Initialize the power state select.""" """Initialize the power state select."""
super().__init__(device, entry) super().__init__(device, entry)
self._attr_entity_category = EntityCategory.CONFIG
self._attr_name = f"{entry.data[CONF_NAME]} Power Restored" self._attr_name = f"{entry.data[CONF_NAME]} Power Restored"
if entry.unique_id: if entry.unique_id:
self._attr_unique_id = f"{entry.unique_id}_power_restored" self._attr_unique_id = f"{entry.unique_id}_power_restored"
@ -65,3 +94,74 @@ class FluxPowerState(FluxBaseEntity, SelectEntity):
await self._device.async_set_power_restore(channel1=self._name_to_state[option]) await self._device.async_set_power_restore(channel1=self._name_to_state[option])
self._async_set_current_option_from_device() self._async_set_current_option_from_device()
self.async_write_ha_state() 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."""
_attr_icon = "mdi:chip"
@property
def options(self) -> list[str]:
"""Return the available ic types."""
assert self._device.ic_types is not None
return self._device.ic_types
@property
def current_option(self) -> str | None:
"""Return the current ic type."""
return self._device.ic_type
async def async_select_option(self, option: str) -> None:
"""Change the ic type."""
await self._device.async_set_device_config(ic_type=option)
class FluxWiringsSelect(FluxConfigSelect):
"""Representation of Flux wirings."""
_attr_icon = "mdi:led-strip-variant"
@property
def options(self) -> list[str]:
"""Return the available wiring options based on the strip protocol."""
assert self._device.wirings is not None
return self._device.wirings
@property
def current_option(self) -> str | None:
"""Return the current wiring."""
return self._device.wiring
async def async_select_option(self, option: str) -> None:
"""Change the wiring."""
await self._device.async_set_device_config(wiring=option)
class FluxOperatingModesSelect(FluxConfigSelect):
"""Representation of Flux operating modes."""
@property
def options(self) -> list[str]:
"""Return the current operating mode."""
assert self._device.operating_modes is not None
return self._device.operating_modes
@property
def current_option(self) -> str | None:
"""Return the current operating mode."""
return self._device.operating_mode
async def async_select_option(self, option: str) -> None:
"""Change the ic type."""
await self._device.async_set_device_config(operating_mode=option)
# reload since we need to reinit the device
self.hass.async_create_task(
self.hass.config_entries.async_reload(self.coordinator.entry.entry_id)
)

View file

@ -38,13 +38,15 @@ async def async_setup_entry(
name = entry.data[CONF_NAME] name = entry.data[CONF_NAME]
if coordinator.device.device_type == DeviceType.Switch: if coordinator.device.device_type == DeviceType.Switch:
entities.append(FluxSwitch(coordinator, unique_id, name)) entities.append(FluxSwitch(coordinator, unique_id, name, None))
if entry.data.get(CONF_REMOTE_ACCESS_HOST): if entry.data.get(CONF_REMOTE_ACCESS_HOST):
entities.append(FluxRemoteAccessSwitch(coordinator.device, entry)) entities.append(FluxRemoteAccessSwitch(coordinator.device, entry))
if coordinator.device.microphone: if coordinator.device.microphone:
entities.append(FluxMusicSwitch(coordinator, unique_id, name)) entities.append(
FluxMusicSwitch(coordinator, unique_id, f"{name} Music", "music")
)
if entities: if entities:
async_add_entities(entities) async_add_entities(entities)
@ -62,7 +64,6 @@ class FluxSwitch(FluxOnOffEntity, CoordinatorEntity, SwitchEntity):
class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity): class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity):
"""Representation of a Flux remote access switch.""" """Representation of a Flux remote access switch."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.CONFIG _attr_entity_category = EntityCategory.CONFIG
def __init__( def __init__(
@ -112,18 +113,6 @@ class FluxRemoteAccessSwitch(FluxBaseEntity, SwitchEntity):
class FluxMusicSwitch(FluxEntity, SwitchEntity): class FluxMusicSwitch(FluxEntity, SwitchEntity):
"""Representation of a Flux music switch.""" """Representation of a Flux music switch."""
def __init__(
self,
coordinator: FluxLedUpdateCoordinator,
unique_id: str | None,
name: str,
) -> None:
"""Initialize the flux music switch."""
super().__init__(coordinator, unique_id, name)
self._attr_name = f"{name} Music"
if unique_id:
self._attr_unique_id = f"{unique_id}_music"
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the microphone on.""" """Turn the microphone on."""
await self._async_ensure_device_on() await self._async_ensure_device_on()

View file

@ -81,6 +81,7 @@ def _mocked_bulb() -> AIOWifiLedBulb:
bulb.async_set_effect = AsyncMock() bulb.async_set_effect = AsyncMock()
bulb.async_set_white_temp = AsyncMock() bulb.async_set_white_temp = AsyncMock()
bulb.async_set_brightness = AsyncMock() bulb.async_set_brightness = AsyncMock()
bulb.async_set_device_config = AsyncMock()
bulb.pixels_per_segment = 300 bulb.pixels_per_segment = 300
bulb.segments = 2 bulb.segments = 2
bulb.music_pixels_per_segment = 150 bulb.music_pixels_per_segment = 150
@ -142,6 +143,16 @@ def _mocked_switch() -> AIOWifiLedBulb:
channel3=PowerRestoreState.LAST_STATE, channel3=PowerRestoreState.LAST_STATE,
channel4=PowerRestoreState.LAST_STATE, channel4=PowerRestoreState.LAST_STATE,
) )
switch.pixels_per_segment = None
switch.segments = None
switch.music_pixels_per_segment = None
switch.music_segments = None
switch.operating_mode = None
switch.operating_modes = None
switch.wirings = None
switch.wiring = None
switch.ic_types = None
switch.ic_type = None
switch.requires_turn_on = True switch.requires_turn_on = True
switch.async_set_time = AsyncMock() switch.async_set_time = AsyncMock()
switch.async_reboot = AsyncMock() switch.async_reboot = AsyncMock()

View file

@ -1,17 +1,26 @@
"""Tests for the flux_led number platform.""" """Tests for the flux_led number platform."""
from unittest.mock import patch
from flux_led.const import COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB from flux_led.const import COLOR_MODE_RGB as FLUX_COLOR_MODE_RGB
import pytest import pytest
from homeassistant.components import flux_led from homeassistant.components import flux_led
from homeassistant.components.flux_led import number as flux_number
from homeassistant.components.flux_led.const import DOMAIN from homeassistant.components.flux_led.const import DOMAIN
from homeassistant.components.number import ( from homeassistant.components.number import (
ATTR_VALUE, ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN, DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
) )
from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_ON from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_HOST,
CONF_NAME,
STATE_ON,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -225,3 +234,155 @@ async def test_addressable_light_effect_speed(hass: HomeAssistant) -> None:
state = hass.states.get(number_entity_id) state = hass.states.get(number_entity_id)
assert state.state == "100" assert state.state == "100"
async def test_addressable_light_pixel_config(hass: HomeAssistant) -> None:
"""Test an addressable light pixel config."""
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.raw_state = bulb.raw_state._replace(
model_num=0xA2
) # Original addressable model
bulb.color_modes = {FLUX_COLOR_MODE_RGB}
bulb.color_mode = FLUX_COLOR_MODE_RGB
with patch.object(
flux_number, "DEBOUNCE_TIME", 0
), _patch_discovery(), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
pixels_per_segment_entity_id = "number.bulb_rgbcw_ddeeff_pixels_per_segment"
state = hass.states.get(pixels_per_segment_entity_id)
assert state.state == "300"
segments_entity_id = "number.bulb_rgbcw_ddeeff_segments"
state = hass.states.get(segments_entity_id)
assert state.state == "2"
music_pixels_per_segment_entity_id = (
"number.bulb_rgbcw_ddeeff_music_pixels_per_segment"
)
state = hass.states.get(music_pixels_per_segment_entity_id)
assert state.state == "150"
music_segments_entity_id = "number.bulb_rgbcw_ddeeff_music_segments"
state = hass.states.get(music_segments_entity_id)
assert state.state == "4"
with pytest.raises(ValueError):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: pixels_per_segment_entity_id, ATTR_VALUE: 5000},
blocking=True,
)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: pixels_per_segment_entity_id, ATTR_VALUE: 100},
blocking=True,
)
bulb.async_set_device_config.assert_called_with(pixels_per_segment=100)
bulb.async_set_device_config.reset_mock()
with pytest.raises(ValueError):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: music_pixels_per_segment_entity_id, ATTR_VALUE: 5000},
blocking=True,
)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: music_pixels_per_segment_entity_id, ATTR_VALUE: 100},
blocking=True,
)
bulb.async_set_device_config.assert_called_with(music_pixels_per_segment=100)
bulb.async_set_device_config.reset_mock()
with pytest.raises(ValueError):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: segments_entity_id, ATTR_VALUE: 50},
blocking=True,
)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: segments_entity_id, ATTR_VALUE: 5},
blocking=True,
)
bulb.async_set_device_config.assert_called_with(segments=5)
bulb.async_set_device_config.reset_mock()
with pytest.raises(ValueError):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: music_segments_entity_id, ATTR_VALUE: 50},
blocking=True,
)
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: music_segments_entity_id, ATTR_VALUE: 5},
blocking=True,
)
bulb.async_set_device_config.assert_called_with(music_segments=5)
bulb.async_set_device_config.reset_mock()
async def test_addressable_light_pixel_config_music_disabled(
hass: HomeAssistant,
) -> None:
"""Test an addressable light pixel config with music pixels disabled."""
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.pixels_per_segment = 150
bulb.segments = 1
bulb.music_pixels_per_segment = 150
bulb.music_segments = 1
bulb.raw_state = bulb.raw_state._replace(
model_num=0xA2
) # Original addressable model
bulb.color_modes = {FLUX_COLOR_MODE_RGB}
bulb.color_mode = FLUX_COLOR_MODE_RGB
with patch.object(
flux_number, "DEBOUNCE_TIME", 0
), _patch_discovery(), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
pixels_per_segment_entity_id = "number.bulb_rgbcw_ddeeff_pixels_per_segment"
state = hass.states.get(pixels_per_segment_entity_id)
assert state.state == "150"
segments_entity_id = "number.bulb_rgbcw_ddeeff_segments"
state = hass.states.get(segments_entity_id)
assert state.state == "1"
music_pixels_per_segment_entity_id = (
"number.bulb_rgbcw_ddeeff_music_pixels_per_segment"
)
state = hass.states.get(music_pixels_per_segment_entity_id)
assert state.state == STATE_UNAVAILABLE
music_segments_entity_id = "number.bulb_rgbcw_ddeeff_music_segments"
state = hass.states.get(music_segments_entity_id)
assert state.state == STATE_UNAVAILABLE

View file

@ -1,5 +1,8 @@
"""Tests for select platform.""" """Tests for select platform."""
from unittest.mock import patch
from flux_led.protocol import PowerRestoreState from flux_led.protocol import PowerRestoreState
import pytest
from homeassistant.components import flux_led from homeassistant.components import flux_led
from homeassistant.components.flux_led.const import DOMAIN from homeassistant.components.flux_led.const import DOMAIN
@ -12,6 +15,7 @@ from . import (
DEFAULT_ENTRY_TITLE, DEFAULT_ENTRY_TITLE,
IP_ADDRESS, IP_ADDRESS,
MAC_ADDRESS, MAC_ADDRESS,
_mocked_bulb,
_mocked_switch, _mocked_switch,
_patch_discovery, _patch_discovery,
_patch_wifibulb, _patch_wifibulb,
@ -47,3 +51,99 @@ async def test_switch_power_restore_state(hass: HomeAssistant) -> None:
switch.async_set_power_restore.assert_called_once_with( switch.async_set_power_restore.assert_called_once_with(
channel1=PowerRestoreState.ALWAYS_ON channel1=PowerRestoreState.ALWAYS_ON
) )
async def test_select_addressable_strip_config(hass: HomeAssistant) -> None:
"""Test selecting addressable strip configs."""
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.raw_state = bulb.raw_state._replace(model_num=0xA2) # addressable model
with _patch_discovery(), _patch_wifibulb(device=bulb):
await async_setup_component(hass, flux_led.DOMAIN, {flux_led.DOMAIN: {}})
await hass.async_block_till_done()
wiring_entity_id = "select.bulb_rgbcw_ddeeff_wiring"
state = hass.states.get(wiring_entity_id)
assert state.state == "BGRW"
ic_type_entity_id = "select.bulb_rgbcw_ddeeff_ic_type"
state = hass.states.get(ic_type_entity_id)
assert state.state == "WS2812B"
with pytest.raises(ValueError):
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: wiring_entity_id, ATTR_OPTION: "INVALID"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: wiring_entity_id, ATTR_OPTION: "GRBW"},
blocking=True,
)
bulb.async_set_device_config.assert_called_once_with(wiring="GRBW")
bulb.async_set_device_config.reset_mock()
with pytest.raises(ValueError):
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: ic_type_entity_id, ATTR_OPTION: "INVALID"},
blocking=True,
)
await hass.services.async_call(
SELECT_DOMAIN,
"select_option",
{ATTR_ENTITY_ID: ic_type_entity_id, ATTR_OPTION: "UCS1618"},
blocking=True,
)
bulb.async_set_device_config.assert_called_once_with(ic_type="UCS1618")
async def test_select_mutable_0x25_strip_config(hass: HomeAssistant) -> None:
"""Test selecting mutable 0x25 strip configs."""
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.operating_mode = "RGBWW"
bulb.operating_modes = ["DIM", "CCT", "RGB", "RGBW", "RGBWW"]
bulb.raw_state = bulb.raw_state._replace(model_num=0x25) # addressable model
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_operating_mode"
state = hass.states.get(operating_mode_entity_id)
assert state.state == "RGBWW"
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: "CCT"},
blocking=True,
)
await hass.async_block_till_done()
bulb.async_set_device_config.assert_called_once_with(operating_mode="CCT")
assert len(mock_setup_entry.mock_calls) == 1