Skip to content

Commit

Permalink
Add support for effects in Govee lights (#137846)
Browse files Browse the repository at this point in the history
  • Loading branch information
Galorhallen authored Feb 25, 2025
1 parent 9ec9110 commit f3021b4
Show file tree
Hide file tree
Showing 6 changed files with 567 additions and 251 deletions.
4 changes: 4 additions & 0 deletions homeassistant/components/govee_light_local/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ async def set_temperature(self, device: GoveeDevice, temperature: int) -> None:
"""Set light color in kelvin."""
await device.set_temperature(temperature)

async def set_scene(self, device: GoveeController, scene: str) -> None:
"""Set light scene."""
await device.set_scene(scene)

@property
def devices(self) -> list[GoveeDevice]:
"""Return a list of discovered Govee devices."""
Expand Down
62 changes: 62 additions & 0 deletions homeassistant/components/govee_light_local/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_RGB_COLOR,
ColorMode,
LightEntity,
LightEntityFeature,
filter_supported_color_modes,
)
from homeassistant.core import HomeAssistant, callback
Expand All @@ -25,6 +27,8 @@

_LOGGER = logging.getLogger(__name__)

_NONE_SCENE = "none"


async def async_setup_entry(
hass: HomeAssistant,
Expand All @@ -50,10 +54,22 @@ def discovery_callback(device: GoveeDevice, is_new: bool) -> bool:
class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
"""Govee Light."""

_attr_translation_key = "govee_light"
_attr_has_entity_name = True
_attr_name = None
_attr_supported_color_modes: set[ColorMode]
_fixed_color_mode: ColorMode | None = None
_attr_effect_list: list[str] | None = None
_attr_effect: str | None = None
_attr_supported_features: LightEntityFeature = LightEntityFeature(0)
_last_color_state: (
tuple[
ColorMode | str | None,
int | None,
tuple[int, int, int] | tuple[int | None] | None,
]
| None
) = None

def __init__(
self,
Expand All @@ -80,6 +96,13 @@ def __init__(
if GoveeLightFeatures.BRIGHTNESS & capabilities.features:
color_modes.add(ColorMode.BRIGHTNESS)

if (
GoveeLightFeatures.SCENES & capabilities.features
and capabilities.scenes
):
self._attr_supported_features = LightEntityFeature.EFFECT
self._attr_effect_list = [_NONE_SCENE, *capabilities.scenes.keys()]

self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._attr_supported_color_modes) == 1:
# If the light supports only a single color mode, set it now
Expand Down Expand Up @@ -143,12 +166,27 @@ async def async_turn_on(self, **kwargs: Any) -> None:

if ATTR_RGB_COLOR in kwargs:
self._attr_color_mode = ColorMode.RGB
self._attr_effect = None
self._last_color_state = None
red, green, blue = kwargs[ATTR_RGB_COLOR]
await self.coordinator.set_rgb_color(self._device, red, green, blue)
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
self._attr_color_mode = ColorMode.COLOR_TEMP
self._attr_effect = None
self._last_color_state = None
temperature: float = kwargs[ATTR_COLOR_TEMP_KELVIN]
await self.coordinator.set_temperature(self._device, int(temperature))
elif ATTR_EFFECT in kwargs:
effect = kwargs[ATTR_EFFECT]
if effect and self._attr_effect_list and effect in self._attr_effect_list:
if effect == _NONE_SCENE:
self._attr_effect = None
await self._restore_last_color_state()
else:
self._attr_effect = effect
self._save_last_color_state()
await self.coordinator.set_scene(self._device, effect)

self.async_write_ha_state()

async def async_turn_off(self, **kwargs: Any) -> None:
Expand All @@ -159,3 +197,27 @@ async def async_turn_off(self, **kwargs: Any) -> None:
@callback
def _update_callback(self, device: GoveeDevice) -> None:
self.async_write_ha_state()

def _save_last_color_state(self) -> None:
color_mode = self.color_mode
self._last_color_state = (
color_mode,
self.brightness,
(self.color_temp_kelvin,)
if color_mode == ColorMode.COLOR_TEMP
else self.rgb_color,
)

async def _restore_last_color_state(self) -> None:
if self._last_color_state:
color_mode, brightness, color = self._last_color_state
if color:
if color_mode == ColorMode.RGB:
await self.coordinator.set_rgb_color(self._device, *color)
elif color_mode == ColorMode.COLOR_TEMP:
await self.coordinator.set_temperature(self._device, *color)
if brightness:
await self.coordinator.set_brightness(
self._device, int((float(brightness) / 255.0) * 100.0)
)
self._last_color_state = None
24 changes: 24 additions & 0 deletions homeassistant/components/govee_light_local/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,29 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
},
"entity": {
"light": {
"govee_light": {
"state_attributes": {
"effect": {
"state": {
"none": "None",
"sunrise": "Sunrise",
"sunset": "Sunset",
"movie": "Movie",
"dating": "Dating",
"romantic": "Romantic",
"twinkle": "Twinkle",
"candlelight": "Candlelight",
"snowflake": "Snowflake",
"energetic": "Energetic",
"breathe": "Breathe",
"crossing": "Crossing"
}
}
}
}
}
}
}
26 changes: 22 additions & 4 deletions tests/components/govee_light_local/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch

from govee_local_api import GoveeLightCapabilities
from govee_local_api.light_capabilities import COMMON_FEATURES
from govee_local_api import GoveeLightCapabilities, GoveeLightFeatures
from govee_local_api.light_capabilities import COMMON_FEATURES, SCENE_CODES
import pytest

from homeassistant.components.govee_light_local.coordinator import GoveeController


@pytest.fixture(name="mock_govee_api")
def fixture_mock_govee_api():
def fixture_mock_govee_api() -> Generator[AsyncMock]:
"""Set up Govee Local API fixture."""
mock_api = AsyncMock(spec=GoveeController)
mock_api.start = AsyncMock()
Expand All @@ -21,8 +21,20 @@ def fixture_mock_govee_api():
mock_api.turn_on_off = AsyncMock()
mock_api.set_brightness = AsyncMock()
mock_api.set_color = AsyncMock()
mock_api.set_scene = AsyncMock()
mock_api._async_update_data = AsyncMock()
return mock_api

with (
patch(
"homeassistant.components.govee_light_local.coordinator.GoveeController",
return_value=mock_api,
) as mock_controller,
patch(
"homeassistant.components.govee_light_local.config_flow.GoveeController",
return_value=mock_api,
),
):
yield mock_controller.return_value


@pytest.fixture(name="mock_setup_entry")
Expand All @@ -38,3 +50,9 @@ def fixture_mock_setup_entry() -> Generator[AsyncMock]:
DEFAULT_CAPABILITIES: GoveeLightCapabilities = GoveeLightCapabilities(
features=COMMON_FEATURES, segments=[], scenes={}
)

SCENE_CAPABILITIES: GoveeLightCapabilities = GoveeLightCapabilities(
features=COMMON_FEATURES | GoveeLightFeatures.SCENES,
segments=[],
scenes=SCENE_CODES,
)
60 changes: 23 additions & 37 deletions tests/components/govee_light_local/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,9 @@ async def test_creating_entry_has_no_devices(

mock_govee_api.devices = []

with (
patch(
"homeassistant.components.govee_light_local.config_flow.GoveeController",
return_value=mock_govee_api,
),
patch(
"homeassistant.components.govee_light_local.config_flow.DISCOVERY_TIMEOUT",
0,
),
with patch(
"homeassistant.components.govee_light_local.config_flow.DISCOVERY_TIMEOUT",
0,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
Expand All @@ -67,24 +61,20 @@ async def test_creating_entry_has_with_devices(

mock_govee_api.devices = _get_devices(mock_govee_api)

with patch(
"homeassistant.components.govee_light_local.config_flow.GoveeController",
return_value=mock_govee_api,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)

# Confirmation form
assert result["type"] is FlowResultType.FORM
# Confirmation form
assert result["type"] is FlowResultType.FORM

result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY

await hass.async_block_till_done()
await hass.async_block_till_done()

mock_govee_api.start.assert_awaited_once()
mock_setup_entry.assert_awaited_once()
mock_govee_api.start.assert_awaited_once()
mock_setup_entry.assert_awaited_once()


async def test_creating_entry_errno(
Expand All @@ -99,21 +89,17 @@ async def test_creating_entry_errno(
mock_govee_api.start.side_effect = e
mock_govee_api.devices = _get_devices(mock_govee_api)

with patch(
"homeassistant.components.govee_light_local.config_flow.GoveeController",
return_value=mock_govee_api,
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)

# Confirmation form
assert result["type"] is FlowResultType.FORM
# Confirmation form
assert result["type"] is FlowResultType.FORM

result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.ABORT

await hass.async_block_till_done()
await hass.async_block_till_done()

assert mock_govee_api.start.call_count == 1
mock_setup_entry.assert_not_awaited()
assert mock_govee_api.start.call_count == 1
mock_setup_entry.assert_not_awaited()
Loading

0 comments on commit f3021b4

Please sign in to comment.