Skip to content

Themes API Reference

The theme system provides professionally-curated color palettes for coordinated lighting across LIFX devices.

Theme Class

The Theme class represents a collection of HSBK colors forming a coordinated palette.

Theme

Theme(
    colors: list[HSBK] | None = None,
    *,
    slug: str | None = None,
    name: str | None = None,
    unicode_name: str | None = None,
    category: str | None = None,
    disposition: Disposition | None = None,
    replaced_by: str | None = None,
    static_mode: StaticMode | None = None,
    dynamic_mode: DynamicMode | None = None,
    tags: Iterable[str] = (),
)

A collection of colors representing a theme or color palette.

Themes can be applied to LIFX devices to coordinate colors across multiple lights. Supports both single-zone and multi-zone devices.

ATTRIBUTE DESCRIPTION
colors

List of HSBK colors in the theme

TYPE: list[HSBK]

slug

Library key for a theme from ThemeLibrary (None for a caller-constructed theme)

name

Display name for a theme from ThemeLibrary (None for a caller-constructed theme)

unicode_name

Correctly spelt display name for a theme from ThemeLibrary -- the accented form where one exists, else the same as name (None for a caller-constructed theme)

category

Category for a theme from ThemeLibrary (None for a caller-constructed theme)

disposition

Recorded fate of a theme from ThemeLibrary (None for a caller-constructed theme)

replaced_by

Successor key of a deprecated or renamed theme from ThemeLibrary; None unless disposition is "deprecated" or "renamed" (and None for a caller-constructed theme)

static_mode

Still-image mode the LIFX app paints this theme with, such as "blended" or "grid_static" (None for a caller-constructed theme, which renders as blended)

TYPE: StaticMode | None

dynamic_mode

Effect the app's Dynamic toggle starts, when the theme names one; see resolved_dynamic_mode for the effect used when it does not

TYPE: DynamicMode | None

tags

The app's search tags for this theme, such as "Calm" (empty for a caller-constructed theme)

TYPE: tuple[str, ...]

Note

shuffled() returns an identity-less copy: slug, name, category, disposition, replaced_by, static_mode, dynamic_mode and tags do not propagate. This is a known deferred limitation of the identity round-trip guarantee. (random() returns a single HSBK, not a Theme, so it carries no identity to begin with.)

Note

== compares identity, so a Theme stays hashable and usable as a dict key or set member. To compare palettes, call palette_equals().

Example
# Create a theme with specific colors
theme = Theme(
    [
        HSBK(hue=0, saturation=1.0, brightness=1.0, kelvin=3500),  # Red
        HSBK(hue=120, saturation=1.0, brightness=1.0, kelvin=3500),  # Green
        HSBK(hue=240, saturation=1.0, brightness=1.0, kelvin=3500),  # Blue
    ]
)

# Access colors
for color in theme:
    print(f"Color: {color.hue}°")

# Get a specific color
first_color = theme[0]

# Add more colors
theme.add_color(HSBK(hue=180, saturation=1.0, brightness=1.0, kelvin=3500))
PARAMETER DESCRIPTION
colors

List of HSBK colors (defaults to white if None or empty)

TYPE: list[HSBK] | None DEFAULT: None

slug

Library key for the theme (attached by ThemeLibrary)

TYPE: str | None DEFAULT: None

name

Display name for the theme (attached by ThemeLibrary)

TYPE: str | None DEFAULT: None

unicode_name

Correctly spelt display name (attached by ThemeLibrary)

TYPE: str | None DEFAULT: None

category

Category for the theme (attached by ThemeLibrary)

TYPE: str | None DEFAULT: None

disposition

Recorded fate of the theme (attached by ThemeLibrary)

TYPE: Disposition | None DEFAULT: None

replaced_by

Successor key of a deprecated or renamed theme (attached by ThemeLibrary); None unless disposition is "deprecated" or "renamed"

TYPE: str | None DEFAULT: None

static_mode

Still-image mode (attached by ThemeLibrary)

TYPE: StaticMode | None DEFAULT: None

dynamic_mode

Dynamic effect mode, when the theme names one (attached by ThemeLibrary)

TYPE: DynamicMode | None DEFAULT: None

tags

Search tags (attached by ThemeLibrary)

TYPE: Iterable[str] DEFAULT: ()

RAISES DESCRIPTION
ValueError

If a mode is not a canonical identifier.

TypeError

If tags is a single string.

Example
# Create from list of colors
theme = Theme([color1, color2, color3])

# Create with default white color
theme = Theme()
METHOD DESCRIPTION
add_color

Add a color to the theme.

random

Get a random color from the theme.

shuffled

Get a new theme with colors in random order.

get_next_bounds_checked

Get the next color after index or the last color if at end.

ensure_color

Ensure the theme has at least one color.

__len__

Get the number of colors in the theme.

__iter__

Iterate over colors in the theme.

__getitem__

Get a color by index.

__contains__

Check if a color is in the theme.

palette_equals

Check whether two themes carry the same palette.

__repr__

Return a string representation of the theme.

Source code in src/lifx/theme/theme.py
def __init__(
    self,
    colors: list[HSBK] | None = None,
    *,
    slug: str | None = None,
    name: str | None = None,
    unicode_name: str | None = None,
    category: str | None = None,
    disposition: Disposition | None = None,
    replaced_by: str | None = None,
    static_mode: StaticMode | None = None,
    dynamic_mode: DynamicMode | None = None,
    tags: Iterable[str] = (),
) -> None:
    """Create a new theme with the given colors.

    Args:
        colors: List of HSBK colors (defaults to white if None or empty)
        slug: Library key for the theme (attached by ``ThemeLibrary``)
        name: Display name for the theme (attached by ``ThemeLibrary``)
        unicode_name: Correctly spelt display name (attached by ``ThemeLibrary``)
        category: Category for the theme (attached by ``ThemeLibrary``)
        disposition: Recorded fate of the theme (attached by
            ``ThemeLibrary``)
        replaced_by: Successor key of a deprecated or renamed theme
            (attached by ``ThemeLibrary``); None unless ``disposition``
            is ``"deprecated"`` or ``"renamed"``
        static_mode: Still-image mode (attached by ``ThemeLibrary``)
        dynamic_mode: Dynamic effect mode, when the theme names one
            (attached by ``ThemeLibrary``)
        tags: Search tags (attached by ``ThemeLibrary``)

    Raises:
        ValueError: If a mode is not a canonical identifier.
        TypeError: If ``tags`` is a single string.

    Example:
        ```python
        # Create from list of colors
        theme = Theme([color1, color2, color3])

        # Create with default white color
        theme = Theme()
        ```
    """
    for field, mode in (
        ("static_mode", static_mode),
        ("dynamic_mode", dynamic_mode),
    ):
        if mode is not None and not validate_key(mode):
            raise ValueError(
                f"{field} {mode!r} is not a canonical identifier "
                f"(non-empty, ASCII, lowercase, valid identifier)"
            )
    if isinstance(tags, str):
        # A str is itself an iterable of str; storing it letter by letter
        # would be silent nonsense.
        raise TypeError("tags must be an iterable of tags, not a single string")
    tags = tuple(tags)
    if not all(type(tag) is str for tag in tags):
        raise TypeError("tags must contain only strings")
    if colors and len(colors) > 0:
        # Copied, never aliased: a Theme built over a caller's list would
        # otherwise mutate that list through add_color(), and a Theme
        # built over a cached or shared list would let add_color() corrupt
        # the source. Every construction path gets the isolation, not just
        # ThemeLibrary.get().
        self.colors: list[HSBK] = list(colors)
    else:
        # Default to white if no colors provided
        self.colors = [Colors.WHITE_NEUTRAL]
    self.slug = slug
    self.name = name
    self.unicode_name = unicode_name
    self.category = category
    self.disposition = disposition
    self.replaced_by = replaced_by
    self.static_mode: StaticMode | None = static_mode
    self.dynamic_mode: DynamicMode | None = dynamic_mode
    self.tags: tuple[str, ...] = tags

Attributes

resolved_dynamic_mode property
resolved_dynamic_mode: DynamicMode

The effect the LIFX app's Dynamic toggle starts for this theme.

dynamic_mode when the theme names one. Otherwise the app's rule: MORPH for a blended theme (or one with no static mode) and MOVE for every other static mode. Substituting MORPH on a light that cannot run MOVE depends on the device and is left to the renderer.

RETURNS DESCRIPTION
DynamicMode

The dynamic mode to start.

Methods:

add_color
add_color(color: HSBK) -> None

Add a color to the theme.

PARAMETER DESCRIPTION
color

HSBK color to add

TYPE: HSBK

Example
theme = Theme()
theme.add_color(HSBK(hue=0, saturation=1.0, brightness=1.0, kelvin=3500))
Source code in src/lifx/theme/theme.py
def add_color(self, color: HSBK) -> None:
    """Add a color to the theme.

    Args:
        color: HSBK color to add

    Example:
        ```python
        theme = Theme()
        theme.add_color(HSBK(hue=0, saturation=1.0, brightness=1.0, kelvin=3500))
        ```
    """
    self.colors.append(color)
random
random() -> HSBK

Get a random color from the theme.

RETURNS DESCRIPTION
HSBK

A random HSBK color from the theme

Example
theme = Theme([red, green, blue])
color = theme.random()
Source code in src/lifx/theme/theme.py
def random(self) -> HSBK:
    """Get a random color from the theme.

    Returns:
        A random HSBK color from the theme

    Example:
        ```python
        theme = Theme([red, green, blue])
        color = theme.random()
        ```
    """
    return random.choice(self.colors)
shuffled
shuffled() -> Theme

Get a new theme with colors in random order.

RETURNS DESCRIPTION
Theme

New Theme instance with shuffled colors

Example
theme = Theme([color1, color2, color3])
shuffled_theme = theme.shuffled()
Source code in src/lifx/theme/theme.py
def shuffled(self) -> Theme:
    """Get a new theme with colors in random order.

    Returns:
        New Theme instance with shuffled colors

    Example:
        ```python
        theme = Theme([color1, color2, color3])
        shuffled_theme = theme.shuffled()
        ```
    """
    shuffled_colors = self.colors.copy()
    random.shuffle(shuffled_colors)
    return Theme(shuffled_colors)
get_next_bounds_checked
get_next_bounds_checked(index: int) -> HSBK

Get the next color after index or the last color if at end.

PARAMETER DESCRIPTION
index

Index of current color

TYPE: int

RETURNS DESCRIPTION
HSBK

Next HSBK color or the last color if index is at the end

Example
theme = Theme([red, green, blue])
color = theme.get_next_bounds_checked(0)  # green
color = theme.get_next_bounds_checked(2)  # blue (last color)
Source code in src/lifx/theme/theme.py
def get_next_bounds_checked(self, index: int) -> HSBK:
    """Get the next color after index or the last color if at end.

    Args:
        index: Index of current color

    Returns:
        Next HSBK color or the last color if index is at the end

    Example:
        ```python
        theme = Theme([red, green, blue])
        color = theme.get_next_bounds_checked(0)  # green
        color = theme.get_next_bounds_checked(2)  # blue (last color)
        ```
    """
    if index + 1 < len(self.colors):
        return self.colors[index + 1]
    return self.colors[-1]
ensure_color
ensure_color() -> None

Ensure the theme has at least one color.

If the theme is empty, adds a default white color.

Source code in src/lifx/theme/theme.py
def ensure_color(self) -> None:
    """Ensure the theme has at least one color.

    If the theme is empty, adds a default white color.
    """
    if not self.colors:
        self.colors.append(
            HSBK(hue=0, saturation=0, brightness=1.0, kelvin=3500)
        )  # pragma: no cover
__len__
__len__() -> int

Get the number of colors in the theme.

Source code in src/lifx/theme/theme.py
def __len__(self) -> int:
    """Get the number of colors in the theme."""
    return len(self.colors)
__iter__
__iter__() -> Iterator[HSBK]

Iterate over colors in the theme.

Source code in src/lifx/theme/theme.py
def __iter__(self) -> Iterator[HSBK]:
    """Iterate over colors in the theme."""
    return iter(self.colors)
__getitem__
__getitem__(index: int) -> HSBK

Get a color by index.

PARAMETER DESCRIPTION
index

Index of the color (0-based)

TYPE: int

RETURNS DESCRIPTION
HSBK

HSBK color at the given index

RAISES DESCRIPTION
IndexError

If index is out of range

Example
theme = Theme([red, green, blue])
color = theme[1]  # green
Source code in src/lifx/theme/theme.py
def __getitem__(self, index: int) -> HSBK:
    """Get a color by index.

    Args:
        index: Index of the color (0-based)

    Returns:
        HSBK color at the given index

    Raises:
        IndexError: If index is out of range

    Example:
        ```python
        theme = Theme([red, green, blue])
        color = theme[1]  # green
        ```
    """
    return self.colors[index]
__contains__
__contains__(color: HSBK) -> bool

Check if a color is in the theme.

PARAMETER DESCRIPTION
color

HSBK color to check

TYPE: HSBK

RETURNS DESCRIPTION
bool

True if color is in theme (by value comparison)

Example
theme = Theme([red, green, blue])
if red in theme:
    print("Red is in the theme")
Source code in src/lifx/theme/theme.py
def __contains__(self, color: HSBK) -> bool:
    """Check if a color is in the theme.

    Args:
        color: HSBK color to check

    Returns:
        True if color is in theme (by value comparison)

    Example:
        ```python
        theme = Theme([red, green, blue])
        if red in theme:
            print("Red is in the theme")
        ```
    """
    return any(c == color for c in self.colors)
palette_equals
palette_equals(other: Theme) -> bool

Check whether two themes carry the same palette.

This compares palettes, not layouts, so order is never compared: two orderings of one palette are the same palette. For an ordered comparison, use a.colors == b.colors. Identity (slug, name, category, disposition and replaced_by) is excluded too: an identity-bearing library theme and a caller-built theme with the same colors have the same palette. Colors compare at uint16 (protocol) granularity via HSBK equality, and duplicate counts matter — a multiset comparison, not a set comparison.

This is deliberately a named method rather than __eq__. A Theme's palette is mutable via add_color(), so value equality could not be paired with a stable __hash__; making == compare palettes would leave Theme unhashable and silently change what theme in [a, b], list.index() and list.remove() mean. == therefore stays identity comparison and the palette comparison is spelled out at the call site.

PARAMETER DESCRIPTION
other

Theme to compare palettes with.

TYPE: Theme

RETURNS DESCRIPTION
bool

True if both palettes are the same multiset of colors.

Example
# independence and old_glory ship one shared app palette
assert ThemeLibrary.get("independence").palette_equals(
    ThemeLibrary.get("old_glory")
)
Source code in src/lifx/theme/theme.py
def palette_equals(self, other: Theme) -> bool:
    """Check whether two themes carry the same palette.

    This compares palettes, not layouts, so order is never compared: two
    orderings of one palette are the same palette. For an ordered
    comparison, use ``a.colors == b.colors``. Identity (slug, name,
    category, disposition and replaced_by) is excluded too: an
    identity-bearing library theme and a caller-built theme with the
    same colors have the same palette.
    Colors compare at uint16 (protocol) granularity via HSBK equality,
    and duplicate counts matter — a multiset comparison, not a set
    comparison.

    This is deliberately a named method rather than ``__eq__``. A
    Theme's palette is mutable via ``add_color()``, so value equality
    could not be paired with a stable ``__hash__``; making ``==``
    compare palettes would leave Theme unhashable and silently change
    what ``theme in [a, b]``, ``list.index()`` and ``list.remove()``
    mean. ``==`` therefore stays identity comparison and the palette
    comparison is spelled out at the call site.

    Args:
        other: Theme to compare palettes with.

    Returns:
        True if both palettes are the same multiset of colors.

    Example:
        ```python
        # independence and old_glory ship one shared app palette
        assert ThemeLibrary.get("independence").palette_equals(
            ThemeLibrary.get("old_glory")
        )
        ```
    """
    if not isinstance(other, Theme):
        raise TypeError(
            f"palette_equals() expects a Theme, got {type(other).__name__}"
        )
    return Counter(self.colors) == Counter(other.colors)
__repr__
__repr__() -> str

Return a string representation of the theme.

Source code in src/lifx/theme/theme.py
def __repr__(self) -> str:
    """Return a string representation of the theme."""
    color_count = len(self.colors)
    return f"Theme({color_count} colors)"

ThemeLibrary Class

The ThemeLibrary provides access to 378 themes, resolvable under 381 names.

ThemeLibrary

Collection of built-in colour themes for LIFX devices.

Provides access to every theme in the LIFX app (sport themes excluded), the themes the app has since dropped and the pre-6.3.0 library keys, organised by the app's own categories.

Example
# Get a specific theme
evening_theme = ThemeLibrary.get("evening")

# List all available themes
all_themes = ThemeLibrary.get_available_themes()

# Get themes by category
categories = ThemeLibrary.get_categories()
holidays = ThemeLibrary.get_by_category("Holidays")

# Find themes by tag, category and effect mode. The available tags
# come from the app and change with each resync, so check first.
tags = ThemeLibrary.get_tags()
if "Calm" in tags:
    calm = ThemeLibrary.get_by_tag("Calm")
blended = ThemeLibrary.find(static_mode="blended")

# Apply to a light
await light.apply_theme(evening_theme, power_on=True)
METHOD DESCRIPTION
get

Get a theme by name.

get_available_themes

Get all available themes by name.

get_categories

Get every category present in the library's data.

get_by_category

Get all themes in a category.

get_tags

Get every tag present in the library's data.

get_by_tag

Get all themes carrying a tag.

find

Find themes by tags, category and static mode together.

Methods:

get classmethod
get(name: str) -> Theme

Get a theme by name.

PARAMETER DESCRIPTION
name

Theme name (case-insensitive)

TYPE: str

RETURNS DESCRIPTION
Theme

Theme object

RAISES DESCRIPTION
KeyError

If theme name is not found

Example
from lifx.theme import ThemeLibrary

evening_theme = ThemeLibrary.get("evening")
await light.apply_theme(evening_theme, power_on=True)
Source code in src/lifx/theme/library.py
@classmethod
def get(cls, name: str) -> Theme:
    """Get a theme by name.

    Args:
        name: Theme name (case-insensitive)

    Returns:
        Theme object

    Raises:
        KeyError: If theme name is not found

    Example:
        ```python
        from lifx.theme import ThemeLibrary

        evening_theme = ThemeLibrary.get("evening")
        await light.apply_theme(evening_theme, power_on=True)
        ```
    """
    record = cls._THEMES.get(name.lower())
    if record is None:
        raise KeyError(
            f"Theme '{name}' not found. Use "
            f"ThemeLibrary.get_available_themes() to list the "
            f"available themes."
        )
    # Theme.__init__ copies the palette, so mutating a returned Theme can
    # never corrupt the library's own record.
    return Theme(
        list(record.colors),
        slug=record.slug,
        name=record.name,
        unicode_name=record.unicode_name or record.name,
        category=record.category,
        disposition=record.disposition,
        replaced_by=record.replaced_by,
        static_mode=record.static_mode,
        dynamic_mode=record.dynamic_mode,
        tags=record.tags,
    )
get_available_themes classmethod
get_available_themes() -> list[str]

Get all available themes by name.

RETURNS DESCRIPTION
list[str]

Sorted list of theme names

Example
from lifx.theme import ThemeLibrary

all_themes = ThemeLibrary.get_available_themes()
for theme_name in all_themes:
    print(f"- {theme_name}")
Source code in src/lifx/theme/library.py
@classmethod
def get_available_themes(cls) -> list[str]:
    """Get all available themes by name.

    Returns:
        Sorted list of theme names

    Example:
        ```python
        from lifx.theme import ThemeLibrary

        all_themes = ThemeLibrary.get_available_themes()
        for theme_name in all_themes:
            print(f"- {theme_name}")
        ```
    """
    return sorted(cls._THEMES)
get_categories classmethod
get_categories() -> list[str]

Get every category present in the library's data.

RETURNS DESCRIPTION
list[str]

Sorted list[str] of the category names present in the

list[str]

library's theme records.

Example
from lifx.theme import ThemeLibrary

for category in ThemeLibrary.get_categories():
    print(f"- {category}")
Source code in src/lifx/theme/library.py
@classmethod
def get_categories(cls) -> list[str]:
    """Get every category present in the library's data.

    Returns:
        Sorted ``list[str]`` of the category names present in the
        library's theme records.

    Example:
        ```python
        from lifx.theme import ThemeLibrary

        for category in ThemeLibrary.get_categories():
            print(f"- {category}")
        ```
    """
    return sorted({record.category for record in cls._THEMES.values()})
get_by_category classmethod
get_by_category(category: str) -> dict[str, Theme]

Get all themes in a category.

PARAMETER DESCRIPTION
category

Category name. Matching is case- and punctuation- insensitive: both sides are normalised by the slug rule, so "Art Series", "art series" and "art_series" all resolve. The categories are Archives, Art Series, Holidays, Library (pre-6.3.0 keys with no app counterpart, defined by this library rather than the LIFX app), Moods, Music, Nature, Play, Space and Worldly.

TYPE: str

RETURNS DESCRIPTION
dict[str, Theme]

Dictionary of Theme objects in the category, keyed by slug and

dict[str, Theme]

sorted by slug.

RAISES DESCRIPTION
ValueError

If category is not a string, or names no category in the library. The pre-6.4.0 names (seasonal, holiday, mood, ambient, functional, atmosphere) are among the unrecognised: they were never a taxonomy this data carries, and the message lists the categories that exist.

Source code in src/lifx/theme/library.py
@classmethod
def get_by_category(cls, category: str) -> dict[str, Theme]:
    """Get all themes in a category.

    Args:
        category: Category name. Matching is case- and punctuation-
            insensitive: both sides are normalised by the slug rule, so
            ``"Art Series"``, ``"art series"`` and ``"art_series"`` all
            resolve. The categories are Archives, Art Series, Holidays,
            Library (pre-6.3.0 keys with no app counterpart, defined by
            this library rather than the LIFX app), Moods, Music, Nature,
            Play, Space and Worldly.

    Returns:
        Dictionary of Theme objects in the category, keyed by slug and
        sorted by slug.

    Raises:
        ValueError: If ``category`` is not a string, or names no category
            in the library. The pre-6.4.0 names (``seasonal``, ``holiday``,
            ``mood``, ``ambient``, ``functional``, ``atmosphere``) are
            among the unrecognised: they were never a taxonomy this data
            carries, and the message lists the categories that exist.
    """
    slugs = cls._require_category_slugs(category)
    return {slug: cls.get(slug) for slug in sorted(slugs)}
get_tags classmethod
get_tags() -> list[str]

Get every tag present in the library's data.

Tags are the LIFX app's search tags, such as "Calm" or "Date night". A theme with no app source carries none.

RETURNS DESCRIPTION
list[str]

Distinct tags, sorted case-insensitively.

Source code in src/lifx/theme/library.py
@classmethod
def get_tags(cls) -> list[str]:
    """Get every tag present in the library's data.

    Tags are the LIFX app's search tags, such as ``"Calm"`` or
    ``"Date night"``. A theme with no app source carries none.

    Returns:
        Distinct tags, sorted case-insensitively.
    """
    return sorted({tag for r in cls._records() for tag in r.tags}, key=tag_sort_key)
get_by_tag classmethod
get_by_tag(tag: str) -> dict[str, Theme]

Get all themes carrying a tag.

PARAMETER DESCRIPTION
tag

Tag name. Matching is case- and punctuation-insensitive: both sides are normalised by the slug rule, so "Date night", "date night" and "date_night" all resolve.

TYPE: str

RETURNS DESCRIPTION
dict[str, Theme]

Dictionary of Theme objects keyed by slug and sorted by slug.

RAISES DESCRIPTION
ValueError

If tag is not a string, or matches no tag in the library. The message lists the tags that exist.

Source code in src/lifx/theme/library.py
@classmethod
def get_by_tag(cls, tag: str) -> dict[str, Theme]:
    """Get all themes carrying a tag.

    Args:
        tag: Tag name. Matching is case- and punctuation-insensitive:
            both sides are normalised by the slug rule, so
            ``"Date night"``, ``"date night"`` and ``"date_night"`` all
            resolve.

    Returns:
        Dictionary of Theme objects keyed by slug and sorted by slug.

    Raises:
        ValueError: If ``tag`` is not a string, or matches no tag in the
            library. The message lists the tags that exist.
    """
    return {slug: cls.get(slug) for slug in sorted(cls._require_tag_slugs(tag))}
find classmethod
find(
    *,
    tags: Iterable[str] = (),
    match: Literal["all", "any"] = "all",
    category: str | None = None,
    static_mode: str | None = None,
) -> dict[str, Theme]

Find themes by tags, category and static mode together.

Every given criterion must hold. match decides only how the tags combine: "all" requires every tag, "any" at least one. With no criteria, every theme is returned.

PARAMETER DESCRIPTION
tags

Tag names, matched as get_by_tag() matches them.

TYPE: Iterable[str] DEFAULT: ()

match

"all" or "any".

TYPE: Literal['all', 'any'] DEFAULT: 'all'

category

Category name, matched as get_by_category() matches it.

TYPE: str | None DEFAULT: None

static_mode

Exact static mode, such as "grid_static".

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
dict[str, Theme]

Dictionary of matching Theme objects keyed by slug and sorted by

dict[str, Theme]

slug; empty when valid criteria select nothing.

RAISES DESCRIPTION
TypeError

If tags is a single string rather than an iterable of tags.

ValueError

If match is invalid, or a tag, category or static mode names nothing in the library.

Example
from lifx.theme import ThemeLibrary

tags = ThemeLibrary.get_tags()
if "Calm" in tags:
    calm = ThemeLibrary.find(tags=["Calm"])
blended = ThemeLibrary.find(static_mode="blended")
Source code in src/lifx/theme/library.py
@classmethod
def find(
    cls,
    *,
    tags: Iterable[str] = (),
    match: Literal["all", "any"] = "all",
    category: str | None = None,
    static_mode: str | None = None,
) -> dict[str, Theme]:
    """Find themes by tags, category and static mode together.

    Every given criterion must hold. ``match`` decides only how the
    tags combine: ``"all"`` requires every tag, ``"any"`` at least one.
    With no criteria, every theme is returned.

    Args:
        tags: Tag names, matched as ``get_by_tag()`` matches them.
        match: ``"all"`` or ``"any"``.
        category: Category name, matched as ``get_by_category()``
            matches it.
        static_mode: Exact static mode, such as ``"grid_static"``.

    Returns:
        Dictionary of matching Theme objects keyed by slug and sorted by
        slug; empty when valid criteria select nothing.

    Raises:
        TypeError: If ``tags`` is a single string rather than an
            iterable of tags.
        ValueError: If ``match`` is invalid, or a tag, category or
            static mode names nothing in the library.

    Example:
        ```python
        from lifx.theme import ThemeLibrary

        tags = ThemeLibrary.get_tags()
        if "Calm" in tags:
            calm = ThemeLibrary.find(tags=["Calm"])
        blended = ThemeLibrary.find(static_mode="blended")
        ```
    """
    if isinstance(tags, str):
        raise TypeError(
            "tags must be an iterable of tags, not a single string; "
            "pass ['Calm'] for one tag"
        )
    if match not in ("all", "any"):
        raise ValueError(f"match must be 'all' or 'any', got {match!r}")
    records = cls._records()
    selected = {r.slug for r in records}
    tag_sets = [cls._require_tag_slugs(tag) for tag in tags]
    if tag_sets:
        if match == "all":
            selected &= set.intersection(*tag_sets)
        else:
            selected &= set.union(*tag_sets)
    if category is not None:
        selected &= cls._require_category_slugs(category)
    if static_mode is not None:
        modes = sorted({r.static_mode for r in records})
        if static_mode not in modes:
            raise ValueError(
                f"static_mode '{static_mode}' is not recognised. "
                f"Available static modes: {', '.join(modes) or '(none)'}"
            )
        selected &= {r.slug for r in records if r.static_mode == static_mode}
    return {slug: cls.get(slug) for slug in sorted(selected)}

Effect Modes and Tags

Every library theme records how the LIFX app shows it:

  • Theme.static_mode is the still image the app paints, such as blended (a gradient from the palette) or grid_static (the colours are an ordered grid). A theme you build yourself has none and is treated as blended.
  • Theme.dynamic_mode is the effect the app's Dynamic toggle starts, when the theme names one. Theme.resolved_dynamic_mode gives the effect either way: MORPH for blended themes and MOVE for the rest, unless the theme says otherwise.
  • Theme.tags holds the app's search tags, such as Calm.

StaticMode and DynamicMode are the matching Literal types. They list every mode the library's data contains, so a new release can widen them.

Colours are stored in source order, which matters most for grid_static themes and other non-blended static modes: there the order is the layout.

Tags are spelt as the app spells them (for example Cozy). Tag matching ignores case and punctuation, so "calm" finds Calm.

Only the LIFX app's own themes carry tags; library-only and deprecated themes have none and use the blended static mode. An unknown tag, category or static mode raises ValueError, so check ThemeLibrary.get_tags() first.

Find themes with ThemeLibrary.get_tags(), ThemeLibrary.get_by_tag() and ThemeLibrary.find():

from lifx.theme import ThemeLibrary

tags = ThemeLibrary.get_tags()

if "Calm" in tags:
    calm = ThemeLibrary.get_by_tag("calm")

if {"Calm", "Cozy"} <= set(tags):
    calm_or_cosy = ThemeLibrary.find(tags=["Calm", "Cozy"], match="any")

Moods

apply_mood() paints a theme the way the LIFX app paints a mood, and animate_mood() starts the effect the app's Dynamic toggle starts. Both take only the theme and use the app's own timings: a 0.3 second fade, and the mood rescaled so its brightest colour matches the light's brightness, animated palettes included. apply_mood() powers lights on only when every targeted light is off; animate_mood() turns on every light it targets.

stop_effect() after animate_mood() puts back what the light showed before its mood animation started: power, and its colour, zones or tiles. Starting another mood on an animating light keeps that state, and it is restored once. A DeviceGroup has no stop_effect(); call it on each light.

Light Still image (apply_mood) Effect (animate_mood)
Bulb One of the theme's colours; a DeviceGroup deals them one per bulb EffectColorloop through the theme's colours; bulbs in one DeviceGroup share one loop
Strip The mood across the zones Firmware MOVE, after painting the still image
Matrix light The mood's image, by static_mode Firmware MORPH for a MORPH mood; EffectScroll for a MOVE mood
Spot and Path The mood's image, painted like any other matrix light Firmware MORPH for every mood
Mirror Stripe moods paint as bands along the long axis, first colour at the bottom; other moods as for a matrix light Firmware MORPH for every mood
Candle and Tube Stripe moods paint as bands along the long axis, first colour at the bottom A MOVE stripe mood scrolls the bands down the light; other moods as for a matrix light

The Mirror's still image is painted over its 4x13 buffer as a single matrix light, not ring by ring, with stripe moods as bands along its long axis. The app runs a firmware effect on Spot and Path that this library does not send, so MORPH stands in for it.

A Tile chain is painted as one image in chain order, as the app does, so tiles arranged in an L or a stack show the image in chain order rather than following their arrangement. A Tile whose accelerometer reports a rotation (left, right or upside down) is remapped; FaceUp and FaceDown are not. Only chain-capable products (the Tile) have their reported orientation applied; other matrix products, such as the Luna, are fixed panels whose accelerometer readings are not used. A chain scrolls as one canvas, in chain order.

If a light is already running a mood effect, apply_mood() restarts that effect with the new theme instead of painting a still image.

apply_theme() is unchanged; use it for the library's own gradients.

MoodGenerator

MoodGenerator(theme: Theme, rng: Random | None = None)

Turn a theme into the colours the LIFX app paints for it.

The get_*_colors() methods paint a still image; get_palette() gives the colours an animated mood steps through. Each is rescaled so its brightest colour matches the light's brightness.

Example
generator = MoodGenerator(get_theme("van_gogh"))
colors = generator.get_matrix_colors(8, 8, brightness=0.6)
palette = generator.get_palette(brightness=0.6)
PARAMETER DESCRIPTION
theme

The theme to paint. Its static_mode picks the recipe; None, or a mode with no recipe, paints as blended.

TYPE: Theme

rng

Random source for the shuffles, for repeatable output.

TYPE: Random | None DEFAULT: None

METHOD DESCRIPTION
get_multizone_colors

Colours for a strip, one per zone.

get_matrix_colors

Colours for one matrix light, in row-major order.

get_chain_colors

Colours for a Tile chain, laid out in chain order.

get_bulb_colors

Deal the theme's distinct colours, shuffled, one per bulb in turn.

get_palette

The theme's colours, in order, rescaled to the light's brightness.

morph_palette

Reduce a palette to what firmware MORPH can carry.

Source code in src/lifx/theme/generators/mood.py
def __init__(self, theme: Theme, rng: random.Random | None = None) -> None:
    """Create a generator for one theme.

    Args:
        theme: The theme to paint. Its ``static_mode`` picks the recipe;
            None, or a mode with no recipe, paints as ``blended``.
        rng: Random source for the shuffles, for repeatable output.
    """
    self._colors = list(theme.colors)
    mode = theme.static_mode
    self._mode = mode if mode in _RECIPES else "blended"
    self._rng = rng if rng is not None else random.Random()

Methods:

get_multizone_colors
get_multizone_colors(zone_count: int, brightness: float) -> list[HSBK]

Colours for a strip, one per zone.

PARAMETER DESCRIPTION
zone_count

Number of zones to fill

TYPE: int

brightness

The light's current brightness, 0.0 to 1.0

TYPE: float

RETURNS DESCRIPTION
list[HSBK]

One colour per zone

Source code in src/lifx/theme/generators/mood.py
def get_multizone_colors(self, zone_count: int, brightness: float) -> list[HSBK]:
    """Colours for a strip, one per zone.

    Args:
        zone_count: Number of zones to fill
        brightness: The light's current brightness, 0.0 to 1.0

    Returns:
        One colour per zone
    """
    if self._mode == "blended":
        colors = self._gradient(self._shuffled(self._colors), zone_count)
    elif self._mode == "solid":
        colors = self._stretch(
            self._shuffled(self._distinct(self._colors)), zone_count
        )
    else:
        colors = self._stretch(self._colors, zone_count)
    return self._rescale(colors, brightness)
get_matrix_colors
get_matrix_colors(
    width: int, height: int, brightness: float, *, vertical: bool = False
) -> list[HSBK]

Colours for one matrix light, in row-major order.

PARAMETER DESCRIPTION
width

Pixels per row, as the device reports

TYPE: int

height

Rows, as the device reports

TYPE: int

brightness

The light's current brightness, 0.0 to 1.0

TYPE: float

vertical

Paint stripe moods as bands along the long axis, as the app does on every Candle, the Tube and the Mirror

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[HSBK]

width * height colours

Source code in src/lifx/theme/generators/mood.py
def get_matrix_colors(
    self, width: int, height: int, brightness: float, *, vertical: bool = False
) -> list[HSBK]:
    """Colours for one matrix light, in row-major order.

    Args:
        width: Pixels per row, as the device reports
        height: Rows, as the device reports
        brightness: The light's current brightness, 0.0 to 1.0
        vertical: Paint stripe moods as bands along the long axis, as the
            app does on every Candle, the Tube and the Mirror

    Returns:
        ``width * height`` colours
    """
    return self._rescale(self._paint(width, height, vertical=vertical), brightness)
get_chain_colors
get_chain_colors(
    tile_count: int, width: int, height: int, brightness: float
) -> list[list[HSBK]]

Colours for a Tile chain, laid out in chain order.

The app ignores where the tiles sit: one canvas width * tile_count wide is painted and sliced per tile. A blended mood paints each tile on its own.

PARAMETER DESCRIPTION
tile_count

Tiles in the chain

TYPE: int

width

Pixels per row of one tile

TYPE: int

height

Rows of one tile

TYPE: int

brightness

The light's current brightness, 0.0 to 1.0

TYPE: float

RETURNS DESCRIPTION
list[list[HSBK]]

Per tile, width * height colours in row-major order

Source code in src/lifx/theme/generators/mood.py
def get_chain_colors(
    self, tile_count: int, width: int, height: int, brightness: float
) -> list[list[HSBK]]:
    """Colours for a Tile chain, laid out in chain order.

    The app ignores where the tiles sit: one canvas ``width * tile_count``
    wide is painted and sliced per tile. A blended mood paints each tile
    on its own.

    Args:
        tile_count: Tiles in the chain
        width: Pixels per row of one tile
        height: Rows of one tile
        brightness: The light's current brightness, 0.0 to 1.0

    Returns:
        Per tile, ``width * height`` colours in row-major order
    """
    if self._mode == "blended":
        tiles = [
            self._blended_matrix(self._colors, width, height)
            for _ in range(tile_count)
        ]
    else:
        canvas_width = width * tile_count
        canvas = self._paint(canvas_width, height)
        tiles = [
            [
                canvas[row * canvas_width + tile * width + col]
                for row in range(height)
                for col in range(width)
            ]
            for tile in range(tile_count)
        ]
    flat = self._rescale([c for tile in tiles for c in tile], brightness)
    size = width * height
    return [flat[i * size : (i + 1) * size] for i in range(tile_count)]
get_bulb_colors
get_bulb_colors(brightnesses: Sequence[float]) -> list[HSBK]

Deal the theme's distinct colours, shuffled, one per bulb in turn.

PARAMETER DESCRIPTION
brightnesses

Each bulb's current brightness, in bulb order

TYPE: Sequence[float]

RETURNS DESCRIPTION
list[HSBK]

One colour per bulb, rescaled to that bulb's brightness

Source code in src/lifx/theme/generators/mood.py
def get_bulb_colors(self, brightnesses: Sequence[float]) -> list[HSBK]:
    """Deal the theme's distinct colours, shuffled, one per bulb in turn.

    Args:
        brightnesses: Each bulb's current brightness, in bulb order

    Returns:
        One colour per bulb, rescaled to that bulb's brightness
    """
    deck = self._shuffled(self._distinct(self._colors))
    dealt: list[HSBK] = []
    for index, brightness in enumerate(brightnesses):
        # Rescale the whole deck so each bulb keeps the theme's relative
        # brightness, then take this bulb's card.
        dealt.append(self._rescale(deck, brightness)[index % len(deck)])
    return dealt
get_palette
get_palette(brightness: float) -> list[HSBK]

The theme's colours, in order, rescaled to the light's brightness.

An animated mood steps through these: a bulb's colour loop and a matrix light's firmware MORPH, as the app rescales them.

PARAMETER DESCRIPTION
brightness

The light's brightness, 0.0 to 1.0. At 0.0 the colours are returned unchanged.

TYPE: float

RETURNS DESCRIPTION
list[HSBK]

One colour per theme colour, the brightest at brightness

Source code in src/lifx/theme/generators/mood.py
def get_palette(self, brightness: float) -> list[HSBK]:
    """The theme's colours, in order, rescaled to the light's brightness.

    An animated mood steps through these: a bulb's colour loop and a
    matrix light's firmware MORPH, as the app rescales them.

    Args:
        brightness: The light's brightness, 0.0 to 1.0. At 0.0 the
            colours are returned unchanged.

    Returns:
        One colour per theme colour, the brightest at ``brightness``
    """
    return self._rescale(self._colors, brightness)
morph_palette staticmethod
morph_palette(colors: Sequence[HSBK]) -> list[HSBK]

Reduce a palette to what firmware MORPH can carry.

The app's run-weighted reduction: deterministic, so only the order the caller shuffles it into varies.

PARAMETER DESCRIPTION
colors

Palette in source order

TYPE: Sequence[HSBK]

RETURNS DESCRIPTION
list[HSBK]

The palette unchanged when it fits, else MAX_PALETTE_COLORS

list[HSBK]

colours weighted by the area each run covers

Source code in src/lifx/theme/generators/mood.py
@staticmethod
def morph_palette(colors: Sequence[HSBK]) -> list[HSBK]:
    """Reduce a palette to what firmware MORPH can carry.

    The app's run-weighted reduction: deterministic, so only the order
    the caller shuffles it into varies.

    Args:
        colors: Palette in source order

    Returns:
        The palette unchanged when it fits, else ``MAX_PALETTE_COLORS``
        colours weighted by the area each run covers
    """
    if len(colors) <= MAX_PALETTE_COLORS:
        return list(colors)
    return MoodGenerator._stretch(colors, MAX_PALETTE_COLORS)

Convenience Function

get_theme

get_theme(name: str) -> Theme

Get a theme by name.

Convenience function equivalent to ThemeLibrary.get(name).

PARAMETER DESCRIPTION
name

Theme name (case-insensitive)

TYPE: str

RETURNS DESCRIPTION
Theme

Theme object

Example
from lifx.theme import get_theme

evening = get_theme("evening")
await light.apply_theme(evening, power_on=True)
Source code in src/lifx/theme/library.py
def get_theme(name: str) -> Theme:
    """Get a theme by name.

    Convenience function equivalent to ThemeLibrary.get(name).

    Args:
        name: Theme name (case-insensitive)

    Returns:
        Theme object

    Example:
        ```python
        from lifx.theme import get_theme

        evening = get_theme("evening")
        await light.apply_theme(evening, power_on=True)
        ```
    """
    return ThemeLibrary.get(name)

Built-in Theme Catalogue

For the live category/count table, executable enumeration examples, compatibility notes and fidelity boundary, see the Built-in Theme Catalogue.

Theme.disposition and Theme.replaced_by record each theme's fate; the six pre-6.4.0 category names are retired and raise ValueError. Both are documented on the Theme Taxonomy Changes page.