A reusable color value for Home Assistant. Each color is a color you
can store, edit, reference from automations, capture in scenes, and apply to
one or more lights. Think of it like input_number or input_boolean, but
the value is a color.
Installs as a custom integration via HACS. Lives entirely
in custom_components/color/ — no frontend bundle, just the standard
Home Assistant attribute display and service-call UI.
There is no built-in way in Home Assistant to store a color that isn't bound to a specific light. People want this for:
- Cross-light scenes — pick one favorite color and apply it to whichever lights are on, without hardcoding the value in each scene
- UI-driven color selection — a Lovelace color picker that doesn't have to point at a fake light
- LED strip choreography — separate the choice of color from the act of setting any particular zone
- Nightlight / accent presets — store both color and brightness as one named thing
See the Home Assistant community thread for the long version.
Each input color stores:
| Field | Type | Notes |
|---|---|---|
xy |
(x, y) |
CIE 1931 chromaticity — the canonical color value |
kind |
"chromatic" | "white" |
How the user selected the color |
kelvin |
int | None |
Only set when kind == "white" |
brightness |
0-255 | None |
Independent of color; optional |
All other representations (hex, RGB, HS, kelvin-for-chromatic) are derived on
the fly from xy. State persists across restarts via RestoreEntity.
RGB is fixture-dependent. The same rgb(255, 0, 0) is a different physical
red on a Hue Play, a LIFX A19, and a cheap WS2812 strip. CIE xy is a
device-independent chromaticity, which is what Hue itself stores for its
favorites and what makes apply_to work consistently across mixed-vendor
setups. The optional kind == "white" flag remembers when the user picked a
color temperature so that tunable-white targets get a real kelvin value
instead of a converted-then-reconverted xy.
- Add this repository as a custom HACS repository (category: Integration)
- Search for "Color" in HACS → Integrations → Explore & Add
- Install, then restart Home Assistant
- Settings → Devices & Services → Add Integration → "Color"
Each color you want is one integration instance (one config entry per color).
Copy custom_components/color/ into <config>/custom_components/,
restart Home Assistant, and add the integration from the UI.
Set the stored color. Provide exactly one of hex_value, rgb_color,
hs_color, xy_color, color_temp_kelvin, or color_name. Brightness is
optional and independent of the color shape.
service: color.set_color
target:
entity_id: color.couch_color
data:
hex_value: "#FF8000"service: color.set_color
target:
entity_id: color.evening_warm
data:
color_temp_kelvin: 2700
brightness: 180Set or clear the stored brightness. Pass null to clear.
service: color.set_brightness
target:
entity_id: color.couch_color
data:
brightness: 200Send the stored color to one or more lights. The dispatcher sends
color_temp_kelvin for whites and xy_color for chromatic colors; Home
Assistant's light integration converts to whatever supported_color_modes
each target advertises.
service: color.apply_to
target:
entity_id: color.couch_color
data:
lights:
- light.living_room_strip
- light.ceiling_island
override_brightness: false # if true, also push the stored brightness
# brightness: 200 # OR set an explicit per-call brightnessBrightness precedence:
- Explicit
brightnessfield wins (0-255) — useful when a script wants this color with a per-call brightness without touching stored state (e.g. workout intervals where each phase has its own brightness). - Else if
override_brightness: trueAND the color has a stored brightness, push the stored value. - Else: omit brightness — each target light keeps its current level.
The dispatcher is intentionally small: it picks one of two color shapes
based on kind, optionally adds brightness per the precedence above, and
sends one batched light.turn_on call. Home Assistant's light component
handles per-fixture conversion from there.
What apply_to sends, given helper state + call options:
| Helper kind | Stored brightness | Call brightness |
Call override_brightness |
light.turn_on payload |
|---|---|---|---|---|
| chromatic | any | absent | any | xy_color: [x, y] |
| chromatic | null |
absent | true |
xy_color (no brightness — nothing stored) |
| chromatic | 150 |
absent | false |
xy_color (override is off) |
| chromatic | 150 |
absent | true |
xy_color, brightness: 150 |
| chromatic | any | 60 |
any | xy_color, brightness: 60 (explicit wins) |
| white(2700K) | null |
absent | any | color_temp_kelvin: 2700 |
| white(2700K) | 200 |
absent | true |
color_temp_kelvin: 2700, brightness: 200 |
| white(2700K) | 200 |
60 |
true |
color_temp_kelvin: 2700, brightness: 60 |
What Home Assistant then does with our payload, per target light:
| We send | Target's supported_color_modes |
HA's behavior |
|---|---|---|
xy_color |
includes xy |
passes through; Hue-style gamut clamp applies |
xy_color |
includes rgb, rgbw, or rgbww (no xy) |
converts xy → sRGB internally |
xy_color |
includes hs only |
converts xy → hs |
xy_color |
color_temp only (tunable-white bulb) |
McCamy-approximates xy → kelvin; meaningful near the Planckian locus, arbitrary for saturated colors |
color_temp_kelvin |
includes color_temp |
passes through; clamped to bulb's min/max_color_temp_kelvin |
color_temp_kelvin |
RGB/HS/XY only (no color_temp) |
converts kelvin → Planckian-locus xy → target's preferred shape |
So a kind=white helper applied to a chromatic RGB strip yields the
Planckian-locus chromaticity for the chosen Kelvin — the right answer.
A kind=chromatic saturated red applied to a tunable-white bulb yields
a very-low McCamy kelvin, which is technically meaningless but is what
the user implicitly asked for. If you want stricter behavior, branch
in your automation on the helper's kind attribute before calling
apply_to.
| Attribute | Description |
|---|---|
state |
Hex color string (e.g. "#FF8000") |
kind |
"chromatic" or "white" |
xy_color |
[x, y] chromaticity |
rgb_color |
[r, g, b] derived sRGB for display |
hs_color |
[hue, saturation] |
color_temp_kelvin |
Stored value when kind == "white"; null for chromatic colors |
brightness |
0-255 or null |
hex_color |
Same as state, repeated for convenience |
source_hex |
Exact echo of the user's input when it had a hex equivalent (hex/rgb/hs/color_name). null for xy/kelvin inputs. Read this when you need the bytes the user picked, independent of the gamut-mapped value used for apply_to. |
- blueprints/ — one-click importable blueprints. Start with "Sync Color to Lights" for the most common pattern.
- examples/ — raw YAML snippets for scripts, automations,
and scenes. Includes a
demo_walkthrough.yamlscript that cycles a single color through every input shape with 2-second delays so you can see each one render.
color composes with scenes; it doesn't compete with them. This is
the most useful pattern in the integration and worth understanding before
you build anything else.
The integration ships a reproduce_state hook, which means scenes that
include an color entity snapshot its full canonical state (kind,
xy, kelvin, brightness) — and restore it on scene.turn_on.
The composition that falls out:
| Layer | Holds | Mutable | Example |
|---|---|---|---|
color helper |
A named color you can edit | Yes | color.favorite_blue |
scene |
A frozen moment, including the helper's value | No (until you re-create it) | scene.movie_night |
A user-facing workflow that becomes natural:
- Edit the favorite from a dashboard card or automation — the
coloris the named slot. Change it whenever. - Capture a moment with
scene.create snapshot_entities: [color.x, light.a, light.b]. The scene now remembers the helper's value at capture time and the lights' state. - Restore later with
scene.turn_on— both the helper and the lights snap back to what they were when you captured.
This is different from a static scene because the helper between captures
is editable: you can build a "Living Room — Evening" scene that includes
color.living_room_color, then later edit that helper to a new
favorite, then re-capture the scene to update the snapshot. The helper is
the named handle; the scene is the frozen application.
It's also the answer to "but scenes already do this" — they do, for
literal device states. color adds a named, reusable color value
that scenes can include alongside device states, without you having to
hardcode hex values in the scene YAML.
See examples/scenes/scene_capture.yaml for a concrete walk-through.
Internally we implement async_reproduce_states so scene.create /
scene.turn_on round-trip the helper's canonical state through restore
data — the lossy hex state is sufficient for chromatic colors, and the
kind/color_temp_kelvin attributes carry the white-temperature path.
Malformed snapshots (state=unavailable, missing brightness attr) are
tolerated — see reproduce_state.py.
The two most useful patterns:
# Use the color in a light.turn_on directly. Robust to missing entity.
service: light.turn_on
data:
entity_id: light.island
rgb_color: "{{ state_attr('color.gym_work_color', 'rgb_color') | default([255, 0, 0]) }}"
# Same but with explicit brightness for this call only — no stored state.
service: color.apply_to
target:
entity_id: color.gym_work_color
data:
lights: [light.island]
brightness: 255 # explicit; ignores stored brightnessFor exact reads (no gamut drift), use source_hex:
{{ state_attr('color.x', 'source_hex') or state('color.x') }}This returns the literal hex the user picked (when set via hex/rgb/hs/name), or falls back to the gamut-mapped state for xy/kelvin inputs.
The normal install flow is the UI (Settings → Devices & services → Add
Integration → "Color"). If you need to create entries from a script
or another integration, the ConfigEntry data dict shape is:
{
"name": "Couch Color", # required, becomes entity title
"initial_mode": "chromatic", # or "white"
"initial_color": "#FF8000", # required if mode=chromatic; hex string
"initial_kelvin": 4000, # required if mode=white; int Kelvin
"initial_brightness": 200, # optional; 0-255
"icon": "mdi:palette", # optional; MDI icon string
}Then start a config flow:
result = await hass.config_entries.flow.async_init(
"color",
context={"source": "user"},
data={...}, # not actually consumed at user step; flow walks steps
)In practice most callers build entries via MockConfigEntry-like patterns
in tests, or simply prompt the user through the UI. See config_flow.py
for the multi-step shape; const.py for field names.
- Gamut clipping is per-fixture and invisible to the helper. The swatch
you see in the UI is sRGB. The light you apply it to has its own gamut
triangle and may clip saturated cyans/greens. The helper passes the color
through to
light.turn_onand lets the light component clamp. color_temp_kelvinis null for chromatic colors. Only set when the user picked a white. Whenapply_tosends a chromatic color to a tunable-white target, HA's light component picks the closest representable white internally — that approximation lives in the light integration, not this helper.- No custom color picker UI yet. v1 uses Home Assistant's built-in
color_rgbselector in the config flow and the standard attribute display for the more-info panel. Custom Lovelace UI is on the roadmap. - Not in the Helpers picker. Home Assistant's frontend hardcodes which domains appear under Settings → Helpers → Add Helper. Until that registry is patched, this integration appears under Settings → Devices & Services instead.
python3 -m venv .venv
.venv/bin/pip install -r requirements_test.txt
.venv/bin/python -m pytest tests/Unit tests cover the colorimetric normalizer; integration tests use
pytest-homeassistant-custom-component and exercise the full config-flow →
entity → service-call → reproduce_state path.
Apache 2.0.