Use average color for Hue light group state (#149499)
parent
ad154dce40
commit
36483dd785
|
@ -226,15 +226,26 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
lights_with_color_support = 0
|
||||
lights_with_color_temp_support = 0
|
||||
lights_with_dimming_support = 0
|
||||
lights_on_with_dimming_support = 0
|
||||
total_brightness = 0
|
||||
all_lights = self.controller.get_lights(self.resource.id)
|
||||
lights_in_colortemp_mode = 0
|
||||
lights_in_xy_mode = 0
|
||||
lights_in_dynamic_mode = 0
|
||||
# accumulate color values
|
||||
xy_total_x = 0.0
|
||||
xy_total_y = 0.0
|
||||
xy_count = 0
|
||||
temp_total = 0.0
|
||||
|
||||
# loop through all lights to find capabilities
|
||||
for light in all_lights:
|
||||
# reset per-light colortemp on flag
|
||||
light_in_colortemp_mode = False
|
||||
# check if light has color temperature
|
||||
if color_temp := light.color_temperature:
|
||||
lights_with_color_temp_support += 1
|
||||
# we assume mired values from the first capable light
|
||||
# default to mired values from the last capable light
|
||||
self._attr_color_temp_kelvin = (
|
||||
color_util.color_temperature_mired_to_kelvin(color_temp.mirek)
|
||||
if color_temp.mirek
|
||||
|
@ -250,15 +261,39 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
color_temp.mirek_schema.mirek_minimum
|
||||
)
|
||||
)
|
||||
if color_temp.mirek is not None and color_temp.mirek_valid:
|
||||
# counters for color mode vote and average temp
|
||||
if (
|
||||
light.on.on
|
||||
and color_temp.mirek is not None
|
||||
and color_temp.mirek_valid
|
||||
):
|
||||
lights_in_colortemp_mode += 1
|
||||
light_in_colortemp_mode = True
|
||||
temp_total += color_util.color_temperature_mired_to_kelvin(
|
||||
color_temp.mirek
|
||||
)
|
||||
# check if light has color xy
|
||||
if color := light.color:
|
||||
lights_with_color_support += 1
|
||||
# we assume xy values from the first capable light
|
||||
# default to xy values from the last capable light
|
||||
self._attr_xy_color = (color.xy.x, color.xy.y)
|
||||
# counters for color mode vote and average xy color
|
||||
if light.on.on:
|
||||
xy_total_x += color.xy.x
|
||||
xy_total_y += color.xy.y
|
||||
xy_count += 1
|
||||
# only count for colour mode vote if
|
||||
# this light is not in colortemp mode
|
||||
if not light_in_colortemp_mode:
|
||||
lights_in_xy_mode += 1
|
||||
# check if light has dimming
|
||||
if dimming := light.dimming:
|
||||
lights_with_dimming_support += 1
|
||||
total_brightness += dimming.brightness
|
||||
# accumulate brightness values
|
||||
if light.on.on:
|
||||
total_brightness += dimming.brightness
|
||||
lights_on_with_dimming_support += 1
|
||||
# check if light is in dynamic mode
|
||||
if (
|
||||
light.dynamics
|
||||
and light.dynamics.status == DynamicStatus.DYNAMIC_PALETTE
|
||||
|
@ -266,10 +301,11 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
lights_in_dynamic_mode += 1
|
||||
|
||||
# this is a bit hacky because light groups may contain lights
|
||||
# of different capabilities. We set a colormode as supported
|
||||
# if any of the lights support it
|
||||
# of different capabilities
|
||||
# this means that the state is derived from only some of the lights
|
||||
# and will never be 100% accurate but it will be close
|
||||
|
||||
# assign group color support modes based on light capabilities
|
||||
if lights_with_color_support > 0:
|
||||
supported_color_modes.add(ColorMode.XY)
|
||||
if lights_with_color_temp_support > 0:
|
||||
|
@ -278,19 +314,38 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
|||
if len(supported_color_modes) == 0:
|
||||
# only add color mode brightness if no color variants
|
||||
supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
self._brightness_pct = total_brightness / lights_with_dimming_support
|
||||
self._attr_brightness = round(
|
||||
((total_brightness / lights_with_dimming_support) / 100) * 255
|
||||
)
|
||||
# as we have brightness support, set group brightness values
|
||||
if lights_on_with_dimming_support > 0:
|
||||
self._brightness_pct = total_brightness / lights_on_with_dimming_support
|
||||
self._attr_brightness = round(
|
||||
((total_brightness / lights_on_with_dimming_support) / 100) * 255
|
||||
)
|
||||
else:
|
||||
supported_color_modes.add(ColorMode.ONOFF)
|
||||
self._dynamic_mode_active = lights_in_dynamic_mode > 0
|
||||
self._attr_supported_color_modes = supported_color_modes
|
||||
# pick a winner for the current colormode
|
||||
if lights_with_color_temp_support > 0 and lights_in_colortemp_mode > 0:
|
||||
# set the group color values if there are any color lights on
|
||||
if xy_count > 0:
|
||||
self._attr_xy_color = (
|
||||
round(xy_total_x / xy_count, 5),
|
||||
round(xy_total_y / xy_count, 5),
|
||||
)
|
||||
if lights_in_colortemp_mode > 0:
|
||||
avg_temp = temp_total / lights_in_colortemp_mode
|
||||
self._attr_color_temp_kelvin = round(avg_temp)
|
||||
# pick a winner for the current color mode based on the majority of on lights
|
||||
# if there is no winner pick the highest mode from group capabilities
|
||||
if lights_in_xy_mode > 0 and lights_in_xy_mode >= lights_in_colortemp_mode:
|
||||
self._attr_color_mode = ColorMode.XY
|
||||
elif (
|
||||
lights_in_colortemp_mode > 0
|
||||
and lights_in_colortemp_mode > lights_in_xy_mode
|
||||
):
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif lights_with_color_support > 0:
|
||||
self._attr_color_mode = ColorMode.XY
|
||||
elif lights_with_color_temp_support > 0:
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif lights_with_dimming_support > 0:
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
else:
|
||||
|
|
|
@ -518,9 +518,8 @@ async def test_grouped_lights(
|
|||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# the light should now be on and have the properties we've set
|
||||
# The light should now be on and have the properties we've set
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
|
@ -528,6 +527,364 @@ async def test_grouped_lights(
|
|||
assert test_light.attributes["brightness"] == 255
|
||||
assert test_light.attributes["xy_color"] == (0.123, 0.123)
|
||||
|
||||
# While we have a group on, test the color aggregation logic, XY first
|
||||
|
||||
# Turn off one of the bulbs in the group
|
||||
# "hue_light_with_color_and_color_temperature_1" corresponds to "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1"
|
||||
mock_bridge_v2.mock_requests.clear()
|
||||
single_light_id = "light.hue_light_with_color_and_color_temperature_1"
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": single_light_id},
|
||||
blocking=True,
|
||||
)
|
||||
event = {
|
||||
"id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1",
|
||||
"type": "light",
|
||||
"on": {"on": False},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The group should still show the same XY color since other lights maintain their color
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["xy_color"] == (0.123, 0.123)
|
||||
|
||||
# Turn the light back on with a white XY color (different from the rest of the group)
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{"entity_id": single_light_id, "xy_color": [0.3127, 0.3290]},
|
||||
blocking=True,
|
||||
)
|
||||
event = {
|
||||
"id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1",
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"color": {"xy": {"x": 0.3127, "y": 0.3290}},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Now the group XY color should be the average of all three lights:
|
||||
# Light 1: (0.3127, 0.3290) - white
|
||||
# Light 2: (0.123, 0.123)
|
||||
# Light 3: (0.123, 0.123)
|
||||
# Average: ((0.3127 + 0.123 + 0.123) / 3, (0.3290 + 0.123 + 0.123) / 3)
|
||||
# Average: (0.1862, 0.1917) rounded to 4 decimal places
|
||||
expected_x = round((0.3127 + 0.123 + 0.123) / 3, 4)
|
||||
expected_y = round((0.3290 + 0.123 + 0.123) / 3, 4)
|
||||
|
||||
# Check that the group XY color is now the average of all lights
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
group_x, group_y = test_light.attributes["xy_color"]
|
||||
assert abs(group_x - expected_x) < 0.001 # Allow small floating point differences
|
||||
assert abs(group_y - expected_y) < 0.001
|
||||
|
||||
# Test turning off another light in the group, leaving only two lights on - one white and one original color
|
||||
# "hue_light_with_color_and_color_temperature_2" corresponds to "b3fe71ef-d0ef-48de-9355-d9e604377df0"
|
||||
second_light_id = "light.hue_light_with_color_and_color_temperature_2"
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": second_light_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Simulate the second light turning off
|
||||
event = {
|
||||
"id": "b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"type": "light",
|
||||
"on": {"on": False},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Now only two lights are on:
|
||||
# Light 1: (0.3127, 0.3290) - white
|
||||
# Light 3: (0.123, 0.123) - original color
|
||||
# Average of remaining lights: ((0.3127 + 0.123) / 2, (0.3290 + 0.123) / 2)
|
||||
expected_x_two_lights = round((0.3127 + 0.123) / 2, 4)
|
||||
expected_y_two_lights = round((0.3290 + 0.123) / 2, 4)
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
# Check that the group color is now the average of only the two remaining lights
|
||||
group_x, group_y = test_light.attributes["xy_color"]
|
||||
assert abs(group_x - expected_x_two_lights) < 0.001
|
||||
assert abs(group_y - expected_y_two_lights) < 0.001
|
||||
|
||||
# Test colour temperature aggregation
|
||||
# Set all three lights to colour temperature mode with different mirek values
|
||||
for mirek, light_name, light_id in zip(
|
||||
[300, 250, 200],
|
||||
[
|
||||
"light.hue_light_with_color_and_color_temperature_1",
|
||||
"light.hue_light_with_color_and_color_temperature_2",
|
||||
"light.hue_light_with_color_and_color_temperature_gradient",
|
||||
],
|
||||
[
|
||||
"02cba059-9c2c-4d45-97e4-4f79b1bfbaa1",
|
||||
"b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"8015b17f-8336-415b-966a-b364bd082397",
|
||||
],
|
||||
strict=True,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": light_name,
|
||||
"color_temp": mirek,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
# Emit update event with matching mirek value
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": light_id,
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"color_temperature": {"mirek": mirek, "mirek_valid": True},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
# Expected average kelvin calculation:
|
||||
# 300 mirek ≈ 3333K, 250 mirek ≈ 4000K, 200 mirek ≈ 5000K
|
||||
expected_avg_kelvin = round((3333 + 4000 + 5000) / 3)
|
||||
assert abs(test_light.attributes["color_temp_kelvin"] - expected_avg_kelvin) <= 5
|
||||
|
||||
# Switch light 3 off and check average kelvin temperature of remaining two lights
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": "light.hue_light_with_color_and_color_temperature_gradient"},
|
||||
blocking=True,
|
||||
)
|
||||
event = {
|
||||
"id": "8015b17f-8336-415b-966a-b364bd082397",
|
||||
"type": "light",
|
||||
"on": {"on": False},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
# Expected average kelvin calculation:
|
||||
# 300 mirek ≈ 3333K, 250 mirek ≈ 4000K
|
||||
expected_avg_kelvin = round((3333 + 4000) / 2)
|
||||
assert abs(test_light.attributes["color_temp_kelvin"] - expected_avg_kelvin) <= 5
|
||||
|
||||
# Turn light 3 back on in XY mode and verify majority still favours COLOR_TEMP
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": "light.hue_light_with_color_and_color_temperature_gradient",
|
||||
"xy_color": [0.123, 0.123],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": "8015b17f-8336-415b-966a-b364bd082397",
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"color": {"xy": {"x": 0.123, "y": 0.123}},
|
||||
"color_temperature": {
|
||||
"mirek": None,
|
||||
"mirek_valid": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
# Switch light 2 to XY mode to flip the majority
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": "light.hue_light_with_color_and_color_temperature_2",
|
||||
"xy_color": [0.321, 0.321],
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": "b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"color": {"xy": {"x": 0.321, "y": 0.321}},
|
||||
"color_temperature": {
|
||||
"mirek": None,
|
||||
"mirek_valid": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light.attributes["color_mode"] == ColorMode.XY
|
||||
|
||||
# Test brightness aggregation with different brightness levels
|
||||
mock_bridge_v2.mock_requests.clear()
|
||||
|
||||
# Set all three lights to different brightness levels
|
||||
for brightness, light_name, light_id in zip(
|
||||
[90.0, 60.0, 30.0],
|
||||
[
|
||||
"light.hue_light_with_color_and_color_temperature_1",
|
||||
"light.hue_light_with_color_and_color_temperature_2",
|
||||
"light.hue_light_with_color_and_color_temperature_gradient",
|
||||
],
|
||||
[
|
||||
"02cba059-9c2c-4d45-97e4-4f79b1bfbaa1",
|
||||
"b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"8015b17f-8336-415b-966a-b364bd082397",
|
||||
],
|
||||
strict=True,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": light_name,
|
||||
"brightness": brightness,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
# Emit update event with matching brightness value
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": light_id,
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"dimming": {"brightness": brightness},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that the group brightness is the average of all three lights
|
||||
# Expected average: (90 + 60 + 30) / 3 = 60% -> 153 (60% of 255)
|
||||
expected_brightness = round(((90 + 60 + 30) / 3 / 100) * 255)
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["brightness"] == expected_brightness
|
||||
|
||||
# Turn off the dimmest light 3 (30% brightness) while keeping the other two on
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": "light.hue_light_with_color_and_color_temperature_gradient"},
|
||||
blocking=True,
|
||||
)
|
||||
event = {
|
||||
"id": "8015b17f-8336-415b-966a-b364bd082397",
|
||||
"type": "light",
|
||||
"on": {"on": False},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that the group brightness is now the average of the two remaining lights
|
||||
# Expected average: (90 + 60) / 2 = 75% -> 191 (75% of 255)
|
||||
expected_brightness_two_lights = round(((90 + 60) / 2 / 100) * 255)
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["brightness"] == expected_brightness_two_lights
|
||||
|
||||
# Turn off light 2 (60% brightness), leaving only the brightest one
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_off",
|
||||
{"entity_id": "light.hue_light_with_color_and_color_temperature_2"},
|
||||
blocking=True,
|
||||
)
|
||||
event = {
|
||||
"id": "b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"type": "light",
|
||||
"on": {"on": False},
|
||||
}
|
||||
mock_bridge_v2.api.emit_event("update", event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Check that the group brightness is now just the remaining light's brightness
|
||||
# Expected brightness: 90% -> 230 (round(90 / 100 * 255))
|
||||
expected_brightness_one_light = round((90 / 100) * 255)
|
||||
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["brightness"] == expected_brightness_one_light
|
||||
|
||||
# Set all three lights back to 100% brightness for consistency with later tests
|
||||
for light_name, light_id in zip(
|
||||
[
|
||||
"light.hue_light_with_color_and_color_temperature_1",
|
||||
"light.hue_light_with_color_and_color_temperature_2",
|
||||
"light.hue_light_with_color_and_color_temperature_gradient",
|
||||
],
|
||||
[
|
||||
"02cba059-9c2c-4d45-97e4-4f79b1bfbaa1",
|
||||
"b3fe71ef-d0ef-48de-9355-d9e604377df0",
|
||||
"8015b17f-8336-415b-966a-b364bd082397",
|
||||
],
|
||||
strict=True,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"light",
|
||||
"turn_on",
|
||||
{
|
||||
"entity_id": light_name,
|
||||
"brightness": 100.0,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
# Emit update event with matching brightness value
|
||||
mock_bridge_v2.api.emit_event(
|
||||
"update",
|
||||
{
|
||||
"id": light_id,
|
||||
"type": "light",
|
||||
"on": {"on": True},
|
||||
"dimming": {"brightness": 100.0},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify group is back to 100% brightness
|
||||
test_light = hass.states.get(test_light_id)
|
||||
assert test_light is not None
|
||||
assert test_light.state == "on"
|
||||
assert test_light.attributes["brightness"] == 255
|
||||
|
||||
# Test calling the turn off service on a grouped light.
|
||||
mock_bridge_v2.mock_requests.clear()
|
||||
await hass.services.async_call(
|
||||
|
|
Loading…
Reference in New Issue