Skip to content

Add color helper integration - #177605

Draft
kkilchrist wants to merge 15 commits into
home-assistant:devfrom
kkilchrist:color-helper
Draft

Add color helper integration#177605
kkilchrist wants to merge 15 commits into
home-assistant:devfrom
kkilchrist:color-helper

Conversation

@kkilchrist

Copy link
Copy Markdown

Proposed change

Adds a new color helper integration: a reusable, named color value stored as a helper entity, addressing a decade of feature requests for a color equivalent of input_number/input_datetime (see the proposal in the discussion linked below).

Design summary:

  • Config-entry helper (integration_type: helper, config flow + options flow), matching the pattern of threshold/derivative/template. Zero external dependencies.
  • Canonical value: CIE 1931 xy + kind (chromatic|white) + kelvin (when white) + optional brightness. All other representations (hex, rgb_color, hs_color, color_temp_kelvin) are derived state attributes; state is the hex string.
  • color_params attribute: a dict splattable directly into light.turn_on (xy_color or color_temp_kelvin, plus brightness when stored) so automations need no kind/capability branching — in the spirit of light.turn_on's profile: parameter.
  • Actions: set_color (exactly one of six input shapes, validated at schema level with ServiceValidationError translation keys), set_brightness, clear_brightness. Deliberately no action that commands other entities.
  • Scene support via async_reproduce_states; persistence via RestoreEntity + versioned ExtraStoredData.
  • 56 tests; 100% coverage on config_flow.py, __init__.py, and reproduce_state.py; hassfest, ruff, and mypy strict pass.

Field-tested as a HACS custom integration: https://github.com/kkilchrist/ha-color-ext

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Per the AI policy: Claude (Claude Code) was used to assist with the port, tests, and documentation. The design, implementation decisions, and review are mine.

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

🤖 Generated with Claude Code

https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Replaces the earlier storage-collection input_color port with the
config-entry helper pattern. Stores a color (chromatic or white CCT,
optional brightness) as a restorable helper entity with derived
representations, a color_params attribute splattable into
light.turn_on, and scene reproduce support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Color conversion, scene restoration, persisted-data handling, icon clearing, and quality-scale compliance have unresolved issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

Adds a config-entry-based Color helper for storing reusable color values, brightness, and derived representations.

Changes:

  • Adds color normalization, persistence, services, scenes, and config/options flows.
  • Adds UI metadata, translations, generated registrations, and ownership.
  • Adds comprehensive integration and unit tests.
File summaries
File Description
.strict-typing Enables strict typing.
CODEOWNERS Adds integration ownership.
homeassistant/components/color/__init__.py Registers entities and services.
homeassistant/components/color/color_math.py Implements color conversion.
homeassistant/components/color/config_flow.py Adds configuration flows.
homeassistant/components/color/const.py Defines integration constants.
homeassistant/components/color/entity.py Implements state and persistence.
homeassistant/components/color/icons.json Defines service icons.
homeassistant/components/color/manifest.json Declares integration metadata.
homeassistant/components/color/reproduce_state.py Adds scene reproduction.
homeassistant/components/color/services.yaml Defines service selectors.
homeassistant/components/color/strings.json Adds user-facing strings.
homeassistant/generated/config_flows.py Registers the helper flow.
homeassistant/generated/integrations.json Adds generated metadata.
script/hassfest/quality_scale.py Adds quality-scale exemptions.
tests/components/color/__init__.py Initializes the test package.
tests/components/color/test_color_math.py Tests color conversion.
tests/components/color/test_config_flow.py Tests configuration flows.
tests/components/color/test_init.py Tests setup and unloading.
tests/components/color/test_reproduce_state.py Tests scene restoration.
tests/components/color/test_services.py Tests service behavior.
tests/components/color/test_tricky_paths.py Tests restoration and edge cases.
Review details
  • Files reviewed: 20/22 changed files
  • Comments generated: 6
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread script/hassfest/quality_scale.py Outdated
Comment thread script/hassfest/quality_scale.py Outdated
Comment thread homeassistant/components/color/color_math.py
Comment thread homeassistant/components/color/entity.py Outdated
Comment thread homeassistant/components/color/reproduce_state.py Outdated
Comment thread homeassistant/components/color/entity.py Outdated
- Set quality_scale internal in the manifest (hassfest new-integration
  gate; matches threshold/derivative/schedule)
- Use a typed HassKey instead of hass.data[DOMAIN] (pylint W7405)
- Reject zero-intensity color inputs, which have no chromaticity
- Validate restored xy payloads (length, finite) and kind
- Prefer the canonical xy attribute over the derived hex state when
  reproducing scene snapshots
- Store the options-flow icon explicitly so clearing it does not
  resurrect the creation-time icon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 30, 2026 00:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Entities lack config-entry registry association, and the chromatic default fails its selector schema.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (6)

homeassistant/components/color/color_math.py:207

  • Document that this preserves the exact sRGB value, not the exact input bytes. The function deliberately canonicalizes the result to an uppercase #RRGGBB, so the current wording promises provenance that the returned value cannot retain.
    For inputs that map cleanly to a single sRGB triple (hex/rgb/hs/
    color_name) we echo back the exact bytes the user supplied without losing
    them to the xy gamut round-trip. For xy/kelvin inputs there is no
    canonical "source hex" so we return None.

homeassistant/components/color/color_math.py:43

  • Strip at most one leading # so malformed hex values are rejected. lstrip("#") accepts inputs such as ##FF0000, even though the action contract is an optional single prefix followed by six hexadecimal digits.

This issue also appears on line 204 of the same file.

    stripped = hex_value.strip().lstrip("#")

homeassistant/components/color/config_flow.py:63

  • Use an RGB list as the ColorRGBSelector default. The selector validates a three-byte list, so submitting this step without changing the field applies "#FFFFFF" as the default and fails schema validation instead of creating the helper.
        vol.Required(
            CONF_INITIAL_COLOR, default=DEFAULT_HEX
        ): selector.ColorRGBSelector(),

script/hassfest/quality_scale.py:230

  • Classify this new internal helper under NO_QUALITY_SCALE instead of the grandfathered missing-file list. validate_iqs_file explicitly directs new internal integrations there at script/hassfest/quality_scale.py:2133-2135, and analogous internal helpers such as input_number are listed in NO_QUALITY_SCALE.
    "color",

script/hassfest/quality_scale.py:1165

  • Remove this INTEGRATIONS_WITHOUT_SCALE exemption when moving the internal helper to NO_QUALITY_SCALE. This list is only consulted when a quality-scale file exists (script/hassfest/quality_scale.py:2174-2199), while this integration intentionally has no such file.
    "color",

homeassistant/components/color/const.py:27

  • Describe source_hex as a normalized sRGB equivalent rather than an exact-byte echo. The implementation strips the prefix/whitespace and uppercases hex values, while RGB, HS, and color-name inputs do not contain source bytes to echo.
# Exact-byte echo of the user's input when it had a clear hex equivalent
# (hex/rgb/hs/color_name). Null for xy/kelvin inputs.
  • Files reviewed: 20/22 changed files
  • Comments generated: 1
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread homeassistant/components/color/__init__.py Outdated
Copilot AI review requested due to automatic review settings July 30, 2026 18:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Entity registry association, selector defaults, black input handling, and restored-data validation need correction.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (4)

homeassistant/components/color/init.py:130

  • Add the entity through a config-entry-backed EntityPlatform. Calling the root component's async_add_entities uses the domain platform, whose config_entry is None; entity_platform.py:1014-1021 therefore registers this entity without config_entry_id, so config-entry cleanup cannot remove its registry entry and the helper leaves an orphan when deleted.
    await component.async_add_entities([entity])

homeassistant/components/color/entity.py:100

  • Reject restored payloads that violate the canonical value ranges. This check accepts out-of-range xy, brightness, and Kelvin values (and ignores the stored version), so corrupted restore data such as brightness -1 is exposed in color_params even though the public action schemas reject it, breaking the promised directly-splattable payload instead of falling back to the configured initial value.
        if not (isfinite(x) and isfinite(y)) or kind not in (
            KIND_CHROMATIC,
            KIND_WHITE,
        ):
            return None

homeassistant/components/color/config_flow.py:131

  • Reject pure black in the chromatic flow instead of silently creating a white helper. ColorRGBSelector accepts [0, 0, 0], _coerce_color_input turns it into #000000, and _initial_canonical later catches normalize's pure-black error and falls back to DEFAULT_HEX, leaving the user with a different color (and an inconsistent source_hex).
                CONF_INITIAL_COLOR: _coerce_color_input(
                    user_input.get(CONF_INITIAL_COLOR)
                ),

homeassistant/components/color/config_flow.py:63

  • Use an RGB list as the ColorRGBSelector default. The selector validates values as three-element integer lists (helpers/selector.py:657-660), so accepting this "#FFFFFF" default without changing the field fails schema validation instead of creating the helper.

This issue also appears on line 129 of the same file.

        vol.Required(
            CONF_INITIAL_COLOR, default=DEFAULT_HEX
        ): selector.ColorRGBSelector(),
  • Files reviewed: 21/23 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 30, 2026 19:26
Routes entity setup through EntityComponent.async_setup_entry so the
entity registry entry is linked to its config entry and cleaned up
automatically on entry removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Entity-registry association, selector defaults, and malformed-state handling contain blocking correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (2)

homeassistant/components/color/reproduce_state.py:69

  • Catch validation failures from malformed snapshot color attributes and continue reproducing brightness. An old or manually defined snapshot can contain an invalid Kelvin or xy value, causing this blocking service call to raise and abort asyncio.gather, so this state and potentially other scene states are not reproduced despite the documented malformed-snapshot resilience.
        await hass.services.async_call(

homeassistant/components/color/entity.py:100

  • Reject restored payloads that violate the canonical invariants before applying them. This check accepts out-of-range xy/brightness/Kelvin values, a white value with no Kelvin, and any schema version; such cache data then exposes invalid attributes (for example brightness 999) and an invalid color_params payload instead of falling back to the configured initial value.
        if not (isfinite(x) and isfinite(y)) or kind not in (
            KIND_CHROMATIC,
            KIND_WHITE,
        ):
            return None
  • Files reviewed: 22/24 changed files
  • Comments generated: 2
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread homeassistant/components/color/__init__.py Outdated
Comment thread homeassistant/components/color/config_flow.py
Copilot AI review requested due to automatic review settings July 30, 2026 19:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Restore validation, malformed scene handling, and the RGB selector default contain correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (16)

homeassistant/components/color/entity.py:100

  • Validate restored fields against the canonical invariants before applying them. A payload with kind="white" and no Kelvin, out-of-range xy, or brightness outside 0–255 is currently accepted and can expose inconsistent or invalid color_params after restart.
        if not (isfinite(x) and isfinite(y)) or kind not in (
            KIND_CHROMATIC,
            KIND_WHITE,
        ):
            return None

homeassistant/components/color/entity.py:95

  • Catch OverflowError while decoding restored numeric fields. A malformed stored Kelvin or brightness such as Infinity reaches int(...) and currently escapes this fallback path, causing entity setup to fail instead of using the initial value.
        except KeyError, TypeError, ValueError:
            return None

homeassistant/components/color/config_flow.py:63

  • Use an RGB sequence as the ColorRGBSelector default. The selector validates only three-item lists, so omitting the field applies "#FFFFFF" and then fails schema validation instead of creating the helper with its advertised default.
        vol.Required(
            CONF_INITIAL_COLOR, default=DEFAULT_HEX
        ): selector.ColorRGBSelector(),

homeassistant/components/color/entity.py:80

  • Reject restore payloads whose schema version does not match. The payload writes STATE_SCHEMA_VERSION but never reads it, so missing or future incompatible schemas are silently interpreted as version 1 instead of falling back to the configured initial value.
        try:

homeassistant/components/color/color_math.py:43

  • Strip at most one optional # prefix. Using lstrip("#") accepts malformed values such as ##FF0000 as valid hex input instead of raising ColorInputError.
    stripped = hex_value.strip().lstrip("#")

homeassistant/components/color/reproduce_state.py:58

  • Validate or isolate malformed color attributes before calling the service. A two-item but invalid xy_color (or an invalid white Kelvin) reaches the service schema, whose exception propagates through gather, prevents the later brightness restoration, and can abort reproduction of the whole scene despite this function's stated malformed-snapshot resilience.
    if attrs.get(ATTR_KIND) == KIND_WHITE and attrs.get(ATTR_COLOR_TEMP_KELVIN):
        color_data = {FIELD_KELVIN: attrs[ATTR_COLOR_TEMP_KELVIN]}
    elif isinstance(xy, (list, tuple)) and len(xy) == 2:
        # Canonical xy beats the derived hex state (hex -> xy is lossy).
        color_data = {FIELD_XY: list(xy)}

homeassistant/components/color/reproduce_state.py:79

  • Skip malformed snapshot brightness values instead of letting them abort scene reproduction. Values outside the service schema (for example 999 or a non-numeric string) currently raise from async_call and propagate through gather after the color has already been changed.
    if ATTR_BRIGHTNESS in attrs:
        snapshot_brightness = attrs[ATTR_BRIGHTNESS]
        if snapshot_brightness is None:

homeassistant/components/color/color_math.py:98

  • Convert overflowed Kelvin inputs into ColorInputError as well. int(math.inf) raises OverflowError, so a corrupted config entry can escape _initial_canonical's fallback and fail entity setup.
    try:
        value = int(kelvin)
    except (TypeError, ValueError) as err:
        raise ColorInputError(
            f"color_temp_kelvin must be an int, got {kelvin!r}"
        ) from err

homeassistant/components/color/entity.py:151

  • Treat overflowed initial brightness as invalid instead of failing setup. A corrupted entry containing an infinite numeric value reaches int(brightness), which raises OverflowError and bypasses the intended None fallback.
        try:
            value = int(brightness)
        except TypeError, ValueError:
            return None

homeassistant/components/color/const.py:27

  • Describe source_hex as normalized rather than byte-exact. compute_source_hex uppercases the digits, adds #, and converts non-hex shapes, so this comment currently promises preservation the implementation does not provide.
# Exact-byte echo of the user's input when it had a clear hex equivalent
# (hex/rgb/hs/color_name). Null for xy/kelvin inputs.

homeassistant/components/color/color_math.py:208

  • Describe the result as a normalized hex equivalent rather than the user's exact bytes. The return expression always rebuilds and uppercases the value, so case, prefix, and original representation are not preserved.
    """Return the literal hex equivalent of the user's input, if one exists.

    For inputs that map cleanly to a single sRGB triple (hex/rgb/hs/
    color_name) we echo back the exact bytes the user supplied without losing
    them to the xy gamut round-trip. For xy/kelvin inputs there is no
    canonical "source hex" so we return None.

tests/components/color/test_tricky_paths.py:313

  • Describe this as preserving the color in normalized hex form, not preserving the user's exact bytes. Normalization can change case and add the # prefix.
    """The source_hex echoes the user's bytes exactly, normalized to uppercase."""

tests/components/color/test_config_flow.py:7

  • Exercise color coercion through the config flow instead of importing its private helper. Cases such as tuples, dictionaries, and None cannot pass ColorRGBSelector validation, so these tests couple to unreachable internal fallback behavior rather than the user-facing contract.
from homeassistant.components.color.config_flow import _coerce_color_input

tests/components/color/test_color_math.py:61

  • Replace the per-channel branch with parameterized expected bounds. Home Assistant's test convention avoids conditions in test bodies because each parameter should encode its complete expected outcome and fail independently.
    for i, got in enumerate(derive_rgb(canonical)):
        if i == dominant:
            assert got > 200, f"dominant channel too low: {got}"
        else:
            assert got < 30, f"off-channel too high: {got}"

tests/components/color/test_tricky_paths.py:312

  • Rename the test to reflect normalized hex preservation rather than byte-exact preservation. The implementation canonicalizes case and prefix, so exact describes a stronger contract than the assertion verifies.
async def test_source_hex_exact_for_hex_input(hass: HomeAssistant) -> None:

tests/components/color/test_tricky_paths.py:12

  • Remove the duplicated test inventory from the module docstring. It restates test names, requires manual synchronization, and is already incomplete relative to the behaviors covered later in this file.
These tests target paths where regressions would silently corrupt user data
or break promised semantics:
- Full restart round-trip via mock_restore_cache_with_extra_data
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

The ColorRGBSelector validates a three-element list, so the chromatic
step's default must be an RGB triplet rather than a hex string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 30, 2026 19:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Restore payload validation and quality-scale registration contain unresolved correctness and policy issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (7)

script/hassfest/quality_scale.py:230

  • Move color to NO_QUALITY_SCALE instead of adding it to the legacy exemption lists. validate_iqs_file at script/hassfest/quality_scale.py:2133-2139 explicitly directs new quality_scale: internal integrations there; this entry and the one at line 1165 bypass that new-integration policy.
    "color",

homeassistant/components/color/entity.py:101

  • Validate the complete restored canonical payload before accepting it. For example, kind="white" with kelvin=None, or brightness=999, passes this check and exposes an internally inconsistent white entity or invalid color_params; malformed snapshots should fall back to the configured initial value.
        if not (isfinite(x) and isfinite(y)) or kind not in (
            KIND_CHROMATIC,
            KIND_WHITE,
        ):
            return None
        return cls(canonical, brightness, source_hex)

tests/components/color/test_config_flow.py:7

  • Exercise color coercion through the config flow instead of importing its private helper. Home Assistant integration tests should avoid internal implementation details; these direct cases couple the suite to _coerce_color_input, and several inputs cannot be produced by ColorRGBSelector.
from homeassistant.components.color.config_flow import _coerce_color_input

homeassistant/components/color/entity.py:81

  • Reject restore payloads with a missing or unsupported schema version. as_dict() writes STATE_SCHEMA_VERSION, but from_dict() never reads it, so an older or newer payload with the current key names is silently interpreted as the current schema instead of falling back safely.
        try:
            x, y = (float(value) for value in data["xy"])

homeassistant/components/color/entity.py:7

  • Use the already imported local attribute constants and drop this light-component import. ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, and ATTR_XY_COLOR already define the same payload keys, so importing the full light entity component solely for these names creates unnecessary cross-integration coupling.
from homeassistant.components import light

homeassistant/components/color/const.py:28

  • Describe this as a normalized hex equivalent rather than an exact-byte echo. compute_source_hex strips formatting and emits uppercase, so lowercase or hashless input is not preserved byte-for-byte.
# Exact-byte echo of the user's input when it had a clear hex equivalent
# (hex/rgb/hs/color_name). Null for xy/kelvin inputs.
ATTR_SOURCE_HEX: Final = "source_hex"

homeassistant/components/color/color_math.py:207

  • Document the normalized result instead of claiming the original bytes are echoed. Hex input is parsed and re-emitted with a leading hash and uppercase digits, so this description currently promises semantics the function does not provide.
    For inputs that map cleanly to a single sRGB triple (hex/rgb/hs/
    color_name) we echo back the exact bytes the user supplied without losing
    them to the xy gamut round-trip. For xy/kelvin inputs there is no
    canonical "source hex" so we return None.
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Input validation currently accepts malformed hex syntax and unsafe chromaticity values.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (2)

homeassistant/components/color/color_math.py:86

  • Reject xy values with a denominator too small for color_xy_to_RGB() to handle safely. Tiny positive y values pass this predicate, but the conversion divides by y, can produce infinities/NaNs, and then raises while converting to integers; the service has already assigned that unusable canonical value to the entity.
    return isfinite(x) and isfinite(y) and 0.0 <= x <= 1.0 and y > 0.0 and x + y <= 1.0

homeassistant/components/color/color_math.py:44

  • Strip at most one leading # when validating a hex value. lstrip("#") accepts malformed values such as ##FF0000 or any number of prefixes, so the set_color action silently treats invalid hex syntax as valid.
    stripped = hex_value.strip().lstrip("#")
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 31, 2026 01:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The entity component lacks shutdown registration, and the quality-scale exemptions use the wrong lists.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (3)

script/hassfest/quality_scale.py:1165

  • Remove color from INTEGRATIONS_WITHOUT_SCALE and list this no-file internal integration in NO_QUALITY_SCALE instead. The validator's no-file path at lines 2124-2141 names NO_QUALITY_SCALE as the required destination, while this list only tracks integrations that have a quality-scale file but no declared tier.
    "color",

script/hassfest/quality_scale.py:230

  • Move color to NO_QUALITY_SCALE instead of grandfathering it in the legacy exemption lists. The validator at script/hassfest/quality_scale.py:2124-2141 explicitly requires new integrations marked internal to be listed in NO_QUALITY_SCALE; adding it here and to INTEGRATIONS_WITHOUT_SCALE bypasses that new-integration check and gives these legacy lists another permanent exception.

This issue also appears on line 1165 of the same file.

    "color",

homeassistant/components/color/init.py:94

  • Register the entity component's shutdown callback before returning from setup. This config-entry-only component never calls EntityComponent.async_setup(), so without register_shutdown() its entity platforms are not shut down on Home Assistant stop; the established config-entry-only pattern is homeassistant/components/wake_word/__init__.py:60-63, and EntityComponent.register_shutdown() explicitly exists for this case.
    component = EntityComponent[ColorEntity](_LOGGER, DOMAIN, hass)
    hass.data[DATA_COMPONENT] = component
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

From an internal adversarial review pass:
- Non-finite numbers in service schemas and kelvin validation raise
  vol.Invalid/ColorInputError instead of leaking OverflowError
- Non-numeric rgb/hs/xy components raise ColorInputError instead of
  bare ValueError, keeping normalize()'s single-error-path contract
- Scene restore validates snapshot xy before calling set_color (corrupt
  xy no longer aborts scene activation), restores via source_hex when
  it matches the snapshot chromaticity so the attribute survives
  round-trips, and logs white snapshots missing kelvin
- Restore rejects payloads with a kelvin on chromatic colors
- Follow helper conventions: has_entity_name, strings.json title, and
  recorder exclusion for attributes derived from the state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 31, 2026 02:06
kkilchrist and others added 2 commits July 30, 2026 19:08
NO_QUALITY_SCALE is where the hassfest no-file path directs internal
integrations; the legacy exemption lists were the wrong destination.
The config-entry-only EntityComponent also needs an explicit
register_shutdown() since async_setup() never runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

The new integration bypasses quality-scale checks and accepts inconsistent persisted source-color data.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (2)

script/hassfest/quality_scale.py:230

  • Use the supported quality-scale path instead of grandfathering this new integration. These two lists are legacy exemptions; the validator directs new internal integrations to NO_QUALITY_SCALE, or otherwise requires a quality_scale.yaml and at least Bronze (script/hassfest/quality_scale.py:2122-2149, 2185-2199), so adding color here bypasses both new-integration checks.
    "color_extractor",

homeassistant/components/color/entity.py:92

  • Reject invalid or inconsistent source_hex values during restore. Unlike every other persisted field, this value is accepted by stringifying arbitrary data, so a corrupted payload can restore a white 2700 K color with source_hex="#FF0000" (or expose a non-hex string) and then persist those contradictory attributes indefinitely; validate its type/format and require it to be absent for white colors.
            source_hex = data.get("source_hex")
            if source_hex is not None:
                source_hex = str(source_hex)
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 02:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Persisted infinite numeric values can currently crash helper setup instead of triggering the intended fallback behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Comments suppressed due to low confidence (2)

homeassistant/components/color/entity.py:171

  • Handle overflowing initial brightness like the other malformed entry values. A corrupted config entry containing an infinite brightness makes int(brightness) raise OverflowError, preventing the helper from being set up rather than treating brightness as unset.
        try:
            value = int(brightness)
        except TypeError, ValueError:
            return None

homeassistant/components/color/entity.py:93

  • Treat overflowing numeric values as malformed restore data. int(math.inf) raises OverflowError, so a corrupted saved version, kelvin, or brightness currently aborts entity setup instead of returning None and falling back to the configured initial value.

This issue also appears on line 168 of the same file.

        except KeyError, TypeError, ValueError:
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 02:46
Systematic sweep of every int()/float() on externally-sourced data:
- _validate_rgb catches OverflowError so infinite components raise
  ColorInputError like every other malformed input
- Scene restore validates snapshot kelvin and brightness before the
  service call; corrupt values fall back or are skipped with a debug
  log instead of aborting scene activation, and overflowing ints in
  xy comparisons are treated as invalid
- The config flow color coercion falls back to the default color on
  non-numeric or overflowing components

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Malformed scene attributes can currently abort reproduction of every state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (6)

homeassistant/components/color/reproduce_state.py:119

  • Catch validation errors from the color service and skip the malformed color. A syntactically valid snapshot such as #000000, or a truthy invalid white Kelvin, reaches this call and raises ServiceValidationError/vol.Invalid; that escapes asyncio.gather and aborts scene reproduction for every state.
        color_data = {FIELD_KELVIN: int(attrs[ATTR_COLOR_TEMP_KELVIN])}
    elif _source_hex_matches(source_hex, xy):
        # The canonical value was derived from this exact hex, so restoring
        # via hex loses nothing and preserves the source_hex attribute.
        color_data = {FIELD_HEX: source_hex}

homeassistant/components/color/reproduce_state.py:138

  • Validate or catch invalid snapshot brightness before dispatching it. An out-of-range or non-numeric attribute raises vol.Invalid here, which escapes asyncio.gather and aborts the entire scene reproduction instead of leaving brightness untouched.
        )

    if color_data is not None:
        await hass.services.async_call(
            DOMAIN,

tests/components/color/test_tricky_paths.py:12

  • Shorten this module docstring to state only the module's purpose. The bullet list repeats the test functions below and will become stale as cases are added or renamed.
"""Tests for non-trivial behaviors: restore, snapshots, options reload.

These tests target paths where regressions would silently corrupt user data
or break promised semantics:
- Full restart round-trip via mock_restore_cache_with_extra_data

homeassistant/components/color/const.py:27

  • Describe source_hex as a normalized sRGB equivalent rather than an exact-byte echo. The implementation uppercases hex input and adds #, while RGB/HS/name inputs do not supply literal hex bytes at all.
# Exact-byte echo of the user's input when it had a clear hex equivalent
# (hex/rgb/hs/color_name). Null for xy/kelvin inputs.

homeassistant/components/color/color_math.py:231

  • Document the normalized pre-xy sRGB value instead of claiming the user's exact bytes are echoed. compute_source_hex canonicalizes hex syntax and computes bytes for RGB, HS, and named-color inputs, so the current contract is inaccurate.
    """Return the literal hex equivalent of the user's input, if one exists.

    For inputs that map cleanly to a single sRGB triple (hex/rgb/hs/
    color_name) we echo back the exact bytes the user supplied without losing
    them to the xy gamut round-trip. For xy/kelvin inputs there is no

tests/components/color/test_color_math.py:63

  • Parameterize the expected channel bounds instead of branching in the test body. Repository test guidance requires splitting or parametrizing test cases without conditions, and declarative expected bounds make each case's assertion explicit.
    for i, got in enumerate(derive_rgb(canonical)):
        if i == dominant:
            assert got > 200, f"dominant channel too low: {got}"
        else:
            assert got < 30, f"off-channel too high: {got}"
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 02:53
A snapshot value that passes the pre-dispatch guards but fails service
validation (e.g. a #000000 state string) no longer aborts reproduction
of every state in the scene; it is logged and skipped. Also corrects
the source_hex documentation (the value is the normalized pre-xy sRGB
hex, not an exact-byte echo) and reworks two tests per repo guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Non-finite HS values can trigger an unhandled internal service failure.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

homeassistant/components/color/color_math.py:86

  • Reject non-finite HS components before conversion. NaN passes these range checks, then color_hs_to_RGB() raises a ValueError outside ColorInputError, so color.set_color fails internally instead of returning the translated validation error.
    if not 0 <= hue <= 360:
        raise ColorInputError("hs_color hue must be 0-360")
    if not 0 <= sat <= 100:
        raise ColorInputError("hs_color saturation must be 0-100")
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings July 31, 2026 02:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

HS and xy inputs can leak OverflowError instead of producing validation errors.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (5)

homeassistant/components/color/color_math.py:105

  • Catch OverflowError when converting xy components. Extremely large integer input makes float() raise OverflowError, so normalize() leaks an unexpected exception rather than rejecting the color with ColorInputError.
    except (TypeError, ValueError) as err:

homeassistant/components/color/init.py:79

  • Use an overflow-safe float coercer for xy service data. An extremely large integer raises OverflowError inside vol.Coerce(float), bypassing both schema validation and the integration's translated ServiceValidationError path.
                [vol.All(vol.Coerce(float), vol.Range(min=0.0, max=1.0))],

homeassistant/components/color/color_math.py:79

  • Catch OverflowError when converting HS components. A caller can supply an integer such as 10**400, for which float() raises OverflowError instead of the documented ColorInputError, bypassing the translated service-validation path.

This issue also appears on line 105 of the same file.

    except (TypeError, ValueError) as err:

tests/components/color/test_config_flow.py:8

  • Exercise color coercion through the config flow instead of importing the private _coerce_color_input helper. Directly testing this implementation detail conflicts with the integration-testing guideline and couples the suite to an internal helper that the selector already guards.
from homeassistant.components.color.config_flow import _coerce_color_input

homeassistant/components/color/init.py:74

  • Use an overflow-safe float coercer for HS service data. vol.Coerce(float) does not translate OverflowError, so an input such as [10**400, 50] escapes schema validation and fails the service call unexpectedly before normalize() can return the translated color error.

This issue also appears on line 79 of the same file.

                [vol.Coerce(float)],
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

float() raises OverflowError for arbitrarily large ints (unlike inf,
which it accepts), so hs/xy inputs like 10**400 leaked an unexpected
exception past both the service schema and normalize()'s error path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DSnLnMp43HRSiDXyq8RGY3
Copilot AI review requested due to automatic review settings July 31, 2026 03:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Not ready to approve

Extreme valid xy input can break state writes, while invalid initial black data produces inconsistent source metadata.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

homeassistant/components/color/color_math.py:96

  • Reject xy values with a y component too close to zero. The current y > 0.0 check accepts subnormal values such as 5e-324; color_xy_to_RGB() then overflows while dividing by y, and deriving the entity state raises instead of completing a valid set_color call (also leaving the in-memory canonical value poisoned).
    return isfinite(x) and isfinite(y) and 0.0 <= x <= 1.0 and y > 0.0 and x + y <= 1.0

homeassistant/components/color/entity.py:182

  • Clear source_hex when the configured initial color is rejected. For a corrupted/custom entry containing #000000, _initial_canonical() falls back to DEFAULT_HEX, but this method still returns #000000, leaving the entity permanently advertising a source value that never produced its canonical color.
        initial = entry.data.get(CONF_INITIAL_COLOR)
        if not initial:
            return None
        return compute_source_hex({FIELD_HEX: initial})
  • Files reviewed: 22/24 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants