Skip to content
137 changes: 99 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,124 @@ This is a fork of [indiefan/home_assistant_nanit](https://github.com/indiefan/ho
## 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.
- **Health endpoint with grace period**: Adds an HTTP health endpoint on port 8080 (`/health`) that reports whether the websocket is connected and the RTMP stream is alive. Includes a 3-minute grace period after stream loss to allow the reconnection fix to recover before triggering a restart.
- **Docker HEALTHCHECK**: The Dockerfile includes a built-in health check (every 3 minutes, 2 retries). If the stream doesn't recover within the grace period, the container is restarted automatically.
- **Updated base image**: Upgraded from `debian:buster` (EOL) to `debian:bookworm-slim`.
- **Bugfix**: Fixed a nil pointer dereference in `GetIsWebsocketAlive()` where it checked `StreamState` instead of `IsWebsocketAlive`.
- **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 ghcr.io/stuart22/home_assistant_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 ghcr.io/stuart22/home_assistant_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 \
ghcr.io/stuart22/home_assistant_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