Metadata-Version: 2.4
Name: neopi
Version: 0.1.14
Summary: NeoPixel animation scenes and a friendly Raspberry Pi web controller
License-Expression: MIT AND Apache-2.0
Project-URL: Homepage, https://github.com/tvarovski/neopi
Project-URL: Documentation, https://github.com/tvarovski/neopi/tree/main/docs
Project-URL: Repository, https://github.com/tvarovski/neopi
Project-URL: Issues, https://github.com/tvarovski/neopi/issues
Project-URL: Releases, https://github.com/tvarovski/neopi/releases
Keywords: neopixel,raspberry-pi,led,lighting,fastapi
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSES/Apache-2.0.txt
License-File: THIRD_PARTY_NOTICES.md
Requires-Dist: fastapi<1,>=0.110
Requires-Dist: pydantic<3,>=2
Requires-Dist: uvicorn[standard]<1,>=0.29
Provides-Extra: hardware
Requires-Dist: Adafruit-Blinka>=8; extra == "hardware"
Requires-Dist: adafruit-circuitpython-neopixel>=6; extra == "hardware"
Requires-Dist: rpi-ws281x>=5; extra == "hardware"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Provides-Extra: docs
Requires-Dist: furo>=2024.8.6; extra == "docs"
Requires-Dist: myst-parser<5,>=3; extra == "docs"
Requires-Dist: sphinx<9,>=7.4; extra == "docs"
Dynamic: license-file

<p align="center">
  <img src="src/neopi/static/favicon.svg" alt="NeoPi neon pie logo" width="128">
</p>

# NeoPi

[![CI](https://github.com/tvarovski/neopi/actions/workflows/ci.yml/badge.svg)](https://github.com/tvarovski/neopi/actions/workflows/ci.yml)
[![Documentation](https://github.com/tvarovski/neopi/actions/workflows/docs.yml/badge.svg)](https://github.com/tvarovski/neopi/tree/main/docs)
[![PyPI](https://img.shields.io/pypi/v/neopi.svg)](https://pypi.org/project/neopi/)
[![Python versions](https://img.shields.io/pypi/pyversions/neopi.svg)](https://pypi.org/project/neopi/)
[![License: MIT + Apache-2.0](https://img.shields.io/badge/license-MIT%20%2B%20Apache--2.0-blue.svg)](LICENSE)

NeoPi is a Python package for running individually addressable RGB and RGBW lightstrips from a Raspberry Pi. It includes a local web dashboard, a library of general and seasonal animations, editable presets, automatic animation queues, a hardware-free simulator, and optional Philips Hue room-color synchronization.

NeoPi is designed for two kinds of use:

- Install the package on a Raspberry Pi and control a physical strip from a browser.
- Clone the repository, run it directly in mock or hardware mode, and create animations in ordinary Python modules.

Python 3.9 or newer is required.

## Install and run on a Raspberry Pi

Create a virtual environment and install the published package with its hardware drivers:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install "neopi[hardware]"
```

Keep NeoPi's settings in your normal user's home directory, including when the server needs elevated GPIO access:

```bash
export NEOPI_DATA_DIR="$HOME/.config/neopi"
.venv/bin/neopi setup
sudo --preserve-env=NEOPI_DATA_DIR .venv/bin/neopi serve
```

Open `http://<raspberry-pi-hostname-or-ip>:8000` from a device on the same network. If your account already has permission to access the GPIO hardware, run `.venv/bin/neopi serve` without `sudo`.

`neopi setup` asks for the pixel count, channel order, and GPIO data pin. The generated hardware configuration is stored at `~/.config/neopi/config.json`.

### Hardware power

Use a separate power supply that matches the strip's rated voltage and can supply its maximum current. Connect the Raspberry Pi ground to the strip power-supply ground, but do not power a long strip from a Pi power pin. A level shifter, a 300–500 ohm data resistor, and a 500–1000 µF capacitor near the strip input are recommended. Verify voltage, ground, data direction, and channel order before starting NeoPi.

## Run directly from a cloned repository

The repository includes `run_source.py`, which adds `src/` to Python's import path and launches the same CLI as the installed package. This lets you run NeoPi without installing NeoPi itself. Its third-party dependencies still need to be installed in a virtual environment.

Clone the repository and enter it before following one of the paths below:

```bash
git clone https://github.com/tvarovski/neopi.git
cd neopi
```

### Mock mode on Linux or macOS

```bash
python3 -m venv .venv
.venv/bin/python -m pip install "fastapi>=0.110,<1" "pydantic>=2,<3" "uvicorn[standard]>=0.29,<1" "pytest>=8" "httpx>=0.27"
NEOPI_DRIVER=simulator .venv/bin/python run_source.py serve --reload
```

### Mock mode on Windows PowerShell

```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install "fastapi>=0.110,<1" "pydantic>=2,<3" "uvicorn[standard]>=0.29,<1" "pytest>=8" "httpx>=0.27"
$env:NEOPI_DRIVER = "simulator"
.\.venv\Scripts\python.exe run_source.py serve --reload
```

Open `http://127.0.0.1:8000`. Mock mode executes the real server and animation code while storing frames in memory instead of sending them to LEDs. The dashboard's live preview shows a sample of the generated strip output.

Set `NEOPI_DRIVER=simulator` explicitly when testing. On a non-Raspberry Pi host, `driver: auto` also falls back to the simulator, but the explicit setting makes the intended mode clear and prevents accidental hardware initialization.

### Hardware mode from a Raspberry Pi checkout

Install the runtime and hardware dependencies without installing NeoPi:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install "fastapi>=0.110,<1" "pydantic>=2,<3" "uvicorn[standard]>=0.29,<1" "Adafruit-Blinka>=8" "adafruit-circuitpython-neopixel>=6" "rpi-ws281x>=5"
export NEOPI_DATA_DIR="$HOME/.config/neopi"
.venv/bin/python run_source.py setup
sudo --preserve-env=NEOPI_DATA_DIR .venv/bin/python run_source.py serve
```

Changes under `src/neopi/` take effect after a restart. During development, add `--reload` to restart the web process automatically when Python files change.

### Editable development installation

If you are comfortable installing the checkout as a package, an editable install is shorter and keeps code changes immediately available:

```bash
python3 -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
NEOPI_DRIVER=simulator .venv/bin/neopi serve --reload
```

On a Raspberry Pi, use `.[hardware,dev]` and follow the same configuration and GPIO guidance as the installed-package path above.

## Configure the strip

The default configuration file is `~/.config/neopi/config.json`, and all fields are shown in `config.example.json`. For example, a two-meter RGBW strip with 144 pixels per meter could use:

```json
{
  "num_pixels": 288,
  "pixel_order": "GRBW",
  "pixel_pin": "D21",
  "startup_brightness": 0.2,
  "rgbw_white_point_kelvin": 4000,
  "rgbw_white_luminance_scale": 2.8,
  "rgb_calibration": {
    "red": {"x": 0.6911, "y": 0.3045, "luminance": 276.7},
    "green": {"x": 0.1407, "y": 0.7034, "luminance": 800.0},
    "blue": {"x": 0.1456, "y": 0.0872, "luminance": 324.2}
  },
  "driver": "auto"
}
```

- `num_pixels` is the total number of individually addressable pixels.
- `pixel_order` accepts `RGB`, `GRB`, `RGBW`, or `GRBW`.
- `pixel_pin` accepts the supported board names `D10`, `D12`, `D18`, or `D21`.
- `startup_brightness` is a global multiplier from 0.0 through 1.0.
- `rgbw_white_point_kelvin` describes the physical white LED and accepts 1500–10000 K.
- `rgbw_white_luminance_scale` describes W-diode output relative to modeled full RGB white and accepts 0.1–10.0. The default 2.8 is the measured ratio for the available WS2815 RGBW report; because that report used a 6479 K emitter, treat it as the best available estimate for the 4000 K option.
- `rgb_calibration` defines each physical RGB primary as CIE 1931 `x`/`y` coordinates plus relative luminance. The defaults normalize the measured 54.93/158.8/64.36 lm channel outputs to 276.7/800/324.2. The previous nominal-intensity defaults were 310/800/190.
- `driver` accepts `auto`, `neopixel`, or `simulator`. The `NEOPI_DRIVER` environment variable overrides it.

Restart NeoPi after changing hardware configuration. Controller state and Hue credentials are stored separately in the same data directory.

## Use the dashboard

The dashboard lets you select an animation, change its exposed parameters, and then play it immediately or add it to the automatic queue. **Auto** advances through the configured queue or categories; **Manual** keeps the selected animation active. When **Auto-return to cycle** is enabled, a manual selection runs for one configured animation duration before Auto resumes. Changes to the active manual animation are sent to the running animation. The active custom-queue row and the header countdown show current cycle progress. Queue order, cycle duration, transition duration, and other controller settings persist across restarts.

RGBW color parameters include a separate white-channel control. NeoPi represents RGB colors as `#RRGGBB` and RGBW colors as `#RRGGBBWW` when exchanging values with the dashboard.

## Create or modify an animation

Each built-in animation lives in its own module under `src/neopi/animations/`. To add one to the dashboard:

1. Create the animation module.
2. Import the function and add it to `ANIMATIONS` in `src/neopi/animations/__init__.py`.
3. Add one or more entries directly to `PRESETS_DEFINITIONS` in `src/neopi/presets.py`.
4. Run the tests and start the source checkout in mock mode.

The animation function must accept `wait` and `duration`. New animations write three-channel sRGB colors; NeoPi's pixel adapter converts them to calibrated RGB or RGBW emitter values for the configured strip. Four-channel values remain available for legacy animations and explicit low-level RGBW control. Other defaulted numeric and color parameters are inspected to create bounded dashboard controls automatically.

```python
"""A small example of a custom NeoPi animation."""

from neopi.config import NUM_PIXELS, pixels
from neopi.runtime import time
from neopi.type_defs import RGBColor
from neopi.utils import fade_transition


@fade_transition()
def my_custom_pattern(
    wait: float = 0.05,
    duration: float = 10,
    main_color: RGBColor = (255, 0, 128),
) -> None:
    """Move a configurable color pattern along the strip.

    Args:
        wait: Delay between frames in seconds.
        duration: Total animation runtime in seconds.
        main_color: Display-referred sRGB red, green, and blue values.
    """
    started = time.monotonic()
    frame = 0
    while time.monotonic() - started < duration:
        for index in range(frame % 2, NUM_PIXELS, 2):
            pixels[index] = main_color if (index + frame) % 12 < 6 else (0, 0, 0)
        pixels.show()
        time.sleep(max(0.01, wait))
        frame += 1
```

Import NeoPi's `time` facade rather than the standard-library module so sleeps
remain interruptible without process-wide monkey-patching. Other time functions,
including `monotonic()`, are delegated unchanged.

Register the function:

```python
from .my_custom_pattern import my_custom_pattern

ANIMATIONS = {
    # Existing entries...
    "my_custom_pattern": my_custom_pattern,
}
```

Then add its dashboard preset directly inside `PRESETS_DEFINITIONS`:

```python
{
    "id": "my_custom_pattern",
    "func_name": "my_custom_pattern",
    "label": "My Custom Pattern",
    "description": "Alternating neon-colored bands moving along the strip.",
    "category": "General",
    "defaults": {
        "wait": 0.05,
        "main_color": "#ff0080"
    }
},
```

Use Google-style docstrings and type hints. Expose meaningful colors, speeds, sizes, densities, and probabilities as defaulted function parameters instead of embedding them in the rendering loop. `duration` is managed by the server and is not rendered as a dashboard control.

For long, dense strips, animations use interlaced updates to reduce sudden current changes and data instability:

```python
for index in range(frame % 2, NUM_PIXELS, 2):
    pixels[index] = color
```

Avoid calling `pixels.fill()` on every frame. Use it for initialization or shutdown, and update moving content through the interlaced loop.

Run the complete validation suite from the repository root:

```bash
.venv/bin/python -m pytest
```

On Windows, use `.\.venv\Scripts\python.exe -m pytest`.

## Run without the web server

`example_serverless.py` runs a small loop containing Rainbow Cycle, Color Waves, and Counterflow Waves:

```bash
python example_serverless.py
```

It works directly from a source checkout because it adds `src/` to its import path. It uses the same hardware configuration and driver as the server, but does not start FastAPI, save a queue, or synchronize with Philips Hue. Set `NEOPI_DRIVER=simulator` to run it without LEDs. Press `Ctrl+C` to stop and blank the strip.

## Philips Hue room synchronization

Start NeoPi, open **Hue Room Sync**, enter the Hue Bridge LAN address, and press the physical bridge link button before pairing. Load the rooms, select a room or zone, choose palette and brightness behavior, set the refresh interval, and enable synchronization. Hue polling defaults to 10 seconds and accepts 5–3600 seconds; animation speed remains independent.

The seven Hue-aware animations continuously blend toward the latest shared room palette. NeoPi reads one bridge snapshot per refresh interval, filters for powered-on and reachable bulbs, and uses their current XY, hue/saturation, or color-temperature state. The last successful palette is shared by every Hue animation and saved locally, so changing animations or restarting NeoPi does not restore an animation-specific palette. Before the first successful bridge reading, Hue animations leave the strip unchanged rather than generating fallback colors.

- **Inferred scene colors** groups similar bulbs and keeps up to five observed representative colors, including isolated accents.
- **Direct bulb colors** keeps every exact unique bulb color in bridge room order.

NeoPi samples current bulb states, including dynamic scenes, rather than reading the stored definition of a named scene. It never changes the Hue bulbs. **Follow Hue brightness** is the default: each powered-on bulb's level is represented in its palette color, with a configurable minimum (4% by default) so very dim Hue states remain visible. A room whose reachable lights are all off produces black. **Use NeoPi brightness only** ignores Hue brightness and lets the dashboard's master brightness control set strip output independently. The master control is always a final multiplicative limit. If polling fails or bulbs are unreachable, the last usable palette remains active.

Picker and Hue colors are solved against the strip's configured physical RGB-to-XYZ matrix. The default profile uses measured primary chromaticities and relative luminous flux from a [SuperLightingLED WS2815 RGBW spectrum report](https://www.superlightingled.com/PDF/10mm-300LEDs-12V-WS2815-RGBW-LED-Light-Strip-Individually-Addressable.pdf). On RGBW strips, six-digit picker colors automatically substitute the largest exact contribution from the configured white emitter; saturated colors retain little or no W, while whites and pastels use it efficiently. Disable **Automatically match RGBW emitters** in the picker for direct `R`, `G`, `B`, and `W` channel control. Eight-digit `#RRGGBBWW` values always represent that explicit manual form.

The integration uses the Hue Bridge local v1 REST API and the [Hue link-button pairing workflow](https://developers.meethue.com/develop/get-started-2/). It requires bridge-connected bulbs; direct Bluetooth and Zigbee pairing are outside NeoPi. Credentials remain in the local NeoPi data directory. Because the local v1 API uses HTTP, use Hue synchronization only on a trusted LAN.

## Files and architecture

NeoPi uses a `src` package layout:

```text
neopi/
├── pyproject.toml
├── README.md
├── run_source.py           # Full CLI from a source checkout
├── example_serverless.py   # Small loop without FastAPI
├── src/neopi/
│   ├── app.py              # FastAPI application and controller
│   ├── cli.py              # setup and serve commands
│   ├── config.py           # Hardware initialization and simulator
│   ├── hue.py              # Hue polling and color conversion
│   ├── presets.py          # Dashboard preset catalog
│   ├── animations/         # One module per animation
│   └── static/             # Dashboard HTML, CSS, and JavaScript
└── tests/
```

Configuration defaults to these paths:

- `~/.config/neopi/config.json`: strip hardware configuration.
- `~/.config/neopi/settings.json`: dashboard and queue state.
- `~/.config/neopi/hue.json`: Hue settings and bridge credential.

Set `NEOPI_DATA_DIR` to move all three files. `NEOPI_CONFIG_FILE`, `NEOPI_SETTINGS_FILE`, and `NEOPI_HUE_SETTINGS_FILE` override individual paths. Older checkout-local `lightstrip_config.json`, `settings.json`, and `hue_settings.json` files are not migrated automatically.

## Run as a systemd service

Create `/etc/systemd/system/neopi.service` after replacing the user and virtual-environment paths. The service account must have access to the GPIO driver. If hardware access requires root, use `User=root` while keeping `NEOPI_DATA_DIR` pointed at the intended persistent configuration directory.

```ini
[Unit]
Description=NeoPi lightstrip server
After=network.target

[Service]
User=yourusername
Environment=NEOPI_DATA_DIR=/home/yourusername/.config/neopi
ExecStart=/home/yourusername/neopi-venv/bin/neopi serve
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Enable and inspect it with:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now neopi.service
sudo systemctl status neopi.service
sudo journalctl -u neopi.service -f
```

Restart the service after installing a new NeoPi release:

```bash
sudo systemctl restart neopi.service
```

## Documentation and contributing

The user and API documentation source lives in [docs](docs), and every change
is validated by the Documentation workflow. Bug fixes, documentation
improvements, hardware compatibility updates, and new animations are welcome.
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request, and use
the issue templates when reporting a bug or proposing a feature.

NeoPi is released under the [MIT License](LICENSE). Portions of the Hue color
conversion work are derived from Apache-2.0-licensed diyHue code; see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for details.

## Build a package

From a development installation containing a PEP 517 build frontend:

```bash
python -m build
```

The wheel includes the Python package, built-in animations, type marker, and dashboard assets. Releases are uploaded only by the reviewed GitHub Trusted Publishing workflow described in `docs/releasing.md`.
