feat(birdnet-go): REST-first, a stats screen, and one distinct species per slot (1.2.0) - #253
Conversation
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
|
Warning Review limit reached
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughAdds a BirdNET-Go plugin with REST polling, optional MQTT updates, detection and statistics displays, image caching, configurable rendering, validation, lifecycle management, and catalog metadata. ChangesBirdNET-Go plugin
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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 313 |
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.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
plugins/birdnet-go/manager.py (2)
904-920: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider 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 usesapi_timeout, which the schema allows up to 30 seconds. If the BirdNET-Go host accepts connections but stalls, a singleupdate()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 winBound
_species_img_cacheand cache the resized frame.Two related costs come from storing full-resolution source images here:
_species_img_cachehas no eviction. It only clears incleanup(). The set of distinct species grows over days of continuous operation, and every decoded source image stays resident. Memory grows without a ceiling._render_detectioncalls_resize_imageon 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
📒 Files selected for processing (7)
plugins.jsonplugins/birdnet-go/LICENSEplugins/birdnet-go/README.mdplugins/birdnet-go/config_schema.jsonplugins/birdnet-go/manager.pyplugins/birdnet-go/manifest.jsonplugins/birdnet-go/requirements.txt
| 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)) |
There was a problem hiding this comment.
📐 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
`_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
Brings the BirdNET-Go plugin onto current
mainand makes it work against a real feed. Supersedesfeat/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 withmax_age, but the signature isset(key, data, ttl=None). Both calls raisedTypeErrorinside atry/exceptthat 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, andcompatibility.declared_min_versionreadsversions[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.enabledfor 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:
Now 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 instead.unique_species: falserestores the old behaviour. Interrupts always show the bird that just called, never the cycle's current card.The cycle is built from
/analytics/species/dailyrather than the detection stream, because/detections/recentcaps at ten rows regardless ofnumResults— 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 of97.).Verification
recentandfrequencyordering.check_manifest_version_fields.pyandcheck_module_collisions.pyboth pass.Reviewer notes
plugins.jsonwas regenerated by the pre-commit hook, not hand-edited.mqtt.enabled,birdnet_api.poll_interval, thestatsblock,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
Documentation