1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 09:38:58 +01:00

Support UniFi LED control for devices without RGB (#156812)

This commit is contained in:
Sebastian Schneider
2025-11-28 17:33:15 +01:00
committed by GitHub
parent ca35102138
commit 8c8708d5bc
2 changed files with 166 additions and 28 deletions
+61 -28
View File
@@ -35,11 +35,29 @@ if TYPE_CHECKING:
from .hub import UnifiHub
def convert_brightness_to_unifi(ha_brightness: int) -> int:
"""Convert Home Assistant brightness (0-255) to UniFi brightness (0-100)."""
return round((ha_brightness / 255) * 100)
def convert_brightness_to_ha(
unifi_brightness: int,
) -> int:
"""Convert UniFi brightness (0-100) to Home Assistant brightness (0-255)."""
return round((unifi_brightness / 100) * 255)
def get_device_brightness_or_default(device: Device) -> int:
"""Get device's current LED brightness. Defaults to 100 (full brightness) if not set."""
value = device.led_override_color_brightness
return value if value is not None else 100
@callback
def async_device_led_supported_fn(hub: UnifiHub, obj_id: str) -> bool:
"""Check if device supports LED control."""
device: Device = hub.api.devices[obj_id]
return device.supports_led_ring
return device.led_override is not None or device.supports_led_ring
@callback
@@ -56,17 +74,24 @@ async def async_device_led_control_fn(
status = "on" if turn_on else "off"
brightness = (
int((kwargs[ATTR_BRIGHTNESS] / 255) * 100)
if ATTR_BRIGHTNESS in kwargs
else device.led_override_color_brightness
)
# Only send brightness and RGB if device has LED_RING hardware support
if device.supports_led_ring:
# Use provided brightness or fall back to device's current brightness
if ATTR_BRIGHTNESS in kwargs:
brightness = convert_brightness_to_unifi(kwargs[ATTR_BRIGHTNESS])
else:
brightness = get_device_brightness_or_default(device)
color = (
f"#{kwargs[ATTR_RGB_COLOR][0]:02x}{kwargs[ATTR_RGB_COLOR][1]:02x}{kwargs[ATTR_RGB_COLOR][2]:02x}"
if ATTR_RGB_COLOR in kwargs
else device.led_override_color
)
# Use provided RGB color or fall back to device's current color
color: str | None
if ATTR_RGB_COLOR in kwargs:
rgb = kwargs[ATTR_RGB_COLOR]
color = f"#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}"
else:
color = device.led_override_color
else:
brightness = None
color = None
await hub.api.request(
DeviceSetLedStatus.create(
@@ -127,12 +152,19 @@ class UnifiLightEntity[HandlerT: APIHandler, ApiItemT: ApiItem](
entity_description: UnifiLightEntityDescription[HandlerT, ApiItemT]
_attr_supported_features = LightEntityFeature(0)
_attr_color_mode = ColorMode.RGB
_attr_supported_color_modes = {ColorMode.RGB}
@callback
def async_initiate_state(self) -> None:
"""Initiate entity state."""
device = cast(Device, self.entity_description.object_fn(self.api, self._obj_id))
if device.supports_led_ring:
self._attr_supported_color_modes = {ColorMode.RGB}
self._attr_color_mode = ColorMode.RGB
else:
self._attr_supported_color_modes = {ColorMode.ONOFF}
self._attr_color_mode = ColorMode.ONOFF
self.async_update_state(ItemEvent.ADDED, self._obj_id)
async def async_turn_on(self, **kwargs: Any) -> None:
@@ -150,23 +182,24 @@ class UnifiLightEntity[HandlerT: APIHandler, ApiItemT: ApiItem](
"""Update entity state."""
description = self.entity_description
device_obj = description.object_fn(self.api, self._obj_id)
device = cast(Device, device_obj)
self._attr_is_on = description.is_on_fn(self.hub, device_obj)
brightness = device.led_override_color_brightness
self._attr_brightness = (
int((int(brightness) / 100) * 255) if brightness is not None else None
)
# Only set brightness and RGB if device has LED_RING hardware support
if device.supports_led_ring:
self._attr_brightness = convert_brightness_to_ha(
get_device_brightness_or_default(device)
)
hex_color = (
device.led_override_color.lstrip("#")
if self._attr_is_on and device.led_override_color
else None
)
if hex_color and len(hex_color) == 6:
rgb_list = rgb_hex_to_rgb_list(hex_color)
self._attr_rgb_color = (rgb_list[0], rgb_list[1], rgb_list[2])
else:
self._attr_rgb_color = None
# Parse hex color from device and convert to RGB tuple
hex_color = (
device.led_override_color.lstrip("#")
if self._attr_is_on and device.led_override_color
else None
)
if hex_color and len(hex_color) == 6:
rgb_list = rgb_hex_to_rgb_list(hex_color)
self._attr_rgb_color = (rgb_list[0], rgb_list[1], rgb_list[2])
else:
self._attr_rgb_color = None
+105
View File
@@ -86,6 +86,24 @@ DEVICE_LED_OFF = {
"hw_caps": 2,
}
DEVICE_WITH_LED_NO_RGB = {
"board_rev": 2,
"device_id": "mock-id-4",
"ip": "10.0.0.4",
"last_seen": 1562600145,
"mac": "10:00:00:00:01:04",
"model": "US-16-150W",
"name": "Device LED No RGB",
"next_interval": 20,
"state": 1,
"type": "usw",
"version": "4.0.42.10433",
"led_override": "on",
"led_override_color": "#ffffff",
"led_override_color_brightness": 100,
"hw_caps": 0,
}
@pytest.mark.parametrize("device_payload", [[DEVICE_WITH_LED, DEVICE_WITHOUT_LED]])
@pytest.mark.usefixtures("config_entry_setup")
@@ -321,3 +339,90 @@ async def test_light_platform_snapshot(
with patch("homeassistant.components.unifi.PLATFORMS", [Platform.LIGHT]):
config_entry = await config_entry_factory()
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize("device_payload", [[DEVICE_WITH_LED_NO_RGB]])
@pytest.mark.usefixtures("config_entry_setup")
async def test_light_onoff_mode_only(
hass: HomeAssistant,
) -> None:
"""Test light with ONOFF mode only (no LED ring support)."""
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
light_entity = hass.states.get("light.device_led_no_rgb_led")
assert light_entity is not None
assert light_entity.state == STATE_ON
# Device without LED ring support should not expose brightness or RGB
assert light_entity.attributes.get("brightness") is None
assert light_entity.attributes.get("rgb_color") is None
assert light_entity.attributes.get("supported_color_modes") == ["onoff"]
assert light_entity.attributes.get("color_mode") == "onoff"
@pytest.mark.parametrize("device_payload", [[DEVICE_WITH_LED_NO_RGB]])
@pytest.mark.usefixtures("config_entry_setup")
async def test_light_onoff_mode_turn_on_off(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
config_entry_setup: MockConfigEntry,
) -> None:
"""Test ONOFF-only light turn on and off."""
aioclient_mock.clear_requests()
aioclient_mock.put(
f"https://{config_entry_setup.data[CONF_HOST]}:1234"
f"/api/s/{config_entry_setup.data[CONF_SITE_ID]}/rest/device/mock-id-4",
)
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "light.device_led_no_rgb_led"},
blocking=True,
)
assert aioclient_mock.call_count == 1
call_data = aioclient_mock.mock_calls[0][2]
assert call_data["led_override"] == "off"
# Should not send brightness or color for ONOFF-only devices
assert call_data.get("led_override_color_brightness") is None
assert call_data.get("led_override_color") is None
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "light.device_led_no_rgb_led"},
blocking=True,
)
assert aioclient_mock.call_count == 2
call_data = aioclient_mock.mock_calls[1][2]
assert call_data["led_override"] == "on"
# Should not send brightness or color for ONOFF-only devices
assert call_data.get("led_override_color_brightness") is None
assert call_data.get("led_override_color") is None
@pytest.mark.parametrize("device_payload", [[DEVICE_WITH_LED, DEVICE_WITH_LED_NO_RGB]])
@pytest.mark.usefixtures("config_entry_setup")
async def test_light_rgb_vs_onoff_modes(
hass: HomeAssistant,
) -> None:
"""Test that RGB and ONOFF modes are correctly assigned based on device capabilities."""
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 2
# Device with LED ring support should have RGB mode
rgb_light = hass.states.get("light.device_with_led_led")
assert rgb_light is not None
assert rgb_light.state == STATE_ON
assert rgb_light.attributes.get("supported_color_modes") == ["rgb"]
assert rgb_light.attributes.get("color_mode") == "rgb"
assert rgb_light.attributes.get("brightness") == 204
assert rgb_light.attributes.get("rgb_color") == (0, 0, 255)
# Device without LED ring support should have ONOFF mode
onoff_light = hass.states.get("light.device_led_no_rgb_led")
assert onoff_light is not None
assert onoff_light.state == STATE_ON
assert onoff_light.attributes.get("supported_color_modes") == ["onoff"]
assert onoff_light.attributes.get("color_mode") == "onoff"
assert onoff_light.attributes.get("brightness") is None
assert onoff_light.attributes.get("rgb_color") is None