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:
committed by
GitHub
parent
ca35102138
commit
8c8708d5bc
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user