Metadata-Version: 2.4
Name: pyneolink
Version: 0.4.4
Summary: Experimental Python client for Reolink cameras
Author-email: AlzGamer <dev@alzgamer.pro>
Maintainer-email: AlzGamer <dev@alzgamer.pro>
License-Expression: MIT
Project-URL: Homepage, https://github.com/alzgamer-kr/pyneolink
Project-URL: Repository, https://github.com/alzgamer-kr/pyneolink
Project-URL: Issues, https://github.com/alzgamer-kr/pyneolink/issues
Project-URL: Changelog, https://github.com/alzgamer-kr/pyneolink/blob/main/CHANGELOG.md
Keywords: reolink,neolink,baichuan,camera,p2p,uid,ip-camera,security-camera
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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 :: Multimedia :: Video
Classifier: Topic :: System :: Networking
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42
Provides-Extra: voice
Requires-Dist: sounddevice>=0.4; extra == "voice"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.16; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Dynamic: license-file

# PyNeolink

[![Downloads](https://img.shields.io/pypi/dm/pyneolink?style=for-the-badge&label=downloads&labelColor=006dad&color=005d94&cacheSeconds=86400)](https://pypistats.org/packages/pyneolink)
[![Release](https://img.shields.io/github/v/release/alzgamer-kr/pyneolink?style=for-the-badge&label=release&labelColor=006dad&color=005d94&cacheSeconds=7200)](https://github.com/alzgamer-kr/pyneolink/releases)
[![GitHub](https://img.shields.io/github/forks/alzgamer-kr/pyneolink?style=for-the-badge&label=forks&labelColor=006dad&color=005d94&cacheSeconds=7200)](https://github.com/alzgamer-kr/pyneolink)
[![GitHub](https://img.shields.io/github/stars/alzgamer-kr/pyneolink?style=for-the-badge&label=stars&labelColor=006dad&color=005d94&cacheSeconds=7200)](https://github.com/alzgamer-kr/pyneolink)

PyNeolink is a Python client for Reolink/Neolink-style Baichuan cameras. It focuses on UID/P2P access, camera information, SD-card recordings, live viewing, snapshots, local recording, motion events, battery status, voice/talk, and siren control.

Version: `0.4.4` alpha.

This project was developed with OpenAI Codex as an AI-assisted implementation effort. It is a Python port inspired by and based on protocol knowledge from the Rust `neolink` project, especially `QuantumEntangledAndy/neolink` and `surfzoid/neolink`. The reverse-engineering foundation belongs to the Neolink contributors. The goal is not to replace Neolink, but to make a working Python implementation available for people who want to study, adapt, or extend this protocol without working in Rust.

Neolink and Reolink protocol support are reverse engineered. This project is not affiliated with Reolink.

## Status

PyNeolink is experimental alpha software. It works against a limited set of real cameras, but Reolink firmware and model behavior can differ. APIs may change before `1.0.0`.

The test suite is currently verified with Conda on CPython 3.11.15, 3.12.13, 3.13.15, and 3.14.7.

## What Works

- JSON camera configuration with `address` or `uid`
- Reolink UID/P2P lookup, registration, UDP relay, and local UDP connection
- Baichuan login and command framing
- BC XOR encryption and AES-CFB support through `cryptography`
- Camera information, UID, LED command, and reboot command
- Battery status, including reconnect and online polling modes
- SD-card recording list with pagination and time sorting
- SD-card recording download with high/low quality selection
- SD-card preview playback cache with an HTTP stream helper for players such as VLC
- Snapshot download to bytes or JPEG file
- Local MPEG-TS recording from the live stream with `Camera.record()` or the
  `record` CLI command
- Live HTTP MPEG-TS viewing with H264/H265 video and AAC audio
- HLS timeshift viewing with an in-memory sliding buffer
- Concurrent SDK stream, motion, battery, and command handling inside one
  `with Camera(...)` session
- Automatic dispatcher and live-stream recovery when a camera session closes
- Motion status and motion event watch mode
- Two-way voice/talk from microphone, audio file, or generated test tone
- Camera siren trigger
- PIR status and PIR on/off settings
- IR light status and IR on/off/auto settings
- Stored PTZ preset listing and recall

## Current Limits

- This is reverse engineered and tested against a small number of real cameras, so behavior may differ between models and firmware versions.
- Voice file playback uses `ffmpeg`/`ffprobe` for format validation and conversion. Install FFmpeg and make sure both commands are available in `PATH`.
- Microphone voice input needs the Python `sounddevice` package and a working local input device.
- Local stream recording writes MPEG-TS (`.ts`) files, not MP4. It records the
  live stream on the client machine; it does not start or stop the camera's own
  SD-card recording schedule.
- SD-card `remove()` and `format()` exist, but are intentionally guarded.
- Some tested battery cameras end a continuous stream near 30 minutes. The SDK
  reconnects and restarts the stream, which can cause a short playback gap.
- Arbitrary directional PTZ movement, image settings, alarm schedules, floodlight settings, and Web UI are not implemented yet.

## Install

From PyPI:

```powershell
python -m pip install pyneolink==0.4.4
```

With microphone voice input support:

```powershell
python -m pip install "pyneolink[voice]==0.4.4"
```

For local development from a checkout:

```powershell
python -m venv .venv
.venv\Scripts\activate
python -m pip install -r requirements.txt
python -m pip install -e ".[dev,voice]"
```

With Conda from a checkout:

```powershell
conda create -n pyneolink-dev python=3.14 pip
conda activate pyneolink-dev
python -m pip install -r requirements.txt
python -m pip install -e ".[dev,voice]"
python -m pytest
```

The `cryptography` package is required for AES-encrypted cameras. It is installed automatically when installing the package.

## FFmpeg

FFmpeg is a system dependency for audio-file voice playback and some media conversion paths. PyNeolink expects both `ffmpeg` and `ffprobe` to be available in `PATH`.

Official FFmpeg download page:

- Windows: https://ffmpeg.org/download.html
- Linux: https://ffmpeg.org/download.html
- macOS: https://ffmpeg.org/download.html

On Linux and macOS, using the OS package manager is usually the simplest path when available. On Windows, install one of the builds linked from the official FFmpeg download page and add its `bin` directory to `PATH`.

## Configuration

Create a local `config.json`:

```json
{
  "bind": "0.0.0.0",
  "bind_port": 8554,
  "cameras": [
    {
      "name": "Home-Front",
      "username": "admin",
      "password": "password",
      "uid": "ABCDEF0123456789",
      "discovery": "relay"
    }
  ]
}
```

`config.json` is ignored by Git because it can contain camera credentials.

## CLI

Camera info:

```powershell
python pyneolink/cli.py info --camera "Home-Front" --config config.json
python pyneolink/cli.py --info --camera "Home-Front"
```

Battery:

```powershell
python pyneolink/cli.py battery --camera "Home-Front"
python pyneolink/cli.py battery --camera "Home-Front" --watch --interval 60
python pyneolink/cli.py battery --camera "Home-Front" --watch --interval 60 --mode online
```

Snapshots and local recording:

```powershell
python pyneolink/cli.py snapshot --camera "Home-Front" --out snapshots/
python pyneolink/cli.py record --camera "Home-Front" --out recordings/ --duration 30 --quality high
python pyneolink/cli.py record --camera "Home-Front" --out recordings/live.ts --quality low
```

`record` opens the selected live stream and saves it locally as MPEG-TS. Omit
`--duration` to record until Ctrl+C. This is not a command to force the camera
to start writing a new SD-card recording.

Motion:

```powershell
python pyneolink/cli.py motion --camera "Home-Front"
python pyneolink/cli.py motion --camera "Home-Front" --watch --duration 30
```

PIR:

```powershell
python pyneolink/cli.py pir --config config.json --camera "Home-Front" status
python pyneolink/cli.py pir --config config.json --camera "Home-Front" on
python pyneolink/cli.py pir --config config.json --camera "Home-Front" off
```

IR light:

```powershell
python pyneolink/cli.py ir --config config.json --camera "Home-Front" status
python pyneolink/cli.py ir --config config.json --camera "Home-Front" on
python pyneolink/cli.py ir --config config.json --camera "Home-Front" off
python pyneolink/cli.py ir --config config.json --camera "Home-Front" auto
python pyneolink/cli.py led --config config.json --camera "Home-Front" auto
```

PTZ presets:

```powershell
python pyneolink/cli.py ptz --config config.json --camera "Home-Front" presets
python pyneolink/cli.py ptz --config config.json --camera "Home-Front" preset 3
```

The tested Argus PT Ultra requires a snapshot exchange after login before it
accepts PTZ preset recall. The SDK caller must perform that snapshot and recall
in the same `Camera` session; `goto_preset()` does not do it automatically.
On that model, the standalone `ptz preset` CLI command may still return `400`
because it does not currently perform that snapshot warm-up automatically.

Voice and siren:

```powershell
python pyneolink/cli.py voice --camera "Home-Front" --file alert.mp3
python pyneolink/cli.py voice --camera "Home-Front" --microphone --seconds 10
python pyneolink/cli.py voice --camera "Home-Front" --tone 1000 --seconds 3
python pyneolink/cli.py voice --camera "Home-Front" --siren
```

Live view server:

```powershell
python pyneolink/cli.py serve --config config.json
```

Open direct MPEG-TS in VLC/ffplay:

```text
http://127.0.0.1:8554/Home-Front/high
http://127.0.0.1:8554/Home-Front/low
```

Open HLS timeshift in VLC/ffplay:

```text
http://127.0.0.1:8554/Home-Front/high/hls.m3u8
http://127.0.0.1:8554/Home-Front/low/hls.m3u8
```

HLS keeps a sliding in-memory buffer. By default it stores up to 100 MB and cuts segments around 2 seconds:

```powershell
python pyneolink/cli.py serve --config config.json --hls-buffer-mb 100 --hls-segment-seconds 2
```

When binding to `0.0.0.0`, do not open `0.0.0.0` in VLC. Use `127.0.0.1` on the same PC or the PC's LAN IP from another device.

## Library Use

`with Camera(...) as camera:` opens the camera session and starts the internal
message dispatcher automatically. This lets one `Camera` object handle live
stream reads, motion events, and ordinary status/config commands without the
caller managing the socket reader manually.

Camera information:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    info = camera.info()
    print(info)
```

Battery:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    battery = camera.battery()

    with battery.info() as info:
        print(info["level_percent"], info["is_charging"], info["adapter_status"])

    with battery.info(interval=60, mode="reconnect", count=3) as updates:
        for update in updates:
            print(update["level_percent"], update["adapter_status"])
```

SD-card download:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    sd = camera.sd_card()
    videos = sd.files(start="2026-06-03", end="2026-06-03", name=".mp4")
    if videos:
        videos[-1].download("downloads", quality="high", rewrite_exists=False, progress=True)
```

Motion:

```python
from pyneolink import Camera, EVENTS

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    motion = camera.motion()
    print(motion.status())

    with motion.watch(duration=30) as events:
        for event in events:
            if event == EVENTS.human and event.active:
                print("human detected")
```

Snapshot:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    camera.snapshot(out="snapshots")
    image = camera.snapshot()
```

Local recording from the SDK:

```python
import time

from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    path = camera.record(out="recordings", duration=30, stream="mainStream")
    print(f"saved to {path}")

    with camera.record(out="recordings/live.ts", stream="subStream") as recorder:
        time.sleep(10)
    print(f"saved to {recorder.path}")
```

`Camera.record()` records the live stream on the client machine and writes
MPEG-TS (`.ts`). With `duration`, it waits and returns the final `Path`. Without
`duration`, it returns a running `StreamRecorder`; use it as a context manager
or call `stop()` to finalize the file. This does not enable or disable the
camera's own SD-card recording.

Voice and siren:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    voice = camera.voice()
    voice.play("alert.mp3")
    voice.siren()
```

Settings and PIR:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    settings = camera.settings()
    print(settings.pir.status())
    settings.pir.on()
    settings.pir.off()
    print(settings.ir.status())
    settings.ir.on()
    settings.ir.off()
    settings.ir.auto()
```

PTZ presets:

```python
from pyneolink import Camera

with Camera(uuid="ABCDEF0123456789", username="admin", password="password") as camera:
    print(camera.ptz().presets())
    # Required by the tested Argus PT Ultra before preset recall.
    camera.snapshot()
    camera.ptz().goto_preset(3)
```

The tested Argus PT Ultra requires a snapshot exchange after login before it
accepts PTZ preset recall. The caller must perform and discard that snapshot in
the same session; `goto_preset()` intentionally has no model-specific side
effects and does not start a continuous video stream.

Live stream server from a dict:

```python
from pyneolink import StreamServer

config = {
    "bind": "0.0.0.0",
    "bind_port": 8554,
    "cameras": [
        {
            "name": "Home-Front",
            "username": "admin",
            "password": "password",
            "uid": "ABCDEF0123456789",
            "discovery": "relay",
        }
    ],
}

StreamServer(config, buffer_seconds=1.5, hls_buffer_mb=100, hls_segment_seconds=2).serve_forever()
```

## Examples

See the `examples/` directory:

- `camera_example.py`: info, snapshot, LED, and guarded reboot helpers
- `sd_card_example.py`: list, filter, download, preview playback, remove, and guarded format calls
- `battery_example.py`: one-shot battery status plus reconnect/online polling
- `motion_example.py`: motion status and watch mode
- `record_example.py`: duration and manual local stream recording
- `voice_example.py`: file, microphone, tone, and siren helpers
- `settings_example.py`: PIR and IR status plus guarded setting helpers
- `ptz_example.py`: list and recall stored PTZ presets
- `stream_example.py`: live MPEG-TS and HLS timeshift server from a dict config

Each example keeps camera settings as a small local dict near the top of the file. Edit those values directly or replace the dict with your own configuration loader.

## Internals

See `docs/` for a sorted internal documentation set that explains the core files, connection flow, login, encryption, Baichuan messages, UDP/P2P transport, SD-card downloads, media streaming, motion, voice, and camera controls.

## Credits

PyNeolink exists because of the reverse-engineering work done by the Neolink community. In particular, the Rust `neolink` implementations and documentation were used as the protocol reference for this Python port.

This Python implementation was developed with OpenAI Codex. Human testing against real cameras guided the implementation and bug fixes.

## Reference

The Rust `neolink/` checkout can be kept locally as a protocol reference, but it is ignored by Git in this workspace. The Python code here is intended to be understandable and hackable for people who want to experiment with Reolink cameras from Python.

## Tested Cameras

- Reolink Argus Eco
