Skip to main content

Hueify

PyPI Python

Hueify is a typed async client for the Philips Hue CLIP v2 API. Lights, rooms and zones share one command surface, and the raw JSON resources stay reachable underneath it.

pip install hueify

Setup

A bridge IP and an application key are needed. hueify setup discovers the bridge, waits for the link button and prints both:

$ hueify setup
...
Setup complete. Hueify reads these two values:

  HUE_BRIDGE_IP=192.168.1.10
  HUE_APP_KEY=Xf3k…

Put both into your environment or a .env file and Hueify() picks them up. The individual steps are available too, and return their result:

from hueify.onboarding import discover_bridges, register_app_key, setup

bridges = await discover_bridges()
app_key = await register_app_key(bridges[0].internalipaddress)

credentials = setup()  # the interactive flow, as HueBridgeCredentials

Constructor arguments win over the environment:

hue = Hueify(bridge_ip="192.168.1.10", app_key="…")

Without any of these, Hueify() raises MissingCredentialsError and names what is missing.

Quickstart

import asyncio

from hueify import Hueify


async def main() -> None:
    async with Hueify() as hue:
        desk = await hue.lights.find_by_name("Desk")

        await hue.lights.turn_on(desk.id, brightness=60)
        await hue.lights.set_hex(desk.id, "#ff8800")
        await hue.lights.turn_off(desk.id, transition=2)

        office = await hue.rooms.find_by_name("Office")
        await hue.rooms.turn_on(office.id, brightness=70)


asyncio.run(main())

Hueify owns one HTTP client. Use it as an async context manager or call await hue.close() yourself; entering it does not talk to the bridge yet.

Commands

hue.lights, hue.rooms and hue.zones understand the same commands. A room or zone is switched through its grouped light, so it takes one bridge call instead of one per lamp - hueify resolves that service for you.

Command Effect
turn_on(id, brightness=…, kelvin=…) Switch on, optionally in one shot
turn_off(id) Switch off
toggle(id) Read the current state and flip it
is_on(id) True if the light or group is on
set_brightness(id, 65) Absolute brightness in percent; 0 switches off
brighten(id, by=10) / dim(id, by=10) Relative step, applied by the bridge
set_hex(id, "#ff8800") Color from a hex string, #rgb or #rrggbb
set_rgb(id, 0, 128, 255) Color from three 0-255 channels
set_color_temperature(id, 2700) White point in kelvin
set_state(id, …) Send exactly the given fields and nothing else
identify(id) Let the lamp breathe so you can tell which one it is

Every set_* command switches the target on, because asking for a brightness or a color implies it. set_state does not: it sends what it is given.

await hue.lights.set_state(desk.id, brightness=30)          # dim without switching on
await hue.lights.set_state(desk.id, on=True, mirek=370)     # raw mirek instead of kelvin

Every command returns the native HueApiResponse[ResourceIdentifier] of the bridge.

Colors

One command per color format, so the signature says which one it wants. Both take brightness and transition, and both switch the light on:

await hue.lights.set_hex(desk.id, "#ff8800")     # or "#f80"
await hue.lights.set_rgb(desk.id, 0, 128, 255)

set_hex(id, "warm white") fails rather than guessing.

Color temperatures are given in kelvin and clamped to the 2000-6500 K range Hue lamps support. hueify.color exposes the conversions themselves - to_xy, hex_to_rgb, xy_to_hex, kelvin_to_mirek - and to_xy is the lenient one: it takes a hex string, a name from NAMED_COLORS, an RGB tuple or a ColorXY. That is what you want for colors that arrive as strings, and for CIE xy, which set_state passes through unchanged:

from hueify.color import to_xy

await hue.lights.set_state(desk.id, on=True, color=to_xy(configured_color))
await hue.lights.set_state(desk.id, on=True, color=ColorXY(x=0.5, y=0.4))

Transitions

Any command takes a transition, in seconds or as a timedelta. The bridge runs the fade:

from datetime import timedelta

await hue.lights.set_brightness(desk.id, 100, transition=timedelta(seconds=10))
await hue.rooms.turn_off(office.id, transition=3)

Finding resources

IDs are the lookup keys, names are what you see in the Hue app. Every namespace resolves both, and unwraps single resources for you:

light = await hue.lights.find_by_name("Desk")       # ignores case and surrounding space
light = await hue.lights.find_by_name("desklamp")   # close enough also matches
light = await hue.lights.get_one(light.id)          # by ID, the resource itself

find_by_name takes an exact match first and otherwise falls back to the closest name above a similarity cutoff. If nothing is close enough it raises ResourceNotFoundError listing the names it did find, which is usually enough to spot the typo.

The envelope-returning reads stay available for callers that want the errors alongside the data:

response = await hue.lights.list()   # HueApiResponse[Light]
response = await hue.lights.get(light.id)

print(response.errors, response.data)

Reading state

Resources come back as Pydantic models mirroring the CLIP v2 JSON. State lives in optional sub-models there, so lights and grouped lights carry flat accessors next to the raw fields:

light = await hue.lights.get_one(desk.id)

light.name          # metadata.name
light.is_on         # bool | None
light.brightness    # float | None, percent
light.mirek         # int | None
light.xy            # ColorXY | None
light.on.on         # the underlying field is still right there

All Hue models use Pydantic with extra="allow". Known fields are statically typed, while fields introduced by newer bridge firmware are retained. Convert a response back to complete JSON with response.model_dump(mode="json").

Reads are snapshots. Nothing is cached and nothing is polled in the background, so a later read returns whatever the bridge reports then. To follow changes as they happen, use the event stream.

Rooms, zones and scenes

A room groups devices, a zone groups light services, and scenes belong to either. Both namespaces resolve that hierarchy:

office = await hue.rooms.find_by_name("Office")

for light in await hue.rooms.lights(office.id):
    print(light.name, light.is_on)

for scene in await hue.rooms.scenes(office.id):
    print(scene.name)

await hue.scenes.activate(scene.id, brightness=40, transition=2)
await hue.scenes.activate(scene.id, dynamic=True)

hue.rooms.grouped_light(id) returns the aggregated state of a group, and hue.rooms.apply(id, LightUpdate(...)) sends a raw update to it. Rooms, zones and scenes also expose their native create, update and delete operations, and hue.scenes.recall(id, SceneRecallRequest(...)) remains available next to activate for the full recall payload.

Event stream

Entering Hueify does not connect to the SSE stream. Register handlers with the @hue.on(...) decorator, then start the stream explicitly:

import asyncio

from hueify import Hueify
from hueify.models import HueEvent, LightEvent, ResourceType


async with Hueify() as hue:
    @hue.on(ResourceType.LIGHT)
    async def on_light(event: LightEvent) -> None:
        print(event.id, event.is_on, event.brightness)

    @hue.on("*")
    async def on_any(event: HueEvent) -> None:
        print(event.type, event.id)

    await hue.start_events()
    await asyncio.Event().wait()

Events arrive as LightEvent, RoomEvent, ZoneEvent and SceneEvent - the matching update model plus an ID, so a LightEvent reads like a light, including the flat accessors. Anything else arrives as the base HueEvent. A "*" handler receives every event, in addition to the type-specific ones.

hue.off(resource_type, handler) removes a handler, hue.stop_events() ends the stream and hue.events_connected reports whether it is running. Leaving the context manager closes a started stream along with the HTTP client.

Examples

Runnable scripts for each use case live in examples/.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hueify-0.7.0.tar.gz (60.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hueify-0.7.0-py3-none-any.whl (27.3 kB view details)

Uploaded Python 3

File details

Details for the file hueify-0.7.0.tar.gz.

File metadata

  • Download URL: hueify-0.7.0.tar.gz
  • Upload date:
  • Size: 60.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.2

File hashes

Hashes for hueify-0.7.0.tar.gz
Algorithm Hash digest
SHA256 a8a88f16848808b3d8db9fcdf37bc5298d1fc6f2242c8c554d0f5160272ef635
MD5 7f006c0bf81c3bea83d2c942d8cc0ac8
BLAKE2b-256 d7265d82ce49009e823df0d8a74a0fad0d09b4c293ae2271812b44fefb5b6d56

See more details on using hashes here.

File details

Details for the file hueify-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: hueify-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 27.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.2

File hashes

Hashes for hueify-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1c2cbfab3b8e098c68f6b9d908c4cd8916546117a8841c8f824fb76e478d4699
MD5 f76d2bd906f951a8fa7f73b500e1fcd0
BLAKE2b-256 7f31571862aed92ba581a21f3d04e749a93fdc8c89ee0d51e40774e3f935d98b

See more details on using hashes here.

Release history Release notifications | RSS feed

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

This release

0.7.0 This release

2 files

0.6.1

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page