Skip to content

feat: Litter-Robot 5 Pro camera recording with visit tracking#1

Open
Legendberg wants to merge 1227 commits intodevfrom
litterrobot-lr5-basic-support
Open

feat: Litter-Robot 5 Pro camera recording with visit tracking#1
Legendberg wants to merge 1227 commits intodevfrom
litterrobot-lr5-basic-support

Conversation

@Legendberg
Copy link
Owner

@Legendberg Legendberg commented Feb 25, 2026

Summary

LR5 Pro camera integration for Home Assistant with local video recording — no Whisker cloud subscription required.

Camera & Recording

  • Camera entity with WebRTC live streaming (front/globe view switching)
  • Local recording pipeline — WebRTC + PyAV + ffmpeg faststart, triggered by visit and cycle events
  • Media source — browse and play back local recordings from the HA media browser
  • HTTP serving — custom API view with range request support for mobile playback
  • Options flow — enable/disable recording, duration, retention settings

Sensors & Controls

  • Per-pet visit sensors with daily aggregates (visit count, total duration, avg weight)
  • Reassign/unassign button entities — fix AI pet misidentifications from the dashboard
  • Camera controls: mic switch, camera view select (front/globe), night vision, status light
  • Event sensor with pet name resolution ("Willow - Pet visit")

Services

  • litterrobot.start_recording — manually trigger a recording session
  • litterrobot.reassign_visit — reassign or unassign a pet visit (config_entry_id auto-detected)

Technical Notes

  • Whisker cloud video returns 403 without subscription — local WebRTC recording bypasses this
  • ICE signaling uses Whisker's turnUrl/stunUrl/password format
  • LR5 uses REST polling (no WebSocket push), cycle recording start has ~10-15s cloud API propagation delay

Testing

  • Recording playback verified on Android + iOS
  • Fixed recordings (cycle, standalone pet_visit) confirmed working
  • Visit recording (cat_detect flow) awaiting real event verification

Dependencies

@Legendberg
Copy link
Owner Author

Progress Update (2026-02-25)

New in this push

  • Full-duration cycle recording: Cycles now record start-to-finish (~2 min) instead of fixed 30s clips. Recording starts when robot enters CLEAN_CYCLE state and stops when cycle completes + 5s grace period.
  • Robot state transition detection: 10s polling via robot.refresh() detects CLEAN_CYCLE (cycling start) and CAT_DETECTED (cat near box) in real-time.
  • Camera videos API polling: cat_detected events come from the camera videos endpoint, not the activities API. Added separate polling for this.
  • Refactored fast poll: Activities, camera videos, and robot state checks now run independently — a failure in one doesn't skip the others.
  • Deduplication: cycle_completed activity events now signal active cycle recordings instead of creating duplicate fixed recordings.

Verified working

  • Full cycle recording: triggered manually, recorded ~2 min of cycling on globe camera (17MB vs old 2.8MB 30s clips)
  • State transitions detected correctly: Ready → Clean Cycle In Progress → Ready
  • Visit recording from camera videos API (Willow visit captured)
  • Recording saved with faststart, plays on Android + iOS

Still needs testing

  • Visit recording triggered by CAT_DETECTED robot state (needs real cat approach)
  • Full visit flow: cat_detect → front camera → globe switch → pet_visit signal → stop
  • Cat interrupting a cycle (should stop recording on CAT_SENSOR_INTERRUPTED)

@Legendberg Legendberg marked this pull request as ready for review March 2, 2026 01:46
Legendberg added a commit to Legendberg/pylitterbot that referenced this pull request Mar 2, 2026
This library change is one half of a two-part update. The companion
Home Assistant integration PR (Legendberg/core#1) builds on top of
these changes to deliver:
- Local MP4 recording of every litter box visit and cleaning cycle
- HTTP serving of recordings with Range request support for mobile playback
- Media source browser in HA ("Browse Snapshots") for reviewing recordings
- Per-pet visit sensors with daily aggregates (weight, waste, duration)
- Reassign/unassign visit button entities in the HA UI
- Example Lovelace dashboard (Whisker Dashboard)

---

## Camera streaming (pylitterbot/camera.py)

Adds full WebRTC camera support for the Litter-Robot 5 Pro:

- `CameraClient`: REST client for the Watford camera API. Handles session
  generation (`generate_session()`), which returns TURN credentials, a
  session token, and a signaling WebSocket URL. Supports `auto_start=False`
  for pre-caching TURN credentials without waking the camera.

- `CameraSignalingRelay`: Manages the browser↔camera WebRTC signaling
  flow. Connects to the Watford signaling WebSocket, forwards the SDP
  offer, receives the SDP answer and ICE candidates, and relays browser
  ICE candidates back to the camera.

- ICE fix: was sending `offer.sdp` (no ICE candidates) instead of
  `localDescription.sdp` (all gathered candidates). aiortc gathers ICE
  candidates synchronously in `setLocalDescription()`.

- TURN credential fix: Whisker uses non-standard keys (`turnUrl`,
  `stunUrl`, `password`) instead of the standard WebRTC format
  (`urls`, `credential`). Both formats are now handled.

- Late ICE candidate reconnect: the Watford signaling WebSocket closes
  ~17 s after the offer is sent (intentional server behaviour, after
  delivering answer + ICE candidates). Browser ICE candidates arriving
  after closure are buffered in `_pending_candidates` and forwarded via
  a lightweight reconnect on the same `sessionToken`. A faster ping
  interval (5 s, down from 15 s) keeps the connection alive until the
  server closes it.

- `LitterRobot5.get_camera_client()` returns a `CameraClient` for the
  robot's camera, raising `CameraNotAvailableException` when the robot
  has no camera or the metadata is missing.

## Pet visit reassignment (robot/litterrobot5.py)

- `reassign_pet_visit(event_id, from_pet_id, to_pet_id)`: PATCHes the
  Whisker activities endpoint to reassign or unassign a litter box visit.
  Omit `to_pet_id` to unassign; omit `from_pet_id` to assign without
  removing from another pet. Returns the updated activity dict.

## Camera mic control (robot/litterrobot5.py)

- `set_audio_enabled(value)`: enables/disables the camera microphone via
  the camera settings API. Updates local state and emits EVENT_UPDATE.

## Bug fix: sleep_mode_enabled (robot/litterrobot5.py)

- `sleep_mode_enabled` now handles both list-shaped and dict-shaped
  `sleepSchedules` responses from the API.

## Dependencies (pyproject.toml)

- Adds `aiortc>=1.9.0`, `aiohttp>=3.9.0`, `av>=11.0.0` for WebRTC/media.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Legendberg Legendberg force-pushed the litterrobot-lr5-basic-support branch from 235af56 to 8bb609b Compare March 2, 2026 01:59
Legendberg added a commit to Legendberg/pylitterbot that referenced this pull request Mar 6, 2026
This library change is one half of a two-part update. The companion
Home Assistant integration PR (Legendberg/core#1) builds on top of
these changes to deliver:
- Local MP4 recording of every litter box visit and cleaning cycle
- HTTP serving of recordings with Range request support for mobile playback
- Media source browser in HA ("Browse Snapshots") for reviewing recordings
- Per-pet visit sensors with daily aggregates (weight, waste, duration)
- Reassign/unassign visit button entities in the HA UI
- Example Lovelace dashboard (Whisker Dashboard)

---

Adds full WebRTC camera support for the Litter-Robot 5 Pro:

- `CameraClient`: REST client for the Watford camera API. Handles session
  generation (`generate_session()`), which returns TURN credentials, a
  session token, and a signaling WebSocket URL. Supports `auto_start=False`
  for pre-caching TURN credentials without waking the camera.

- `CameraSignalingRelay`: Manages the browser↔camera WebRTC signaling
  flow. Connects to the Watford signaling WebSocket, forwards the SDP
  offer, receives the SDP answer and ICE candidates, and relays browser
  ICE candidates back to the camera.

- ICE fix: was sending `offer.sdp` (no ICE candidates) instead of
  `localDescription.sdp` (all gathered candidates). aiortc gathers ICE
  candidates synchronously in `setLocalDescription()`.

- TURN credential fix: Whisker uses non-standard keys (`turnUrl`,
  `stunUrl`, `password`) instead of the standard WebRTC format
  (`urls`, `credential`). Both formats are now handled.

- Late ICE candidate reconnect: the Watford signaling WebSocket closes
  ~17 s after the offer is sent (intentional server behaviour, after
  delivering answer + ICE candidates). Browser ICE candidates arriving
  after closure are buffered in `_pending_candidates` and forwarded via
  a lightweight reconnect on the same `sessionToken`. A faster ping
  interval (5 s, down from 15 s) keeps the connection alive until the
  server closes it.

- `LitterRobot5.get_camera_client()` returns a `CameraClient` for the
  robot's camera, raising `CameraNotAvailableException` when the robot
  has no camera or the metadata is missing.

- `reassign_pet_visit(event_id, from_pet_id, to_pet_id)`: PATCHes the
  Whisker activities endpoint to reassign or unassign a litter box visit.
  Omit `to_pet_id` to unassign; omit `from_pet_id` to assign without
  removing from another pet. Returns the updated activity dict.

- `set_audio_enabled(value)`: enables/disables the camera microphone via
  the camera settings API. Updates local state and emits EVENT_UPDATE.

- `sleep_mode_enabled` now handles both list-shaped and dict-shaped
  `sleepSchedules` responses from the API.

- Adds `aiortc>=1.9.0`, `aiohttp>=3.9.0`, `av>=11.0.0` for WebRTC/media.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Poshy163 and others added 22 commits March 6, 2026 20:12
Co-authored-by: Robert Resch <robert@resch.dev>
…tant#164264)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
masterkoppa and others added 29 commits March 15, 2026 07:09
…nt#165288)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…ssistant#165503)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds Home Assistant entity support for the Litter-Robot 5 Pro camera
and expands LR5 platform coverage with new entity types.

- `LitterRobotCameraEntity` supports WebRTC live streaming via
  `async_handle_async_webrtc_offer()` / `async_on_webrtc_candidate()`.
- Delegates signaling to `CameraSignalingRelay` from pylitterbot, which
  relays SDP offer/answer and ICE candidates to the Watford signaling
  server.
- Pre-caches a camera session on startup (`auto_start=False`) to have
  TURN credentials ready before the first stream request, without waking
  the camera unnecessarily.
- Checks session expiration before each stream; refreshes in background
  if stale.
- `async_camera_image()` returns the latest cloud video thumbnail.

- **Light** (`light.py`): globe night light with brightness and mode
  (on/off/auto/random).
- **Event** (`event.py`): camera motion event entity that fires on
  pet_visit, cat_detect, cycle_completed, and related activity types.
- **Time** (`time.py`): sleep mode start/end time configuration.
- **Binary sensor** (`binary_sensor.py`): LR5-specific sensors —
  sleeping, hopper connected, laser dirty, bonnet/drawer removed.

- `entity.py`: adds `LitterRobot5` to the typed entity generic and
  exposes `robot.has_camera` guard for camera-specific entities.
- `select.py`: adds camera view select (globe/front) for LR5 Pro.
- `switch.py`: adds camera enable and microphone enable switches.
- `manifest.json`: adds `aiortc>=1.9.0` dependency for WebRTC support.
- `icons.json`: adds icons for new entity types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records every litter box visit and cleaning cycle locally as MP4 files,
served via a custom HA HTTP view and browseable in the media browser.
Enabled and configured through a new options flow entry.

`RecordingManager` drives three recording modes:

- **Visit recording** (`trigger_visit_recording()`): starts on `cat_detect`,
  switches camera view from front → globe, records until `pet_visit`
  signals completion or a max-duration timeout. File is renamed with the
  pet name on completion (e.g. `YYYYMMDD_HHMMSS_VISIT_Willow.mp4`).

- **Cycle recording** (`trigger_cycle_recording()`): starts when robot
  enters `CLEAN_CYCLE` status, records continuously until
  `signal_cycle_complete()` or a max-duration timeout.

- **Fixed-duration recording** (`trigger_recording()`): used for
  standalone events (e.g. a `pet_visit` with no prior `cat_detect`).

Files are written as `YYYYMMDD_HHMMSS_{EVENT_TYPE}[_{PetName}].mp4`
under `<config>/media/litterrobot/<serial>/`. PyAV encodes H.264/yuv420p
and ffmpeg post-processes with `-movflags faststart` for immediate
playback before download completes.

`LitterRobotRecordingView` serves recordings at
`/api/litterrobot/recordings/{serial}/{filename}`. Uses
`aiohttp.web.FileResponse` which handles HTTP Range requests natively,
enabling seek and mobile playback. Filename is validated to prevent path
traversal.

`LitterRobotMediaSource` exposes recordings in the HA media browser
("Browse Snapshots"). Lists MP4 files newest-first per robot, with
human-readable titles parsed from the filename stem:
- `PET_VISIT_Willow` → "Pet Visit (Willow) - 2026-03-01 17:05"
- `CYCLE_COMPLETED`  → "Cycle Completed - 2026-03-01 17:05"
- `VISIT`            → "Visit (Unassigned) - 2026-03-01 17:05"

Adds a recording options step with:
- Enable/disable recording toggle
- Recording duration (seconds)
- Retention period (days) — old recordings pruned automatically
- Event type multi-select (pet_visit, cat_detect, cycle_completed, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hboard

`LitterRobotPetLastVisitSensor` — one entity per pet, under the pet's
own HA device:
- State: most recent event label (e.g. "Pet Visit")
- Attributes: `timestamp`, `duration`, `pet_weight`, `waste_type`,
  `waste_weight_oz`, `pet_id`, `event_id`, `is_reassigned`
- Daily aggregates: `visits_today`, `urine_today`, `feces_today`,
  `urine_weight_today_oz`, `feces_weight_today_oz`, `total_duration_today`

`LitterRobotLastEventSensor` — last activity across all event types on
the robot device, with the same attribute set.

- Activity cache: warm-up fetch on first poll (up to 100 pet_visit
  activities) for accurate daily aggregates; subsequent polls merge new
  activities incrementally using `messageId` deduplication.
- Fast camera poll (10 s): polls activities and camera videos for new
  events, triggers recordings via `RecordingManager`.
- Fast state poll (3 s): polls robot status for `CLEAN_CYCLE` detection
  to start cycle recordings with minimal latency.
- `EVENT_UPDATE` subscription: fires immediately on any robot state
  change, providing a real-time path for cycle/visit detection alongside
  the polling fallback.
- Camera thumbnail download on each 5-minute coordinator refresh.

`litterrobot.reassign_visit` service:
- `event_id`: the activity event to reassign
- `from_pet_id` / `to_pet_id`: omit either to assign/unassign
- `config_entry_id`: optional — auto-detected when only one entry exists

- `ReassignVisitButton`: one per pet pair (e.g. Willow→Loki, Loki→Willow).
  Pressing reassigns the pet's most recent visit to the target pet.
- `UnassignVisitButton`: one per pet. Pressing removes the pet assignment
  from their most recent visit.

A ready-to-use Lovelace dashboard for the full integration. Requires
`lovelace_gen` and `fold-entity-row` from HACS. Covers:
- Robot status, actions, controls, night light, camera, sleep schedule,
  alerts, and diagnostics sections
- Feeder-Robot status, actions, and controls
- Per-pet activity and last-visit detail with reassign buttons
- Last litter box visit summary across all robots

Adds translation keys for all new entities: per-pet sensors, event
sensor, reassign/unassign buttons, and the reassign service.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix cooldown reset key format in manual recording service — was
  using plain serial string but _last_trigger_times uses (serial,
  event_type) tuples, so the pop never matched
- Fix _apply_faststart deleting temp file before checking ffmpeg
  return code — recording was silently lost on ffmpeg failure
- Fix path traversal in HTTP recording view — use Path.resolve()
  with is_relative_to() instead of string-matching for ".."
- Fix blocking I/O in _finalize_recording — move all file operations
  into executor job to avoid blocking the event loop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix duplicate unique IDs: remove LR5 litter_level/pet_weight
  duplicated between shared (LR4,LR5) and LR5-only sensor entries
- Fix duplicate panel_brightness select: split LR4 into own key
- Add noqa: BLE001 for intentional broad exception catches
- Replace try/except/pass with contextlib.suppress
- Fix import sorting and formatting (ruff)
- Guard hass.http.register_view for test environments
- Remove unused test imports, fix PET_DATA redefinition
- Add check=False to subprocess.run call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Revert button map to upstream's single-description-per-key pattern
  with tuple type keys, add (LitterRobot5,) for change_filter
- Use translation-based HomeAssistantError in ReassignVisitButton and
  UnassignVisitButton instead of raw English strings
- Wrap reassign_pet_visit calls in LitterRobotException handler
- Add translation_key to ReassignVisitButton with pet_name placeholder
- Add exception translations: no_recent_visit, visit_no_event_id,
  robot_not_found, reassign_failed
- Move LR5/LR5Pro test data from inline common.py to JSON fixture
  files matching upstream's fixture pattern
- Fix test_light ROBOT_5_DATA import after fixture migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix command_exception tests: match translation key (command_failed,
  firmware_update_failed) instead of English error text
- Fix LR5 select duplicates: move globe_brightness, globe_light, and
  panel_brightness to LR4-only since LR5 uses its own night light
  entity and LR5-specific brightness/mode enums
- All 79 tests pass (77 teardown errors are upstream vacuum
  clean_area.name translation issue)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @whisker_command to camera switch, mic switch, night light,
  and camera view select methods for consistent exception handling
- Convert all raw English HomeAssistantError strings to translation-
  based errors in camera.py and services.py
- Add explicit _attr_translation_key to LitterRobotNightLight
- Add translations: camera_not_available, camera_stream_failed,
  recording_not_enabled, no_cameras_found, activity_not_found

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add PARALLEL_UPDATES to camera.py and light.py
- Add start_recording service to services.yaml and strings.json
- Add service icons for start_recording and reassign_visit
- Remove invalid options.selector section from strings.json
- Fix placeholder quoting in activity_not_found translation
- Sync translations/en.json with strings.json (all new exceptions,
  entities, and services)
- Revert test match patterns to translated messages now that
  translations load correctly
- hassfest validates clean, all 79 tests pass with 0 errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Legendberg Legendberg force-pushed the litterrobot-lr5-basic-support branch from 3c4fc81 to ce0e513 Compare March 16, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.