Skip to content

Per Camera Snooze - #1273

Open
austinc3030 wants to merge 4 commits into
fronzbot:devfrom
dynacylabs:feature/per-camera-snooze
Open

Per Camera Snooze#1273
austinc3030 wants to merge 4 commits into
fronzbot:devfrom
dynacylabs:feature/per-camera-snooze

Conversation

@austinc3030

Copy link
Copy Markdown
Contributor

Description:

Adds async_snooze() and a snoozed property to BlinkCamera so individual cameras can be snoozed. A new request_camera_snooze() API function handles routing to the right endpoint per product type (catalina/sedona → cameras, owl/hawk → owls, doorbell/lotus → doorbells).

Checklist:

  • Local tests with tox run successfully PR cannot be meged unless tests pass
  • Changes tested locally to ensure platform still works as intended
  • Tests added to verify new code works

- Add snoozed async property to BlinkCamera (checks all camera types)
- Add async_snooze() method to BlinkCamera
- Add request_camera_snooze() API function routing by product type
- Tests
Copilot AI review requested due to automatic review settings July 20, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-camera snoozing support to BlinkPy by exposing a camera-level snooze API and a convenient camera property for reading snooze state, with product-type-specific endpoint routing.

Changes:

  • Added BlinkCamera.async_snooze() to set snooze duration and refresh homescreen for applicable product types.
  • Added BlinkCamera.snoozed async property to report current snooze state for both config-based and homescreen-based devices.
  • Added api.request_camera_snooze() plus tests covering routing and camera behaviors.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
tests/test_cameras.py Adds test coverage for per-camera snooze and snooze-state reading across product types and edge cases.
tests/test_api.py Adds API-level test ensuring request_camera_snooze() routes per product type and returns expected responses.
blinkpy/camera.py Implements snoozed async property and async_snooze() on BlinkCamera.
blinkpy/api.py Extends request_get_config() for sedona and introduces request_camera_snooze() endpoint routing helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread blinkpy/camera.py
Comment on lines +201 to +205
for device in self.sync.blink.homescreen.get(collection_key, []):
if int(device.get("id")) == int(self.camera_id):
snooze_value = device.get("snooze")
return bool(snooze_value)
return False

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 70046de — replaced int() ID comparison in snoozed with str() to prevent TypeError from aborting the search loop on missing/non-numeric IDs, and extended request_update_config() to accept sedona alongside catalina to match request_get_config().

Comment thread blinkpy/api.py
Comment on lines +581 to 582
elif product_type in ["catalina", "sedona"]:
url = f"{blink.urls.base_url}/network/{network}/camera/{camera_id}/config"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit 70046de — replaced int() ID comparison in snoozed with str() to prevent TypeError from aborting the search loop on missing/non-numeric IDs, and extended request_update_config() to accept sedona alongside catalina to match request_get_config().

@sebguischr

Copy link
Copy Markdown

I tested this branch against a live account (4 cameras: catalina, owl, chickadee, sonoran) while wiring the matching action up in Home Assistant core. Three findings, all measured rather than inferred.

1. snooze_time is in minutes, not seconds

The docstring says "Time in seconds to snooze camera. Default is 3600 (1 hour)". Sending snooze_time: 300 produced:

snooze_till            = 2026-08-10T11:24:53+00:00   (299.8 minutes away)
snooze_time_remaining  = 299

So 300 was read as 300 minutes, and snooze_time_remaining counts down in minutes too. The current default of 3600 therefore asks for 60 hours, which the API refuses (see below). A default of 60 would match the documented "1 hour".

2. Accepted range is 1..1439 minutes; 0 does not cancel

snooze_time=0     -> {'message': 'Unsupported value for snooze time', 'code': 2800}
snooze_time=-1    -> {'message': 'Unsupported value for snooze time', 'code': 2800}
snooze_time=1     -> {'message': 'Snooze set for Camera'}
snooze_time=1439  -> {'message': 'Snooze set for Camera'}
snooze_time=1440  -> {'message': 'Unsupported value for snooze time', 'code': 2800}

Found by bisection between 300 and 1440. Two consequences:

  • There is no documented way to clear a snooze outright. snooze_time: 1 is the practical cancel: it moves snooze_till to roughly now and snooze_time_remaining to 0.
  • Rejections come back as HTTP 200 with a code in the body, so callers cannot rely on None alone to detect failure. async_snooze currently returns that body as-is, which reads as success.

3. product_lookup is missing two product types

sonoran and chickadee both fell through to the "not implemented" branch and returned None. Probing the segments directly:

  • sonoran -> cameras ({'message': 'Snooze set for Camera'})
  • chickadee -> owls ({'message': 'Snooze set for Owl'})
     product_lookup = {
         "catalina": "cameras",
         "sedona": "cameras",
+        "sonoran": "cameras",
         "owl": "owls",
         "hawk": "owls",
+        "chickadee": "owls",
         "doorbell": "doorbells",
         "lotus": "doorbells",
     }

Caveat on chickadee: the cameras attempt returned a connection error rather than a clean rejection, so that mapping rests on the explicit "Snooze set for Owl" success rather than on the other segment failing cleanly.

4. Snooze state is not exposed on the camera, and the two model families report it differently

The async snoozed property costs an extra request_get_config round trip per read, which makes it awkward for a polling consumer like Home Assistant. The data is already in the payload each camera is updated from — but not in the same shape:

  • wired cameras (catalina, sonoran): request_camera_info returns snooze_till, an absolute ISO timestamp, and no snooze / snooze_time_remaining
  • minis and doorbells (owl, chickadee): the homescreen entry returns snooze (bool) and snooze_time_remaining (minutes), and no snooze_till

Observed on a live account:

Caméra jardin   catalina   snooze_till='2026-08-10T06:29:44+00:00'   (no snooze key)
Caméra cellier  owl        snooze=False, snooze_time_remaining=None  (no snooze_till key)

Normalising both into snooze / snooze_time_remaining during extract_config_info costs no extra request:

def extract_snooze_info(self, config):
    """Normalize snooze status across camera models."""
    self.snooze_till = config.get("snooze_till")
    remaining = config.get("snooze_time_remaining")
    snoozed = config.get("snooze")

    if remaining is None and self.snooze_till:
        try:
            until = datetime.datetime.fromisoformat(self.snooze_till)
        except (TypeError, ValueError):
            _LOGGER.warning(
                "Could not parse snooze_till %r for %s", self.snooze_till, self.name
            )
        else:
            if until.tzinfo is None:
                until = until.replace(tzinfo=datetime.timezone.utc)
            seconds = (
                until - datetime.datetime.now(datetime.timezone.utc)
            ).total_seconds()
            remaining = max(0, math.ceil(seconds / 60))

    self.snooze_time_remaining = remaining
    self.snooze = bool(remaining) if snoozed is None else bool(snoozed)

called from extract_config_info, with snooze and snooze_time_remaining added to the attributes dict. A stale snooze_till in the past correctly yields remaining=0, snooze=False.

Happy to open a PR against your branch with the lookup entries, the docstring fix and this normalisation if that is easier than folding it in here.

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.

3 participants