Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5b05310
Fix RTMP stream not recovering after websocket reconnection
stuart22 Apr 11, 2026
12355a5
Update base image from debian:buster to debian:bookworm-slim
stuart22 Apr 11, 2026
c060090
Add grace period to health check before triggering restart
stuart22 Apr 12, 2026
2c0a4b6
Reduce health grace period to 3 minutes
stuart22 Apr 12, 2026
734f5ae
Update README changelog with grace period and base image changes
stuart22 Apr 12, 2026
ea2e8c1
Add design spec for nanit fork enhancements
stuart22 Apr 12, 2026
95621dc
Add implementation plans for three parallel work streams
stuart22 Apr 12, 2026
cbb3e09
Add entrypoint script for nanit + go2rtc process management
stuart22 Apr 12, 2026
969985b
Bundle go2rtc for RTSP/ONVIF output, update entrypoint
stuart22 Apr 12, 2026
f334f41
Add PersistentBroadcaster for subscriber persistence across reconnect…
stuart22 Apr 12, 2026
cb8fdc3
Add PersistentBroadcaster for subscriber persistence across reconnect…
stuart22 Apr 12, 2026
ccbfe4d
Integrate PersistentBroadcaster into RTMP server
stuart22 Apr 12, 2026
50ab300
Refactor RTMP server to return handle for health/go2rtc integration
stuart22 Apr 12, 2026
5c69f7b
Merge pull request #2 from stuart22/feature/persistent-broadcaster
stuart22 Apr 12, 2026
8aff434
Merge pull request #1 from stuart22/feature/go2rtc-bundling
stuart22 Apr 12, 2026
1c364ed
Add multi-camera MQTT routing and websocket reset (from tanvach PR #33)
stuart22 Apr 12, 2026
37eb92c
Add Stream D plan: UniFi Protect ONVIF adoption via macvlan sidecars
stuart22 Apr 12, 2026
02e5f1f
Add MQTT device control: night light brightness, playback, volume
stuart22 Apr 12, 2026
4539046
Add HA MQTT auto-discovery for sensors, switches, and binary sensors
stuart22 Apr 12, 2026
df8b2fb
Add notification event polling: motion, sound, crying, standing, alerts
stuart22 Apr 12, 2026
d282521
Fix protobuf generated file: remove stray download log line
stuart22 Apr 12, 2026
37df47d
Update README with MQTT features, env var table, and credits
stuart22 Apr 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Build and push Docker image

on:
push:
branches: [main]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
build-args: |
CI_COMMIT_SHORT_SHA=${{ github.sha }}
17 changes: 15 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ ARG CI_COMMIT_SHORT_SHA
ARG TARGETOS TARGETARCH
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-X main.GitCommit=$CI_COMMIT_SHORT_SHA" -o ./bin/nanit ./cmd/nanit/*.go

FROM debian:buster
# Download go2rtc
FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS go2rtc-download
ARG TARGETARCH
RUN apt-get -yqq update && apt-get install -yq --no-install-recommends curl ca-certificates && \
GO2RTC_VERSION="1.9.8" && \
ARCH=$(case "$TARGETARCH" in "amd64") echo "amd64" ;; "arm64") echo "arm64" ;; *) echo "$TARGETARCH" ;; esac) && \
curl -fsSL "https://github.com/AlexxIT/go2rtc/releases/download/v${GO2RTC_VERSION}/go2rtc_linux_${ARCH}" -o /go2rtc && \
chmod +x /go2rtc

FROM debian:bookworm-slim

COPY --from=build /app/bin/nanit /app/bin/nanit
COPY --from=build /app/scripts /app/scripts
COPY --from=go2rtc-download /go2rtc /app/bin/go2rtc

RUN apt-get -yqq update && \
apt-get install -yq --no-install-recommends ca-certificates ffmpeg bash curl jq && \
Expand All @@ -24,4 +34,7 @@ RUN mkdir -p /data && \
chmod +x /app/scripts/*.sh

WORKDIR /app
ENTRYPOINT ["/app/bin/nanit"]
EXPOSE 1935 8080 8554
HEALTHCHECK --interval=180s --timeout=5s --start-period=120s --retries=2 \
CMD curl -f http://localhost:8080/health || (kill 1 && exit 1)
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
139 changes: 104 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,128 @@
# Background

This is a fork of a no-longer-maintained project (https://gitlab.com/adam.stanek/nanit) with added support for Nanit's (now required) 2FA authentication.
This is a fork of [indiefan/home_assistant_nanit](https://github.com/indiefan/home_assistant_nanit), which is itself a fork of the original [nanit project](https://gitlab.com/adam.stanek/nanit) by Adam Stanek. Credit to both for the foundational work.

## What this fork changes

- **Automatic stream recovery on reconnection**: The upstream container maintains a websocket to Nanit's cloud and asks the camera to push its RTMP stream locally. After websocket disconnections, it would reconnect but never re-request the stream — leaving the RTMP server running with no publisher. This fork resets stream state on disconnect so the stream is always re-requested after reconnection.
- **Persistent broadcaster**: Subscribers (HA/Apple Home) stay connected across publisher reconnections. No more visible stream interruptions when the camera briefly disconnects.
- **go2rtc bundling**: Bundles go2rtc for RTSP output, enabling UniFi Protect and other RTSP-based consumers.
- **Multi-camera MQTT routing**: Global connection registry routes MQTT commands to the correct camera by baby UID (from tanvach PR #33).
- **MQTT device control**: Control night light brightness, sound playback, and volume via MQTT.
- **HA MQTT auto-discovery**: Automatically registers sensors, binary sensors, and switches in Home Assistant.
- **Notification event polling**: Polls Nanit REST API for events (motion, sound, crying, standing, alerts) and publishes to MQTT.
- **Sleep tracking**: Polls for sleep events and statistics, publishes sleep state and daily stats to MQTT.
- **Health endpoint with grace period**: HTTP health endpoint on port 8080 (`/health`) with 3-minute grace period for stream recovery.
- **Docker HEALTHCHECK**: Built-in health check (every 3 minutes, 2 retries) with automatic container restart.

# Installation (Docker)

## Pull the Docker Image

While it is possible to build the image locally from the included Dockerfile, it is recommended to install and update by pulling the official image directly from Docker Hub. To pull the image manually without running the container, run:

`docker pull indiefan/nanit`
```bash
docker pull ghcr.io/stuart22/home_assistant_nanit
```

## Authentication

Because Nanit requires 2FA authentication, before we can start we need to acquire a refresh token for your Nanit account, which can be done by running the included init-nanit.sh CLI tool, which will prompt you for required account information and the 2FA code which will be emailed during the process. The script will save this to a session.json file, where it will be updated automatically going forward. Note that the `/data` volume provided to the script command must be the same used when running the primary container image later.
Nanit requires 2FA authentication. Before starting, acquire a refresh token:

### Acquire the Refresh Token
```bash
docker run -it -v /path/to/data:/data --entrypoint=/app/scripts/init-nanit.sh ghcr.io/stuart22/home_assistant_nanit
```

Run the bundled init-nanit.sh utility directly via the Docker command line to acquire the token (replace `/path/to/data` with the local path you'd like the container to use for storing session data):
**Security Note:** The refresh token provides complete access to your Nanit account. Protect your system accordingly.

## Docker Compose

```yaml
version: '3'
services:
nanit:
container_name: nanit
image: ghcr.io/stuart22/home_assistant_nanit:latest
volumes:
- /path/to/data:/data
- /etc/localtime:/etc/localtime:ro
environment:
- NANIT_RTMP_ADDR=192.168.1.x:1935
- NANIT_LOG_LEVEL=info
- TZ=America/New_York
# MQTT (optional)
- NANIT_MQTT_ENABLED=true
- NANIT_MQTT_BROKER_URL=mqtt://192.168.1.x:1883
# Notifications (optional, requires MQTT)
- NANIT_NOTIFICATIONS_ENABLED=true
ports:
- "1935:1935" # RTMP
- "8554:8554" # RTSP (go2rtc)
restart: unless-stopped
```

`docker run -it -v /path/to/data:/data --entrypoint=/app/scripts/init-nanit.sh indiefan/nanit`
## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `NANIT_EMAIL` | — | Nanit account email (for initial login) |
| `NANIT_PASSWORD` | — | Nanit account password (for initial login) |
| `NANIT_REFRESH_TOKEN` | — | Refresh token (alternative to email/password) |
| `NANIT_SESSION_FILE` | `/data/session.json` | Path to session file |
| `NANIT_RTMP_ENABLED` | `true` | Enable RTMP streaming |
| `NANIT_RTMP_ADDR` | — | Public IP:port for RTMP (e.g., `192.168.1.x:1935`) |
| `NANIT_RTSP_ENABLED` | `true` | Enable go2rtc RTSP output |
| `NANIT_LOG_LEVEL` | `info` | Log level (trace, debug, info, warn, error) |
| `NANIT_MQTT_ENABLED` | `false` | Enable MQTT integration |
| `NANIT_MQTT_BROKER_URL` | — | MQTT broker URL (e.g., `mqtt://192.168.1.x:1883`) |
| `NANIT_MQTT_CLIENT_ID` | `nanit` | MQTT client ID |
| `NANIT_MQTT_USERNAME` | — | MQTT username |
| `NANIT_MQTT_PASSWORD` | — | MQTT password |
| `NANIT_MQTT_PREFIX` | `nanit` | MQTT topic prefix |
| `NANIT_MQTT_DISCOVERY` | `true` | Enable HA MQTT auto-discovery |
| `NANIT_MQTT_RESET_WHEN_FAILED` | `false` | Auto-reconnect websocket on failed MQTT commands |
| `NANIT_WEBSOCKET_TIMEOUT` | `1` | Command response timeout in seconds |
| `NANIT_NOTIFICATIONS_ENABLED` | `false` | Enable notification event polling |
| `NANIT_NOTIFICATIONS_POLL_INTERVAL` | `10` | Event poll interval in seconds |
| `NANIT_NOTIFICATIONS_MAX_BACKOFF` | `300` | Max backoff on API errors in seconds |
| `NANIT_SLEEP_TRACKING_ENABLED` | `true` | Enable sleep tracking (requires notifications) |
| `NANIT_SLEEP_EVENT_POLL_INTERVAL` | `30` | Sleep event poll interval in seconds |
| `NANIT_STATS_POLL_INTERVAL` | `60` | Sleep stats poll interval in seconds |

** Important Note regarding Security**
The refresh token provides complete access to your Nanit account without requiring any additional account information, so be sure to protect your system from access by unauthorized parties, and proceed at your own risk.
## Home Assistant

## Docker Run
### Camera Entity (RTMP)

Now that the initial authentication has been done, and the refresh token has been generated, it's time to start the container:
```yaml
camera:
- name: Nanit
platform: ffmpeg
input: rtmp://192.168.1.x:1935/local/[baby_uid]
```

```bash
# Note: use your local IP, reachable from Cam (not 127.0.0.1 nor localhost)

docker run \
-d \
--name=nanit \
--restart unless-stopped \
-v /path/to/data:/data \
-e NANIT_RTMP_ADDR=xxx.xxx.xxx.xxx:1935 \
-e NANIT_LOG_LEVEL=trace \
-p 1935:1935 \
indiefan/nanit
### Camera Entity (RTSP via go2rtc)

```yaml
camera:
- name: Nanit
platform: ffmpeg
input: rtsp://192.168.1.x:8554/[baby_name]
```

If this is your initial run, you may want to omit the `-d` flag so you can observe the output to find your `baby_uid` (which will be needed later if you plan on connecting anything to the feed, like Home Assistant). After getting the baby id (which won't change) you can stop the container and restart it with the `-d` flag.
### MQTT Auto-Discovery

As a note, the NANIT_RTMP_ADDR should be the local ip address of your docker environment, NOT the ip address of your nanit camera.
When MQTT is enabled with discovery, entities are automatically created in HA:

## Home Assistant
- **Sensors**: Temperature, Humidity, Last Motion, Last Sound, Stream URL
- **Binary Sensors**: Night, Stream Active, Camera Online, Crying, Standing, Left Bed, Alert Zone, Alerts (temp/humidity/breathing), Low Battery, Asleep, In Bed
- **Switches**: Night Light, Standby Mode

Once the server is running and mirroring the feed, you can then setup an entity in Home Assistant. Open your `configuration.yaml` file and add the following:
### UniFi Protect

```
camera:
- name: Nanit
platform: ffmpeg
input: rtmp://xxx.xxx.xxx.xxx:1935/local/[your_baby_uid]
```
Add cameras via RTSP: `rtsp://192.168.1.x:8554/[baby_name]`

## Credits

Restart Home Assistant and you should now have a camera entity named Nanit for use in dashboards.
- [adam.stanek/nanit](https://gitlab.com/adam.stanek/nanit) — original project
- [indiefan/home_assistant_nanit](https://github.com/indiefan/home_assistant_nanit) — HA integration fork
- [tanvach](https://github.com/tanvach/home_assistant_nanit) — multi-camera MQTT routing (PR #33)
- [combmag](https://github.com/combmag/home_assistant_nanit) — device control (playback, volume, brightness)
- [scgreenhalgh](https://github.com/scgreenhalgh/home_assistant_nanit) — MQTT discovery, notifications, sleep tracking
30 changes: 25 additions & 5 deletions cmd/nanit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,32 @@ func main() {

if utils.EnvVarBool("NANIT_MQTT_ENABLED", false) {
opts.MQTT = &mqtt.Opts{
BrokerURL: utils.EnvVarReqStr("NANIT_MQTT_BROKER_URL"),
ClientID: utils.EnvVarStr("NANIT_MQTT_CLIENT_ID", "nanit"),
Username: utils.EnvVarStr("NANIT_MQTT_USERNAME", ""),
Password: utils.EnvVarStr("NANIT_MQTT_PASSWORD", ""),
TopicPrefix: utils.EnvVarStr("NANIT_MQTT_PREFIX", "nanit"),
BrokerURL: utils.EnvVarReqStr("NANIT_MQTT_BROKER_URL"),
ClientID: utils.EnvVarStr("NANIT_MQTT_CLIENT_ID", "nanit"),
Username: utils.EnvVarStr("NANIT_MQTT_USERNAME", ""),
Password: utils.EnvVarStr("NANIT_MQTT_PASSWORD", ""),
TopicPrefix: utils.EnvVarStr("NANIT_MQTT_PREFIX", "nanit"),
DiscoveryEnabled: utils.EnvVarBool("NANIT_MQTT_DISCOVERY", true),
RTMPAddr: utils.EnvVarStr("NANIT_RTMP_ADDR", ""),
}

opts.WebSocketReset = app.WebSocketResetOpts{
Enabled: utils.EnvVarBool("NANIT_MQTT_RESET_WHEN_FAILED", false),
CommandTimeout: utils.EnvVarSeconds("NANIT_WEBSOCKET_TIMEOUT", 1*time.Second),
}
}

if utils.EnvVarBool("NANIT_NOTIFICATIONS_ENABLED", false) {
opts.Notifications = app.NotificationOpts{
Enabled: true,
PollInterval: utils.EnvVarSeconds("NANIT_NOTIFICATIONS_POLL_INTERVAL", 10*time.Second),
Jitter: 0.3,
MaxBackoff: utils.EnvVarSeconds("NANIT_NOTIFICATIONS_MAX_BACKOFF", 300*time.Second),
EnableSleepTracking: utils.EnvVarBool("NANIT_SLEEP_TRACKING_ENABLED", true),
SleepEventPollInterval: utils.EnvVarSeconds("NANIT_SLEEP_EVENT_POLL_INTERVAL", 30*time.Second),
StatsPollInterval: utils.EnvVarSeconds("NANIT_STATS_POLL_INTERVAL", 60*time.Second),
}
log.Info().Msgf("Notifications enabled with poll interval %v", opts.Notifications.PollInterval)
}

if opts.EventPolling.Enabled {
Expand Down
Loading