Metadata-Version: 2.4
Name: pyeisenberg
Version: 0.7.2
Summary: Async Python client for the Arlo camera API
Author-email: Marcello Barnaba <vjt@openssl.it>
License-Expression: MIT
Project-URL: Homepage, https://github.com/vjt/ha-eisenberg
Project-URL: Repository, https://github.com/vjt/ha-eisenberg
Project-URL: Issues, https://github.com/vjt/ha-eisenberg/issues
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Home Automation
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-timeout>=2.2; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: aioresponses>=0.7; extra == "dev"
Requires-Dist: homeassistant>=2026.2; extra == "dev"
Requires-Dist: numpy>=1.26; extra == "dev"
Requires-Dist: pyright>=1.1; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# Eisenberg — Arlo for Home Assistant

<img src="docs/hero.jpg" alt="A skater grinding a rail that doubles as a data bus, with camera irises wired back to a central hub" width="100%">

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)
[![GitHub Release](https://img.shields.io/github/v/release/vjt/ha-eisenberg?include_prereleases&sort=semver)](https://github.com/vjt/ha-eisenberg/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![CI](https://github.com/vjt/ha-eisenberg/actions/workflows/ci.yml/badge.svg)](https://github.com/vjt/ha-eisenberg/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/vjt/ha-eisenberg/branch/master/graph/badge.svg)](https://codecov.io/gh/vjt/ha-eisenberg)

A Home Assistant custom integration for Arlo cameras, named after
skating legend Arlo Eisenberg. Built around event-driven MQTT (no
polling) with a typed Pydantic API client.

📖 Long-form walk-through (UX, auth, MQTT, streaming): **[Eisenberg: Arlo cameras on Home Assistant, the easy way](https://sindro.me/posts/2026-04-28-eisenberg-arlo-on-home-assistant/)** ([🇮🇹 italiano](https://sindro.me/it/posts/2026-04-28-eisenberg-arlo-on-home-assistant/)).

[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=vjt&repository=ha-eisenberg&category=integration)

## Entities

Seven platforms. Per camera, unless noted.

| Entity | Platform | What it does |
| --- | --- | --- |
| Camera | `camera` | Live RTSPS stream plus a tile backed by snapshots, motion thumbnails and stream keyframes, cached to disk so it survives restarts and stays populated while disarmed |
| Motion | `binary_sensor` | Generic motion, from the MQTT `motionDetected` property |
| Person / Vehicle / Animal detected | `binary_sensor` | Arlo's AI classification, one sensor per category, with a configurable reset timeout |
| Contact | `binary_sensor` | **Per all-in-one sensor (MS1001)**: open/closed, from the sensor's own `contactState`. The sensor also gets a Battery |
| Base station connectivity | `binary_sensor` | Whether the hub that gateways this camera is reachable — resolved by `parentId`, so it reports the camera's *own* hub on multi-hub accounts |
| Battery | `sensor` | Percentage, with the standard HA battery device class |
| Signal strength | `sensor` | WiFi signal, in bars |
| Siren | `switch` | On/off |
| Camera | `switch` | Switch the camera itself on/off — Arlo's privacy mode, the same as the app's toggle. On cameras behind a base station it shows as *unknown* until the camera first reports its state, which can take a few minutes after HA starts |
| Spotlight | `light` | On/off plus brightness — Arlo's 0–100 intensity mapped onto HA's 0–255 |
| Snapshot | `button` | Request a fresh full-frame snapshot |
| Security mode | `select` | armAway / armHome / standby plus your custom modes from the Arlo app, kept live as you add, rename or delete them. **One per location**, named after the location when you have more than one, and attached to a device in that location |

Battery and signal work on cameras that report them directly *and* on
cameras that keep them on a hub — see
[Base stations and SmartHubs](#base-stations-and-smarthubs).

## Also in the box

- **Media archival** — opt-in storage of motion clips, thumbnails and
  stream keyframes to a configured `media_dirs` location, with rolling
  retention (default 14 days).
- **An `eisenberg.snapshot` service** for dashboards and automations.
- **`eisenberg_media` events** on every motion capture, for your own
  automations.
- **Multi-location and shared-account support** — devices shared to you
  from somebody else's Arlo account included.
- **Reauthentication that doesn't lose your place** — the integration
  surfaces an auth failure as a proper HA reauth flow instead of going
  quietly deaf, and keeps your trust cookie across it.

Want to record clips from the live stream on your own schedule,
without an Arlo subscription? Tor Seldén's
[eisenberg_clip](https://github.com/torselden/eisenberg_clip) is a
companion component that does exactly that on top of Eisenberg, with
ffmpeg.

## Base stations and SmartHubs

Base-stationed accounts went from unusable to working across 0.3.14–0.4.0
and 0.4.10, driven entirely by bug reports from people who own the
hardware. If you have a VMB-series hub, this is what the integration does
for you:

- **Hubs are gateways, not cameras.** A base station is filtered out of
  the camera platform by `deviceType` — no phantom camera entity whose
  live view can never work. It stays in the device list as a gateway for
  routing, mode control and connectivity.
- **Commands route through the hub.** Arlo rejects snapshot, siren,
  spotlight and stream requests addressed to a camera that sits behind a
  base station (errors `4006` and `2217`). Every per-device command is
  addressed to the controlling `parentId`, with the camera kept as the
  subject of the request.
- **The hub's event stream is registered, and renewed.** Being granted a
  topic by Arlo's MQTT broker is not the same as the hub agreeing to
  talk: an older base station publishes *nothing* until the session is
  explicitly registered with it. The integration registers at startup and
  renews every 25 minutes, because those registrations expire.
- **Hub-held state is pulled, not waited for.** Cameras behind a hub keep
  battery, signal strength and connection state on the hub, which only
  publishes them when asked. The integration asks — at startup and on
  every health tick — and merges partial updates instead of overwriting,
  so a camera's activity frame can't wipe the battery the hub just
  delivered.
- **One xCloudId per hub.** Accounts whose cameras span several base
  stations get a distinct cloud id per hub. Every REST call carries the
  right one, and MQTT subscribes to every hub's topic tree.
- **Partial MQTT refusals are expected, not fatal.** Shared and multi-hub
  accounts routinely get some topic filters refused. The integration
  subscribes to the exact per-device filters Arlo advertises and treats a
  partial refusal as normal.
- **Duplicate hub records are collapsed.** A base station with a built-in
  siren is returned twice by Arlo, as itself and as a `-siren` twin.
- **One mode select per location**, resolved through each hub's
  `gatewayDeviceIds` — including locations shared to you from another
  Arlo account, which live in a different part of the API entirely.

## Installation

### HACS (recommended)

The fastest path: click the **Open in HACS** badge above. It opens your
HA instance's HACS UI prepared to add this repo as a custom integration
— review and confirm.

Manual HACS path:

1. HACS → Integrations → ⋮ → **Custom repositories** → add
   `https://github.com/vjt/ha-eisenberg` with category **Integration**.
2. Install **Eisenberg (Arlo)**.
3. Restart Home Assistant.
4. Settings → Devices & Services → **Add Integration** → search
   "Eisenberg".

### Manual

Copy `custom_components/eisenberg/` into your HA `custom_components/`
directory, install the `pyeisenberg` Python package into the HA Python
environment, and restart.

## Configuration

The config flow asks for your Arlo email and password.

**Since 6 October 2026, Arlo has two-step verification disabled
service-wide** — the 2FA settings are gone from the Arlo Secure app and
from my.arlo.com. While that holds, setup is just email and password:
there is no second factor to satisfy and the integration doesn't ask for
one. If Arlo turns it back on, the flow below returns automatically.

When MFA is live:

- If your browser is already trusted at Arlo, login is silent — no
  verification step needed.
- Otherwise, you pick how Arlo should verify this device. Push
  notification, email or SMS — whichever factors your account has
  enabled. Push is fastest; email/SMS is the fallback if the Arlo
  Secure app on your phone hasn't been opened recently.

  <img src="docs/factor-picker.png" alt="Factor picker" width="480">

- Push factor: approve the notification on your phone, then click
  **Submit**. Email/SMS factor: type the one-time code Arlo just sent
  you. Each click is a single API call — no polling — so rate-limit
  risk stays low.

After login you pick a media storage location (or **Disabled** to skip
archival).

Already configured? Settings → Devices & Services → **Eisenberg** →
⋮ → **Reconfigure** re-runs the verification step (useful if your
trust cookie expired, or you want to switch factor).

### Options

- **Storage Location** — change the archive directory, or disable
  archival.
- **Detection sensor reset timeout** — how long person/vehicle/animal
  binary sensors stay on after a detection (default 30 s).
- **Archived media retention** — days to keep on disk (default 14).
- **Route stream through ffmpeg** — off by default. Home Assistant's
  go2rtc reads Arlo's RTSPS natively, in-process, with HEVC passthrough.
  Turn this on only if live view comes up black on your camera, which
  means go2rtc's native RTSP client can't read that particular stream;
  it costs an extra remux hop.

## Streaming

Live view is RTSP-over-TLS on port 443, forced to TCP with low-delay
decoder flags, which is what keeps lag sub-second. Arlo gates the
protocol on the User-Agent: a browser UA gets DASH, a mobile UA gets
RTSP, so the stream call — and only the stream call — presents a mobile
UA.

An Arlo stream URL is valid only for the session that minted it, which
is unlike an ordinary RTSP camera and trips up Home Assistant's
assumption that a camera's source is stable. The integration therefore
serialises concurrent stream requests onto a single Arlo session,
reuses a fresh URL briefly rather than waking the camera twice, and
drops the cached stream as soon as its session ends — so the next viewer
gets a live URL instead of a dead one.

## Services

### `eisenberg.snapshot`

Request a fresh full-frame snapshot from a camera. The image arrives
asynchronously via MQTT and refreshes the camera tile. Fails with a
clear error if the camera is in standby (Arlo refuses cloud snapshots
while disarmed).

```yaml
service: eisenberg.snapshot
target:
  entity_id: camera.front_door
```

## Events

The integration fires `eisenberg_media` events on motion detection
with `device_id`, `category`, `categories`, `content_url`,
`thumbnail_url`, `duration`, `timestamp`. Use these in automations to
log clips elsewhere or trigger downstream actions.

## Architecture

- **`eisenberg/`** — pure async Arlo client. REST + raw MQTT 3.1.1
  over WebSocket. Pydantic models for every payload.
- **`custom_components/eisenberg/`** — the HA integration. A single
  coordinator owns the client + MQTT stream and pushes state to
  entities via `_handle_coordinator_update`.

MQTT is the primary data source, not a supplement: REST is used for
commands and for discovery, and everything else arrives as an event. The
periodic tick exists only for the things that genuinely need a clock —
token refresh, hub re-registration, media pruning, MQTT reconnection.

## Why another Arlo integration?

The existing reference is [pyaarlo](https://github.com/twrecked/pyaarlo)
and the HA integration that wraps it. It works, it's been kept alive
heroically, and we owe it the protocol reverse-engineering this client
stands on. But it carries years of accreted workarounds for problems
Arlo has since stopped having:

- **No Cloudflare bypass.** pyaarlo ships `cloudscraper` because the
  old Arlo endpoints were behind a JS challenge. The current
  `ocapi-app.arlo.com` / `myapi.arlo.com` endpoints accept plain
  aiohttp with normal browser headers — the same flow `my.arlo.com`
  uses.
- **No User-Agent spoofing tricks.** We use a vanilla Chrome UA for
  REST and a mobile UA only where Arlo gates RTSP on it (the
  `startStream` endpoint returns DASH for browsers, RTSP for the
  mobile app). Two distinct paths, both documented.
- **No 2FA email polling, no IMAP scraping.** Auth is driven by the
  same browser-trust cookie flow Arlo's own web app uses: one push to
  your phone, one click in HA, then a 14-day trusted-browser cookie.
  No polling, no inbox access, no rate-limit risk from background
  retries.
- **No sync-over-thread-pool fake async.** The whole client is native
  `aiohttp` + `asyncio`. MQTT 3.1.1 is implemented from scratch over
  the existing aiohttp WebSocket session — no `paho-mqtt`, no second
  TCP stack to keep alive.
- **Typed at the boundary.** Every Arlo API/MQTT payload lands in a
  Pydantic model. Unknown shapes log loudly instead of silently
  succeeding — that's how new MQTT topics surfaced during development.

This is still a smaller surface than pyaarlo's and not a drop-in
replacement: there's no locally-stored video, no IMAP 2FA fallback, and
the device matrix is narrower. Base stations, however, are no longer on
that list — see above.

## Hardware support

The only hardware the author owns is an **Arlo Essential XL HD
(VMC2052A)** — battery + solar, WiFi, cloud-only. Everything else below
works because somebody filed a bug report, sent a debug log, and
confirmed the fix on their own account. That is the honest provenance,
and it's worth saying out loud.

| Hardware | How it got supported |
| --- | --- |
| Essential XL HD (VMC2052A) | Author's own camera; the development rig |
| 2K Essential XL (VMC3052A) | Reported black live view; fixed via the ffmpeg stream option |
| Pro 2 base station (VMB4000) | Reported, diagnosed and fixed across several releases |
| Ultra / Pro 3 SmartHub (VMB5000) | Reported; hub registration and state pull field-confirmed by the reporter |
| Ultra (VMC5040) | Behind a VMB5000, same reports |
| Pro-series cameras (VMC4030, VMC4060) | Behind hubs, in reporters' accounts |
| Video Doorbell (FB1001A) | Publishes on a separate `doorbells` resource, handled |
| All-in-One Sensor (MS1001) | Contact and battery; a user's captures showed where the contact state arrives |

Device *types* are handled generically — `basestation`, `arlobridge` and
`hub` are treated as gateways; `sensors` as all-in-one sensors;
`camera`, `doorbell` and `arloq` as cameras — so a model not listed here stands a good chance of working.
**If yours doesn't, file an issue with a debug log.** Every row in that
table above the first one exists because somebody did.

## Limitations

- All control flows through Arlo's cloud — there's no local API on
  these cameras.
- Arlo refuses on-demand snapshots while a camera is in standby. The
  tile keeps showing the last cached image instead.
- When Arlo has MFA enabled, the trust cookie it issues lasts about 14
  days. When it expires, HA fires a reauth; one click re-fires a push.
- Arlo changes things without notice — they disabled MFA service-wide on
  6 October 2026 and broke every third-party client within the hour. If
  the integration suddenly can't log in, check the issue tracker before
  assuming it's your account.

## Development

```bash
./scripts/check.sh    # pyright + pytest + ruff
```

The deploy skill (`/eisenberg-deploy`) pushes to a HAOS box over SSH.
The release skill (`/eisenberg-release`) cuts PyPI + GitHub releases.

## License

MIT.
