Metadata-Version: 2.4
Name: neopi
Version: 0.1.11
Summary: NeoPixel animation scenes and a friendly Raspberry Pi web controller
Requires-Python: >=3.9
Description-Content-Type: text/markdown
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"

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

# NeoPi

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": 1.0,
  "rgb_calibration": {
    "red": {"x": 0.6911, "y": 0.3045, "luminance": 310.0},
    "green": {"x": 0.1407, "y": 0.7034, "luminance": 800.0},
    "blue": {"x": 0.1456, "y": 0.0872, "luminance": 190.0}
  },
  "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 RGB white and accepts 0.1–10.0. Raise it when white output is too strong; a value of 2.0 sends half as much W for the same modeled contribution.
- `rgb_calibration` defines each physical RGB primary as CIE 1931 `x`/`y` coordinates plus relative luminance. Its defaults use measured WS2815 RGB chromaticities and the WS2815B chip's typical 310/800/190 mcd R/G/B intensity values. Change these only when you have measurements for another strip.
- `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. Changes to the active manual animation are sent to the running animation. 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`. Other defaulted numeric and RGB/RGBW tuple parameters are inspected to create dashboard controls automatically.

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

import time

from neopi.config import NUM_PIXELS, pixels
from neopi.type_defs import RGBWColor
from neopi.utils import fade_transition


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

    Args:
        wait: Delay between frames in seconds.
        duration: Total animation runtime in seconds.
        main_color: Red, green, blue, and white channel 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, 0)
        pixels.show()
        time.sleep(max(0.01, wait))
        frame += 1
```

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": "#ff008000"
    }
},
```

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 a palette mode, 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. If polling fails or no bulbs are available, the last usable palette remains active.

Hue XY colors are solved against the strip's configured physical RGB-to-XYZ matrix. The default profile combines the [WS2815B chip's typical per-channel intensity](https://www.superlightingled.com/PDF/SPEC/WS2815B-RGB-LED-Chip-Spec.pdf) with measured primary chromaticities from a [SuperLightingLED WS2815 RGBW spectrum report](https://www.superlightingled.com/PDF/10mm-300LEDs-12V-WS2815-RGBW-LED-Light-Strip-Individually-Addressable.pdf). This compensates for the green diode producing substantially more light than red or blue at the same numeric drive. Hue XY and hue/saturation colors use RGB only so the white diode cannot wash out scene colors. Hue color-temperature states use `rgbw_white_point_kelvin` to model the W emitter and `rgbw_white_luminance_scale` to compensate for its relative output, retaining any warm or cool correction in RGB.

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
```

## 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. On Windows, `publish.cmd` builds distributions and invokes Twine for publishing.
