diff --git a/CLAUDE.md b/CLAUDE.md
index 93ff20e..e7c9441 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,16 +21,16 @@ python3 -m pytest tests/test_engine.py
python3 -m pytest tests/test_engine.py -k "test_motion_on"
# CLI (after install)
-hue-iac -c examples/home.yaml validate
-hue-iac -c examples/home.yaml preview
-hue-iac -c examples/home.yaml plan
-hue-iac -c examples/home.yaml apply
-hue-iac -c examples/home.yaml circadian run # the resident daemon (foreground)
-hue-iac -c examples/home.yaml watch --dry-run # legacy motion controller
+hueman -c examples/home.yaml validate
+hueman -c examples/home.yaml preview
+hueman -c examples/home.yaml plan
+hueman -c examples/home.yaml apply
+hueman -c examples/home.yaml circadian run # the resident daemon (foreground)
+hueman -c examples/home.yaml watch --dry-run # legacy motion controller
```
Bridge-touching commands need `HUE_APPLICATION_KEY` — set it from your bridge
-pairing (`hue-iac auth` performs trust-on-first-use pairing and TLS pinning).
+pairing (`hueman auth` performs trust-on-first-use pairing and TLS pinning).
The bridge host lives in the config (`bridge.host`); `HUE_BRIDGE_HOST` only
overrides it.
@@ -48,7 +48,7 @@ mean there are no sensors.** MotionAware surfaces under its own resource types:
`sensitivity{sensitivity, sensitivity_max}`
`state.py` indexes these (`MotionArea`, `_index_motion_areas`) and
-`hue-iac inventory` prints them. Only the legacy `watch.py` runtime is blind to
+`hueman inventory` prints them. Only the legacy `watch.py` runtime is blind to
MotionAware (it routes just `motion`/`light_level`/`grouped_light` events and
requires a legacy PIR sensor to even construct). MotionAware **sensitivity is a
live-only bridge setting**: no reconciler manages it, and a daily apply neither
@@ -62,7 +62,7 @@ reverts nor recreates it.
## The two day-cycle deployment modes
-**Mode 1 — the circadian daemon** (`hue-iac circadian run`, typically a Docker
+**Mode 1 — the circadian daemon** (`hueman circadian run`, typically a Docker
container near the bridge): re-samples the solar curve every 60 s and drives one
grouped_light zone with long cross-fades from sunrise to `hand_off`, detects
manual overrides by settle-and-compare, and owns the TV-bias hold. Night
@@ -70,12 +70,12 @@ guidance stays native on the bridge (`night_motion:` → a MotionAware
`behavior_instance`), provisioned by `apply`.
**Mode 2 — native, daemon-less**: `CircadianSceneReconciler` (+ pure
-`hue_iac/circadian_scene.py`) generates ≤6 sun-anchored knee scenes + a
+`hueman/circadian_scene.py`) generates ≤6 sun-anchored knee scenes + a
`smart_scene` with long cross-fades; a daily cron `apply` re-anchors the
timeslot times to real sun (idempotent — the looks are curve regime-constants).
The reconcilers in `reconcile.py` (all provisioned by `apply`):
-- `NightMotionReconciler` (+ pure `hue_iac/nightmotion.py`) tunes a MotionAware `behavior_instance` for night soft-red guidance on a guide zone, backing it up first. `plan` detects zone-scene look drift via the tolerant `scene_actions_match`; apply re-PUTs the automation only when its wiring actually changes. Sun-anchored timeslots it can't re-time surface as `BLOCKED`, not a crash.
+- `NightMotionReconciler` (+ pure `hueman/nightmotion.py`) tunes a MotionAware `behavior_instance` for night soft-red guidance on a guide zone, backing it up first. `plan` detects zone-scene look drift via the tolerant `scene_actions_match`; apply re-PUTs the automation only when its wiring actually changes. Sun-anchored timeslots it can't re-time surface as `BLOCKED`, not a crash.
- `CircadianSceneReconciler` — the native day-cycle generator (mode 2 above).
- `SmartSceneReconciler` re-times/prunes an *existing* (e.g. app-built) `smart_scene` to real sun. Empty-floor prune → `BLOCKED` (never PUTs an empty scene); timeslots whose scene rid is unresolved are preserved, not silently dropped; backs up before the PUT.
@@ -165,7 +165,7 @@ show built into the circadian daemon (`circadian_daemon.py` + pure
high frame rates (the daemon logs drop stats on exit); real per-frame control
would need the Entertainment streaming API.
-Triggers (OR-combined, no arming): `hue-iac security on|off` (writes the
+Triggers (OR-combined, no arming): `hueman security on|off` (writes the
`control_file` flags), and/or a Hue button/scene the daemon sees on SSE
(`triggers.sse`). On startup security is always OFF and stale control-files are cleared.
diff --git a/Dockerfile b/Dockerfile
index e741979..14d1d56 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,8 +1,8 @@
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml README.md ./
-COPY hue_iac ./hue_iac
+COPY hueman ./hueman
RUN pip install --no-cache-dir .
# Config + secrets + logs are mounted at runtime (not baked in).
WORKDIR /data
-ENTRYPOINT ["hue-iac", "-c", "/data/hue.yaml", "circadian", "run"]
+ENTRYPOINT ["hueman", "-c", "/data/hue.yaml", "circadian", "run"]
diff --git a/README.md b/README.md
index 5bd08ed..182d9a0 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,7 @@ Declarative, Terraform-style management of Philips Hue lighting — a sun-anchor
circadian day cycle, TV-aware bias lighting, night motion guidance, a panic
mode, and honoured manual overrides.
-> The CLI and Python package are currently named `hue-iac`; they will be renamed
-> to `hueman` in an upcoming release. Everywhere you see `hue-iac` below, that's
-> this project.
-
-You describe the desired state of your lights in a YAML file; `hue-iac` diffs it
+You describe the desired state of your lights in a YAML file; `hueman` diffs it
against the bridge (`plan`), converges it (`apply`), and — for the behaviours a
bridge can't run natively — ships a small resident **circadian daemon** that
drives a zone smoothly along the solar curve, holds a TV-bias look while you
@@ -21,7 +17,7 @@ watch, and steps aside the moment you touch a dimmer. Built for the local
flowchart LR
yaml["hue.yaml
(desired state)"]
- subgraph cli["hue-iac"]
+ subgraph cli["hueman"]
plan["plan / apply
(diff + converge)"]
daemon["circadian daemon
(circadian run)"]
end
@@ -77,15 +73,15 @@ timeline
## The commands
```
-hue-iac -c my-home.yaml validate # parse + validate the config (no bridge needed)
-hue-iac -c my-home.yaml preview # print the circadian colour curve for a day
-hue-iac -c my-home.yaml inventory # list everything on your bridge, by name
-hue-iac -c my-home.yaml plan # show what apply would change (read-only)
-hue-iac -c my-home.yaml apply # converge the bridge to the declared state
-hue-iac -c my-home.yaml circadian run # run the circadian + TV-bias daemon (foreground)
-hue-iac -c my-home.yaml circadian resume # clear a manual-override suspension out-of-band
-hue-iac -c my-home.yaml security on|off # arm / disarm the panic show
-hue-iac -c my-home.yaml watch # legacy live motion controller (see Limitations)
+hueman -c my-home.yaml validate # parse + validate the config (no bridge needed)
+hueman -c my-home.yaml preview # print the circadian colour curve for a day
+hueman -c my-home.yaml inventory # list everything on your bridge, by name
+hueman -c my-home.yaml plan # show what apply would change (read-only)
+hueman -c my-home.yaml apply # converge the bridge to the declared state
+hueman -c my-home.yaml circadian run # run the circadian + TV-bias daemon (foreground)
+hueman -c my-home.yaml circadian resume # clear a manual-override suspension out-of-band
+hueman -c my-home.yaml security on|off # arm / disarm the panic show
+hueman -c my-home.yaml watch # legacy live motion controller (see Limitations)
```
`-c` defaults to `./hue.yaml` if you omit it.
@@ -94,11 +90,12 @@ hue-iac -c my-home.yaml watch # legacy live motion controller (see Li
```bash
git clone https://github.com/cnewkirk/hueman && cd hueman
-python3 -m pip install -e . # installs the `hue-iac` console script
+python3 -m pip install -e . # installs the `hueman` console script
# contributors: python3 -m pip install -e ".[dev]" && python3 -m pytest
```
-Requires Python 3.10+.
+Requires Python 3.10+. A transitional `hue-iac` alias for the `hueman` command
+also installs (the project's former name); it will be removed in a future release.
## First-time setup
@@ -111,7 +108,7 @@ Requires Python 3.10+.
`my-home.yaml` (or `export HUE_BRIDGE_HOST=...`).
3. **Pair** — press the bridge's physical link button, then:
```bash
- hue-iac -c my-home.yaml auth
+ hueman -c my-home.yaml auth
```
It prints an application key. Keep it in an environment variable so it never
lands in a file:
@@ -122,28 +119,28 @@ Requires Python 3.10+.
areas by name, then edit `my-home.yaml`'s `areas:` (and light names
throughout) to match your home:
```bash
- hue-iac -c my-home.yaml inventory
+ hueman -c my-home.yaml inventory
```
5. **Validate and preview** (no bridge writes):
```bash
- hue-iac -c my-home.yaml validate
- hue-iac -c my-home.yaml preview # prints your circadian curve for today
+ hueman -c my-home.yaml validate
+ hueman -c my-home.yaml preview # prints your circadian curve for today
```
6. **Plan, then apply** — `plan` is read-only and shows exactly what `apply`
would change; nothing touches the bridge until you apply:
```bash
- hue-iac -c my-home.yaml plan
- hue-iac -c my-home.yaml apply
+ hueman -c my-home.yaml plan
+ hueman -c my-home.yaml apply
```
7. **Run the daemon** (foreground; use Docker/systemd to keep it up — a Synology
Docker runbook lives in `deploy/synology/README-docker.md`):
```bash
- hue-iac -c my-home.yaml circadian run
+ hueman -c my-home.yaml circadian run
```
## TLS
-The bridge serves a self-signed certificate. `hue-iac` defaults to
+The bridge serves a self-signed certificate. `hueman` defaults to
`tls.mode: pin` — trust-on-first-use: it records the bridge certificate's
SHA-256 fingerprint in `.hue-pin.json` on first connect and enforces it on
**every** connection the session opens (including the long-lived event stream),
@@ -229,7 +226,7 @@ Key sections:
`watch --dry-run` logs intended commands without sending them.
- The daemon runs in the foreground; pair it with Docker (`--restart always`),
`systemd`, or similar. On a manual override it suspends until a power-cycle of
- the zone, `hue-iac circadian resume`, or the daily safety resume.
+ the zone, `hueman circadian resume`, or the daily safety resume.
- Security mode's chaos runs over the REST API, which rate-limits under load —
the show degrades gracefully (the daemon logs drop stats). True per-frame
control would need the Entertainment streaming API.
@@ -260,7 +257,7 @@ unit-tested; the network surface is thin and replaceable.
| `pin.py` | Trust-on-first-use TLS certificate pinning |
| `circadian_daemon.py` | The resident daemon: curve ticks, SSE events, TV-bias triggers, security show |
| `watch.py` | Legacy live runtime: bridge event stream → `PolicyEngine` → commands |
-| `cli.py` | The `hue-iac` command-line interface |
+| `cli.py` | The `hueman` command-line interface |
diff --git a/deploy/synology/README-docker.md b/deploy/synology/README-docker.md
index b76300c..bc81ae5 100644
--- a/deploy/synology/README-docker.md
+++ b/deploy/synology/README-docker.md
@@ -1,4 +1,4 @@
-# hue-iac circadian daemon — Synology Container Manager runbook
+# hueman circadian daemon — Synology Container Manager runbook
This document covers running the circadian daemon as a Docker container on the NAS
via Synology Container Manager. The container replaces the re-anchor cron approach:
@@ -16,16 +16,16 @@ daemon.** The cron job is retired once this container is confirmed running.
- `.hue-key` — bridge application key (chmod 600); read by the **host shell** to
populate `$HUE_APPLICATION_KEY`, **not** mounted into the container
- `.hue-pin.json` — TLS pin (chmod 600); **must be pre-pinned** — the read-only
- container mount cannot perform trust-on-first-use (run `hue-iac auth` once from a
+ container mount cannot perform trust-on-first-use (run `hueman auth` once from a
host shell to create it if starting fresh)
-> **`hue.yaml` MUST contain a `circadian_daemon:` block.** `hue-iac circadian run`
+> **`hue.yaml` MUST contain a `circadian_daemon:` block.** `hueman circadian run`
> raises `HueIacError("no 'circadian_daemon' block in config")` and the container exits
> immediately (crash-loops under `--restart always`) if it is missing. At minimum the
> block needs `zone:` (the zone the daemon drives). The daemon tunables also
> live here — notably `manual_override.control_file` (the resume-signal file, default
> `.hue-circadian-resume`) and `log.path` (default `logs/circadian.log`, which lands
-> under the mounted `/data/logs`). See `hue_iac/config.py` (`CircadianDaemonSpec`) for the
+> under the mounted `/data/logs`). See `hueman/config.py` (`CircadianDaemonSpec`) for the
> full set of keys and their defaults. If yours doesn't have the block yet, add
> it before building.
>
@@ -44,18 +44,18 @@ Throughout this document, substitute `` with the actual share name
## Step 1 — Build the image on the NAS
The NAS keeps a plain **build context** at `$HUE_IAC_HOME/build/` (Dockerfile,
-`hue_iac/`, `pyproject.toml`, `README.md`) — **not a git clone** (the live deploy
+`hueman/`, `pyproject.toml`, `README.md`) — **not a git clone** (the live deploy
has no `src/` checkout; source is copied in from the dev machine). From your dev machine,
sync the current `main` into it, then build over SSH:
```sh
# On your dev machine, from a checkout of this repo at origin/main:
-rsync -r --delete hue_iac/ @:/volume1//hue-iac/build/hue_iac/
-rsync pyproject.toml Dockerfile @:/volume1//hue-iac/build/
+rsync -r --delete hueman/ @:/volume1//hueman/build/hueman/
+rsync pyproject.toml Dockerfile @:/volume1//hueman/build/
# (scp also works but needs -O — Synology has SFTP disabled.)
# On the NAS (docker is not on the non-interactive PATH — use the full path):
-sudo /usr/local/bin/docker build -t hue-iac:latest /volume1//hue-iac/build
+sudo /usr/local/bin/docker build -t hueman:latest /volume1//hueman/build
```
Alternatively, import a pre-built tarball via Container Manager UI:
@@ -66,7 +66,7 @@ Container Manager → Image → Import → select the `.tar` file exported from
## Step 2 — Start the daemon
```sh
-HUE_IAC_HOME=/volume1//hue-iac
+HUE_IAC_HOME=/volume1//hueman
docker run -d \
--name hue-circadian \
@@ -77,7 +77,7 @@ docker run -d \
-v "$HUE_IAC_HOME/logs":/data/logs \
-v /volume1//homeassistant/tv-signal:/data/tvsig \
-e HUE_APPLICATION_KEY="$(cat $HUE_IAC_HOME/.hue-key)" \
- hue-iac:latest
+ hueman:latest
```
**Why the `tv-signal` mount (REQUIRED for TV-bias):** TV detection runs through
@@ -95,7 +95,7 @@ ensures the container can reach that address and keep an SSE stream open without
interference.
**Key delivery:** the key is read **only** from the `HUE_APPLICATION_KEY` environment
-variable — no code path in `hue_iac/` reads a mounted key file at runtime (the only
+variable — no code path in `hueman/` reads a mounted key file at runtime (the only
`.hue-key` reference is the `auth` command printing an `export HUE_APPLICATION_KEY=...`
line). The `-e HUE_APPLICATION_KEY="$(cat $HUE_IAC_HOME/.hue-key)"` flag has the **host
shell** read the file and pass its contents as the env var; keep it exactly as shown.
@@ -139,11 +139,11 @@ first circadian scene applied.
## Step 4 — Trigger a manual circadian resume
-If the daemon is paused (e.g. you ran `hue-iac circadian pause` for debugging) and you
+If the daemon is paused (e.g. you ran `hueman circadian pause` for debugging) and you
want to resume without restarting the container:
```sh
-docker exec hue-circadian hue-iac -c /data/hue.yaml circadian resume
+docker exec hue-circadian hueman -c /data/hue.yaml circadian resume
```
---
@@ -154,9 +154,9 @@ Same as Step 1: re-sync the source from your dev machine into `build/`, rebuild,
recreate the container:
```sh
-# Mac: rsync hue_iac/ pyproject.toml Dockerfile into …/hue-iac/build/ (Step 1)
+# Mac: rsync hueman/ pyproject.toml Dockerfile into …/hueman/build/ (Step 1)
# NAS:
-sudo /usr/local/bin/docker build -t hue-iac:latest /volume1//hue-iac/build
+sudo /usr/local/bin/docker build -t hueman:latest /volume1//hueman/build
sudo /usr/local/bin/docker rm -f hue-circadian
# Re-run the docker run command from Step 2
```
@@ -170,14 +170,14 @@ sudo /usr/local/bin/docker rm -f hue-circadian
| Container exits immediately | Bad config / missing file mount | `docker logs hue-circadian` for the error |
| SSE stream drops / reconnects | Bridge rebooted or LAN blip | Daemon auto-reconnects; monitor logs |
| TLS pin mismatch | Bridge cert rotated | Refresh `.hue-pin.json` from your dev machine, restart container |
-| `BLOCKED` on scene apply | Zone or scene not found | Run `hue-iac plan` from your dev machine to diagnose |
+| `BLOCKED` on scene apply | Zone or scene not found | Run `hueman plan` from your dev machine to diagnose |
---
## Relationship to the re-anchor cron
-The daily `re-anchor.sh` cron (`README.md`) calls `hue-iac apply` once a day to keep
-the circadian smart-scene timed to the real sun. This Docker daemon runs `hue-iac
+The daily `re-anchor.sh` cron (`README.md`) calls `hueman apply` once a day to keep
+the circadian smart-scene timed to the real sun. This Docker daemon runs `hueman
circadian run` continuously, reacting to SSE events in real time — it includes its own
daily re-anchor logic. Once this daemon is confirmed stable, disable or delete
the Task Scheduler job created in `README.md` to avoid double-applying.
diff --git a/deploy/synology/README.md b/deploy/synology/README.md
index 37c439a..aa51fce 100644
--- a/deploy/synology/README.md
+++ b/deploy/synology/README.md
@@ -1,12 +1,12 @@
# Synology daily re-anchor — runbook
-Runs `hue-iac apply` once a day on the NAS so the bridge's circadian cycle
+Runs `hueman apply` once a day on the NAS so the bridge's circadian cycle
re-anchors to the real sun. DST is handled inside the tool (`location.tz`), so
nothing here needs a seasonal edit.
## Layout
```
-$HUE_IAC_HOME/ # e.g. /volume1//hue-iac
+$HUE_IAC_HOME/ # e.g. /volume1//hueman
src/ # git clone of this repo
.venv/ # python3 -m venv; pip install -e ./src
hue.yaml .hue-key .hue-pin.json .hue-backup/
@@ -15,7 +15,7 @@ $HUE_IAC_HOME/ # e.g. /volume1//hue-iac
## Install (over SSH, one time)
```sh
-HUE_IAC_HOME=/volume1//hue-iac
+HUE_IAC_HOME=/volume1//hueman
mkdir -p "$HUE_IAC_HOME" && cd "$HUE_IAC_HOME"
git clone https://github.com/cnewkirk/hueman.git src
python3 -m venv .venv
@@ -32,19 +32,19 @@ chmod 600 .hue-key .hue-pin.json
```sh
cd "$HUE_IAC_HOME"
export HUE_APPLICATION_KEY=$(cat .hue-key)
-.venv/bin/hue-iac -c hue.yaml validate
-.venv/bin/hue-iac -c hue.yaml plan # NOOP / time deltas, ZERO blocked
+.venv/bin/hueman -c hue.yaml validate
+.venv/bin/hueman -c hue.yaml plan # NOOP / time deltas, ZERO blocked
sh bin/re-anchor.sh && tail -n 20 logs/re-anchor.log # exit 0, "re-anchor OK"
-.venv/bin/hue-iac -c hue.yaml plan # NOOP (idempotent)
-.venv/bin/hue-iac -c hue.yaml preview --date 2026-12-15
-.venv/bin/hue-iac -c hue.yaml preview --date 2026-07-15 # sunrise ~1h apart => DST works
+.venv/bin/hueman -c hue.yaml plan # NOOP (idempotent)
+.venv/bin/hueman -c hue.yaml preview --date 2026-12-15
+.venv/bin/hueman -c hue.yaml preview --date 2026-07-15 # sunrise ~1h apart => DST works
```
## Schedule (DSM GUI — the one manual step)
Control Panel → Task Scheduler → Create → Scheduled Task → User-defined script
- **User:** `root`
- **Schedule:** Daily, **03:30**
-- **Run command:** `sh /volume1//hue-iac/bin/re-anchor.sh`
+- **Run command:** `sh /volume1//hueman/bin/re-anchor.sh`
- **Notification:** email → "only when the script terminates abnormally"
Then press **Run** once and check `logs/re-anchor.log`.
diff --git a/examples/home.yaml b/examples/home.yaml
index 326c045..fbfd365 100644
--- a/examples/home.yaml
+++ b/examples/home.yaml
@@ -1,24 +1,24 @@
# ---------------------------------------------------------------------------
-# hue-iac example configuration
+# hueman example configuration
#
# Copy this file (`cp examples/home.yaml my-home.yaml`), point bridge.host at
# your bridge, and replace the light/room/zone names with yours — run
-# `hue-iac -c my-home.yaml inventory` to see what your bridge actually has.
+# `hueman -c my-home.yaml inventory` to see what your bridge actually has.
#
# Declarative desired state for a Hue bridge (CLIP v2; night_motion needs a
# Bridge Pro running MotionAware). Then:
-# hue-iac -c my-home.yaml validate # check syntax (no bridge needed)
-# hue-iac -c my-home.yaml preview # see the circadian curve
-# hue-iac -c my-home.yaml plan # diff against the bridge (read-only)
-# hue-iac -c my-home.yaml apply # provision areas/scenes/night-red
-# hue-iac -c my-home.yaml circadian run # run the circadian + TV-bias daemon
-# hue-iac -c my-home.yaml watch # legacy motion controller
+# hueman -c my-home.yaml validate # check syntax (no bridge needed)
+# hueman -c my-home.yaml preview # see the circadian curve
+# hueman -c my-home.yaml plan # diff against the bridge (read-only)
+# hueman -c my-home.yaml apply # provision areas/scenes/night-red
+# hueman -c my-home.yaml circadian run # run the circadian + TV-bias daemon
+# hueman -c my-home.yaml watch # legacy motion controller
# ---------------------------------------------------------------------------
bridge:
host: 192.0.2.2 # or set $HUE_BRIDGE_HOST and omit this
# The application key is read from $HUE_APPLICATION_KEY by default so it
- # never lives in version control. Run `hue-iac auth` to create one.
+ # never lives in version control. Run `hueman auth` to create one.
application_key_env: HUE_APPLICATION_KEY
tls:
mode: pin # pin (trust-on-first-use) | cacert | insecure
@@ -71,7 +71,7 @@ circadian:
ramp_minutes: 90 # smooth 90-min dawn/dusk transitions
# ---------------------------------------------------------------------------
-# The circadian daemon (`hue-iac circadian run`) — the resident runtime.
+# The circadian daemon (`hueman circadian run`) — the resident runtime.
# Drives one zone smoothly along the solar curve from `start` to `hand_off`,
# steps back when it detects a manual override (settle-and-compare), and holds
# a TV-bias look on the viewing lights while the TV is on.
@@ -85,7 +85,7 @@ circadian_daemon:
fade_off: 90s # the hand-off fade to dark
manual_override:
detect: true
- control_file: .hue-circadian-resume # `hue-iac circadian resume` touches this
+ control_file: .hue-circadian-resume # `hueman circadian resume` touches this
resume_on_power_cycle: true
daily_safety_resume: true
# TV-aware bias hold: while the TV is on, each light below holds its `look`;
diff --git a/hue_iac/__init__.py b/hueman/__init__.py
similarity index 62%
rename from hue_iac/__init__.py
rename to hueman/__init__.py
index a6d7f20..99b9e63 100644
--- a/hue_iac/__init__.py
+++ b/hueman/__init__.py
@@ -1,9 +1,9 @@
-"""hue-iac: declarative, Terraform-style management of Philips Hue lighting.
+"""hueman: declarative, Terraform-style management of Philips Hue lighting.
The package is intentionally split so the pure-logic pieces (config parsing,
circadian math, sun times, plan diffing, motion translation) carry no network
dependency and are unit-testable, while the I/O surface (the CLIP API v2 client
-and TLS pinning) is isolated in :mod:`hue_iac.client` and :mod:`hue_iac.pin`.
+and TLS pinning) is isolated in :mod:`hueman.client` and :mod:`hueman.pin`.
"""
__version__ = "0.1.0"
diff --git a/hue_iac/bias_control.py b/hueman/bias_control.py
similarity index 96%
rename from hue_iac/bias_control.py
rename to hueman/bias_control.py
index ed2c6c9..e393174 100644
--- a/hue_iac/bias_control.py
+++ b/hueman/bias_control.py
@@ -1,10 +1,10 @@
"""Pure decision core for the daemon's TV bias hold (no clock, no I/O).
-Given the parsed :class:`~hue_iac.config.BiasSpec`, whether the TV is on, and the
+Given the parsed :class:`~hueman.config.BiasSpec`, whether the TV is on, and the
current circadian curve sample, :func:`bias_actions` decides the per-light action
for this tick. :class:`TriggerAggregator` folds on/off edges from the (I/O)
trigger sources into a single ``tv_on`` signal with optional debounce. This
-mirrors the pure/I-O split of :mod:`hue_iac.circadian_control`: everything here is
+mirrors the pure/I-O split of :mod:`hueman.circadian_control`: everything here is
a pure function of explicit inputs, so it is fully unit-tested without a bridge.
"""
diff --git a/hue_iac/circadian.py b/hueman/circadian.py
similarity index 100%
rename from hue_iac/circadian.py
rename to hueman/circadian.py
diff --git a/hue_iac/circadian_control.py b/hueman/circadian_control.py
similarity index 99%
rename from hue_iac/circadian_control.py
rename to hueman/circadian_control.py
index 4fa4b8f..f3b27b0 100644
--- a/hue_iac/circadian_control.py
+++ b/hueman/circadian_control.py
@@ -4,7 +4,7 @@
action for this tick — drive the zone to the current curve point, fade it off at
hand-off, or hold — and tracks the daemon mode. Time and events are explicit
arguments so the whole state machine is unit-tested without a bridge, mirroring
-:class:`hue_iac.engine.PolicyEngine`.
+:class:`hueman.engine.PolicyEngine`.
"""
from __future__ import annotations
diff --git a/hue_iac/circadian_daemon.py b/hueman/circadian_daemon.py
similarity index 98%
rename from hue_iac/circadian_daemon.py
rename to hueman/circadian_daemon.py
index 3bc24c8..dfecdd4 100644
--- a/hue_iac/circadian_daemon.py
+++ b/hueman/circadian_daemon.py
@@ -2,9 +2,9 @@
Where ``apply`` provisions native bridge resources, this module is the *daemon*
alternative: a persistent process that, every tick, asks the pure
-:class:`~hue_iac.circadian_control.CircadianController` what the zone should look
+:class:`~hueman.circadian_control.CircadianController` what the zone should look
like *right now* and writes that to the zone's ``grouped_light`` service with a
-smooth transition. It mirrors :class:`hue_iac.watch.MotionController` almost
+smooth transition. It mirrors :class:`hueman.watch.MotionController` almost
exactly — a tick thread plus an SSE event loop and a capped-backoff reconnect
loop — but for one zone and the circadian state machine instead of per-area
motion engines.
@@ -62,7 +62,7 @@
from .sun import SolarCalculator
from .watch import BridgeEvent, HueEventStream
-_LOG = logging.getLogger("hue_iac.circadian_daemon")
+_LOG = logging.getLogger("hueman.circadian_daemon")
class CircadianDaemon:
@@ -70,14 +70,14 @@ class CircadianDaemon:
Args:
client: An authenticated bridge client.
- state: A loaded :class:`~hue_iac.state.BridgeState` (used to resolve the
+ state: A loaded :class:`~hueman.state.BridgeState` (used to resolve the
zone's ``grouped_light`` id and an optional resume-trigger scene).
config: The parsed IaC configuration; ``config.circadian_daemon`` must
be present.
clock: Callable returning epoch seconds; injectable for testing.
sleep: Sleep function used for reconnect backoff; injectable for testing.
stream_factory: Builds the SSE stream each (re)connect; defaults to a
- :class:`~hue_iac.watch.HueEventStream` over ``client``.
+ :class:`~hueman.watch.HueEventStream` over ``client``.
"""
def __init__(
@@ -102,7 +102,7 @@ def __init__(
raise ConfigError(
"circadian_daemon.bias references light(s) not on the bridge: "
+ ", ".join(repr(name) for name in missing)
- + " — run `hue-iac inventory` to list available light names"
+ + " — run `hueman inventory` to list available light names"
)
self._bias_sse_on_rid = self._resolve_resume_trigger(state, spec.bias.sse_on)
self._bias_sse_off_rid = self._resolve_resume_trigger(state, spec.bias.sse_off)
@@ -114,7 +114,7 @@ def __init__(
raise ConfigError(
"security.groups references group(s) not on the bridge: "
+ ", ".join(repr(g) for g in missing)
- + " — run `hue-iac inventory` to list available group names"
+ + " — run `hueman inventory` to list available group names"
)
no_gl = [g for g in sec.groups if state.group(g).grouped_light_rid is None]
if no_gl:
@@ -321,7 +321,7 @@ def run(self, max_reconnects: int | None = None) -> None:
A daemon thread ticks the controller every ``interval`` seconds; the
main thread consumes the SSE stream inside a capped-backoff reconnect
loop, routing each event to :meth:`_handle_event`. Mirrors
- :meth:`hue_iac.watch.MotionController.run`'s stop/lock/backoff discipline.
+ :meth:`hueman.watch.MotionController.run`'s stop/lock/backoff discipline.
Args:
max_reconnects: Stop after this many connect attempts (for tests);
diff --git a/hue_iac/circadian_scene.py b/hueman/circadian_scene.py
similarity index 95%
rename from hue_iac/circadian_scene.py
rename to hueman/circadian_scene.py
index d45fc55..9472bbf 100644
--- a/hue_iac/circadian_scene.py
+++ b/hueman/circadian_scene.py
@@ -1,9 +1,9 @@
"""Pure generator for the smooth-circadian smart scene.
-Samples the sun-elevation circadian curve (:mod:`hue_iac.circadian`) at a handful
+Samples the sun-elevation circadian curve (:mod:`hueman.circadian`) at a handful
of afternoon-weighted *knee* times so a native ``smart_scene`` — capped by the
bridge at six timeslots — can fade continuously through the day instead of
-snapping between a few hand-built looks. I/O-free: :mod:`hue_iac.reconcile`
+snapping between a few hand-built looks. I/O-free: :mod:`hueman.reconcile`
owns the bridge writes.
Knees are weighted toward the afternoon decline: sunrise, solar noon, two
diff --git a/hue_iac/cli.py b/hueman/cli.py
similarity index 97%
rename from hue_iac/cli.py
rename to hueman/cli.py
index 39fbf7e..0ce6c97 100644
--- a/hue_iac/cli.py
+++ b/hueman/cli.py
@@ -1,4 +1,4 @@
-"""Command-line interface for hue-iac.
+"""Command-line interface for hueman.
Subcommands:
auth Pair with the bridge (press the link button) and print the key.
@@ -49,8 +49,8 @@ def __init__(self) -> None:
def _build_parser(self) -> argparse.ArgumentParser:
"""Construct the argument parser and its subcommands."""
- parser = argparse.ArgumentParser(prog="hue-iac", description="Declarative Philips Hue management.")
- parser.add_argument("--version", action="version", version=f"hue-iac {__version__}")
+ parser = argparse.ArgumentParser(prog="hueman", description="Declarative Philips Hue management.")
+ parser.add_argument("--version", action="version", version=f"hueman {__version__}")
parser.add_argument(
"-c", "--config", default="hue.yaml", help="path to the config file (default: hue.yaml)"
)
@@ -188,7 +188,7 @@ def _cmd_watch(self, args: argparse.Namespace) -> int:
state = BridgeState(client).load()
controller = MotionController(client, state, config, dry_run=args.dry_run)
mode = " (dry-run)" if args.dry_run else ""
- print(f"hue-iac watching {len(config.motion_policies)} policy/policies{mode}; Ctrl-C to stop.")
+ print(f"hueman watching {len(config.motion_policies)} policy/policies{mode}; Ctrl-C to stop.")
try:
controller.run()
except KeyboardInterrupt:
@@ -211,7 +211,7 @@ def _cmd_circadian(self, args: argparse.Namespace) -> int:
client = HueClient(config.bridge)
state = BridgeState(client).load()
daemon = CircadianDaemon(client, state, config)
- print(f"hue-iac circadian daemon driving '{config.circadian_daemon.zone}'; Ctrl-C to stop.")
+ print(f"hueman circadian daemon driving '{config.circadian_daemon.zone}'; Ctrl-C to stop.")
try:
daemon.run()
except KeyboardInterrupt:
@@ -357,7 +357,7 @@ def _confirm(count: int) -> bool:
def main(argv: list[str] | None = None) -> int:
- """Module entry point used by the ``hue-iac`` console script."""
+ """Module entry point used by the ``hueman`` console script."""
return Cli().run(argv)
diff --git a/hue_iac/client.py b/hueman/client.py
similarity index 97%
rename from hue_iac/client.py
rename to hueman/client.py
index e394310..cf0d7e8 100644
--- a/hue_iac/client.py
+++ b/hueman/client.py
@@ -2,7 +2,7 @@
Covers exactly what the IaC workflow needs: link-button key creation, typed
resource GET/POST/PUT/DELETE, and a couple of convenience lookups. TLS identity
-is established up-front via :mod:`hue_iac.pin` (trust-on-first-use) or a CA
+is established up-front via :mod:`hueman.pin` (trust-on-first-use) or a CA
bundle; we never blindly disable verification without having pinned first.
API surface used here is the documented v2 model
@@ -21,7 +21,7 @@
from .errors import AuthError, BridgeError
from .pin import verify_or_pin
-_KEY_CREATE_DEVICETYPE = "hue-iac#cli"
+_KEY_CREATE_DEVICETYPE = "hueman#cli"
class _FingerprintAdapter(HTTPAdapter):
@@ -84,7 +84,7 @@ def _url(self, path: str) -> str:
def _request(self, method: str, path: str, *, json_body=None, need_key: bool = True) -> dict:
if need_key and not self.bridge.application_key:
- raise AuthError("no application key; run 'hue-iac auth' to pair with the bridge")
+ raise AuthError("no application key; run 'hueman auth' to pair with the bridge")
try:
resp = self._session.request(
method, self._url(path), json=json_body, timeout=self.timeout
diff --git a/hue_iac/config.py b/hueman/config.py
similarity index 99%
rename from hue_iac/config.py
rename to hueman/config.py
index 2e66419..37e6cff 100644
--- a/hue_iac/config.py
+++ b/hueman/config.py
@@ -1,6 +1,6 @@
"""Declarative configuration model and loader.
-This is the user-facing "language" of hue-iac. A YAML document is parsed into a
+This is the user-facing "language" of hueman. A YAML document is parsed into a
tree of frozen dataclasses with eager validation, so an invalid config fails at
``validate``/``plan`` time with a precise message rather than mid-apply against
the live bridge.
@@ -61,7 +61,7 @@ def parse_time_ref(value: Any, *, ctx: str) -> str:
"""Validate a time reference: ``HH:MM``, ``sunrise``, or ``sunset``.
Returned verbatim; resolution to a concrete minute happens later against
- the day's :class:`~hue_iac.sun.SunTimes` so the same config works all year.
+ the day's :class:`~hueman.sun.SunTimes` so the same config works all year.
"""
if value in ("sunrise", "sunset"):
return value
diff --git a/hue_iac/engine.py b/hueman/engine.py
similarity index 98%
rename from hue_iac/engine.py
rename to hueman/engine.py
index 987b3f3..a0acd3f 100644
--- a/hue_iac/engine.py
+++ b/hueman/engine.py
@@ -3,7 +3,7 @@
This is the brain that turns sensor events into light commands. It is kept free
of any I/O: it consumes already-classified events (motion, ambient light level,
a manual-override signal, and clock ticks) and emits :class:`Action` values that
-the runtime (:mod:`hue_iac.watch`) executes against the bridge. Time is passed
+the runtime (:mod:`hueman.watch`) executes against the bridge. Time is passed
in as an explicit epoch-seconds float so tests drive it directly.
Why a custom engine instead of the bridge's built-in motion behaviour? Three of
@@ -150,7 +150,7 @@ def current_timeslot(self, ts: float) -> Timeslot:
ts: Epoch seconds of the moment to evaluate.
Returns:
- The :class:`~hue_iac.config.Timeslot` currently in effect.
+ The :class:`~hueman.config.Timeslot` currently in effect.
"""
sun = self._sun_for(self._local_dt(ts).date())
now = self._minute_of_day(ts)
diff --git a/hue_iac/errors.py b/hueman/errors.py
similarity index 95%
rename from hue_iac/errors.py
rename to hueman/errors.py
index 557b30a..34e5a5b 100644
--- a/hue_iac/errors.py
+++ b/hueman/errors.py
@@ -1,4 +1,4 @@
-"""Exception hierarchy for hue-iac.
+"""Exception hierarchy for hueman.
Everything user-facing inherits from :class:`HueIacError` so the CLI can catch a
single type, print a clean message, and exit non-zero without a traceback.
diff --git a/hue_iac/nightmotion.py b/hueman/nightmotion.py
similarity index 99%
rename from hue_iac/nightmotion.py
rename to hueman/nightmotion.py
index e8d9724..a903a9d 100644
--- a/hue_iac/nightmotion.py
+++ b/hueman/nightmotion.py
@@ -5,7 +5,7 @@
config so every timeslot targets the whole-apartment zone, with the night
timeslot rewritten to recall the soft-red scene and switch off quickly.
-The reconciler in :mod:`hue_iac.reconcile` owns the bridge I/O; this module only
+The reconciler in :mod:`hueman.reconcile` owns the bridge I/O; this module only
produces request bodies from inputs.
"""
diff --git a/hue_iac/payload.py b/hueman/payload.py
similarity index 100%
rename from hue_iac/payload.py
rename to hueman/payload.py
diff --git a/hue_iac/pin.py b/hueman/pin.py
similarity index 97%
rename from hue_iac/pin.py
rename to hueman/pin.py
index 38e4226..43c63e6 100644
--- a/hue_iac/pin.py
+++ b/hueman/pin.py
@@ -10,7 +10,7 @@
This module only captures/loads the pinned fingerprint (a raw TLS handshake at
startup, TOFU on first run). Enforcement is NOT a one-shot pre-flight check —
that alone would leave a TOCTOU window between the probe and the real requests.
-Instead :class:`hue_iac.client._FingerprintAdapter` passes the fingerprint to
+Instead :class:`hueman.client._FingerprintAdapter` passes the fingerprint to
urllib3 as ``assert_fingerprint``, so *every* connection the session opens
(including the long-lived SSE stream) verifies the leaf certificate against the
pin at handshake time.
diff --git a/hue_iac/reconcile.py b/hueman/reconcile.py
similarity index 99%
rename from hue_iac/reconcile.py
rename to hueman/reconcile.py
index 1496b9a..c9f5b22 100644
--- a/hue_iac/reconcile.py
+++ b/hueman/reconcile.py
@@ -8,7 +8,7 @@
The only persistent bridge resource the motion policies own is the motion
sensor's *sensitivity*; the live colour/timing behaviour is enforced at runtime
-by :mod:`hue_iac.watch`. Keeping sensitivity here means a single ``apply`` fully
+by :mod:`hueman.watch`. Keeping sensitivity here means a single ``apply`` fully
provisions the declarative surface, and the reconciler base class leaves room
for additional resource kinds (such as scenes) without changing the CLI.
"""
@@ -86,7 +86,7 @@ class Reconciler(abc.ABC):
Args:
client: An authenticated bridge client.
- state: A loaded :class:`~hue_iac.state.BridgeState`.
+ state: A loaded :class:`~hueman.state.BridgeState`.
config: The parsed IaC configuration.
"""
@@ -647,8 +647,8 @@ def _backup(self, inst: dict) -> None:
class CircadianSceneReconciler(Reconciler):
"""Generates a smooth, sun-anchored circadian ``smart_scene`` from the curve.
- Samples :mod:`hue_iac.circadian` at the day's knee times (via
- :func:`hue_iac.circadian_scene.circadian_timeslots`) into up to six zone scenes
+ Samples :mod:`hueman.circadian` at the day's knee times (via
+ :func:`hueman.circadian_scene.circadian_timeslots`) into up to six zone scenes
and wires them to a ``smart_scene`` whose long ``transition_duration`` makes the
bridge fade continuously between them — a native, daemon-free circadian cycle
that re-anchors to the real sun on every apply. Built on the same scene/backup
@@ -912,7 +912,7 @@ class Planner:
Args:
client: An authenticated bridge client.
- state: A loaded :class:`~hue_iac.state.BridgeState`.
+ state: A loaded :class:`~hueman.state.BridgeState`.
config: The parsed IaC configuration.
"""
diff --git a/hue_iac/security_control.py b/hueman/security_control.py
similarity index 98%
rename from hue_iac/security_control.py
rename to hueman/security_control.py
index bbe1e11..f4cabc3 100644
--- a/hue_iac/security_control.py
+++ b/hueman/security_control.py
@@ -1,6 +1,6 @@
"""Pure decision core for the daemon's security mode (no clock, no I/O).
-Given the parsed :class:`~hue_iac.config.SecuritySpec` and an injected elapsed
+Given the parsed :class:`~hueman.config.SecuritySpec` and an injected elapsed
time + frame index, decides the light frame for an escalating ALERT -> CHAOS
show. The safety cap is **luminance-only**: a unit's brightness changes no
faster than ``min_flash_interval`` (out of the 3-30 Hz photosensitive-seizure
@@ -10,8 +10,8 @@
in a rotating budget (``lights_per_frame``) instead of whole groups, so the
apartment churns as a decorrelated patchwork. The ALERT phase stays whole-group
and legible. Everything here is a pure function of explicit inputs, so it is
-fully unit-tested without a bridge, mirroring :mod:`hue_iac.bias_control` and
-:mod:`hue_iac.circadian_control`.
+fully unit-tested without a bridge, mirroring :mod:`hueman.bias_control` and
+:mod:`hueman.circadian_control`.
"""
from __future__ import annotations
diff --git a/hue_iac/state.py b/hueman/state.py
similarity index 99%
rename from hue_iac/state.py
rename to hueman/state.py
index 6ed2efd..5b52d68 100644
--- a/hue_iac/state.py
+++ b/hueman/state.py
@@ -106,7 +106,7 @@ class BridgeState:
"""Loads and indexes the bridge resources the tool operates on.
Args:
- client: An authenticated :class:`~hue_iac.client.HueClient`.
+ client: An authenticated :class:`~hueman.client.HueClient`.
"""
def __init__(self, client: HueClient) -> None:
diff --git a/hue_iac/sun.py b/hueman/sun.py
similarity index 100%
rename from hue_iac/sun.py
rename to hueman/sun.py
diff --git a/hue_iac/watch.py b/hueman/watch.py
similarity index 98%
rename from hue_iac/watch.py
rename to hueman/watch.py
index 0d65bc3..67b364a 100644
--- a/hue_iac/watch.py
+++ b/hueman/watch.py
@@ -14,7 +14,7 @@
``plan``/``apply`` provision the declarative surface; this module is the live
controller. It subscribes to the CLIP v2 Server-Sent Events stream
(``/eventstream/clip/v2``), feeds motion, light-level and manual-control events
-into the per-area :class:`~hue_iac.engine.PolicyEngine` instances, and writes the
+into the per-area :class:`~hueman.engine.PolicyEngine` instances, and writes the
resulting commands back to the relevant ``grouped_light`` services.
Manual-override detection is a trust heuristic: every command we send is
@@ -25,7 +25,7 @@
This is the one module that genuinely needs the hardware to exercise end to end,
so its decision logic is delegated to the pure, tested engine and its
-serialisation to the pure, tested :mod:`hue_iac.payload`; what remains here is
+serialisation to the pure, tested :mod:`hueman.payload`; what remains here is
event plumbing.
"""
@@ -48,7 +48,7 @@
from .payload import GroupedLightCommand
from .state import BridgeState
-_LOG = logging.getLogger("hue_iac.watch")
+_LOG = logging.getLogger("hueman.watch")
#: How long a command we issued stays in the echo buffer before a matching
#: bridge event is assumed to be a human action instead of our own write.
@@ -107,7 +107,7 @@ def events(self) -> Iterator[BridgeEvent]:
if response.status_code in (401, 403):
raise AuthError(
f"bridge rejected the application key (HTTP {response.status_code}); "
- "re-run 'hue-iac auth'"
+ "re-run 'hueman auth'"
)
response.raise_for_status() # other 4xx/5xx -> retryable HTTPError
for line in response.iter_lines(decode_unicode=True):
@@ -143,7 +143,7 @@ class MotionController:
Args:
client: An authenticated bridge client.
- state: A loaded :class:`~hue_iac.state.BridgeState`.
+ state: A loaded :class:`~hueman.state.BridgeState`.
config: The parsed IaC configuration.
clock: Callable returning epoch seconds; injectable for testing.
dry_run: When ``True``, log actions instead of writing to the bridge.
diff --git a/pyproject.toml b/pyproject.toml
index 3b9067d..02df3ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,12 +3,12 @@ requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
-name = "hue-iac"
+name = "hueman"
version = "0.1.0"
description = "Declarative, Terraform-style management of Philips Hue lighting policies with circadian, motion and timing optimisation."
readme = "README.md"
requires-python = ">=3.10"
-license = { text = "MIT" }
+license = "MIT"
dependencies = [
"requests>=2.28",
"PyYAML>=6.0",
@@ -21,10 +21,11 @@ dev = [
]
[project.scripts]
-hue-iac = "hue_iac.cli:main"
+hueman = "hueman.cli:main"
+hue-iac = "hueman.cli:main" # transitional alias, remove after one release
[tool.setuptools.packages.find]
-include = ["hue_iac*"]
+include = ["hueman*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
diff --git a/tests/conftest.py b/tests/conftest.py
index 18c5e89..ab74456 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,7 +4,7 @@
import datetime as _dt
-from hue_iac.config import Config
+from hueman.config import Config
TZ_OFFSET = -5.0
diff --git a/tests/test_areas.py b/tests/test_areas.py
index 6a5aed9..a750517 100644
--- a/tests/test_areas.py
+++ b/tests/test_areas.py
@@ -4,10 +4,10 @@
import pytest
-from hue_iac.config import Config
-from hue_iac.errors import ConfigError
-from hue_iac.reconcile import AreaReconciler, ChangeType
-from hue_iac.state import BridgeState
+from hueman.config import Config
+from hueman.errors import ConfigError
+from hueman.reconcile import AreaReconciler, ChangeType
+from hueman.state import BridgeState
class FakeClient:
diff --git a/tests/test_bias_control.py b/tests/test_bias_control.py
index 11c33e9..ba763bd 100644
--- a/tests/test_bias_control.py
+++ b/tests/test_bias_control.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from hue_iac.bias_control import (
+from hueman.bias_control import (
BiasDrive,
BiasHold,
BiasOff,
@@ -10,8 +10,8 @@
bias_actions,
unknown_bias_lights,
)
-from hue_iac.circadian_control import DriveTo
-from hue_iac.config import BiasLight, BiasSpec, Color, LightState
+from hueman.circadian_control import DriveTo
+from hueman.config import BiasLight, BiasSpec, Color, LightState
def _spec(*lights: tuple[str, str]) -> BiasSpec:
diff --git a/tests/test_circadian.py b/tests/test_circadian.py
index edacaa5..24be4ba 100644
--- a/tests/test_circadian.py
+++ b/tests/test_circadian.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from hue_iac.circadian import CircadianCurve, CircadianParams
+from hueman.circadian import CircadianCurve, CircadianParams
P = CircadianParams()
NOON = 65.0 # a representative solar-noon elevation (degrees)
diff --git a/tests/test_circadian_control.py b/tests/test_circadian_control.py
index a448f69..2247f22 100644
--- a/tests/test_circadian_control.py
+++ b/tests/test_circadian_control.py
@@ -1,11 +1,11 @@
from __future__ import annotations
import datetime as _dt
-from hue_iac.circadian import CircadianParams
-from hue_iac.circadian_control import CircadianController, DriveTo, FadeOff, Hold
-from hue_iac.config import CircadianDaemonSpec
-from hue_iac.config import Anchor
-from hue_iac.sun import SolarCalculator
+from hueman.circadian import CircadianParams
+from hueman.circadian_control import CircadianController, DriveTo, FadeOff, Hold
+from hueman.config import CircadianDaemonSpec
+from hueman.config import Anchor
+from hueman.sun import SolarCalculator
TZ = -7.0 # PDT, matches the test date
SOLAR = SolarCalculator(45.5152, -122.6784, TZ) # Portland
diff --git a/tests/test_circadian_daemon.py b/tests/test_circadian_daemon.py
index e5e092f..5d30a22 100644
--- a/tests/test_circadian_daemon.py
+++ b/tests/test_circadian_daemon.py
@@ -6,10 +6,10 @@
import pytest
import requests
-from hue_iac.circadian_daemon import CircadianDaemon, _LOG
-from hue_iac.config import Config
-from hue_iac.errors import AuthError, BridgeError
-from hue_iac.watch import BridgeEvent
+from hueman.circadian_daemon import CircadianDaemon, _LOG
+from hueman.config import Config
+from hueman.errors import AuthError, BridgeError
+from hueman.watch import BridgeEvent
@pytest.fixture(autouse=True)
@@ -253,7 +253,7 @@ def test_init_raises_bridge_error_if_zone_has_no_grouped_light():
Without this guard every write silently fails (PUT to a None rid is a no-op)
and the daemon runs indefinitely without driving any lights.
"""
- from hue_iac.state import Group
+ from hueman.state import Group
class _StateNoGl:
def group(self, name: str) -> Group:
@@ -273,8 +273,8 @@ def test_init_raises_clear_error_listing_all_unknown_bias_lights():
"""A bias light absent from the bridge fails fast at startup, naming *every* bad
name at once, instead of crash-looping on a cryptic first-name resolution error.
"""
- from hue_iac.errors import ConfigError
- from hue_iac.state import Group, LightRef
+ from hueman.errors import ConfigError
+ from hueman.state import Group, LightRef
class _State:
def group(self, name):
@@ -607,7 +607,7 @@ def test_bias_edge_logs_flip_at_info(caplog):
cfg = _cfg_bias({"probe": {"enabled": True, "host": "192.0.2.50", "debounce": "0s"}})
d = CircadianDaemon.for_test(_FakeClient(), cfg, grouped_light_rid="GL")
d._bias_rids = {"Couch": "Lcouch"}
- caplog.set_level(logging.INFO, logger="hue_iac.circadian_daemon")
+ caplog.set_level(logging.INFO, logger="hueman.circadian_daemon")
t = _epoch(13, 14)
d._tick_once(t)
import unittest.mock as _mock
@@ -624,7 +624,7 @@ def test_bias_edge_logs_flip_at_info(caplog):
def test_tick_logs_drive_at_info(caplog):
"""A daytime tick emits an INFO record mentioning 'drive' and the zone name."""
daemon = CircadianDaemon.for_test(_FakeClient(), _cfg(), grouped_light_rid="GL")
- caplog.set_level(logging.INFO, logger="hue_iac.circadian_daemon")
+ caplog.set_level(logging.INFO, logger="hueman.circadian_daemon")
daemon._tick_once(_epoch(13, 14))
assert any(
r.levelno == logging.INFO and "drive" in r.message and "Night Guide" in r.message
@@ -635,7 +635,7 @@ def test_tick_logs_drive_at_info(caplog):
def test_external_event_logs_override(caplog):
"""A brightness that SETTLES far from target logs INFO 'override' + 'suspended'."""
daemon = CircadianDaemon.for_test(_FakeClient(), _cfg(), grouped_light_rid="GL")
- caplog.set_level(logging.INFO, logger="hue_iac.circadian_daemon")
+ caplog.set_level(logging.INFO, logger="hueman.circadian_daemon")
t0 = _epoch(12, 0)
daemon._tick_once(t0)
daemon._handle_event(_dim(30.0), t0 + 0.4) # moves (from None)
@@ -795,8 +795,8 @@ def test_security_control_file_arms(tmp_path):
def test_init_raises_listing_unknown_security_groups():
- from hue_iac.errors import ConfigError
- from hue_iac.state import Group
+ from hueman.errors import ConfigError
+ from hueman.state import Group
class _State:
def group(self, name):
@@ -1016,7 +1016,7 @@ def update_resource(self, rtype, rid, body):
_FlakyClient(), _cfg_security(alert_seconds=1, max_duration="2s"),
grouped_light_rid="GL")
d._security_rids = {"Night Guide": "GLng", "TV Viewing": "GLtv"}
- caplog.set_level(_logging.INFO, logger="hue_iac.circadian_daemon")
+ caplog.set_level(_logging.INFO, logger="hueman.circadian_daemon")
monkeypatch.setattr(d._stop_event, "wait", lambda *a, **k: None)
start = _epoch(2, 0)
seq = iter(start + 0.25 * i for i in range(0, 400))
diff --git a/tests/test_circadian_scene.py b/tests/test_circadian_scene.py
index 770ce9b..06fac1c 100644
--- a/tests/test_circadian_scene.py
+++ b/tests/test_circadian_scene.py
@@ -5,13 +5,13 @@
import datetime as _dt
import pytest
-from hue_iac.circadian import CircadianCurve, CircadianParams
-from hue_iac.circadian_scene import MAX_TIMESLOTS, circadian_timeslots
-from hue_iac.config import Config
-from hue_iac.nightmotion import scene_body
-from hue_iac.reconcile import ChangeType, CircadianSceneReconciler
-from hue_iac.state import BridgeState
-from hue_iac.sun import SolarCalculator
+from hueman.circadian import CircadianCurve, CircadianParams
+from hueman.circadian_scene import MAX_TIMESLOTS, circadian_timeslots
+from hueman.config import Config
+from hueman.nightmotion import scene_body
+from hueman.reconcile import ChangeType, CircadianSceneReconciler
+from hueman.state import BridgeState
+from hueman.sun import SolarCalculator
SOLAR = SolarCalculator(40.7128, -74.0060, -4) # NYC, EDT
PARAMS = CircadianParams() # day 233/100, evening 370/60, night 454/15
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 92e267e..83232f3 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -4,13 +4,13 @@
import pytest
-from hue_iac.cli import Cli
-from hue_iac.errors import HueIacError
-from hue_iac.reconcile import Change, ChangeType
+from hueman.cli import Cli
+from hueman.errors import HueIacError
+from hueman.reconcile import Change, ChangeType
def test_circadian_resume_writes_control_file(tmp_path):
- import hue_iac.cli as climod
+ import hueman.cli as climod
cfgfile = tmp_path / "hue.yaml"
ctrl = tmp_path / ".resume"
cfgfile.write_text(
@@ -24,7 +24,7 @@ def test_circadian_resume_writes_control_file(tmp_path):
def test_circadian_parser_has_run_and_resume():
- from hue_iac.cli import Cli
+ from hueman.cli import Cli
args = Cli()._build_parser().parse_args(["circadian", "run"])
assert args.command == "circadian" and args.circadian_cmd == "run"
diff --git a/tests/test_client.py b/tests/test_client.py
index 9ecd750..093d550 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -9,9 +9,9 @@
import pytest
-from hue_iac import client as client_mod
-from hue_iac.client import HueClient
-from hue_iac.config import Bridge, TlsConfig
+from hueman import client as client_mod
+from hueman.client import HueClient
+from hueman.config import Bridge, TlsConfig
FP = "9142" + "0" * 60 # 64 hex chars
@@ -56,7 +56,7 @@ def test_create_application_key_non_json_response_is_clean_error(monkeypatch):
CLI can print, not a raw requests JSONDecodeError traceback."""
import requests
- from hue_iac.errors import HueIacError
+ from hueman.errors import HueIacError
monkeypatch.setattr(client_mod, "verify_or_pin", lambda host, pin_file: FP)
c = HueClient(_bridge("pin"))
diff --git a/tests/test_config.py b/tests/test_config.py
index 445748e..0e1588c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -4,8 +4,8 @@
import pytest
-from hue_iac.config import Color, Config, SecuritySpec, parse_duration, parse_time_ref
-from hue_iac.errors import ConfigError
+from hueman.config import Color, Config, SecuritySpec, parse_duration, parse_time_ref
+from hueman.errors import ConfigError
from tests.conftest import make_config
@@ -89,7 +89,7 @@ def _cfg_doc(extra: dict) -> dict:
def test_circadian_scene_parses_full() -> None:
"""A full circadian_scene block parses into a typed spec."""
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_scene": {
"smart_scene": "Golden hours", "zone": "Night Guide",
@@ -104,7 +104,7 @@ def test_circadian_scene_parses_full() -> None:
def test_circadian_scene_explicit_transition_and_default_handoff() -> None:
"""An explicit duration parses to ms; hand_off defaults to 22:34."""
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_scene": {
"smart_scene": "X", "zone": "Z", "transition": "90m"}}))
@@ -115,7 +115,7 @@ def test_circadian_scene_explicit_transition_and_default_handoff() -> None:
def test_circadian_scene_requires_zone() -> None:
"""A circadian_scene without a zone is rejected."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"circadian_scene": {"smart_scene": "X"}}))
@@ -123,7 +123,7 @@ def test_circadian_scene_requires_zone() -> None:
def test_circadian_scene_rejects_sun_handoff() -> None:
"""hand_off must be a clock time, not a sun anchor."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"circadian_scene": {
@@ -132,7 +132,7 @@ def test_circadian_scene_rejects_sun_handoff() -> None:
def test_no_circadian_scene_is_none() -> None:
"""Omitting circadian_scene leaves it unset."""
- from hue_iac.config import Config
+ from hueman.config import Config
assert Config.parse(_cfg_doc({})).circadian_scene is None
@@ -151,14 +151,14 @@ def test_duplicate_policy_names_rejected() -> None:
"motion_policies": [base, dict(base)],
}
with pytest.raises(ConfigError):
- from hue_iac.config import Config
+ from hueman.config import Config
Config.parse(doc)
def test_location_tz_only_backfills_a_float_offset() -> None:
"""`tz` without tz_offset_hours parses; offset is back-filled to a float."""
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc(
{"location": {"lat": 45.5, "lon": -122.7, "tz": "America/Los_Angeles"}}
@@ -169,7 +169,7 @@ def test_location_tz_only_backfills_a_float_offset() -> None:
def test_location_tz_and_offset_both_retained() -> None:
"""When both are given, both are kept (tz drives per-date derivation downstream)."""
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc(
{"location": {"lat": 45.5, "lon": -122.7, "tz_offset_hours": -7, "tz": "America/Los_Angeles"}}
@@ -180,7 +180,7 @@ def test_location_tz_and_offset_both_retained() -> None:
def test_location_unknown_tz_rejected() -> None:
"""A bogus IANA name is a config error, not a runtime crash."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"location": {"lat": 45.5, "lon": -122.7, "tz": "Not/A_Zone"}}))
@@ -188,7 +188,7 @@ def test_location_unknown_tz_rejected() -> None:
def test_location_requires_tz_or_offset() -> None:
"""Neither tz nor tz_offset_hours -> ConfigError (unchanged contract)."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"location": {"lat": 45.5, "lon": -122.7}}))
@@ -196,7 +196,7 @@ def test_location_requires_tz_or_offset() -> None:
def test_location_empty_or_malformed_tz_rejected() -> None:
"""Empty string and path-like tz values are ConfigErrors, not ValueError crashes."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"location": {"lat": 45.5, "lon": -122.7, "tz": ""}}))
@@ -207,14 +207,14 @@ def test_location_empty_or_malformed_tz_rejected() -> None:
def test_location_tz_must_be_a_string() -> None:
"""A non-string tz (e.g. an integer) is a ConfigError."""
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"location": {"lat": 45.5, "lon": -122.7, "tz": 42}}))
def test_circadian_daemon_defaults():
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_daemon": {"zone": "Night Guide"}}))
d = cfg.circadian_daemon
assert d.zone == "Night Guide"
@@ -240,7 +240,7 @@ def test_circadian_daemon_defaults():
def test_circadian_daemon_overrides_and_anchor():
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_daemon": {
"zone": "Z", "start": "sunrise+30m", "hand_off": "23:00",
"interval": "90s", "transition": "120s", "fade_off": "2m",
@@ -268,13 +268,13 @@ def test_circadian_daemon_overrides_and_anchor():
def test_circadian_daemon_requires_zone():
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError):
Config.parse(_cfg_doc({"circadian_daemon": {}}))
def test_circadian_daemon_absent_is_none():
- from hue_iac.config import Config
+ from hueman.config import Config
assert Config.parse(_cfg_doc({})).circadian_daemon is None
@@ -283,13 +283,13 @@ def test_circadian_daemon_absent_is_none():
# --------------------------------------------------------------------------- #
def _daemon_bias(bias: dict):
"""Parse a config whose circadian_daemon carries the given bias block."""
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_daemon": {"zone": "Night Guide", "bias": bias}}))
return cfg.circadian_daemon.bias
def test_bias_parses_full() -> None:
- from hue_iac.config import BiasSpec
+ from hueman.config import BiasSpec
bias = _daemon_bias({
"lights": {
"Play bars": {"look": {"mirek": 153, "brightness": 28}, "idle": "off"},
@@ -365,7 +365,7 @@ def test_bias_edge_transition_explicit() -> None:
def test_bias_absent_is_none() -> None:
- from hue_iac.config import Config
+ from hueman.config import Config
cfg = Config.parse(_cfg_doc({"circadian_daemon": {"zone": "Night Guide"}}))
assert cfg.circadian_daemon.bias is None
@@ -486,7 +486,7 @@ def test_circadian_night_start_is_rejected_as_removed() -> None:
loudly instead of silently doing nothing."""
doc = _cfg_doc({})
doc["circadian"] = {"day_mirek": 233, "night_start": "23:00"}
- from hue_iac.config import Config
+ from hueman.config import Config
with pytest.raises(ConfigError, match="night_start"):
Config.parse(doc)
diff --git a/tests/test_engine.py b/tests/test_engine.py
index 58392c6..2fd808c 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -7,7 +7,7 @@
from __future__ import annotations
-from hue_iac.engine import Phase, PolicyEngine
+from hueman.engine import Phase, PolicyEngine
from tests.conftest import epoch_at, make_config
LOCATION = (40.7128, -74.0060, -5.0)
diff --git a/tests/test_nightmotion.py b/tests/test_nightmotion.py
index 41d1c33..b71ec61 100644
--- a/tests/test_nightmotion.py
+++ b/tests/test_nightmotion.py
@@ -8,11 +8,11 @@
import pytest
-from hue_iac.config import Config
-from hue_iac.errors import ConfigError
-from hue_iac.nightmotion import scene_actions_match, scene_body, transform_automation
-from hue_iac.reconcile import ChangeType, NightMotionReconciler
-from hue_iac.state import BridgeState
+from hueman.config import Config
+from hueman.errors import ConfigError
+from hueman.nightmotion import scene_actions_match, scene_body, transform_automation
+from hueman.reconcile import ChangeType, NightMotionReconciler
+from hueman.state import BridgeState
FIXTURE = json.loads(
(pathlib.Path(__file__).parent / "fixtures" / "main_room_behavior.json").read_text()
diff --git a/tests/test_payload.py b/tests/test_payload.py
index 18916a6..2f4ef22 100644
--- a/tests/test_payload.py
+++ b/tests/test_payload.py
@@ -2,8 +2,8 @@
from __future__ import annotations
-from hue_iac.engine import TargetState
-from hue_iac.payload import ColorConverter, GroupedLightCommand
+from hueman.engine import TargetState
+from hueman.payload import ColorConverter, GroupedLightCommand
def test_red_hex_maps_to_red_corner_of_gamut() -> None:
@@ -61,7 +61,7 @@ def test_build_without_transition_unchanged() -> None:
def test_light_command_matches_grouped_body() -> None:
"""Per-light writes reuse the same body shape as grouped_light."""
- from hue_iac.payload import LightCommand
+ from hueman.payload import LightCommand
t = TargetState(on=True, brightness=28.0, mirek=153, hex=None)
assert LightCommand.build(t, transition_ms=500) == GroupedLightCommand.build(t, transition_ms=500)
@@ -74,7 +74,7 @@ def test_light_command_matches_grouped_body() -> None:
def test_light_command_hex_and_off() -> None:
- from hue_iac.payload import LightCommand
+ from hueman.payload import LightCommand
assert "color" in LightCommand.build(TargetState(on=True, brightness=5.0, hex="1a0a00"))
assert LightCommand.build(TargetState.off(), transition_ms=90000) == {
diff --git a/tests/test_pin.py b/tests/test_pin.py
index e968c2c..9937c3b 100644
--- a/tests/test_pin.py
+++ b/tests/test_pin.py
@@ -10,8 +10,8 @@
import pytest
-from hue_iac import pin
-from hue_iac.errors import PinError
+from hueman import pin
+from hueman.errors import PinError
FP_A = "a" * 64
FP_B = "b" * 64
diff --git a/tests/test_scenes.py b/tests/test_scenes.py
index 851d2c6..5c255ee 100644
--- a/tests/test_scenes.py
+++ b/tests/test_scenes.py
@@ -4,10 +4,10 @@
import pytest
-from hue_iac.config import Color, Config, LightState
-from hue_iac.errors import ConfigError
-from hue_iac.reconcile import ChangeType, Planner, SceneReconciler
-from hue_iac.state import BridgeState
+from hueman.config import Color, Config, LightState
+from hueman.errors import ConfigError
+from hueman.reconcile import ChangeType, Planner, SceneReconciler
+from hueman.state import BridgeState
def _doc(scenes):
diff --git a/tests/test_security_control.py b/tests/test_security_control.py
index 1fd324a..c142a11 100644
--- a/tests/test_security_control.py
+++ b/tests/test_security_control.py
@@ -2,8 +2,8 @@
from __future__ import annotations
-from hue_iac.config import Config
-from hue_iac.security_control import (
+from hueman.config import Config
+from hueman.security_control import (
PHASE_ALERT,
PHASE_CHAOS,
SecurityController,
diff --git a/tests/test_smartscene.py b/tests/test_smartscene.py
index 8499d70..f241e95 100644
--- a/tests/test_smartscene.py
+++ b/tests/test_smartscene.py
@@ -6,11 +6,11 @@
import pytest
-from hue_iac.config import Anchor, Config
-from hue_iac.errors import ConfigError, HueIacError
-from hue_iac.reconcile import Change, ChangeType, SmartSceneReconciler
-from hue_iac.state import BridgeState
-from hue_iac.sun import SolarCalculator
+from hueman.config import Anchor, Config
+from hueman.errors import ConfigError, HueIacError
+from hueman.reconcile import Change, ChangeType, SmartSceneReconciler
+from hueman.state import BridgeState
+from hueman.sun import SolarCalculator
def _doc(smart_scenes):
diff --git a/tests/test_state.py b/tests/test_state.py
index 095df3a..263043a 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -8,7 +8,7 @@
from __future__ import annotations
-from hue_iac.state import BridgeState
+from hueman.state import BridgeState
class FakeClient:
diff --git a/tests/test_sun.py b/tests/test_sun.py
index 53fe6ac..34bd52e 100644
--- a/tests/test_sun.py
+++ b/tests/test_sun.py
@@ -6,7 +6,7 @@
import pytest
-from hue_iac.sun import SolarCalculator, zone_offset_hours
+from hueman.sun import SolarCalculator, zone_offset_hours
def test_summer_solstice_nyc_sunrise_sunset_reasonable() -> None:
diff --git a/tests/test_watch.py b/tests/test_watch.py
index 23db544..4846448 100644
--- a/tests/test_watch.py
+++ b/tests/test_watch.py
@@ -10,11 +10,11 @@
import pytest
import requests
-from hue_iac.config import Config
-from hue_iac.engine import Action, Phase, TargetState
-from hue_iac.errors import AuthError, BridgeError
-from hue_iac.state import BridgeState
-from hue_iac.watch import BridgeEvent, MotionController
+from hueman.config import Config
+from hueman.engine import Action, Phase, TargetState
+from hueman.errors import AuthError, BridgeError
+from hueman.state import BridgeState
+from hueman.watch import BridgeEvent, MotionController
GL = "gl1" # grouped_light rid for the Office room
MOT = "mot1" # motion service rid