Metadata-Version: 2.4
Name: pyswann
Version: 0.0.4
Summary: Python client for Swann DVRs
Author-email: brendann993 <brendann993@icloud.com>
License-Expression: MIT
Project-URL: Documentation, https://github.com/brendann993/pyswann#readme
Project-URL: Issues, https://github.com/brendann993/pyswann/issues
Project-URL: Source, https://github.com/brendann993/pyswann
Project-URL: Download, https://github.com/brendann993/pyswann/releases
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Home Automation
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: coverage>=7.6; extra == "dev"
Requires-Dist: ruff>=0.15.1; extra == "dev"
Requires-Dist: twine>=6.0; extra == "dev"
Dynamic: license-file

# pyswann

An unofficial, environment-agnostic, pure Python client under development for
Swann DVR/NVR hardware using the RaySharp client service on TCP port 9000.

This project is not affiliated with or endorsed by Swann or RaySharp. Protocol
support has been tested against one Swann recorder and firmware version; other
models and firmware may behave differently.

The project deliberately has no dependency on RaySharp's DLLs or shared
libraries. Protocol behaviour is added only after it is supported by SDK source
evidence and confirmed against the physical Swann recorder.

## Transport policy

This library is intentionally TCP-first. The native RaySharp client service on
TCP port 9000 remains the default path for authentication, session lifecycle,
heartbeat maintenance, runtime status, and live event delivery. The web API on
port 85 is kept for supplementary metadata and configuration enrichment when it
exposes the relevant data, but it is not the live runtime transport and it is
not the default path for motion or alarm handling.

Compatibility runs that intentionally prefer HTTP are useful as diagnostics and
coverage comparisons, but they are not the supported default architecture.

## Current capability

The package currently provides:

- a bounded asyncio TCP transport;
- explicit connection/session state;
- recorder-confirmed encrypted RaySharp login and response parsing;
- a managed authenticated session with encrypted type-32 heartbeats and a
  single protocol reader;
- framed, decrypted motion-alarm reception (`891/191`);
- combined pedestrian/vehicle (PD+VD) decoding with explicit ON/OFF state
  (`891/251`);
- a typed async event iterator currently yielding `MotionEvent` and `PdVdEvent`;
- typed read-only recorder and channel metadata from the HTTP API;
- lossless timestamped capture of every received byte;
- a diagnostic CLI that runs anywhere Python 3.12 runs;
- no dependency on any higher-level application framework; the library is
  intentionally standalone and reusable outside Home Assistant.

The physical recorder now confirms authentication, heartbeat maintenance, and
automatic motion-report delivery. Parallel channel/status bitmasks are decoded
into explicit per-channel ON/OFF transitions.

Only `MotionEvent` and `PdVdEvent` are currently implemented because those are
the event types validated against the available test recorder. RaySharp devices
may emit additional alarm families, but they remain unsupported until they can
be captured and decoded reliably.

## Normalized snapshot model

Both the TCP protocol client and the HTTP metadata client can be normalized into
a single downstream-friendly shape. This keeps the transport-specific raw payload
as diagnostics while giving callers one consistent contract for system/channel
status and capability discovery.

```python
from swann.normalized import RecorderSnapshot

snapshot: RecorderSnapshot
```

The normalized shape keeps the common fields aligned across both transports:

```python
{
    "source": "tcp",
    "system": {
        "name": "DVR84580RN",
        "model": "DVR8-4580RN",
        "firmware_version": "V8.2.2-20240820",
        "mac_address": "BC-51-FE-E2-EC-CB",
        "channel_count": 8,
    },
    "channels": [
        {
            "channel": 1,
            "alias": "Front Door",
            "enabled": True,
            "record_state": "idle",
            "motion_state": "inactive",
            "video_loss_state": "normal",
            "state": "online",
            "mainstream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=0",
            "substream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=1",
        }
    ],
    "capabilities": {
        "motion": True,
        "pid": False,
        "lcd": False,
        "pd": False,
        "sod": False,
        "video_loss": True,
    },
    "raw": {
        "transport": "tcp",
        "payload": {"...protocol-specific fields...": "..."},
    },
}
```

The raw transport payload is deliberately kept in a separate `raw` field so the
common consumer path remains stable regardless of whether the data originated in
the TCP protocol or the HTTP metadata API.

### Snapshot usage

```python
from swann import SwannClient

client = SwannClient(
    "192.0.2.1",
    username="admin",
    password="secret",
)

async with client:
    tcp_snapshot = client.snapshot()
    print(tcp_snapshot.source)
    print(tcp_snapshot.channels[0].alias)
    print(tcp_snapshot.channels[0].mainstream_url)

    # Optional: when HTTP metadata was explicitly collected, surface the same
    # normalized contract using the HTTP adapter path.
    if client.http_metadata is not None:
        http_snapshot = client.snapshot(source="http")
        print(http_snapshot.source)
        print(http_snapshot.channels[0].alias)
```

Example TCP-normalized output:

```python
{
    "source": "tcp",
    "system": {
        "name": "DVR84580RN",
        "model": "DVR8-4580RN",
        "firmware_version": "V8.2.2-20240820",
        "channel_count": 8,
    },
    "channels": [
        {
            "channel": 1,
            "alias": "Front Door",
            "enabled": True,
            "record_state": "idle",
            "motion_state": "inactive",
            "video_loss_state": "normal",
            "state": "online",
            "mainstream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=0",
            "substream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=1",
        }
    ],
    "capabilities": {
        "motion": True,
        "pid": False,
        "lcd": False,
        "pd": False,
        "sod": False,
        "video_loss": True,
    },
    "raw": {
        "transport": "tcp",
        "payload": {...},
    },
}
```

Example HTTP-normalized output:

```python
{
    "source": "http",
    "system": {
        "name": "DVR84580RN",
        "model": "DVR8-4580RN",
        "firmware_version": "V8.2.2-20240820",
        "channel_count": 8,
    },
    "channels": [
        {
            "channel": 1,
            "alias": "Front Door",
            "enabled": True,
            "record_state": "unknown",
            "motion_state": "unknown",
            "video_loss_state": "unknown",
            "state": "online",
            "mainstream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=0",
            "substream_url": "rtsp://10.1.1.118:554/rtsp/streaming?channel=01&subtype=1",
        }
    ],
    "capabilities": {
        "motion": True,
        "pid": False,
        "lcd": False,
        "pd": False,
        "sod": False,
        "video_loss": True,
    },
    "raw": {
        "transport": "http",
        "payload": {...},
    },
}
```

The managed client still behaves as a TCP event stream: the event iterator and
heartbeat/reader tasks continue to run in the background, and the snapshot API
simply exposes the normalized current state without replacing the live event
delivery path.

## Managed client

The primary library API owns authentication, heartbeat maintenance, packet
reading, and typed event dispatch for the lifetime of the context manager:

```python
from swann import MotionEvent, PdVdEvent, SwannClient

client = SwannClient(
    "ip.address or hostname",
    username="admin",
    password="recorder-password",
)

async with client:
    info = client.device_info
    if info is not None:
        print(info.model, info.firmware_version, info.preferred_identifier)

    async for event in client.events():
        if isinstance(event, MotionEvent):
            print("motion", event.channel, event.active)
        elif isinstance(event, PdVdEvent):
            print("PD+VD", event.channel, event.active)
```

`RaySharpClient` remains available as a compatibility alias for existing
callers; new code should use `SwannClient`.

`PdVdEvent` intentionally represents the combined DVR category. Controlled
captures show that pedestrian and vehicle detections use the same wire subtype
and alarm value, so the low-level library does not invent a distinction that
the recorder does not transmit.

After authentication, `client.device_info` contains confirmed `LoginRsp`
fields including model, recorder name, firmware version, serial number, MAC
address, channel counts, and raw advertised push/control flags. The preferred
stable identifier uses the serial number when present and otherwise falls back
to the MAC address.

The managed client can also collect supplementary HTTP metadata after TCP login
when the caller opts in with `collect_http_metadata=True`. `client.http_metadata`
contains the recorder model, hardware and software versions, storage/video
details, and typed channel aliases, enabled states, and stream descriptions.
HTTP failure is exposed as `client.http_metadata_error` and does not stop TCP
event delivery. TCP port 9000 remains the primary transport for the live
session, heartbeat management, and runtime event/state information. The HTTP
API serves as an optional enrichment source for human-readable metadata and
configuration whenever it exposes the relevant data.

Each `ChannelMetadata` also exposes `mainstream_url` and `substream_url`.
These use explicit URL fields when a recorder returns them; otherwise they use
the confirmed RaySharp RTSP path with the channel number formatted as `01`,
`02`, and so on.

The client accepts an injected `aiohttp.ClientSession` when a caller already has
one, but it never closes a caller-owned session. For standalone use, omitting
`websession` creates an internal session which is closed with the client. The
recorder HTTP flow uses Digest authentication plus the returned CSRF token and
session cookie. Credentials are not logged, and the sensitive P2P identifier
returned by the recorder is intentionally not included in `SystemMetadata`.

HTTP metadata collection includes the Motion alarm configuration endpoint. It
exposes Motion through `IntelligentAlarmType.MOTION` with the typed channel
configuration model, and this is the preferred config source when the recorder
provides it.

Compatibility and fallback helpers still exist for low-level TCP parameter
queries via `await client.refresh_alarm_configurations()`, but project policy is
to keep TCP as the primary transport for live alarms/events and runtime state,
with HTTP as an optional enrichment source for recorder metadata and
configuration when it exposes the relevant data. HTTP metadata collection is
opt-in, while TCP startup state remains the default runtime path.

TCP media/configuration refreshes remain available as compatibility helpers,
but they are not the default path for normal operation and should not be used
as an eager fallback simply because another transport is slower. The slow
optional Living/preview page 573 is disabled by default and must be requested
explicitly with `include_living=True`.

HDD, Motion, Abnormal, and AVD status pages are still available as optional
TCP compatibility queries, but they are not the primary source when the web
API exposes equivalent data. Set `collect_status_configurations=False` to skip
these pages entirely. Refresh failures remain available through
`client.status_configuration_errors`, and the last attempt time is exposed as
`client.status_configuration_refreshed_at`.

Alarm configuration uses a primary/fallback policy. TCP remains the primary
transport for live runtime status and compatibility pages, while HTTP is used
as an optional enrichment/fallback source for human-readable configuration when
it exposes the values. A TCP query should not be replaced by HTTP merely because
it is slower; fallback should happen only on actual failure or missing data.

Use `--http-primary-test --status-output captures/http-primary.json` only as an
explicit compatibility probe. This collects system/channel metadata and alarm
configuration through HTTP so the project can compare HTTP coverage against the
TCP-first runtime path. TCP remains responsible for authentication, heartbeats,
and live alarm events. It does not query TCP alarm configuration or the other
optional TCP status pages unless explicitly requested for compatibility testing.

## Diagnostic connection

Copy `.env.example` to `.env`, then replace the password value. `.env` is
ignored by Git and loaded automatically by the diagnostic CLI:

```bash
cp .env.example .env
```

```dotenv
RAYSHARP_HOST=ip.address or hostname
RAYSHARP_PORT=9000
RAYSHARP_HTTP_PORT=85
RAYSHARP_USERNAME=admin
RAYSHARP_PASSWORD=your-recorder-password
RAYSHARP_LOGIN_VARIANT=crypto
```

Then run from the project directory:

```bash
py -m swann.cli --output captures/connection.jsonl
```

To write an authenticated TCP status snapshot for review, use
`--status-output`. This reports recorder identity, channel names and
availability, motion/recording/video-loss masks, and intelligent-analysis
capabilities without writing credentials or the raw login request:

```bash
py -m swann.cli --status-output captures/status.json
```

The status snapshot can be combined with the raw JSON Lines capture:

```bash
py -m swann.cli --output captures/connection.jsonl --status-output captures/status.json
```

Existing exported environment variables take precedence over `.env`. Set
`PYSWANN_ENV_FILE` to load a different file.

PowerShell environment variables are also supported:

```powershell
$env:RAYSHARP_HOST = "ip.address or hostname"
$env:RAYSHARP_PORT = "9000"
$env:RAYSHARP_USERNAME = "admin"
$env:RAYSHARP_PASSWORD = "your-recorder-password"

py -m swann.cli --output captures\connection.jsonl
```

This sends the recorder-confirmed encrypted `CryptoMsgLogin` packet and then an
encrypted heartbeat every five seconds. Password-bearing transmit packets are
always omitted from logs and captures. Incoming motion reports are logged as
human-numbered channel transitions while retaining both raw bitmasks.

See [`docs/protocol.md`](docs/protocol.md) for established findings, observed
packet layouts, and remaining protocol questions.
