Skip to content

Add light.adjust service/action#1

Open
dankarization wants to merge 8 commits into
devfrom
feature/light-adjust-action
Open

Add light.adjust service/action#1
dankarization wants to merge 8 commits into
devfrom
feature/light-adjust-action

Conversation

@dankarization

@dankarization dankarization commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a new light.adjust service/action to the light domain for attribute-only light changes without turning on lights that are currently off.

This is implemented conservatively:

  • off lights default to no-op
  • on lights route through async_adjust
  • Home Assistant light groups expand only to members that are currently on
  • service metadata is included so the action is human-readable in the UI

This PR is paired with the frontend PR that calls light.adjust from group light controls:

This proposal shape follows the recent architecture discussion comment:

Why

Recent frontend UX improvements made HA light groups a natural room-level control surface. In that UI, changing brightness or color-related attributes on a partially-on group should adjust the active lights in the group, not wake up members that are currently off.

Using light.turn_on for that intent is too broad because it inherently carries a power-on semantic. light.adjust gives the frontend and automations a precise action for "change attributes, but do not turn on currently-off lights".

What Changed

  • add light.adjust service schema and handler
  • add default LightEntity.async_adjust
  • route active lights through adjust semantics without changing off lights
  • make LightGroup.async_adjust target only currently-on members
  • keep all-off group calls as no-op
  • add service metadata in strings.json and icons.json
  • add regression coverage for light groups and base light behavior

Edge Cases Resolved

  • removed adjust-only selectors from light.turn_on metadata so the action UI does not advertise unsupported fields there
  • copy filtered params before per-entity dispatch so custom async_adjust() handlers do not observe shared mutation across group members
  • preserve per-member brightness_step and brightness_step_pct semantics when a group fans out to active members with different current brightness
  • keep nested and all: true light groups on adjust-only semantics without waking members that are currently off
  • preserve color_name when light.adjust is combined with step-based brightness changes on groups
  • avoid applying default profiles from group-level process_turn_on_params() during custom group async_adjust() handling
  • run zero-value rejection after unsupported brightness params are filtered so mixed-capability groups do not fail on ONOFF-only members

Relation To Frontend

This core change is intentionally paired with a frontend caller change. On its own, this PR provides the backend semantics and service metadata. The companion frontend PR updates group light controls to use light.adjust when the entity is a Home Assistant light group with member entities, while preserving light.turn_on for off or fully-off groups.

That frontend PR should land together with this one, or after this one.

Testing

  • Python compile on changed core files
  • services.yaml parse check
  • git diff --check
  • regression tests added in:
    • tests/components/group/test_light.py
    • tests/components/light/test_init.py

References

@dankarization

Copy link
Copy Markdown
Owner Author

@codex review этого кода, учитывая что в HA эта тема щепетильная и меня будут терроризировать на точность, аккуратность, лишность и тд.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a82bddfe5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread homeassistant/components/light/__init__.py Outdated
@dankarization

Copy link
Copy Markdown
Owner Author

@codex review new changes and all other code until you find everything.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cbbc2b13bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if not call.data["params"]:
return

params = process_turn_on_params(hass, light, call.data["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 Preserve per-light brightness steps for groups

When light.adjust is called on a Home Assistant light group with brightness_step or brightness_step_pct, this line resolves the step against the group entity before LightGroup.async_adjust fans the request out to the currently-on members. Because the group brightness is an aggregate, members at different brightness levels all receive the same absolute brightness instead of being adjusted relative to their own current values (for example, bulbs at 50 and 200 with step 10 both become the group average + 10), unlike targeting those lights directly.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

well, yes, but this problem exists in light.turn_on also< in current behaviour. So solving it is for another pr

Comment on lines +572 to +573
if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:
return

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 Don't ignore zero-brightness adjustments

When a caller uses light.adjust on an already-on light with brightness: 0, brightness_pct: 0, or a negative brightness step that resolves to 0, process_turn_on_params normalizes the request to ATTR_BRIGHTNESS: 0 and this return exits without calling async_adjust. Since the new service schema exposes 0 as a valid brightness value, slider/action callers can try to dim active lights all the way down and get a silent no-op instead of either turning those active lights off or rejecting the value.

Useful? React with 👍 / 👎.

Comment on lines +193 to +197
entity_ids = [
entity_id
for entity_id in self._entity_ids
if self.hass.states.is_state(entity_id, STATE_ON)
]

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 👍 / 👎.

@dankarization

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f013d82210

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +583 to +585
params = process_turn_on_params(hass, light, raw_params)

if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:

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 Skip off lights before resolving adjust steps

When light.adjust is called with a negative brightness_step/brightness_step_pct and the target set includes any currently-off light, process_turn_on_params() resolves that off light from brightness 0 to brightness 0 and this handler raises before LightEntity.async_adjust() can no-op the off entity. In that mixed-target scenario the whole service call can fail instead of adjusting the on lights and ignoring the off ones, which contradicts the new service contract that off lights are no-ops.

Useful? React with 👍 / 👎.

color_name: *color_name
hs_color: *hs_color
xy_color: *xy_color
brightness: *brightness

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 Keep adjust selectors in sync with zero rejection

The service metadata reuses the turn_on brightness selector, which allows selecting 0, but the new LIGHT_ADJUST_SCHEMA and handler reject zero brightness/white values. Users configuring light.adjust from the actions UI can therefore select a value the backend refuses at runtime; the adjust metadata should use selectors whose minimums match the adjust validation instead of the turn_on aliases.

Useful? React with 👍 / 👎.

@dankarization

Copy link
Copy Markdown
Owner Author

@codex review please

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bededb8ee8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +583 to +587
if not light.is_on and (
ATTR_BRIGHTNESS_STEP in raw_params
or ATTR_BRIGHTNESS_STEP_PCT in raw_params
):
return

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 Let all-mode groups handle brightness steps

When light.adjust is called with brightness_step or brightness_step_pct on a light group configured with all: true, the group reports is_on == false whenever only some members are on, so this early return drops the service call before LightGroup.async_adjust can expand the target to the active members. In that partially-on all-mode group scenario, absolute brightness/color adjusts reach the on children, but step-based dim/brighten actions become a no-op for the same active lights.

Useful? React with 👍 / 👎.

Comment on lines +216 to +220
brightness_pct_adjust: &brightness_pct_adjust
filter: *brightness_support
selector:
number:
min: 1

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 Remove adjust-only fields from turn_on metadata

In the light.turn_on service metadata, this creates a selectable field named brightness_pct_adjust (and brightness_adjust is added similarly below), but the registered LIGHT_TURN_ON_SCHEMA only accepts brightness_pct/brightness. When the action editor exposes one of these new fields for light.turn_on, saving or running that action sends an extra key that the backend rejects instead of turning the light on.

Useful? React with 👍 / 👎.

):
return

params = process_turn_on_params(hass, 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 Preserve step adjustments for light groups

For a light.adjust call that targets a light group with multiple active members at different brightness levels, process_turn_on_params runs against the group entity before LightGroup.async_adjust expands to the children, so brightness_step/brightness_step_pct is converted into one absolute brightness based on the group's averaged brightness. A dim-by-10 action on children at 100 and 50 therefore forwards the same value (about 65) to both instead of adjusting them independently to 90 and 40, unlike targeting those lights directly.

Useful? React with 👍 / 👎.

@dankarization

Copy link
Copy Markdown
Owner Author

@codex GOD I WISH THIS WILL BE THE LAST TIME!!!
check this please.

Repository owner deleted a comment from chatgpt-codex-connector Bot Jun 7, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Committed follow-up changes in 2050222f and created the PR “Tighten light.adjust follow-up fixes.”
  • Updated step-based light.adjust dispatch for custom adjust handlers so filtered params are copied before mutation, avoiding shared service param mutation while preserving raw step forwarding for groups.
  • Removed adjust-only selector fields from light.turn_on metadata and kept turn_on brightness selectors at their original zero-capable ranges.
  • Added inline light.adjust brightness selectors with minimums that match the backend zero-rejection validation.
  • Added parameterized regression coverage proving group brightness_step and brightness_step_pct resolve independently per active member while off members remain untouched.

Testing

  • ⚠️ pytest -q tests/components/light/test_init.py::test_adjust_service_only_adjusts_on_lights tests/components/light/test_init.py::test_adjust_service_rejects_zero_brightness tests/components/light/test_init.py::test_adjust_service_rejects_step_resolving_to_zero tests/components/light/test_init.py::test_adjust_service_ignores_off_lights_for_negative_steps tests/components/group/test_light.py::test_adjust_service_call_only_targets_on_group_members tests/components/group/test_light.py::test_adjust_service_call_preserves_group_member_brightness_steps tests/components/group/test_light.py::test_adjust_service_call_all_off_group_is_noop tests/components/group/test_light.py::test_adjust_service_call_targets_on_members_for_all_group tests/components/group/test_light.py::test_adjust_service_call_targets_on_members_for_all_group_step tests/components/group/test_light.py::test_adjust_service_call_reaches_nested_all_group (warning: failed during collection because paho is not installed in this environment)
  • python -m compileall homeassistant/components/light/__init__.py homeassistant/components/group/light.py tests/components/light/test_init.py tests/components/group/test_light.py
  • python - <<'PY' from pathlib import Path import yaml with Path('homeassistant/components/light/services.yaml').open() as f: data = yaml.safe_load(f) assert 'brightness_pct_adjust' not in data['turn_on']['fields'] assert 'brightness_adjust' not in data['turn_on']['fields']['advanced_fields']['fields'] assert data['adjust']['fields']['brightness_pct']['selector']['number']['min'] == 0.001 assert data['adjust']['fields']['advanced_fields']['fields']['brightness']['selector']['number']['min'] == 1 print('services.yaml adjust metadata ok') PY
  • git diff --check
  • python -m ruff check homeassistant/components/light/__init__.py homeassistant/components/group/light.py tests/components/light/test_init.py tests/components/group/test_light.py

View task →

@dankarization

Copy link
Copy Markdown
Owner Author

@codex re-check please

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 326d0d34bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +194 to +195
data = {
key: value for key, value in kwargs.items() if key in FORWARDED_ATTRIBUTES

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 👍 / 👎.

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 👍 / 👎.

):
return

params = process_turn_on_params(hass, light, raw_params.copy())

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 Avoid applying off-state defaults to partially-on all groups

When light.adjust targets an all: true light group with only some members on, the group state is off even though LightGroup.async_adjust will still forward the adjustment to the active members. Running process_turn_on_params against that off group makes Profiles.apply_default inject the group or group.all_lights default profile, so a brightness-only adjust can unexpectedly add the default color/transition to the on members.

Useful? React with 👍 / 👎.

Comment on lines +601 to +604
if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0:
raise HomeAssistantError(
"light.adjust does not accept zero brightness or white values"
)

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 👍 / 👎.

@dankarization

Copy link
Copy Markdown
Owner Author

@codex допушил follow-up на 3 P2 замечания.

Что сделал коротко:

  • сохранил color_name при group light.adjust со step
  • убрал лишнюю group-level обработку adjust для custom async_adjust(), чтобы не подмешивались default profiles на partially-on all: true group
  • перенёс zero-check после фильтрации unsupported brightness params, чтобы mixed groups не падали на ONOFF-only участниках
  • добавил regression tests под эти кейсы

Пуш в эту же ветку: 55898c6ec5.
Проверка локально: выбранные pytest зелёные, ruff зелёный.

Посмотри, пожалуйста, ещё раз.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55898c6ec5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant