From 8b1b38fb61dc3583313ec68da1011a1acd6245b0 Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Thu, 4 Jun 2026 00:59:05 +0400 Subject: [PATCH 1/8] Add light adjust action --- homeassistant/components/group/light.py | 24 +++++ homeassistant/components/light/__init__.py | 65 +++++++++++- homeassistant/components/light/services.yaml | 27 ++++- tests/components/group/test_light.py | 104 +++++++++++++++++++ tests/components/light/test_init.py | 35 +++++++ 5 files changed, 252 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 7ee020c1c6255..d7a004e16b6a0 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -185,6 +185,30 @@ async def async_turn_on(self, **kwargs: Any) -> None: context=self._context, ) + async def async_adjust(self, **kwargs: Any) -> None: + """Forward the adjust command to on lights in the light group.""" + data = { + key: value for key, value in kwargs.items() if key in FORWARDED_ATTRIBUTES + } + entity_ids = [ + entity_id + for entity_id in self._entity_ids + if self.hass.states.is_state(entity_id, STATE_ON) + ] + if not entity_ids: + return + data[ATTR_ENTITY_ID] = entity_ids + + _LOGGER.debug("Forwarded adjust command: %s", data) + + await self.hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + data, + blocking=True, + context=self._context, + ) + async def async_turn_off(self, **kwargs: Any) -> None: """Forward the turn_off command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index becedf024e7a4..5a13b70270ead 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -44,6 +44,7 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE +SERVICE_ADJUST = "adjust" # Color mode of the light ATTR_COLOR_MODE = "color_mode" @@ -182,7 +183,9 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, - vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, + vol.Exclusive( + ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS + ): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): cv.positive_int, vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( @@ -216,6 +219,39 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | ATTR_FLASH: VALID_FLASH, } +LIGHT_ADJUST_SCHEMA: VolDictType = { + ATTR_TRANSITION: VALID_TRANSITION, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, + vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_COLOR_TEMP_KELVIN, COLOR_GROUP): cv.positive_int, + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + ), + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 3) + ), + vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 4) + ), + vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte,) * 5) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) + ), + vol.Exclusive(ATTR_WHITE, COLOR_GROUP): vol.Any(True, VALID_BRIGHTNESS), + ATTR_EFFECT: cv.string, +} + _LOGGER = logging.getLogger(__name__) @@ -524,6 +560,23 @@ async def async_handle_light_off_service( await light.async_turn_off(**filter_turn_off_params(light, params)) + async def async_handle_light_adjust_service( + light: LightEntity, call: ServiceCall + ) -> None: + """Handle adjusting a light without turning it on.""" + if not light.is_on: + return + + if not call.data["params"]: + return + + params = process_turn_on_params(hass, light, call.data["params"]) + + if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: + return + + await light.async_adjust(**filter_turn_on_params(light, params)) + async def async_handle_toggle_service( light: LightEntity, call: ServiceCall ) -> None: @@ -538,6 +591,12 @@ async def async_handle_toggle_service( async_handle_light_on_service, ) + component.async_register_entity_service( + SERVICE_ADJUST, + vol.All(cv.make_entity_service_schema(LIGHT_ADJUST_SCHEMA), preprocess_data), + async_handle_light_adjust_service, + ) + component.async_register_entity_service( SERVICE_TURN_OFF, vol.All(cv.make_entity_service_schema(LIGHT_TURN_OFF_SCHEMA), preprocess_data), @@ -1062,3 +1121,7 @@ async def async_toggle(self, **kwargs: Any) -> None: params = process_turn_off_params(self.hass, self, kwargs) await self.async_turn_off(**filter_turn_off_params(self, params)) + + async def async_adjust(self, **kwargs: Any) -> None: + """Adjust the entity without turning it on.""" + await self.async_turn_on(**kwargs) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index aa83d47841cf3..27c45932f9468 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -213,7 +213,7 @@ turn_on: min: 0 max: 100 unit_of_measurement: "%" - brightness_step_pct: + brightness_step_pct: &brightness_step_pct filter: *brightness_support selector: number: @@ -262,7 +262,7 @@ turn_on: number: min: 0 max: 255 - brightness_step: + brightness_step: &brightness_step filter: *brightness_support selector: number: @@ -303,6 +303,29 @@ turn_off: fields: flash: *flash +adjust: + target: + entity: + domain: light + fields: + transition: *transition + rgb_color: *rgb_color + color_temp_kelvin: *color_temp_kelvin + brightness_pct: *brightness_pct + effect: *effect + advanced_fields: + collapsed: true + fields: + rgbw_color: *rgbw_color + rgbww_color: *rgbww_color + color_name: *color_name + hs_color: *hs_color + xy_color: *xy_color + brightness: *brightness + brightness_step: *brightness_step + brightness_step_pct: *brightness_step_pct + white: *white + toggle: target: entity: diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index bd2c5cc826d15..003c99d02ea07 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -24,6 +24,7 @@ ATTR_TRANSITION, ATTR_WHITE, DOMAIN as LIGHT_DOMAIN, + SERVICE_ADJUST, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -1401,6 +1402,109 @@ async def test_service_calls( assert state.attributes[ATTR_RGB_COLOR] == (255, 0, 0) +async def test_adjust_service_call_only_targets_on_group_members( + hass: HomeAssistant, +) -> None: + """Test the adjust service only targets on group members.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + MockLight("test3", STATE_ON), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity in entities: + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = 64 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + assert hass.states.get("light.test1").state == STATE_ON + assert hass.states.get("light.test1").attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get("light.test2").state == STATE_OFF + assert entities[1].brightness == 64 + assert hass.states.get("light.test3").state == STATE_ON + assert hass.states.get("light.test3").attributes[ATTR_BRIGHTNESS] == 128 + + _, data = entities[0].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 128} + assert entities[1].last_call("turn_on") is None + _, data = entities[2].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 128} + + +async def test_adjust_service_call_all_off_group_is_noop( + hass: HomeAssistant, +) -> None: + """Test the adjust service is a no-op when all group members are off.""" + entities = [ + MockLight("test1", STATE_OFF), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity in entities: + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = 64 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.light_group").state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + for entity in entities: + assert hass.states.get(entity.entity_id).state == STATE_OFF + assert entity.brightness == 64 + assert entity.last_call("turn_on") is None + + async def test_service_call_effect(hass: HomeAssistant) -> None: """Test service calls.""" await async_setup_component( diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index cf8a6dacf82f1..9ca63f595a1fb 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -443,6 +443,41 @@ async def test_services( assert data == {} +async def test_adjust_service_only_adjusts_on_lights( + hass: HomeAssistant, + mock_light_entities: list[MockLight], +) -> None: + """Test adjust service only adjusts lights that are already on.""" + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, ent2, _ = mock_light_entities + ent1.supported_color_modes = {light.ColorMode.BRIGHTNESS} + ent1.color_mode = light.ColorMode.BRIGHTNESS + ent2.supported_color_modes = {light.ColorMode.BRIGHTNESS} + ent2.color_mode = light.ColorMode.BRIGHTNESS + + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + { + ATTR_ENTITY_ID: [ent1.entity_id, ent2.entity_id], + light.ATTR_BRIGHTNESS: 128, + }, + blocking=True, + ) + + assert light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + _, data = ent1.last_call("turn_on") + assert data == {light.ATTR_BRIGHTNESS: 128} + assert ent2.last_call("turn_on") is None + + @pytest.mark.parametrize( ("profile_name", "last_call", "expected_data"), [ From 7a82bddfe50111f8c155f3ab4b991623cba5b71f Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Fri, 5 Jun 2026 14:02:16 +0400 Subject: [PATCH 2/8] Add light.adjust service metadata --- homeassistant/components/light/icons.json | 3 + homeassistant/components/light/strings.json | 67 +++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/homeassistant/components/light/icons.json b/homeassistant/components/light/icons.json index 8a5716f774ac1..172afacb290a0 100644 --- a/homeassistant/components/light/icons.json +++ b/homeassistant/components/light/icons.json @@ -27,6 +27,9 @@ } }, "services": { + "adjust": { + "service": "mdi:lightbulb-on-50" + }, "toggle": { "service": "mdi:lightbulb" }, diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 1c438e5f7c167..dd256163b3981 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -334,6 +334,73 @@ } }, "services": { + "adjust": { + "description": "Adjusts the properties of one or more lights without turning on lights that are off.", + "fields": { + "brightness": { + "description": "[%key:component::light::common::field_brightness_description%]", + "name": "[%key:component::light::common::field_brightness_name%]" + }, + "brightness_pct": { + "description": "[%key:component::light::common::field_brightness_pct_description%]", + "name": "[%key:component::light::common::field_brightness_pct_name%]" + }, + "brightness_step": { + "description": "[%key:component::light::common::field_brightness_step_description%]", + "name": "[%key:component::light::common::field_brightness_step_name%]" + }, + "brightness_step_pct": { + "description": "[%key:component::light::common::field_brightness_step_pct_description%]", + "name": "[%key:component::light::common::field_brightness_step_pct_name%]" + }, + "color_name": { + "description": "[%key:component::light::common::field_color_name_description%]", + "name": "[%key:component::light::common::field_color_name_name%]" + }, + "color_temp_kelvin": { + "description": "[%key:component::light::common::field_color_temp_kelvin_description%]", + "name": "[%key:component::light::common::field_color_temp_kelvin_name%]" + }, + "effect": { + "description": "[%key:component::light::common::field_effect_description%]", + "name": "[%key:component::light::common::field_effect_name%]" + }, + "hs_color": { + "description": "[%key:component::light::common::field_hs_color_description%]", + "name": "[%key:component::light::common::field_hs_color_name%]" + }, + "rgb_color": { + "description": "[%key:component::light::common::field_rgb_color_description%]", + "name": "[%key:component::light::common::field_rgb_color_name%]" + }, + "rgbw_color": { + "description": "[%key:component::light::common::field_rgbw_color_description%]", + "name": "[%key:component::light::common::field_rgbw_color_name%]" + }, + "rgbww_color": { + "description": "[%key:component::light::common::field_rgbww_color_description%]", + "name": "[%key:component::light::common::field_rgbww_color_name%]" + }, + "transition": { + "description": "[%key:component::light::common::field_transition_description%]", + "name": "[%key:component::light::common::field_transition_name%]" + }, + "white": { + "description": "[%key:component::light::common::field_white_description%]", + "name": "[%key:component::light::common::field_white_name%]" + }, + "xy_color": { + "description": "[%key:component::light::common::field_xy_color_description%]", + "name": "[%key:component::light::common::field_xy_color_name%]" + } + }, + "name": "Adjust light", + "sections": { + "advanced_fields": { + "name": "[%key:component::light::common::section_advanced_fields_name%]" + } + } + }, "toggle": { "description": "Toggles one or more lights, from on to off, or off to on, based on their current state.", "fields": { From cbbc2b13bb0debc29fb7da24723011347bea5029 Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Fri, 5 Jun 2026 14:52:29 +0400 Subject: [PATCH 3/8] Fix light.adjust for all-mode light groups --- homeassistant/components/light/__init__.py | 5 +- tests/components/group/test_light.py | 53 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 5a13b70270ead..86a30baad52ce 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -564,9 +564,6 @@ async def async_handle_light_adjust_service( light: LightEntity, call: ServiceCall ) -> None: """Handle adjusting a light without turning it on.""" - if not light.is_on: - return - if not call.data["params"]: return @@ -1124,4 +1121,6 @@ async def async_toggle(self, **kwargs: Any) -> None: async def async_adjust(self, **kwargs: Any) -> None: """Adjust the entity without turning it on.""" + if not self.is_on: + return await self.async_turn_on(**kwargs) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 003c99d02ea07..dc4cf43c66127 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1505,6 +1505,59 @@ async def test_adjust_service_call_all_off_group_is_noop( assert entity.last_call("turn_on") is None +async def test_adjust_service_call_targets_on_members_for_all_group( + hass: HomeAssistant, +) -> None: + """Test adjust on an all-group still reaches active members only.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity in entities: + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = 64 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "true", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # all-groups report off until every member is on + assert hass.states.get("light.light_group").state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + assert hass.states.get("light.test1").state == STATE_ON + assert hass.states.get("light.test1").attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get("light.test2").state == STATE_OFF + assert entities[1].brightness == 64 + + _, data = entities[0].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 128} + assert entities[1].last_call("turn_on") is None + + async def test_service_call_effect(hass: HomeAssistant) -> None: """Test service calls.""" await async_setup_component( From f013d82210c657d176cbcb6fc96bcfd9299a8f74 Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Fri, 5 Jun 2026 16:26:00 +0400 Subject: [PATCH 4/8] Handle zero light.adjust values and nested all-groups --- homeassistant/components/group/light.py | 8 ++- homeassistant/components/light/__init__.py | 25 ++++++-- tests/components/group/test_light.py | 63 ++++++++++++++++++++ tests/components/light/test_init.py | 67 ++++++++++++++++++++++ 4 files changed, 156 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index d7a004e16b6a0..df5eabffc42e9 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -191,9 +191,13 @@ async def async_adjust(self, **kwargs: Any) -> None: key: value for key, value in kwargs.items() if key in FORWARDED_ATTRIBUTES } entity_ids = [ - entity_id + state.entity_id for entity_id in self._entity_ids - if self.hass.states.is_state(entity_id, STATE_ON) + if (state := self.hass.states.get(entity_id)) is not None + and ( + state.state == STATE_ON + or isinstance(state.attributes.get(ATTR_ENTITY_ID), list) + ) ] if not entity_ids: return diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 86a30baad52ce..8044e93958fe1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -175,6 +175,8 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) VALID_BRIGHTNESS_STEP = vol.All(vol.Coerce(int), vol.Clamp(min=-255, max=255)) VALID_BRIGHTNESS_STEP_PCT = vol.All(vol.Coerce(float), vol.Clamp(min=-100, max=100)) +VALID_BRIGHTNESS_ADJUST = vol.All(vol.Coerce(int), vol.Range(min=1, max=255)) +VALID_BRIGHTNESS_PCT_ADJUST = vol.All(vol.Coerce(float), vol.Range(min=0.001, max=100)) VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG]) LIGHT_TURN_ON_SCHEMA: VolDictType = { @@ -221,8 +223,8 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | LIGHT_ADJUST_SCHEMA: VolDictType = { ATTR_TRANSITION: VALID_TRANSITION, - vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS, - vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_BRIGHTNESS, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_ADJUST, + vol.Exclusive(ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_PCT_ADJUST, vol.Exclusive(ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP, vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, @@ -248,7 +250,7 @@ def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), - vol.Exclusive(ATTR_WHITE, COLOR_GROUP): vol.Any(True, VALID_BRIGHTNESS), + vol.Exclusive(ATTR_WHITE, COLOR_GROUP): vol.Any(True, VALID_BRIGHTNESS_ADJUST), ATTR_EFFECT: cv.string, } @@ -567,10 +569,23 @@ async def async_handle_light_adjust_service( if not call.data["params"]: return - params = process_turn_on_params(hass, light, call.data["params"]) + raw_params = call.data["params"] + + if ( + raw_params.get(ATTR_BRIGHTNESS) == 0 + or raw_params.get(ATTR_BRIGHTNESS_PCT) == 0 + or raw_params.get(ATTR_WHITE) == 0 + ): + raise HomeAssistantError( + "light.adjust does not accept zero brightness or white values" + ) + + params = process_turn_on_params(hass, light, raw_params) if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: - return + raise HomeAssistantError( + "light.adjust does not accept zero brightness or white values" + ) await light.async_adjust(**filter_turn_on_params(light, params)) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index dc4cf43c66127..9265c86937ca6 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1558,6 +1558,69 @@ async def test_adjust_service_call_targets_on_members_for_all_group( assert entities[1].last_call("turn_on") is None +async def test_adjust_service_call_reaches_nested_all_group( + hass: HomeAssistant, +) -> None: + """Test adjust on a parent group still reaches a nested all-group.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + MockLight("test3", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity in entities: + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = 64 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "name": "Inner Group", + "entities": ["light.test1", "light.test2"], + "all": "true", + }, + { + "platform": DOMAIN, + "name": "Outer Group", + "entities": ["light.inner_group", "light.test3"], + "all": "false", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # The nested all-group reports off while only one child is on. + assert hass.states.get("light.inner_group").state == STATE_OFF + assert hass.states.get("light.outer_group").state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.outer_group", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + assert hass.states.get("light.test1").state == STATE_ON + assert hass.states.get("light.test1").attributes[ATTR_BRIGHTNESS] == 128 + assert hass.states.get("light.test2").state == STATE_OFF + assert hass.states.get("light.test3").state == STATE_OFF + + _, data = entities[0].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 128} + assert entities[1].last_call("turn_on") is None + assert entities[2].last_call("turn_on") is None + + async def test_service_call_effect(hass: HomeAssistant) -> None: """Test service calls.""" await async_setup_component( diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 9ca63f595a1fb..f9ac4678e49ca 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -478,6 +478,73 @@ async def test_adjust_service_only_adjusts_on_lights( assert ent2.last_call("turn_on") is None +async def test_adjust_service_rejects_zero_brightness( + hass: HomeAssistant, + mock_light_entities: list[MockLight], +) -> None: + """Test adjust service rejects zero brightness values.""" + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + { + ATTR_ENTITY_ID: mock_light_entities[0].entity_id, + light.ATTR_BRIGHTNESS: 0, + }, + blocking=True, + ) + + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + { + ATTR_ENTITY_ID: mock_light_entities[0].entity_id, + light.ATTR_BRIGHTNESS_PCT: 0, + }, + blocking=True, + ) + + +async def test_adjust_service_rejects_step_resolving_to_zero( + hass: HomeAssistant, + mock_light_entities: list[MockLight], +) -> None: + """Test adjust service rejects steps that resolve to zero brightness.""" + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1 = mock_light_entities[0] + ent1.supported_color_modes = {light.ColorMode.BRIGHTNESS} + ent1.color_mode = light.ColorMode.BRIGHTNESS + ent1._attr_brightness = 10 + + with pytest.raises( + HomeAssistantError, + match="light.adjust does not accept zero brightness or white values", + ): + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_BRIGHTNESS_STEP: -10, + }, + blocking=True, + ) + + @pytest.mark.parametrize( ("profile_name", "last_call", "expected_data"), [ From bededb8ee879b8411d0e0762a9e425e2244857df Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Fri, 5 Jun 2026 17:21:13 +0400 Subject: [PATCH 5/8] Fix step adjust no-op targets and metadata --- homeassistant/components/light/__init__.py | 6 ++++ homeassistant/components/light/services.yaml | 17 +++++++-- tests/components/light/test_init.py | 36 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 8044e93958fe1..d4862677d92a4 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -580,6 +580,12 @@ async def async_handle_light_adjust_service( "light.adjust does not accept zero brightness or white values" ) + if not light.is_on and ( + ATTR_BRIGHTNESS_STEP in raw_params + or ATTR_BRIGHTNESS_STEP_PCT in raw_params + ): + return + params = process_turn_on_params(hass, light, raw_params) if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 27c45932f9468..7f371ba521f42 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -213,6 +213,13 @@ turn_on: min: 0 max: 100 unit_of_measurement: "%" + brightness_pct_adjust: &brightness_pct_adjust + filter: *brightness_support + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" brightness_step_pct: &brightness_step_pct filter: *brightness_support selector: @@ -262,6 +269,12 @@ turn_on: number: min: 0 max: 255 + brightness_adjust: &brightness_adjust + filter: *brightness_support + selector: + number: + min: 1 + max: 255 brightness_step: &brightness_step filter: *brightness_support selector: @@ -311,7 +324,7 @@ adjust: transition: *transition rgb_color: *rgb_color color_temp_kelvin: *color_temp_kelvin - brightness_pct: *brightness_pct + brightness_pct: *brightness_pct_adjust effect: *effect advanced_fields: collapsed: true @@ -321,7 +334,7 @@ adjust: color_name: *color_name hs_color: *hs_color xy_color: *xy_color - brightness: *brightness + brightness: *brightness_adjust brightness_step: *brightness_step brightness_step_pct: *brightness_step_pct white: *white diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index f9ac4678e49ca..dbb2e9db9ecdb 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -545,6 +545,42 @@ async def test_adjust_service_rejects_step_resolving_to_zero( ) +async def test_adjust_service_ignores_off_lights_for_negative_steps( + hass: HomeAssistant, + mock_light_entities: list[MockLight], +) -> None: + """Test step-based adjust keeps off lights as a no-op.""" + setup_test_component_platform(hass, light.DOMAIN, mock_light_entities) + + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, ent2, _ = mock_light_entities + ent1.supported_color_modes = {light.ColorMode.BRIGHTNESS} + ent1.color_mode = light.ColorMode.BRIGHTNESS + ent1.brightness = 100 + ent2.supported_color_modes = {light.ColorMode.BRIGHTNESS} + ent2.color_mode = light.ColorMode.BRIGHTNESS + + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_ADJUST, + { + ATTR_ENTITY_ID: [ent1.entity_id, ent2.entity_id], + light.ATTR_BRIGHTNESS_STEP: -10, + }, + blocking=True, + ) + + assert light.is_on(hass, ent1.entity_id) + assert not light.is_on(hass, ent2.entity_id) + _, data = ent1.last_call("turn_on") + assert data == {light.ATTR_BRIGHTNESS: 90} + assert ent2.last_call("turn_on") is None + + @pytest.mark.parametrize( ("profile_name", "last_call", "expected_data"), [ From 549a7ae502b159c16dccfd09fb61778eec519c3a Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Sun, 7 Jun 2026 17:27:32 +0400 Subject: [PATCH 6/8] Fix light.adjust step handling for groups --- homeassistant/components/group/light.py | 4 ++ homeassistant/components/light/__init__.py | 12 ++++- tests/components/group/test_light.py | 53 ++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index df5eabffc42e9..4f4a92f010b16 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -10,6 +10,8 @@ from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_MODE, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, @@ -129,6 +131,8 @@ def async_create_preview_light( FORWARDED_ATTRIBUTES = frozenset( { ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_FLASH, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d4862677d92a4..5e025996bb57a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -580,9 +580,19 @@ async def async_handle_light_adjust_service( "light.adjust does not accept zero brightness or white values" ) - if not light.is_on and ( + has_step_adjust = ( ATTR_BRIGHTNESS_STEP in raw_params or ATTR_BRIGHTNESS_STEP_PCT in raw_params + ) + + if light.__class__.async_adjust is not LightEntity.async_adjust and has_step_adjust: + await light.async_adjust(**filter_turn_on_params(light, raw_params)) + return + + if ( + light.__class__.async_adjust is LightEntity.async_adjust + and not light.is_on + and has_step_adjust ): return diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 9265c86937ca6..ac0b51dc0596c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -9,6 +9,7 @@ from homeassistant.components.group import DOMAIN, SERVICE_RELOAD, light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_STEP, ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, @@ -1558,6 +1559,58 @@ async def test_adjust_service_call_targets_on_members_for_all_group( assert entities[1].last_call("turn_on") is None +async def test_adjust_service_call_targets_on_members_for_all_group_step( + hass: HomeAssistant, +) -> None: + """Test step adjust on an all-group still reaches active members.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity, brightness in zip(entities, (64, 32), strict=True): + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = brightness + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "true", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # all-groups report off until every member is on + assert hass.states.get("light.light_group").state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS_STEP: -10}, + blocking=True, + ) + + assert hass.states.get("light.test1").state == STATE_ON + assert hass.states.get("light.test2").state == STATE_OFF + assert entities[1].brightness == 32 + + _, data = entities[0].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 54} + assert entities[1].last_call("turn_on") is None + + async def test_adjust_service_call_reaches_nested_all_group( hass: HomeAssistant, ) -> None: From 326d0d34bcd07db0ce825123fac5bb81fe12eb7e Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Sun, 7 Jun 2026 23:04:40 +0400 Subject: [PATCH 7/8] Tighten light.adjust follow-up fixes --- homeassistant/components/light/__init__.py | 4 +- homeassistant/components/light/services.yaml | 28 +++++----- tests/components/group/test_light.py | 58 ++++++++++++++++++++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 5e025996bb57a..a0192795d0d4e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -569,7 +569,7 @@ async def async_handle_light_adjust_service( if not call.data["params"]: return - raw_params = call.data["params"] + raw_params = dict(call.data["params"]) if ( raw_params.get(ATTR_BRIGHTNESS) == 0 @@ -596,7 +596,7 @@ async def async_handle_light_adjust_service( ): return - params = process_turn_on_params(hass, light, raw_params) + params = process_turn_on_params(hass, light, raw_params.copy()) if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: raise HomeAssistantError( diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 7f371ba521f42..27a43c46adcc4 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -213,13 +213,6 @@ turn_on: min: 0 max: 100 unit_of_measurement: "%" - brightness_pct_adjust: &brightness_pct_adjust - filter: *brightness_support - selector: - number: - min: 1 - max: 100 - unit_of_measurement: "%" brightness_step_pct: &brightness_step_pct filter: *brightness_support selector: @@ -269,12 +262,6 @@ turn_on: number: min: 0 max: 255 - brightness_adjust: &brightness_adjust - filter: *brightness_support - selector: - number: - min: 1 - max: 255 brightness_step: &brightness_step filter: *brightness_support selector: @@ -324,7 +311,13 @@ adjust: transition: *transition rgb_color: *rgb_color color_temp_kelvin: *color_temp_kelvin - brightness_pct: *brightness_pct_adjust + brightness_pct: + filter: *brightness_support + selector: + number: + min: 0.001 + max: 100 + unit_of_measurement: "%" effect: *effect advanced_fields: collapsed: true @@ -334,7 +327,12 @@ adjust: color_name: *color_name hs_color: *hs_color xy_color: *xy_color - brightness: *brightness_adjust + brightness: + filter: *brightness_support + selector: + number: + min: 1 + max: 255 brightness_step: *brightness_step brightness_step_pct: *brightness_step_pct white: *white diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index ac0b51dc0596c..54f39220a03ae 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP, + ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, @@ -1611,6 +1612,63 @@ async def test_adjust_service_call_targets_on_members_for_all_group_step( assert entities[1].last_call("turn_on") is None +@pytest.mark.parametrize( + ("step_attr", "step_value", "expected_brightness"), + [ + (ATTR_BRIGHTNESS_STEP, -10, (90, 40)), + (ATTR_BRIGHTNESS_STEP_PCT, -10, (74, 26)), + ], +) +async def test_adjust_service_call_preserves_group_member_brightness_steps( + hass: HomeAssistant, + step_attr: str, + step_value: int, + expected_brightness: tuple[int, int], +) -> None: + """Test step adjust resolves independently for each active group member.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_ON), + MockLight("test3", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity, brightness in zip(entities, (100, 50, 25), strict=True): + entity.supported_color_modes = {ColorMode.BRIGHTNESS} + entity.color_mode = ColorMode.BRIGHTNESS + entity.brightness = brightness + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", step_attr: step_value}, + blocking=True, + ) + + for entity, expected in zip(entities[:2], expected_brightness, strict=True): + _, data = entity.last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: expected} + + assert entities[2].last_call("turn_on") is None + + async def test_adjust_service_call_reaches_nested_all_group( hass: HomeAssistant, ) -> None: From 55898c6ec5cc492848b02e98469aebca1fee67b8 Mon Sep 17 00:00:00 2001 From: Daniil Karpenko Date: Sun, 7 Jun 2026 23:56:51 +0400 Subject: [PATCH 8/8] Fix light.adjust group follow-up regressions --- homeassistant/components/group/light.py | 2 + homeassistant/components/light/__init__.py | 33 ++--- tests/components/group/test_light.py | 145 +++++++++++++++++++++ 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 4f4a92f010b16..ae9381c7dd3d4 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -13,6 +13,7 @@ ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -133,6 +134,7 @@ def async_create_preview_light( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP, ATTR_BRIGHTNESS_STEP_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_FLASH, diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a0192795d0d4e..bb273e199f5b8 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -334,6 +334,8 @@ def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[st ) if not brightness_supported(supported_color_modes): params.pop(ATTR_BRIGHTNESS, None) + params.pop(ATTR_BRIGHTNESS_STEP, None) + params.pop(ATTR_BRIGHTNESS_STEP_PCT, None) if ColorMode.COLOR_TEMP not in supported_color_modes: params.pop(ATTR_COLOR_TEMP_KELVIN, None) if ColorMode.HS not in supported_color_modes: @@ -571,39 +573,38 @@ async def async_handle_light_adjust_service( raw_params = dict(call.data["params"]) - if ( - raw_params.get(ATTR_BRIGHTNESS) == 0 - or raw_params.get(ATTR_BRIGHTNESS_PCT) == 0 - or raw_params.get(ATTR_WHITE) == 0 - ): - raise HomeAssistantError( - "light.adjust does not accept zero brightness or white values" - ) + if light.__class__.async_adjust is not LightEntity.async_adjust: + params = filter_turn_on_params(light, raw_params) + if not params: + return + await light.async_adjust(**params) + return has_step_adjust = ( ATTR_BRIGHTNESS_STEP in raw_params or ATTR_BRIGHTNESS_STEP_PCT in raw_params ) - if light.__class__.async_adjust is not LightEntity.async_adjust and has_step_adjust: - await light.async_adjust(**filter_turn_on_params(light, raw_params)) - return - if ( - light.__class__.async_adjust is LightEntity.async_adjust - and not light.is_on + not light.is_on and has_step_adjust ): return - params = process_turn_on_params(hass, light, raw_params.copy()) + params = filter_turn_on_params( + light, + process_turn_on_params(hass, light, raw_params.copy()), + ) + + if not params: + return if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: raise HomeAssistantError( "light.adjust does not accept zero brightness or white values" ) - await light.async_adjust(**filter_turn_on_params(light, params)) + await light.async_adjust(**params) async def async_handle_toggle_service( light: LightEntity, call: ServiceCall diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 54f39220a03ae..d0919bbd4223c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -25,12 +25,15 @@ ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, + DATA_PROFILES, DOMAIN as LIGHT_DOMAIN, SERVICE_ADJUST, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, ColorMode, + LightEntityFeature, + Profile, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -1669,6 +1672,148 @@ async def test_adjust_service_call_preserves_group_member_brightness_steps( assert entities[2].last_call("turn_on") is None +async def test_adjust_service_call_preserves_color_name_with_group_steps( + hass: HomeAssistant, +) -> None: + """Test step adjust keeps color_name when forwarding through a group.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_ON), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity, brightness in zip(entities, (100, 50), strict=True): + entity.supported_color_modes = {ColorMode.RGB} + entity.color_mode = ColorMode.RGB + entity.brightness = brightness + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + { + ATTR_ENTITY_ID: "light.light_group", + ATTR_BRIGHTNESS_STEP: -10, + ATTR_COLOR_NAME: "red", + }, + blocking=True, + ) + + for entity, expected_brightness in zip(entities, (90, 40), strict=True): + _, data = entity.last_call("turn_on") + assert data == { + ATTR_BRIGHTNESS: expected_brightness, + ATTR_RGB_COLOR: (255, 0, 0), + } + + +async def test_adjust_service_call_all_group_does_not_apply_default_profile( + hass: HomeAssistant, +) -> None: + """Test adjust on a partially-on all-group does not inject default profiles.""" + entities = [ + MockLight("test1", STATE_ON), + MockLight("test2", STATE_OFF), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + for entity in entities: + entity.supported_color_modes = {ColorMode.HS} + entity.color_mode = ColorMode.HS + entity.supported_features = LightEntityFeature.TRANSITION + entity.brightness = 64 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + "all": "true", + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.data[DATA_PROFILES].data["light.light_group.default"] = Profile( + "light.light_group.default", 0.4, 0.6, 99, 2 + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + + _, data = entities[0].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 128} + assert entities[1].last_call("turn_on") is None + + +async def test_adjust_service_call_ignores_unsupported_steps_in_mixed_group( + hass: HomeAssistant, +) -> None: + """Test unsupported brightness steps do not fail mixed groups.""" + entities = [ + MockLight("test1", STATE_ON, {ColorMode.ONOFF}), + MockLight("test2", STATE_ON, {ColorMode.BRIGHTNESS}), + ] + setup_test_component_platform(hass, LIGHT_DOMAIN, entities) + + entities[1].brightness = 100 + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_ADJUST, + {ATTR_ENTITY_ID: "light.light_group", ATTR_BRIGHTNESS_STEP: -10}, + blocking=True, + ) + + assert entities[0].last_call("turn_on") is None + _, data = entities[1].last_call("turn_on") + assert data == {ATTR_BRIGHTNESS: 90} + + async def test_adjust_service_call_reaches_nested_all_group( hass: HomeAssistant, ) -> None: