Skip to content
Open
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
34 changes: 34 additions & 0 deletions homeassistant/components/group/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
from homeassistant.components import light
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_STEP,
ATTR_BRIGHTNESS_STEP_PCT,
ATTR_COLOR_MODE,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_EFFECT_LIST,
Expand Down Expand Up @@ -129,6 +132,9 @@ def async_create_preview_light(
FORWARDED_ATTRIBUTES = frozenset(
{
ATTR_BRIGHTNESS,
ATTR_BRIGHTNESS_STEP,
ATTR_BRIGHTNESS_STEP_PCT,
ATTR_COLOR_NAME,
ATTR_COLOR_TEMP_KELVIN,
ATTR_EFFECT,
ATTR_FLASH,
Expand Down Expand Up @@ -185,6 +191,34 @@ 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
Comment on lines +196 to +197

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve color_name when forwarding step adjusts

When light.adjust is called on a Home Assistant light group with both color_name and brightness_step/brightness_step_pct, the service handler deliberately skips process_turn_on_params so each member can resolve its own step, then this filter drops color_name because it is not in FORWARDED_ATTRIBUTES. The nested light.adjust call therefore receives only the brightness step and silently ignores the requested color, even though the service schema accepts that combination.

Useful? React with 👍 / 👎.

}
entity_ids = [
state.entity_id
for entity_id in self._entity_ids
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)
)
]
Comment on lines +199 to +207

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward adjust into nested all-groups

When a light group contains another HA light group configured with all: true, that child group reports off until every member is on; this filter skips it, so light.adjust on the parent is a no-op even when the child group has currently-on members. Nested light groups are already supported, and the new direct all-group adjust path is intended to reach active members, so parent groups need to forward into child light groups instead of filtering only STATE_ON children.

Useful? React with 👍 / 👎.

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}
Expand Down
96 changes: 95 additions & 1 deletion homeassistant/components/light/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -174,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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Keep adjust brightness_pct from rounding to zero

This validator accepts very small positive percentages such as brightness_pct: 0.1, but preprocess_turn_on_alternatives converts percentages with round(255 * pct / 100), so any value below roughly 0.196 becomes brightness: 0 and is later rejected by light.adjust with a service error. Users can submit values allowed by the new schema/UI selector that still fail at runtime; raising the minimum to the first percentage that maps to brightness 1 or clamping after conversion would make the contract consistent.

Useful? React with 👍 / 👎.

VALID_FLASH = vol.In([FLASH_SHORT, FLASH_LONG])

LIGHT_TURN_ON_SCHEMA: VolDictType = {
Expand All @@ -182,7 +185,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(
Expand Down Expand Up @@ -216,6 +221,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_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,
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_ADJUST),
ATTR_EFFECT: cv.string,
}


_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -296,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:
Expand Down Expand Up @@ -524,6 +564,48 @@ 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 call.data["params"]:
return

raw_params = dict(call.data["params"])

if light.__class__.async_adjust is not LightEntity.async_adjust:
params = filter_turn_on_params(light, raw_params)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Convert colors before filtering group adjusts

When light.adjust targets a Home Assistant light group whose active members support color but not the exact color space supplied by the service UI (for example an HS-only light and rgb_color), this custom-adjust branch filters the raw params against the group's supported modes before process_turn_on_params can convert RGB to HS/XY/color-temp. The result is that the color field is dropped and the group adjust becomes a no-op, while targeting the same light directly would convert and apply the color.

Useful? React with 👍 / 👎.

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 (
not light.is_on
and has_step_adjust
):
return

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"
)
Comment on lines +602 to +605

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter unsupported brightness steps before zero checks

For an on light that does not support brightness, a negative brightness_step/brightness_step_pct is resolved from light.brightness == None to brightness: 0, and this check raises before filter_turn_on_params can drop the unsupported brightness field. This can make light.adjust fail for mixed groups where one active member is ONOFF-only while another member exposes brightness support, instead of ignoring the unsupported step on the ONOFF member.

Useful? React with 👍 / 👎.


await light.async_adjust(**params)

async def async_handle_toggle_service(
light: LightEntity, call: ServiceCall
) -> None:
Expand All @@ -538,6 +620,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),
Expand Down Expand Up @@ -1062,3 +1150,9 @@ 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."""
if not self.is_on:
return
await self.async_turn_on(**kwargs)
3 changes: 3 additions & 0 deletions homeassistant/components/light/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
}
},
"services": {
"adjust": {
"service": "mdi:lightbulb-on-50"
},
"toggle": {
"service": "mdi:lightbulb"
},
Expand Down
38 changes: 36 additions & 2 deletions homeassistant/components/light/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -262,7 +262,7 @@ turn_on:
number:
min: 0
max: 255
brightness_step:
brightness_step: &brightness_step
filter: *brightness_support
selector:
number:
Expand Down Expand Up @@ -303,6 +303,40 @@ 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:
filter: *brightness_support
selector:
number:
min: 0.001
max: 100
unit_of_measurement: "%"
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:
filter: *brightness_support
selector:
number:
min: 1
max: 255
brightness_step: *brightness_step
brightness_step_pct: *brightness_step_pct
white: *white

toggle:
target:
entity:
Expand Down
67 changes: 67 additions & 0 deletions homeassistant/components/light/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading