Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 62 additions & 29 deletions homeassistant/components/unifi/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,29 @@
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:
Comment thread
Sese-Schneider marked this conversation as resolved.
"""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
Expand All @@ -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
Comment thread
Sese-Schneider marked this conversation as resolved.

await hub.api.request(
DeviceSetLedStatus.create(
Expand Down Expand Up @@ -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))
Comment thread
Sese-Schneider marked this conversation as resolved.

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:
Expand All @@ -150,23 +182,24 @@ def async_update_state(self, event: ItemEvent, obj_id: str) -> None:
"""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
)

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
# 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)
)

# 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 changes: 105 additions & 0 deletions tests/components/unifi/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@
"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")
Expand Down Expand Up @@ -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
Loading