mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add Govee local light integration (#106697)
* Added govee_local_api * Code cleanup * Fix discovery * Add missing supported device * Fix autodiscovery * Add missing quality scale in manifest.json * QA * QA: Moved coordinator creation to __init__.py * QA * Fix typo and update test * QA * Removed unecessary code * Fix typo * Fix typo * QA, typing and strings * Removed unsed logger in __init__.py * QA, using ColorMode for lights capabilities * Bump govee_local_api to 1.4.0 Moved capabilities to library. * Update requirements * Update library to 1.4.1 with unsupported dvice warning * Fix tests after library update * QA * Add test for retry config * Update integration name and domain * Update homeassistant/components/govee_light_local/light.py --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@ -495,6 +495,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_travel_time/ @eifinger
|
||||
/homeassistant/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/tests/components/govee_ble/ @bdraco @PierreAronnax
|
||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||
/tests/components/govee_light_local/ @Galorhallen
|
||||
/homeassistant/components/gpsd/ @fabaff
|
||||
/homeassistant/components/gree/ @cmroche
|
||||
/tests/components/gree/ @cmroche
|
||||
|
5
homeassistant/brands/govee.json
Normal file
5
homeassistant/brands/govee.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "govee",
|
||||
"name": "Govee",
|
||||
"integrations": ["govee_ble", "govee_light_local"]
|
||||
}
|
44
homeassistant/components/govee_light_local/__init__.py
Normal file
44
homeassistant/components/govee_light_local/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""The Govee Light local integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GoveeLocalApiCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Govee light local from a config entry."""
|
||||
|
||||
coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(hass=hass)
|
||||
entry.async_on_unload(coordinator.cleanup)
|
||||
|
||||
await coordinator.start()
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(delay=5):
|
||||
while not coordinator.devices:
|
||||
await asyncio.sleep(delay=1)
|
||||
except asyncio.TimeoutError as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
58
homeassistant/components/govee_light_local/config_flow.py
Normal file
58
homeassistant/components/govee_light_local/config_flow.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""Config flow for Govee light local."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from govee_local_api import GoveeController
|
||||
|
||||
from homeassistant.components import network
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .const import (
|
||||
CONF_LISTENING_PORT_DEFAULT,
|
||||
CONF_MULTICAST_ADDRESS_DEFAULT,
|
||||
CONF_TARGET_PORT_DEFAULT,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
"""Return if there are devices that can be discovered."""
|
||||
|
||||
adapter = await network.async_get_source_ip(hass, network.PUBLIC_TARGET_IP)
|
||||
|
||||
controller: GoveeController = GoveeController(
|
||||
loop=hass.loop,
|
||||
logger=_LOGGER,
|
||||
listening_address=adapter,
|
||||
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
|
||||
broadcast_port=CONF_TARGET_PORT_DEFAULT,
|
||||
listening_port=CONF_LISTENING_PORT_DEFAULT,
|
||||
discovery_enabled=True,
|
||||
discovery_interval=1,
|
||||
update_enabled=False,
|
||||
)
|
||||
|
||||
await controller.start()
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(delay=5):
|
||||
while not controller.devices:
|
||||
await asyncio.sleep(delay=1)
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.debug("No devices found")
|
||||
|
||||
devices_count = len(controller.devices)
|
||||
controller.cleanup()
|
||||
|
||||
return devices_count > 0
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, "Govee light local", _async_has_devices
|
||||
)
|
13
homeassistant/components/govee_light_local/const.py
Normal file
13
homeassistant/components/govee_light_local/const.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Constants for the Govee light local integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "govee_light_local"
|
||||
MANUFACTURER = "Govee"
|
||||
|
||||
CONF_MULTICAST_ADDRESS_DEFAULT = "239.255.255.250"
|
||||
CONF_TARGET_PORT_DEFAULT = 4001
|
||||
CONF_LISTENING_PORT_DEFAULT = 4002
|
||||
CONF_DISCOVERY_INTERVAL_DEFAULT = 60
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
90
homeassistant/components/govee_light_local/coordinator.py
Normal file
90
homeassistant/components/govee_light_local/coordinator.py
Normal file
@ -0,0 +1,90 @@
|
||||
"""Coordinator for Govee light local."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
|
||||
from govee_local_api import GoveeController, GoveeDevice
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
CONF_DISCOVERY_INTERVAL_DEFAULT,
|
||||
CONF_LISTENING_PORT_DEFAULT,
|
||||
CONF_MULTICAST_ADDRESS_DEFAULT,
|
||||
CONF_TARGET_PORT_DEFAULT,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
|
||||
"""Govee light local coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
name="GoveeLightLocalApi",
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
self._controller = GoveeController(
|
||||
loop=hass.loop,
|
||||
logger=_LOGGER,
|
||||
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
|
||||
broadcast_port=CONF_TARGET_PORT_DEFAULT,
|
||||
listening_port=CONF_LISTENING_PORT_DEFAULT,
|
||||
discovery_enabled=True,
|
||||
discovery_interval=CONF_DISCOVERY_INTERVAL_DEFAULT,
|
||||
discovered_callback=None,
|
||||
update_enabled=False,
|
||||
)
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the Govee coordinator."""
|
||||
await self._controller.start()
|
||||
self._controller.send_update_message()
|
||||
|
||||
async def set_discovery_callback(
|
||||
self, callback: Callable[[GoveeDevice, bool], bool]
|
||||
) -> None:
|
||||
"""Set discovery callback for automatic Govee light discovery."""
|
||||
self._controller.set_device_discovered_callback(callback)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Stop and cleanup the cooridinator."""
|
||||
self._controller.cleanup()
|
||||
|
||||
async def turn_on(self, device: GoveeDevice) -> None:
|
||||
"""Turn on the light."""
|
||||
await device.turn_on()
|
||||
|
||||
async def turn_off(self, device: GoveeDevice) -> None:
|
||||
"""Turn off the light."""
|
||||
await device.turn_off()
|
||||
|
||||
async def set_brightness(self, device: GoveeDevice, brightness: int) -> None:
|
||||
"""Set light brightness."""
|
||||
await device.set_brightness(brightness)
|
||||
|
||||
async def set_rgb_color(
|
||||
self, device: GoveeDevice, red: int, green: int, blue: int
|
||||
) -> None:
|
||||
"""Set light RGB color."""
|
||||
await device.set_rgb_color(red, green, blue)
|
||||
|
||||
async def set_temperature(self, device: GoveeDevice, temperature: int) -> None:
|
||||
"""Set light color in kelvin."""
|
||||
await device.set_temperature(temperature)
|
||||
|
||||
@property
|
||||
def devices(self) -> list[GoveeDevice]:
|
||||
"""Return a list of discovered Govee devices."""
|
||||
return self._controller.devices
|
||||
|
||||
async def _async_update_data(self) -> list[GoveeDevice]:
|
||||
self._controller.send_update_message()
|
||||
return self._controller.devices
|
160
homeassistant/components/govee_light_local/light.py
Normal file
160
homeassistant/components/govee_light_local/light.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""Govee light local."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from govee_local_api import GoveeDevice, GoveeLightCapability
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import GoveeLocalApiCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Govee light setup."""
|
||||
|
||||
coordinator: GoveeLocalApiCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
def discovery_callback(device: GoveeDevice, is_new: bool) -> bool:
|
||||
if is_new:
|
||||
async_add_entities([GoveeLight(coordinator, device)])
|
||||
return True
|
||||
|
||||
async_add_entities(
|
||||
GoveeLight(coordinator, device) for device in coordinator.devices
|
||||
)
|
||||
|
||||
await coordinator.set_discovery_callback(discovery_callback)
|
||||
|
||||
|
||||
class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
|
||||
"""Govee Light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GoveeLocalApiCoordinator,
|
||||
device: GoveeDevice,
|
||||
) -> None:
|
||||
"""Govee Light constructor."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
device.set_update_callback(self._update_callback)
|
||||
|
||||
self._attr_unique_id = device.fingerprint
|
||||
|
||||
capabilities = device.capabilities
|
||||
color_modes = set()
|
||||
if capabilities:
|
||||
if GoveeLightCapability.COLOR_RGB in capabilities:
|
||||
color_modes.add(ColorMode.RGB)
|
||||
if GoveeLightCapability.COLOR_KELVIN_TEMPERATURE in capabilities:
|
||||
color_modes.add(ColorMode.COLOR_TEMP)
|
||||
self._attr_max_color_temp_kelvin = 9000
|
||||
self._attr_min_color_temp_kelvin = 2000
|
||||
if GoveeLightCapability.BRIGHTNESS in capabilities:
|
||||
color_modes.add(ColorMode.BRIGHTNESS)
|
||||
else:
|
||||
color_modes.add(ColorMode.ONOFF)
|
||||
|
||||
self._attr_supported_color_modes = color_modes
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
# Serial numbers are unique identifiers within a specific domain
|
||||
(DOMAIN, device.fingerprint)
|
||||
},
|
||||
name=device.sku,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=device.sku,
|
||||
connections={(CONNECTION_NETWORK_MAC, device.fingerprint)},
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if device is on (brightness above 0)."""
|
||||
return self._device.on
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return int((self._device.brightness / 100.0) * 255.0)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temperature in Kelvin."""
|
||||
return self._device.temperature_color
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
"""Return the rgb color."""
|
||||
return self._device.rgb_color
|
||||
|
||||
@property
|
||||
def color_mode(self) -> ColorMode | str | None:
|
||||
"""Return the color mode."""
|
||||
if (
|
||||
self._device.temperature_color is not None
|
||||
and self._device.temperature_color > 0
|
||||
):
|
||||
return ColorMode.COLOR_TEMP
|
||||
if self._device.rgb_color is not None and any(self._device.rgb_color):
|
||||
return ColorMode.RGB
|
||||
|
||||
if (
|
||||
self._attr_supported_color_modes
|
||||
and ColorMode.BRIGHTNESS in self._attr_supported_color_modes
|
||||
):
|
||||
return ColorMode.BRIGHTNESS
|
||||
return ColorMode.ONOFF
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
if not self.is_on or not kwargs:
|
||||
await self.coordinator.turn_on(self._device)
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness: int = int((float(kwargs[ATTR_BRIGHTNESS]) / 255.0) * 100.0)
|
||||
await self.coordinator.set_brightness(self._device, brightness)
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs:
|
||||
self._attr_color_mode = ColorMode.RGB
|
||||
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
|
||||
temperature: float = kwargs[ATTR_COLOR_TEMP_KELVIN]
|
||||
await self.coordinator.set_temperature(self._device, int(temperature))
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
await self.coordinator.turn_off(self._device)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_callback(self, device: GoveeDevice) -> None:
|
||||
self.async_write_ha_state()
|
10
homeassistant/components/govee_light_local/manifest.json
Normal file
10
homeassistant/components/govee_light_local/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "govee_light_local",
|
||||
"name": "Govee lights local",
|
||||
"codeowners": ["@Galorhallen"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.4.1"]
|
||||
}
|
13
homeassistant/components/govee_light_local/strings.json
Normal file
13
homeassistant/components/govee_light_local/strings.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
}
|
||||
}
|
||||
}
|
@ -197,6 +197,7 @@ FLOWS = {
|
||||
"google_translate",
|
||||
"google_travel_time",
|
||||
"govee_ble",
|
||||
"govee_light_local",
|
||||
"gpslogger",
|
||||
"gree",
|
||||
"growatt_server",
|
||||
|
@ -2266,11 +2266,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"govee_ble": {
|
||||
"name": "Govee Bluetooth",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
"govee": {
|
||||
"name": "Govee",
|
||||
"integrations": {
|
||||
"govee_ble": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Govee Bluetooth"
|
||||
},
|
||||
"govee_light_local": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
"name": "Govee lights local"
|
||||
}
|
||||
}
|
||||
},
|
||||
"gpsd": {
|
||||
"name": "GPSD",
|
||||
|
@ -954,6 +954,9 @@ gotailwind==0.2.2
|
||||
# homeassistant.components.govee_ble
|
||||
govee-ble==0.27.3
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.4.1
|
||||
|
||||
# homeassistant.components.remote_rpi_gpio
|
||||
gpiozero==1.6.2
|
||||
|
||||
|
@ -771,6 +771,9 @@ gotailwind==0.2.2
|
||||
# homeassistant.components.govee_ble
|
||||
govee-ble==0.27.3
|
||||
|
||||
# homeassistant.components.govee_light_local
|
||||
govee-local-api==1.4.1
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==1.4.1
|
||||
|
||||
|
1
tests/components/govee_light_local/__init__.py
Normal file
1
tests/components/govee_light_local/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Govee Light local integration."""
|
37
tests/components/govee_light_local/conftest.py
Normal file
37
tests/components/govee_light_local/conftest.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Tests configuration for Govee Local API."""
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from govee_local_api import GoveeLightCapability
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.govee_light_local.coordinator import GoveeController
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_govee_api")
|
||||
def fixture_mock_govee_api():
|
||||
"""Set up Govee Local API fixture."""
|
||||
mock_api = AsyncMock(spec=GoveeController)
|
||||
mock_api.start = AsyncMock()
|
||||
mock_api.turn_on_off = AsyncMock()
|
||||
mock_api.set_brightness = AsyncMock()
|
||||
mock_api.set_color = AsyncMock()
|
||||
mock_api._async_update_data = AsyncMock()
|
||||
return mock_api
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_setup_entry")
|
||||
def fixture_mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
DEFAULT_CAPABILITEIS: set[GoveeLightCapability] = {
|
||||
GoveeLightCapability.COLOR_RGB,
|
||||
GoveeLightCapability.COLOR_KELVIN_TEMPERATURE,
|
||||
GoveeLightCapability.BRIGHTNESS,
|
||||
}
|
74
tests/components/govee_light_local/test_config_flow.py
Normal file
74
tests/components/govee_light_local/test_config_flow.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Test Govee light local config flow."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from govee_local_api import GoveeDevice
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.govee_light_local.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import DEFAULT_CAPABILITEIS
|
||||
|
||||
|
||||
async def test_creating_entry_has_no_devices(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_govee_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test setting up Govee with no devices."""
|
||||
|
||||
mock_govee_api.devices = []
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_govee_api.start.assert_awaited_once()
|
||||
mock_setup_entry.assert_not_called()
|
||||
|
||||
|
||||
async def test_creating_entry_has_with_devices(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_govee_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting up Govee with devices."""
|
||||
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd1",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
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}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_govee_api.start.assert_awaited_once()
|
||||
mock_setup_entry.assert_awaited_once()
|
338
tests/components/govee_light_local/test_light.py
Normal file
338
tests/components/govee_light_local/test_light.py
Normal file
@ -0,0 +1,338 @@
|
||||
"""Test Govee light local."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from govee_local_api import GoveeDevice
|
||||
|
||||
from homeassistant.components.govee_light_local.const import DOMAIN
|
||||
from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import DEFAULT_CAPABILITEIS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_light_known_device(
|
||||
hass: HomeAssistant, mock_govee_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test adding a known device."""
|
||||
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
|
||||
color_modes = light.attributes[ATTR_SUPPORTED_COLOR_MODES]
|
||||
assert ColorMode.RGB in color_modes
|
||||
assert ColorMode.BRIGHTNESS in color_modes
|
||||
assert ColorMode.COLOR_TEMP in color_modes
|
||||
|
||||
# Remove
|
||||
assert await hass.config_entries.async_remove(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("light.H615A") is None
|
||||
|
||||
|
||||
async def test_light_unknown_device(
|
||||
hass: HomeAssistant, mock_govee_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test adding an unknown device."""
|
||||
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.101",
|
||||
fingerprint="unkown_device",
|
||||
sku="XYZK",
|
||||
capabilities=None,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
light = hass.states.get("light.XYZK")
|
||||
assert light is not None
|
||||
|
||||
assert light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF]
|
||||
|
||||
|
||||
async def test_light_remove(hass: HomeAssistant, mock_govee_api: AsyncMock) -> None:
|
||||
"""Test adding a known device."""
|
||||
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd1",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("light.H615A") is not None
|
||||
|
||||
# Remove 1
|
||||
assert await hass.config_entries.async_remove(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all()) == 0
|
||||
|
||||
|
||||
async def test_light_setup_retry(
|
||||
hass: HomeAssistant, mock_govee_api: AsyncMock
|
||||
) -> None:
|
||||
"""Test adding an unknown device."""
|
||||
|
||||
mock_govee_api.devices = []
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.asyncio.timeout",
|
||||
side_effect=asyncio.TimeoutError,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_light_on_off(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
||||
"""Test adding a known device."""
|
||||
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], True)
|
||||
|
||||
# Turn off
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": light.entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "off"
|
||||
mock_govee_api.turn_on_off.assert_awaited_with(mock_govee_api.devices[0], False)
|
||||
|
||||
|
||||
async def test_light_brightness(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
||||
"""Test changing brightness."""
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id, "brightness_pct": 50},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
mock_govee_api.set_brightness.assert_awaited_with(mock_govee_api.devices[0], 50)
|
||||
assert light.attributes["brightness"] == 127
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id, "brightness": 255},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
assert light.attributes["brightness"] == 255
|
||||
mock_govee_api.set_brightness.assert_awaited_with(
|
||||
mock_govee_api.devices[0], 100
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id, "brightness": 255},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
assert light.attributes["brightness"] == 255
|
||||
mock_govee_api.set_brightness.assert_awaited_with(
|
||||
mock_govee_api.devices[0], 100
|
||||
)
|
||||
|
||||
|
||||
async def test_light_color(hass: HomeAssistant, mock_govee_api: MagicMock) -> None:
|
||||
"""Test changing brightness."""
|
||||
mock_govee_api.devices = [
|
||||
GoveeDevice(
|
||||
controller=mock_govee_api,
|
||||
ip="192.168.1.100",
|
||||
fingerprint="asdawdqwdqwd",
|
||||
sku="H615A",
|
||||
capabilities=DEFAULT_CAPABILITEIS,
|
||||
)
|
||||
]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.govee_light_local.coordinator.GoveeController",
|
||||
return_value=mock_govee_api,
|
||||
):
|
||||
entry = MockConfigEntry(domain=DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.states.async_all()) == 1
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "off"
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id, "rgb_color": [100, 255, 50]},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
assert light.attributes["rgb_color"] == (100, 255, 50)
|
||||
assert light.attributes["color_mode"] == ColorMode.RGB
|
||||
|
||||
mock_govee_api.set_color.assert_awaited_with(
|
||||
mock_govee_api.devices[0], rgb=(100, 255, 50), temperature=None
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": light.entity_id, "kelvin": 4400},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
light = hass.states.get("light.H615A")
|
||||
assert light is not None
|
||||
assert light.state == "on"
|
||||
assert light.attributes["color_temp_kelvin"] == 4400
|
||||
assert light.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
mock_govee_api.set_color.assert_awaited_with(
|
||||
mock_govee_api.devices[0], rgb=None, temperature=4400
|
||||
)
|
Reference in New Issue
Block a user