Skip to content

feat(birdnet-go): REST-first, a stats screen, and one distinct species per slot (1.2.0) - #253

Merged
ChuckBuilds merged 3 commits into
mainfrom
feat/birdnet-go-stats
Aug 5, 2026
Merged

feat(birdnet-go): REST-first, a stats screen, and one distinct species per slot (1.2.0)#253
ChuckBuilds merged 3 commits into
mainfrom
feat/birdnet-go-stats

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Brings the BirdNET-Go plugin onto current main and makes it work against a real feed. Supersedes feat/birdnet-go-plugin, which was never opened as a PR and is now 136 commits behind.

The first commit re-lands that branch byte-for-byte so the second reads as a reviewable diff rather than a thousand lines of new code.

Bugs fixed

  • cache_manager.set() was called with max_age, but the signature is set(key, data, ttl=None). Both calls raised TypeError inside a try/except that logged at debug, so the failure was invisible: the last detection never survived a restart, and the 30-day species-image cache never worked — every boot re-fetched every photo.
  • versions[] was ascending. Every other plugin is newest-first, and compatibility.declared_min_version reads versions[0].

What's new

MQTT is now optional. BirdNET-Go's REST API serves detections, daily analytics and species images, so the plugin polls it directly. Setup is one line — your base URL — with no broker user, password or topic to configure. MQTT stays available via mqtt.enabled for sub-second interrupt pop-ups, and polling keeps running alongside it so a broker outage can't freeze the panel.

A second screen, birdnet_stats — today's species count, total detections, and the most-heard species with counts.

The detection screen cycles distinct species. This is the change that matters most in practice. A busy yard is ~90% two loud species, so "show the latest bird" showed those two nearly every slot while the other nine never appeared:

raw /detections/recent  ->  8x Fish Crow, 2x Downy Woodpecker

Now one distinct species per rotation slot, each card carrying that species' count for the day:

slot 1: Blue Jay          x272    slot 5: Chimney Swift     x7
slot 2: Fish Crow         x265    slot 6: Cooper's Hawk     x9
slot 3: Downy Woodpecker   x17    slot 7: Northern Cardinal x5
slot 4: House Finch         x7    slot 8: Red-tailed Hawk   x9

species_order: "frequency" turns it into a top-N countdown instead. unique_species: false restores the old behaviour. Interrupts always show the bird that just called, never the cycle's current card.

The cycle is built from /analytics/species/daily rather than the detection stream, because /detections/recent caps at ten rows regardless of numResults — and on a real feed those ten rows are often a single species. One consequence worth knowing: the cycle only covers species heard today, so it's short first thing in the morning and grows through the day.

Layouts are size-adaptive. The old two-line layout stranded text in the middle of a 512-wide panel. Tall panels now get a third line for the scientific name and larger type; narrow panels drop detail rather than truncating it (97% instead of 97.).

Verification

  • Core harness passes on 9 panel sizes × both display modes, rendered against a live BirdNET-Go instance.
  • Deployed to a 512x64 device and confirmed on the physical panel: species photo, common name, scientific name, confidence, time-ago and daily count all render correctly, plugin loads clean, both modes register, no errors.
  • Species cycling driven end-to-end against the live API: 8 distinct species across 10 consecutive slots, zero back-to-back repeats, in both recent and frequency ordering.
  • check_manifest_version_fields.py and check_module_collisions.py both pass.

Reviewer notes

  • plugins.json was regenerated by the pre-commit hook, not hand-edited.
  • The new config keys (mqtt.enabled, birdnet_api.poll_interval, the stats block, display.unique_species / max_species / species_order / show_today_count, text.accent_color) all default to sensible values, so an existing config keeps working — except that MQTT is now off unless explicitly enabled.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

Summary by CodeRabbit

  • New Features

    • Added the BirdNET-Go integration for displaying detected bird species and daily statistics.
    • Supports REST API polling, optional MQTT notifications, confidence filtering, species cycling, and cached photos.
    • Includes configurable display modes, scrolling text, colors, timing, and payload mappings.
    • Added validation, status reporting, stale-detection handling, and automatic recovery for connection issues.
  • Documentation

    • Added setup instructions, configuration reference, troubleshooting guidance, supported display modes, and licensing information.

claude and others added 2 commits August 5, 2026 15:22
Re-lands the plugin from the unmerged feat/birdnet-go-plugin branch
(fb2c292, a80113d) onto current main, byte-for-byte, so that the rework
in the following commit reads as a reviewable diff instead of arriving
as a thousand lines of new code.

Co-Authored-By: ChuckBuilds <ChuckBuilds@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
…s (1.2.0)

Fix two silent cache failures. `cache_manager.set()` takes `ttl`, not
`max_age`; both calls passed `max_age` and raised TypeError inside a
`try/except` that logged at debug. So the last detection never survived
a restart, and the 30-day species-image cache never worked — every boot
re-fetched every photo.

Make MQTT optional. BirdNET-Go's REST API serves detections, daily
analytics and species images, so the plugin now polls it and setup is
one line: your base URL, no broker credentials. MQTT stays available via
`mqtt.enabled` for sub-second interrupt pop-ups, and polling keeps
running alongside it so a broker outage can't freeze the panel.

Add a `birdnet_stats` screen: today's species count, total detections,
and the most-heard species with their counts.

Cycle distinct species. A busy yard is ~90% two loud species, so "show
the latest bird" showed those two nearly every slot while the other nine
never appeared. The detection screen now advances one distinct species
per rotation slot, each card carrying that species' count for the day;
`species_order: "frequency"` turns it into a top-N countdown. The cycle
comes from the daily analytics endpoint rather than the detection
stream, because `/detections/recent` caps at ten rows — which on a real
feed was eight Fish Crow and two Downy Woodpecker.

Layouts are size-adaptive now: a third line for the scientific name and
larger type on tall panels, and meta text that degrades by dropping
detail ("97%") instead of truncating it ("97."). Verified against a live
instance across nine panel sizes in both modes.

Also reorders `versions[]` newest-first to match every other plugin,
since `compatibility.declared_min_version` reads `versions[0]`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2610933e-bfab-4240-a324-d25103b0d652

📥 Commits

Reviewing files that changed from the base of the PR and between 7d00919 and dfebba6.

📒 Files selected for processing (3)
  • plugins/birdnet-go/config_schema.json
  • plugins/birdnet-go/manager.py
  • plugins/birdnet-go/requirements.txt
📝 Walkthrough

Walkthrough

Adds a BirdNET-Go plugin with REST polling, optional MQTT updates, detection and statistics displays, image caching, configurable rendering, validation, lifecycle management, and catalog metadata.

Changes

BirdNET-Go plugin

Layer / File(s) Summary
Plugin contracts and registration
plugins/birdnet-go/config_schema.json, plugins/birdnet-go/manifest.json, plugins.json, plugins/birdnet-go/requirements.txt, plugins/birdnet-go/README.md, plugins/birdnet-go/LICENSE
Defines configuration, dependencies, manifest metadata, catalog registration, licensing, and setup documentation.
Configuration and payload normalization
plugins/birdnet-go/manager.py
Initializes plugin state and rendering helpers. Normalizes payload fields, timestamps, confidence values, duplicates, cache state, and display triggers.
REST, MQTT, statistics, and image data
plugins/birdnet-go/manager.py
Adds MQTT subscription and reconnect handling, REST polling, daily statistics aggregation, and species-image caching.
Display rendering and lifecycle
plugins/birdnet-go/manager.py
Renders detection and statistics screens. Adds rotation, stale-data handling, validation, lifecycle hooks, cleanup, and status reporting.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BirdNETGoPlugin
  participant MQTTBroker
  participant BirdNETGoAPI
  participant DisplayManager
  BirdNETGoPlugin->>MQTTBroker: subscribe to detection updates
  MQTTBroker-->>BirdNETGoPlugin: deliver detection payload
  BirdNETGoPlugin->>BirdNETGoAPI: poll detections and daily statistics
  BirdNETGoAPI-->>BirdNETGoPlugin: return API data
  BirdNETGoPlugin->>DisplayManager: submit detection or statistics frame
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: REST-first polling, the statistics screen, and distinct species cycling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/birdnet-go-stats

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-production Bot commented Aug 5, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 313 complexity

Metric Results
Complexity 313

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (2)
plugins/birdnet-go/manager.py (2)

904-920: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider bounding the total blocking time of one update() call.

One update() call can make up to five sequential HTTP requests: two API polls plus three image fetches. Each uses api_timeout, which the schema allows up to 30 seconds. If the BirdNET-Go host accepts connections but stalls, a single update() can block for roughly two and a half minutes on the host update path.

Track elapsed time in the image warm-up loop and stop early once a budget is exceeded.

🔧 Proposed fix
         fetched = 0
+        deadline = time.time() + max(2.0, self.api_timeout * 1.5)
         for species in wanted:
             if (not species or species in self._species_img_cache
                     or species in self._species_img_failed):
                 continue
+            if time.time() >= deadline:
+                break
             img = self._fetch_species_image(species)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/birdnet-go/manager.py` around lines 904 - 920, Bound the total
blocking time of update() by tracking elapsed time across the image warm-up loop
and stopping additional image fetches once the allowed budget is exceeded. Reuse
the existing timing context and preserve normal fetching while within the
budget; ensure the bound covers the sequential requests performed during one
update cycle.

935-946: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Bound _species_img_cache and cache the resized frame.

Two related costs come from storing full-resolution source images here:

  1. _species_img_cache has no eviction. It only clears in cleanup(). The set of distinct species grows over days of continuous operation, and every decoded source image stays resident. Memory grows without a ceiling.
  2. _render_detection calls _resize_image on the source image on every frame, so each frame runs a LANCZOS resample of a full-resolution photo.

Cap the cache and store the panel-sized frame instead of the source. The panel dimensions are stable at runtime, so one resize per species is enough.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/birdnet-go/manager.py` around lines 935 - 946, Bound
_species_img_cache with a fixed maximum and evict entries when the limit is
reached, using the existing species-image update flow around
_fetch_species_image. Resize each newly fetched image to the stable panel
dimensions before storing it, then update _render_detection to use the cached
panel-sized frame directly and avoid calling _resize_image on every frame.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/birdnet-go/manager.py`:
- Around line 85-87: Align the `base_url` fallback in the manager initialization
with the `birdnet_api.base_url` default declared in `config_schema.json`; update
the `api_config.get` default used for `self.api_base_url` so omitted
configuration preserves the documented REST endpoint, while leaving URL
normalization unchanged.
- Around line 509-511: Update _poll_daily_stats to accept both a bare list and a
response wrapped in a data field, matching _poll_latest_detection. Unwrap the
data envelope before the existing list validation and continue processing the
resulting list so daily_stats and _recent_species populate for either response
shape.
- Around line 389-442: Update _connect_mqtt to tear down any existing MQTT
client before creating a replacement: stop its loop, disconnect it, and clear
self.mqtt_client, tolerating cleanup exceptions so reconnection can continue.
Ensure this cleanup occurs at the start of each connection attempt and does not
disrupt the existing setup and error handling.

---

Nitpick comments:
In `@plugins/birdnet-go/manager.py`:
- Around line 904-920: Bound the total blocking time of update() by tracking
elapsed time across the image warm-up loop and stopping additional image fetches
once the allowed budget is exceeded. Reuse the existing timing context and
preserve normal fetching while within the budget; ensure the bound covers the
sequential requests performed during one update cycle.
- Around line 935-946: Bound _species_img_cache with a fixed maximum and evict
entries when the limit is reached, using the existing species-image update flow
around _fetch_species_image. Resize each newly fetched image to the stable panel
dimensions before storing it, then update _render_detection to use the cached
panel-sized frame directly and avoid calling _resize_image on every frame.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c93fbe-fded-4a8a-912c-ff5f43f60ada

📥 Commits

Reviewing files that changed from the base of the PR and between 99b8c99 and 7d00919.

📒 Files selected for processing (7)
  • plugins.json
  • plugins/birdnet-go/LICENSE
  • plugins/birdnet-go/README.md
  • plugins/birdnet-go/config_schema.json
  • plugins/birdnet-go/manager.py
  • plugins/birdnet-go/manifest.json
  • plugins/birdnet-go/requirements.txt

Comment on lines +85 to +87
self.api_base_url = str(api_config.get('base_url', '') or '').rstrip('/')
self.api_timeout = float(api_config.get('request_timeout', 5.0))
self.poll_interval = float(api_config.get('poll_interval', 60))

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the base_url default with the schema default.

config_schema.json declares birdnet_api.base_url default http://birdnet-go.local:8080, but this code falls back to ''. If the host does not merge schema defaults into the config it passes to the plugin, an omitted base_url disables all REST polling instead of using the documented default. Pick one source of truth: either use the schema default here, or change the schema default to "".

As per coding guidelines: "Configuration defaults declared in config_schema.json must match the defaults used by the plugin's config.get(key, default) calls."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/birdnet-go/manager.py` around lines 85 - 87, Align the `base_url`
fallback in the manager initialization with the `birdnet_api.base_url` default
declared in `config_schema.json`; update the `api_config.get` default used for
`self.api_base_url` so omitted configuration preserves the documented REST
endpoint, while leaving URL normalization unchanged.

Source: Coding guidelines

Comment thread plugins/birdnet-go/manager.py
Comment thread plugins/birdnet-go/manager.py
`_connect_mqtt` overwrote `self.mqtt_client` without stopping the old
one. `_on_mqtt_disconnect` only flips a flag, so every broker drop leaked
a paho network thread and socket, and the abandoned client kept its own
reconnect loop — duplicate subscriptions could double-deliver a
detection, and `on_disable` could only ever stop the newest client. Tear
the previous client down before reconnecting.

`_species_img_cache` had no eviction and only cleared in `cleanup()`, so
a yard that keeps turning up new species grew without a ceiling.
`_render_detection` also ran a LANCZOS resample of a full-resolution
photo on every frame. Both caches are bounded now, and the panel-sized
frame is cached per (species, width, height) — keyed by size because the
core can hand a plugin a smaller logical screen mid-run.

`update()` could block for minutes: two polls plus three image fetches,
each allowed up to the 30s timeout the schema permits. Image warm-up now
stops at a deadline and picks up the rest on the next tick.

`_poll_daily_stats` bailed unless the response was a bare list, while
`_poll_latest_detection` also accepted `{"data": [...]}`. If the
analytics endpoint ever returns that envelope, the stats screen and the
species cycle would both stay empty with nothing logged. Unwrap it too.

`birdnet_api.base_url` defaulted to the schema's placeholder host but to
`''` in code. Aligned on `''` and marked the key required: a self-hosted
service has no useful default, and a placeholder host that doesn't
resolve just produces repeated connection warnings.

Raised the `requests` and `Pillow` floors past the CVEs Codacy flagged,
matching what the rest of the repo already pins, and replaced five
`except: pass` blocks with debug logging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@ChuckBuilds
ChuckBuilds merged commit fb8459d into main Aug 5, 2026
4 checks passed
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.

2 participants